
SSE 流式输出跑到一半断连重连后要么丢块、要么同一段重复渲染——TaoToken 给 Claude Code 开出的这条排查通道就是拿 last-event-id 把续接和去重一起拆。先去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建一个 Key把 Base URL 填成https://taotoken.net/api别手抖多写/v1再让 Claude Code 按原文“流式处理与实时通信”第 15 题逐层接手。这套排查思路的源头是那份前端 AI 战备题库里的两道题第 15 题讲 EventSource 的last-event-id如何做中断续接第 1 题把“断线重连 消息去重”放在一起要求同步解决。很多人只做到重连没做去重于是在长文本场景里开始频繁看到“同一段代码被打字机效果回放两遍”。这篇不背题只把你手里的 Claude Code 变成一条能对照这两道题给出排查结论的通道。1. SSE 断线重连后丢块或重复渲染先分清断在哪一头1.1 丢块重连时没带 last-event-id服务端从头推丢块最典型的指纹是页面已经渲染到第 N 个 chunk网络抖动一下几秒后事件流又回来了但内容从第 1 个 chunk 重新推起只是用户视觉上看起来“从中间跳了一段”。真相是客户端重连时没有把已收到的最后事件 ID 通过Last-Event-ID请求头带回去服务端自然按照默认起点重放。EventSource 原生支持这个机制。浏览器在断线后自动重连只要服务端在前一次推送里写过id:字段浏览器就会在下一次请求里自动带上Last-Event-ID请求头。这一步不需要你写任何额外代码问题往往出在服务端没有正确写出id:或者网关层把请求头吃掉了。1.2 重复渲染客户端把“重放”当成“新事件”第二类指纹反过来内容完整不丢块但有一部分被打字机效果重放了两三遍。原因很直接客户端在 reconnect 之后无条件把每条事件都渲染一次服务端为了保险把从断点开始的若干事件又推了一遍两边叠加就是重复。去重不是简单地拿event.data做 Set。长文本场景里多个 chunk 内容可能碰巧相同比如连续的空格、换行或者}只按内容去重会丢掉合法片段。稳妥做法是把“事件 ID 文本片段”拼成指纹例如seq加内容哈希这样既不误删也不漏削。2. 让 Claude Code 接管排查settings.json 里把 base URL 指向 TaoToken2.1 从落地页创建 Key模型 ID 以模型广场当时的列表为准在动手改配置文件之前先打开 TaoToken 官网 完成注册进控制台创建一把 API Key看到YOUR_API_KEY里那串字符就说明这一步成了。同一页面的模型广场列着当前可用的模型 ID配置里要填的YOUR_MODEL_ID直接抄那里的名字别凭记忆写带日期的后缀。Key 创建完之后不要关页面顺手把 Base URL 记在心里填进工具里的固定写法是https://taotoken.net/api末尾不带/v1后面几处配置都会用到它。官网链接和接口地址是两回事别混。2.2 用~/.claude/settings.json的 env 段落一次配好三件套Claude Code 的配置文件放在~/.claude/settings.json里面有一个env字段专门用来注入模型通道相关的环境变量。三段值各司其职ANTHROPIC_BASE_URL指向https://taotoken.net/apiANTHROPIC_AUTH_TOKEN填YOUR_API_KEYANTHROPIC_MODEL填模型广场里抄下来的 ID。{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY, ANTHROPIC_MODEL: YOUR_MODEL_ID } }写完保存重启 Claude Code让它用一把临时 Key 发一条测试消息能正常回一句话就说明通道已经通了。如果你更习惯用 shell 变量而不用 settings.json也可以在启动前 export 同样的三项两者不要同时写避免互相覆盖以后排错困难。2.3 为什么不让 Claude Code 去“直连”你的 SSE 服务这里有一个容易走偏的方向让 Claude Code 自己去连你的生产 SSE 服务抓流然后实时诊断。这个思路在本篇里不成立也不安全。它应该承担的角色是根据你贴过来的症状、日志片段和客户端代码给出对照原文第 15 题的排查结论与改写建议。真正建立连接、模拟抖动、观察Last-Event-ID请求头的动作全都由你在本地跑。3. 沿着 last-event-id 把“断线→续接→去重”三层拆开3.1 断线层EventSource 自动重连但不会自动帮你续EventSource 在连接断开后默认大约 3 秒会重试这个行为由服务端返回的retry:字段控制。断开的原因可能是网络抖动也可能是服务端主动把连接关了比如推理任务超时、网关 idle 超时、或者反代有连接数上限。前一种你只能等后一种需要在服务端把超时参数调大或者换成更短的心跳事件来保活。排查断线时最有用的两个观察点一是浏览器的 Network 面板能看到 EventSource 那条请求的Pending状态变成Failed再变回Pending说明重连已经在发生二是请求头里能看到Last-Event-ID说明浏览器确实把断点信息带回来了。这两个都在说明断线层没问题问题在续接层。3.2 续接层服务端必须认id:字段并正确读Last-Event-ID续接能不能真的生效取决于服务端。它在推送每条事件时写id: 42重连请求过来后再读Last-Event-ID: 42决定从哪一段继续推。如果服务端只会从零开始推或者把id:字段漏写了客户端就算把请求头带回来也没用。一个容易忽略的细节Last-Event-ID请求头的值是字符串需要显式parseInt或按你自己定义的 ID 格式解析。跳过这一步直接当数字用某些语言的隐式比较会出错表现就是“看起来拿到了请求头但续接位置永远是从 0 开始”。3.3 去重层指纹要同时包含事件 ID 和内容签名去重层放在客户端用一个Set记录已经渲染过的事件指纹。指纹最简单的形式是${lastEventId}-${hash(chunk)}或者更直接一点用服务端下发的事件自增 ID。不要只对内容字符串做去重长文本里重复内容太常见了会误伤合法 chunk。还有一个必须同时处理的动作渲染重放。同一指纹再次出现时不要只是“跳过渲染”最好把它和一个状态标记一起处理让 UI 有明确的“已确认收到忽略重复”路径。否则留给用户的就是打字机上“同一行被擦掉再写一遍”的怪异体验。4. 401 和 /v1Base URL 多一段就报错4.1 多写/v1会直接 401很多从别的 SDK 迁过来的开发者会下意识把https://taotoken.net/api拼成https://taotoken.net/api/v1觉得“OpenAI 都是这么写的”。这在 TaoToken 的通道上会把请求落到错误的路径前缀鉴权层拿到的路径不匹配直接返回 401。第一次看到这个报错先别怀疑 Key 有问题先回头确认ANTHROPIC_BASE_URL是不是多了/v1。4.2 环境变量拼错或没生效第二类 401 更隐蔽。settings.json 里字段名写对了值也填了但字段名和 Claude Code 期望的不一致例如把ANTHROPIC_AUTH_TOKEN误写成ANTHROPIC_API_KEY。Claude Code 不会用另一套变量兜底它只认约定的那一个读不到就当匿名请求结果也是 401。排查方法很简单临时在终端里env | grep ANTHROPIC看三项变量到底有没有被读到值是不是和 settings.json 一致。命令输出里ANTHROPIC_BASE_URL结尾多了一截/v1、或者ANTHROPIC_AUTH_TOKEN整条不存在就是问题所在。5. 本地验一遍续接与去重再让 Claude Code 给结论5.1 用一段最小 SSE 服务做抖动场景验证续接和去重最省事的办法是在本地起一个能被打断的 SSE 服务。下面这段 Node 脚本只在内存里维护一个自增 ID收到Last-Event-ID就从那个位置继续推id:字段一定写在前面。const http require(http); http.createServer((req, res) { res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }); let cursor parseInt(req.headers[last-event-id] || 0, 10); const total 30; const timer setInterval(() { if (cursor total) { res.write(event: done\ndata: [DONE]\n\n); clearInterval(timer); return res.end(); } cursor 1; res.write(id: ${cursor}\n); res.write(data: {seq: ${cursor}, text: chunk-${cursor}}\n\n); }, 200); req.on(close, () clearInterval(timer)); }).listen(3210);把这段存成sse-demo.jsnode sse-demo.js跑起来。断线模拟可以用两个curl交替请求第一次拿到一部分 chunk 之后按 CtrlC 断开第二次用curl -H Last-Event-ID: 10 http://localhost:3210再请求一次观察服务端是不是从 11 开始推。5.2 把结果贴回 Claude Code让它对照原文第 15 题给结论本地跑完一轮之后把关键输出整理好贴给 Claude Code第一次断在第几号 chunk、第二次请求的Last-Event-ID是什么值、服务端返回的第一条事件 ID 是不是恰好等于“断点 1”。这三条信息对照原文第 15 题“用 EventSource 的 last-event-id 实现 AI 生成中断后的续接”就能一眼看出问题是不是出在服务端未读取请求头上。如果续接是通的继续把去重指纹的写法贴给它。让它按原文第 1 题“断线重连 消息去重”的框架逐条比对你的指纹构造有没有覆盖“事件 ID 组合内容哈希”这两个维度、重复事件走的是“跳过渲染”还是“重放渲染”、有没有记录上一次已渲染指纹。对照完它一般会给出一个改写建议改完再本地跑一次就能验证。6. 配通之后回控制台看这次调用通道配通之后先用同一把 Key 在 TaoToken 模型对话 里发一条测试消息确认 Base URL 和模型 ID 都对得上返回正常再回到 Claude Code 里继续排查。长期拿它做前端流式代码的排查与改写可以到 Coding Plan 对照套餐是否够用Key 的创建与轮换都在 控制台 API Keys 里完成。Claude Code 三件套环境变量的官方对照说明在 Claude Code 接入文档配置字段拿不准的时候直接翻这一页比凭记忆拼参数省事。最后回控制台看一眼这次调用有没有记上账模型广场、日志、用量三处对得上说明整条链路——从创建 Key 到 settings.json从 last-event-id 到去重指纹——都跑在了你希望它跑的形态里。下一次再遇到长文本生成时抖了一下没断、断了也能续回来回头看这段配置你会发现真正值钱的不是那三行 env而是你顺着 last-event-id 把“断线—续接—去重”三层彻底拆清楚的那个过程。