Files
connectai/tests/hangulHygiene.test.ts
koriweb 1208050557 fix(output): 한·영 깨진 토큰("덩ey") 결정론 감지 + 1회 수리 패스 (v2.2.230)
소형 로컬 모델이 한국어 단어 중간에 영문 토큰을 섞는 디코딩 사고
("덩어리"→"덩ey", "결과적으로"→"결ently"). 프롬프트 출력 위생 규칙으로는
못 막음 — 지시 불이행이 아니라 토큰 붕괴라서. 사후 보정으로 해결:

- hangulHygiene.ts: 고정밀 감지 패턴(한글 음절+영문 소문자 2+ 연속) —
  "API를"/"Code의"(영문+조사)·"플랜B"(한글+대문자)는 정상 표기로 미감지,
  코드 블록 제외. 감지 시 LLM 수리 1회 (깨진 토큰만 복원, 내용 변경 금지).
- 수리 검증 게이트: 길이 ±35% 이내 + 깨진 토큰 감소 — 미통과 시 원문 유지
  (수리가 더 망치는 것 방지). 실패 전 과정 로그 (관측성 원칙).
- 적용 경로: 채팅 답변(스트림 후·확정 전) + /wikify 산출물(영구 자산이라
  더 중요 — "🩹 표기 오류 N건 교정" 표시).

테스트 11건 (감지 정밀도·검증 게이트·실패 안전).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:50:26 +09:00

55 lines
2.5 KiB
TypeScript

/**
* 한·영 깨진 토큰 감지·수리 — 순수 로직 테스트.
*/
import { findBrokenHangulTokens, repairBrokenHangul } from '../src/agent/hangulHygiene';
describe('findBrokenHangulTokens', () => {
test('깨진 토큰 감지: 한글+영문소문자 연속', () => {
const t = '문서를 단순히 텍스트 덩ey로 두지 말고, 결ently 헤더를 강제해야 합니다.';
const broken = findBrokenHangulTokens(t);
expect(broken.some(b => b.includes('덩ey'))).toBe(true);
expect(broken.some(b => b.includes('결ently'))).toBe(true);
});
test.each([
'API를 호출하고 Code의 구조를 본다', // 영문 단어 + 한글 조사 = 정상
'VS Code랑 LM Studio에서 모델을 로드한다',
'플랜B로 진행하고 옵션A를 검토한다', // 한글+대문자 1글자 = 정상
'RAG 파이프라인과 recall@1 지표',
])('정상 표기는 미감지: %s', (t) => {
expect(findBrokenHangulTokens(t)).toHaveLength(0);
});
test('코드 블록 안은 제외', () => {
const t = '설명\n```js\nconst 덩ey = 1;\n```\n그리고 `값ab` 도 변수다';
expect(findBrokenHangulTokens(t)).toHaveLength(0);
});
});
describe('repairBrokenHangul — 검증 게이트', () => {
const broken = ['덩ey로'];
const text = '텍스트 덩ey로 두지 말 것. '.repeat(3);
test('수리 성공 (깨진 토큰 감소 + 길이 유지) → 채택', async () => {
const fixed = text.replace(/덩ey로/g, '덩어리로');
const r = await repairBrokenHangul(text, broken, async () => fixed);
expect(r).toBe(fixed.trim());
});
test('수리 결과가 너무 짧으면 기각 (내용 유실 방지)', async () => {
const r = await repairBrokenHangul(text, broken, async () => '덩어리.');
expect(r).toBeNull();
});
test('깨진 토큰이 줄지 않으면 기각', async () => {
const r = await repairBrokenHangul(text, broken, async () => text);
expect(r).toBeNull();
});
test('LLM 실패 시 null (원문 유지)', async () => {
const r = await repairBrokenHangul(text, broken, async () => { throw new Error('down'); });
expect(r).toBeNull();
});
test('깨진 토큰 없으면 호출 자체를 안 함', async () => {
let called = false;
const r = await repairBrokenHangul(text, [], async () => { called = true; return text; });
expect(r).toBeNull();
expect(called).toBe(false);
});
});