실사용 로그의 사용자 메시지 1,011건(고유 782건)을 모든 게이트에 통과시켜 오탐/미탐을
실측했다([[feedback_calibrate_on_real_data]]).
가장 큰 건: rankResults의 토큰화가 `q.replace(/[^a-z0-9\s]/g, ' ')`였다. 이 문자 클래스는
한글을 전부 지운다. **모든 한국어 쿼리**에서 관련성 토큰이 빈 배열이 되고 rel 점수가 항상
0이 되어, 결과가 오로지 domainTrustScore로만 정렬됐다 — 질문에 답하는 페이지인지와 무관하게.
시어엔진 라이브 실측("영리의료법인 비영리 병원 비율"): 수정 전에는 "Chee Lai"라는 사람의
LinkedIn 프로필 4건이 1~4위였고 유일한 관련 기사가 아래로 밀려 있었다. 수정 후 그 기사가
1위가 된다. rankResults는 7개 provider 전부가 쓰므로 영향 범위가 검색 전체다.
유니코드 인식 토큰화로 교체(한글·한자 2자 / 영문은 기존 4자 유지). 시점 표현도 영어만
있어서 한국어 "지금/현재/최신"은 배수를 받은 적이 없었다.
나머지:
* isCancelIntent 오탐 — "하지마 유적지는 바다와 떨어져 있는데?"에서 지명 하지마(波島)가
`하지\s*마`에 걸렸다. 참이면 진행 중이던 작업이 일시정지되고 지리 질문이 작업제어 응답으로
하이재킹된다(server.ts 두 호출부 모두). 취소는 문장 끝 명령이므로 종결 위치로 앵커를 걸었다.
영어 쪽은 표본에 사례가 없어 건드리지 않았다.
* isExecutionLikeRequest 오탐 — 맨 "패치"가 목록에 있어 "인슐린 패치는 혈당 측정 기능이
없나?", "패치는 많이 비싼가?"가 실행형으로 분류됐다. 이 함수는 검증 강제에서 면제시키는
쪽이라, 실존 의료기기의 기능·가격을 검색 없이 기억으로 답해도 아무도 막지 않았다.
"구속"을 체포 맥락으로 좁힌 것과 같은 처리 — 소프트웨어 패치 활용형일 때만 걸린다.
* isFactualInfoRequest 미탐 — "얼마" 계열 수량·가격 질문. 표본에서 이 표현으로 물었는데
어떤 검증 게이트에도 안 걸린 5건이 있었고 전부 정당한 사실 질문이었다(오탐 0):
"tesla v100 32gb 메모리 대역폭은 얼마지?", "미국 올 해 예산은 전 부 얼마지?"(실제로
기억에서 $7조 5,400억이 나갔다), "이자 비용은 연간 얼마지?" 등. 맨 "얼마나"는 비-사실
용법이 흔해 넣지 않았다.
* tool-scope climate 오탐 — "메모리 가격 추세"가 기후 재분석 아카이브 4종(~820토큰)을
통째로 실었다. "추세"/"장기"는 기후·기상 명사와 같이 나올 때만 걸리게 좁혔다.
* tool-scope kakao 오탐 — `` 첨부 파일명이 kakao_send_message를 실었다.
executeTool은 스키마 소속을 확인하지 않으므로([[project_tool_schema_leak]]) 이름만 언급돼도
호출될 수 있는 종류의 도구다. 첨부 파일명을 판정에서 빼고 카카오맵도 제외했다.
순효과 측정(고유 782건): 검색 강제 리마인더 주입률 23.8% → 25.4%(+13건). 새로 잡힌 13건은
전부 정당한 사실 질문이고, 잃은 건 0건이다.
감사에서 이상 없음으로 확인한 것: EMPTY-GROUNDING·MESSAGING(전제를 직접 검증),
decideAutoRecover·correctNewsCategoryForBreadth(도구 출력 대조 안 함), link-validator(URL 직접
프로브), satellite 게이트("도달 > 비용" 원칙상 유지), 0건 발동 게이트 6종(전용 앱 탭 경로).
테스트 536개 통과(+15).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
50 lines
2.5 KiB
TypeScript
50 lines
2.5 KiB
TypeScript
/**
|
|
* 검색 결과 랭킹의 관련성 토큰 — 2026-09-04 감사에서 나온 버그.
|
|
*
|
|
* rankResults의 토큰화가 `q.replace(/[^a-z0-9\s]/g, ' ')`였다. 이 문자 클래스는 한글을 전부
|
|
* 지운다. 그래서 **모든 한국어 쿼리**에서 토큰이 빈 배열이 되고, 관련성 점수가 항상 0이 되어
|
|
* 결과가 domainTrustScore만으로 정렬됐다 — 질문에 답하는 페이지인지와 무관하게.
|
|
*
|
|
* 실측(시어엔진 라이브, "영리의료법인 비영리 병원 비율"): 수정 전에는 "Chee Lai"라는 사람의
|
|
* LinkedIn 프로필 4건이 1~4위였고 유일한 관련 기사("개인병원도 비영리법인 전환 허용해야")가
|
|
* 아래로 밀려 있었다. 수정 후 그 기사가 1위가 된다.
|
|
*
|
|
* rankResults는 7개 provider 전부가 쓰므로 영향 범위가 검색 전체다.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { relevanceTokens } from '../src/tools/web';
|
|
|
|
describe('relevanceTokens — 한국어 쿼리에서도 내용어가 나와야 한다', () => {
|
|
test('한글 쿼리가 더 이상 빈 배열이 아니다 (이 버그의 직접 회귀)', () => {
|
|
assert.deepEqual(relevanceTokens('구미 날씨 어때'), ['구미', '날씨', '어때']);
|
|
assert.deepEqual(relevanceTokens('영리의료법인 법인세율'), ['영리의료법인', '법인세율']);
|
|
});
|
|
|
|
test('한글은 2자부터 내용어 — 날씨/태풍/세율이 잘려나가면 안 된다', () => {
|
|
assert.ok(relevanceTokens('태풍 경로').includes('태풍'));
|
|
});
|
|
|
|
test('영어는 기존 4자 기준 그대로 — 기존 랭킹을 흔들지 않는다', () => {
|
|
assert.deepEqual(
|
|
relevanceTokens('typical option premium underlying asset'),
|
|
['typical', 'option', 'premium', 'underlying', 'asset'],
|
|
);
|
|
// 관사·전치사는 계속 걸러진다.
|
|
assert.deepEqual(relevanceTokens('the cost of a in on at'), ['cost']);
|
|
});
|
|
|
|
test('숫자가 붙은 토큰과 혼합 문자열', () => {
|
|
assert.deepEqual(relevanceTokens('RTX 5090 memory bandwidth'), ['5090', 'memory', 'bandwidth']);
|
|
// 한글 옆의 짧은 영문 토큰은 영어 기준(4자)을 그대로 따른다.
|
|
assert.deepEqual(relevanceTokens('M6 맥미니 시장반응'), ['맥미니', '시장반응']);
|
|
});
|
|
|
|
test('구두점·빈 입력에서 터지지 않는다', () => {
|
|
assert.deepEqual(relevanceTokens(''), []);
|
|
assert.deepEqual(relevanceTokens('!!! ??? ...'), []);
|
|
assert.deepEqual(relevanceTokens(null as any), []);
|
|
});
|
|
});
|