fix: weather_openmeteo가 여러 도시를 직접 전부 조회해 하나로 합침
실제 사고(2026-08-10): "오늘 유럽 대도시 최고 기온"에 모델이 5개
도시("London, Paris, Berlin, Madrid, Rome")를 한 번에 넘겨 실패
(도구별로 나눠 부르라는 안내 자체는 이미 08-09에 잘 만들어져 있었음),
그 뒤 로마·런던 2개만 재시도하고 파리·베를린·마드리드는 빠뜨렸다.
지어내진 않았지만("다른 도시 필요하시면 말씀해주세요") 불완전했다 —
같은 날 news_search/weather_kma에서 겪은 것과 같은 패턴: 프롬프트/
에러 메시지가 정확히 뭘 해야 하는지 알려줘도 여러 번의 후속 호출을
전부 완수하지는 못했다.
같은 해법 적용: 전체 문자열 지오코딩이 실패하면 배치인지 확인하고,
배치면 모델에게 재시도를 맡기는 대신 도구가 직접 각 도시를 병렬로
조회해서 구분선으로 나눈 하나의 결과로 합쳐 반환한다. 일부 도시가
실패해도 나머지는 정상 반환하고 실패한 도시만 목록에 남긴다 — 전체
호출이 부분 실패로 죽지 않는다.
부수적으로 OUTPUT FORMAT의 날씨 지침에 "여러 도시 비교 시 표만 던지지
말고 어디가 제일 덥/추운지, 특이한 도시가 있는지 코멘트를 붙일 것"을
추가 — 실제 로그에서 표 하나에 문장 하나짜리 부실한 답변이 관찰됨.
기존 배치 테스트(2026-08-09, success=false 검증)를 새 동작에 맞게
갱신(success=true + 도시별 결과 포함 검증) — 일부 실패 케이스 테스트도
추가.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+42
-14
@@ -685,18 +685,7 @@ export const weatherOpenMeteoTool = {
|
||||
// not maxima at all. Route them before the request instead.
|
||||
const { hourly: hourlyVars, daily: dailyVars } = splitOpenMeteoVariables(vars);
|
||||
|
||||
try {
|
||||
let lat: number, lon: number, displayName: string;
|
||||
const coords = parseCoords(location);
|
||||
if (coords) {
|
||||
[lat, lon] = coords;
|
||||
displayName = location;
|
||||
} else {
|
||||
const geo = await geocodeCity(location);
|
||||
if (!geo) return await locationNotFoundError(location, 'weather_openmeteo');
|
||||
({ lat, lon, displayName } = geo);
|
||||
}
|
||||
|
||||
const fetchOneCity = async (lat: number, lon: number, displayName: string) => {
|
||||
const params = new URLSearchParams({
|
||||
latitude: String(lat),
|
||||
longitude: String(lon),
|
||||
@@ -717,7 +706,6 @@ export const weatherOpenMeteoTool = {
|
||||
const res = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`, { signal: AbortSignal.timeout(15_000) });
|
||||
if (!res.ok) throw new Error(`Open-Meteo API error ${res.status}`);
|
||||
const data: any = await res.json();
|
||||
|
||||
const text = formatOpenMeteoReport({
|
||||
displayName,
|
||||
timezone: data.timezone || 'auto',
|
||||
@@ -735,7 +723,47 @@ export const weatherOpenMeteoTool = {
|
||||
return String(v);
|
||||
},
|
||||
});
|
||||
return { success: true, stdout: text, data: { location: displayName, lat, lon, raw: data } };
|
||||
return { text, lat, lon, raw: data };
|
||||
};
|
||||
|
||||
try {
|
||||
const coords = parseCoords(location);
|
||||
if (coords) {
|
||||
const [lat, lon] = coords;
|
||||
const { text, raw } = await fetchOneCity(lat, lon, location);
|
||||
return { success: true, stdout: text, data: { location, lat, lon, raw } };
|
||||
}
|
||||
|
||||
const geo = await geocodeCity(location);
|
||||
if (geo) {
|
||||
const { text, raw } = await fetchOneCity(geo.lat, geo.lon, geo.displayName);
|
||||
return { success: true, stdout: text, data: { location: geo.displayName, lat: geo.lat, lon: geo.lon, raw } };
|
||||
}
|
||||
|
||||
// 2026-08-10: the error path below already tells the model exactly how to retry
|
||||
// ("call once per city"), but a real incident showed that guidance only gets partially
|
||||
// followed — asked for 5 cities, it retried 2 and silently dropped 3 (no fabrication,
|
||||
// just incomplete: it said "let me know if you need the others" instead of finishing the
|
||||
// list it had already committed to). Rather than trust N follow-up calls to all happen,
|
||||
// fetch every city here and return one combined report — there is no follow-up to skip.
|
||||
const cities = await looksLikeMultipleLocations(location);
|
||||
if (!cities) return await locationNotFoundError(location, 'weather_openmeteo');
|
||||
|
||||
const settled = await Promise.allSettled(cities.map(async (c) => {
|
||||
const g = await geocodeCity(c);
|
||||
if (!g) throw new Error(`${c}: 위치를 찾을 수 없음`);
|
||||
const { text } = await fetchOneCity(g.lat, g.lon, g.displayName);
|
||||
return { city: c, text };
|
||||
}));
|
||||
const ok = settled.filter((r): r is PromiseFulfilledResult<{ city: string; text: string }> => r.status === 'fulfilled').map(r => r.value);
|
||||
const failed = settled
|
||||
.map((r, i) => (r.status === 'rejected' ? cities[i] : null))
|
||||
.filter((c): c is string => c !== null);
|
||||
if (!ok.length) return { success: false, error: `${cities.length}개 도시 전부 조회 실패: ${cities.join(', ')}` };
|
||||
|
||||
const combined = ok.map(r => r.text).join('\n\n' + '─'.repeat(40) + '\n\n')
|
||||
+ (failed.length ? `\n\n(다음 도시는 조회 실패: ${failed.join(', ')})` : '');
|
||||
return { success: true, stdout: combined, data: { locations: ok.map(r => r.city), failed } };
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user