1208050557
소형 로컬 모델이 한국어 단어 중간에 영문 토큰을 섞는 디코딩 사고
("덩어리"→"덩ey", "결과적으로"→"결ently"). 프롬프트 출력 위생 규칙으로는
못 막음 — 지시 불이행이 아니라 토큰 붕괴라서. 사후 보정으로 해결:
- hangulHygiene.ts: 고정밀 감지 패턴(한글 음절+영문 소문자 2+ 연속) —
"API를"/"Code의"(영문+조사)·"플랜B"(한글+대문자)는 정상 표기로 미감지,
코드 블록 제외. 감지 시 LLM 수리 1회 (깨진 토큰만 복원, 내용 변경 금지).
- 수리 검증 게이트: 길이 ±35% 이내 + 깨진 토큰 감소 — 미통과 시 원문 유지
(수리가 더 망치는 것 방지). 실패 전 과정 로그 (관측성 원칙).
- 적용 경로: 채팅 답변(스트림 후·확정 전) + /wikify 산출물(영구 자산이라
더 중요 — "🩹 표기 오류 N건 교정" 표시).
테스트 11건 (감지 정밀도·검증 게이트·실패 안전).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
2.5 KiB
TypeScript
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);
|
|
});
|
|
});
|