diff --git a/.env b/.env index b431a13..d7a631a 100644 --- a/.env +++ b/.env @@ -3,7 +3,7 @@ SECRET_KEY=your_secret_key DEBUG=True ANALYSIS_SERVICE_URL=http://127.0.0.1:3030 -ANALYSIS_AUTH_TOKEN=fastgpt-r13smJwPgXfGj1HDfc4SWAvIoNrL5Wc6o0BYnezqBs7hgzPdQ7Q34hVl2FJc0R -APP_ID=6a310def7132e9f7d592dabb +ANALYSIS_AUTH_TOKEN=fastgpt-ikgXptijS8NPkSuDxXLS4ShWc8PzMD4fZ5PlIXKt2dAcUrWEkfhp +APP_ID=691c1ae753e3f8d9f25ebc9b VOICE_CONFIG=config/voice-fastgpt-state-xfyunSuperTTS.json diff --git a/docs/langgraph-backend-migration-plan.md b/docs/langgraph-backend-migration-plan.md new file mode 100644 index 0000000..ad26c11 --- /dev/null +++ b/docs/langgraph-backend-migration-plan.md @@ -0,0 +1,1398 @@ +# ZNJJ LangGraph 后端改造计划 + +> 状态:Draft +> 日期:2026-07-25 +> 目标项目:`ZNJJ-api-server` +> 参考项目:`fastapi-langgraph-agent-production-ready-template` + +## 1. 背景 + +当前 ZNJJ 的 `/chat`、`/set_info`、`/get_info` 主要依赖 FastGPT: + +- `/chat` 将用户输入发送给 FastGPT,由模型输出 `状态码`,服务端再解析状态码和正文。 +- `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 业务目标 + +- 自然语言轮次继续使用 `XXXX` 前缀,由模型在单次回复中给出候选状态码和正文。 +- 服务端负责流式解析状态前缀、校验状态码及迁移合法性,并将通过校验的状态持久化。 +- 拍照完成、连续拍照失败、无回复、人工转接等使用结构化业务事件。 +- 事故字段经过结构化提取和代码校验后才能写入。 +- `/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 | 自然语言轮次由模型输出 `XXXX`,服务端负责解析和合法性校验 | +| 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. 目标架构 + +```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 +1002请问事故中有没有人员受伤? +``` + +服务端从开头解析候选状态码,校验通过后写入 `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 必须位于回复开头,格式为 `四位数字`。 +- 服务端 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 +XXXX用户可见正文 +``` + +如果某个阶段还需要结构化字段提取,可在独立节点中使用 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:基线与契约冻结 + +#### 工作项 + +- 梳理现有三个接口的真实请求/响应行为。 +- 补齐 API 契约测试。 +- 整理全部状态码、字段、分组和转换规则。 +- 从现有 FastGPT 工作流和 Prompt 提取业务规则。 +- 建立脱敏黄金对话数据集。 +- 记录当前延迟、错误率和转人工比例基线。 + +#### 交付物 + +- API 契约测试。 +- 状态码表和字段注册表。 +- 状态转换矩阵。 +- 黄金场景数据集。 +- 当前系统基线报告。 + +#### 验收 + +- 在未改业务代码前,契约测试能够通过。 +- 三个接口的兼容行为有明确文档。 +- 不再依赖口头说明解释状态码和字段。 + +### Phase 1:工程骨架与配置 + +#### 工作项 + +- 建立 `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 抽象。 +- 实现超时、有限重试和熔断策略。 +- 定义 `XXXX正文` 的 Prompt 输出契约。 +- 对需要字段提取的节点单独定义结构化 schema,避免把状态码复判拆成第二次调用。 +- 在阶段 LLM 节点中合并必要的事故字段理解、安全语义判断、回答有效性分类、候选状态前缀和正文生成。 +- 实现字段验证和 `formUpdate`。 +- 接入非流式 `/chat`。 +- 执行黄金场景评测。 + +#### 验收 + +- 非流式 `/chat` 能从回复开头解析 ``,并且不把标签暴露到 `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 发布 + +```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。 +- 自然语言轮次统一解析模型回复开头的 `XXXX`,并在对外正文中移除标签。 +- `/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 下线前需要保留的历史会话和迁移方式。 + diff --git a/docs/视频快处智能信息采集机器人交互接口文档V1.0.8.docx b/docs/视频快处智能信息采集机器人交互接口文档V1.0.8.docx deleted file mode 100644 index 5e9b43a..0000000 Binary files a/docs/视频快处智能信息采集机器人交互接口文档V1.0.8.docx and /dev/null differ