RTX 4080 vs 5060 비교 요청에서 모델이 답을 못 주는 이유로
"RTX 5060의 미출시 상태"를 단정했다. 실제로는 이미 출시돼 사용자
지서버에 꽂혀 있는 카드다.
기존 가드 셋이 전부 통과시킨 이유:
- AUTO-RECOVER: 검색을 하긴 했다 (도구 호출 있음)
- EMPTY-GROUNDING: 결과가 비어있지 않았다
- NUMERIC-GROUNDING: 답변에 수치가 0건이라 검증할 주장 자체가 없었다
세 가드 모두 숫자를 전제로 하는데, 이 답변의 유일한 거짓말에는
단위가 없었다.
부재 주장은 별도로 검증할 값어치가 있다. 모델은 "학습 데이터 시점
기준으로 출시 안 됨"만 알 수 있을 뿐 출시 여부 자체를 알 수 없다
(5060은 이 모델 컷오프 4개월 뒤 출시). 기억에 없는 것을 세상에 없는
것으로 바꿔 말하는 건 사실 판단이 아니라 범주 오류다. 검증 방법도
날짜 검증과 같다 — 출처가 정말 미출시라고 하면 그 표현이 출처 텍스트에
있고, 없으면 모델이 지어낸 것이다.
- checkNumericGrounding에 nonexistenceClaim/nonexistenceUnsupported 추가
- 출처 지지 판정(NONEXISTENCE_SUPPORT)은 느슨하게 잡았다. 느슨한 쪽이
재시도를 억제할 뿐 만들어내지는 않는 안전한 실패 방향이다
- 사용자가 질문에 적은 전제("그거 아직 안 나왔지?")를 되풀이한 건
숫자와 동일하게 검증 대상에서 제외
- 재프롬프트 문구를 분리했다. 기존 "출처에 있는 숫자만 써서 다시 써라"는
이 경우 헛도는 지시다 — 고칠 숫자가 없고, 문제는 정반대(지어낸 사실을
구실로 답변을 거절)이기 때문
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
246 lines
12 KiB
TypeScript
246 lines
12 KiB
TypeScript
/**
|
|
* numeric-grounding.test.ts
|
|
*
|
|
* The last guard in the chain, and the only one that asks whether a NUMBER came from the search
|
|
* rather than whether a search happened. Its failure mode is the worst in the codebase: an
|
|
* invented specification delivered as a measurement, with a real search in the transcript to
|
|
* make it look sourced.
|
|
*
|
|
* Both directions matter equally here and pull against each other:
|
|
* - miss a fabrication → the user acts on an invented figure
|
|
* - flag a good answer → a pointless retry, and eventually the guard gets disabled
|
|
* The balance comes from scoping: judge the figure that ANSWERS the question (first claim in the
|
|
* unit the question named), not every number in the reply. Two earlier thresholds failed live and
|
|
* both are pinned below — "every claim must miss" was shielded by the question's own echoed
|
|
* numbers, and then by a genuine comparison figure quoted alongside the invented one.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { checkNumericGrounding, extractNumericClaims } from '../src/gateway/chat/numeric-grounding';
|
|
|
|
describe('주장 추출 — (숫자, 단위) 쌍', () => {
|
|
test('단위가 붙은 수치만 뽑는다', () => {
|
|
const c = extractNumericClaims('VRAM은 32GB이고 대략 5개 정도 있습니다.');
|
|
assert.equal(c.length, 1);
|
|
assert.deepEqual(c[0].numbers, ['32']);
|
|
assert.equal(c[0].unit, 'GB');
|
|
});
|
|
|
|
test('범위는 양 끝을 모두 담는다', () => {
|
|
const c = extractNumericClaims('초당 약 2~5 토큰');
|
|
assert.deepEqual(c[0].numbers, ['2', '5']);
|
|
});
|
|
|
|
test('천단위 쉼표를 정규화한다', () => {
|
|
assert.deepEqual(extractNumericClaims('1,024GB')[0].numbers, ['1024']);
|
|
});
|
|
|
|
test('코드블록 안 숫자는 주장이 아니다', () => {
|
|
assert.equal(extractNumericClaims('```py\nSIZE_GB = 80\n```').length, 0);
|
|
});
|
|
});
|
|
|
|
describe('근거 판정 — 단위 근처에 있어야 인정', () => {
|
|
test('도구가 준 수치는 근거 있음', () => {
|
|
const r = checkNumericGrounding(
|
|
'내일 구미는 최저 23.0°C 최고 33.0°C, 강수확률은 20%입니다.',
|
|
['기상청 단기예보\n 기온: 23.0~33.0°C\n 강수확률: 20%'],
|
|
);
|
|
assert.equal(r.allUnsupported, false);
|
|
});
|
|
|
|
test('[2026-07-29] 출처에 없는 수치는 걸린다 — 실제 사고 케이스', () => {
|
|
// M40+Qwen 조합의 실측치는 어디에도 없는데 모델이 값을 냈다. 검색은 실제로 수행됐고
|
|
// 결과도 비어있지 않아서 앞선 세 가드가 모두 통과시킨 상황.
|
|
const r = checkNumericGrounding(
|
|
'초당 약 2~5 토큰 정도 나옵니다.',
|
|
['[1] Running Local LLMs on M4: 24GB RAM Performance Guide ... [2] Qwen 3.6 35B MoE 일반 가이드'],
|
|
);
|
|
assert.equal(r.allUnsupported, true);
|
|
assert.equal(r.unsupported[0].text, '2~5 토큰');
|
|
});
|
|
|
|
test('맨숫자 우연 일치로 통과시키지 않는다', () => {
|
|
// "2"와 "5"는 출처 어딘가에 파편으로 존재한다. 단위를 함께 보지 않으면 근거 있음이 된다.
|
|
const r = checkNumericGrounding('초당 2 토큰입니다.', ['버전 2.5 문서, 페이지 5, 항목 2개']);
|
|
assert.equal(r.allUnsupported, true);
|
|
});
|
|
});
|
|
|
|
describe('직접 답변 판정 — 진짜 인용이 날조를 가리지 못하게', () => {
|
|
test('[2026-07-29] 같은 단위의 진짜 인용이 옆에 있어도 날조를 잡는다', () => {
|
|
// 실제 2차 실패: "157.66 tok/s on RTX 3090"은 페이지에 진짜 있는 값이라 근거 있음으로
|
|
// 잡히고, 그 하나가 질문 대상(M40 조합)의 날조를 가려주고 있었다.
|
|
const r = checkNumericGrounding(
|
|
'총 48GB 구성에서 초당 약 2~5 토큰입니다. 참고로 RTX 3090은 157 tok/s.',
|
|
['UD-Q4_K_M ... runs at 157.66 tok/s on our own RTX 3090'],
|
|
'Tesla M40 24GB 2장으로 Qwen3.6 35B 돌리면 초당 몇 토큰?',
|
|
);
|
|
assert.equal(r.allUnsupported, true);
|
|
assert.equal(r.primaryClaim?.text, '2~5 토큰');
|
|
});
|
|
|
|
test('질문이 물은 수치를 제대로 인용하면 통과', () => {
|
|
const r = checkNumericGrounding(
|
|
'RTX 3090은 157.66 tok/s가 나옵니다.',
|
|
['runs at 157.66 tok/s on our own RTX 3090'],
|
|
'RTX 3090 초당 몇 토큰?',
|
|
);
|
|
assert.equal(r.allUnsupported, false);
|
|
});
|
|
|
|
test('질문 단위 밖의 수치는 판정 대상이 아니다', () => {
|
|
// "몇 토큰"을 물었으면 GB 수치는 맥락일 뿐이다.
|
|
const r = checkNumericGrounding(
|
|
'초당 5 토큰이며 VRAM은 99GB 필요합니다.',
|
|
['throughput: 5 토큰'],
|
|
'초당 몇 토큰?',
|
|
);
|
|
assert.equal(r.askedUnit, '토큰');
|
|
assert.equal(r.allUnsupported, false);
|
|
});
|
|
});
|
|
|
|
describe('보수적 임계값 — 오탐 방지가 우선', () => {
|
|
test('파생 수치는 허용한다 — 앞선 근거가 답을 지탱하면 통과', () => {
|
|
// 48GB는 24GB×2로 유도한 값이라 출처에 없다. 이런 계산을 막으면 안 된다.
|
|
const r = checkNumericGrounding('카드당 24GB이므로 2장이면 48GB입니다.', ['spec: 24GB per card']);
|
|
assert.equal(r.allUnsupported, false);
|
|
});
|
|
|
|
test('질문에 있던 숫자를 되뇌는 것은 근거로 치지 않는다', () => {
|
|
// 이 예외가 없으면 질문의 "24GB"가 유일한 근거로 잡혀 날조 답변이 통과한다(실제로 그랬음).
|
|
const r = checkNumericGrounding(
|
|
'Tesla M40 24GB 2장이면 초당 약 2~5 토큰입니다.',
|
|
['관련 없는 검색 결과'],
|
|
'Tesla M40 24GB 2장으로 돌리면 초당 몇 토큰?',
|
|
);
|
|
assert.equal(r.allUnsupported, true);
|
|
});
|
|
|
|
test('수치 주장이 없으면 발동하지 않는다', () => {
|
|
assert.equal(checkNumericGrounding('메모리 대역폭이 병목입니다.', ['아무 문서']).allUnsupported, false);
|
|
});
|
|
|
|
test('출처 자체가 비면 발동하지 않는다 — 빈검색결과 가드 담당', () => {
|
|
assert.equal(checkNumericGrounding('32GB입니다.', []).allUnsupported, false);
|
|
assert.equal(checkNumericGrounding('32GB입니다.', [' ']).allUnsupported, false);
|
|
});
|
|
});
|
|
|
|
describe('날짜 검증 — 출처마다 표기가 다르므로 형식 관용이 필수', () => {
|
|
const q = 'Tesla M40은 정확히 몇 년 몇 월에 출시됐어?';
|
|
const a = 'NVIDIA Tesla M40은 2015년 11월 10일에 출시되었습니다.';
|
|
|
|
test('영문·ISO·한글 어떤 표기든 근거로 인정한다', () => {
|
|
// 한국어 답변 + 영문 출처는 정상 조합이다. 이걸 못 맞추면 정답이 오탐으로 걸린다.
|
|
for (const src of [
|
|
'NVIDIA Tesla M40 ... Release Date: Nov 10th, 2015 ... Maxwell',
|
|
'Launched November 2015 alongside the M4',
|
|
'release_date: 2015-11-10, arch: Maxwell',
|
|
'테슬라 M40은 2015년 11월 출시',
|
|
]) {
|
|
assert.equal(checkNumericGrounding(a, [src], q).dateUnsupported, false, `표기 인식 실패: ${src}`);
|
|
}
|
|
});
|
|
|
|
test('연도만 맞고 월이 다르면 걸린다', () => {
|
|
const r = checkNumericGrounding(a, ['Released in 2015. In March 2015 NVIDIA announced ...'], q);
|
|
assert.equal(r.dateUnsupported, true);
|
|
assert.equal(r.primaryDate?.year, '2015');
|
|
});
|
|
|
|
test('출처가 무관하면 걸린다', () => {
|
|
assert.equal(
|
|
checkNumericGrounding(a, ['Nvidia Corporation is an American company headquartered in Santa Clara'], q).dateUnsupported,
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('날짜를 묻지 않은 질문에서는 답변의 연월을 판정하지 않는다', () => {
|
|
// 날씨 답변에 "2026년 7월 기준" 같은 표현이 섞여도 검증 대상이 아니다.
|
|
const r = checkNumericGrounding('내일 구미는 23~33°C입니다. 2026년 7월 기준 평년보다 높습니다.', ['기온: 23.0~33.0°C'], '내일 구미 날씨');
|
|
assert.equal(r.allUnsupported, false);
|
|
assert.equal(r.primaryDate, null);
|
|
});
|
|
});
|
|
|
|
describe('순위는 의도적으로 검증하지 않는다', () => {
|
|
test('순위표는 "40 구미시" 처럼 단위 없이 쓰므로 정답이 오탐되면 안 된다', () => {
|
|
// 이 케이스가 순위 검증을 넣지 않기로 한 근거다. "위" 인접을 요구하면 정답이 걸리고,
|
|
// 맨숫자 "40"만 찾으면 어떤 문서에나 있어 검증이 무의미해진다.
|
|
const r = checkNumericGrounding(
|
|
'구미시의 인구는 402,726명으로 전국 기초자치단체 중 40위입니다.',
|
|
['순위 지역 인구(명) ... 39 대구광역시 북구 407,548 40 경상북도 구미시 402,726'],
|
|
'구미시 인구는 전국 시 단위에서 몇 위야?',
|
|
);
|
|
assert.equal(r.allUnsupported, false);
|
|
});
|
|
});
|
|
|
|
describe('"아직 출시 안 됐다" 주장 — 숫자가 없어도 검증한다', () => {
|
|
// 2026-08-11 실제 사고. RTX 4080 vs 5060 비교 요청에 모델이 검색은 했고(→AUTO-RECOVER 통과),
|
|
// 결과도 비어있지 않았고(→EMPTY-GROUNDING 통과), 수치를 하나도 안 썼다(→기존 숫자검증에 걸릴
|
|
// 주장 0건). 그러면서 "RTX 5060의 미출시 상태"를 답을 못 주는 이유로 단정했다 — 실제로는
|
|
// 사용자 지서버에 꽂혀 있는 카드다. 모델 학습 컷오프(2025-01) 4개월 뒤 출시라 기억에 없을 뿐인데,
|
|
// "내 기억에 없다"를 "세상에 없다"로 바꿔 말한 것이다.
|
|
const REAL_ANSWER = `현재 검색 결과만으로는 RTX 4080과 RTX 5060의 구체적인 성능 차이를 확정적으로 말씀드리기 어렵습니다.
|
|
- 비교 데이터 부재: 기술적 비교를 시도한 페이지는 현재 접속이 불가능한 상태입니다.
|
|
- RTX 5060의 미출시 상태: NVIDIA 공식 페이지에서는 RTX 50 시리즈에 대한 일반적인 소개만 있을 뿐입니다.`;
|
|
|
|
test('출처에 근거가 없는 미출시 단정을 잡는다', () => {
|
|
const r = checkNumericGrounding(
|
|
REAL_ANSWER,
|
|
['GeForce RTX 50 시리즈 - Blackwell 아키텍처 기반 그래픽카드 라인업 소개'],
|
|
'5060 보다 얼마나 빠르지 4080?',
|
|
);
|
|
assert.equal(r.nonexistenceUnsupported, true);
|
|
assert.equal(r.allUnsupported, true);
|
|
});
|
|
|
|
test('출처가 실제로 미출시라고 하면 발동하지 않는다', () => {
|
|
const r = checkNumericGrounding(
|
|
'RTX 6090은 아직 출시되지 않았습니다.',
|
|
['RTX 6090 is not yet released; NVIDIA has announced an expected launch in late 2027.'],
|
|
'RTX 6090 나왔어?',
|
|
);
|
|
assert.equal(r.nonexistenceUnsupported, false);
|
|
assert.equal(r.allUnsupported, false);
|
|
});
|
|
|
|
test('사용자가 질문에 적은 전제를 되풀이한 것은 모델의 주장이 아니다', () => {
|
|
const r = checkNumericGrounding(
|
|
'말씀하신 대로 아직 출시되지 않은 제품이라 벤치마크가 없습니다.',
|
|
['관련 정보를 찾을 수 없습니다'],
|
|
'그거 아직 출시되지 않았지?',
|
|
);
|
|
assert.equal(r.nonexistenceClaim, null);
|
|
assert.equal(r.nonexistenceUnsupported, false);
|
|
});
|
|
|
|
test('출처가 아예 없으면 판정하지 않는다 (EMPTY-GROUNDING이 담당)', () => {
|
|
const r = checkNumericGrounding('아직 출시되지 않았습니다.', [], '나왔어?');
|
|
assert.equal(r.nonexistenceUnsupported, false);
|
|
});
|
|
|
|
test('영어 표현도 잡는다', () => {
|
|
const r = checkNumericGrounding(
|
|
'The RTX 5060 has not been released yet, so no benchmarks exist.',
|
|
['NVIDIA GeForce graphics cards overview page'],
|
|
'how much faster is the 4080 than the 5060?',
|
|
);
|
|
assert.equal(r.nonexistenceUnsupported, true);
|
|
});
|
|
|
|
test('부정문이라고 다 잡지는 않는다 — 출시/존재 동사만', () => {
|
|
const r = checkNumericGrounding(
|
|
'이 기능은 무료 요금제에서는 지원되지 않습니다.',
|
|
['Free plan feature comparison table'],
|
|
'무료로 쓸 수 있어?',
|
|
);
|
|
assert.equal(r.nonexistenceClaim, null);
|
|
assert.equal(r.allUnsupported, false);
|
|
});
|
|
});
|