Files
homeclaw/tests/direct-price-answer.test.ts
T
kimandClaude Opus 5 b2075c62d8 fix: buildDirectPriceAnswer가 무관한 달러 값을 "Answer:"로 합성해 붙이던 문제
이 함수가 만드는 줄은 검색 결과 stdout 맨 앞에 붙는다. 시스템 프롬프트의 ANTI-HALLUCINATION은
"도구가 돌려준 것을 정확히 보고하라, 절대 무시하지 말라"고 지시하므로, 여기서 틀리면 모델에게
틀린 값을 신뢰하라고 시키는 셈이다. 게다가 합성된 숫자는 도구 출력 텍스트의 일부가 되므로,
모델이 그대로 옮기면 NUMERIC-GROUNDING이 "소스에 있다"며 통과시킨다 — 우리가 만든 숫자가
'근거 있음' 도장을 받아 나간다.

실제 사고(프로덕션 로그):
  USER: 컨테이너선 급유비가 얼마나 될까?
  TOOL: web_search("average fuel cost for 20,000 TEU container ship per voyage")
  TOOL OK: Answer: The current price is approximately $38.00 USD.
$38은 스니펫에 있던 "신조선 7일 항해 시 TEU 1개당 추가 연료비"였고, 실제 답은 항해당 수백만
달러다. 그 턴은 모델이 결과 [1]을 직접 읽어 스스로 바로잡았지만 그건 운이다.

* isPriceQuery에 단어 경계가 없어 통화·가격 낱말이 다른 단어 **안에서** 걸렸다. "eur"가
  "Europe"·"Neuralink"에, "cost"가 "costume"에, "value"가 "valuable"에 들어맞는다. 실사용
  고유 검색어 195건 중 19건이 통과했고 그중 8건이 가격과 무관했다("Europe drought status
  August 2026", "Neuralink Blindsight resolution pixels" 등). 경계를 넣어 19 → 11건.

* generic 자산 경로를 제거했다. 금·은·비트코인은 타당성 범위(온스당 300~10,000 등)와 자산명
  대조가 성립해 판정이 의미가 있다. generic은 둘 다 없다 — 허용 범위가 $0.5~$5,000,000이라
  사실상 모든 달러 표기를 받고, 문구도 무엇의 가격인지 말하지 못한 채 "The current price"라고만
  쓴다. 이 로그 표본에서 generic 경로가 실제로 답을 낸 유일한 사례가 위 컨테이너선 건이다.

* 자산명 대조를 가점(+2)에서 **필수 조건**으로 올렸다. 가점일 때는 자산을 한 번도 언급하지
  않은 스니펫의 숫자가 score 0으로 통과했다 — "금 시세" 질문에 다른 상품 가격이 답으로 나갈
  수 있다는 뜻이다. 티커(XAU/XAG/BTC)도 함께 본다.

순효과(실사용 고유 검색어 195건): 발동 19 → 11건, 그중 실제로 "Answer:" 줄이 생길 수 있는
것 0건. 이 표본의 가격 질문은 전부 GPU·옵션·펌프 같은 generic 대상이라, 원래도 이 기능이
맞는 답을 낸 적이 없다.

테스트 544개 통과(+8).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:06:31 +09:00

84 lines
3.9 KiB
TypeScript

/**
* buildDirectPriceAnswer — 검색 결과 맨 앞에 붙는 합성 "Answer:" 줄.
*
* 이 줄이 왜 위험한지: 시스템 프롬프트의 ANTI-HALLUCINATION은 "도구가 돌려준 것을 정확히
* 보고하라, 절대 무시하지 말라"고 지시한다. 여기서 틀리면 모델에게 틀린 값을 신뢰하라고
* 시키는 셈이다. 게다가 합성된 숫자는 도구 출력 텍스트의 일부라, 모델이 그대로 옮기면
* NUMERIC-GROUNDING이 "소스에 있다"며 통과시킨다 — 우리가 만든 숫자가 근거 도장을 받는다.
*
* 실제 사고(프로덕션 로그, 2026-08):
* USER: 컨테이너선 급유비가 얼마나 될까?
* TOOL: web_search("average fuel cost for 20,000 TEU container ship per voyage")
* TOOL OK: Answer: The current price is approximately $38.00 USD.
* $38은 "신조선 7일 항해 시 TEU 1개당 추가 연료비"였고, 실제 답은 항해당 수백만 달러다.
*/
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { buildDirectPriceAnswer, isPriceQuery } from '../src/tools/web';
describe('isPriceQuery — 통화·가격 낱말이 다른 단어 안에서 걸리면 안 된다', () => {
test('단어 경계가 없어 오탐하던 실사용 검색어들', () => {
// 실사용 고유 검색어 195건 중 19건이 통과했고, 아래는 그중 가격과 무관한 것들이다.
for (const q of [
'Europe drought status August 2026', // Eur(ope)
'Neuralink Blindsight resolution pixels', // N(eur)alink
'European weather outlook August 2026 precipitation',
'costume design ideas', // (cost)ume
'valuable lesson', // (value)able
]) {
assert.equal(isPriceQuery(q), false, q);
}
});
test('진짜 가격 질문은 계속 잡는다', () => {
for (const q of ['gold price today', 'bitcoin usd quote', 'RTX 4090 current price', 'average fuel cost for container ship']) {
assert.equal(isPriceQuery(q), true, q);
}
});
});
describe('buildDirectPriceAnswer — 무엇의 가격인지 모르면 답을 만들지 않는다', () => {
const R = (title: string, snippet: string) => ({ title, url: 'https://example.invalid/x', snippet });
test('generic 자산에는 더 이상 "Answer:" 줄을 만들지 않는다 — 컨테이너선 사고의 직접 수정', () => {
const out = buildDirectPriceAnswer(
'average fuel cost for 20,000 TEU container ship per voyage',
[R('Oil Prices and Container Shipping Costs', 'For a new ship, a 7-day voyage adds about $38 per TEU in fuel.')],
);
assert.equal(out, '');
});
test('금 시세는 계속 만든다', () => {
const out = buildDirectPriceAnswer(
'gold price today',
[R('Gold Price Today', 'Spot gold is currently trading at $2,410.50 per ounce.')],
);
assert.match(out, /Answer: The current gold price is approximately \$2,410\.50 USD per ounce\./);
});
test('자산을 언급하지 않은 스니펫의 숫자는 쓰지 않는다 — 가점이 아니라 필수 조건', () => {
const out = buildDirectPriceAnswer(
'gold price today',
[R('Weekly commodity roundup', 'Copper settled at $4,200.00 per tonne on light volume.')],
);
assert.equal(out, '');
});
test('과거 시세 단서가 있으면 배제한다', () => {
const out = buildDirectPriceAnswer(
'gold price today',
[R('Gold in 1980', 'Gold was worth $850.00 per ounce back in 1980, a historical peak.')],
);
assert.equal(out, '');
});
test('가격 질문이 아니면 아무것도 만들지 않는다', () => {
assert.equal(buildDirectPriceAnswer('Europe drought status', [R('Drought', 'Damage reached $38 billion.')]), '');
});
test('결과가 없어도 터지지 않는다', () => {
assert.equal(buildDirectPriceAnswer('gold price today', []), '');
});
});