Commit Graph
8 Commits
Author SHA1 Message Date
kimandClaude Opus 5 10e54d38eb fix: 게이트 전수 감사 — 한국어 검색 랭킹이 관련성을 0으로 계산하던 버그 외 5건
실사용 로그의 사용자 메시지 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 오탐 — `![KakaoMap_….png](…)` 첨부 파일명이 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
2026-09-04 18:01:26 +09:00
kimandClaude Opus 5 db6a8634c3 fix: 가드가 맞는 답을 지우던 경로 — 무관한 검색 결과에 대고 판정하던 문제
2026-09-04 메인챗 세션(0c367bd7) 감사. 사용자가 "옵션 프리미엄이 실물의 몇 %냐"를
다섯 턴에 걸쳐 물었고, 검색 6회에 산출물 0건으로 끝났다. 로그를 보니 원인이 모델이
아니라 우리 가드였다:

  TOOL: web_search("typical option premium as percentage of underlying asset price")
  TOOL OK: [1] TYPICAL Definition & Meaning - Merriam-Webster
  NUMERIC-GROUNDING POST-CHECK (1/1): 3 figure(s) not found in tool output [10%, 1%, 2%]

모델은 매 턴 답을 냈다(10%/1%/2%, 다음 턴엔 3~10%/0.1~1%). 검색엔진이 "typical"의
사전 뜻풀이를 물어왔고, 그 뭉치에 숫자가 없다는 이유로 답이 날조 판정을 받아 지워졌다.
이 가드의 전제는 "검색이 그 주제를 다뤘는데 숫자가 없다 → 지어냈다"인데, 앞부분을
한 번도 확인한 적이 없었다. 무관한 말뭉치에서의 부재는 날조의 증거가 아니다.

* 관련성 관문(sourceCoversQuery): 검색어의 내용어 중 결과에 등장하는 비율이 0.34
  미만이면 판정을 포기한다. 실측 — 사전 뭉치 0.14 / 정상 결과 0.86. 수치·날짜·부재주장
  세 갈래가 같은 전제 위에 있어 한꺼번에 게이트했다. 검색어를 못 넘기는 호출부는 기존
  동작 유지.

* SEARCH-ABANDONMENT도 같은 결함이 있었다(감사 중 발견). countSubstantiveResults는
  스니펫 길이만 봐서 사전 페이지 4건을 전부 "알맹이 있음"으로 세고, 모델에게 "그걸로
  답하라"고 민다 — 답을 지우는 대신 틀린 답을 강요하는, 같은 전제의 반대 방향 피해다.
  같은 관문을 공유시켜 기준이 갈라지지 않게 했다.

* 그 가드는 애초에 죽어 있었다. NOTHING_FOUND가 "정보가 없다"류만 담고 있어, gemma4가
  실제로 쓰는 "결과에 …이 포함되어 있지 않다"(주어가 결과)를 하나도 못 잡았다. 이
  세션의 포기 답변 6건 중 0건 탐지. 사과로 시작하면 판정 대상인 첫 문장이 "죄송합니다."가
  되는 두 번째 구멍도 함께 수정(한글 뒤 \b 미매칭 함정 주의).

* dead-end-fallback (신규): 두 번째로 빈손이면 검색을 끊고 "아는 것을 검증 못 했다고
  라벨 붙여 말하라"로 돌린다. 문제의 스레드에서 3턴 앞서 끊긴다. 재프롬프트가 숫자를
  요구하지 않아 NUMERIC-GROUNDING과 충돌하지 않는다.

* 제품·모델 선택 질문이 isFactualInfoRequest를 통째로 빠져나갔다("모델이 있을까",
  "고른다면", "신형/최신"). 그래서 소설-모델 3턴이 도구 0개로 굴러가 2026년 9월에
  "Gemma 2 27B"를 로컬 추천으로 냈다. 주제어가 앞 턴에만 있는 짧은 후속 질문용
  isFactualThreadFollowUp 추가.

* confirmsUserGuessWithoutGrounding: 직전 턴에 확인 못 했다고 해놓고 사용자 추측에
  "네, 맞습니다"로 동조하는 경로 차단(맥미니 M6 건). 4중 조건으로 좁혀 정당한 맞장구는
  건드리지 않는다.

