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

40 KiB
Raw Blame History

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_idIdempotency-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_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不再创建辅助聊天记录。

处理流程:

鉴权
  -> 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),
}

禁止外部接口修改:

  • phase
  • stage_code
  • clarification_counts
  • no_response_count
  • handoff_reason
  • state_version
  • LangGraph 内部路由字段

6.3 /get_info

/get_info 是纯读取接口:

鉴权
  -> session 对象级授权
  -> 从业务投影读取当前版本
  -> 按 key/group 转换
  -> 返回现有 ProcessResponse_get

兼容以下 key

  • all
  • acdinfo
  • acdhuman1
  • acdhuman2
  • 字段注册表中的单个字段

兼容期内 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 检查状态码是否存在于枚举,以及是否允许从当前状态迁移。
  • 标签缺失、重复、未知或迁移非法时,最多进行一次受控重试;仍失败时按配置返回错误或进入保守转人工状态。
  • PhotoCompletedEventPhotoRecognitionFailedEventNoResponseEventSetInfoEvent 等确定性事件可以不调用 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_input
  • hydrate_state
  • global_handoff_gate 中的显式指令判断
  • safety_precheck 中的确定性高风险规则
  • route_phase
  • 拍照状态推进
  • 澄清计数
  • 无回复计数
  • 字段格式校验
  • 状态转换合法性校验
  • apply_external_update
  • apply_form_patch
  • 固定话术选择
  • handoff
  • complete

8.3 LLM 节点

LLM 仅用于:

  • 事故描述字段提取。
  • 用户回答有效性分类。
  • 人伤或复杂情况的语义识别;该判断应与当前阶段的 state-prefix 回复合并在同一次模型调用中。
  • 车牌、时间、车辆数量等自然语言归一化的辅助判断。
  • 必要的自然语言措辞。

自然语言回复契约:

<state>XXXX</state>用户可见正文

如果某个阶段还需要结构化字段提取,可在独立节点中使用 Pydantic schema但不得仅为了再次判断状态码而增加第二次 LLM 调用。

处理原则:

  • State prefix 解析或迁移校验失败时最多进行一次受控重试。

  • 模型输出只是候选数据,不能直接写业务状态。

  • 安全判断不确定时进入确认或人工处理,不允许冒险放行。

  • 模型可以通过 prefix 提议 stage_code,但只有通过枚举和迁移校验后才能写入 Statephase 和计数器仍只由代码修改。

  • 自然语言轮次的状态前缀和正文由同一次模型回复产生;固定系统事件优先使用代码模板,减少模型延迟和随机性。 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
  • expnbf
  • subclient_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 只管理 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

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 建议告警

  • /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 门禁

建议流水线:

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.9pydantic-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、领域模型和枚举。
  • 建立字段注册表和分组映射。
  • 实现 SetInfoEventapply_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_codeformUpdate 与非流式一致。
  • 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 发布

开发环境全量
  -> 测试环境全量
  -> 生产 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. 待确认事项

实施前需要由产品、对接团队或基础设施负责人确认:

  1. 是否已有统一 API Gateway/OIDC/JWKS。
  2. 调用团队与 tenant_id 的映射方式。
  3. 是否允许为请求增加可选 Idempotency-Key Header。
  4. 历史接口 HTTP 状态码和响应体业务码的精确兼容要求。
  5. /chat SSE 中 stage_codeformUpdate 的最终顺序约定。
  6. sessionId 的创建方、唯一性和生命周期。
  7. 事故信息、checkpoint、事件、日志和 trace 的保留期限。
  8. 身份证、手机号等字段的加密和密钥托管方案。
  9. 生产 PostgreSQL 版本、schema 权限和连接池限制。
  10. Langfuse/OpenTelemetry 数据是否允许发送到外部或自建平台。
  11. LLM provider、模型白名单、超时、配额和容灾策略。
  12. Shadow 和灰度的业务验收指标。
  13. FastGPT 下线前需要保留的历史会话和迁移方式。