fix: 검증 강제 가드의 오탐 3종 — 사용자가 준 숫자, NHC/EONET, 붙여넣기 본문

실사용 로그와 세션 104건을 가드에 직접 돌려서 찾은 오탐들이다.

1) 사용자가 질문에 직접 적은 숫자로 AUTO-RECOVER가 걸렸다.

   로그 실례: "북위 27.7도, 동경 124.8도 여기가 어디쯤이지?" — 그 좌표 자체가
   한 턴 전 web_search 결과였고 사용자가 그대로 옮겨 물은 것이다. 모델이
   정답("동중국해")을 냈는데 답에 좌표를 되풀이했다는 이유로 COORDINATE_CLAIM이
   걸려 재생성 + "latitude 27.7 longitude 124.8 location" 검색을 태웠고,
   돌아온 건 "농림통계연보(2005).hwp"였다. 그러고서 같은 답을 다시 썼다.

   이 가드가 잡으려는 건 "모델이 지어낸 숫자"인데, 사용자가 적어 넣은 숫자는
   정의상 모델이 만든 값이 아니다. looksLikeUnverifiedSpecClaim에 userMessage를
   받아 스캔 전에 지운다. 모델이 새로 덧붙인 수치는 그대로 남는다. 한 자리 수는
   지우지 않는다 — 메시지의 "2" 하나로 답변의 모든 "2"가 지워지면 가드가 통째로
   무력화된다(회귀 테스트 포함).

2) nhc_active_storms/eonet_events가 GROUNDING_TOOL_PATTERN에 없었다.

   둘 다 권위 있는 원본(미 국립허리케인센터, NASA EONET) 직결이고 태풍 질문이
   실제로 도는 도구인데, 08-09에 추가한 MEASUREMENT_UNITS/COORDINATE_CLAIM이
   바로 그 도구들이 돌려주는 값(hPa·m/s·북위)에 반응한다. 그래서 NHC 데이터로만
   답한 턴이 "근거 도구 미호출"로 분류돼, 방금 읽은 권위 있는 출처를 의심하라고
   web_search 재시도를 걸었다. memory_stats 때와 같은 구멍이다.

3) 긴 붙여넣기 문서 본문의 우연한 키워드로 강제검색이 걸렸다.

   7,583자짜리 고소장 작성 요청이 본문 한가운데(2578자 지점)의 "시세"(장물
   시세) 때문에 isLiveDataRequest에 걸렸다. 문서를 써 달라는 턴에 "메모리에서
   답하지 말고 검색부터 하라"는 리마인더가 붙는 건 토큰 낭비를 넘어 작업 방향을
   반대로 민다.

   처음엔 "길다 + isExecutionLikeRequest"로 막으려 했는데 회귀 테스트가 그게
   틀렸음을 잡아냈다 — 그 메시지가 실행형으로 분류된 근거가 본문의 "화면" 한
   단어였다. 막으려는 문제와 똑같은 우연 위에 얹은 수정이었다. 대신 위치를
   본다: 사람은 지시를 자료의 앞이나 뒤에 쓰지 한가운데 묻지 않는다.
   600자 초과 메시지는 앞 400자 + 뒤 300자만 스캔한다(instructionZone).
   검증 강제 게이트끼리 예외가 어긋나면 안 되므로 isFactualInfoRequest에도
   같이 적용했다.

실사용 104건 대조: isLiveDataRequest 오탐 3건 제거하고 정당한 25건은 전부
유지, 출력측 가드는 로그의 그 좌표 케이스가 정확히 하나 걸러졌다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-08-10 12:40:50 +09:00
co-authored by Claude Opus 5
parent fe69cc0126
commit 305f93d08b
4 changed files with 153 additions and 6 deletions
+39
View File
@@ -236,6 +236,45 @@ describe('isNewsRequest / isLiveDataRequest', () => {
assert.equal(isLiveDataRequest('지서버 최근에 왜 이렇게 느려'), false);
assert.equal(isLiveDataRequest('현재 시어엔진 상태 어때?'), false);
});
// [2026-08-10] 실사용 104건 대조에서 나온 유일한 오탐: 7,583자짜리 고소장 작성 요청이 본문
// 한가운데(2578자 지점)의 "시세"(장물 시세) 때문에 걸렸다. 매칭된 단어가 사용자의 '요청'이
// 아니라 붙여넣은 '자료' 쪽에 있었던 것 — 문서를 써 달라는 턴에 "검색부터 하라"는 리마인더가
// 붙으면 토큰 낭비를 넘어 작업 방향을 반대로 민다.
test('긴 붙여넣기 문서 한가운데의 우연한 키워드로는 라이브 데이터가 되지 않는다', () => {
const 고소장 = '다음 사건 정보를 바탕으로 고소장을 작성해주세요.\n\n[사건정보]\n'
+ '피해자 진술을 정리한 내용입니다. 관련 경위를 시간순으로 적었습니다.\n'.repeat(12)
+ '피해품의 시세는 최근 거래가 기준으로 산정하였음.\n'
+ '진술을 계속 정리한 내용입니다. 참고자료를 덧붙였습니다.\n'.repeat(12)
+ '\n위 내용으로 고소장을 작성해 주세요.';
assert.ok(고소장.length > 600);
assert.equal(isLiveDataRequest(고소장), false);
});
// 앞선 시도는 "길다 + 실행형"으로 막으려 했는데, 그 메시지가 실행형으로 분류된 근거가 본문의
// "화면" 한 단어였다 — 막으려는 문제와 똑같은 우연 위에 얹은 수정이라 이 케이스가 그걸 잡는다.
test('실행형 키워드가 없는 긴 문서에도 적용된다', () => {
const 자료 = '아래 회의록을 정리해 주세요.\n'
+ '참석자들이 논의한 사항을 순서대로 기록하였습니다.\n'.repeat(20)
+ '환율 관련 언급이 한 차례 있었습니다.\n'
+ '이후 논의는 다음 안건으로 넘어갔습니다.\n'.repeat(20);
assert.ok(자료.length > 600);
assert.equal(isLiveDataRequest(자료), false);
});
test('장문이어도 지시부에 시점표현이 있으면 라이브 데이터다', () => {
// 길이만으로 끄면 안 된다 — 앞뒤(지시부)에 있으면 그대로 걸려야 한다.
const 장문질문 = '지금 한반도 주변에 발달한 태풍이 있는지 궁금합니다. '
+ '지난주에 뉴스에서 얼핏 본 기억이 있어서 배경을 좀 적어봅니다. '.repeat(20);
assert.ok(장문질문.length > 600);
assert.equal(isLiveDataRequest(장문질문), true);
const 끝에지시 = '아래는 제가 정리해둔 자료입니다.\n'
+ '작년에 다녀온 여행 기록을 옮겨 적었습니다.\n'.repeat(25)
+ '\n그건 그렇고 지금 서울 날씨 어때?';
assert.ok(끝에지시.length > 600);
assert.equal(isLiveDataRequest(끝에지시), true);
});
});
describe('isUsableGroundingResult — 빈 검색결과 판별', () => {