Files
ZNJJ-api-server/docs/langgraph-backend-migration-plan.md
2026-07-27 17:21:29 +08:00

1422 lines
40 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.
# ZNJJ LangGraph 后端改造计划
> 状态Phase 0 已冻结LangGraph 最小开发纵切已启动
> 日期2026-07-25
> Phase 0 收口日期2026-07-26
> 目标项目:`ZNJJ-api-server`
> 参考项目:`fastapi-langgraph-agent-production-ready-template`
## 1. 背景
当前 ZNJJ 的 `/chat``/set_info``/get_info` 主要依赖 FastGPT
- `/chat` 将用户输入发送给 FastGPT由模型输出 `<state>状态码</state>`,服务端再解析状态码和正文。
- `formUpdate` 从 FastGPT 工作流节点的响应中提取。
- `/set_info``/get_info` 通过额外的 FastGPT 对话调用读写工作流变量,并通过删除聊天记录隐藏辅助调用。
- 事故采集、拍照引导、信息确认、安全转人工等规则大量写在 Prompt 和 FastGPT 工作流 JSON 中。
这种实现使确定性的业务状态依赖模型输出和平台内部结构,存在状态跳转不可控、接口结果不稳定、难以测试、难以审计以及供应商耦合等问题。
本次改造使用 LangGraph 承担事故处理流程编排,同时保持现有外部接口协议稳定,使其他团队可以继续通过 `/chat``/set_info``/get_info` 对接。
## 2. 范围
### 2.1 本期范围
-`/chat` 后端从 FastGPT 迁移到 LangGraph。
-`/set_info``/get_info` 改为类型化业务状态的直接读写。
- 引入 PostgreSQL、LangGraph checkpointer 和业务数据表。
- 引入面向系统调用的鉴权和 session 级授权。
- 建立统一配置、结构化日志、指标、链路追踪和脱敏机制。
- 建立领域状态机、图节点、API 契约、数据库、安全、并发恢复和 LLM 评测。
- 通过功能开关、shadow 对比和灰度发布逐步替换 FastGPT。
### 2.2 本期不包含
- Pipecat 语音管线改造。
- STT、TTS、VAD、语音打断和 WebSocket 协议改造。
- 面向终端用户的注册、密码登录和账号体系。
- mem0、pgvector 或跨 session 长期语义记忆。
- 前端或其他调用团队的接口升级。
- 将 FastGPT 工作流 JSON 自动转换成 LangGraph。
### 2.3 兼容性原则
- 保持 `/chat``/set_info``/get_info` 路径不变。
- 保持现有请求和响应字段的 camelCase 命名。
- 保持响应体中的字符串业务码,例如 `"200"``"500"`
- 保持 `/chat?stream=true` 的 SSE 方式和既有事件名称。
- 兼容期内不同时改变接口协议和后端实现。
- 内部统一使用 snake_case 和类型化模型,通过 Pydantic alias 适配外部协议。
## 3. 改造目标
### 3.1 业务目标
- 自然语言轮次继续使用 `<state>XXXX</state>` 前缀,由模型在单次回复中给出候选状态码和正文。
- 服务端负责流式解析状态前缀、校验状态码及迁移合法性,并将通过校验的状态持久化。
- 拍照完成、连续拍照失败、无回复、人工转接等使用结构化业务事件。
- 事故字段经过结构化提取和代码校验后才能写入。
- `/chat``/set_info``/get_info` 使用同一份业务状态。
- 服务重启或请求失败后能够从持久化状态恢复。
- 同一 session 的并发请求不会静默覆盖数据或重复推进流程。
### 3.2 工程目标
- API、应用服务、LangGraph、领域模型、数据库和基础设施职责分离。
- 所有外部调用、数据库操作和模型调用均为异步。
- 配置启动时严格校验,生产环境禁止不安全默认值。
- 日志、指标和 trace 不泄漏身份证、手机号、车牌、Token 等敏感数据。
- 核心状态迁移不依赖真实 LLM 即可完成自动化测试。
- 能够按调用方或流量比例灰度切换 FastGPT/LangGraph。
## 4. 核心架构决策
| 编号 | 决策 | 说明 |
|---|---|---|
| ADR-001 | 保留 FastAPI对 API 层做薄适配 | Endpoint 只负责鉴权、校验和协议转换 |
| ADR-002 | 使用 LangGraph `StateGraph` | 显式表达业务节点、条件边和持久化状态 |
| ADR-003 | 本版保留 state prefix | 自然语言轮次由模型输出 `<state>XXXX</state>`,服务端负责解析和合法性校验 |
| ADR-004 | 不为状态码增加第二次 LLM 调用 | 同一次模型回复产生状态前缀和用户可见正文;代码只校验候选状态,不再次做语义判断 |
| ADR-005 | PostgreSQL 是生产数据库 | 同时承载 LangGraph checkpoint 和应用业务表 |
| ADR-006 | Checkpoint 与业务数据分离 | Checkpoint 用于恢复;业务表用于 `/get_info`、审计和报表 |
| ADR-007 | 只使用 thread 级短期记忆 | 不引入跨事故会话的长期语义记忆 |
| ADR-008 | B2B 服务身份鉴权 | 优先使用网关/OIDC Client CredentialsAPI Key 仅作兼容方案 |
| ADR-009 | 每个 session 串行修改 | 使用数据库锁和 `state_version` 防止并发覆盖 |
| ADR-010 | 业务副作用必须幂等 | 通过 `event_id``Idempotency-Key` 支持安全重试 |
| ADR-011 | 生产环境持久化失败时拒绝服务 | 不允许退化为无 checkpointer 的有状态流程 |
| ADR-012 | 先保持接口兼容,再设计 v2 | 本次不调整历史协议中的 JSON 字符串等设计 |
## 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"]
Caller --> Auth --> API --> Service --> Graph
Graph --> Rules
Graph --> LLM
Graph <--> Checkpoint
Graph <--> DomainDB
API --> Telemetry
Service --> Telemetry
Graph --> Telemetry
```
### 5.1 推荐目录
```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/
```
## 6. API 契约设计
### 6.1 `/chat`
外部接口保持:
```text
POST /chat
POST /chat?stream=true
```
内部统一请求:
```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
<state>1002</state>请问事故中有没有人员受伤?
```
服务端从开头解析候选状态码,校验通过后写入 `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 必须位于回复开头,格式为 `<state>四位数字</state>`
- 服务端 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
<state>XXXX</state>用户可见正文
```
如果某个阶段还需要结构化字段提取,可在独立节点中使用 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基线与契约冻结
#### 完成记录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 前补录。
#### 工作项
- 梳理现有三个接口的真实请求/响应行为。
- 补齐 API 契约测试。
- 整理全部状态码、字段、分组和转换规则。
- 从现有 FastGPT 工作流和 Prompt 提取业务规则。
- 建立脱敏黄金对话数据集。
- 记录当前延迟、错误率和转人工比例基线。
#### 交付物
- API 契约测试。
- 状态码表和字段注册表。
- 状态转换矩阵。
- 黄金场景数据集。
- 当前系统基线报告。
#### 验收
- 在未改业务代码前,契约测试能够通过。
- 三个接口的兼容行为有明确文档。
- 不再依赖口头说明解释状态码和字段。
### 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。
- 引入 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 3PostgreSQL 与持久化
#### 工作项
- 引入 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 5LangGraph 流程骨架与状态校验
#### 工作项
- 实现输入事件归一化。
- 实现状态转换表。
- 实现非流式和增量流式 state prefix parser。
- 实现状态码枚举和迁移 validator。
- 实现全局转人工、安全路由和阶段路由。
- 实现单车/双车拍照流程。
- 实现澄清和无回复计数。
- 实现固定话术。
- 建立节点和图场景测试。
#### 验收
- 不调用 LLM 即可完整执行拍照完成、拍照失败、无回复和外部字段更新等确定性流程。
- Prefix parser 能处理标签跨 chunk、缺失、重复、未知状态码和非法迁移。
- 拍照状态不能跳步。
- 所有转人工状态符合优先级。
- 非法转换被阻止并可观测。
### Phase 6State prefix LLM 与非流式 `/chat`
#### 工作项
- 建立 LLM provider/service 抽象。
- 实现超时、有限重试和熔断策略。
- 定义 `<state>XXXX</state>正文` 的 Prompt 输出契约。
- 对需要字段提取的节点单独定义结构化 schema避免把状态码复判拆成第二次调用。
- 在阶段 LLM 节点中合并必要的事故字段理解、安全语义判断、回答有效性分类、候选状态前缀和正文生成。
- 实现字段验证和 `formUpdate`
- 接入非流式 `/chat`
- 执行黄金场景评测。
#### 验收
- 非流式 `/chat` 能从回复开头解析 `<state>`,并且不把标签暴露到 `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 9Shadow、灰度和下线 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。
- 自然语言轮次统一解析模型回复开头的 `<state>XXXX</state>`,并在对外正文中移除标签。
- `/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 下线前需要保留的历史会话和迁移方式。