Start LangGraph backend migration foundation

This commit is contained in:
Xin Wang
2026-07-27 17:21:29 +08:00
parent 5c719ed2ea
commit 1c8e9da486
33 changed files with 1859 additions and 86 deletions

View File

@@ -0,0 +1,101 @@
# FastGPT 迁移前基线
> 采集日期2026-07-26
> Git 基线:`5c719ed`
> 环境:本地 macOS项目 `.venv`FakeBackend/FakeClient未调用真实 FastGPT
> 用途:冻结可复现工程基线,不冒充生产业务指标
## 自动化基线
执行:
```bash
.venv/bin/python -m pytest -q
```
Phase 0 开始前结果:
```text
32 passed in 0.32s
```
覆盖范围:
- Pydantic 公共请求/响应 schema
- `/chat` 对 backend-neutral contract 的适配;
- FastGPT backend 的 SDK/Event 转换;
- state prefix 跨 chunk 的基础兼容;
- 文本分句。
未覆盖范围:
- 真实 FastGPT 网络延迟和错误;
- 真实 workflow 状态序列稳定性;
- `/set_info``/get_info` 完整兼容行为;
- 生产转人工比例和 prefix 失败率;
- 数据库、并发恢复和 LangGraph。
Phase 0 完成后的测试数量和耗时见本报告底部。
## 本地 API 适配层耗时
使用内存 FakeBackend关闭日志后直接调用 endpoint 并完整消费流式响应。该数据只衡量 Python 适配、prefix/SSE 处理开销,不包含 HTTP、网络、FastGPT 或模型延迟。
```text
nonstream n=2000 p50=0.004ms p95=0.014ms p99=0.030ms
stream-consume n=1000 p50=0.011ms p95=0.034ms p99=0.051ms
```
这组数据用于后续发现 API 适配层的明显性能回退,不能与生产端到端延迟混用。
## 生产指标采集口径
以下数据无法从仓库推导,必须由部署环境日志或监控采集。负责人应使用同一时间窗口、同一调用方集合,并排除压测流量。
| 指标 | 计算方式 | 当前值 |
|---|---|---|
| `/chat` 请求数 | 成功与失败总请求 | 待生产采集 |
| 非流式 P50/P95/P99 | endpoint 总耗时 | 待生产采集 |
| 流式 TTFB P50/P95/P99 | 收到请求至首个 `text_delta` | 待生产采集 |
| 响应体错误率 | `code != "200"` / 请求数 | 待生产采集 |
| FastGPT 超时率 | timeout / FastGPT 调用数 | 待生产采集 |
| Prefix 失败率 | 缺失、格式错误、未知码 / 模型回复数 | 当前未结构化记录 |
| 转人工率 | `0001/0002/0003/0004/0005` / session 数 | 待生产采集 |
| `formUpdate` 产生率 | 非空 patch / `needFormUpdate=true` 轮次 | 待生产采集 |
| 平均轮次 | chat 轮次 / 完结 session 数 | 待生产采集 |
| `/set_info``/get_info` 错误率 | `code != "200"` / 请求数 | 待生产采集 |
## 当前可观测性限制
当前日志虽然记录延迟,但也记录完整 `sessionId`、输入、输出和 `formUpdate`不能直接作为长期生产基线方案。Phase 1/8 应先加入 request ID、session hash、结构化事件和脱敏然后再持续采集。
建议临时聚合时只输出:
- 时间桶;
- endpoint
- 成功/稳定错误类别;
- 耗时;
- stage code
- 是否产生 form patch
- 不可逆 session hash。
不得导出原始对话、身份证、手机号、车牌、Token 或完整表单。
## Phase 0 最终验证
完成日期2026-07-26
```text
48 passed in 0.49s
```
相对 Phase 0 开始前新增 16 个测试,覆盖:
- `/set_info``/get_info` 的 FastGPT 辅助调用和兼容序列化;
- SSE 成功事件顺序、唯一终止事件和文本拼接;
- 当前缺失流式 prefix 的历史行为;
- 状态/迁移/字段注册表闭合性;
- 黄金场景唯一性和敏感号码扫描;
- 源码、文档、测试和配置样例中的 FastGPT Token 扫描。
生产业务指标仍标记为“待生产采集”。这是外部可观测数据依赖,不用估算值替代;最迟必须在 Phase 9 shadow 前完成采集。

40
docs/domain/README.md Normal file
View File

