40 KiB
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 Credentials,API Key 仅作兼容方案 |
| ADR-009 | 每个 session 串行修改 | 使用数据库锁和 state_version 防止并发覆盖 |
| ADR-010 | 业务副作用必须幂等 | 通过 event_id 和 Idempotency-Key 支持安全重试 |
| ADR-011 | 生产环境持久化失败时拒绝服务 | 不允许退化为无 checkpointer 的有状态流程 |
| ADR-012 | 先保持接口兼容,再设计 v2 | 本次不调整历史协议中的 JSON 字符串等设计 |
5. 目标架构
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 推荐目录
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
外部接口保持:
POST /chat
POST /chat?stream=true
内部统一请求:
class UserMessageEvent(BaseModel):
event_id: str
text: str
need_form_update: bool = False
use_text_chunk: bool = False
内部统一非流式结果:
class AgentTurnResult(BaseModel):
output_text: str
stage_code: StageCode
stage_name: str
form_update: dict[str, object]
state_version: int
非流式 API 将 AgentTurnResult 转换为现有 ProcessResponse_chat。
自然语言轮次的原始模型回复契约:
<state>1002</state>请问事故中有没有人员受伤?
服务端从开头解析候选状态码,校验通过后写入 AgentTurnResult.stage_code,并从 output_text 中移除标签。标签缺失、格式错误、未知状态码或非法迁移不得直接对外返回。
流式 API 使用同一张图,固定输出顺序:
stage_code state prefix 解析并校验成功后发送,每轮最多一次
formUpdate 有字段更新时发送
text_delta 零个或多个
done 成功时必须且仅一次
错误事件:
error 失败时必须且仅一次,为终止事件
兼容期内继续支持现有事件名称:
stage_codeformUpdatetext_deltadoneerror
6.1.1 流式与非流式一致性
- 流式所有
text_delta.text拼接后必须等于非流式outputText。 stage_code.nextStageCode必须等于从同一条模型回复解析并校验后的非流式nextStageCode。formUpdate必须来自同一份合法状态 patch。- 不允许为流式和非流式维护两套业务实现。
6.2 /set_info
/set_info 不再调用 LLM,不再创建辅助聊天记录。
处理流程:
鉴权
-> session 对象级授权
-> key 白名单校验
-> value 类型转换与领域校验
-> SetInfoEvent
-> LangGraph apply_external_update 分支
-> 幂等写业务状态与事件
-> 保存 checkpoint
只允许修改字段注册表中声明的业务字段:
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),
}
禁止外部接口修改:
phasestage_codeclarification_countsno_response_counthandoff_reasonstate_version- LangGraph 内部路由字段
6.3 /get_info
/get_info 是纯读取接口:
鉴权
-> session 对象级授权
-> 从业务投影读取当前版本
-> 按 key/group 转换
-> 返回现有 ProcessResponse_get
兼容以下 key:
allacdinfoacdhuman1acdhuman2- 字段注册表中的单个字段
兼容期内 value 保持为 JSON 编码后的字符串。将来若设计 v2,再改成真正的 JSON 对象。
6.4 错误处理
内部使用稳定错误类型:
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 状态模型
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 输入事件
UserMessageEvent
SetInfoEvent
PhotoCompletedEvent
PhotoRecognitionFailedEvent
NoResponseEvent
SessionStartedEvent
兼容期由 API 边界把历史魔法字符串转换成结构化事件,例如:
【拍摄完成】 -> PhotoCompletedEvent
【客户端连续3次拍摄识别失败:原因】 -> PhotoRecognitionFailedEvent
【用户无回复】 -> NoResponseEvent
图内部不再根据自然语言魔法字符串推进流程。
7.3 状态码
使用枚举和集中式状态转换表:
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 主图
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_inputhydrate_stateglobal_handoff_gate中的显式指令判断safety_precheck中的确定性高风险规则route_phase- 拍照状态推进
- 澄清计数
- 无回复计数
- 字段格式校验
- 状态转换合法性校验
apply_external_updateapply_form_patch- 固定话术选择
handoffcomplete
8.3 LLM 节点
LLM 仅用于:
- 事故描述字段提取。
- 用户回答有效性分类。
- 人伤或复杂情况的语义识别;该判断应与当前阶段的 state-prefix 回复合并在同一次模型调用中。
- 车牌、时间、车辆数量等自然语言归一化的辅助判断。
- 必要的自然语言措辞。
自然语言回复契约:
<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:
调用团队
-> client_id/client_secret
-> 身份平台签发 JWT
-> ZNJJ 通过 JWKS 验证
JWT 必须验证:
- 允许的签名算法。
iss。aud。exp、nbf。sub或client_id。scope。- 可选
jti。
Token 中禁止包含身份证、手机号、车牌等敏感信息。
9.2 权限
chat:write
session:read
session:write
建议映射:
| 接口 | Scope |
|---|---|
/chat |
chat:write |
/get_info |
session:read |
/set_info |
session:write |
9.3 对象级授权
任何 session 查询都必须包含当前身份中的 tenant_id:
WHERE tenant_id = :current_tenant_id
AND session_id = :session_id
内部 LangGraph thread_id:
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:
langgraph schema
LangGraph 自管理 checkpoint 表
app schema
Alembic 管理业务表
生产环境禁止以 SQLite 替代。
11.2 业务表
api_clients
id
tenant_id
client_id
credential_hash
scopes
status
expires_at
created_at
updated_at
如果完全使用外部 OIDC/JWT,可只保留客户端策略和 tenant 映射,不保存 credential。
accident_sessions
tenant_id
session_id
status
phase
stage_code
state_version
created_at
updated_at
last_activity_at
completed_at
约束:
UNIQUE (tenant_id, session_id)
accident_state
tenant_id
session_id
form_data JSONB
state_version
updated_at
首版可使用 JSONB 保持与现有字段模型兼容;稳定后再按查询和监管需要拆分规范化表。
accident_events
event_id
tenant_id
session_id
event_type
actor_client_id
payload
previous_version
new_version
created_at
约束:
UNIQUE (tenant_id, event_id)
idempotency_records
client_id
idempotency_key
request_hash
response_data
expires_at
created_at
约束:
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。
配合乐观版本控制:
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 只管理
appschema。 - 排除 LangGraph checkpointer 自管理表。
- 不在应用启动时调用 ORM
create_all()。 - 自动生成 migration 后必须人工审查。
- CI 执行
alembic check。 - 测试从空数据库升级到最新 revision。
- 上线前验证 downgrade 或制定明确的 forward-fix 策略。
11.6 数据保护
- 数据库和备份加密。
- 身份证、手机号等高敏字段应用层加密。
- 需要等值查询时额外保存不可逆 HMAC。
- 日志和 trace 只保留掩码。
- checkpoint、业务表、事件、幂等记录和 trace 定义统一保留期限。
- session 清理必须覆盖 checkpoint 和业务数据。
12. 配置设计
使用 pydantic-settings:
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",
)
环境变量示例:
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 固定。 - 不直接复制参考项目无上限的
>=依赖策略。
主要功能开关:
AGENT_BACKEND=fastgpt|langgraph
LANGGRAPH_SHADOW_ENABLED=true|false
LANGFUSE_ENABLED=true|false
OTEL_ENABLED=true|false
13. 可观测性
13.1 结构化日志
统一使用 structlog,生产输出 JSON。
每条请求日志绑定:
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 指标:
http_requests_total{endpoint,status}
http_request_duration_seconds{endpoint}
业务和 Agent 指标:
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 健康检查
GET /health/live
GET /health/ready
live 只判断进程是否存活。
ready 至少检查:
- 配置已加载。
- PostgreSQL 可连接。
- LangGraph checkpointer 可用。
- 必要的模型配置存在。
生产持久化不可用时 readiness 必须失败。
13.6 建议告警
/chat5xx 或内部错误率。- P95/P99 响应时间和流式 TTFB。
- LLM 超时、state prefix 解析失败和非法状态迁移率。
- checkpoint 写入失败。
- session 冲突率。
- 异常状态转换。
0002/0003/0005转人工比例异常变化。- 数据库连接池使用率。
14. 测试策略
14.1 领域单元测试
不连接数据库、不调用 LLM,覆盖:
- 每个状态允许的下一状态。
- 单车和双车拍照不可跳步。
- 全局转人工优先级。
- 人伤/复杂情况路由。
- 澄清次数和无回复次数。
- 时间、车辆数量、手机号、身份证、车牌校验。
- 字段白名单和类型转换。
formUpdatediff。- 非法状态转换。
- 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_infomass assignment。- API Key 禁用、过期和轮换。
- 请求重放。
- 超长输入和资源限制。
- 日志、trace 中不存在 Token、手机号、身份证。
14.7 并发与故障恢复测试
- 同一 session 两个
/chat并发。 /chat与/set_info并发。- 相同
Idempotency-Key重复提交。 - 相同 key 携带不同请求体。
- 业务状态写入成功但响应超时。
- 图节点执行成功但 checkpoint 写入失败。
- 进程在节点之间退出。
- 服务重启后恢复并继续。
14.8 LLM 评测
使用脱敏黄金数据集评测:
- 人伤识别召回率。
- 主动转人工意图识别。
- 字段提取准确率。
- 无效/无关回答分类。
- 时间逻辑。
- 车辆数量识别。
- State prefix 格式有效率和合法迁移率。
- 字段提取节点的结构化输出有效率。
- 不允许的状态跳转次数。
安全类指标优先控制漏判,不只看总体准确率。
14.9 性能测试
/get_infoP95/P99。/set_infoP95/P99。/chat非流式总耗时。/chat流式 TTFB 和总耗时。- 并发 session 数量。
- 单 session 高频请求。
- 数据库连接池和 LLM 限流。
14.10 CI 门禁
建议流水线:
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 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 抽象。
- 实现超时、有限重试和熔断策略。
- 定义
<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 9:Shadow、灰度和下线 FastGPT
工作项
- 引入
AGENT_BACKEND=fastgpt|langgraph。 - 实现无副作用 shadow 对比。
- Shadow 使用隔离 thread namespace,不写真实业务状态。
- 对比状态码、字段 patch、转人工原因和文本。
- 按调用方或流量比例灰度。
- 监控错误率、延迟、转人工率和字段差异。
- 稳定后切换默认后端。
- 最后移除 FastGPT 依赖、标签解析和辅助聊天记录逻辑。
验收
- 可以快速切回 FastGPT。
- 灰度指标满足发布阈值。
- LangGraph 全量稳定运行一个约定观察周期。
- 删除 FastGPT 后契约和回归测试全部通过。
16. 发布与回滚
16.1 发布
开发环境全量
-> 测试环境全量
-> 生产 shadow
-> 指定调用方灰度
-> 小比例流量
-> 扩大比例
-> 全量 LangGraph
-> 观察期
-> 删除 FastGPT
16.2 回滚
兼容期:
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. 待确认事项
实施前需要由产品、对接团队或基础设施负责人确认:
- 是否已有统一 API Gateway/OIDC/JWKS。
- 调用团队与
tenant_id的映射方式。 - 是否允许为请求增加可选
Idempotency-KeyHeader。 - 历史接口 HTTP 状态码和响应体业务码的精确兼容要求。
/chatSSE 中stage_code、formUpdate的最终顺序约定。sessionId的创建方、唯一性和生命周期。- 事故信息、checkpoint、事件、日志和 trace 的保留期限。
- 身份证、手机号等字段的加密和密钥托管方案。
- 生产 PostgreSQL 版本、schema 权限和连接池限制。
- Langfuse/OpenTelemetry 数据是否允许发送到外部或自建平台。
- LLM provider、模型白名单、超时、配额和容灾策略。
- Shadow 和灰度的业务验收指标。
- FastGPT 下线前需要保留的历史会话和迁移方式。