Files
ZNJJ-api-server/docs/assignment6-compat.md
T

136 lines
10 KiB
Markdown
Raw Normal View History

# 作业 6 兼容差异清单:LangGraph 业务状态 vs FastGPT 现状
> 范围:`/set_info`、`/get_info`、`/delete_session`、`/chat` 四个接口在
> `AGENT_BACKEND=langgraph` 下接入应用服务 + repository 后的行为差异。
> FastGPT 模式(`AGENT_BACKEND=fastgpt`)保留旧实现,不受影响。
## 接口响应契约(保持不变)
| 项 | 现状(FastGPT) | LangGraph(作业 6) |
|---|---|---|
| 请求/响应模型 | `ProcessRequest/Response_*` | 相同(endpoint 未改模型) |
| `get_info` 分组名 | `all` / `acdinfo` / `acdhuman1` / `acdhuman2` | 相同 |
| `get_info` 布尔外部表示 | `True→"1"`,`False→"0"`,未知→`""` | 相同(`to_external_string`) |
| `get_info` 未知 key | 返回 JSON 空串 `'""'` | **保持** `'""'`(现状兼容) |
| `includeInputInfo` 字段 | 触发 FastGPT 辅助对话 | 继续接受但**忽略**——新后端零辅助对话 |
## 有意的行为差异(新后端收紧)
| 项 | FastGPT 现状 | LangGraph | 理由 |
|---|---|---|---|
| `set_info` 未知 key | 任意 key 写入 state 变量 | **拒绝**(500 + "入参不规范:未知或受保护的字段 key") | 白名单保护;任意写入会污染图输入。上线前需审计旧调用方是否依赖任意 key 写入 |
| `set_info` 图内部键(`stage_code` 等) | 可写(state 是裸 dict) | **拒绝**(`INTERNAL_FIELD_KEYS` 保护) | 内部状态只允许图自己推进 |
| `set_info` 字段值 | 任意字符串原样存 | 按注册表类型解析(bool/int 严格),无法解析拒绝 | 保留 空/False/0/未知 区分,杜绝 `bool("0")` 陷阱 |
| 图表单键与业务键 | FastGPT state 为裸 dict,无映射概念 | 建立双向映射(作业 6):表单六键(`has_injury` 等)<-> acd 业务键(`ywrysw` 等);`bjrjs`/`ywmtc` 无表单来源,仅 `set_info` 可写 | 图的表单语义与 get_info 业务分组需要同一份事实 |
| LLM 调用 | set/get 走辅助对话 + 事后删记录 | **零 LLM 调用**(测试 spy 保证) | 成本与延迟;删除 race 消失 |
| 重复 `delete_session` | FastGPT SDK 行为未固定 | **幂等成功**(checkpoint 正常返回或适配器明确报告不存在才可继续;存储故障返回业务码 500) | 不再将清理失败当成删除成功 |
## 图表单键 <-> 业务键映射(作业 6 建立)
| 图表单键(accident_fields) | 业务键(acdinfo 分组) |
|---|---|
| `has_injury` | `ywrysw` |
| `has_non_motor_vehicle_hit` | `ywfjdc` |
| `is_on_scene` | `sfsgxc` |
| `vehicle_count` | `jdcsl` |
| `accident_time` | `sgfssj` |
| `accident_cause` | `sgyy` |
- chat 方向:repository 业务键经映射注入图为表单键(图只认表单语义)。
- 写回方向:图产出的表单键差量经映射落 repository 业务键,get_info 分组即可读到。
- `bjrjs`(报警人是否驾驶员)、`ywmtc`(是否撞摩托车)无表单来源:
不进入注入映射,仅支持 set_info 直写、get_info 直读。
## 与 workflow 20261022 的字段对账(2026-10-22 核对)
核对来源:`workflow/20261022/事故信息采集2025118.json`(已与 main@c7ca781 同步)
及历史版本 20250419—20261021。
| 注册表内容 | 对账结果 |
|---|---|
| 图表单六键(`has_injury` 等,FORM_FIELDS 白名单) | **一致**。workflow `form` 全局变量(bNJkeXot)初始即这六键;「更新表单」code 节点白名单语义(只遍历表单已有字段、无效值/同值筛除)与本服务 `_merge_form` 相同 |
| acd 业务八键(`ywrysw` 等) | **语义一致,出处不同**。八键全部出自 **20250419 版** workflow 的提取 schema(含 `sgyy` 事故原因);20251015 起 workflow 已弃用,API 代码自 a1a4bce 起沿用为对外契约。新后端注册表保留 = 兼容旧契约 |
| 人员分组键(`xm1/sfzmhm1/sfzmwh1/sjwh1/csbw1` 等每组 8 键) | **任何 workflow 版本都不存在**。系 API 服务端自定义契约(get_info 分组)。`sfzmwh/sjwh/csbw` 的全量覆盖是契约要求,与 workflow 无关 |
| 作业 6 表单键<->业务键映射(`has_injury`->`ywrysw` 等) | workflow 中**无此映射**——两侧键名从未在同一系统共存(workflow 只用表单键,API 只用业务键)。映射是迁移时按中文语义建立的桥(对照 20250419 提取 schema 的 desc 字段),属本服务新增语义,已双向测试固定 |
### 对账发现的一个真实差异
20261022 workflow 的 `state` 变量中人员信息键为
`driver1ID / driver1Phonenumber`(×2,sandbox 另把 `hphm1/2`
车牌改名 `driver{i}License` 输出给确认提示词——该名不是 state 键),
与 API `get_info` 分组键(`sfzmhm1/sjwh1/sjhm1` 等)**不同名**。
旧 FastGPT 实现按 API 分组键读 state,这些键在 workflow state 中
本就读空——属于既有行为。
**处理(2026-10-22 已落地)**:注册表把 workflow 原生键
`driver{i}ID / driver{i}Phonenumber`(×2,同义 sfzmhm/sjhm)
登记进 human1/human2 分组——
- set/get_info 单键直读写 driver 系键与旧 API 行为一致;
- **有意差异**:`acdhuman1/2` 分组输出现包含 driver 系键
(旧 FastGPT 分组输出没有它们)。调用方按 JSON 取键,多键无害;
这修正了旧实现"workflow 真实人员数据无法分组读到"的问题;
- `driver{i}License` 非 state 键,对其 set_info 按未知键拒绝
(workflow 不消费它);
- 同义键统一存储:`sfzmhm{i}` 与 `driver{i}ID` 共用后者,
`sjhm{i}` 与 `driver{i}Phonenumber` 共用后者。单键和分组读取
继续提供两套键名;图输入同时注入一致的别名视图,避免旧 driver 值
遮盖 API 键的新值。同一批差量包含相互冲突的别名值时拒绝提交。
## 待审计项(作业 6-7 要求的后续工作)
- [ ] 审计现有调用方是否存在对 `set_info` 任意 key 写入的依赖
(如动态自定义键)。存在则在注册表补登记或提供逃生口。
workflow 人员键 `driver{i}ID` / `driver{i}Phonenumber` 及对应
API 别名均已登记并统一存储;`driver{i}License` 只是车牌的
输出名称,外部写车牌继续使用 `hphm{i}`。
- [x] ~~流式 `chat` 的 `formUpdate` 差量写回 repository~~
(图提交时即同步业务字段;FormUpdate 是否返回不影响写回)。
- [ ] `get_info` 返回值中的敏感字段(证件号/手机号)回显边界:
注册表已记录 `SensitiveLevel`,访问控制随作业 9 落地。
## 流式、并发与字段一致性约定
- 阶段前缀先缓冲并按当前节点的转换规则校验,非法前缀不推送;
未输出时仍可使用一次修复预算。已开始输出后,全文格式或时间依据
校验失败会以错误终止,不在同一流中拼接修复答案,不提交本轮业务字段。
已经到达客户端的正文不可撤回;客户端只在收到 done 后将一轮视为成功。
- 会话锁引用包括持有者与等待者,只有最后一个使用者退出才回收。
chat、stream、set、get、delete 全部按同一会话串行;不同会话独立。
取消流时先关闭内部生成器、取消图任务,再释放会话锁。
- needFormUpdate 只决定对外是否返回差量。图提交时通过内部回调同步
repository,关闭差量显示或消费者在提交后断开不会丢失业务字段。
- 删除在同会话锁内先清理 checkpoint,再删除业务字段。只有存储正常返回,
或适配器明确抛出 CheckpointNotFoundError 时才视为 checkpoint 清理成功;
连接、超时、一般 RuntimeError/KeyError 等故障必须上报,业务字段保留供重试。
缺少 checkpointer 或不支持删除操作也返回失败。优先使用 adelete_thread,
仅在没有异步接口时兼容同步 delete_thread。若业务存储删除本身失败,
同样返回失败;此顺序不构成跨存储事务,可能需要再次重试完成清理。
- SSE 错误按 error 事件的 code 区分:鉴权 401、限流 429、其余 500,
使用与非流式相同的固定文案,不回传底层异常原文。首包前和输出途中
都使用同一分类;错误是终止事件,随后不会发送 done。已经建立的
SSE HTTP 响应保持 200,调用方检查事件内的业务码。
- LangGraph 文本模型调用使用 `langchain-openai.ChatOpenAI` 的 ainvoke/astream;
命名 profile 统一映射到 Chat Completions,温度未配置时不发送该参数,
EXTRA_BODY 保留原样。callbacks、metadata、tags 从图节点继承,每次尝试
标记运行名及 llm_attempt。流式 usage 默认不额外请求,兼容既有网关。
提前关闭流时显式关闭 provider 迭代器,避免等待 GC 后才释放 HTTP 连接。
- SDK 自动重试关闭,由应用层统一控制;默认首次尝试加 3 次重试,
每次尝试总超时 60 秒,退避 1、2、4 秒。单次 LLM 调用理论预算
4×60+1+2+4=247 秒(另有响应关闭的清理时间)。FastGPT SDK 的
max_retries 实际代表总尝试数,适配层传入 4 以表达额外重试 3 次;
其 timeout 是 HTTP SDK 超时,不将它等同于整个多节点请求总期限。
流式首个文本 token 前可重试,发出文本后失败则终止,不重放。
该预算并非整个多次 LLM 调用的聊天回合总超时。
## 验证
- `test/services/test_fields_registry.py`:注册表完整性、类型/敏感级别、
外部表示、`bool("0")` 防线。
- `test/services/test_app_service.py`:四接口组合(set→get→chat 注入→
差量写回→分组读→删除→无泄漏),set/get/delete 零模型调用 spy,
重复删除幂等。
- `test/api/test_assignment6_endpoints.py`:HTTP 层验收演示、删除失败保留数据及重试成功链路。
- `test/services/test_delete_failure_semantics.py`:存储故障、明确不存在、取消、清理能力缺失与重试。
- `test/api/test_stream_error_codes.py`:实际 LangGraphBackend 过 HTTP 协议层,
验证首包前/输出途中 401、429、500 分类、文案与非流式一致、错误后无 done。