@@ -0,0 +1,40 @@
# Phase 0 领域契约索引
> 基线版本2026-07-26
> 适用范围:`/chat`、`/set_info`、`/get_info`
> 规则来源优先级V1.0.9 接口文档 > 2026-07-26 workflow/Prompt > 当前服务代码 > 历史 workflow/Prompt
本目录冻结 LangGraph 迁移前的外部契约和业务规则。JSON 文件是后续代码生成、参数化测试和 Graph validator 的机器可读输入Markdown 文件解释兼容行为及来源。
## 交付物
| 文件 | 用途 |
|---|---|
| `api-contract.md` | 三个接口的当前兼容行为和已知偏差 |
| `stage-codes.json` | 状态码、内部/外部映射和产生方式 |
| `stage-transitions.json` | 权威迁移矩阵和拍照顺序 |
| `field-registry.json` | 字段、分组、类型、敏感性和写权限 |
| `business-rules.md` | 从 workflow、Prompt 和 endpoint 提取的规则 |
| `event-mapping.md` | 历史魔法字符串到结构化事件的映射 |
| `../baselines/fastgpt-baseline-20260726.md` | 迁移前可复现测试/性能基线 |
| `../../test/fixtures/golden/accident-scenarios.json` | 脱敏黄金场景 |
## 已冻结的关键决定
1. 对外继续使用 camelCase、字符串业务码和 JSON 编码的 `/get_info.value`
2. `3001``3002` 为内部信息确认状态,对外仍返回 `1002`
3. `2006``2017``2020` 仅作为历史别名接收,不作为新图的合法目标状态。
4. `0004` 纳入正式状态表;当前 endpoint 状态名称映射缺失是待修复偏差,不代表删除该状态。
5. 拍照完成、连续拍照失败、无回复和外部字段更新在新图中必须是结构化确定性事件。
6. `sfzmwh1/2``sjwh1/2` 是只读兼容字段,不允许 `/set_info` 写入。
7. `phase``stage_code`、计数器、版本和图路由字段禁止外部修改。
## 变更规则
Phase 0 冻结后,修改这里的状态、字段或外部契约必须同时:
1. 说明业务原因和兼容影响;
2. 更新机器可读 JSON
3. 更新黄金场景;
4. 更新对应契约/领域测试;
5. 获得接口调用方或产品确认。

115
docs/domain/api-contract.md Normal file
View File

