Files
ZNJJ-api-server/docs/langgraph-backend-migration-plan.md
T
Eric Wang a890f41f6f chore(prompts): regenerate collection prompt from workflow@20261021-r2
- merge main (fb9271e): collection prompt restructured (state-bound-to-action,
  gatekeeper principle); fill_missing eval cases relaxed to semantic regexes
- prompts/collection.md regenerated; byte-identity test updated and passing
- rerun golden evals: collection_skip 11/12, time_confirmation 1/2
  (inter-run variance; only single_no_inferred_count fails consistently -
  model routes without asking vehicle count after time confirmation)
2026-10-21 17:28:44 +08:00

417 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LangGraph 迁移执行作业:从脚手架到等价 API 服务
> 版本:以 `workflow/20261020/事故信息采集2025118.json` 为当前迁移基准;20261016 保留作历史对照。
>
> 读者:掌握 Python、异步函数、HTTP 和基本测试,尚未独立交付过 LangGraph 服务的本科生。
>
> 最终成果:调用方继续使用现有 API,事故处理流程由 LangGraph 执行,业务数据可以持久化和恢复。
>
> 本文是执行计划;下文标为“新增”的目录、命令和能力均为待实现交付物。
## 作业说明
你将接手一个真实的交通事故信息采集服务。现有 FastAPI 服务把对话交给 FastGPT 工作流,本作业要求逐步用 LangGraph 实现同样的外部服务。
先搭建一个可以运行的图,再把它接到现有接口,然后逐项补齐业务能力。每次作业结束,都必须留下一份他人可以运行的程序和验证证据。不要等到所有模块写完才第一次启动服务。
完成作业后,你应当能够解释:一次请求如何变成图输入,节点如何读取和更新状态,条件边为何选择某个阶段,消息如何进入模型,业务字段如何落库,以及客户端收到成功后为何可以相信数据已经保存。
本期迁移范围为 `POST /chat`、`POST /chat?stream=true`、`POST /set_info`、`POST /get_info`、`DELETE /delete_session`。保留 `GET /` 的存活响应。语音 WebSocket、Pipecat、STT/TTS 和语音协议不在本次改造范围;切换文本后端时,必须明确现有语音管线仍依赖 FastGPT 的配置与生命周期,不能因此破坏语音服务启动。
### 提交规则
1. 按作业 1—10 顺序推进。每次提交对应一个可独立审查的里程碑。
2. 每次提交附运行命令、预期结果、实际结果和已知限制;测试失败必须解释原因。
3. 真实模型只用于后期联调和评测。日常图测试使用固定输出的 fake LLM,能够离线重复执行。
4. 每个业务规则都要注明来源:代码、工作流节点或评测案例。来源冲突时记录差异及采用依据。
5. 配置样例使用占位值;对话样例使用合成数据;不得提交凭证、真实身份证和手机号。
6. 本文给出的工时是单人专注开发的估算,不包含等待基础设施和业务确认的时间。
### 里程碑总览
| 作业 | 结束时可以演示什么 | 预计工时 | 分值 |
|---|---|---:|---:|
| 1. LangGraph 脚手架 | 不依赖模型运行最小图,并恢复同一会话 | 4—6 小时 | 8 |
| 2. 接入现有 API | fake 图经现有 `/chat` 返回 JSON 和 SSE | 6—8 小时 | 10 |
| 3. 领域状态与事件 | 校验模型预测的下一阶段,记录事件上下文 | 8—12 小时 | 12 |
| 4. 事故信息采集 | 多轮采集、跳过已知信息并正确分流 | 12—18 小时 | 15 |
| 5. 拍照与信息核实 | 跑完单车、双车及更正信息流程 | 10—16 小时 | 12 |
| 6. 同一份业务数据 | 四个业务接口共享内存态并正确删除 | 8—12 小时 | 10 |
| 7. PostgreSQL 与恢复 | 重启、并发和失败重试后数据仍一致 | 16—24 小时 | 13 |
| 8. 完整流式执行 | 真正增量输出,并处理错误和断连 | 10—16 小时 | 8 |
| 9. 服务运行保障 | 配置、健康检查、日志和访问边界可验证 | 8—12 小时 | 5 |
| 10. 等价性与发布 | 完整回归、演示、灰度与回退演练 | 10—16 小时 | 7 |
预计合计 92—140 小时,可按每周 12—18 小时安排为 7—10 周。先完成离线里程碑,再安排真实模型和 PostgreSQL 的集成环境。
## 开工前的阅读地图
只做必要阅读,记录一页笔记后立即进入作业 1。不要先照搬大型生产模板。
| 文件 | 阅读时应回答的问题 |
|---|---|
| `src/backends/chat.py` | `ChatInput`、`ChatResult`、`TextDelta`、`FormUpdate` 怎样隔离供应商? |
| `src/backends/fastgpt.py` | FastGPT 返回的文本和表单如何转成中立结果? |
| `src/core/fastgpt_client.py` | 客户端何时初始化,依赖如何注入,何时关闭? |
| `src/api/endpoints.py` | 哪些逻辑是协议适配,哪些仍直接调用 FastGPT? |
| `src/schemas/models.py` | 请求默认值、字段长度、返回类型分别是什么? |
| `workflow/20261020/事故信息采集2025118.json` | 当前各模式实际走哪些节点和条件边? |
| `evals/cases/`、`evals/README.md` | 当前已经覆盖哪些对话?runner 实际断言了什么? |
| `test/api/`、`test/backends/`、`test/test_client_mode.py` | 已有后端边界测试和公共契约测试是否与当前代码一致? |
阅读 LangGraph 官方文档中的 `StateGraph`、state/reducer、conditional edges、`InMemorySaver`、`AsyncPostgresSaver`、异步执行和 streaming 章节。采用依赖版本对应的 API,首次提交记录版本和文档链接,避免混用旧教程。
有两处特别需要辨别:`prompts/20261012/事故信息收集.txt` 与最新工作流内嵌提示词并不完全一致;`docs/chat-stream-mode.md` 未列出全部现有字段和事件。以当前代码、最新工作流和新评测案例交叉核对,不能单独以旧文档决定兼容行为。
## 必须守住的外部契约
以下是执行作业时的检查表。作业 2 将其变成可运行的契约夹具。
| 项目 | 必须保留的行为 |
|---|---|
| `/chat` 请求 | `sessionId`、`timeStamp`、`text`;`clientMode` 默认为 `direct`,空串、空白和 null 也归一到 `direct`;允许 `browser_addon` |
| 可选开关 | `needFormUpdate=false`、`useTextChunk=false`;两者的当前行为分别取样冻结 |
| 非流式响应 | `sessionId`、`timeStamp`、`outputText`、`formUpdate`、`nextStage`、`nextStageCode`、字符串 `code`、`msg` |
| 状态映射 | 内部 `3001/3002/1002 → 1002`,`2006 → 2004`,`2017 → 2016`,`2020 → 0002`;历史映射保留,即使新流程不再经过该状态 |
| 客户端命名空间 | `browser_addon` 的外部码为 `browser_addon.<归一化状态码>`,领域状态仍保存原始内部码 |
| SSE | 事件名为 `stage_code`、`formUpdate`、`text_delta`、`done`、`error`;正文不泄漏 state 标签 |
| `/set_info` | `value` 是字符串,`includeInputInfo` 默认为 false;保留输入 schema,内部改为直接写业务状态 |
| `/get_info` | `value` 仍为 JSON 编码后的字符串;支持 `all/acdinfo/acdhuman1/acdhuman2` 和单字段 |
| 分组读值 | 分组内布尔值映射为字符串 `"1"/"0"`,缺失字段为 `""`;单字段当前直接 `json.dumps`,不得错误套用分组转换 |
| `/delete_session` | 使用 DELETE 和 JSON 请求体,保留已有响应字段和字符串业务码 |
| 错误 | 区分 HTTP 状态与响应体业务码,保存当前认证、限流、后端失败和参数错误的样例 |
现有 SSE 允许 `formUpdate` 在状态码前后到达,不能把旧计划中的固定事件顺序当成既有契约。最低保证为:`stage_code` 先于正文、一次成功只有一个 `done`、失败以一个 `error` 终止且不再发送 `done`。如果要进一步收紧表单事件顺序,须先用真实消费者验证并记录约定。
模型流式和非流式的一致性,使用**同一固定模型输出或同一已记录回放**验证。对真实模型分别调用两次,不能要求自由生成文本逐字相同。
## 作业 1:搭建并运行 LangGraph 脚手架
**学习目标:** 理解图、状态、节点、边与 thread 的关系,先让它们在命令行中工作。
**输入:** 当前仓库、可用 Python 环境;不需要数据库和真实模型密钥。
**任务:**
1. 在现有依赖管理方式上加入明确版本的 LangGraph 及必要依赖,记录 Python 版本和可复现安装方法。此时无需为迁移包管理器重构全仓。
2. 新增 `src/agent/state.py` 和 `src/agent/graph.py`。最小状态含 `session_id`、`turn_count`、`input_text`、`response_text`、`stage_code`。
3. 创建 `START → normalize_input → reply → END`。`reply` 使用固定模板,每次只增加一次轮数。
4. 使用 `InMemorySaver` 编译图,通过 `configurable.thread_id` 区分会话;使用 `ainvoke` 执行。
5. 新增 `examples/langgraph_smoke.py`:同一 thread 输入两次,另一个 thread 输入一次,打印轮数和文本。脚本不导入 FastGPT、不读取其密钥。
6. 添加最小图测试:同会话累计轮数、不同会话隔离、重复执行脚本得到相同结果。
**交付物:** 最小图、示例脚本、依赖安装说明和上述三个断言。
**验收命令(本作业新增):**
```bash
python -m examples.langgraph_smoke
python -m pytest test/agent/test_scaffold.py -q
```
预期三个调用的轮数分别为 `1、2、1`。内存 saver 的数据在进程退出后消失,这是本阶段明确的限制。
**讲解题:** 如果每轮随机生成 thread ID,会丢失什么?把数据库连接放进 state 会造成什么问题?
## 作业 2:把最小图接入现有 API
**学习目标:** 建立第一条端到端路径:HTTP → 后端适配器 → 图 → HTTP。
**前置:** 作业 1 通过。
**任务:**
1. 新增 `src/backends/langgraph.py`,实现现有 `ChatBackend`。`complete()` 返回 `ChatResult`,`stream()` 返回 `TextDelta | FormUpdate`。
2. 初期继续让 API 层解析 state prefix。图返回 `<state>1002</state>固定文本`,避免同时重写供应商边界和公共协议。
3. 提供 `AGENT_BACKEND=fastgpt|langgraph` 类型化配置和依赖工厂。LangGraph fake 模式启动不得要求 FastGPT 密钥;仍启用语音 FastGPT 路径时单独校验所需配置。
4. 将资源初始化和关闭纳入 lifespan。两种后端分别执行启动冒烟验证,不仅在测试中覆盖依赖。
5. 在现有 `test/api/` 补充 HTTP 级契约测试;检查 OpenAPI schema、JSON 响应和 SSE 事件。
6. 在 Conda 的 `py3.11` 环境先运行已有测试并记录基线。当前测试已覆盖默认 `clientMode=direct`、API 向 `ChatInput` 传递该模式,以及 `nextStageCode` 的 32 字符上限。后续按当前实际契约维护预期,不能为消除红灯收窄合法协议。
7. 保存请求与响应夹具,覆盖两种 clientMode、分句开关、表单开关、state 标签跨 chunk 和错误响应。
**交付物:** 可切换的图后端、HTTP 契约夹具、修正说明、两种后端的启动方法。
**验收:** `POST /chat` 返回固定文本和 `1002`;`browser_addon` 返回 `browser_addon.1002`;流式请求得到 stage、正文和单个 done;所有已有测试通过或有明确的环境阻断记录。此时允许把固定完整答案切为几个片段,提交说明必须标注“尚未接入模型增量输出”。
## 作业 3:建立领域状态和确定性事件
**学习目标:** 让业务状态可以检查和推演,并区分“事件”与“用户说了一句话”。
**任务:**
1. 扩充 state:内部阶段、原始状态码、待回答问题、事故字段、两方人员字段、字段确认状态、澄清计数、无回复计数、本轮 patch、响应文本和业务版本。
2. 使用 Pydantic 校验边界数据;图 state 保持可序列化,不保存 HTTP client、连接、音频和图片 base64。
3. 在 API/应用边界把 `【拍摄完成】`、`【用户无回复】`、`【客户端连续3次拍摄识别失败:原因】` 转成明确事件。图内部按事件类型处理,普通“拍好了”不能当完成事件。
4. 图结构对齐工作流节点:事故信息收集、单车拍照、双车拍照、信息确认各为独立节点(对照 `workflow/20261016/事故信息采集2025118.json` 的 chatNode),节点间用条件边按 `stage_code` 路由(对照 Condition#2/#7/#9 等分支)。节点内部的小步推进(拍照步骤、分流判断)由节点内 LLM 提出目标阶段码——模型负责自然语言,代码按白名单(`transitions.LEGAL_TRANSITIONS`)校验,非法提议不写状态(受控修复重试在作业 4-10);白名单条目必须对照工作流提示词与条件边登记来源,不能只凭状态码相像填写。所有 stage_code(含拍照失败 → 0005、连续两次无回复 → 0004)都由节点内 LLM 提议,代码只负责事件解析、无回复计数等上下文簿记与合法性校验,不代替模型决定。
5. 拍照失败通知收到一次就产生 `0005`;客户端已经完成三次统计,服务端不得再累计三次。
6. 连续两次无回复产生 `0004`,中间有效回复如何重置计数必须有测试。
7. 把“本轮输出字段”在每轮开始时清空;历史事故字段继续保留,避免旧 `formUpdate` 在下一轮重复返回。
**交付物:** `events.py`、`transitions.py`(模型提议的转换校验)、按工作流拆分的多节点图(`graph.py`)、状态模型、规则来源表、参数化转换测试。
**验收:** 使用可注入的 fake LLM 回放阶段提议,验证合法提议、非法跳步、无回复和失败输入的上下文传递;普通“拍好了”不解析为客户端标记。真实阶段码由模型预测,代码不凭标记强制推进。
**讲解题:** 为什么外部看到 `1002`,内部仍必须区分采集、单车核实和双车核实?
## 作业 4:实现事故信息采集与 LLM 接入
**学习目标:** 用模型理解自然语言,用代码验证并提交业务状态。
**任务:**
1. 从最新工作流提取事故采集提示词和节点路由,保存为可审查文本并注明源节点 ID。明确 `direct` 与 `browser_addon` 的差异。
2. 实现可注入的异步 LLM 服务:fake 与真实 provider 使用相同接口,包含超时、取消、有限重试和模型配置。
3. 普通自然语言轮次由同一次回复生成 `<state>XXXX</state>正文`,代码验证前缀、状态枚举和转换守卫。不得仅为复判状态再调用一次模型。
4. 定义候选字段的结构化提取接口和校验。确需独立提取调用时记录原因、调用次数和延迟;不得从用户可见回复中靠正则猜全部业务字段。
5. 对每个字段区分未知、候选、已确认。最新工作流要求:已经明确提供的有效信息直接记录,跳过已知项;一次补齐剩余信息后直接分流;不能沿用旧提示词逐项重复确认。
6. 注入可控时钟和时区,校验相对时间、当前时间、未来时间与边界;测试不依赖运行机器当天时间。
7. 实现人伤明确肯定、明确否定、模糊/ASR 破碎三类输入;未知时停留并澄清,不凭关键词直接转人工。
8. 用户已不在现场仍继续采集。数量未知不能从“追尾”等词擅自推断。已明确无人伤后不重复问,新的明确矛盾信息可以触发更新。
9. 澄清阈值按阶段配置。采集的计入失败与核实的计入失败规则不同,不能全局统一成“任意无效回答两次转人工”。
10. 对 prefix 缺失、重复、未知或迁移非法最多做一次受控修复重试;失败返回可识别错误,不提交业务状态,也不泄漏原始模型内容。
**交付物:** 版本化提示词、字段候选与校验模型、采集节点、模型服务、fake 对话夹具、真实模型评测报告。
**验收:** 离线用例覆盖正常单/双车、一次提供全部信息、一次补齐剩余字段、数量未知、无人伤否定、模糊人伤、不在现场、未来时间、主动转人工、澄清重置。真实模型至少运行 `collection_skip_known_info` 和 `no_false` 全部现有案例。
**当前状态(2026-10-21):** 已实现。真实模型下 `collection_skip_known_info` 12/12 通过;`no_false` 2/8,其余 6 例与 `photo_skip_plate_confirm` 0/4 全部卡在同一模式——开放问题上的短促回答(“对”)被模型直接分流,而评测期望先澄清。移植侧已与工作流逐字对齐(提示词、history=50、welcome 话术、变量渲染),差异仅存于模型行为/应用层配置,详见文末开放问题。
现有评测 runner 只检查 SSE 状态码,无法证明“没有重复询问”。本作业必须增加当前问题 ID、已确认字段及提问行为的可检查结果,或提供带人工判定的脱敏对话报告。新增字段不进入公共 API。
## 作业 5:完成拍照和人员信息核实
**学习目标:** 实现阶段交接,避免换节点后重复提问或丢失上一轮答案。
**任务:**
1. 单车按 `2000 → 2001 → 2002 → 2003 → 2005` 推进。`2004` 是确认单车车牌步骤(来源:当事人确认,20261020 提示词仍写 2003→2005):车牌已明确(当事人已提供或拍摄识别成功)时由 2003 直达 2005,未明确时进入 2004 询问、确认后 2004→2005。两种走向都合法,由模型按上下文判断,代码按白名单校验(2003→{2004,2005}、2004→{2005} 已登记)。
2. 双车按 `2010 → 2011 → 2012 → 2013 → 2014 → 2015` 推进。`direct` 的最后一次拍摄完成直接进入 `3002`,在同一回复中完整询问第一辆车驾驶员身份。
3. 核实节点消费上一节点已经提出的问题。用户回答“是”后直接问姓名,不能因进入新节点再问一次驾驶员身份。
4. 根据工作流实际条件边分别复现两种 clientMode 的后续路径。当前 `browser_addon` 拍照分支可以结束于 `0000`,不能强行套用 direct 的人员核实流程。
5. 单车 direct 在有效车损位置后进入核实链,参照实际工作流连边与评测;不要仅依据单车拍照节点内部的结束话术决定最终路由。
6. 实现姓名、身份证后四位、手机号后四位核实;否认后采集正确值;分段号码先累计,完整后复述确认,再覆盖正式字段。
7. 双车保存当前人员索引,第一方完成后移交电话并核实第二方。不能把第二方数据写入第一方字段。
8. 每个阶段记录无效回答计数规则和全局中断优先级,覆盖短促词不误转人工。
**交付物:** 单车/双车阶段节点、模式路由表、人员核实状态、完整场景回放。
**验收:** `photo_skip_plate_confirm` 四个现有案例全部通过;新增单车 direct 核实完成、双车两方核实完成、拒绝驾驶员身份、号码分段更正、阶段交接不重复提问场景。拍照步骤、失败和无回复均由对应 chatNode 提示词驱动模型判断;正常每轮一次对话模型调用,格式或转换错误共用最多一次修复。记录调用次数,不要求零调用。
## 作业 6:使四个业务接口共享同一份状态
**学习目标:** 将流程状态和表单数据连接起来,形成第一个功能完整的内存版本。
**任务:**
1. 新增应用服务和 repository 接口:`chat`、`set_info`、`get_info`、`delete_session`,endpoint 仅处理校验、协议和依赖。
2. 用内存 repository 完成接口接线。图每轮读取最新业务状态,`set_info` 后的下一次聊天必须看到新值。
3. 建立字段注册表,完整覆盖当前代码中的三个分组。人员字段包括 `sfzmwh1/2`、`sjwh1/2`、`csbw1/2`,不能只迁移姓名和号码。
4. 在注册表记录外部字符串表示、内部类型、可写性、分组和敏感级别。保留区分空值、false、0 和未知的能力,不得用 `bool("0")` 转换布尔值。
5. `set_info` 和 `get_info` 不调用 LLM,也不创建/删除辅助聊天记录;继续接受 `includeInputInfo`,说明它在新后端已无需触发辅助对话。
6. `formUpdate` 从已验证并正式提交的字段差量产生,按 `needFormUpdate` 的契约返回,保留已有嵌套或平铺结构的夹具。
7. 定义未知 key 的兼容策略。当前 get 对未知 key 返回 JSON 空串;若旧调用方存在任意 key 写入,先审计其用途。新增白名单和内部字段保护的行为差异必须进入兼容清单。
8. 删除时清理业务状态、图 checkpoint、消息和会话路由;重复删除的行为固定为测试。不得仅删除 FastGPT 历史而留下 LangGraph 数据。
**交付物:** 应用服务、字段注册表、内存 repository、四接口组合测试、兼容差异清单。
**验收演示:** 创建会话 → `/set_info` 写入车牌 → `/get_info` 读回 → `/chat` 使用车牌继续核实 → 分组读值 → 删除 → 再读与再次聊天均不泄漏旧数据。用 spy 证明 set/get/delete 没有模型调用。
## 作业 7:接入 PostgreSQL,验证并发和故障恢复
**学习目标:** 将“单进程能运行”推进到“进程退出后仍可信”。
**任务:**
1. 新增本地 PostgreSQL 启动配置、`.env.example`、业务表 migration 和清理步骤。建议 `app` schema 管业务数据,单独配置 checkpointer 表;确认采用版本是否支持所选 schema/search path。
2. 使用异步数据库访问和 `AsyncPostgresSaver`,在 lifespan 管理连接池。业务 migration 与 saver 初始化分别执行,生产部署前完成,不在每个请求中建表。
3. 至少建立 session、业务字段、业务事件、幂等记录。每轮保存原始内部状态码、业务版本、规则/图版本、已提交 patch。
4. 明确业务表是 `/get_info` 的权威读源,checkpoint 用于执行恢复。两个存储使用不同连接时不能宣称天然处于同一事务。
5. 采用可恢复的提交协议:先记录稳定 turn/event ID 和 pending 状态;执行图;在一个业务事务内提交字段、版本、事件与可重放结果;确认 checkpoint 与提交版本一致后才发送成功。恢复时按业务事件修复滞后 checkpoint,或丢弃未提交的图进度。
6. 若选择其他一致性方案,提交对应的时序图,并证明下列三个故障窗口有确定结果:checkpoint 先成功、业务事务先成功、响应发送前进程退出。
7. 同 session 修改跨进程串行化,使用 PostgreSQL 锁或数据库 lease;不要只用进程内 `asyncio.Lock`。避免跨长时间模型调用占用行锁事务;使用独立连接的会话 advisory lock 时说明连接固定与释放方式。
8. 添加 `state_version` 冲突检测。每个请求使用独立异步会话,不跨并发任务共享 ORM session。
9. 可选 `Idempotency-Key` 进入 HTTP 边界;相同 key/相同请求重放结果,不同请求返回明确冲突。没有调用方稳定 key 时,只能保证内部节点恢复幂等,不能承诺客户端重新 POST 也自动去重。
10. 完成删除的持久化清理和删除/聊天并发控制。定义“删除后新聊天”的新会话边界,防止迟到任务复活旧数据。
**交付物:** PostgreSQL 配置、migration、持久化 repository、恢复协议时序图、真实数据库集成测试。
**验收:** 从空库安装成功;重启服务后继续同会话;chat 与 set 并发无静默覆盖;相同 key 重试不重复推进;三个故障窗口均可恢复;删除后重启也无残留。数据库不可用时不得偷偷降级成内存模式。
## 作业 8:完成真实增量 SSE
**学习目标:** 流式输出既低延迟,又能诚实表达尚未提交和已提交的状态。
**任务:**
1. 使用当前 LangGraph 版本支持的异步事件/消息流,从实际 LLM token 到 `TextDelta` 建立增量链路;不要用“先等完整回复,再逐字切分”冒充真实流式。
2. 校验完整 prefix 后才能释放正文。覆盖 `<state>` 和结束标签被任意切分、正文中再次出现标签、前缀长期不闭合、未知和非法状态码;设缓存上限和超时。
3. 业务状态的合法性由共享 validator 决定;API 保留既有状态映射。非流式和流式不能各维护一套转换规则。
4. 允许前缀和正文在最终持久化前发出,但把它们视为本轮暂定输出;`done` 只能在业务状态和 checkpoint 提交确认后发送。持久化失败必须发送终止 error,不能再发送 done。
5. 一旦对外发送正文,不得透明重跑并拼接另一条模型答案。prefix 前的修复重试与正文后的失败使用不同策略。
6. `formUpdate` 仅发布已提交字段;处理没有更新、更新先就绪、更新晚于正文的情况,核对消费者允许的顺序。
7. 保留 `useTextChunk` 的分句行为:改变片段边界不改变最终正文;使用同一录制输出验证流式与非流式结果相等。
8. 客户端断开后取消模型/图任务并释放资源;若提交已经完成,保存结果供稳定幂等 key 重放;若尚未完成,恢复协议决定下次从何处继续。
**交付物:** 增量流适配、断连/错误处理、事件轨迹夹具、首段正文耗时报告。
**验收:** 第一个正文片段在模型生成结束前出现;成功只有一个 done;模型/数据库失败只有一个终止 error;断连无遗留任务;同一回放的两种模式正文、状态与 patch 一致。HTTP 已开始发送 SSE 后的失败通过事件表达,不能尝试再改 HTTP 状态。
## 作业 9:配置、可观测性与访问边界
**学习目标:** 让运维和其他开发者能够定位问题并安全运行服务。
**任务:**
1. 配置区分 development/test/production,校验模型、数据库、后端开关、超时和连接池。生产持久化缺失时启动或 readiness 失败。
2. 保留 `GET /`,新增 `/health/live`、`/health/ready`。readiness 检查数据库和 saver,不每次真实调用收费模型。
3. 为请求、turn、节点和模型调用建立关联 ID。日志记录状态、版本、耗时、错误类型,不记录原始表单、完整用户输入、凭证和完整号码。
4. 记录成功率、模型调用次数、正文首段延迟、总耗时、非法转换、持久化失败和 session 冲突;指标 label 不放 session ID。
5. 复用部署环境已有服务身份和会话归属。若现有服务尚无鉴权,把新增认证列为上线前联调事项,不在兼容迁移中默默要求全部调用方更换协议。
6. 多租户环境所有查询与 thread ID 绑定可信租户身份;禁止从未经验证的请求字段直接相信 tenant。无多租户需求时不要为作业虚构账号系统。
7. 记录敏感业务表、事件、checkpoint 和备份的保护与保留方案;删除检查覆盖这些数据,不仅覆盖 API 返回值。
**交付物:** 启动配置说明、健康检查、脱敏日志样例、指标说明、部署访问边界说明。
**验收:** 注入数据库故障可看到 readiness 失败及关联日志;运行合成敏感数据用例后,日志无明文泄漏;同一请求可以定位到具体失败节点。
## 作业 10:证明等价,并交付可运行服务
**学习目标:** 用证据说明调用方可以继续使用同样的服务。
**任务:**
1. 完成下表全部验收。HTTP schema、JSON/SSE、字段、分组和业务规则分别验证,不用“状态码正确”代替全部正确。
2. 保留现有 runner 调用真实 HTTP 服务,补充读取字段、patch、终止事件和提问行为的验证。所有 fixture 明确来源与预期,禁止只按新实现生成预期答案。
3. FastGPT 与 LangGraph 使用相同合成场景、相同初始数据、独立会话执行,比较状态轨迹、字段和终止原因;自然语言允许合理改写。
4. 每个真实模型黄金案例至少执行三次,报告逐次结果和波动。固定模型与提示词版本,并给出失败对话,不能只汇报平均值。
5. 记录 FastGPT 与 LangGraph 在同一环境的成功率、正文首段与总耗时 P50/P95、模型调用数;明确样本量和并发。性能发布预算须在灰度前填写,不能到发布时仍写“待定”。
6. 新会话按 session 固定选择后端;不要每个请求随机切换。同一会话的 chat/set/get/delete 必须使用相同路由。旧 FastGPT 会话留在旧后端直至结束,首版不自动迁移历史对话。
7. Shadow 使用独立 thread、独立数据和受控模型额度,禁止写真实业务表。比较请求序列时保持对应会话上下文。
8. 演练关闭新会话的 LangGraph 分流。已经进入 LangGraph 的会话继续路由至 LangGraph 或执行明确的状态转换;单改环境开关不会自动把两套会话数据互相转换。
9. 编写从干净检出开始的安装、数据库准备、启动、验证、恢复和停止说明。启动命令应为 `python -m uvicorn src.main:app --host 127.0.0.1 --port 8000`,依赖步骤按实际交付填写。
10. 观察期结束后再决定移除 FastGPT。语音仍依赖 FastGPT 时保留该依赖,文本迁移完成不等于整个仓库都能删除 FastGPT。
### 最终验收矩阵
| 类别 | 必测内容 | 通过要求 |
|---|---|---|
| API 契约 | 四个业务接口、两种 clientMode、输入默认值、长度限制和历史映射 | 所有固定契约用例通过;差异有显式记录和处理 |
| 事故采集 | 跳过已知项、补齐即分流、不推断数量、不在现场、时间校验 | 所有离线场景通过;真实模型黄金案例逐次报告 |
| 安全与澄清 | 人伤肯定/否定/模糊、主动人工、计数阈值、无回复 | 确定性规则全部通过;黄金集中明确人伤无漏转,否定/模糊无误转 |
| 拍照与核实 | 单/双车、模式差异、跨阶段回答、分段更正、两方隔离 | 完整状态与字段断言通过,无非法跳步 |
| 数据 | set/get/chat 一致、分组布尔值、单字段 JSON、差量和删除 | 无丢字段、旧 patch 重发或删除残留 |
| 持久化 | 重启、三类故障窗口、并发、幂等与冲突 | 无重复提交、静默覆盖和不可恢复分歧 |
| 流式 | prefix 切分、真实增量、分句、断连、失败与完成 | 不泄漏标签;终止事件唯一;done 前已持久化 |
| 运行 | 无 FastGPT 的文本模式启动、健康检查、脱敏、路由回退 | 干净环境可复现;语音原有启动和路由不受破坏 |
真实模型小样本通过不能证明线上错误率为零。达不到矩阵中的黄金集要求时,修正后重新执行受影响案例,并在报告中保留失败记录。
### 最终提交目录
```text
src/
agent/ # state、events、transitions、graph、nodes、prompts
backends/langgraph.py # 适配已有 ChatBackend
services/ # 事故应用服务、LLM 服务
repositories/ # 内存实现与 PostgreSQL 实现
core/ # 配置、资源生命周期、数据库
api/ # 保留现有路径与公共 schema
examples/langgraph_smoke.py
test/
agent/ # 离线状态与图场景
api/ # HTTP JSON/SSE 契约与组合调用
integration/ # PostgreSQL、并发和恢复
evals/ # 复用并扩展现有真实服务评测
alembic/ # 业务表迁移(使用该工具时)
docs/langgraph/
runbook.md # 从零启动和日常操作
contract.md # 真实接口样例与兼容差异
rule-sources.md # 工作流节点、规则和测试对应关系
recovery.md # 提交协议、故障窗口与恢复
evaluation.md # 等价性、质量和性能证据
rollout.md # session 路由、灰度、回退演练
```
目录可以按实际实现微调,但每个交付物必须有明确位置。保持现有 `test/`,无需为套用模板重命名整个测试树。
## 评分与完成条件
每次作业按表中分值计分:功能与正确性占该作业 60%,可复现的验证占 30%,设计讲解与限制说明占 10%。只提交图示或设计文档、没有运行结果,不能取得该作业的功能分。
以下为结课必过项,分数不能替代:公共契约保持兼容;四个接口使用一致的会话路由和状态;非法阶段转换不得写状态(由白名单校验,模型提议不算数);成功返回前数据已持久化;失败可恢复;删除不留下可被复活的旧会话;完整请求链路在 LangGraph 文本模式下不调用 FastGPT。
最终演示使用一个全新会话:启动服务 → 采集双车事故 → 完成拍照 → 注入人员预填值 → 更正一段手机号 → 重启服务 → 继续第二方核实 → 查询全部信息 → 删除会话 → 验证无残留。随后展示一次明确人伤分流、一次模糊人伤澄清和一次数据库失败的 SSE error。
## 执行时需要登记的环境决策
作业 1—3 可直接离线推进。到相应阶段前再落实下列信息,并写入 runbook;不要把所有环境问题都变成搭脚手架的前置阻塞。
| 决策 | 最晚落实时间 | 未落实时可以继续的工作 |
|---|---|---|
| 真实 LLM provider、模型、超时与预算 | 作业 4 真实模型验收前 | fake LLM、领域与接口测试 |
| 工作流冲突规则和两种模式的最终路由 | 作业 4—5 验收前 | 来源提取、离线夹具、差异记录 |
| PostgreSQL 版本、连接与部署权限 | 作业 7 前 | 内存完整功能与本地数据库开发 |
| 调用方对任意字段写入、错误、SSE 顺序的依赖 | 作业 6/8 验收前 | 当前行为取样、兼容层实现 |
| 服务身份、租户归属与数据保护方案 | 作业 9/生产灰度前 | 本地服务与合成数据验证 |
| 延迟/成本预算、灰度观察周期与负责人 | 作业 10 灰度前 | 基线测量、压测与回退演练 |
| FastGPT 应用层配置(应用级系统提示词、思考模式开关)与线上部署差异 | 作业 4/5 验收对账前 | 移植代码、离线评测与差异记录 |
| 评测案例与工作流导出的分歧(“对”分流语义、2004 走向)的权威口径 | 作业 5 验收前 | 白名单双走向、逐字比对测试、失败证据留档 |
## 当前进度与已知差异(2026-10-21 更新)
### 作业进度
| 作业 | 状态 | 证据 |
|---|---|---|
| 1. 脚手架 | ✅ 完成 | 冒烟轮数 1/2/1;`test_scaffold.py` |
| 2. 接入 API | ✅ 完成 | `AGENT_BACKEND=langgraph` 启动验证;`test_langgraph_backend.py` |
| 3. 领域状态与事件 | ✅ 完成 | 事件解析 14 例、白名单校验 24 例;`LEGAL_TRANSITIONS` 17 条均带 `source_ref` |
| 4. 采集 + LLM | ✅ 完成 | 真实模型 `collection_skip_known_info` 12/12;单测含逐字比对 |
| 5. 拍照与核实 | ✅ 完成 | 真实模型 4/4 + 12/12 + 2/2(20261021 基准) |
| 6—10 | 未开始 | — |
全量单元测试 143 通过;七份提示词与 20261020 工作流逐字比对由参数化测试锁定。
### 已落地的架构决策
1. **LLM 提议、代码校验(propose-validate)**:所有 stage_code 由节点内 LLM 产出(提示词即程序),代码只做前缀解析、白名单校验(含 clientMode 出口)和落库;曾尝试的代码强制推进/短促词守卫已按此分工移除。
2. **依赖注入走 `context_schema`**:`GraphDeps(llm=...)` 经 `Runtime` 注入调用期,不进 state/checkpoint;`build_graph()` 与 LLM 解耦。
3. **与工作流逐字对齐**:提示词原文渲染(含 `{{$变量}}` 替换:系统时间按 Asia/Shanghai、人员预填字段缺失时显式标注“未提供”);聊天历史按 user/assistant 消息传递(上限 50 条),对齐 chatNode `history=50`;welcome 话术为 Assigned Reply#3 原文并计入对话历史。
4. **不引入 LangGraph Server / langgraph_sdk**:生产形态的 Agent Server 需要 LangSmith license key、外网 beacon 出网及 Postgres+Redis 全栈,与课程目标(作业 7 亲手实现持久化)和部署环境不符;`langgraph dev` 仅为内存态,无增量能力。自部署高并发方案 = 嵌入库模式:无状态 FastAPI 多副本 + AsyncPostgresSaver + 每会话 advisory lock 串行 + 连接池预算(社区参照:langgraph-agent-toolkit、agent-bakery)。
5. **2004 语义**:确认单车车牌;车牌已明确时可 2003→2005 跳过,未明确时 2003→2004→2005。两种走向均在白名单登记,由模型判断。
### 真实模型评测现状(deepseek-v4.1-flash)
| 黄金案例 | 结果 | 说明 |
|---|---|---|
| `collection_skip_known_info` | 11/12 | 存在轮次间波动;仅 `single_no_inferred_count`(双模式)稳定失败:时间确认后漏问数量直接分流 |
| `photo_skip_plate_confirm` | 4/4 | 20261021 步骤5(时间复述确认)修复了原“对”分歧 |
| `no_false`(asr,main 已移除旧 6 例) | 2/2 | — |
| `photo_button_guidance` | 12/12 | — |
| `non_motor_vehicle`(新增) | 14/14 | 依赖 formUpdate 管线(提取节点 + 更新表单差量语义) |
| `time_confirmation`(新增) | 1/2 | 轮次间波动;失败例为话术字面偏离(main 已将 fill_missing 断言放宽为语义正则,此套件仍用字面模板) |
**统一失败模式**:开放问题(“您现在还在事故现场吗?”)未获明确回答时,用户输入短促词(“对”),评测期望保持 1002 澄清,模型直接分流 2000/2010。移植侧已排除提示词、历史、变量、温度(0 与缺省均试过)差异;模型名与 FastGPT 应用一致(deepseek-v4.1-flash)。
### 开放问题(20261021 合并后更新)
1. **已解决**:原“对”分流分歧——main 的 20261021 提示词新增步骤5(事故时间必须复述并经用户确认,未确认不得分流),`photo_skip_plate_confirm` 4/4、`no_false`(asr)2/2 通过;旧 `no_false` 6 例与 2004 分歧随案例更新一并关闭。2004 按“车牌确认、双走向合法”实现并保留在白名单。
2. **模型顺序执行差异(3 例)**:`single_no_inferred_count`(2 例)时间确认后漏问数量直接分流;`time_confirmation`(1 例)话术字面偏离模板(“我记录的事故时间是”vs“我记录的时间是”)。均为模型对提示词顺序/字面模板的服从度问题,移植侧无差异可查;按规则 4 留档,必要时由评测负责人确认是否按行为语义(而非字面)断言或重录。
3. **待落实**:FastGPT 应用层配置(应用级系统提示词/思考开关)仍建议对账一次,作为作业 10 等价性证据的一部分。
### 代码地图(截至作业 5)
```text
src/agent/
graph.py 图装配 + 条件路由 + GraphDeps(context_schema) + welcome
state.py AgentState(全部可序列化;client 等依赖不进 state)
events.py 客户端标记 → ClientEvent
transitions.py 白名单校验(LEGAL_TRANSITIONS,含 2004 双走向)
collect.py 采集节点 + validated_reply(一次调用、一次修复预算)
photo.py 拍照节点(四份提示词按 kind×mode 选择)
confirm.py 核实节点(字段提取按 person_index 隔离)
llm.py LLMService Protocol / Fake / OpenAI(超时重试温度) / workflow fake
fields.py 字段三态校验;clock.py 可控时钟
prompts/ 七份版本化提示词(来源 nodeId + 逐字原文)
```