feat(roundtable): 第二/三步模型自动容错(出问题自动换下一个模型)

模型出问题时按 config.yaml models[] 顺序自动切到下一个模型继续,并告知用户
「『X』模型异常 → 已切换『Y』继续」。前台实时 + 后台挂起两条路径都覆盖。

- 中间件兜底/熔断 AIMessage 打 LLM_ERROR_MARKER 标记,is_llm_error_message 可识别
- roundtable_model_fallback: fallback_model_chain 候选链 + reason_text + 总开关
  env ROUNDTABLE_MODEL_FALLBACK
- 触发: 异常(start_run失败/idle卡死) / 兜底错误文案 / 交付为空(仅seat/leader)
- 后台 InProcessRoundtableGateway._run_turn_with_fallback 包住每轮 run
- 前台 _special_run/_leader_run 收尾检测失败→model_switch 帧+换模型重流
- 结构化抽取 flow_extract round1 astream 同款换模型循环
- Step2 单席位异常隔离: 单席位失败不拖垮整批/整链/整个作业
- 时间线插「系统提示」气泡 + toast + 诊断 model_switch 事件
- Tests: test_roundtable_model_fallback.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-20 21:37:45 +08:00
1 parent ae2706dad3
commit d383b0e9dd
18 files changed
+1033 -174

No files matched your search

@@ -1059,6 +1059,16 @@ export function useStep3Summary(
model: selectedModel || undefined,
files: files && files.length > 0 ? files : undefined,
signal: aborter.signal,
// 模型自动容错:后端切到下一个模型重跑 → 清空失败尝试的半截正文 + 在步骤卡留一条可见提示。
onModelSwitch: (info) => {
setDialogues((prev) => prev.map((d) => (d.id === bubbleId ? { ...d, content: "" } : d)));
setDialogueConfidence(bubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
bubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
onTextDelta: (text) => appendToDialogue(bubbleId, text),
onPhaseChange: (p, toolName, pArgs) => {
const label = summaryPhaseLabel(p, toolName);
@@ -1160,6 +1170,16 @@ export function useStep3Summary(
currentGraph: opts?.currentGraph ?? null,
instruction: opts?.instruction,
signal: aborter.signal,
// 模型自动容错:后端切到下一个模型重抽 → 清空抽取气泡里失败尝试的半截内容 + 留可见提示。
onModelSwitch: (info) => {
setDialogues((prev) => prev.map((d) => (d.id === extractBubbleId ? { ...d, content: "" } : d)));
setDialogueConfidence(extractBubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
appendStepIfNew(
extractBubbleId,
`⚠️ 模型容错:「${info.failedModel}」${info.reason},改用「${info.nextModel}」`,
`model_switch:${info.nextModel}`,
);
},
onExtractDelta: (t) => appendToDialogue(extractBubbleId, t),
onValidating: () => {
setFlowPhase("validating");