@@ -0,0 +1,115 @@
# 兼容 API 契约基线
## 通用约定
- 路径保持 `/chat``/set_info``/get_info`
- 请求和响应字段保持 camelCase。
- 业务成功/失败主要通过响应体字符串 `code` 表达;现有 endpoint 通常仍返回 HTTP 200。
- `sessionId` 最大 64 字符,`timeStamp` 最大 32 字符。
- Pydantic 校验失败由 FastAPI 返回 HTTP 422。
- 本文冻结的是当前可观察行为;“目标行为”标记为后续迁移必须修复的已批准偏差。
## `POST /chat`
### 请求
| 字段 | 类型 | 必填 | 默认值 |
|---|---|---:|---|
| `sessionId` | string | 是 | - |
| `timeStamp` | string | 是 | - |
| `text` | string | 是 | - |
| `needFormUpdate` | boolean | 否 | `false` |
| `useTextChunk` | boolean | 否 | `false` |
### 非流式响应
字段为 `sessionId``timeStamp``outputText``formUpdate``nextStage``nextStageCode``code``msg`
- 成功时 `code="200"`
- `<state>XXXX</state>` 从正文中移除。
- `3001/3002/1002 → 1002``2006 → 2004``2017 → 2016``2020 → 0002`
- Prefix 缺失或正文不可解析时当前返回 `code="500"` 和“消息不完整”。
- FastGPT 认证、限流和 API 异常分别映射为响应体 `401``429``500`
- `formUpdate` 保持无固定 schema 的 JSON 值,以兼容现有调用方。
### SSE 响应
事件名和数据:
| 事件 | 数据 | 基数 |
|---|---|---|
| `stage_code` | `{"nextStageCode":"1002","nextStage":"通话中"}` | 成功轮最多一次 |
| `formUpdate` | 表单 patch 对象 | 有更新时最多一次 |
| `text_delta` | `{"text":"..."}` | 零到多次 |
| `done` | `{"status":"completed"}` | 成功恰好一次 |
| `error` | `{"msg":"...","code":"500"}` | 失败恰好一次且终止 |
V1.0.9 文档要求 `stage_code` 先于 `text_delta``formUpdate` 的位置由 FastGPT `flowResponses` 到达时间决定,文档示例允许它出现在两个 `text_delta` 之间。LangGraph 迁移目标固定为:
```text
stage_code -> formUpdate(可选) -> text_delta* -> done
```
迁移后的错误路径不得同时产生 `done``error``useTextChunk=true` 只改变 `text_delta` 切分,不改变拼接后的文本。
### 已知偏差
- 当前流式 parser 会在整段文本中搜索标签,而不是强制标签位于开头。
- 当前流式未知状态码仍可能发送空 `nextStage`
- 当前流式缺少 prefix 时仍可能输出正文和 `done`
- 当前流式内部事件处理异常会记录后继续,可能掩盖部分失败。
- 当前实现记录完整输入、输出和 `formUpdate`,不符合数据保护目标。
以上偏差被字符化测试记录,但不作为 LangGraph 新实现的目标行为Phase 58 必须按迁移计划修正。
## `POST /set_info`
### 请求
字段为 `sessionId``timeStamp``key``value``includeInputInfo`;其中 `includeInputInfo` 默认 `false`
当前实现:
1. 通过一次 FastGPT 对话读取 `newVariables.state`
2. 删除辅助对话记录;
3. 直接执行 `state[key] = value`
4. 再通过 FastGPT 对话写回并删除辅助记录。
成功返回 `code="200"`;任一步失败返回响应体 `code="500"`
目标行为:
- 仅接受 `field-registry.json``external_write=true` 的 key。
- 进行类型转换和领域校验。
- 禁止写内部状态字段。
- 直接事务化写业务状态,不调用 LLM不创建/删除辅助聊天记录。
## `POST /get_info`
请求字段为 `sessionId``timeStamp``key``includeInputInfo``includeInputInfo` 默认 `false`
支持:
- `all`
- `acdinfo`
- `acdhuman1`
- `acdhuman2`
- 单个字段 key
兼容序列化:
- `value` 始终是 JSON 编码后的字符串。
- boolean 转为字符串 `"1"``"0"`
- 缺失字段转为空字符串。
- 未知单字段 key 当前返回 JSON 字符串 `""`,而不是报错。
目标实现仍保留上述响应编码,但直接读取业务投影,不调用 FastGPT。
## 契约来源
- `src/schemas/models.py`
- `src/api/endpoints.py`
- `docs/视频快处智能信息采集机器人交互接口文档V1.0.9.docx`
- `docs/chat-stream-mode.md`
- `test/api/test_public_schema_contract.py`
- `test/api/test_chat_backend_boundary.py`

View File

