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>
184 lines
11 KiB
HTML
184 lines
11 KiB
HTML
<!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>。一旦大模型真实时延
|
||
(实测 11–60 秒)越过 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 秒(< 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 秒(> 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>
|
||
时延实测 11–60s,上游总窗口已配 <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>
|