diff --git a/src/gateway/guards/prompt-gates.ts b/src/gateway/guards/prompt-gates.ts index 10cb9ee..6acf5d2 100644 --- a/src/gateway/guards/prompt-gates.ts +++ b/src/gateway/guards/prompt-gates.ts @@ -65,7 +65,11 @@ export function isNewsRequest(message: string): boolean { export function isLiveDataRequest(message: string): boolean { const m = String(message || ''); return isNewsRequest(m) + // 태풍/지진 등 재해 현황이 빠져 있었다. 2026-08-09 "지금 태풍 돌핀 어디 있지?"가 이 게이트를 + // 그냥 통과해 검색 없이 답변됐고(로그에 TOOL 호출 0건), 모델이 일주일 전 대화에서 쓴 것과 + // 똑같은 좌표·기압·풍속을 그대로 다시 뱉었다. 날씨만큼이나 시시각각 변하는 값들이다. || /(날씨|기온|예보|미세먼지|황사|환율|주가|금리|시세|장마)/.test(m) + || /(태풍|호우|폭우|폭설|한파|폭염|지진|해일|쓰나미|산불|홍수|가뭄|열대저압부)/.test(m) || /\b(weather|forecast|stock price|exchange rate)\b/i.test(m); } @@ -109,6 +113,13 @@ export function isFactualInfoRequest(message: string): boolean { // is not (discounts, humidity, battery %) so it joins the ambiguous tier gated by // SPEC_CONTEXT_KEYWORD like GHz/원/달러 already are. const STRICT_SPEC_UNITS = /\d[\d,.]*\s*(GB|TB|MB|tok\/s|tps|TFLOPS?|GFLOPS?|토큰(\s*\/\s*초)?)/i; +// Geophysical measurements: as spec-like as GB/TFLOPS, and unlike them they change hour to hour, +// so a remembered value is always wrong. 2026-08-09 a typhoon answer stated "중심기압 905hPa, +// 최대풍속 58m/s, 북위 18.5°, 동경 150.0°" with no tool call — every unit here was absent from +// both tiers, so the output guard saw nothing to verify. +const MEASUREMENT_UNITS = /\d[\d,.]*\s*(hPa|헥토파스칼|m\/s|㎧|미터\s*퍼\s*세크|kt|노트|km\/h|mm|㎜|cm|㎝|℃|°C|도씨)/i + ; +const COORDINATE_CLAIM = /(북위|남위|동경|서경)\s*\d|\d+(\.\d+)?\s*°\s*[NSEW]\b/i; const AMBIGUOUS_SPEC_UNITS = /\d[\d,.]*\s*(GHz|MHz|watts?|원|달러|USD|%|\$)/i; // 2026-07-29 (2): found by the regression test added the same day. The percentage tier above // only fires when SPEC_CONTEXT_KEYWORD also matches, and a throughput claim phrased without any @@ -124,6 +135,8 @@ export function looksLikeUnverifiedSpecClaim(content: string): boolean { if (!text) return false; const withoutCode = text.replace(/```[\s\S]*?```/g, ''); return STRICT_SPEC_UNITS.test(withoutCode) + || MEASUREMENT_UNITS.test(withoutCode) + || COORDINATE_CLAIM.test(withoutCode) || (AMBIGUOUS_SPEC_UNITS.test(withoutCode) && SPEC_CONTEXT_KEYWORD.test(withoutCode)) || (/초당\s*(약\s*)?\d/.test(withoutCode) && /(토큰|tok|GPU|추론|생성\s*속도|처리량)/i.test(withoutCode)) || /(것으로\s*예상|추정됩니다|추정치|짐작)/.test(withoutCode); diff --git a/tests/prompt-gates.test.ts b/tests/prompt-gates.test.ts index b970d2b..aedb038 100644 --- a/tests/prompt-gates.test.ts +++ b/tests/prompt-gates.test.ts @@ -37,6 +37,21 @@ describe('looksLikeUnverifiedSpecClaim — 출력에서 검증 안 된 수치 assert.equal(looksLikeUnverifiedSpecClaim('약 15 TFLOPS 수준입니다.'), true); }); + // [2026-08-09] 실제 로그(12065행)의 태풍 답변. 기압/풍속/위경도는 어느 단위 목록에도 없어서 + // 출력측 가드가 검증할 대상 자체를 못 봤다. GB/TFLOPS만큼이나 스펙적인 수치이고, 그것들과 + // 달리 시시각각 변하므로 기억해서 쓴 값은 언제나 틀리다. + test('[2026-08-09] 기압·풍속·위경도 같은 관측 수치도 걸린다', () => { + assert.equal( + looksLikeUnverifiedSpecClaim( + '현재 태풍 돌핀의 위치는 북위 18.5°, 동경 150.0° 부근이며 중심기압 905hPa, 최대풍속 58m/s입니다.', + ), + true, + ); + assert.equal(looksLikeUnverifiedSpecClaim('중심기압 905hPa를 유지하고 있습니다.'), true); + assert.equal(looksLikeUnverifiedSpecClaim('최대풍속 58m/s입니다.'), true); + assert.equal(looksLikeUnverifiedSpecClaim('북위 18.5° 부근에 있습니다.'), true); + }); + test('[2026-07-29] 단위 없는 "N토큰"도 걸린다 — /초 접미사 없는 실제 사례', () => { assert.equal( looksLikeUnverifiedSpecClaim( @@ -161,6 +176,16 @@ describe('isNewsRequest / isLiveDataRequest', () => { assert.equal(isLiveDataRequest('환율 알려줘'), true); assert.equal(isLiveDataRequest('파이썬 리스트 정렬법'), false); }); + + // [2026-08-09] 실제 로그(12065행): "지금 태풍 돌핀 어디 있지?"가 이 게이트를 통과해 + // 검색 없이 답변됐고, 모델이 일주일 전(9119행, 8월 2일) 대화와 똑같은 좌표·기압·풍속을 + // 그대로 다시 뱉었다. 재해 현황은 날씨만큼이나 시시각각 변한다. + test('태풍 등 재해 현황도 라이브 데이터다', () => { + assert.equal(isLiveDataRequest('지금 태풍 돌핀 어디 있지?'), true); + assert.equal(isLiveDataRequest('태풍 돌핀 세력이 어느 정도야?'), true); + assert.equal(isLiveDataRequest('방금 지진 났어?'), true); + assert.equal(isLiveDataRequest('내일 호우 예보 있나'), true); + }); }); describe('isUsableGroundingResult — 빈 검색결과 판별', () => { diff --git a/tests/retry-decisions.test.ts b/tests/retry-decisions.test.ts index 7abcbfd..69a8631 100644 --- a/tests/retry-decisions.test.ts +++ b/tests/retry-decisions.test.ts @@ -56,6 +56,36 @@ describe('AUTO-RECOVER — 근거 없이 답한 경우 재시도', () => { const d = recover('이 사이트 좀 찾아줘', "I'm sorry, I can't help with that."); assert.equal(d.shouldRetry, true); }); + + // [2026-08-09] 실제 사고: "지금 태풍 돌핀 어디 있지?"에 도구를 하나도 안 부르고 + // 좌표·기압·풍속을 단언했다(로그 12065행). 같은 수치가 일주일 전 답변(9119행)과 + // 글자까지 동일해서, 검색이 아니라 대화 기억에서 나온 값임이 드러났다. + test('[2026-08-09] 태풍 위치를 검색 없이 답하면 재시도', () => { + const d = recover( + '지금 태풍 돌핀 어디 있지?', + '현재 태풍 돌핀의 위치는 북위 18.5°, 동경 150.0° 부근이며 중심기압 905hPa, 최대풍속 58m/s입니다.', + ); + assert.equal(d.shouldRetry, true); + assert.equal(d.kind, 'search'); + }); + + // 반대 방향이 더 중요하다: 관측 단위(℃/mm/m/s)를 가드에 추가했으므로, 도구로 받아온 + // 정상 예보까지 되던지면 날씨 기능 자체가 무한 재검색에 빠진다. + test('[2026-08-09] 날씨 도구로 받은 기온·강수량 답변은 되던지지 않는다', () => { + assert.equal( + recover('런던 오늘 최고기온?', '런던의 오늘 최고기온은 31.5°C이고 강수량은 2mm 예상됩니다.', ['weather_openmeteo']) + .shouldRetry, + false, + ); + }); + + test('[2026-08-09] 검색 후의 태풍 답변도 되던지지 않는다', () => { + assert.equal( + recover('지금 태풍 돌핀 어디 있지?', '태풍 돌핀은 북위 18.5°, 동경 150.0° 부근에 있습니다.', ['web_search']) + .shouldRetry, + false, + ); + }); }); describe('AUTO-RECOVER — 어떤 도구를 요구할지 (kind)', () => {