@@ -0,0 +1,116 @@
# 事故采集业务规则基线
## 规则来源
本基线交叉比对以下来源:
1. V1.0.9 交互接口文档;
2. `workflow/20260726/事故信息采集20260726.json`
3. `prompts/20260723/单车拍照.txt`
4. `prompts/20260723/双车拍照.txt`
5. 当前 `src/api/endpoints.py`
6. 2025 版本 workflow/Prompt仅用于识别历史兼容行为。
发生冲突时采用接口文档和 2026-07-26 规则;历史状态别名只在 API 边界兼容。
## 全局规则
1. 每个自然语言回复必须以且仅以一个 `<state>四位数字</state>` 开头。
2. 用户明确要求“转人工”“找人工”“人工客服”等时,立即进入 `0001`
3. 明确或高度可信的人伤、三辆及以上机动车、涉及行人/非机动车等复杂情况进入 `0003`
4. 明确否定人伤时不得因句中出现“受伤”“流血”等词误触发 `0003`
5. ASR 内容破碎或人伤语义矛盾时,用当前采集状态封闭确认,不能直接冒险放行。
6. 当前问题没有有效答案时不得跳题。第一次澄清,第二次强制选择;仍失败进入 `0002`
7. 连续第一次无回复使用固定唤醒话术;连续第二次进入 `0004`
8. 用户有效回复后无回复计数清零。
9. 状态候选必须经过枚举和迁移矩阵校验才能持久化。
## 准备与事故信息采集
1. 新 session 初始为 `1001`,提示撤离到安全区域、开启双闪、放置警告牌。
2. `【开始】``【继续办理】` 后进入 `1002`
3. 采集顺序:
- 事故经过;
- 是否有人伤;
- 是否涉及非机动车/摩托车/自行车;
- 事故时间并校验不能晚于当前时间;
- 是否仍在现场;
- 机动车数量。
4. 用户提前提供的字段用于填槽,但进入下一项前应做封闭式确认。
5. 一辆机动车、无人伤且不涉及非机动车/行人:进入 `2000`
6. 两辆机动车、无人伤且不涉及非机动车/行人:进入 `2010`
7. 三辆及以上,或涉及非机动车/行人,或有人伤:进入 `0003`
## 单车拍照
严格顺序:
```text
2000 车前/车牌
-> 2001 车辆碰撞部位
-> 2002 被撞物品
-> 2003 本人正面
-> 2004 确认或纠正车牌
-> 2005 确认车损位置
-> 3001 单车信息确认
```
- `2000``2003` 只有 `PhotoCompletedEvent` 可以正常推进;其他普通输入重复当前固定指令。
- `2004` 肯定车牌或提供完整新车牌后进入 `2005`;仅否定但不提供号码时停留并追问。
- `2005` 获得有效车损位置后进入 `3001`;连续两次无效回答进入 `0002`
- 任意单车照片状态收到拍照失败事件立即进入 `0005`
## 双车拍照
严格顺序:
```text
2010 第一辆车侧前方/车牌
-> 2011 第一辆车碰撞部位
-> 2012 第二辆车碰撞部位
-> 2013 第二辆车侧后方/车牌
-> 2014 另一方驾驶人正面
-> 2015 本人正面
-> 2016 确认或纠正车牌
-> 3002 双车信息确认
```
- `2010``2015` 只有 `PhotoCompletedEvent` 可以正常推进。
- `2016` 肯定或提供完整新车牌后进入 `3002`;无关或不完整回答停留,连续两次失败进入 `0002`
- 任意双车照片状态收到拍照失败事件立即进入 `0005`
## 当事人信息确认
### 单车 `3001`
依次确认:
1. 是否为对应车辆车主/驾驶人;
2. 姓名;
3. 身份证后四位;不一致时采集完整号码并二次确认;
4. 手机号后四位;不一致时采集完整号码并二次确认;
5. 完成后进入 `0000`
### 双车 `3002`
先完成第一位驾驶人上述信息,再要求将电话交给第二位驾驶人,重复相同步骤。第二位完成后进入 `0000`
身份证和手机号允许分段输入中间态只保存已接收片段不应把未完成号码写入已确认业务字段。日志、trace 和黄金数据不得包含真实号码。
## 表单更新
- `needFormUpdate=false` 时无需返回 `formUpdate`
- `needFormUpdate=true` 时只返回本轮相对当前表单发生变化的字段。
- LLM 提取结果必须经过 `field-registry.json` 白名单和类型校验。
- patch 之外的原字段保持不变。
- 不允许 LLM 更新 phase、状态码、计数器或版本号。
## 当前实现与目标规则的差异
- FastGPT Prompt 承担了多数计数和迁移逻辑,服务端未校验迁移合法性。
- 当前 prefix 正则不是开头锚定且接受任意位数字。
- 当前 `/set_info` 可写任意 key。
- 当前 `/get_info``/set_info` 通过辅助 LLM 对话访问状态。
- 当前日志会记录完整用户输入、回复和表单。
这些差异是 Phase 18 的明确改造项,不能被解释为本基线认可的目标行为。

View File

