feat(agent): use native parallel nodes and early reply streaming

This commit is contained in:
Eric Wang
2026-10-27 00:29:14 +08:00
parent e6a96ed135
commit 666dda4dba
14 changed files with 635 additions and 180 deletions
+82
View File
@@ -0,0 +1,82 @@
# LangGraph 原生并行与流式输出
文本模型使用 ChatOpenAI,业务图嵌入 FastAPI。对外仍使用原来的
stage_code / text_delta / formUpdate / done / error SSE 契约。
## 图内并行
事故采集阶段:
```mermaid
flowchart LR
P[normalize_input] --> R[collect:生成并校验回复]
P --> X[extract_accident_fields:提取候选字段]
R --> M[merge_collect]
X --> M
M --> C[应用服务事务提交]
```
条件边只在采集阶段选中两个分支;welcome、拍照、人员核实走自己的分支。
通过 `add_edge(["collect", "extract_accident_fields"], "merge_collect")`
保证两个分支都完成后,汇合节点只运行一次。
`normalize_input` 固定服务器参考时间并清空本轮临时通道。回复节点只更新
回复、阶段和对话簿记;提取节点只写 `extracted_accident_fields`;汇合节点
更新 accident_fields / turn_patch,再清空候选通道。两条分支都读取本轮开始时
的快照,不需要为并发写同一字段增加合并 reducer。
人员核实的提取提示词当前依赖本轮模型回复,并负责声明人员切换。
本次保留它的串行依赖与提示词,仍能在抽取完成前发送完整回复。
## 原生流与校验
后端使用 LangChain `astream_events(version="v2")`,其生命周期管理负责
消费期间的任务调度,以及流关闭后的取消与等待。
| 原生事件 | 条件 | 应用服务适配 |
|---|---|---|
| on_chain_stream | name=validated_reply_tokens | TextDelta |
| on_chain_end | name=validated_reply | ReplyFinished,刷新分句器 |
| on_chain_end | 根 accident_turn 正常结束 | 已提交结果,可发送 FormUpdate |
`validated_reply_tokens` 是原生 RunnableGenerator,封装现有模型文本迭代器,
等待完整合法的 state 前缀后才产出第一段文本。原始 ChatOpenAI 的 model 事件
仍可被 callbacks 追踪,但公开文本只消费受控回复 Runnable 的事件。
原生 messages 流包含校验前 token,单纯按模型或节点过滤不能替代前缀校验。
`validated_reply` 是原生 RunnableLambda。完整回复通过格式、阶段转换和可选
时间依据校验后,其正常 on_chain_end 才表示回复结束。模型结束事件本身
不能证明这些业务校验通过,因此不作为刷新最后一句的依据。
字段提取保留 complete / ainvoke,显式 stream=False,避免事件追踪 callback
把该调用隐式切换为流式。辅助模型输出不会映射为 TextDelta。
## 回复结束与事务完成
ReplyFinished 仅是后端中立适配层的内部标记,不新增公开 SSE 事件。
它立即刷新 useTextChunk 的剩余完整回复,覆盖有/无字段差量和
needFormUpdate=false 的情况。
文本可以早于提取和业务事务完成到达客户端;FormUpdate 与 done 仍在事务
成功后出现。完整回复失败、图失败、业务提交失败时,发送 error 而不发送 done。
已经输出的文字不可撤回,客户端仍以 done 判断本轮成功。
取消流时必须等图分支与模型关闭后才释放会话锁。持久化模式继续从业务提交
快照恢复;并行分支产生的未提交 checkpoint 不成为业务成功状态。
## 验证
`test/backends/test_native_parallel_streaming.py` 使用事件屏障验证:
- 回复和抽取同时运行,汇合与提交等待两者完成。
- 抽取阻塞时已经收到完整正文和 ReplyFinished。
- 分句器最后一句及时刷新;公开 SSE 类型、文本顺序和终止事件保持兼容。
- 抽取 JSON 不泄漏,原生节点和汇合在事件流中可见。
- 断开取消剩余抽取,SQLite 未提交状态可恢复并重试。
- 完整回复校验失败和业务提交失败均不产生虚假成功。
- ChatOpenAI 的真实 HTTP mock 路径保留普通提取与流式回复的请求参数。
官方接口参考:
- https://docs.langchain.com/oss/python/langgraph/streaming
- https://docs.langchain.com/oss/python/langgraph/use-graph-api
+12
View File
@@ -90,6 +90,18 @@
## 流式、并发与字段一致性约定
- 事故采集的回复与抽取为独立 LangGraph 节点,通过原生条件分支并行,
`add_edge(["collect", "extract_accident_fields"], "merge_collect")` 汇合;
分支各写独立通道,参考时间在 normalize_input 固定,合并后统一提交。
人员抽取依赖本轮模型回复,暂保留串行,不改变其提示词和人员切换语义。
- 模型文本与生命周期由原生 `astream_events` / `RunnableGenerator` 驱动,
不再自行维护图后台任务、队列或 custom event bus。原始模型 token 先经过
业务前缀校验;只有受控回复 Runnable 的文本流能映射为 TextDelta。
`validated_reply` 的正常 chain-end 映射为内部 ReplyFinished,立即刷新
分句缓冲;它不会对外新增 SSE 事件,也不表示字段已提交。
即使本轮无 formUpdate 或 needFormUpdate=false,最后一句也不等待抽取结束。
业务提交失败仍发 error,不能发 formUpdate/done 冒充成功。
- 阶段前缀先缓冲并按当前节点的转换规则校验,非法前缀不推送;
未输出时仍可使用一次修复预算。已开始输出后,全文格式或时间依据
校验失败会以错误终止,不在同一流中拼接修复答案,不提交本轮业务字段。
+7 -1
View File
@@ -370,7 +370,7 @@ docs/langgraph/
| 5. 拍照与核实 | ✅ 完成 | 真实模型 4/4 + 12/12 + 2/2(20261021 基准) |
| 6. 四接口业务状态 | 已实现 | 应用服务、异步 repository、字段别名、会话串行与接口回归 |
| 7. 持久化 | SQLite/PostgreSQL 初版已实现 | `src/storage/`;真实两库集成、进程退出恢复、幂等和版本冲突;见 [agent-storage.md](agent-storage.md) |
| 8. 真流式 | 已实现核心路径 | 模型增量、前缀校验、取消与提交回调;流式终止事件回归 |
| 8. 真流式 | 已实现核心路径 | 原生 astream_events / Runnable 流、采集并行节点、回复校验后提前刷新;见 [agent-streaming.md](agent-streaming.md) |
| 9—10 | 未完成 | 生产访问边界、容量验证与双后端等价性验收继续保留 |
数据库实现阶段全量测试 315 通过,包含真实 SQLite/PostgreSQL 集成测试。
@@ -380,6 +380,11 @@ docs/langgraph/
提示词来源和逐字比对见 `test/agent/test_workflow_prompts.py`。下方真实模型
黄金评测结果是此前轮次,数据库开发和本次模型适配迁移均未重新运行 46 条评测。
原生并行/流式更新:采集回复与字段提取通过图条件边并行并原生汇合,
人员核实保留依赖回复的串行提取。validated_reply 的原生结束事件触发
最后一句刷新,业务提交成功后才发送字段差量与 done。取消、失败和
SQLite 快照恢复有独立回归;详见 [agent-streaming.md](agent-streaming.md)。
### 已落地的架构决策
1. **LLM 提议、代码校验(propose-validate)**:所有 stage_code 由节点内 LLM 产出(提示词即程序),代码只做前缀解析、白名单校验(含 clientMode 出口)和落库;曾尝试的代码强制推进/短促词守卫已按此分工移除。
@@ -420,6 +425,7 @@ src/agent/
confirm.py 核实节点(字段提取按 person_index 隔离)
llm.py LLMService Protocol / Fake / ChatOpenAI(超时重试温度) / workflow fake
chat_model.py ChatOpenAI 流式迭代器的显式关闭与并发上下文隔离
reply_stream.py 原生 RunnableGenerator 的前缀校验流
fields.py 字段三态校验;clock.py 可控时钟
prompts/ 七份版本化提示词(来源 nodeId + 逐字原文)
```