POST /api/chat 保留阶段、心跳、公司确认、来源、卡片及最终结果事件。新增正文流事件,后端以 stream=true 读取模型上游 SSE,收到公开正文片段就推送,无需等完整 JSON 或整个节点结束。不是完整回答后的逐字播放。上游一个片段可能包含一个或多个 token,不保证逐汉字、逐词或严格 tokenizer 边界。
区域适用前提在模型首段正文前作为同一stream_id的delta发送;已校验的下一步建议/末尾补充在end前追加一个delta。正常正文仅有末尾新增时,全部delta拼接与end.text一致,end.sequence对应最后一个新增片段;程序补充是一段完整文本,不伪装逐字生成。
卡片讲解顺序为source/item → answer_start → answer_delta(前提、模型正文、末尾补充)→ answer_end → summary → 可选独立answer提示 → result/done;progress/heartbeat仍可穿插,只放状态栏。独立answer不再插在未结束的summary流中,不重复其开头。无流式正文时仍支持完整事件。
这不是保证所有delta始终等于end:来源校验失败、金融真实链接绑定或其他正文修正仍使用最终替换,生成失败仍撤回草稿。前端按request_id+stream_id定位正文、sequence顺序追加,end始终替换,带相同stream_id的summary只更新同一块;独立answer放在总结后的提示区。不要把所有含text事件都追加到一个字符串。result是快照,不能再追加成另一份回答。
反馈样例只读核对:405个delta序号连续,草稿678字符完整包含在1037字符end中,summary与end一致;额外内容为前置适用前提和末尾建议,正文结束前还出现重复前提的answer。所贴result截断,无法据此核对完整result或认定传输层异常。已用合成区域前提+部分检索失败场景验证修复,未重放真实用户资料或联调外部前端/代理;需重启后端。
有入选政策时,先完成筛选、排序、扶持短文案及条件整理,再发送完整 source、item,然后开始综合讲解的 answer_start / answer_delta。普通政策总结和政策+其他知识的联合讲解都遵循此顺序。卡片不逐字段流式生成;同题QA排除的政策不会提前露出。
前端收到 item 即可展示卡片(默认前6张、其余展开),卡片下方接续流式正文;不要等 summary 或 result 才显示卡片。节点结束不会重复发送已发来源和卡片,result 仍含完整快照,用于校准而非重复追加。卡片及来源去重仅限当前请求,不跨会话。
讲解降级或草稿撤回时,已校验卡片保留,正文按原替换/撤回协议处理;请求发生终止错误或断线时,仍须标记本轮未完整完成。无卡片知识问答及闲聊沿用原流程。检索、逐条分析及卡片短文案仍需等待,本次不改变模型轮次或筛选依据。后端需重启,外部前端/代理尚未联调。
沿用外层 {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],不转发推理字段。
内部[[knowledge_image:...]]与模型直接复制的image-scope标签不进入临时answer_delta;校验来源并绑定后的<image-scope>图片URL</image-scope>随最终answer_end.text(replace)、summary.text和result返回,前端在该位置渲染图片。无需独立图片事件,不能只累计delta而忽略最终替换。完整契约见接口文档。