v4.3.28: 날씨 답변 부실 원인 3종 수정 — 도구 라우팅 + 지오코딩 오지정
"내일 구미 날씨"가 75자 한 문장(기온+하늘상태)으로만 나오던 문제를 추적한 결과, 프롬프트가 아니라 도구 쪽에 원인이 세 개 있었음. 1) weather_search가 더 나은 도구를 밀어내고 있었음 설명에 "ALWAYS use this for weather/forecast/temperature queries"라는 절대 지시가 있어 모델이 매번 이걸 골랐는데, 이 도구는 기온 범위와 하늘 상태만 반환함(강수확률·습도·풍속 없음). 강수확률을 주는 weather_openmeteo가 있는데도 도달하지 못했음. 두 설명을 각자의 실제 능력에 맞게 정정하고 openmeteo를 PREFERRED로 표시. 2) 시스템 프롬프트가 없는 데이터를 요구하고 있었음 OUTPUT FORMAT이 "강수확률·평년대비를 서술하라"고 했으나 모델이 호출한 도구는 그 값을 주지 않음 — 지시를 따르려면 지어내는 수밖에 없는 구조였음. "받지 못했으면 지어내지도 말고 조용히 빼지도 말고 weather_openmeteo를 호출하라"로 변경. 3) ⚠ 구미가 엉뚱한 지역으로 해석되고 있었음 (가장 심각) KR_CITY_COORDS에 구미가 없어 Open-Meteo 지오코더로 넘어갔는데, 한국 내 "구미" 결과 10건 중 실제 구미시(36.12, 128.34)가 하나도 없음. 게다가 모든 항목의 population 필드가 비어 있어 "인구 최대 선택" 로직이 results[0]으로 붕괴, 강원도의 작은 마을(38.16)이 선택됐음. 위도 2도 차이 = 다른 기후대인데 에러는 전혀 나지 않았음 — 사용자는 그동안 남의 동네 날씨를 받고 있었을 수 있음. 구미 포함 중견도시 25곳의 좌표를 테이블에 직접 추가. data.go.kr 키는 vault(kma.api_key)에 암호화 저장 — 평문 커밋 없음. 배포 후 확인: 기상청 초단기실황이 구미 기온 32.2°C/습도 60%/풍향 북북서로 정상 응답, 예보 답변도 75자 → 167자로 강수확률·UV지수 포함. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -844,7 +844,12 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) {
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: 'weather_search',
|
name: 'weather_search',
|
||||||
description: 'Get current weather or 5-day forecast for any city or coordinates using OpenWeather API. ALWAYS use this for weather/forecast/temperature queries — do NOT use web_search for weather.',
|
// 2026-07-29: this used to say "ALWAYS use this for weather/forecast/temperature
|
||||||
|
// queries", which crowded out the richer sibling tools — the model reached for it every
|
||||||
|
// time and could only ever report a temperature range and a sky word, so answers came
|
||||||
|
// out as one bare sentence with no precipitation chance at all. The "not web_search"
|
||||||
|
// part is still worth keeping; the "always" part was steering to the weakest option.
|
||||||
|
description: 'Current weather or 5-day min/max forecast for any city/coordinates (OpenWeather). Returns temperature range and sky condition only — NO precipitation chance, humidity or wind. For a forecast the user will act on ("비 와?", "우산 챙겨야 해?", tomorrow/this week), prefer weather_openmeteo, which adds precipitation probability and hourly detail. Never use web_search for weather.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object', required: ['location'],
|
type: 'object', required: ['location'],
|
||||||
properties: {
|
properties: {
|
||||||
@@ -873,7 +878,7 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) {
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: 'weather_openmeteo',
|
name: 'weather_openmeteo',
|
||||||
description: 'Detailed hourly weather forecast via Open-Meteo (free, no key). Supports ECMWF/GFS models, UV index, soil temp, precipitation probability, and more for up to 16 days.',
|
description: 'PREFERRED forecast tool. Detailed weather via Open-Meteo (free, no key) — includes precipitation probability, hourly detail, UV index, ECMWF/GFS models, up to 16 days. Use this for any "will it rain / what is tomorrow like / this week" question; weather_search only carries temperature and a sky word, which is not enough to answer those.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object', required: ['location'],
|
type: 'object', required: ['location'],
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -124,5 +124,5 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string {
|
|||||||
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. 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.
|
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. 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.
|
||||||
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. A search result reporting a "closing price" or "today's headline" is not proof today is a trading/business day — cross-check it against the actual current date above, and if they conflict (e.g. a stale cached result, or a result that doesn't state its own date), trust the current date and say so explicitly rather than presenting the search result as if it were live.${imageEditRuleBlock}${mediaGenRuleBlock}${browserRuleBlock}
|
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. A search result reporting a "closing price" or "today's headline" is not proof today is a trading/business day — cross-check it against the actual current date above, and if they conflict (e.g. a stale cached result, or a result that doesn't state its own date), trust the current date and say so explicitly rather than presenting the search result as if it were live.${imageEditRuleBlock}${mediaGenRuleBlock}${browserRuleBlock}
|
||||||
CHEMISTRY NOTATION: NEVER draw molecular structures as ASCII art (H/C/#/=/\\/| characters arranged to look like a diagram) — it always renders as garbled, misaligned text. Instead: for a formula or reaction, use LaTeX inside $...$ (e.g. $\\ce{C4H10}$, $\\ce{2H2 + O2 -> 2H2O}$ — mhchem extension is loaded). For an actual 2D structure with real bond lines (rings, branches), output a \`\`\`smiles\`\`\` code block containing the SMILES string (e.g. \`\`\`smiles\\nc1ccccc1\\n\`\`\` for benzene, \`\`\`smiles\\nCC(=O)Oc1ccccc1C(=O)O\\n\`\`\` for aspirin) — the client automatically renders it as a proper 2D diagram with bond lines.${codeOutputRuleBlock}${packageInstallRuleBlock}
|
CHEMISTRY NOTATION: NEVER draw molecular structures as ASCII art (H/C/#/=/\\/| characters arranged to look like a diagram) — it always renders as garbled, misaligned text. Instead: for a formula or reaction, use LaTeX inside $...$ (e.g. $\\ce{C4H10}$, $\\ce{2H2 + O2 -> 2H2O}$ — mhchem extension is loaded). For an actual 2D structure with real bond lines (rings, branches), output a \`\`\`smiles\`\`\` code block containing the SMILES string (e.g. \`\`\`smiles\\nc1ccccc1\\n\`\`\` for benzene, \`\`\`smiles\\nCC(=O)Oc1ccccc1C(=O)O\\n\`\`\` for aspirin) — the client automatically renders it as a proper 2D diagram with bond lines.${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. 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. For weather: do NOT force a table — explain current conditions and forecast in natural, fuller sentences (trend, precipitation chance, notable changes vs yesterday/normal), not just bare numbers.${modelProfileBlock}${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsCtx}`;
|
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. 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. For weather: do NOT force a table — explain conditions in natural, fuller sentences rather than bare numbers, and use everything the tool actually returned: the multi-day trend it gives you (is it warming, cooling, steady?), precipitation chance, and anything notable. If the tool you called did not return precipitation chance, do NOT invent one and do NOT quietly skip it — call weather_openmeteo, which provides it. Never pad a weather answer with figures no tool gave you.${modelProfileBlock}${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsCtx}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,39 @@ const KR_CITY_COORDS: Record<string, [number, number]> = {
|
|||||||
'평택': [36.9921, 127.1128], 'pyeongtaek': [36.9921, 127.1128],
|
'평택': [36.9921, 127.1128], 'pyeongtaek': [36.9921, 127.1128],
|
||||||
'안산': [37.3219, 126.8310], 'ansan': [37.3219, 126.8310],
|
'안산': [37.3219, 126.8310], 'ansan': [37.3219, 126.8310],
|
||||||
'용인': [37.2411, 127.1776], 'yongin': [37.2411, 127.1776],
|
'용인': [37.2411, 127.1776], 'yongin': [37.2411, 127.1776],
|
||||||
|
// 2026-07-29: added after "내일 구미 날씨" silently resolved to the WRONG place. Open-Meteo's
|
||||||
|
// geocoder returns ten 구미 hits for Korea and the real 구미시 (36.12, 128.34) is not among
|
||||||
|
// them — the list is dominated by tiny hamlets, and because every entry comes back with an
|
||||||
|
// empty population field the "pick the most populous" tiebreak degrades to results[0], a
|
||||||
|
// village in 강원도 at 38.16 — two degrees of latitude away, i.e. a different climate.
|
||||||
|
// No error was ever raised; the user just got someone else's weather.
|
||||||
|
// Any mid-size Korean city the geocoder cannot pin down belongs in this table.
|
||||||
|
'구미': [36.1195, 128.3446], 'gumi': [36.1195, 128.3446],
|
||||||
|
'김해': [35.2285, 128.8894], 'gimhae': [35.2285, 128.8894],
|
||||||
|
'안양': [37.3943, 126.9568], 'anyang': [37.3943, 126.9568],
|
||||||
|
'부천': [37.5034, 126.7660], 'bucheon': [37.5034, 126.7660],
|
||||||
|
'남양주': [37.6360, 127.2165], 'namyangju': [37.6360, 127.2165],
|
||||||
|
'화성': [37.1996, 126.8310], 'hwaseong': [37.1996, 126.8310],
|
||||||
|
'시흥': [37.3800, 126.8029], 'siheung': [37.3800, 126.8029],
|
||||||
|
'파주': [37.7599, 126.7800], 'paju': [37.7599, 126.7800],
|
||||||
|
'의정부': [37.7381, 127.0337], 'uijeongbu': [37.7381, 127.0337],
|
||||||
|
'김포': [37.6152, 126.7156], 'gimpo': [37.6152, 126.7156],
|
||||||
|
'광명': [37.4786, 126.8646], 'gwangmyeong': [37.4786, 126.8646],
|
||||||
|
'군포': [37.3617, 126.9352], 'gunpo': [37.3617, 126.9352],
|
||||||
|
'이천': [37.2723, 127.4350], 'icheon': [37.2723, 127.4350],
|
||||||
|
'양산': [35.3350, 129.0372], 'yangsan': [35.3350, 129.0372],
|
||||||
|
'거제': [34.8806, 128.6212], 'geoje': [34.8806, 128.6212],
|
||||||
|
'통영': [34.8544, 128.4331], 'tongyeong': [34.8544, 128.4331],
|
||||||
|
'경주': [35.8562, 129.2247], 'gyeongju': [35.8562, 129.2247],
|
||||||
|
'안동': [36.5684, 128.7294], 'andong': [36.5684, 128.7294],
|
||||||
|
'아산': [36.7898, 127.0018], 'asan': [36.7898, 127.0018],
|
||||||
|
'충주': [36.9910, 127.9259], 'chungju': [36.9910, 127.9259],
|
||||||
|
'원주': [37.3422, 127.9202], 'wonju': [37.3422, 127.9202],
|
||||||
|
'군산': [35.9676, 126.7369], 'gunsan': [35.9676, 126.7369],
|
||||||
|
'익산': [35.9483, 126.9576], 'iksan': [35.9483, 126.9576],
|
||||||
|
'순천': [34.9506, 127.4872], 'suncheon': [34.9506, 127.4872],
|
||||||
|
'광양': [34.9407, 127.6959], 'gwangyang': [34.9407, 127.6959],
|
||||||
|
'서귀포': [33.2541, 126.5601], 'seogwipo': [33.2541, 126.5601],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Same-named cities across countries (e.g. "Rome" → Rome, Italy vs Rome, Georgia US)
|
// Same-named cities across countries (e.g. "Rome" → Rome, Italy vs Rome, Georgia US)
|
||||||
|
|||||||
Reference in New Issue
Block a user