v4.1.10: 웹검색/도구호출 신뢰성 대개선 — 모델별 프로필 시스템 도입

- AUTO-RECOVER 구조적 허점 수정: "라운드 0 & 도구 0회 호출"일 때만 발동하던 걸 "이 요청에 필요한 도구가 아직 안 불렸으면" 언제든 발동하도록 변경 — 엉뚱한 도구 하나만 불러도 안전장치가 통째로 무장해제되던 버그 수정
- 뉴스/날씨/스펙·가격·비교 등 팩트성 질문에 도구 호출 강제(isLiveDataRequest/isFactualInfoRequest) — 확신에 차서 지어내는 것(mistral-large-3에서 다발, 없는 "2026 파리 올림픽" 등)을 사전 넛지+사후 강제재시도로 이중 차단
- web_search/web_fetch 턴당 5회 하드캡 추가 — kimi-k2.6이 단순 뉴스요청에 최대 50회까지 검색하던 문제 차단
- 모델이 함수 호출 대신 텍스트로 tool call을 흉내내는 케이스(`web_search{...}`, 반복시 토큰 열화로 `웍웍웍_search`, 가비지 토큰 삽입 `web_search Räikk{...}`) 복구 로직을 브레이스 앞 텍스트에서 실제 도구명을 찾는 방식으로 일반화
- 뉴스 검색: 대화 이력 주제로 새는 것 방지, 국제뉴스는 영어 쿼리 사용, 위키/홈페이지뿐인 저품질 결과는 다음 provider(tavily 등)로 폴백, 기사 실제 게재일 라벨링(오늘 날짜로 잘못 표기 금지)
- email_send/kakao_send_message 실제 호출 없이 "보냈습니다"라고 답하면 강제 재시도
- browser_open: image_edit와 동일하게 명시 요청 없으면 코드 레벨 차단(기존엔 프롬프트 텍스트뿐)
- config.json models.profiles로 모델별 시스템프롬프트 추가지침 주입 가능(코드 재배포 없이 모델별 이상행동 교정)
- ollama-client 모델 fallback 재시도를 quota 초과 외 retired/deprecated 에러까지 확장, models.fallback 값 유지
- 각종 인텐트 판별 함수(브라우저/데스크톱/실행형 요청, 백그라운드 작업 재개/취소/상태질문)에 한국어 키워드 커버리지 추가
- shell 툴 스키마 예시와 PACKAGE INSTALL 프롬프트 규칙의 모순(pip install 예시 vs 금지 규칙) 제거

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-07-16 12:58:57 +09:00
co-authored by Claude Sonnet 5
parent 2228aedbf7
commit a5fb92d477
4 changed files with 283 additions and 39 deletions
+34 -2
View File
@@ -795,6 +795,30 @@ async function searchOllama(query: string, limit: number, endpoint: string, mode
};
}
// A provider returning ANY result (even one) short-circuits the fallback chain below —
// which is right for most queries, but for news/headline queries a "result" that's actually
// just a bare homepage or a Wikipedia/Namu year-overview page isn't real news: it stops the
// chain from ever reaching Tavily/Google, which might have had actual headlines. Downgrade
// such results to "empty" (still kept as bestEmpty last-resort) so the chain keeps going —
// but only for queries that are actually asking for news, so a legitimate "무슨 사이트야" /
// "give me the Reuters homepage" style query isn't penalized for getting a homepage back.
function isNewsSeekingQuery(query: string): boolean {
return /(뉴스|헤드라인|속보)/.test(query) || /\b(news|headlines?)\b/i.test(query);
}
function isLowValueNewsResult(url: string): boolean {
try {
const u = new URL(url);
const path = u.pathname.replace(/\/+$/, '');
if (!path) return true; // bare homepage, e.g. https://www.reuters.com/
if (/^\/wiki\/\d{4}$/i.test(path) && /wikipedia\.org$/i.test(u.hostname)) return true; // year-overview page
if (/^\/w\/\d{4}$/i.test(path) && /namu\.wiki$/i.test(u.hostname)) return true;
return false;
} catch {
return false;
}
}
// ── Main web_search tool ──────────────────────────────────────────────────────
export async function executeWebSearch(args: { query: string; max_results?: number }): Promise<ToolResult> {
if (!args.query?.trim()) return { success: false, error: 'query is required' };
@@ -860,7 +884,11 @@ export async function executeWebSearch(args: { query: string; max_results?: numb
const res = await runProvider(provider);
await augmentEventContract(args.query, res);
const resultCount = Array.isArray(res.data?.results) ? res.data.results.length : 0;
const hasResults = res.success && resultCount > 0;
let hasResults = res.success && resultCount > 0;
if (hasResults && isNewsSeekingQuery(args.query)) {
const results = res.data!.results as SearchResultItem[];
if (results.every((r) => isLowValueNewsResult(r.url))) hasResults = false;
}
diagnostics.attempted.push({
provider,
@@ -895,7 +923,11 @@ export async function executeWebSearch(args: { query: string; max_results?: numb
try {
const res = await searchDDGHtml(args.query, limit);
const resultCount = Array.isArray(res.data?.results) ? res.data.results.length : 0;
const hasResults = res.success && resultCount > 0;
let hasResults = res.success && resultCount > 0;
if (hasResults && isNewsSeekingQuery(args.query)) {
const results = res.data!.results as SearchResultItem[];
if (results.every((r) => isLowValueNewsResult(r.url))) hasResults = false;
}
diagnostics.attempted.push({
provider: 'ddg_html',
status: hasResults ? 'success' : (res.success ? 'empty' : 'failed'),