diff --git a/docs/langgraph-backend-migration-plan.md b/docs/langgraph-backend-migration-plan.md
index ad26c11..81bd599 100644
--- a/docs/langgraph-backend-migration-plan.md
+++ b/docs/langgraph-backend-migration-plan.md
@@ -1,1398 +1,354 @@
-# ZNJJ LangGraph 后端改造计划
+# LangGraph 迁移执行作业:从脚手架到等价 API 服务
-> 状态:Draft
-> 日期:2026-07-25
-> 目标项目:`ZNJJ-api-server`
-> 参考项目:`fastapi-langgraph-agent-production-ready-template`
+> 版本:重写版,依据本地 `main` 提交 `22853f1` 及本分支现有后端边界编写。
+>
+> 读者:掌握 Python、异步函数、HTTP 和基本测试,尚未独立交付过 LangGraph 服务的本科生。
+>
+> 最终成果:调用方继续使用现有 API,事故处理流程由 LangGraph 执行,业务数据可以持久化和恢复。
+>
+> 本文是执行计划;下文标为“新增”的目录、命令和能力均为待实现交付物。
-## 1. 背景
+## 作业说明
-当前 ZNJJ 的 `/chat`、`/set_info`、`/get_info` 主要依赖 FastGPT:
+你将接手一个真实的交通事故信息采集服务。现有 FastAPI 服务把对话交给 FastGPT 工作流,本作业要求逐步用 LangGraph 实现同样的外部服务。
-- `/chat` 将用户输入发送给 FastGPT,由模型输出 `状态码`,服务端再解析状态码和正文。
-- `formUpdate` 从 FastGPT 工作流节点的响应中提取。
-- `/set_info`、`/get_info` 通过额外的 FastGPT 对话调用读写工作流变量,并通过删除聊天记录隐藏辅助调用。
-- 事故采集、拍照引导、信息确认、安全转人工等规则大量写在 Prompt 和 FastGPT 工作流 JSON 中。
+先搭建一个可以运行的图,再把它接到现有接口,然后逐项补齐业务能力。每次作业结束,都必须留下一份他人可以运行的程序和验证证据。不要等到所有模块写完才第一次启动服务。
-这种实现使确定性的业务状态依赖模型输出和平台内部结构,存在状态跳转不可控、接口结果不稳定、难以测试、难以审计以及供应商耦合等问题。
+完成作业后,你应当能够解释:一次请求如何变成图输入,节点如何读取和更新状态,条件边为何选择某个阶段,消息如何进入模型,业务字段如何落库,以及客户端收到成功后为何可以相信数据已经保存。
-本次改造使用 LangGraph 承担事故处理流程编排,同时保持现有外部接口协议稳定,使其他团队可以继续通过 `/chat`、`/set_info`、`/get_info` 对接。
+本期迁移范围为 `POST /chat`、`POST /chat?stream=true`、`POST /set_info`、`POST /get_info`、`DELETE /delete_session`。保留 `GET /` 的存活响应。语音 WebSocket、Pipecat、STT/TTS 和语音协议不在本次改造范围;切换文本后端时,必须明确现有语音管线仍依赖 FastGPT 的配置与生命周期,不能因此破坏语音服务启动。
-## 2. 范围
+### 提交规则
-### 2.1 本期范围
+1. 按作业 1—10 顺序推进。每次提交对应一个可独立审查的里程碑。
+2. 每次提交附运行命令、预期结果、实际结果和已知限制;测试失败必须解释原因。
+3. 真实模型只用于后期联调和评测。日常图测试使用固定输出的 fake LLM,能够离线重复执行。
+4. 每个业务规则都要注明来源:代码、工作流节点或评测案例。来源冲突时记录差异及采用依据。
+5. 配置样例使用占位值;对话样例使用合成数据;不得提交凭证、真实身份证和手机号。
+6. 本文给出的工时是单人专注开发的估算,不包含等待基础设施和业务确认的时间。
-- 将 `/chat` 后端从 FastGPT 迁移到 LangGraph。
-- 将 `/set_info`、`/get_info` 改为类型化业务状态的直接读写。
-- 引入 PostgreSQL、LangGraph checkpointer 和业务数据表。
-- 引入面向系统调用的鉴权和 session 级授权。
-- 建立统一配置、结构化日志、指标、链路追踪和脱敏机制。
-- 建立领域状态机、图节点、API 契约、数据库、安全、并发恢复和 LLM 评测。
-- 通过功能开关、shadow 对比和灰度发布逐步替换 FastGPT。
+### 里程碑总览
-### 2.2 本期不包含
+| 作业 | 结束时可以演示什么 | 预计工时 | 分值 |
+|---|---|---:|---:|
+| 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 |
-- Pipecat 语音管线改造。
-- STT、TTS、VAD、语音打断和 WebSocket 协议改造。
-- 面向终端用户的注册、密码登录和账号体系。
-- mem0、pgvector 或跨 session 长期语义记忆。
-- 前端或其他调用团队的接口升级。
-- 将 FastGPT 工作流 JSON 自动转换成 LangGraph。
+预计合计 92—140 小时,可按每周 12—18 小时安排为 7—10 周。先完成离线里程碑,再安排真实模型和 PostgreSQL 的集成环境。
-### 2.3 兼容性原则
+## 开工前的阅读地图
-- 保持 `/chat`、`/set_info`、`/get_info` 路径不变。
-- 保持现有请求和响应字段的 camelCase 命名。
-- 保持响应体中的字符串业务码,例如 `"200"`、`"500"`。
-- 保持 `/chat?stream=true` 的 SSE 方式和既有事件名称。
-- 兼容期内不同时改变接口协议和后端实现。
-- 内部统一使用 snake_case 和类型化模型,通过 Pydantic alias 适配外部协议。
+只做必要阅读,记录一页笔记后立即进入作业 1。不要先照搬大型生产模板。
-## 3. 改造目标
+| 文件 | 阅读时应回答的问题 |
+|---|---|
+| `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/20261016/事故信息采集2025118.json` | 当前各模式实际走哪些节点和条件边? |
+| `evals/cases/`、`evals/README.md` | 当前已经覆盖哪些对话?runner 实际断言了什么? |
+| `test/api/`、`test/backends/`、`test/test_client_mode.py` | 已有后端边界测试和公共契约测试是否与当前代码一致? |
-### 3.1 业务目标
+阅读 LangGraph 官方文档中的 `StateGraph`、state/reducer、conditional edges、`InMemorySaver`、`AsyncPostgresSaver`、异步执行和 streaming 章节。采用依赖版本对应的 API,首次提交记录版本和文档链接,避免混用旧教程。
-- 自然语言轮次继续使用 `XXXX` 前缀,由模型在单次回复中给出候选状态码和正文。
-- 服务端负责流式解析状态前缀、校验状态码及迁移合法性,并将通过校验的状态持久化。
-- 拍照完成、连续拍照失败、无回复、人工转接等使用结构化业务事件。
-- 事故字段经过结构化提取和代码校验后才能写入。
-- `/chat`、`/set_info`、`/get_info` 使用同一份业务状态。
-- 服务重启或请求失败后能够从持久化状态恢复。
-- 同一 session 的并发请求不会静默覆盖数据或重复推进流程。
+有两处特别需要辨别:`prompts/20261012/事故信息收集.txt` 与最新工作流内嵌提示词并不完全一致;`docs/chat-stream-mode.md` 未列出全部现有字段和事件。以当前代码、最新工作流和新评测案例交叉核对,不能单独以旧文档决定兼容行为。
-### 3.2 工程目标
+## 必须守住的外部契约
-- API、应用服务、LangGraph、领域模型、数据库和基础设施职责分离。
-- 所有外部调用、数据库操作和模型调用均为异步。
-- 配置启动时严格校验,生产环境禁止不安全默认值。
-- 日志、指标和 trace 不泄漏身份证、手机号、车牌、Token 等敏感数据。
-- 核心状态迁移不依赖真实 LLM 即可完成自动化测试。
-- 能够按调用方或流量比例灰度切换 FastGPT/LangGraph。
+以下是执行作业时的检查表。作业 2 将其变成可运行的契约夹具。
-## 4. 核心架构决策
+| 项目 | 必须保留的行为 |
+|---|---|
+| `/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 状态与响应体业务码,保存当前认证、限流、后端失败和参数错误的样例 |
-| 编号 | 决策 | 说明 |
-|---|---|---|
-| ADR-001 | 保留 FastAPI,对 API 层做薄适配 | Endpoint 只负责鉴权、校验和协议转换 |
-| ADR-002 | 使用 LangGraph `StateGraph` | 显式表达业务节点、条件边和持久化状态 |
-| ADR-003 | 本版保留 state prefix | 自然语言轮次由模型输出 `XXXX`,服务端负责解析和合法性校验 |
-| ADR-004 | 不为状态码增加第二次 LLM 调用 | 同一次模型回复产生状态前缀和用户可见正文;代码只校验候选状态,不再次做语义判断 |
-| ADR-005 | PostgreSQL 是生产数据库 | 同时承载 LangGraph checkpoint 和应用业务表 |
-| ADR-006 | Checkpoint 与业务数据分离 | Checkpoint 用于恢复;业务表用于 `/get_info`、审计和报表 |
-| ADR-007 | 只使用 thread 级短期记忆 | 不引入跨事故会话的长期语义记忆 |
-| ADR-008 | B2B 服务身份鉴权 | 优先使用网关/OIDC Client Credentials,API Key 仅作兼容方案 |
-| ADR-009 | 每个 session 串行修改 | 使用数据库锁和 `state_version` 防止并发覆盖 |
-| ADR-010 | 业务副作用必须幂等 | 通过 `event_id` 和 `Idempotency-Key` 支持安全重试 |
-| ADR-011 | 生产环境持久化失败时拒绝服务 | 不允许退化为无 checkpointer 的有状态流程 |
-| ADR-012 | 先保持接口兼容,再设计 v2 | 本次不调整历史协议中的 JSON 字符串等设计 |
+现有 SSE 允许 `formUpdate` 在状态码前后到达,不能把旧计划中的固定事件顺序当成既有契约。最低保证为:`stage_code` 先于正文、一次成功只有一个 `done`、失败以一个 `error` 终止且不再发送 `done`。如果要进一步收紧表单事件顺序,须先用真实消费者验证并记录约定。
-## 5. 目标架构
+模型流式和非流式的一致性,使用**同一固定模型输出或同一已记录回放**验证。对真实模型分别调用两次,不能要求自由生成文本逐字相同。
-```mermaid
-flowchart LR
- Caller["其他团队"]
- Auth["服务鉴权与授权"]
- API["FastAPI API Adapter"]
- Service["AccidentAgentService"]
- Graph["LangGraph Accident Workflow"]
- Rules["确定性规则与校验"]
- LLM["结构化 LLM 服务"]
- Checkpoint[("LangGraph Checkpoint")]
- DomainDB[("业务状态与事件")]
- Telemetry["Logs / Metrics / Traces"]
+## 作业 1:搭建并运行 LangGraph 脚手架
- Caller --> Auth --> API --> Service --> Graph
- Graph --> Rules
- Graph --> LLM
- Graph <--> Checkpoint
- Graph <--> DomainDB
- API --> Telemetry
- Service --> Telemetry
- Graph --> Telemetry
+**学习目标:** 理解图、状态、节点、边与 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 examples/langgraph_smoke.py
+python -m pytest test/agent/test_scaffold.py -q
```
-### 5.1 推荐目录
+预期三个调用的轮数分别为 `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。图返回 `1002固定文本`,避免同时重写供应商边界和公共协议。
+3. 提供 `AGENT_BACKEND=fastgpt|langgraph` 类型化配置和依赖工厂。LangGraph fake 模式启动不得要求 FastGPT 密钥;仍启用语音 FastGPT 路径时单独校验所需配置。
+4. 将资源初始化和关闭纳入 lifespan。两种后端分别执行启动冒烟验证,不仅在测试中覆盖依赖。
+5. 在现有 `test/api/` 补充 HTTP 级契约测试;检查 OpenAPI schema、JSON 响应和 SSE 事件。
+6. 先运行已有测试并记录基线。已知需检查的过期预期包括:默认请求缺少 `clientMode`、`ChatInput.client_mode` 仍期望空串、`nextStageCode` 把五字符视为超长。按当前实际契约修正预期,不能为消除红灯收窄合法协议。
+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. 建立带来源的转换表,至少包含 `0000—0005`、`1001/1002`、单车、双车和 `3001/3002`。转换条件必须读取字段和当前问题,不能只判断两个状态码。
+5. 拍照失败通知收到一次就产生 `0005`;客户端已经完成三次统计,服务端不得再累计三次。
+6. 连续两次无回复产生 `0004`,中间有效回复如何重置计数必须有测试。
+7. 把“本轮输出字段”在每轮开始时清空;历史事故字段继续保留,避免旧 `formUpdate` 在下一轮重复返回。
+
+**交付物:** `events.py`、`transitions.py`、状态模型、规则来源表、参数化转换测试。
+
+**验收:** 无模型调用即可走完拍照事件链;在拍照中输入“继续”保持当前步骤;非法跨阶段转换不写状态;明确的转人工事件可以终止当前流程。
+
+**讲解题:** 为什么外部看到 `1002`,内部仍必须区分采集、单车核实和双车核实?
+
+## 作业 4:实现事故信息采集与 LLM 接入
+
+**学习目标:** 用模型理解自然语言,用代码验证并提交业务状态。
+
+**任务:**
+
+1. 从最新工作流提取事故采集提示词和节点路由,保存为可审查文本并注明源节点 ID。明确 `direct` 与 `browser_addon` 的差异。
+2. 实现可注入的异步 LLM 服务:fake 与真实 provider 使用相同接口,包含超时、取消、有限重试和模型配置。
+3. 普通自然语言轮次由同一次回复生成 `XXXX正文`,代码验证前缀、状态枚举和转换守卫。不得仅为复判状态再调用一次模型。
+4. 定义候选字段的结构化提取接口和校验。确需独立提取调用时记录原因、调用次数和延迟;不得从用户可见回复中靠正则猜全部业务字段。
+5. 对每个字段区分未知、候选、已确认。最新工作流要求:已经明确提供的有效信息直接记录,跳过已知项;一次补齐剩余信息后直接分流;不能沿用旧提示词逐项重复确认。
+6. 注入可控时钟和时区,校验相对时间、当前时间、未来时间与边界;测试不依赖运行机器当天时间。
+7. 实现人伤明确肯定、明确否定、模糊/ASR 破碎三类输入;未知时停留并澄清,不凭关键词直接转人工。
+8. 用户已不在现场仍继续采集。数量未知不能从“追尾”等词擅自推断。已明确无人伤后不重复问,新的明确矛盾信息可以触发更新。
+9. 澄清阈值按阶段配置。采集的计入失败与核实的计入失败规则不同,不能全局统一成“任意无效回答两次转人工”。
+10. 对 prefix 缺失、重复、未知或迁移非法最多做一次受控修复重试;失败返回可识别错误,不提交业务状态,也不泄漏原始模型内容。
+
+**交付物:** 版本化提示词、字段候选与校验模型、采集节点、模型服务、fake 对话夹具、真实模型评测报告。
+
+**验收:** 离线用例覆盖正常单/双车、一次提供全部信息、一次补齐剩余字段、数量未知、无人伤否定、模糊人伤、不在现场、未来时间、主动转人工、澄清重置。真实模型至少运行 `collection_skip_known_info` 和 `no_false` 全部现有案例。
+
+现有评测 runner 只检查 SSE 状态码,无法证明“没有重复询问”。本作业必须增加当前问题 ID、已确认字段及提问行为的可检查结果,或提供带人工判定的脱敏对话报告。新增字段不进入公共 API。
+
+## 作业 5:完成拍照和人员信息核实
+
+**学习目标:** 实现阶段交接,避免换节点后重复提问或丢失上一轮答案。
+
+**任务:**
+
+1. 单车按 `2000 → 2001 → 2002 → 2003 → 2005` 推进,保留历史 `2004` 映射但正常新流程不再询问车牌。
+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 核实完成、双车两方核实完成、拒绝驾驶员身份、号码分段更正、阶段交接不重复提问场景。所有确定性拍照步骤为零次 LLM 调用。
+
+## 作业 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 后才能释放正文。覆盖 `` 和结束标签被任意切分、正文中再次出现标签、前缀长期不闭合、未知和非法状态码;设缓存上限和超时。
+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/
- main.py
- api/
- dependencies.py
- endpoints.py
- auth/
- dependencies.py
- models.py
- verifier.py
- policies.py
- agent/
- graph.py
- state.py
- events.py
- transitions.py
- prompts/
- accident_collection.md
- field_extraction.md
- response_generation.md
- nodes/
- normalize_input.py
- hydrate_state.py
- global_handoff_gate.py
- safety_precheck.py
- extract_fields.py
- validate_fields.py
- route_phase.py
- photo_flow.py
- party_verification.py
- apply_form_patch.py
- compose_response.py
- core/
- config.py
- database.py
- logging.py
- metrics.py
- observability.py
- models/
- api_client.py
- accident_session.py
- accident_state.py
- accident_event.py
- idempotency.py
- repositories/
- accident_state.py
- idempotency.py
- schemas/
- api.py
- domain.py
- llm.py
- services/
- accident_agent.py
- llm.py
- utils/
-tests/
- unit/
- graph/
- contract/
- integration/
- security/
- performance/
-alembic/
+ 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 路由、灰度、回退演练
```
-## 6. API 契约设计
+目录可以按实际实现微调,但每个交付物必须有明确位置。保持现有 `test/`,无需为套用模板重命名整个测试树。
-### 6.1 `/chat`
+## 评分与完成条件
-外部接口保持:
+每次作业按表中分值计分:功能与正确性占该作业 60%,可复现的验证占 30%,设计讲解与限制说明占 10%。只提交图示或设计文档、没有运行结果,不能取得该作业的功能分。
-```text
-POST /chat
-POST /chat?stream=true
-```
+以下为结课必过项,分数不能替代:公共契约保持兼容;四个接口使用一致的会话路由和状态;确定性事件不能非法跳步;成功返回前数据已持久化;失败可恢复;删除不留下可被复活的旧会话;完整请求链路在 LangGraph 文本模式下不调用 FastGPT。
-内部统一请求:
+最终演示使用一个全新会话:启动服务 → 采集双车事故 → 完成拍照 → 注入人员预填值 → 更正一段手机号 → 重启服务 → 继续第二方核实 → 查询全部信息 → 删除会话 → 验证无残留。随后展示一次明确人伤分流、一次模糊人伤澄清和一次数据库失败的 SSE error。
-```python
-class UserMessageEvent(BaseModel):
- event_id: str
- text: str
- need_form_update: bool = False
- use_text_chunk: bool = False
-```
+## 执行时需要登记的环境决策
-内部统一非流式结果:
-
-```python
-class AgentTurnResult(BaseModel):
- output_text: str
- stage_code: StageCode
- stage_name: str
- form_update: dict[str, object]
- state_version: int
-```
-
-非流式 API 将 `AgentTurnResult` 转换为现有 `ProcessResponse_chat`。
-自然语言轮次的原始模型回复契约:
-
-```text
-1002请问事故中有没有人员受伤?
-```
-
-服务端从开头解析候选状态码,校验通过后写入 `AgentTurnResult.stage_code`,并从 `output_text` 中移除标签。标签缺失、格式错误、未知状态码或非法迁移不得直接对外返回。
-
-流式 API 使用同一张图,固定输出顺序:
-
-```text
-stage_code state prefix 解析并校验成功后发送,每轮最多一次
-formUpdate 有字段更新时发送
-text_delta 零个或多个
-done 成功时必须且仅一次
-```
-
-错误事件:
-
-```text
-error 失败时必须且仅一次,为终止事件
-```
-
-兼容期内继续支持现有事件名称:
-
-- `stage_code`
-- `formUpdate`
-- `text_delta`
-- `done`
-- `error`
-
-#### 6.1.1 流式与非流式一致性
-
-- 流式所有 `text_delta.text` 拼接后必须等于非流式 `outputText`。
-- `stage_code.nextStageCode` 必须等于从同一条模型回复解析并校验后的非流式 `nextStageCode`。
-- `formUpdate` 必须来自同一份合法状态 patch。
-- 不允许为流式和非流式维护两套业务实现。
-
-### 6.2 `/set_info`
-
-`/set_info` 不再调用 LLM,不再创建辅助聊天记录。
-
-处理流程:
-
-```text
-鉴权
- -> session 对象级授权
- -> key 白名单校验
- -> value 类型转换与领域校验
- -> SetInfoEvent
- -> LangGraph apply_external_update 分支
- -> 幂等写业务状态与事件
- -> 保存 checkpoint
-```
-
-只允许修改字段注册表中声明的业务字段:
-
-```python
-FIELD_REGISTRY = {
- "ywrysw": FieldSpec(type=bool, group="acdinfo"),
- "ywfjdc": FieldSpec(type=bool, group="acdinfo"),
- "ywmtc": FieldSpec(type=bool, group="acdinfo"),
- "bjrjs": FieldSpec(type=str, group="acdinfo"),
- "sgfssj": FieldSpec(type=datetime, group="acdinfo"),
- "sfsgxc": FieldSpec(type=bool, group="acdinfo"),
- "jdcsl": FieldSpec(type=int, group="acdinfo"),
- "sgyy": FieldSpec(type=str, group="acdinfo"),
- "xm1": FieldSpec(type=str, group="acdhuman1"),
- "hpzl1": FieldSpec(type=str, group="acdhuman1"),
- "hphm1": FieldSpec(type=str, group="acdhuman1"),
- "sfzmhm1": FieldSpec(type=str, group="acdhuman1", sensitive=True),
- "sjhm1": FieldSpec(type=str, group="acdhuman1", sensitive=True),
- "xm2": FieldSpec(type=str, group="acdhuman2"),
- "hpzl2": FieldSpec(type=str, group="acdhuman2"),
- "hphm2": FieldSpec(type=str, group="acdhuman2"),
- "sfzmhm2": FieldSpec(type=str, group="acdhuman2", sensitive=True),
- "sjhm2": FieldSpec(type=str, group="acdhuman2", sensitive=True),
-}
-```
-
-禁止外部接口修改:
-
-- `phase`
-- `stage_code`
-- `clarification_counts`
-- `no_response_count`
-- `handoff_reason`
-- `state_version`
-- LangGraph 内部路由字段
-
-### 6.3 `/get_info`
-
-`/get_info` 是纯读取接口:
-
-```text
-鉴权
- -> session 对象级授权
- -> 从业务投影读取当前版本
- -> 按 key/group 转换
- -> 返回现有 ProcessResponse_get
-```
-
-兼容以下 key:
-
-- `all`
-- `acdinfo`
-- `acdhuman1`
-- `acdhuman2`
-- 字段注册表中的单个字段
-
-兼容期内 `value` 保持为 JSON 编码后的字符串。将来若设计 v2,再改成真正的 JSON 对象。
-
-### 6.4 错误处理
-
-内部使用稳定错误类型:
-
-```text
-AUTHENTICATION_FAILED
-PERMISSION_DENIED
-SESSION_NOT_FOUND
-INVALID_FIELD
-INVALID_FIELD_VALUE
-SESSION_CONFLICT
-MODEL_UNAVAILABLE
-STATE_PERSISTENCE_FAILED
-INTERNAL_ERROR
-```
-
-API Adapter 负责映射到兼容响应体。兼容期不改变其他团队依赖的业务码;服务端日志和 metrics 使用内部错误类型,不解析 `msg` 文本。
-
-## 7. 领域状态和事件
-
-### 7.1 状态模型
-
-```python
-class AccidentState(TypedDict):
- messages: Annotated[list[AnyMessage], add_messages]
- tenant_id: str
- session_id: str
- state_version: int
-
- phase: Phase
- stage_code: StageCode
- input_event: InputEvent
-
- accident: AccidentInfo
- parties: list[PartyInfo]
- photo_step: PhotoStep | None
-
- pending_question: QuestionId | None
- clarification_counts: dict[str, int]
- no_response_count: int
-
- form_patch: dict[str, object]
- response_text: str
- handoff_reason: HandoffReason | None
- last_error: str | None
-```
-
-约束:
-
-- State 中只存可序列化数据。
-- State 中不能保存数据库连接、HTTP Client、LLM Client 等运行时对象。
-- 不在 State 中保存音频、图片 base64 或未限制大小的原始文件。
-- 敏感字段进入 checkpoint 前必须确认数据保护和保留策略。
-
-### 7.2 输入事件
-
-```text
-UserMessageEvent
-SetInfoEvent
-PhotoCompletedEvent
-PhotoRecognitionFailedEvent
-NoResponseEvent
-SessionStartedEvent
-```
-
-兼容期由 API 边界把历史魔法字符串转换成结构化事件,例如:
-
-```text
-【拍摄完成】 -> PhotoCompletedEvent
-【客户端连续3次拍摄识别失败:原因】 -> PhotoRecognitionFailedEvent
-【用户无回复】 -> NoResponseEvent
-```
-
-图内部不再根据自然语言魔法字符串推进流程。
-
-### 7.3 状态码
-
-使用枚举和集中式状态转换表:
-
-```text
-0000 完成
-0001 主动转人工
-0002 语义连续无法识别
-0003 人伤或复杂情况转人工
-0004 长时间无回复
-0005 连续拍照识别失败
-1001 未准备好
-1002 信息采集中
-2000-2005 单车拍照流程
-2010-2016 双车拍照流程
-3001-3002 信息确认流程
-```
-
-任何非法转换必须记录为错误并停止处理,禁止由模型绕过状态机。
-本版状态码职责:
-
-- 模型根据当前 Prompt 和会话状态输出候选状态码。
-- Prefix 必须位于回复开头,格式为 `四位数字`。
-- 服务端 parser 负责处理标签被拆分到多个流式 chunk 的情况。
-- 服务端 validator 检查状态码是否存在于枚举,以及是否允许从当前状态迁移。
-- 标签缺失、重复、未知或迁移非法时,最多进行一次受控重试;仍失败时按配置返回错误或进入保守转人工状态。
-- `PhotoCompletedEvent`、`PhotoRecognitionFailedEvent`、`NoResponseEvent`、`SetInfoEvent` 等确定性事件可以不调用 LLM,直接由代码产生目标状态。
-- 代码不为自然语言轮次再次调用 LLM 来“复判”状态。
-
-## 8. LangGraph 设计
-
-### 8.1 主图
-
-```mermaid
-flowchart TD
- START --> Normalize["normalize_input"]
- Normalize --> Hydrate["hydrate_state"]
- Hydrate --> HandoffGate["global_handoff_gate"]
- HandoffGate --> Safety["safety_precheck"]
- Safety --> Route{"route_phase"}
-
- Route --> Collection["accident_collection"]
- Route --> SinglePhoto["single_vehicle_photo"]
- Route --> DoublePhoto["two_vehicle_photo"]
- Route --> Verification["party_verification"]
- Route --> ApplyExternal["apply_external_update"]
- Route --> Handoff["handoff"]
- Route --> Complete["complete"]
-
- Collection --> Validate["validate_fields"]
- Verification --> Validate
- Validate --> Persist["apply_form_patch"]
- SinglePhoto --> Persist
- DoublePhoto --> Persist
- ApplyExternal --> Persist
- Persist --> Compose["compose_response"]
- Handoff --> Compose
- Complete --> Compose
- Compose --> END
-```
-
-### 8.2 确定性节点
-
-以下节点原则上不调用 LLM:
-
-- `normalize_input`
-- `hydrate_state`
-- `global_handoff_gate` 中的显式指令判断
-- `safety_precheck` 中的确定性高风险规则
-- `route_phase`
-- 拍照状态推进
-- 澄清计数
-- 无回复计数
-- 字段格式校验
-- 状态转换合法性校验
-- `apply_external_update`
-- `apply_form_patch`
-- 固定话术选择
-- `handoff`
-- `complete`
-
-### 8.3 LLM 节点
-
-LLM 仅用于:
-
-- 事故描述字段提取。
-- 用户回答有效性分类。
-- 人伤或复杂情况的语义识别;该判断应与当前阶段的 state-prefix 回复合并在同一次模型调用中。
-- 车牌、时间、车辆数量等自然语言归一化的辅助判断。
-- 必要的自然语言措辞。
-
-自然语言回复契约:
-
-```text
-XXXX用户可见正文
-```
-
-如果某个阶段还需要结构化字段提取,可在独立节点中使用 Pydantic schema;但不得仅为了再次判断状态码而增加第二次 LLM 调用。
-
-处理原则:
-
-- State prefix 解析或迁移校验失败时最多进行一次受控重试。
-- 模型输出只是候选数据,不能直接写业务状态。
-- 安全判断不确定时进入确认或人工处理,不允许冒险放行。
-- 模型可以通过 prefix 提议 `stage_code`,但只有通过枚举和迁移校验后才能写入 State;`phase` 和计数器仍只由代码修改。
-- 自然语言轮次的状态前缀和正文由同一次模型回复产生;固定系统事件优先使用代码模板,减少模型延迟和随机性。
-LLM 调用预算:
-
-- 确定性事件默认 0 次 LLM。
-- 普通自然语言轮次默认 1 次 LLM,由阶段节点同时完成必要的语义理解、候选状态前缀和用户可见正文。
-- 只有独立字段抽取无法与阶段回复合并且确有业务必要时,才允许第 2 次 LLM;必须单独记录指标和延迟。
-- State prefix 本身不得导致第 2 次 LLM 调用。
-
-## 9. 鉴权与授权
-
-### 9.1 推荐方案
-
-优先使用公司统一网关或身份平台的 OIDC Client Credentials:
-
-```text
-调用团队
- -> client_id/client_secret
- -> 身份平台签发 JWT
- -> ZNJJ 通过 JWKS 验证
-```
-
-JWT 必须验证:
-
-- 允许的签名算法。
-- `iss`。
-- `aud`。
-- `exp`、`nbf`。
-- `sub` 或 `client_id`。
-- `scope`。
-- 可选 `jti`。
-
-Token 中禁止包含身份证、手机号、车牌等敏感信息。
-
-### 9.2 权限
-
-```text
-chat:write
-session:read
-session:write
-```
-
-建议映射:
-
-| 接口 | Scope |
-|---|---|
-| `/chat` | `chat:write` |
-| `/get_info` | `session:read` |
-| `/set_info` | `session:write` |
-
-### 9.3 对象级授权
-
-任何 session 查询都必须包含当前身份中的 `tenant_id`:
-
-```sql
-WHERE tenant_id = :current_tenant_id
- AND session_id = :session_id
-```
-
-内部 LangGraph `thread_id`:
-
-```python
-thread_id = f"{tenant_id}:{session_id}"
-```
-
-不能把随机 `sessionId` 当成权限校验。
-
-### 9.4 API Key 兼容方案
-
-如果暂时没有统一身份平台:
-
-- 使用 `X-API-Key`。
-- 每个调用团队独立 key。
-- key 使用高强度随机值。
-- 数据库只存哈希/HMAC。
-- 支持 `key_id`、启停、过期和轮换。
-- key 只通过 HTTPS 传输。
-- 日志和 trace 永不记录 key。
-- 后续可以平滑迁移到 JWT。
-
-### 9.5 限流与资源保护
-
-- 按 `client_id` 和 endpoint 限流。
-- 限制 `text` 长度、请求体大小和 SSE 连接时间。
-- 限制单 session 每分钟请求数。
-- 限制调用方并发数和 LLM 配额。
-- 限流 label 不包含 sessionId,避免高基数。
-
-## 10. Memory 设计
-
-### 10.1 短期记忆
-
-使用 LangGraph checkpointer 保存 thread 级状态:
-
-- 当前会话阶段。
-- 已确认字段。
-- 待回答问题。
-- 澄清/无回复次数。
-- 最近消息。
-- 人工转接原因。
-
-相同 `tenant_id + sessionId` 使用相同 `thread_id`。
-
-### 10.2 业务状态
-
-业务字段必须同步投影到应用表:
-
-- `/get_info` 读取业务投影。
-- checkpoint 用于流程恢复。
-- 业务事件表用于审计和故障对账。
-- 图每次开始时读取业务状态和版本,防止 checkpoint 与外部更新脱节。
-
-### 10.3 上下文控制
-
-- checkpoint 可保存最终文本消息。
-- LLM 输入只使用结构化业务状态、当前问题和最近若干轮消息。
-- 超过 token 阈值后生成脱敏摘要。
-- 已经确认并进入业务状态的字段不依赖历史消息记忆。
-- 不保存音频、图片 base64。
-
-### 10.4 长期记忆
-
-本期不实施:
-
-- mem0。
-- pgvector 语义记忆。
-- 跨 session 用户画像。
-- 从历史事故召回个人信息。
-
-未来如有明确需求,必须重新完成授权、隔离、保留期限和敏感数据评审。
-
-## 11. 数据库设计
-
-### 11.1 数据库与 schema
-
-使用 PostgreSQL:
-
-```text
-langgraph schema
- LangGraph 自管理 checkpoint 表
-
-app schema
- Alembic 管理业务表
-```
-
-生产环境禁止以 SQLite 替代。
-
-### 11.2 业务表
-
-#### `api_clients`
-
-```text
-id
-tenant_id
-client_id
-credential_hash
-scopes
-status
-expires_at
-created_at
-updated_at
-```
-
-如果完全使用外部 OIDC/JWT,可只保留客户端策略和 tenant 映射,不保存 credential。
-
-#### `accident_sessions`
-
-```text
-tenant_id
-session_id
-status
-phase
-stage_code
-state_version
-created_at
-updated_at
-last_activity_at
-completed_at
-```
-
-约束:
-
-```text
-UNIQUE (tenant_id, session_id)
-```
-
-#### `accident_state`
-
-```text
-tenant_id
-session_id
-form_data JSONB
-state_version
-updated_at
-```
-
-首版可使用 JSONB 保持与现有字段模型兼容;稳定后再按查询和监管需要拆分规范化表。
-
-#### `accident_events`
-
-```text
-event_id
-tenant_id
-session_id
-event_type
-actor_client_id
-payload
-previous_version
-new_version
-created_at
-```
-
-约束:
-
-```text
-UNIQUE (tenant_id, event_id)
-```
-
-#### `idempotency_records`
-
-```text
-client_id
-idempotency_key
-request_hash
-response_data
-expires_at
-created_at
-```
-
-约束:
-
-```text
-UNIQUE (client_id, idempotency_key)
-```
-
-### 11.3 一致性和幂等
-
-- 每个输入事件有稳定 `event_id`。
-- 调用方可发送 `Idempotency-Key`。
-- 同一个 key 携带不同请求体时返回冲突。
-- `apply_form_patch` 使用 `(tenant_id, event_id)` 去重。
-- 图节点重试或故障恢复时,重复副作用必须成为 no-op。
-- 写业务状态和 `accident_events` 必须处于同一数据库事务。
-- 响应成功前业务状态必须持久化完成。
-
-### 11.4 并发
-
-同一 session 的修改操作串行化:
-
-- `SELECT ... FOR UPDATE` 锁定 `accident_sessions`;或
-- 使用 PostgreSQL advisory lock。
-
-配合乐观版本控制:
-
-```sql
-UPDATE accident_state
-SET form_data = :form_data,
- state_version = state_version + 1
-WHERE tenant_id = :tenant_id
- AND session_id = :session_id
- AND state_version = :expected_version;
-```
-
-更新行数为 0 时按 `SESSION_CONFLICT` 处理。
-
-每个请求/异步任务使用独立 `AsyncSession`,不能跨并发任务共享。
-
-### 11.5 迁移
-
-- Alembic 只管理 `app` schema。
-- 排除 LangGraph checkpointer 自管理表。
-- 不在应用启动时调用 ORM `create_all()`。
-- 自动生成 migration 后必须人工审查。
-- CI 执行 `alembic check`。
-- 测试从空数据库升级到最新 revision。
-- 上线前验证 downgrade 或制定明确的 forward-fix 策略。
-
-### 11.6 数据保护
-
-- 数据库和备份加密。
-- 身份证、手机号等高敏字段应用层加密。
-- 需要等值查询时额外保存不可逆 HMAC。
-- 日志和 trace 只保留掩码。
-- checkpoint、业务表、事件、幂等记录和 trace 定义统一保留期限。
-- session 清理必须覆盖 checkpoint 和业务数据。
-
-## 12. 配置设计
-
-使用 `pydantic-settings`:
-
-```python
-class Settings(BaseSettings):
- environment: Literal["development", "test", "staging", "production"]
- auth: AuthSettings
- database: DatabaseSettings
- llm: LLMSettings
- langgraph: LangGraphSettings
- observability: ObservabilitySettings
- security: SecuritySettings
-
- model_config = SettingsConfigDict(
- env_prefix="ZNJJ_",
- env_nested_delimiter="__",
- env_file=".env",
- extra="ignore",
- )
-```
-
-环境变量示例:
-
-```text
-ZNJJ_ENVIRONMENT=production
-ZNJJ_DATABASE__DSN=postgresql+asyncpg://...
-ZNJJ_AUTH__ISSUER=https://...
-ZNJJ_AUTH__AUDIENCE=znjj-api
-ZNJJ_AUTH__JWKS_URL=https://.../.well-known/jwks.json
-ZNJJ_LLM__PROVIDER=openai
-ZNJJ_LLM__MODEL=...
-ZNJJ_LANGGRAPH__BACKEND=langgraph
-ZNJJ_OBSERVABILITY__TRACING_ENABLED=true
-```
-
-原则:
-
-- `.env` 只用于本地开发。
-- `.env.example` 只包含字段和非敏感示例。
-- 生产 secret 来自 Secret Manager/Kubernetes Secret。
-- 数据库密码、API Key、JWT secret 不允许有默认值。
-- 配置校验失败时启动失败。
-- 启动时输出脱敏配置摘要。
-- Prompt 和状态转换规则版本化存放在仓库。
-- 功能开关使用类型化配置。
-- 依赖写入 `pyproject.toml` 并使用 lockfile 固定。
-- 不直接复制参考项目无上限的 `>=` 依赖策略。
-
-主要功能开关:
-
-```text
-AGENT_BACKEND=fastgpt|langgraph
-LANGGRAPH_SHADOW_ENABLED=true|false
-LANGFUSE_ENABLED=true|false
-OTEL_ENABLED=true|false
-```
-
-## 13. 可观测性
-
-### 13.1 结构化日志
-
-统一使用 structlog,生产输出 JSON。
-
-每条请求日志绑定:
-
-```text
-request_id
-trace_id
-client_id
-tenant_id
-session_id_hash
-event_id
-endpoint
-graph_node
-stage_code
-error_type
-```
-
-事件名称使用 `lowercase_with_underscores`。
-
-禁止记录:
-
-- JWT/API Key。
-- 数据库密码、LLM API Key。
-- 原始身份证和手机号。
-- 未脱敏车牌。
-- 完整 Prompt。
-- 完整用户输入和模型输出。
-- 完整 `formUpdate`。
-- checkpoint 原始内容。
-
-异常使用 `logger.exception()` 保留 traceback,同时确保异常参数已脱敏。
-
-### 13.2 Request ID
-
-- 每个请求生成或验证 `X-Request-ID`。
-- 返回响应头 `X-Request-ID`。
-- 不信任无限长度或任意字符的上游 request ID。
-- request ID 贯穿日志、trace、LLM 调用和数据库事件。
-
-### 13.3 Metrics
-
-基础 HTTP 指标:
-
-```text
-http_requests_total{endpoint,status}
-http_request_duration_seconds{endpoint}
-```
-
-业务和 Agent 指标:
-
-```text
-agent_turn_duration_seconds
-agent_stream_ttfb_seconds
-llm_request_duration_seconds{model,result}
-llm_tokens_total{model,direction}
-state_prefix_parse_failure_total{reason}
-graph_node_duration_seconds{node}
-stage_transition_total{from_stage,to_stage}
-handoff_total{reason}
-field_validation_failure_total{field}
-checkpoint_operation_total{operation,result}
-session_conflict_total
-idempotency_replay_total
-```
-
-禁止把 `sessionId`、request ID、用户 ID 放入 Prometheus label。
-
-### 13.4 Tracing
-
-- OpenTelemetry 覆盖 HTTP、数据库、LangGraph 节点和外部 LLM。
-- 一个 API 请求对应一个 trace。
-- 每个图节点、LLM 调用、业务状态写入是独立 span。
-- Langfuse 仅用于脱敏后的 LLM trace、Prompt 版本和评测。
-- 生产环境配置采样率。
-- 错误、转人工和状态冲突请求可提高采样概率。
-- 不把高敏原文发送到外部 tracing 平台。
-
-### 13.5 健康检查
-
-```text
-GET /health/live
-GET /health/ready
-```
-
-`live` 只判断进程是否存活。
-
-`ready` 至少检查:
-
-- 配置已加载。
-- PostgreSQL 可连接。
-- LangGraph checkpointer 可用。
-- 必要的模型配置存在。
-
-生产持久化不可用时 readiness 必须失败。
-
-### 13.6 建议告警
-
-- `/chat` 5xx 或内部错误率。
-- P95/P99 响应时间和流式 TTFB。
-- LLM 超时、state prefix 解析失败和非法状态迁移率。
-- checkpoint 写入失败。
-- session 冲突率。
-- 异常状态转换。
-- `0002/0003/0005` 转人工比例异常变化。
-- 数据库连接池使用率。
-
-## 14. 测试策略
-
-### 14.1 领域单元测试
-
-不连接数据库、不调用 LLM,覆盖:
-
-- 每个状态允许的下一状态。
-- 单车和双车拍照不可跳步。
-- 全局转人工优先级。
-- 人伤/复杂情况路由。
-- 澄清次数和无回复次数。
-- 时间、车辆数量、手机号、身份证、车牌校验。
-- 字段白名单和类型转换。
-- `formUpdate` diff。
-- 非法状态转换。
-- Prefix 缺失、重复、未知状态码。
-- Prefix 被拆分到多个流式 chunk。
-
-使用参数化测试维护状态转换矩阵。
-
-### 14.2 节点测试
-
-使用 fake LLM 和 `InMemorySaver`:
-
-- 单独测试每个节点。
-- 结构化 LLM 输出固定可控。
-- 断言状态更新和路由。
-- 断言副作用请求,而非执行真实副作用。
-- 不对自由生成文本做逐字断言。
-
-### 14.3 Graph 场景测试
-
-至少覆盖:
-
-- 单车正常流程。
-- 双车正常流程。
-- 主动转人工。
-- 人伤立即转人工。
-- 多车/复杂情况转人工。
-- 连续无效回答。
-- 连续无回复。
-- 连续拍照识别失败。
-- 车牌纠正。
-- 身份证/手机号分段补充和二次确认。
-- `/set_info` 后继续 `/chat`。
-- 服务重启后恢复。
-
-断言:
-
-- 状态码序列。
-- 最终业务字段。
-- 每轮 `formUpdate`。
-- 转人工原因。
-- checkpoint 恢复结果。
-
-### 14.4 API 契约测试
-
-使用 FastAPI ASGI + `httpx.AsyncClient`:
-
-- 三个接口的请求和响应 schema。
-- camelCase 字段。
-- 历史业务码。
-- `all/acdinfo/acdhuman1/acdhuman2`。
-- 单字段查询。
-- `/set_info` 合法/非法字段。
-- SSE 事件名称和顺序。
-- 流式拼接与非流式结果一致。
-- 空值、超长文本、非法 JSON。
-- session 不存在和冲突。
-
-保存对接契约样例,作为其他团队联调依据。
-
-### 14.5 PostgreSQL 集成测试
-
-使用真实临时 PostgreSQL:
-
-- Alembic 从空库升级。
-- LangGraph checkpoint 写入、读取、恢复和清理。
-- 业务状态与事件原子更新。
-- 幂等重试。
-- 乐观版本冲突。
-- 同 session 锁。
-- 数据库连接中断后的恢复。
-- 连接池耗尽和超时。
-
-不能用 SQLite 替代 PostgreSQL 集成测试。
-
-### 14.6 安全测试
-
-- 无凭证。
-- 错误 JWT 签名。
-- 错误 issuer/audience。
-- token 过期或尚未生效。
-- scope 不足。
-- A tenant 访问 B tenant session。
-- `/set_info` mass assignment。
-- API Key 禁用、过期和轮换。
-- 请求重放。
-- 超长输入和资源限制。
-- 日志、trace 中不存在 Token、手机号、身份证。
-
-### 14.7 并发与故障恢复测试
-
-- 同一 session 两个 `/chat` 并发。
-- `/chat` 与 `/set_info` 并发。
-- 相同 `Idempotency-Key` 重复提交。
-- 相同 key 携带不同请求体。
-- 业务状态写入成功但响应超时。
-- 图节点执行成功但 checkpoint 写入失败。
-- 进程在节点之间退出。
-- 服务重启后恢复并继续。
-
-### 14.8 LLM 评测
-
-使用脱敏黄金数据集评测:
-
-- 人伤识别召回率。
-- 主动转人工意图识别。
-- 字段提取准确率。
-- 无效/无关回答分类。
-- 时间逻辑。
-- 车辆数量识别。
-- State prefix 格式有效率和合法迁移率。
-- 字段提取节点的结构化输出有效率。
-- 不允许的状态跳转次数。
-
-安全类指标优先控制漏判,不只看总体准确率。
-
-### 14.9 性能测试
-
-- `/get_info` P95/P99。
-- `/set_info` P95/P99。
-- `/chat` 非流式总耗时。
-- `/chat` 流式 TTFB 和总耗时。
-- 并发 session 数量。
-- 单 session 高频请求。
-- 数据库连接池和 LLM 限流。
-
-### 14.10 CI 门禁
-
-建议流水线:
-
-```text
-ruff check
-ruff format --check
-pyright
-pytest tests/unit tests/graph tests/contract
-pytest tests/integration
-alembic check
-依赖漏洞扫描
-secret 扫描
-```
-
-## 15. 分阶段实施计划
-
-### Phase 0:基线与契约冻结
-
-#### 工作项
-
-- 梳理现有三个接口的真实请求/响应行为。
-- 补齐 API 契约测试。
-- 整理全部状态码、字段、分组和转换规则。
-- 从现有 FastGPT 工作流和 Prompt 提取业务规则。
-- 建立脱敏黄金对话数据集。
-- 记录当前延迟、错误率和转人工比例基线。
-
-#### 交付物
-
-- API 契约测试。
-- 状态码表和字段注册表。
-- 状态转换矩阵。
-- 黄金场景数据集。
-- 当前系统基线报告。
-
-#### 验收
-
-- 在未改业务代码前,契约测试能够通过。
-- 三个接口的兼容行为有明确文档。
-- 不再依赖口头说明解释状态码和字段。
-
-### Phase 1:工程骨架与配置
-
-#### 工作项
-
-- 建立 `pyproject.toml` 和 lockfile。
-- 引入 Pydantic Settings。
-- 拆分数据库、鉴权、LLM、LangGraph、可观测性配置。
-- 使用 FastAPI lifespan 初始化和关闭资源。
-- 建立结构化日志和 request ID。
-- 增加 `/health/live`、`/health/ready`。
-
-#### 验收
-
-- 配置缺失时服务启动失败。
-- 生产配置不存在不安全默认值。
-- 启动/关闭不泄漏连接。
-- 日志中没有 secret。
-
-### Phase 2:鉴权与 session 授权
-
-#### 工作项
-
-- 接入 JWT/JWKS 或 API Key。
-- 建立 `CurrentClient` 依赖。
-- 实现 endpoint scope 检查。
-- 实现 tenant/session 对象级授权。
-- 增加调用方限流。
-- 补齐安全测试。
-
-#### 验收
-
-- 未授权调用无法访问三个接口。
-- A tenant 无法访问 B tenant session。
-- `/set_info` 不能写内部字段。
-- 凭证不出现在日志和 trace。
-
-### Phase 3:PostgreSQL 与持久化
-
-#### 工作项
-
-- 引入 SQLAlchemy async 和连接池。
-- 引入 Alembic。
-- 创建业务表和索引。
-- 引入 `AsyncPostgresSaver`。
-- 配置 LangGraph 表与业务表隔离。
-- 实现 session 锁、版本控制和幂等记录。
-- 建立数据清理策略。
-
-#### 验收
-
-- 服务重启后可恢复 thread。
-- `/set_info` 重试不重复更新。
-- 并发更新产生显式冲突或按序执行。
-- 数据库不可用时 readiness 失败。
-
-### Phase 4:类型化状态、`/set_info`、`/get_info`
-
-#### 工作项
-
-- 定义 `AccidentState`、领域模型和枚举。
-- 建立字段注册表和分组映射。
-- 实现 `SetInfoEvent` 和 `apply_external_update`。
-- 实现业务状态 repository。
-- `/get_info` 改为读取业务状态。
-- 移除这两个接口对 FastGPT 的辅助调用。
-
-#### 验收
-
-- `/set_info` 后 `/get_info` 立即读到一致值。
-- 服务重启后值仍存在。
-- `all/acdinfo/acdhuman1/acdhuman2` 与旧接口兼容。
-- 非法 key、非法类型不会污染状态。
-- `/chat` 后续能够读取外部写入的数据。
-
-### Phase 5:LangGraph 流程骨架与状态校验
-
-#### 工作项
-
-- 实现输入事件归一化。
-- 实现状态转换表。
-- 实现非流式和增量流式 state prefix parser。
-- 实现状态码枚举和迁移 validator。
-- 实现全局转人工、安全路由和阶段路由。
-- 实现单车/双车拍照流程。
-- 实现澄清和无回复计数。
-- 实现固定话术。
-- 建立节点和图场景测试。
-
-#### 验收
-
-- 不调用 LLM 即可完整执行拍照完成、拍照失败、无回复和外部字段更新等确定性流程。
-- Prefix parser 能处理标签跨 chunk、缺失、重复、未知状态码和非法迁移。
-- 拍照状态不能跳步。
-- 所有转人工状态符合优先级。
-- 非法转换被阻止并可观测。
-
-### Phase 6:State prefix LLM 与非流式 `/chat`
-
-#### 工作项
-
-- 建立 LLM provider/service 抽象。
-- 实现超时、有限重试和熔断策略。
-- 定义 `XXXX正文` 的 Prompt 输出契约。
-- 对需要字段提取的节点单独定义结构化 schema,避免把状态码复判拆成第二次调用。
-- 在阶段 LLM 节点中合并必要的事故字段理解、安全语义判断、回答有效性分类、候选状态前缀和正文生成。
-- 实现字段验证和 `formUpdate`。
-- 接入非流式 `/chat`。
-- 执行黄金场景评测。
-
-#### 验收
-
-- 非流式 `/chat` 能从回复开头解析 ``,并且不把标签暴露到 `outputText`。
-- 模型给出的候选状态码只有通过枚举和迁移校验后才进入图状态。
-- Prefix 缺失、格式错误、未知状态码或非法迁移不会污染状态。
-- 黄金场景状态序列和字段达到验收阈值。
-
-### Phase 7:流式 `/chat`
-
-#### 工作项
-
-- 使用同一图实现 streaming。
-- 将图事件适配为现有 SSE。
-- 在 prefix 完整解析前缓存开头 token;解析成功后先发送 `stage_code`,正文才进入 `text_delta`。
-- 固定事件顺序。
-- 保留 `useTextChunk` 兼容行为。
-- 处理客户端断开和生成取消。
-- 增加流式契约与性能测试。
-
-#### 验收
-
-- 流式文本拼接等于非流式文本。
-- `stage_code`、`formUpdate` 与非流式一致。
-- Prefix 被拆分到任意多个 token 时仍能正确解析,标签内容不会进入 `text_delta`。
-- 成功只产生一个 `done`。
-- 失败只产生一个终止 `error`。
-- 客户端断开不会遗留运行任务或连接。
-
-### Phase 8:可观测性与安全加固
-
-#### 工作项
-
-- 接入 Prometheus metrics。
-- 接入 OpenTelemetry traces。
-- 可选接入脱敏 Langfuse。
-- 实现统一脱敏处理器。
-- 建立仪表盘和告警。
-- 执行依赖、secret 和日志泄漏检查。
-- 验证数据保留与清理。
-
-#### 验收
-
-- 能按 request ID/trace ID 定位一次调用。
-- 能观察节点、LLM、数据库和 checkpoint 延迟。
-- 指标 label 无高基数 session 数据。
-- 日志和 trace 无敏感明文。
-
-### Phase 9:Shadow、灰度和下线 FastGPT
-
-#### 工作项
-
-- 引入 `AGENT_BACKEND=fastgpt|langgraph`。
-- 实现无副作用 shadow 对比。
-- Shadow 使用隔离 thread namespace,不写真实业务状态。
-- 对比状态码、字段 patch、转人工原因和文本。
-- 按调用方或流量比例灰度。
-- 监控错误率、延迟、转人工率和字段差异。
-- 稳定后切换默认后端。
-- 最后移除 FastGPT 依赖、标签解析和辅助聊天记录逻辑。
-
-#### 验收
-
-- 可以快速切回 FastGPT。
-- 灰度指标满足发布阈值。
-- LangGraph 全量稳定运行一个约定观察周期。
-- 删除 FastGPT 后契约和回归测试全部通过。
-
-## 16. 发布与回滚
-
-### 16.1 发布
-
-```text
-开发环境全量
- -> 测试环境全量
- -> 生产 shadow
- -> 指定调用方灰度
- -> 小比例流量
- -> 扩大比例
- -> 全量 LangGraph
- -> 观察期
- -> 删除 FastGPT
-```
-
-### 16.2 回滚
-
-兼容期:
-
-```text
-AGENT_BACKEND=fastgpt
-```
-
-回滚约束:
-
-- LangGraph 和 FastGPT 的业务状态格式必须有明确转换或隔离策略。
-- Shadow 不得写真实状态。
-- 数据库 migration 优先向前兼容。
-- 灰度期间不删除 FastGPT 所需配置。
-- 一旦 LangGraph 已写入新业务字段,回滚时 `/get_info` 仍从统一业务表读取,避免数据丢失。
-
-## 17. 风险与缓解
-
-| 风险 | 缓解措施 |
-|---|---|
-| Prompt 行为与 FastGPT 不一致 | 黄金数据集、shadow 差异分析、逐阶段迁移 |
-| 状态机规则遗漏 | 从工作流 JSON、Prompt、接口代码三方交叉整理 |
-| Checkpoint 与业务表不一致 | 幂等事件、版本号、每轮 hydrate、对账任务 |
-| 同 session 并发覆盖 | 数据库锁、乐观版本、冲突指标 |
-| State prefix 缺失、格式错误或非法迁移 | 增量 parser、枚举/迁移校验、一次受控重试、保守转人工 |
-| 敏感信息进入日志/trace | 统一脱敏、默认不记录正文、安全自动化测试 |
-| 依赖升级破坏接口 | lockfile、固定版本、升级专项测试 |
-| 生产数据库不可用 | readiness、连接池指标、明确失败,不无状态降级 |
-| Shadow 产生重复副作用 | Shadow 使用隔离状态且禁用副作用节点 |
-| 调用方重试造成重复推进 | Idempotency-Key、event_id 去重 |
-
-## 18. 完成标准
-
-改造完成需要同时满足:
-
-- `/chat`、`/set_info`、`/get_info` 对外契约兼容。
-- 正常流程不再调用 FastGPT。
-- 自然语言轮次统一解析模型回复开头的 `XXXX`,并在对外正文中移除标签。
-- `/set_info`、`/get_info` 不调用 LLM。
-- 模型输出的候选状态码必须通过代码枚举和迁移校验;阶段和计数器只由代码修改。
-- 生产使用 PostgreSQL checkpointer。
-- 业务状态可以独立于 checkpoint 查询和审计。
-- 鉴权、scope 和 tenant/session 对象级授权已启用。
-- 同 session 并发、重试和进程恢复测试通过。
-- 日志、metrics、trace 和告警可用。
-- 敏感信息脱敏与保留策略已验证。
-- 领域、Graph、契约、集成、安全和 LLM 评测达到约定阈值。
-- FastGPT 回滚开关经过演练。
-- FastGPT 下线后完整测试仍通过。
-
-## 19. 待确认事项
-
-实施前需要由产品、对接团队或基础设施负责人确认:
-
-1. 是否已有统一 API Gateway/OIDC/JWKS。
-2. 调用团队与 `tenant_id` 的映射方式。
-3. 是否允许为请求增加可选 `Idempotency-Key` Header。
-4. 历史接口 HTTP 状态码和响应体业务码的精确兼容要求。
-5. `/chat` SSE 中 `stage_code`、`formUpdate` 的最终顺序约定。
-6. `sessionId` 的创建方、唯一性和生命周期。
-7. 事故信息、checkpoint、事件、日志和 trace 的保留期限。
-8. 身份证、手机号等字段的加密和密钥托管方案。
-9. 生产 PostgreSQL 版本、schema 权限和连接池限制。
-10. Langfuse/OpenTelemetry 数据是否允许发送到外部或自建平台。
-11. LLM provider、模型白名单、超时、配额和容灾策略。
-12. Shadow 和灰度的业务验收指标。
-13. FastGPT 下线前需要保留的历史会话和迁移方式。
+作业 1—3 可直接离线推进。到相应阶段前再落实下列信息,并写入 runbook;不要把所有环境问题都变成搭脚手架的前置阻塞。
+| 决策 | 最晚落实时间 | 未落实时可以继续的工作 |
+|---|---|---|
+| 真实 LLM provider、模型、超时与预算 | 作业 4 真实模型验收前 | fake LLM、领域与接口测试 |
+| 工作流冲突规则和两种模式的最终路由 | 作业 4—5 验收前 | 来源提取、离线夹具、差异记录 |
+| PostgreSQL 版本、连接与部署权限 | 作业 7 前 | 内存完整功能与本地数据库开发 |
+| 调用方对任意字段写入、错误、SSE 顺序的依赖 | 作业 6/8 验收前 | 当前行为取样、兼容层实现 |
+| 服务身份、租户归属与数据保护方案 | 作业 9/生产灰度前 | 本地服务与合成数据验证 |
+| 延迟/成本预算、灰度观察周期与负责人 | 作业 10 灰度前 | 基线测量、压测与回退演练 |