* cms_hospital_compare 게이트가 "병원"에만 걸려 있어 사용자가 내내 쓴 "의료법인"으로는
  8턴 동안 스코프에 들지 않았다. 그 사이 공공 비중 답이 45~50%→39~40%→약 50%로 흔들렸다.

테스트 521개 통과(+65). 실사용 로그의 실제 발화·답변을 표본으로 고정했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 17:51:12 +09:00
kimandClaude Opus 5 9ab4da34e5 fix: "A보다 빠른가" 캐주얼 비교 표현을 isFactualInfoRequest가 못 잡던 문제
실제 사고(2026-08-11): "5060 보다 얼마나 빠르지 4080?"에 도구 호출
0건, 모델이 "RTX 5060은 아직 출시되지 않은 차세대 모델"이라고
완전히 지어냈다 — 실제로는 이미 출시돼 지서버가 쓰고 있는 카드
(RTX 5060 Ti)다. "성능"/"비교" 등 기존 키워드를 하나도 안 써서
이 파일 자체 주석이 경고하던 "casual paraphrases slip through
unmatched"가 실제로 뚫렸다.

"A보다 (얼마나) 빠른/느린/좋은/나은" 형태의 캐주얼 비교 표현을
형용사 목록으로 추가. 구현 중 발견: 빠르다/느리다는 러-불규칙
활용이라 관형형이 "빠르ㄴ"이 아니라 "빠른"으로 어간 자체가
바뀐다(느리다→느린도 동일) — 어간만 넣으면 이 활용형을 놓쳐서
테스트가 그대로 잡아냈다("이게 저것보다 느린가?"가 최초 버전에서
빠짐), 두 형태 다 등록해서 고쳤다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:24:15 +09:00
kimandClaude Opus 5 c5c3c0449b fix: 재해 질문을 news_search로 유도 — web_search 기본 쿼리가 실제 사건을 놓침
실제 사고(2026-08-10): "태풍 돌핀 지금 어디 있지?"에 web_search가 자체
생성한 "태풍 돌핀 현재 위치 실시간 정보" 쿼리로 검색해 일반적인/내용없는
결과 3개(기상청 빈 포털, 오래된 지도 SPA, 무관한 결과 1개)만 받았다.
모델은 정직하게(좌표를 지어내지 않고) "검색결과가 없으니 이미 소멸했을
가능성이 높다"고 추론했다 — 추론 자체는 멀쩡했지만 전제(검색해도 안
나온다)가 검색 실패 때문에 거짓이었다. 실제로는 돌핀이 막 중국에
상륙해 500mm 폭우 경보, 100만명 이상 대피 중이었고, 같은 사건을 다룬
국내 언론사 기사가 6개 이상 있었다 — "태풍 돌핀 중국 상륙 피해"처럼
상황어를 넣은 쿼리로는 바로 찾아졌다.

news_search(NewsData.io, 실제 발행일 보장)가 정확히 이런 "지금 벌어지고
있는 일" 질문을 위한 도구인데, 재해 질문은 뉴스로 분류되지 않아
(isNewsRequest는 "뉴스/속보" 키워드가 있어야 걸림) 이 라우팅을 안 탔다.

isLiveDataRequest 안에 있던 재해 정규식을 isDisasterRequest로 분리해
handle-chat.ts의 사전 리마인더와 AUTO-RECOVER 재시도 리마인더 양쪽에
재사용했다 — 이 파일 헤더의 원칙대로, 검증을 강제하는 게이트끼리
어긋나면 한쪽만 고친 게 무의미해진다. 재해 질문엔 news_search를
우선 권하고, web_search를 쓰더라도 "현재 위치/실시간 정보" 같은
일반 쿼리 대신 "...상륙 피해", "...대피" 같은 상황어를 쓰도록
안내한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:42:31 +09:00
kimandClaude Opus 5 305f93d08b fix: 검증 강제 가드의 오탐 3종 — 사용자가 준 숫자, NHC/EONET, 붙여넣기 본문
실사용 로그와 세션 104건을 가드에 직접 돌려서 찾은 오탐들이다.

