2026-09-01 맥미니 M5/M6 세션 사후분석에서 나온 두 버그.
1. 세대 추측 검색어 오염: 사용자가 "맥미니 신형"(칩 미지정)이라 물었는데
gemma4가 학습시점 지식으로 web_search("M4 Mac mini …")를 던졌고, SearXNG는
시킨 대로 M4(2024) 기사를 정확히 반환 → 답 전체가 구세대 기준으로 틀어짐.
- system-prompt TEMPORAL CONSISTENCY에 "신형/최신인데 세대 미지정이면
쿼리에 기억 속 세대를 넣지 말고 현재 연도로 검색" 규칙
- handle-chat: hasUnversionedNewestIntent + assumedGenerationTokenInQuery로
감지해 1회 재프롬프트(오염된 검색 실행 전 차단)
2. "잠시만 기다려 주세요" 후 턴 종료: 사용자가 오류 지적하자 모델이
"다시 확인해 보겠습니다. 잠시만 기다려 주세요."만 내고 도구 0개로 정지.
isIntentOnlyReply는 답 속 "2026년 9월"을 실질내용으로 오인, lastRoundWas
GuardReprompt도 false라 기존 넛지 미발동.
- isDeferralPromiseReply 신규(좁은 "wait for me" 패턴), 가드 재프롬프트
선행 없이도 1회 강제 continuation
tests/intent-only-reply.test.ts +12 케이스. 전체 456 통과.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
113 lines
5.8 KiB
TypeScript
113 lines
5.8 KiB
TypeScript
/**
|
|
* intent-only-reply.test.ts
|
|
*
|
|
* "계산하다가 멈춘다"는 신고(2026-08-18)의 정체는 생성 중단이 아니었다. 로그에는 답변이
|
|
* 끝까지 있었고, 문제는 **가드 재프롬프트에 모델이 예고만 하고 끝낸 것**이었다:
|
|
*
|
|
* NUMERIC-GROUNDING POST-CHECK (1/1) → "다시 검색해서 그 결과로 답하라"
|
|
* 모델 → "죄송합니다… 다시 한번 정밀하게 검색해 보겠습니다." (도구 호출 없음)
|
|
* 재시도 예산 소진 → 그 예고문이 그대로 최종 답변으로 나감
|
|
*
|
|
* isIntentOnlyReply는 그 응답만 좁게 집어낸다. 넓게 잡으면 멀쩡한 답변까지 재시도로 끌고 가
|
|
* 불필요한 도구 호출이 늘어난다 — 이 가드의 옛 버전이 정확히 그 이유로 꺼져 있었다.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
isIntentOnlyReply,
|
|
isDeferralPromiseReply,
|
|
hasUnversionedNewestIntent,
|
|
assumedGenerationTokenInQuery,
|
|
} from '../src/gateway/guards/prompt-gates';
|
|
|
|
describe('isIntentOnlyReply', () => {
|
|
test('실제로 멈췄던 그 응답을 잡는다', () => {
|
|
assert.equal(isIntentOnlyReply(
|
|
'죄송합니다. 제가 검색 결과에 없다는 이유만으로 데이터가 "존재하지 않는다"고 단정 지어 답변했습니다. '
|
|
+ '이는 명백한 오류이며, 제 지식의 한계를 사실처럼 표현하여 혼란을 드렸습니다.\n\n'
|
|
+ '다시 한번 구체적인 수치나 관련 연구 보고서가 있는지 정밀하게 검색해 보겠습니다.'), true);
|
|
});
|
|
|
|
test('영문 예고도 잡는다', () => {
|
|
assert.equal(isIntentOnlyReply("Let me search for that now."), true);
|
|
assert.equal(isIntentOnlyReply("I'll look this up and get back to you."), true);
|
|
});
|
|
|
|
test('예고가 없으면 아니다', () => {
|
|
assert.equal(isIntentOnlyReply('검색 결과를 찾지 못했습니다.'), false);
|
|
assert.equal(isIntentOnlyReply(''), false);
|
|
});
|
|
|
|
test('수치가 있으면 실질 답변이다 — 예고 문구가 섞여 있어도', () => {
|
|
// "설명드리겠습니다" 같은 관용구로 시작하는 정상 답변을 재시도로 끌고 가면 안 된다.
|
|
assert.equal(isIntentOnlyReply('계산해 보겠습니다. 총 배출량은 50,000톤입니다.'), false);
|
|
});
|
|
|
|
test('목록·코드·링크가 있으면 실질 답변이다', () => {
|
|
assert.equal(isIntentOnlyReply('정리해 드리겠습니다.\n- 첫째 항목\n- 둘째 항목'), false);
|
|
assert.equal(isIntentOnlyReply('만들어 드리겠습니다.\n```py\nx=1\n```'), false);
|
|
assert.equal(isIntentOnlyReply('찾아보겠습니다. https://example.com 참고'), false);
|
|
});
|
|
|
|
test('길면 이미 설명을 한 것이므로 아니다', () => {
|
|
const long = '확인해 보겠습니다. ' + '가'.repeat(500);
|
|
assert.equal(isIntentOnlyReply(long), false);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 2026-09-01 맥미니 M5/M6 세션: 사용자가 검색 결과 오류를 지적하자 모델이
|
|
* "…다시 정확하게 확인해 보겠습니다. 잠시만 기다려 주세요."만 내놓고 도구 0개로 턴 종료.
|
|
* isIntentOnlyReply 는 답 속 "2026년 9월"을 hasSubstance 로 오인해 놓쳤고,
|
|
* lastRoundWasGuardReprompt 도 false 라 기존 넛지가 안 걸렸다.
|
|
*/
|
|
describe('isDeferralPromiseReply', () => {
|
|
test('실제로 멈췄던 그 응답을 잡는다 — 우연한 날짜가 섞여 있어도', () => {
|
|
assert.equal(isDeferralPromiseReply(
|
|
'죄송합니다. 제가 최신 출시 정보를 놓쳤습니다. 현재 시점(2026년 9월) 기준으로 M5, M6 칩을 탑재한 신형 맥미니가 '
|
|
+ '출시된 상황이군요. 최신 모델인 M5, M6 맥미니의 시장 반응을 다시 정확하게 확인해 보겠습니다. 잠시만 기다려 주세요.'), true);
|
|
});
|
|
|
|
test('영문 "get back to you" 도 잡는다', () => {
|
|
assert.equal(isDeferralPromiseReply('Let me get back to you on that.'), true);
|
|
assert.equal(isDeferralPromiseReply('One moment — checking now.'), true);
|
|
});
|
|
|
|
test('바로 실행하겠다는 예고는 아니다 (기존 intent-only 경로가 처리)', () => {
|
|
assert.equal(isDeferralPromiseReply('알겠습니다. 바로 검색해서 정리해 드리겠습니다.'), false);
|
|
});
|
|
|
|
test('정상 답변은 아니다', () => {
|
|
assert.equal(isDeferralPromiseReply('네, RTX 5090의 대역폭은 약 1790 GB/s입니다.'), false);
|
|
assert.equal(isDeferralPromiseReply(''), false);
|
|
});
|
|
});
|
|
|
|
describe('hasUnversionedNewestIntent / assumedGenerationTokenInQuery', () => {
|
|
test('세대 없이 "신형/최신"을 물으면 true', () => {
|
|
assert.equal(hasUnversionedNewestIntent('맥미니 신형이 나왔는데 시장반응은 어때?'), true);
|
|
assert.equal(hasUnversionedNewestIntent('아이폰 최신 모델 가격'), true);
|
|
});
|
|
|
|
test('사용자가 세대를 직접 대면 false', () => {
|
|
assert.equal(hasUnversionedNewestIntent('mac m5 메모리 대역폭이 얼마나 높아졌지?'), false);
|
|
assert.equal(hasUnversionedNewestIntent('RTX 5090 최신 가격'), false);
|
|
});
|
|
|
|
test('신형 얘기가 아니면 false', () => {
|
|
assert.equal(hasUnversionedNewestIntent('서브프라임 사태 원인이 뭐야?'), false);
|
|
});
|
|
|
|
test('쿼리가 사용자가 안 말한 세대를 박으면 그 토큰을 돌려준다', () => {
|
|
assert.equal(
|
|
assumedGenerationTokenInQuery('M4 Mac mini release market reaction reviews', '맥미니 신형이 나왔는데 시장반응은 어때?', ''),
|
|
'M4');
|
|
});
|
|
|
|
test('사용자가 말한 세대거나 연도만 있으면 null', () => {
|
|
assert.equal(assumedGenerationTokenInQuery('Apple M5 chip memory bandwidth specs', 'mac m5 메모리 대역폭', ''), null);
|
|
assert.equal(assumedGenerationTokenInQuery('Mac mini 2026 latest review', '맥미니 신형', ''), null);
|
|
});
|
|
});
|