POST /api/chat 保留阶段、心跳、公司确认、来源、卡片及最终结果事件。新增正文流事件,后端以 stream=true 读取模型上游 SSE,收到公开正文片段就推送,无需等完整 JSON 或整个节点结束。不是完整回答后的逐字播放。上游一个片段可能包含一个或多个 token,不保证逐汉字、逐词或严格 tokenizer 边界。
沿用外层 {thread_id, request_id, data}。以下字段位于 data,以一次请求的 request_id 和 stream_id 定位正文块。
| 事件 | 字段及含义 | 展示动作 |
|---|---|---|
answer_start |
stream_id 本次正文生成ID;target 为 summary 或 answer;sequence=0;provisional=true |
新建或清空对应的生成中正文块 |
answer_delta |
同一ID/target;sequence 从1递增;text 为新增字符串 |
按顺序追加text,立即渲染 |
answer_end |
同一ID/target;sequence为最后片段序号;text 为完整最终正文;operation=replace;status=completed/fallback;provisional=false |
整块替换,不要追加 |
answer_abort |
同一ID/target;sequence为最后片段序号;operation=discard;reason 为 generation_failed/regenerating/not_used/not_finalized |
撤去这次临时正文,等待后续正常回答或失败提示 |
answer_end 后仍保留原 answer 或 summary 完整事件,并新增可选 stream_id 与该正文关联;这些完整事件用于兼容旧客户端及提供来源等字段,不是另一份正文。最终 result 结构不变,不保存流式ID;断线恢复、历史渲染以完整结果为准。不要把 answer_delta 当成完整 answer。
event: answer_start
data: {"thread_id":"demo","request_id":"req","data":{"stream_id":"s1","target":"summary","sequence":0,"provisional":true}}
event: answer_delta
data: {"thread_id":"demo","request_id":"req","data":{"stream_id":"s1","target":"summary","sequence":1,"text":"我们青浦"}}
event: answer_delta
data: {"thread_id":"demo","request_id":"req","data":{"stream_id":"s1","target":"summary","sequence":2,"text":"的合成服务说明。"}}
event: answer_end
data: {"thread_id":"demo","request_id":"req","data":{"stream_id":"s1","target":"summary","sequence":2,"text":"我们青浦的合成服务说明。","operation":"replace","status":"completed","provisional":false}}
前端使用 POST fetch 的 response.body 持续读取;不要先 await response.text() 再解析。使用流式 TextDecoder 处理跨网络包UTF-8,将未组成完整SSE事件的字符保留到下个包;一个网络包不等于一个事件。完成一个空行分隔事件后,解析外层JSON并按事件名处理。
同一个请求的事件分发建议:
answer_start 建立临时正文,记录ID、target及最后序号0。answer_delta 只接受当前ID,忽略已消费序号,按顺序拼接text;序号缺口标记正文不完整,等最终替换。Markdown可按每帧刷新,避免每个片段重排整页;不要把模型正文当原始HTML插入。answer_end 无论状态是completed还是fallback,均用text覆盖已有内容并解除生成中状态。最终校验或程序补充可能改变草稿,不能只停止光标而保留草稿。answer_abort 丢弃当前ID临时内容。后续原answer/summary照常展示;没有流ID的完整事件也要支持。result 校准最终展示,沿用原answer/summary去重规则;error、连接异常或没有正常终止的EOF须撤去尚未完成草稿,不能显示为成功答复。新客户端兼容无新事件的旧后端;旧客户端忽略新事件仍能拿到完整回答,但没有增量展示效果。外部前端不在本仓库,需按本契约接入。
text;闲聊:已确认 conversation_kind=smalltalk 后才提取 direct_response。识别提示要求类型先输出;若模型未遵循字段顺序,保守保持完整答复,不提前猜类型。后端重启加载代码。已有8事件有界队列提供背压,断连/超时通知模型读取循环停止;阻塞的网络读仍可能等至读取超时,不提前释放仍运行的工作线程和会话锁。生产反向代理需关闭SSE缓冲;如直连已逐段而代理整段返回,应排查代理缓冲及压缩。未部署或验证外部代理/前端。
本轮210项问答、53项API合成测试通过,包括真实JsonModel/GraphRuntime到Service SSE消费者的完成前增量检查、跨片段转义、错误撤回、最终替换、并发隔离,以及既有背压与断连回归。
纯合成真实模型验证:.venv\Scripts\python.exe -m src.step3_question_answer.token_stream_check,统计记录 output/checks/token-stream.json,只保存耗时/片段数/断言。2026-09-20政策、知识、闲聊三路径首片段分别约0.58/0.67/0.74秒,模型完成约0.97/1.03/1.65秒;这是直接模型调用的合成样例,不含真实检索、代理或用户网络耗时,不能作为生产延迟承诺。
上游协议参照DeepSeek Chat Completion的delta.content、finish_reason与[DONE],不转发推理字段。