1) 사용자가 질문에 직접 적은 숫자로 AUTO-RECOVER가 걸렸다.

   로그 실례: "북위 27.7도, 동경 124.8도 여기가 어디쯤이지?" — 그 좌표 자체가
   한 턴 전 web_search 결과였고 사용자가 그대로 옮겨 물은 것이다. 모델이
   정답("동중국해")을 냈는데 답에 좌표를 되풀이했다는 이유로 COORDINATE_CLAIM이
   걸려 재생성 + "latitude 27.7 longitude 124.8 location" 검색을 태웠고,
   돌아온 건 "농림통계연보(2005).hwp"였다. 그러고서 같은 답을 다시 썼다.

   이 가드가 잡으려는 건 "모델이 지어낸 숫자"인데, 사용자가 적어 넣은 숫자는
   정의상 모델이 만든 값이 아니다. looksLikeUnverifiedSpecClaim에 userMessage를
   받아 스캔 전에 지운다. 모델이 새로 덧붙인 수치는 그대로 남는다. 한 자리 수는
   지우지 않는다 — 메시지의 "2" 하나로 답변의 모든 "2"가 지워지면 가드가 통째로
   무력화된다(회귀 테스트 포함).

2) nhc_active_storms/eonet_events가 GROUNDING_TOOL_PATTERN에 없었다.

   둘 다 권위 있는 원본(미 국립허리케인센터, NASA EONET) 직결이고 태풍 질문이
   실제로 도는 도구인데, 08-09에 추가한 MEASUREMENT_UNITS/COORDINATE_CLAIM이
   바로 그 도구들이 돌려주는 값(hPa·m/s·북위)에 반응한다. 그래서 NHC 데이터로만
   답한 턴이 "근거 도구 미호출"로 분류돼, 방금 읽은 권위 있는 출처를 의심하라고
   web_search 재시도를 걸었다. memory_stats 때와 같은 구멍이다.

3) 긴 붙여넣기 문서 본문의 우연한 키워드로 강제검색이 걸렸다.

   7,583자짜리 고소장 작성 요청이 본문 한가운데(2578자 지점)의 "시세"(장물
   시세) 때문에 isLiveDataRequest에 걸렸다. 문서를 써 달라는 턴에 "메모리에서
   답하지 말고 검색부터 하라"는 리마인더가 붙는 건 토큰 낭비를 넘어 작업 방향을
   반대로 민다.

   처음엔 "길다 + isExecutionLikeRequest"로 막으려 했는데 회귀 테스트가 그게
   틀렸음을 잡아냈다 — 그 메시지가 실행형으로 분류된 근거가 본문의 "화면" 한
   단어였다. 막으려는 문제와 똑같은 우연 위에 얹은 수정이었다. 대신 위치를
   본다: 사람은 지시를 자료의 앞이나 뒤에 쓰지 한가운데 묻지 않는다.
   600자 초과 메시지는 앞 400자 + 뒤 300자만 스캔한다(instructionZone).
   검증 강제 게이트끼리 예외가 어긋나면 안 되므로 isFactualInfoRequest에도
   같이 적용했다.

실사용 104건 대조: isLiveDataRequest 오탐 3건 제거하고 정당한 25건은 전부
유지, 출력측 가드는 로그의 그 좌표 케이스가 정확히 하나 걸러졌다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:40:50 +09:00
kimandClaude Opus 5 22a8c1a11c fix: 시점 표현("최근/지금/현재")을 화제 무관 라이브데이터 신호로 일반화
세션 papa@ca71bc1c(로그 12804행): "마두로 미국에 잡혀가지 않았나?"에 세 턴
연속 도구 호출 없이 "체포되지 않았다"고 확신에 차 정정했는데, 실제로는
사용자가 맞았다(이미 미국에 체포·압송된 상태). 직전 커밋에서 체포/사임 등
개별 어휘를 isLiveDataRequest에 추가해 이 사고는 막았지만, 그 방식 자체가
화이트홀이었다 — 다음번엔 태풍도 체포도 아닌 제3의 화제에서 또 뚫린다.

