对话流式传输与「思考区」调试记录

Link copied!

AG-UI SSE THINKING/TEXT 分流、NoSQL 会话读写、确认流图拓扑、SSE payload 补丁等 11 个子问题

于成季 by 于成季 ·

背景

神秘人聊天对接 AG-UI SSE:推理应走 THINKING_* 事件,用户可见正文走 TEXT_MESSAGE_*。端上需要灰色可折叠思考区 + 真流式(RN 用 react-native-sse)。实际迭代中问题分几层暴露出来。

方案演进

方案一:只调 UI 状态

做法:收束推理时用 thinkingExpanded: false,首轮正文一到就把思考体收起。

问题:看起来像「思考一出来就变纯黑、收起条没了」——其实是思考体被折叠后视觉焦点全在正文,兼有多通道重复时更像「整段黑字」。

调整:首段正文到达时先保持 thinkingExpanded: true,整轮结束再默认收起。

方案二:客户端启发式去重

做法:流式中若 content === thinkingContent,不渲染那段 Markdown,避免双份黑字 + 灰字。

问题:依赖字符串全等,后端若「前缀相同后接答案」或空白不一致就失效;属于协议不清时的补丁。

方案三:前端状态机收敛(reducer)

做法:抽出 assistant-stream-reducer.ts + agent-chat-types.tsonThinkingDelta / onChunk / 收尾 / 错误只做归约。

未解决:若模型根本不吐 THINKING_*,只把围栏写进正文通道,reducer 再漂亮也拿不到「真推理事件」。

方案四:正文内围栏的客户端剥离

做法:leaked-reasoning-in-content.ts 解析 `thinking` 成对哨兵、代码块、XML、未闭合尾段等,合并进 thinkingContent

问题:能救 UI,但本质是 C 端擦屁股;若在系统提示里禁止模型写围栏,则是回避问题。

方案五(当前):服务端按 AG-UI 真分流

做法:

结果:模型仍可按习惯输出带围栏的推理,SSE 上会出现标准 THINKING 事件;端上 onThinkingDelta / onChunk 各司其职。

小结

阶段核心局限
UI 布尔展开/收起时机无法解决「推理不在正确通道」
字符串去重少一块黑字脆弱
Reducer + 库表状态与持久化清晰不替代协议分流
客户端剥围栏救显示和旧数据不是「解决问题」的终点
服务端改事件协议层对齐 AG-UI依赖部署与环境一致

教训

先弄清「黑字」是样式 bug、重复渲染,还是 TEXT 里混进了本该走 THINKING 的内容;最后一类必须在 Emitter 侧改,而不是禁写或只做前端解析。

后续缺陷修复表

现象原因修复
思考区与正文内容对调split_fence_thinking_from_plain_text 返回顺序误解改为 pub_full, think_full = split(...)
全部进正文、无 THINKING_*模型输出 think 尖括号块,旧逻辑不认服务端 split_fence_thinking_from_stream_buffer
正文头露出 流式断包导致孤立闭标签被当 HTML 解析stripVisibleThinkCloseArtifacts + SafeMarkdownhtml: false
正文停在半截最后一块流常带 finish_reason,用其跳过则整包未进 fence不按 finish_reason 跳过含 raw 的 chunk;OnChatModelEnd 补发
← 所有文章