@@ -0,0 +1,37 @@
# 历史输入到结构化事件的映射
API 兼容层可以继续接收历史字符串,但进入 LangGraph 前必须转换为以下事件。图节点不得再通过自然语言字符串判断系统事件。
| 历史输入 | 结构化事件 | 必要字段 | 确定性效果 |
|---|---|---|---|
| 普通用户文本 | `UserMessageEvent` | `event_id`, `text`, `need_form_update`, `use_text_chunk` | 进入当前阶段处理;自然语言轮次通常调用一次 LLM |
| `【开始】` | `SessionStartedEvent` | `event_id` | `1001 → 1002`,开始事故描述采集 |
| `【继续办理】` | `SessionStartedEvent` | `event_id` | 与 `【开始】` 相同 |
| `【拍摄完成】` | `PhotoCompletedEvent` | `event_id`, `photo_step` | 仅按单车/双车严格顺序推进一步0 次 LLM |
| `【客户端连续3次拍摄识别失败原因】` | `PhotoRecognitionFailedEvent` | `event_id`, `reason` | 任意照片阶段立即进入 `0005`0 次 LLM |
| `【用户无回复】` | `NoResponseEvent` | `event_id` | 第一次重复唤醒;连续第二次进入 `0004` |
| `【用户未回复】` | `NoResponseEvent` | `event_id` | 历史别名,效果同上 |
| `/set_info` 请求 | `SetInfoEvent` | `event_id`, `key`, `value` | 校验白名单/类型后幂等更新,不调用 LLM |
## 优先级
同一轮只允许一个输入事件。事件处理优先级为:
```text
显式人工请求
> 连续拍照失败
> 明确人伤/复杂情况
> 无回复
> 拍照完成
> 外部字段更新
> 普通用户文本
```
显式人工请求来自普通文本时允许确定性关键词 gate 先处理;语义模糊的人伤内容交由同一轮阶段 LLM 判断,但候选状态仍需迁移校验。
## 计数重置
- 收到有效用户回答后,`no_response_count` 清零。
- 当前问题得到有效答案后,对应 `clarification_counts[question_id]` 清零。
- 非连续的无回复不能累计到 `0004`
- 客户端已经负责累计三次拍照识别失败;服务端收到一次结构化失败事件就进入 `0005`,不得再次累计三次。

View File

@@ -0,0 +1,51 @@
{
"version": "2026-07-26",
"groups": {
"acdinfo": ["ywrysw", "ywfjdc", "ywmtc", "bjrjs", "sgfssj", "sfsgxc", "jdcsl", "sgyy"],
"acdhuman1": ["xm1", "hpzl1", "hphm1", "sfzmhm1", "sfzmwh1", "sjhm1", "sjwh1", "csbw1"],
"acdhuman2": ["xm2", "hpzl2", "hphm2", "sfzmhm2", "sfzmwh2", "sjhm2", "sjwh2", "csbw2"]
},
"fields": [
{"key": "ywrysw", "group": "acdinfo", "type": "boolean", "description": "是否有人员伤亡", "sensitive": false, "external_write": true},
{"key": "ywfjdc", "group": "acdinfo", "type": "boolean", "description": "是否涉及非机动车", "sensitive": false, "external_write": true},
{"key": "ywmtc", "group": "acdinfo", "type": "boolean", "description": "是否涉及摩托车", "sensitive": false, "external_write": true},
{"key": "bjrjs", "group": "acdinfo", "type": "string", "description": "报警人角色/描述", "sensitive": false, "external_write": true},
{"key": "sgfssj", "group": "acdinfo", "type": "datetime", "description": "事故发生时间", "sensitive": false, "external_write": true},
{"key": "sfsgxc", "group": "acdinfo", "type": "boolean", "description": "是否在事故现场", "sensitive": false, "external_write": true},
{"key": "jdcsl", "group": "acdinfo", "type": "integer", "description": "事故机动车数量", "sensitive": false, "external_write": true},
{"key": "sgyy", "group": "acdinfo", "type": "string", "description": "事故原因/经过", "sensitive": false, "external_write": true},
{"key": "xm1", "group": "acdhuman1", "type": "string", "description": "驾驶员1姓名", "sensitive": true, "external_write": true},
{"key": "hpzl1", "group": "acdhuman1", "type": "string", "description": "驾驶员1号牌种类", "sensitive": false, "external_write": true},
{"key": "hphm1", "group": "acdhuman1", "type": "license_plate", "description": "驾驶员1车牌号", "sensitive": true, "external_write": true},
{"key": "sfzmhm1", "group": "acdhuman1", "type": "national_id", "description": "驾驶员1身份证号码", "sensitive": true, "external_write": true},
{"key": "sfzmwh1", "group": "acdhuman1", "type": "string", "description": "驾驶员1身份证尾号兼容字段", "sensitive": true, "external_write": false},
{"key": "sjhm1", "group": "acdhuman1", "type": "phone", "description": "驾驶员1手机号码", "sensitive": true, "external_write": true},
{"key": "sjwh1", "group": "acdhuman1", "type": "string", "description": "驾驶员1手机号尾号兼容字段", "sensitive": true, "external_write": false},
{"key": "csbw1", "group": "acdhuman1", "type": "string", "description": "驾驶员1车辆车损部位", "sensitive": false, "external_write": true},
{"key": "xm2", "group": "acdhuman2", "type": "string", "description": "驾驶员2姓名", "sensitive": true, "external_write": true},
{"key": "hpzl2", "group": "acdhuman2", "type": "string", "description": "驾驶员2号牌种类", "sensitive": false, "external_write": true},
{"key": "hphm2", "group": "acdhuman2", "type": "license_plate", "description": "驾驶员2车牌号", "sensitive": true, "external_write": true},
{"key": "sfzmhm2", "group": "acdhuman2", "type": "national_id", "description": "驾驶员2身份证号码", "sensitive": true, "external_write": true},
{"key": "sfzmwh2", "group": "acdhuman2", "type": "string", "description": "驾驶员2身份证尾号兼容字段", "sensitive": true, "external_write": false},
{"key": "sjhm2", "group": "acdhuman2", "type": "phone", "description": "驾驶员2手机号码", "sensitive": true, "external_write": true},
{"key": "sjwh2", "group": "acdhuman2", "type": "string", "description": "驾驶员2手机号尾号兼容字段", "sensitive": true, "external_write": false},
{"key": "csbw2", "group": "acdhuman2", "type": "string", "description": "驾驶员2车辆车损部位", "sensitive": false, "external_write": true}
],
"read_keys": ["all", "acdinfo", "acdhuman1", "acdhuman2"],
"internal_write_denylist": [
"phase",
"stage_code",
"clarification_counts",
"no_response_count",
"handoff_reason",
"state_version",
"messages",
"input_event",
"photo_step"
],
"legacy_serialization": {
"get_info_value": "json_encoded_string",
"boolean": {"true": "1", "false": "0"},
"missing": ""
}
}

