fix: 재해 현황·관측 수치가 할루시네이션 가드를 그냥 통과하던 문제
"지금 태풍 돌핀 어디 있지?"에 도구를 하나도 부르지 않고 좌표·기압·풍속을 단언했다(로그 12065행, TOOL 호출 0건). 같은 수치가 일주일 전 답변(9119행, 8월 2일)과 글자까지 동일해서, 검색이 아니라 대화 기억에서 나온 값임이 드러났다. 실제로는 돌핀이 이미 오키나와를 통과한 뒤였다. 가드 세 개가 전부 통과시켰다: isLiveDataRequest false isFactualInfoRequest false looksLikeUnverifiedSpec false - 입력측: 라이브 데이터 패턴이 날씨/기온/환율/주가뿐이라 태풍이 없었다. 태풍·호우·폭우·폭설·한파·폭염·지진·해일·산불·홍수·가뭄·열대저압부 추가. - 출력측: 스펙 단위 목록이 GB/TFLOPS/원/% 계열뿐이라 hPa·m/s·위경도가 어디에도 없었다. 검증할 대상 자체를 못 본 것. 관측 단위(hPa/m/s/kt/km/h/mm/cm/℃)와 좌표(북위·동경 N.N°)를 추가했다. 관측 단위를 넣으면 정상 날씨 답변까지 되던질 위험이 있는데, 재검색 조건에 !hasGroundingToolCall이 있고 weather_가 grounding으로 인정되므로 도구로 받아온 31.5°C·2mm는 영향받지 않는다. 그 반대 방향을 테스트로 못박았다 — 이쪽이 깨지면 날씨 기능이 무한 재검색에 빠진다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,11 @@ export function isNewsRequest(message: string): boolean {
|
|||||||
export function isLiveDataRequest(message: string): boolean {
|
export function isLiveDataRequest(message: string): boolean {
|
||||||
const m = String(message || '');
|
const m = String(message || '');
|
||||||
return isNewsRequest(m)
|
return isNewsRequest(m)
|
||||||
|
// 태풍/지진 등 재해 현황이 빠져 있었다. 2026-08-09 "지금 태풍 돌핀 어디 있지?"가 이 게이트를
|
||||||
|
// 그냥 통과해 검색 없이 답변됐고(로그에 TOOL 호출 0건), 모델이 일주일 전 대화에서 쓴 것과
|
||||||
|
// 똑같은 좌표·기압·풍속을 그대로 다시 뱉었다. 날씨만큼이나 시시각각 변하는 값들이다.
|
||||||
|| /(날씨|기온|예보|미세먼지|황사|환율|주가|금리|시세|장마)/.test(m)
|
|| /(날씨|기온|예보|미세먼지|황사|환율|주가|금리|시세|장마)/.test(m)
|
||||||
|
|| /(태풍|호우|폭우|폭설|한파|폭염|지진|해일|쓰나미|산불|홍수|가뭄|열대저압부)/.test(m)
|
||||||
|| /\b(weather|forecast|stock price|exchange rate)\b/i.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
|
// is not (discounts, humidity, battery %) so it joins the ambiguous tier gated by
|
||||||
// SPEC_CONTEXT_KEYWORD like GHz/원/달러 already are.
|
// 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;
|
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;
|
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
|
// 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
|
// 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;
|
if (!text) return false;
|
||||||
const withoutCode = text.replace(/```[\s\S]*?```/g, '');
|
const withoutCode = text.replace(/```[\s\S]*?```/g, '');
|
||||||
return STRICT_SPEC_UNITS.test(withoutCode)
|
return STRICT_SPEC_UNITS.test(withoutCode)
|
||||||
|
|| MEASUREMENT_UNITS.test(withoutCode)
|
||||||
|
|| COORDINATE_CLAIM.test(withoutCode)
|
||||||
|| (AMBIGUOUS_SPEC_UNITS.test(withoutCode) && SPEC_CONTEXT_KEYWORD.test(withoutCode))
|
|| (AMBIGUOUS_SPEC_UNITS.test(withoutCode) && SPEC_CONTEXT_KEYWORD.test(withoutCode))
|
||||||
|| (/초당\s*(약\s*)?\d/.test(withoutCode) && /(토큰|tok|GPU|추론|생성\s*속도|처리량)/i.test(withoutCode))
|
|| (/초당\s*(약\s*)?\d/.test(withoutCode) && /(토큰|tok|GPU|추론|생성\s*속도|처리량)/i.test(withoutCode))
|
||||||
|| /(것으로\s*예상|추정됩니다|추정치|짐작)/.test(withoutCode);
|
|| /(것으로\s*예상|추정됩니다|추정치|짐작)/.test(withoutCode);
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ describe('looksLikeUnverifiedSpecClaim — 출력에서 검증 안 된 수치
|
|||||||
assert.equal(looksLikeUnverifiedSpecClaim('약 15 TFLOPS 수준입니다.'), true);
|
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토큰"도 걸린다 — /초 접미사 없는 실제 사례', () => {
|
test('[2026-07-29] 단위 없는 "N토큰"도 걸린다 — /초 접미사 없는 실제 사례', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
looksLikeUnverifiedSpecClaim(
|
looksLikeUnverifiedSpecClaim(
|
||||||
@@ -161,6 +176,16 @@ describe('isNewsRequest / isLiveDataRequest', () => {
|
|||||||
assert.equal(isLiveDataRequest('환율 알려줘'), true);
|
assert.equal(isLiveDataRequest('환율 알려줘'), true);
|
||||||
assert.equal(isLiveDataRequest('파이썬 리스트 정렬법'), false);
|
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 — 빈 검색결과 판별', () => {
|
describe('isUsableGroundingResult — 빈 검색결과 판별', () => {
|
||||||
|
|||||||
@@ -56,6 +56,36 @@ describe('AUTO-RECOVER — 근거 없이 답한 경우 재시도', () => {
|
|||||||
const d = recover('이 사이트 좀 찾아줘', "I'm sorry, I can't help with that.");
|
const d = recover('이 사이트 좀 찾아줘', "I'm sorry, I can't help with that.");
|
||||||
assert.equal(d.shouldRetry, true);
|
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)', () => {
|
describe('AUTO-RECOVER — 어떤 도구를 요구할지 (kind)', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user