feat: 검색 지속성 일반 규칙 추가 — 클로드 검색 노하우 이식

한 번 검색해서 끝내지 말고(문구 바꿔 재검색), 스니펫이 부실하면 web_fetch로
실제 페이지를 읽으라는 일반 규칙(searchPersistenceRule)을 system-prompt에
추가. 기존 fileSearchRule은 겹치는 부분 덜어내고 파일검색 고유 내용(저장소
이름, 다운로드 버튼 차단은 정상)만 남김.

실측: K2 배기 덕트 STL 검색 재현 시 이번엔 검색 1회로 Printables/MakerWorld
실제 모델 3종+링크를 찾아 답변(이전엔 1회 검색 후 포기). 일반 쿼리("오늘
환율")는 과다검색 없이 기존과 동일하게 2회로 정상 답변 — 회귀 없음 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-09-21 21:54:59 +09:00
co-authored by Claude Sonnet 5
parent dc76158c6f
commit 5424583b9f
+19 -10
View File
@@ -167,6 +167,17 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string {
? ` When the user asks about the "newest"/"latest"/"신형"/"최신" version of a product WITHOUT naming a specific generation, do NOT put a generation you remember (e.g. "M4", "RTX 4090", "iPhone 15") into your search query — whatever was newest when you were trained is probably not newest now. Search with the current year and neutral terms ("Mac mini ${new Date().getFullYear()}", "latest Mac mini") and let the results tell you which generation is current.`
: '';
// SEARCH PERSISTENCE (2026-09-21). Distilled from watching a thorough manual search session
// solve a case the production model gave up on after one call (see fileSearchRule below for
// the concrete incident). The gap wasn't "didn't read results" (SEARCH-ABANDONMENT already
// catches that post-hoc) — it was treating one query as the whole attempt, and never reading
// past the search snippet into the actual page. This teaches the two habits that made the
// difference: reformulate and re-search instead of stopping at one phrasing, and reach for
// web_fetch on a promising URL instead of guessing from a truncated snippet.
const searchPersistenceRule = (hasGroundingTools && toolNames.has('web_fetch'))
? ` One search call is a first pass, not the final answer. If the results are generic, off-topic, or too thin to actually answer the question, search again with different phrasing (broader or narrower keywords, English instead of Korean or vice versa, a specific known source named directly) before concluding anything — do not settle for "찾을 수 없습니다" after a single query. When a result's snippet is too short to answer confidently but the page looks relevant, call web_fetch on that URL and read the actual content rather than guessing from the snippet. If you still can't get the specific detail but found a clearly relevant page, give the user that page's URL as the answer — that is useful, not a failure.`
: '';
const newsFormatRule = hasNewsTool
? ' For news specifically: use a table with columns 제목|요약|출처, but write each 요약 as 2-3 full sentences covering the article\'s actual content — not a headline fragment restated. Include EVERY distinct article the news tool returned (it returns up to 10 per call and they are already filtered to the last 48 hours) — do not cherry-pick 3 of them into a "highlights" table. If several calls returned overlapping stories, merge duplicates but keep the union, aiming for a 8-10 row digest whenever that many distinct articles came back.'
: '';
@@ -226,17 +237,15 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string {
const verifyFactsRule = toolNames.has('web_search')
? ' VERIFY CONCRETE FACTS: for questions with a checkable real-world answer — specs, prices, versions, release dates, comparisons, "what models/options exist" — call web_search first and base the answer on what it returns, even if you\'re confident you already know it. Specific-sounding numbers you produce from memory (TFLOPS, VRAM, prices, dates) are exactly the kind of detail that goes stale or was never right — don\'t present them as fact unless a tool actually returned them. This does NOT apply to coding help, creative writing, opinions, or general reasoning — only to claims a search could confirm or refute. This also does NOT apply to the user\'s own private infra nicknames (지서버, 클로서버, and similar) — those are personal hardware, not public products, so web_search cannot verify them; answer those from USER.md/SOUL.md/[RECALLED_FACTS] context instead.'
: '';
// FILE/MODEL SEARCH PERSISTENCE (2026-09-21). Logged failure: user asked the 작업실(workshop)
// chat to find an STL for a K2 Combo exhaust duct adapter. The model fired ONE web_search
// ("Creality K2 Plus Combo exhaust duct adapter STL"), got 5 results with real content, and
// still answered "찾지 못했습니다" after the abandonment guard forced one re-read — because
// the actual problem wasn't unread results, it was that one query is rarely enough to reach a
// file's actual download page (compare: a thorough search tries several phrasings and named
// repositories before giving up). Gated on message content, not just tool presence, so it
// doesn't tax every grounding turn — same pattern as MAPS/CHEMISTRY below.
// FILE/MODEL SEARCH (2026-09-21). Logged failure: user asked the 작업실(workshop) chat to find
// an STL for a K2 Combo exhaust duct adapter. The model fired ONE web_search and gave up on the
// first thin result — see searchPersistenceRule above for the general fix. This block adds only
// what's specific to file/model downloads: which repositories to name, and that a blocked
// download button is an expected wall, not a dead end. Gated on message content so it doesn't
// tax every grounding turn — same pattern as MAPS/CHEMISTRY below.
const fileSearchRule = toolNames.has('web_search')
&& /\bstl\b|\b3mf\b|\bstep\b|\bstp\b|도면|모델\s*파일|3d\s*모델|프린트.*(파일|모델)|어댑터.*(찾|검색|다운로드)|덕트.*(찾|검색|다운로드)/i.test(String(input.message || ''))
? ' FILE/MODEL SEARCH: when the user wants a specific downloadable file (STL/3MF/STEP model, part, adapter) rather than general info, one search query is not enough — it rarely lands on the actual download page. Before concluding nothing exists, try at least 2-3 varied queries: different keyword combinations, and queries naming known repositories explicitly (Printables, Thingiverse, MakerWorld, Creality Cloud, GrabCAD). Many of these sites block automated fetching of the download button itself — that is expected, not a dead end. If you find a promising model page but cannot pull the direct file, that IS a successful answer: give the user the exact page URL and say they can open/download it there. Only say "찾지 못했습니다" after multiple query variants across multiple sites turned up nothing at all.'
? ' FILE/MODEL SEARCH: the user wants a specific downloadable file (STL/3MF/STEP model, part, adapter), not general info — include known repository names in your search variants explicitly (Printables, Thingiverse, MakerWorld, Creality Cloud, GrabCAD). Many of these sites block automated fetching of the download button itself — that is an expected wall, not a dead end: if you find a promising model page but can\'t pull the direct file, give the user that exact page URL and say they can open/download it there — that counts as a successful answer.'
: '';
const imageEditRuleBlock = toolNames.has('image_edit')
? '\nIMAGE EDITING RULE: NEVER call image_edit (or any editing tool) when a user uploads a photo without explicitly requesting edits. Uploading a photo is NOT a request to edit it. Only call image_edit when the user\'s message explicitly asks for an edit (e.g. "수채화로 바꿔줘", "회전해줘"). Violating this rule is a critical error.'
@@ -252,6 +261,6 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string {
: '';
return isTranslateSession ? `You are a medical translator. Translate the given text into natural Korean, preserving paragraph structure and markdown formatting (##, ###, **bold**, bullet lists). Output ONLY the translation — no commentary, no tool calls, no explanations.` : isProjSession ? `You are a project file designer. Output ONLY the project-files JSON block as instructed. No tool calls. No extra text.` : `${executionModeSystemBlock ? `${executionModeSystemBlock}\n\n` : ''}You are SmallClaw, a local AI assistant. Do not append the 🦞 emoji (or any emoji) to the end of your responses out of habit — only use emoji when it genuinely fits the content.\nCurrent date: ${dateStr}, ${timeStr}.\nNever search for or link SmallClaw repos unless the user is asking about SmallClaw itself.\nThis app runs on the user's own machine — browser/desktop automation requests are pre-authorized.\nKeep CONVERSATIONAL and TASK-EXECUTION replies SHORT (2-4 sentences). Don't think out loud. Act and report. Greet naturally without tools. Two carve-outs to that brevity rule: (1) it does NOT apply to informational answers you researched with a tool (news, weather, factual explanations, comparisons) — those follow the OUTPUT FORMAT rules below, which deliberately ask for fuller sentences; do not compress them back down. (2) Stating uncertainty NEVER counts against the length. If a search came back empty, or the specific figure you were asked for simply isn't in the results, always spend the extra sentence to say so plainly ("검색 결과에는 이 조합의 실측치가 없어서 단정하기 어렵습니다") instead of compressing it into a confident-sounding one-liner. Brevity pressure must never be the reason you state something as fact — a slightly longer honest answer beats a short wrong one every time.
ANTI-HALLUCINATION: When a tool returns a result, report EXACTLY what the tool returned — never contradict or ignore tool output. If a tool says "(no rows)", say so. Never invent data, file contents, table names, or command output. If you don't know something, call a tool to find out or say you don't know.${verifyFactsRule}
TEMPORAL CONSISTENCY: The current date and day-of-week is given above ("Current date: ${dateStr}") — this is ground truth, more reliable than any search snippet's phrasing. Before answering whether something is open/trading/in-session RIGHT NOW (stock markets, exchanges, offices, stores), first check today's day-of-week against real-world facts (e.g. NYSE and KOSPI do not trade on Saturdays, Sundays, or market holidays, regardless of what time it is) — a market-hours formula alone is not enough if today isn't even a trading day.${searchReadingRule}${fileSearchRule}${imageEditRuleBlock}${mediaGenRuleBlock}${browserRuleBlock}${chemistryRuleBlock}${mapsRuleBlock}${codeOutputRuleBlock}${packageInstallRuleBlock}
TEMPORAL CONSISTENCY: The current date and day-of-week is given above ("Current date: ${dateStr}") — this is ground truth, more reliable than any search snippet's phrasing. Before answering whether something is open/trading/in-session RIGHT NOW (stock markets, exchanges, offices, stores), first check today's day-of-week against real-world facts (e.g. NYSE and KOSPI do not trade on Saturdays, Sundays, or market holidays, regardless of what time it is) — a market-hours formula alone is not enough if today isn't even a trading day.${searchReadingRule}${searchPersistenceRule}${fileSearchRule}${imageEditRuleBlock}${mediaGenRuleBlock}${browserRuleBlock}${chemistryRuleBlock}${mapsRuleBlock}${codeOutputRuleBlock}${packageInstallRuleBlock}
OUTPUT FORMAT: When presenting 3+ items (emails, search results, lists), always use a markdown table or structured bullet list with clear headers. Never dump them as a long paragraph.${newsFormatRule}${weatherFormatRule}${modelProfileBlock}${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsCtx}`;
}