View File

@@ -0,0 +1,40 @@
{
"version": "2026-07-26",
"sources": [
"src/api/endpoints.py",
"docs/视频快处智能信息采集机器人交互接口文档V1.0.9.docx",
"workflow/20260726/事故信息采集20260726.json",
"prompts/20260723/单车拍照.txt",
"prompts/20260723/双车拍照.txt"
],
"codes": [
{"code": "0000", "name": "通话结束", "phase": "complete", "terminal": true, "external_code": "0000", "producer": "workflow"},
{"code": "0001", "name": "主动转人工", "phase": "handoff", "terminal": true, "external_code": "0001", "producer": "deterministic_or_llm"},
{"code": "0002", "name": "语义连续无法识别", "phase": "handoff", "terminal": true, "external_code": "0002", "producer": "deterministic"},
{"code": "0003", "name": "人伤或复杂情况转人工", "phase": "handoff", "terminal": true, "external_code": "0003", "producer": "deterministic_or_llm"},
{"code": "0004", "name": "长时间无回复", "phase": "handoff", "terminal": true, "external_code": "0004", "producer": "deterministic"},
{"code": "0005", "name": "连续拍照识别失败", "phase": "handoff", "terminal": true, "external_code": "0005", "producer": "deterministic"},
{"code": "1001", "name": "未准备好通话", "phase": "ready_gate", "terminal": false, "external_code": "1001", "producer": "deterministic"},
{"code": "1002", "name": "事故信息采集中", "phase": "collection", "terminal": false, "external_code": "1002", "producer": "llm"},
{"code": "2000", "name": "单车车前照片", "phase": "single_photo", "terminal": false, "external_code": "2000", "producer": "deterministic_or_llm"},
{"code": "2001", "name": "单车碰撞部位照片", "phase": "single_photo", "terminal": false, "external_code": "2001", "producer": "deterministic"},
{"code": "2002", "name": "被撞物品照片", "phase": "single_photo", "terminal": false, "external_code": "2002", "producer": "deterministic"},
{"code": "2003", "name": "本人正面照片", "phase": "single_photo", "terminal": false, "external_code": "2003", "producer": "deterministic"},
{"code": "2004", "name": "确认单车车牌", "phase": "single_photo", "terminal": false, "external_code": "2004", "producer": "llm"},
{"code": "2005", "name": "确认单车车损位置", "phase": "single_photo", "terminal": false, "external_code": "2005", "producer": "llm"},
{"code": "2010", "name": "第一辆车侧前方照片", "phase": "double_photo", "terminal": false, "external_code": "2010", "producer": "deterministic_or_llm"},
{"code": "2011", "name": "第一辆车碰撞部位照片", "phase": "double_photo", "terminal": false, "external_code": "2011", "producer": "deterministic"},
{"code": "2012", "name": "第二辆车碰撞部位照片", "phase": "double_photo", "terminal": false, "external_code": "2012", "producer": "deterministic"},
{"code": "2013", "name": "第二辆车侧后方车牌照片", "phase": "double_photo", "terminal": false, "external_code": "2013", "producer": "deterministic"},
{"code": "2014", "name": "另一方驾驶人正面照片", "phase": "double_photo", "terminal": false, "external_code": "2014", "producer": "deterministic"},
{"code": "2015", "name": "本人正面照片", "phase": "double_photo", "terminal": false, "external_code": "2015", "producer": "deterministic"},
{"code": "2016", "name": "确认双车车牌", "phase": "double_photo", "terminal": false, "external_code": "2016", "producer": "llm"},
{"code": "3001", "name": "单车当事人信息确认", "phase": "single_verification", "terminal": false, "external_code": "1002", "producer": "llm"},
{"code": "3002", "name": "双车当事人信息确认", "phase": "double_verification", "terminal": false, "external_code": "1002", "producer": "llm"}
],
"legacy_aliases": {
"2006": "2004",
"2017": "2016",
"2020": "0002"
}
}

