fix(agents): graceful recovery from structured output validation errors
## Problem SDK's Zod validation for `output_config` occasionally fails with: "Failed to parse structured output: invalid_value at path [intent]" This crashes the entire response — user sees nothing despite model generating a valid answer. ## Root Cause The Anthropic SDK validates streamed structured output against the Zod schema (CoordinatorResponseSchema) after streaming completes. When the model returns an intent value not in the z.enum() (rare but happens), the SDK throws during stream iteration or finalMessage(). ## Fix 1. Catch "Failed to parse structured output" errors in both: - Stream iteration catch block (for-await loop) - stream.finalMessage() catch block 2. Recover by extracting accumulated text from assistantBlocks 3. Manual JSON.parse (skips Zod validation — intent enum mismatch doesn't affect user-facing content) 4. Yield parsed.answer + parsed.followUp normally ## Also Included (from previous commit) - Removed INTENT_MAX_ANSWER_LENGTH hard truncation (弊大于利) - Only 2000-char safety net remains for extreme edge cases - followUp: non-question content always appended (prevents content loss) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
db8617dda8
commit
7af8c4d8de
|
|
@ -288,6 +288,43 @@ export async function* agentLoop(
|
|||
}
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// ---- Structured Output 验证失败恢复 ----
|
||||
// SDK 的 Zod 验证可能失败(如 intent 枚举值不匹配),
|
||||
// 但模型的 answer 文本通常是正确的。从已累积的 blocks 中提取内容。
|
||||
if (errMsg.includes('Failed to parse structured output') && assistantBlocks.length > 0) {
|
||||
logger.warn(`Structured output validation failed, recovering from accumulated text`);
|
||||
const accumulatedText = assistantBlocks
|
||||
.filter(b => b.type === 'text' && 'text' in b)
|
||||
.map(b => (b as any).text)
|
||||
.join('');
|
||||
|
||||
if (accumulatedText) {
|
||||
try {
|
||||
// 手动 JSON.parse 跳过 Zod 验证 — intent 值不影响用户看到的内容
|
||||
const parsed = JSON.parse(accumulatedText);
|
||||
if (parsed.answer) {
|
||||
yield { type: 'text', content: parsed.answer, timestamp: Date.now() };
|
||||
if (parsed.followUp) {
|
||||
if (/?|\?/.test(parsed.followUp)) {
|
||||
yield { type: 'text', content: '\n\n' + parsed.followUp, timestamp: Date.now() };
|
||||
} else {
|
||||
yield { type: 'text', content: parsed.followUp, timestamp: Date.now() };
|
||||
}
|
||||
}
|
||||
logger.debug(`[Turn ${currentTurn + 1}] Recovered intent: ${parsed.intent} (validation bypassed)`);
|
||||
} else {
|
||||
yield { type: 'text', content: accumulatedText, timestamp: Date.now() };
|
||||
}
|
||||
} catch {
|
||||
// JSON 也解析不了 → 原始文本兜底
|
||||
yield { type: 'text', content: accumulatedText, timestamp: Date.now() };
|
||||
}
|
||||
}
|
||||
return; // 流已结束,无法继续正常流程
|
||||
}
|
||||
|
||||
// 其他流错误 → 向用户报错
|
||||
logger.error(`Stream processing error: ${errMsg}`);
|
||||
yield {
|
||||
type: 'error',
|
||||
|
|
@ -304,6 +341,37 @@ export async function* agentLoop(
|
|||
finalMessage = await stream.finalMessage();
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// finalMessage 也可能因 structured output 验证失败而抛错
|
||||
if (errMsg.includes('Failed to parse structured output') && assistantBlocks.length > 0) {
|
||||
logger.warn(`finalMessage structured output validation failed, recovering`);
|
||||
const accumulatedText = assistantBlocks
|
||||
.filter(b => b.type === 'text' && 'text' in b)
|
||||
.map(b => (b as any).text)
|
||||
.join('');
|
||||
|
||||
if (accumulatedText) {
|
||||
try {
|
||||
const parsed = JSON.parse(accumulatedText);
|
||||
if (parsed.answer) {
|
||||
yield { type: 'text', content: parsed.answer, timestamp: Date.now() };
|
||||
if (parsed.followUp) {
|
||||
if (/?|\?/.test(parsed.followUp)) {
|
||||
yield { type: 'text', content: '\n\n' + parsed.followUp, timestamp: Date.now() };
|
||||
} else {
|
||||
yield { type: 'text', content: parsed.followUp, timestamp: Date.now() };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
yield { type: 'text', content: accumulatedText, timestamp: Date.now() };
|
||||
}
|
||||
} catch {
|
||||
yield { type: 'text', content: accumulatedText, timestamp: Date.now() };
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(`Failed to get final message: ${errMsg}`);
|
||||
yield {
|
||||
type: 'error',
|
||||
|
|
|
|||
Loading…
Reference in New Issue