oh-my-muse/design-docs/临时-01-AI流SSE长任务超时修复方案.html
lili 09c35f5eee docs(ai-e2e): AI 流 SSE 长任务超时修复方案评审文档(待 review、不改 code)
studio e2e ai-generation 真红深诊出 SSE 协议/时序 code bug,按规范(产品行为变更先评审)出方案评审文档供 human review。两层根因:①server 启动漏 source acceptance env(AI 凭据)②后端 DEFAULT_TIMEOUT_MILLIS=30s 同时作 SSE 连接死线(:60)+poll deadline(:188)、< 上游自己 180s 总窗口(MUSE_AI_NEW_API_TOTAL_TIMEOUT_SECONDS),前端 connectAIStream 无重连(AIPanel 流结束不重连不报错)。

推荐 A1(启动脚本固化 AI 凭据)+候选③(后端放宽死线兜底+前端断连重连容错),排期紧分阶段 A1→①→②。3 决策点待 review:timeout 取值(应 ≥180s+余量、非初稿 120s)/是否上前端重连(推翻 AI 流一次性设计)/重连续传幂等候选去重。text+2 mermaid(因果时序+选项关系)+html 人读图;大纲注册 v7→v8。本轮未改 code、待 review 后执行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:18:21 -07:00

184 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>临时-01 · AI 流 SSE 长任务候选丢失修复方案(人读图)</title>
<style>
:root{
--bg:#0f1419; --panel:#1a2129; --ink:#e6edf3; --sub:#9fb0bf; --line:#2b3641;
--green:#3fb950; --green-bg:#11331c; --red:#f85149; --red-bg:#3a1718;
--blue:#58a6ff; --blue-bg:#10243e; --amber:#d29922; --amber-bg:#3a2c0a;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
line-height:1.6;padding:32px 20px 80px}
.wrap{max-width:1080px;margin:0 auto}
h1{font-size:24px;margin:0 0 6px;letter-spacing:.3px}
.meta{color:var(--sub);font-size:13px;margin-bottom:24px}
.tldr{background:linear-gradient(135deg,#10243e,#142e1c);border:1px solid var(--line);
border-radius:12px;padding:18px 22px;margin-bottom:28px;font-size:15px}
.tldr b{color:var(--blue)}
h2{font-size:17px;margin:34px 0 14px;padding-left:12px;border-left:4px solid var(--blue)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:760px){.grid2{grid-template-columns:1fr}}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px}
.card h3{margin:0 0 8px;font-size:15px}
.card.layer1 h3{color:var(--amber)} .card.layer2 h3{color:var(--red)}
.card p{margin:6px 0;font-size:13.5px;color:var(--sub)}
.tag{display:inline-block;font-size:11px;padding:2px 9px;border-radius:20px;margin-bottom:8px;font-weight:600}
.tag.fixed{background:var(--amber-bg);color:var(--amber);border:1px solid var(--amber)}
.tag.bug{background:var(--red-bg);color:var(--red);border:1px solid var(--red)}
/* 时序对比 */
.flow{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:6px}
@media(max-width:760px){.flow{grid-template-columns:1fr}}
.path{border-radius:12px;padding:16px;border:1px solid var(--line)}
.path.ok{background:var(--green-bg);border-color:#1f6f33}
.path.bad{background:var(--red-bg);border-color:#7d2a2a}
.path .ph{font-weight:700;font-size:14px;margin-bottom:10px;display:flex;align-items:center;gap:8px}
.path.ok .ph{color:var(--green)} .path.bad .ph{color:var(--red)}
.step{font-size:13px;padding:7px 0;border-top:1px dashed rgba(255,255,255,.08);color:var(--ink)}
.step:first-of-type{border-top:none}
.step .t{display:inline-block;min-width:52px;color:var(--sub);font-variant-numeric:tabular-nums}
.verdict{margin-top:10px;font-weight:700;font-size:13.5px}
.path.ok .verdict{color:var(--green)} .path.bad .verdict{color:var(--red)}
/* 表格 */
table{width:100%;border-collapse:collapse;margin-top:6px;font-size:13px;background:var(--panel);
border-radius:12px;overflow:hidden}
th,td{padding:11px 13px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
th{background:#222c36;color:var(--ink);font-weight:600;font-size:12.5px}
td:first-child{font-weight:600;white-space:nowrap}
.pill{display:inline-block;font-size:11px;padding:1px 8px;border-radius:5px;font-weight:600}
.p-hi{background:var(--green-bg);color:var(--green)}
.p-mid{background:var(--amber-bg);color:var(--amber)}
.p-lo{background:var(--red-bg);color:var(--red)}
.reco{background:var(--blue-bg);border:1px solid var(--blue);border-radius:12px;padding:18px 22px;margin-top:14px}
.reco .big{font-size:16px;font-weight:700;color:var(--blue);margin-bottom:6px}
.reco .order{font-size:13px;color:var(--sub);margin-top:8px}
ol.decide{padding-left:20px}
ol.decide li{margin:12px 0;font-size:13.5px}
ol.decide li b{color:var(--amber)}
.num{font-variant-numeric:tabular-nums;color:var(--blue);font-weight:700}
.footer{margin-top:40px;color:var(--sub);font-size:12px;border-top:1px solid var(--line);padding-top:14px}
code{background:#222c36;padding:1px 6px;border-radius:5px;font-size:12px;color:#ffd9a0}
</style>
</head>
<body>
<div class="wrap">
<h1>AI 流 SSE 长任务候选丢失 · 修复方案</h1>
<div class="meta">临时-01 · 评审版 v1 · 2026-06-25 · 不改代码,供 review 后执行 · 配套主文档 临时-01-*.md</div>
<div class="tldr">
用户长篇 AI 生成卡在「中止生成」、候选永不出现:根因是后端 SSE 在 <b>30 秒硬死线</b>到点后
<b>静默关连接、不补发任何终态事件</b>,而前端 AI 流是<b>一次性连接、不重连</b>。一旦大模型真实时延
(实测 1160 秒)越过 30 秒,候选就被永久丢在后端库里送不到界面。
推荐 <b>A1 启动固化 + 候选③(后端放宽死线 + 前端重连)</b>
</div>
<h2>两层根因(环境隐患 真代码缺陷,叠加才致命)</h2>
<div class="grid2">
<div class="card layer1">
<span class="tag fixed">第一层 · 已手工绕过 / 未固化</span>
<h3>环境与启动隐患</h3>
<p>启动脚本只加载基础设施凭据,<b>漏加载大模型接入凭据</b>那一份配置。</p>
<p>后端拿不到接入信息 → 装"不可用兜底"客户端 → AI 任务十几毫秒内秒失败报"接入不可用"。</p>
<p>已手工带齐全部环境变量重启验证通过,但<b>脚本没改,下次重启复发</b>,会污染验证现场。</p>
</div>
<div class="card layer2">
<span class="tag bug">第二层 · 真代码缺陷(前后端各一处)</span>
<h3>SSE 协议 / 时序缺陷</h3>
<p><b>后端:</b>连接死线与轮询上限都写死 30 秒;到点任务未完则只关连接,<b>不补 done、不补 error</b>
这 30 秒甚至短于后端等大模型的总窗口 180 秒。</p>
<p><b>前端:</b>AI 流是一次性连接、<b>无重连</b>(全局事件流却带重连)。连接被提前关后,流自然结束、上层也不补救。</p>
</div>
</div>
<h2>因果对比:同一缺陷,被大模型时延切到两侧</h2>
<div class="flow">
<div class="path ok">
<div class="ph">✓ 快路径 · 实测约 15 秒(&lt; 30s→ 碰巧绿</div>
<div class="step"><span class="t">0s</span> 发起生成,建立一次性 SSE 连接</div>
<div class="step"><span class="t">~15s</span> 大模型落 done 事件入库</div>
<div class="step"><span class="t">~15s</span> 后端轮询读到 done → 推给前端</div>
<div class="step"><span class="t"></span> 前端渲染候选</div>
<div class="verdict">候选正常出现 → accept-suggestion 用例 flaky 绿</div>
</div>
<div class="path bad">
<div class="ph">✗ 慢路径 · 实测约 36 秒(&gt; 30s→ 稳定红</div>
<div class="step"><span class="t">0s</span> 发起生成,建立一次性 SSE 连接</div>
<div class="step"><span class="t">30s</span> 死线到,任务未完 → 后端静默关连接<br><b>不补 done / 不补 error</b></div>
<div class="step"><span class="t">30s</span> 前端流自然结束 → <b>不重连</b></div>
<div class="step"><span class="t">~36s</span> 大模型才落 done晚约 8s→ 候选只进了库</div>
<div class="verdict">界面无候选也无报错,卡「中止生成」 → ai-generation 用例真红</div>
</div>
</div>
<h2>修法选项与权衡(不夸大任何一项)</h2>
<table>
<thead><tr><th>选项</th><th>做法要点</th><th>根治度</th><th>复杂度</th><th>主要代价 / 权衡</th></tr></thead>
<tbody>
<tr>
<td>第一层 A1<br>启动固化</td>
<td>脚本一并加载大模型接入凭据,缺关键变量直接报错退出</td>
<td><span class="pill p-hi">高·消除复发</span></td>
<td><span class="pill p-hi">极低</span></td>
<td>凭据成启动隐式依赖,需写明文档;<b>唯一从源头止复发,强烈建议必做</b></td>
</tr>
<tr>
<td>候选①<br>后端放宽死线</td>
<td>把 30s 死线放宽到覆盖时延上限、尽量可配;一处同时管连接与轮询</td>
<td><span class="pill p-mid">中·治标</span></td>
<td><span class="pill p-hi">最低</span></td>
<td>仍在赌大模型不更慢极端慢仍无声卡死SSE 长开占轮询线程更久</td>
</tr>
<tr>
<td>候选②<br>前端断连重连</td>
<td>非终态结束按递增退避重连续传,至 done/error/前端总时长上限止</td>
<td><span class="pill p-hi">高·根治断连</span></td>
<td><span class="pill p-lo">偏高</span></td>
<td>推翻"一次性"既有设计、协议变复杂;重连有重放开销,须保候选去重幂等</td>
</tr>
<tr>
<td>候选③<br>①+②组合</td>
<td>后端窗口吸收正常偏慢,前端重连兜住极端慢与偶发断流</td>
<td><span class="pill p-hi">最高</span></td>
<td><span class="pill p-lo">最大</span></td>
<td>前后端同改、回归面最广;但后端放足后重连频率低、重放代价随之降</td>
</tr>
</tbody>
</table>
<h2>推荐</h2>
<div class="reco">
<div class="big">✅ A1 启动固化 候选③(后端放宽死线兜底 前端重连容错)</div>
<div>缺陷本质是<b>长任务容错缺失</b>,大模型时延不可控且会漂移,单点修法治不全:
只做①是把丢候选区间后移、仍赌时延;只做②则每次长生成都靠重连去捞、徒增开销。
组合后正常偏慢由后端窗口稳稳吸收、重连只在真异常时触发,既根治又把代价压到最低,
还顺带把 AI 流收敛到与全局事件流一致的容错模型。</div>
<div class="order">排期紧时分阶段(每步独立可验可回滚):<b>A1 → 候选①(最小改快速止血)→ 候选②(补根治与一致性)</b></div>
</div>
<h2>待人类拍板的三个关键决策点</h2>
<ol class="decide">
<li><b>后端死线放到多少 / 要不要可配?</b>
时延实测 1160s上游总窗口已配 <span class="num">180s</span>。建议 SSE 死线
<b>不低于上游 180s 再加排队与回放余量</b>——初稿提的 120s 不足以覆盖上游尾部,会重演"SSE 比上游先关门"。是否做成可配项请一并定。</li>
<li><b>要不要上前端重连,即是否推翻 AI 流"一次性、不续传"的既有设计?</b>
这是复杂度与根治度的分水岭。上重连换来真容错与跨流一致,代价是协议变复杂、需改写既有设计决策并补测试;若求最小风险、接受"极端慢仍可能卡死",可暂缓②、先做①与 A1。</li>
<li><b>重连续传的幂等与候选去重边界谁保证?</b>
一旦上重连,须明确:后端重放已发生事件时前端如何凭顺序游标避免重复渲染候选、前端总时长上限取多少。否则可能把"丢候选"换成"重复候选 / 无限重试"。</li>
</ol>
<div class="footer">
锚点(仅定位,不贴实现):后端 <code>MuseAiTaskStreamServiceImpl.DEFAULT_TIMEOUT_MILLIS</code>(连接+轮询双死线);
前端 <code>sse.ts · connectAIStream</code>(一次性)对照 <code>connectEventStream</code>(带重连退避);
启动 <code>start-muse-server-infra.sh</code>;调用方 <code>AIPanel.tsx</code>
验收 <code>e2e/ai-generation.spec.ts</code><code>e2e/accept-suggestion.spec.ts</code>。回滚=<code>git checkout</code>
</div>
</div>
</body>
</html>