View File

@@ -0,0 +1,46 @@
{
"version": "2026-07-26",
"global_transitions": {
"explicit_handoff": "0001",
"injury_or_complex": "0003",
"two_invalid_clarifications": "0002",
"two_no_response_events": "0004",
"photo_recognition_failed_event": "0005"
},
"allowed": {
"1001": ["1001", "1002", "0001", "0003", "0004"],
"1002": ["1002", "2000", "2010", "0000", "0001", "0002", "0003", "0004"],
"2000": ["2000", "2001", "0001", "0003", "0004", "0005"],
"2001": ["2001", "2002", "0001", "0003", "0004", "0005"],
"2002": ["2002", "2003", "0001", "0003", "0004", "0005"],
"2003": ["2003", "2004", "0001", "0003", "0004", "0005"],
"2004": ["2004", "2005", "0001", "0002", "0003", "0004", "0005"],
"2005": ["2005", "3001", "0001", "0002", "0003", "0004", "0005"],
"2010": ["2010", "2011", "0001", "0003", "0004", "0005"],
"2011": ["2011", "2012", "0001", "0003", "0004", "0005"],
"2012": ["2012", "2013", "0001", "0003", "0004", "0005"],
"2013": ["2013", "2014", "0001", "0003", "0004", "0005"],
"2014": ["2014", "2015", "0001", "0003", "0004", "0005"],
"2015": ["2015", "2016", "0001", "0003", "0004", "0005"],
"2016": ["2016", "3002", "0001", "0002", "0003", "0004", "0005"],
"3001": ["3001", "0000", "0001", "0002", "0003", "0004"],
"3002": ["3002", "0000", "0001", "0002", "0003", "0004"],
"0000": [],
"0001": [],
"0002": [],
"0003": [],
"0004": [],
"0005": []
},
"photo_sequences": {
"single": ["2000", "2001", "2002", "2003", "2004", "2005", "3001"],
"double": ["2010", "2011", "2012", "2013", "2014", "2015", "2016", "3002"]
},
"notes": [
"同状态迁移表示无效输入后重复当前问题或固定指令。",
"3001 和 3002 是内部信息确认状态,对外兼容码统一为 1002。",
"终止状态不允许继续迁移;新请求必须创建或显式重置 session。",
"0003 在照片和信息确认阶段仍保留全局安全优先级。",
"迁移矩阵是 LangGraph 实现的权威基线FastGPT 当前不会在服务端执行该校验。"
]
}

View File