화제별 어휘 목록 대신 "최근/지금/현재"가 들어가면 화제를 안 가리고 걸리도록
일반화했다. 근거는 실사용 로그 403건 전수조사: 이 세 표현이 들어간 66건
(16.4%) 중 오탐 0건 — 비트코인 시세·현직 대통령·최신 모델 버전 등 전부
정당한 실시간 조회였다. 새 레이어가 추가로 잡아낸 32건 중 31건도 마찬가지로
필요한 검색이었고, "지금 몇 시야?"만 이미 시스템 프롬프트에 있는 시각을
다시 검색시키는 사소한 낭비였다(오답은 아님). "요즘"은 실사용 예가 없어
후보에서 뺐다 — "요즘 어떻게 지내?" 같은 잡담과 부딪힐 위험을 검증 없이
감수하지 않는다.

넓히는 과정에서 진짜 버그 두 개가 드러났다:
- isLiveDataRequest가 isExemptFromVerification(지서버/클로서버 등 개인
  인프라 예외)을 애초에 한 번도 거치지 않고 있었다. 파일 상단 주석은 "모든
  검증 가드가 이 예외를 자동으로 적용받는다"고 주장했지만 사실이 아니었다.
  시점 표현을 넓게 걸자 "지서버 최근에 왜 이렇게 느려"가 웹검색 리마인더에
  걸릴 뻔하면서 드러났다.
- "시어엔진"(SearXNG 별칭)이 예외 목록에 없었다. 실사용 로그의 "현재
  시어엔진 상태 어때?"는 웹검색으로 검증 불가능한 로컬 인프라 질문이다.

둘 다 함께 고쳤다. 부수 효과로, 오타("잡ㅎ간") 때문에 이전 커밋의 체포 어휘
목록이 못 잡던 마두로 사건의 첫 턴도 "현재" 표현으로 이제 잡힌다.

코딩/실행형 요청(isExecutionLikeRequest)과 인사말(isGreetingLikeMessage)은
계속 제외한다 — "지금 이 코드 확인해줘"에 검색 리마인더가 끼어들면 안 된다.

검증: tests/prompt-gates.test.ts 6개 추가, 196개 전부 통과. 실제 채팅에서
"비트코인 현재 가격이 얼마야?" → web_search 호출 확인, "지서버 지금 몇 개
모델 로드돼있나" → 웹검색 없이 로컬 처리 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:29:01 +09:00
kimandClaude Opus 5 05bb3b6d95 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>
2026-08-09 10:38:52 +09:00
kimandClaude Opus 5 2ba39bca76 v4.3.21: 테스트 인프라 복구 — npm test 실행 가능하게 + 회귀 테스트 30개
tests/ 디렉터리가 비어 있었고 package.json의 두 스크립트가 삭제된 파일을
가리킨 채 방치돼 있었음:
- test    → tests/test-v2.ts (존재하지 않음)
- gateway → src/gateway/server-v2.ts (v4.3.5에서 삭제됨)

즉 npm test가 몇 달간 실행 자체가 불가능한 상태였고, 그 대가로 프롬프트 게이트
정규식을 고칠 때마다 node -e 임시 스크립트를 손으로 짜고 버리는 일이 반복됐음
(2026-07-29 하루에만 5회). 회귀 방지는 하나도 남지 않았음.

- Node 22 내장 러너(node:test) + tsx 사용, 테스트 프레임워크 의존성 없음
- tests/prompt-gates.test.ts (25) — 모델 동작을 강제/억제하는 정규식 게이트 전반
- tests/usage-log.test.ts (5)  — Ollama 외 provider의 토큰 집계 단일 지점
- tests/README.md — 무엇을 왜 테스트하는지, 케이스 작성 규칙, 다음 확장 대상
- 전체 30개 통과, 0.6초

부수 성과 — 테스트가 실제 버그를 찾음:
케이스를 쓰면서 프로덕션 원문을 한 단어(GPU) 줄여 썼더니 실패했고, 그게 진짜
구멍이었음. "토큰 처리 속도 손실이 약 15%~25%"처럼 하드웨어 명사가 없는 처리량
날조는 SPEC_CONTEXT_KEYWORD에 걸리지 않아 그대로 통과하고 있었음 — 원문에
우연히 들어있던 "GPU" 덕에 잡히던 것. 토큰/대역폭/추론/처리속도를 키워드에
추가해 막고, 그 케이스를 테스트로 고정함(강수확률·할인율 오탐 없음 확인).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:29:02 +09:00