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 오탐 — `` 첨부 파일명이 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
This commit is contained in:
@@ -87,7 +87,15 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
// could never use. Split onto their own keyword: a question about 기후/평년/과거/추세 is a
|
||||
// different question from today's forecast, and phrasing it names the difference reliably.
|
||||
const climateToolNames = new Set(['weather_nasa_power', 'weather_era5', 'weather_cds', 'weather_cmip6']);
|
||||
const hasClimateKeyword = /기후|기후변화|온난화|평년|장기|과거\s*(기온|날씨|자료|데이터)|재분석|추세|시나리오|연평균|기상\s*통계|climate|reanalysis|historical\s*(weather|climate)|era5|cmip|projection/i.test(topicText);
|
||||
// 2026-09-04 도달률 감사(실사용 1,011건): "메모리 가격 추세"가 이 게이트를 통과해 기후
|
||||
// 재분석 아카이브 4종(~820토큰)을 통째로 실었다. 위 주석은 "기후/평년/과거/추세라는 표현이
|
||||
// 차이를 확실히 짚어 준다"고 적었지만, 실제로는 "추세"와 "장기"가 기후와 아무 상관 없는 문장에
|
||||
// 훨씬 자주 쓰인다(가격 추세, 장기 계획, 장기적으로…). 두 낱말만 기후·기상 명사와 같이 나올 때
|
||||
// 걸리도록 좁힌다 — 나머지 키워드(기후/평년/재분석/era5 등)는 그 자체로 충분히 특정적이라 그대로 둔다.
|
||||
const CLIMATE_SUBJECT = /기후|기온|날씨|강수|강우|우량|적설|기상|해수면|온난화|climate|weather|temperature|precipitation|rainfall/i;
|
||||
const hasClimateKeyword =
|
||||
/기후|기후변화|온난화|평년|과거\s*(기온|날씨|자료|데이터)|재분석|시나리오|연평균|기상\s*통계|climate|reanalysis|historical\s*(weather|climate)|era5|cmip|projection/i.test(topicText)
|
||||
|| (/추세|장기/.test(topicText) && CLIMATE_SUBJECT.test(topicText));
|
||||
const legalToolNames = new Set(['korean_law_search', 'korean_law_fetch', 'us_case_search']);
|
||||
const pptxToolNames = new Set(['create_presentation', 'edit_presentation']);
|
||||
// In practice these are only ever used while building an academic presentation (gather
|
||||
@@ -128,7 +136,13 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
// them were silently no-op alternatives. Dropping the anchor is safe for Korean: these words
|
||||
// are unambiguous as substrings (카카오메일, 기사 등). Keep \b only on ASCII tokens like ppt\b.
|
||||
const hasEmailKeyword = /이메일|메일|email|inbox|받은편지함/i.test(message);
|
||||
const hasKakaoKeyword = /카카오|kakao/i.test(message);
|
||||
// 2026-09-04 감사: `` 한 줄이 이 게이트를
|
||||
// 통과해 kakao_send_message를 실었다. 사용자가 카카오맵 캡처를 올렸을 뿐인데 메신저 발송 도구가
|
||||
// 붙는다 — 그리고 executeTool은 스키마 소속을 확인하지 않으므로([[project_tool_schema_leak]])
|
||||
// 프롬프트가 이름만 언급해도 실제로 호출될 수 있는 종류의 도구다. 첨부 파일명은 사용자의 '요청'이
|
||||
// 아니므로 판정에서 뺀다. 카카오'맵'도 메신저가 아니라 지도라 함께 제외한다.
|
||||
const messageWithoutAttachments = message.replace(/!?\[[^\]]*\]\([^)]*\)/g, ' ');
|
||||
const hasKakaoKeyword = /카카오(?!\s*맵)|kakao(?!map)/i.test(messageWithoutAttachments);
|
||||
const hasSqliteKeyword = /데이터베이스|sqlite|쿼리|query\b|\.db\b/i.test(message);
|
||||
const hasImageEditKeyword = /편집|보정|자르기|리사이즈|크롭|워터마크|배경\s*제거|필터|흑백|그레이스케일|스타일화|crop|resize|watermark|remove.?bg|grayscale|stylize/i.test(message);
|
||||
// news_search is the single biggest ungated schema (2,035 chars, mostly usage guidance for
|
||||
|
||||
@@ -38,7 +38,13 @@ export function isGreetingLikeMessage(text: string): boolean {
|
||||
export function isExecutionLikeRequest(message: string): boolean {
|
||||
const m = String(message || '');
|
||||
return /\b(create|build|implement|develop|scaffold|generate|fix|debug|edit|update|refactor|rewrite|patch|setup|configure|calendar|app|component|project|file|folder|directory|workspace|code|desktop|window|screen|mouse|keyboard|clipboard|vs code|vscode)\b/i.test(m)
|
||||
|| /(만들어|생성해|구현해|개발해|고쳐|수정해|편집해|디버그|리팩터|리팩토링|패치|설정해|프로젝트|파일|폴더|디렉터리|워크스페이스|코드|바탕화면|화면|마우스|키보드|클립보드)/.test(m);
|
||||
// 2026-09-04 도달률 감사(실사용 1,011건). 맨 "패치"가 목록에 있어서 "인슐린 패치는 혈당
|
||||
// 측정 기능이 없나?"와 "패치는 많이 비싼가?"가 실행형으로 분류됐다. 이 함수는 검증 강제
|
||||
// 게이트에서 **면제**시키는 쪽이라, 그 두 질문은 web_search 없이 기억으로 답해도 아무도
|
||||
// 막지 않는다 — 실존 의료기기의 기능과 가격인데도. "구속"을 체포 맥락으로 좁힌 것과 같은
|
||||
// 처리를 한다: 소프트웨어 패치를 뜻하는 활용형일 때만 걸리게 한다.
|
||||
|| /패치\s*(해|하고|하는|했|를?\s*적용|\s*파일|\s*노트|버전)|핫\s*픽스|hotfix/.test(m)
|
||||
|| /(만들어|생성해|구현해|개발해|고쳐|수정해|편집해|디버그|리팩터|리팩토링|설정해|프로젝트|파일|폴더|디렉터리|워크스페이스|코드|바탕화면|화면|마우스|키보드|클립보드)/.test(m);
|
||||
}
|
||||
|
||||
// 긴 붙여넣기 자료 + 짧은 지시로 이루어진 메시지에서, 게이트가 볼 '지시' 부분만 잘라낸다.
|
||||
@@ -173,7 +179,15 @@ export function isFactualInfoRequest(message: string): boolean {
|
||||
// "신형/최신"을 묻는 질문은 정의상 학습 시점 이후를 묻는 것이라, 기억에서 답하면 반드시
|
||||
// 틀린다. 같은 세션의 맥미니 스레드가 그렇게 8턴을 태웠다. hasUnversionedNewestIntent는
|
||||
// 검색어에 세대가 박혔는지를 사후에 보는 가드라, 애초에 검색을 시키는 이쪽과 역할이 다르다.
|
||||
|| /(신형|신제품|새로\s*나온|이번에\s*나온|최신(형|판)?|출시(됐|되었|된|일|가|는)|latest|newest)/.test(m);
|
||||
|| /(신형|신제품|새로\s*나온|이번에\s*나온|최신(형|판)?|출시(됐|되었|된|일|가|는)|latest|newest)/.test(m)
|
||||
// "얼마" 계열 수량·가격 질문. 2026-09-04 감사에서 이 표현으로 물었는데 어떤 검증 게이트에도
|
||||
// 안 걸린 메시지가 5건 있었고 **전부 정당한 사실 질문**이었다(고유 782건 중 오탐 0):
|
||||
// "tesla v100 32gb 메모리 대역폭은 얼마지?" ← GPU 스펙, 이 가드의 존재 이유 그 자체
|
||||
// "미국 올 해 예산은 전 부 얼마지?" ← 실제로 기억에서 "$7조 5,400억"이 나갔다
|
||||
// "이자 비용은 연간 얼마지?" / "서울에서 부산까지 거리가 얼마야?" / "패치는 많이 비싼가?"
|
||||
// 맨 "얼마나"는 넣지 않는다 — "얼마나 걸려/큰가" 같은 비-사실 용법이 흔하고, 비교형
|
||||
// "얼마나 빠르지"는 바로 위 줄이 이미 잡는다.
|
||||
|| /(얼마(예요|에요|인가|일까|야|지|니|죠|죵)|비싼가|비싸[냐지]|저렴한가|가성비|몇\s*(원|달러|억|만원))/.test(m);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,8 +395,15 @@ export function isRerunIntent(message: string): boolean {
|
||||
}
|
||||
|
||||
export function isCancelIntent(message: string): boolean {
|
||||
// 2026-09-04 도달률 감사에서 나온 오탐: "하지마 유적지는 바다와 떨어져 있는데?" — 하지마(波島)는
|
||||
// 지명인데 `하지\s*마`가 문장 한가운데서 걸렸다. 이 게이트가 참이면 진행 중이던 작업이 일시정지되고
|
||||
// 사용자의 지리 질문은 답 대신 작업제어 응답으로 하이재킹된다(server.ts의 두 호출부 모두).
|
||||
//
|
||||
// 취소는 문장 끝에 오는 명령이지 서술의 일부가 아니다. 종결 위치로 앵커를 걸면 "그거 하지마",
|
||||
// "이거 취소해줘"는 그대로 잡히고 지명·인용은 빠진다. 영어 쪽은 이번 표본에 사례가 없어
|
||||
// 건드리지 않았다 — 근거 없이 넓히거나 좁히지 않는다([[feedback_calibrate_on_real_data]]).
|
||||
return /\b(cancel|abort|stop( task)?|do not continue|don't continue)\b/i.test(message)
|
||||
|| /(취소해|중단해|그만해|하지\s*마|멈춰)/.test(message);
|
||||
|| /(취소해|중단해|그만해|하지\s*마|멈춰)(\s*(줘|요|주세요|세요|라|다|주라))?\s*[.!?~]*\s*$/.test(String(message || '').trim());
|
||||
}
|
||||
|
||||
// --- Step 4 (2026-07-25): standalone filename/message classifiers, moved here from
|
||||
|
||||
+22
-2
@@ -447,15 +447,35 @@ function domainTrustScore(url: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 랭킹에 쓸 내용어 뽑기.
|
||||
*
|
||||
* 2026-09-04 감사에서 발견한 버그: 원래 토큰화는 `q.replace(/[^a-z0-9\s]/g, ' ')`였다. 이 클래스는
|
||||
* **한글을 전부 지운다**. 그래서 한국어 쿼리는 토큰이 언제나 빈 배열이 되고, 관련성 점수 rel이
|
||||
* 항상 0이 되어, 결과가 오로지 domainTrustScore로만 정렬됐다 — 질문에 답하는 페이지인지와
|
||||
* 무관하게. 아무 주제의 위키백과(2.0)가 정답이 실린 블로그(1.5)를 이긴다는 뜻이다.
|
||||
* 이 집의 주 언어가 한국어이고 rankResults는 7개 provider 전부가 쓴다.
|
||||
*
|
||||
* 최소 길이를 문자종류별로 나눈다. 영어는 기존대로 4자 이상(관사·전치사를 걸러내는 값이라
|
||||
* 바꾸면 기존 랭킹이 통째로 흔들린다). 한글·한자·가나는 2자면 이미 내용어다(날씨, 태풍, 세율).
|
||||
*/
|
||||
export function relevanceTokens(query: string): string[] {
|
||||
const parts = String(query || '').toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
||||
const CJK = /[\p{Script=Hangul}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
||||
return parts.filter(t => (CJK.test(t) ? t.length >= 2 : t.length >= 4));
|
||||
}
|
||||
|
||||
function rankResults(query: string, results: SearchResultItem[]) {
|
||||
const q = query.toLowerCase();
|
||||
const freshness = /\b(current|latest|today|now|as of|recent)\b/.test(q);
|
||||
// 시점 표현도 영어만 있었다 — 같은 뜻의 한국어 질문은 이 배수를 받은 적이 없다.
|
||||
const freshness = /\b(current|latest|today|now|as of|recent)\b/.test(q)
|
||||
|| /(지금|현재|오늘|최신|요즘|방금)/.test(q);
|
||||
const tokens = relevanceTokens(q);
|
||||
return [...results]
|
||||
.map(r => {
|
||||
const t = domainTrustScore(r.url);
|
||||
const text = `${r.title} ${r.snippet}`.toLowerCase();
|
||||
let rel = 0;
|
||||
const tokens = q.replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(x => x.length >= 4);
|
||||
for (const tok of tokens) if (text.includes(tok)) rel += 1;
|
||||
return { r, score: t * (freshness ? 2 : 1) + rel * 0.4 };
|
||||
})
|
||||
|
||||
@@ -453,3 +453,53 @@ describe('confirmsUserGuessWithoutGrounding — 근거 없는 동조', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 2026-09-04 도달률 감사 — 실사용 로그 1,011건(고유 782건)을 게이트에 통과시켜 나온 오탐/미탐.
|
||||
* 인용은 전부 로그의 실제 발화다.
|
||||
*/
|
||||
describe('도달률 감사 회귀 (2026-09-04)', () => {
|
||||
test('isCancelIntent: 지명이 취소 명령으로 읽히면 안 된다', () => {
|
||||
// "하지마(波島) 유적지"가 `하지\s*마`에 걸렸다. 이 게이트가 참이면 진행 중이던 작업이
|
||||
// 일시정지되고 사용자의 지리 질문은 답 대신 작업제어 응답으로 하이재킹된다.
|
||||
assert.equal(isCancelIntent('하지마 유적지는 바다와 떨어져 있는데?'), false);
|
||||
assert.equal(isCancelIntent('그만해도 되나 궁금한데 계속 할까?'), false);
|
||||
});
|
||||
|
||||
test('isCancelIntent: 진짜 취소 명령은 계속 잡는다', () => {
|
||||
for (const m of ['그거 하지마', '취소해', '작업 중단해', '이거 취소해줘', '그만해!', '멈춰주세요']) {
|
||||
assert.equal(isCancelIntent(m), true, m);
|
||||
}
|
||||
});
|
||||
|
||||
test('isExecutionLikeRequest: 의료용 "패치"가 실행형으로 분류되면 안 된다', () => {
|
||||
// 이 함수는 검증 강제에서 **면제**시키는 쪽이라, 오탐이면 실존 제품의 기능·가격을
|
||||
// 검색 없이 기억으로 답해도 아무도 막지 않는다.
|
||||
assert.equal(isExecutionLikeRequest('인슐린 패치는 혈당 측정 기능이 없나?'), false);
|
||||
assert.equal(isExecutionLikeRequest('패치는 많이 비싼가?'), false);
|
||||
});
|
||||
|
||||
test('isExecutionLikeRequest: 소프트웨어 패치는 계속 실행형', () => {
|
||||
for (const m of ['이거 패치해줘', '패치 적용해줘', '패치 파일 어디 있어']) {
|
||||
assert.equal(isExecutionLikeRequest(m), true, m);
|
||||
}
|
||||
});
|
||||
|
||||
test('isFactualInfoRequest: "얼마" 계열 수량·가격 질문', () => {
|
||||
// 표본에서 이 표현으로 물었는데 어떤 검증 게이트에도 안 걸린 5건 — 전부 정당한 사실 질문이었다.
|
||||
for (const m of [
|
||||
'tesla v100 32gb 메모리 대역폭은 얼마지?',
|
||||
'미국 올 해 예산은 전 부 얼마지?',
|
||||
'이자 비용은 연간 얼마지?',
|
||||
'서울에서 부산까지 거리가 얼마야?',
|
||||
'패치는 많이 비싼가?',
|
||||
]) {
|
||||
assert.equal(isFactualInfoRequest(m), true, m);
|
||||
}
|
||||
});
|
||||
|
||||
test('isFactualInfoRequest: 맨 "얼마나"는 넣지 않았다 — 비-사실 용법이 흔하다', () => {
|
||||
assert.equal(isFactualInfoRequest('얼마나 걸려?'), false);
|
||||
assert.equal(isFactualInfoRequest('시간이 얼마나 남았지'), false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 검색 결과 랭킹의 관련성 토큰 — 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), []);
|
||||
});
|
||||
});
|
||||
@@ -360,3 +360,35 @@ describe('cms_hospital_compare — 소유형태 질문에 닿는가', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 2026-09-04 도달률 감사 — 실사용 1,011건 대조에서 나온 tool-scope 오탐 2건.
|
||||
*/
|
||||
describe('게이트 오탐 (2026-09-04 감사)', () => {
|
||||
test('climate: "추세"/"장기"만으로 기후 아카이브 4종을 싣지 않는다', () => {
|
||||
// "메모리 가격 추세"가 통과해 재분석 아카이브(~820토큰)를 통째로 실었다.
|
||||
for (const m of ['메모리 가격 추세', 'GPU 가격 장기 전망']) {
|
||||
assert.equal(filterFor(m)('weather_era5'), false, m);
|
||||
}
|
||||
});
|
||||
|
||||
test('climate: 기후 맥락이 있으면 그대로 켜진다', () => {
|
||||
for (const m of ['서울 기온 장기 추세', '평년 대비 올해 강수량', '기후의 변동 소주기가 1500년쯤 되는 게 있나?']) {
|
||||
assert.equal(filterFor(m)('weather_era5'), true, m);
|
||||
}
|
||||
});
|
||||
|
||||
test('kakao: 첨부 파일명이 메신저 발송 도구를 켜면 안 된다', () => {
|
||||
// 카카오맵 캡처를 올렸을 뿐인데 kakao_send_message가 붙었다. executeTool은 스키마 소속을
|
||||
// 확인하지 않으므로, 프롬프트가 이름만 언급해도 실제로 호출될 수 있는 종류의 도구다.
|
||||
assert.equal(
|
||||
filterFor('')('kakao_send_message'),
|
||||
false,
|
||||
);
|
||||
assert.equal(filterFor('카카오맵으로 위치 보여줘')('kakao_send_message'), false);
|
||||
});
|
||||
|
||||
test('kakao: 진짜 발송 요청은 계속 켜진다', () => {
|
||||
assert.equal(filterFor('카카오톡으로 보내줘')('kakao_send_message'), true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user