@@ -1,7 +1,8 @@
# ZNJJ LangGraph 后端改造计划
> 状态:Draft
> 日期2026-07-25
> 状态:Phase 0 已冻结LangGraph 最小开发纵切已启动
> 日期2026-07-25
> Phase 0 收口日期2026-07-26
> 目标项目:`ZNJJ-api-server`
> 参考项目:`fastapi-langgraph-agent-production-ready-template`
@@ -1113,6 +1114,17 @@ secret 扫描
### Phase 0基线与契约冻结
#### 完成记录2026-07-26
- 已冻结三个接口的兼容行为:`docs/domain/api-contract.md`
- 已建立机器可读状态码表、字段注册表和状态迁移矩阵:`docs/domain/*.json`
- 已从 V1.0.9 接口文档、2026 workflow、Prompt 和 endpoint 提取业务规则及事件映射。
- 已建立 15 个脱敏黄金场景:`test/fixtures/golden/accident-scenarios.json`
- 全量测试由 32 个增加到 48 个,全部通过。
- 已记录可复现的本地 API 适配层延迟基线和生产指标采集口径。
- 已清理 HTTP/环境样例中的明文 FastGPT Token本地 secret 改由被忽略的 `.env.local` 保存。
- 生产延迟、错误率和转人工比例依赖部署环境数据,当前不得用估算值替代,最迟在 Phase 9 shadow 前补录。
#### 工作项
- 梳理现有三个接口的真实请求/响应行为。
@@ -1138,6 +1150,18 @@ secret 扫描
### Phase 1工程骨架与配置
#### 最小纵切记录2026-07-27
- 已引入 Pydantic Settings 的最小字段环境、backend、FastGPT、LLM、checkpointer。
- 已实现 `AGENT_BACKEND=fastgpt|langgraph` 后端工厂,默认保持 FastGPT。
- 已实现 `StateGraph + InMemorySaver + OpenAI-compatible LLM` 的可运行开发骨架。
- 已验证同一 `sessionId` 的 thread 状态延续和不同 session 隔离。
- 已固定 `langgraph==1.2.9``pydantic-settings==2.14.2`
- 全量测试 58 个通过。
- 完整配置拆分、健康检查、request ID 和结构化日志按用户决定后补。
开发说明见 `docs/langgraph-minimal-slice.md`
#### 工作项
- 建立 `pyproject.toml` 和 lockfile。
@@ -1395,4 +1419,3 @@ AGENT_BACKEND=fastgpt
11. LLM provider、模型白名单、超时、配额和容灾策略。
12. Shadow 和灰度的业务验收指标。
13. FastGPT 下线前需要保留的历史会话和迁移方式。

View File

@@ -0,0 +1,82 @@
# LangGraph 最小开发纵切
> 状态:仅用于本地开发和测试
> 日期2026-07-27
## 启用
在被 Git 忽略的 `.env.local` 中配置:
```text
ZNJJ_ENVIRONMENT=development
AGENT_BACKEND=langgraph
LANGGRAPH_CHECKPOINTER=memory
LLM_API_KEY=...
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=...
LLM_TIMEOUT_SECONDS=60
LLM_MAX_RETRIES=2
```
不设置 `AGENT_BACKEND` 时默认使用 FastGPT。
## 当前调用链
```text
/chat
-> ChatBackend
-> LangGraphBackend
-> StateGraph
-> generate_response
-> OpenAI-compatible LLM
-> ChatResult
-> 现有非流式/SSE API Adapter
```
`InMemorySaver` 使用 `sessionId` 作为 `thread_id`,同一进程内同一 session
可以恢复 `turn_count` 等图状态;不同 session 相互隔离。
## 当前已有能力
- FastGPT/LangGraph 后端配置切换。
- 最小 Pydantic Settings 和条件化启动校验。
- 实际使用 LangGraph `StateGraph`
- 开发/测试使用 `InMemorySaver`
- OpenAI-compatible LLM 节点。
- 非流式 `/chat` 适配。
- 通过完整结果桥接现有 SSE`formUpdate` 先于文本发送。
- Fake LLM 下的多轮 thread 隔离测试。
## 明确限制
当前图只有一个模型节点,目的是尽早建立可执行骨架。以下尚未实现:
- 领域状态、输入事件和确定性路由节点;
- 状态码枚举及迁移合法性校验;
- 单车/双车拍照状态机;
- 字段提取、验证和真实 `formUpdate`
- 模型 token 级流式输出;
- PostgreSQL checkpointer
- session 并发、版本和幂等;
- `/set_info``/get_info` 的业务状态迁移。
因此:
- `LANGGRAPH_CHECKPOINTER=postgres` 当前会启动失败;
- staging/production 禁止使用 memory checkpointer
- `/set_info``/get_info``/delete_session` 暂时仍需要 FastGPT 配置;
- 不得把当前 LangGraph backend 接入生产流量。
## 下一步
直接在现有图中加入 Phase 5 的纯确定性骨架:
1. `AccidentState`、输入事件和状态枚举;
2. state prefix parser 与迁移 validator
3. `normalize_input``route_phase`
4. 单车/双车拍照事件推进;
5. 无回复和澄清计数;
6. 对应参数化 Graph 测试。
这些节点完成后再接 PostgreSQL 和业务状态 repository。