完成可审批客服工单系统

客服工单全流程

本章目标

把前九章能力组合成一个真实闭环,并验证通过、拒绝、重试耗尽和幂等路径。

核心代码:src/support_agent/graph.py

状态只保存可持久化业务数据

class SupportState(TypedDict, total=False):
    tenant_id: str
    ticket_id: str
    question: str
    intent: Literal["business", "other"]
    kb_result: str
    order_result: str
    proposal: str
    approved: bool
    attempts: int
    execution_result: Literal[
        "waiting", "temporary_failure", "success", "rejected", "manual"
    ]
    user_message: str

服务客户端通过构图函数注入,不放入 State:src/support_agent/services.py

业务路径

工单如何协同处理

classify
  ├─ other → fallback → END
  └─ business → query_kb + query_order
                   ↓ join
               proposal 子图
                   ↓
               approval interrupt
                 ├─ reject → END
                 └─ approve → execute
                                  ├─ success → done → END
                                  ├─ retry → execute
                                  └─ exhausted → manual_queue → END

幂等执行

幂等挡住重复退款

idempotency_key = f"{tenant_id}:{ticket_id}:refund:v1"
result = await orders.refund(idempotency_key=idempotency_key)

即使节点重试、客户端重发或 Worker 崩溃后恢复,相同业务动作也使用同一个键。最终是否只执行一次,必须由订单或支付服务保证,不能只依赖 Agent 内存。

运行 API

本地内存模式:

APP_ENV=development uvicorn support_agent.api:app --app-dir src --reload

X-Tenant-ID 在示例中只用于演示线程隔离,不是身份凭证。生产系统必须由 API Gateway 或服务端鉴权中间件从已验证身份推导租户,不能信任客户端任意填写该请求头。

创建工单:

curl -s -X POST http://localhost:8000/tickets/T-100/runs \
  -H 'Content-Type: application/json' \
  -H 'X-Tenant-ID: tenant-a' \
  -d '{"question":"订单尚未发货,请退款"}'

返回值包含审批 payload:

{
  "state": {"ticket_id": "T-100", "proposal": "..."},
  "interrupts": [{"ticket_id": "T-100", "question": "是否批准执行退款?"}]
}

审批通过:

curl -s -X POST http://localhost:8000/tickets/T-100/approval \
  -H 'Content-Type: application/json' \
  -H 'X-Tenant-ID: tenant-a' \
  -d '{"approved":true}'

验证结果:

curl -s http://localhost:8000/tickets/T-100/state \
  -H 'X-Tenant-ID: tenant-a'

成功时 execution_resultsuccessnext 为空列表。

自动化验证矩阵

测试验证结论
审批通过执行退款并完成
审批拒绝不调用退款服务
暂时失败一次第二次业务尝试成功
连续失败三次后转人工
重复幂等键业务服务只执行一次
非业务问题不进入审批,直接转人工
不同租户相同工单 ID状态彼此隔离
缺少租户头API 拒绝请求
重复提交审批返回 409,不重复执行
生产环境缺少数据库服务启动失败而非退回内存模式

运行:

python -m pytest tests/test_production_graph.py tests/test_api.py

哪些部分是真实实现,哪些是教学替身

“生产形态”不应被误解成已经连接真实资金系统。本仓库的边界如下:

能力当前实现上线前替换项
状态图、审批与恢复真实 LangGraph API增加业务审批表和并发运行控制
PostgreSQL Checkpointer真实异步 Saver独立迁移、连接池和保留策略
知识库查询固定文本测试替身RAG 服务、来源引用、超时与降级
订单查询内存测试替身已认证订单 API 和错误码映射
退款幂等进程内集合演示下游数据库唯一约束和结果查询 API
租户识别X-Tenant-ID 演示头网关认证、服务端租户推导与授权

src/support_agent/services.py 定义 KnowledgeGatewayOrderGateway 协议,使生产适配器与测试替身遵守同一最小接口。生产适配器还必须补齐超时、连接池、认证、可观测性和稳定错误映射。

API 运行协议

创建运行应返回稳定 run_idthread_id、当前状态和可轮询地址。相同业务请求重发时,可以用客户端请求键返回已有运行,避免同一工单并发启动两张图。审批接口必须验证当前状态确实等待审批;执行中的运行应返回冲突而不是覆盖状态。

建议把外部错误统一成领域结果:

SUCCESS
TEMPORARY_FAILURE
PERMANENT_REJECTION
UNKNOWN_RESULT

UNKNOWN_RESULT 表示请求可能已经在下游成功,必须通过幂等键查询,不能直接当作失败再次退款。

资金副作用的提交顺序

一个更稳妥的流程是:

  1. 业务数据库原子写入已批准的执行任务和幂等键。
  2. Outbox 或可靠队列投递退款命令。
  3. Worker 调用退款服务并按幂等键查询结果。
  4. 把最终业务结果写回业务数据库。
  5. 图读取业务事实并继续完成或转人工。

这样即使应用在任一步崩溃,也能从业务任务恢复。不要把 Checkpoint 是否写成功当作资金动作是否成功的唯一证据。

端到端故障演练

在宣布项目可上线前,依次注入以下故障并保存证据:知识库超时、订单 API 429、审批重复提交、审批超时、退款成功但响应丢失、PostgreSQL 短暂断连、应用在审批后重启、两个实例同时处理同一线程。每次演练都记录最终业务状态、退款调用次数、Checkpoint 状态、告警和恢复耗时。

本章验收

  • 本地 API 能完成创建、暂停、批准、状态查询和 SSE 流程。
  • 通过、拒绝、技术重试、业务重试耗尽和重复审批均有测试。
  • 能指出测试替身不能提供哪些生产保证。
  • 为真实知识库、订单和退款服务写出适配器契约及错误映射。
  • 资金动作的最终事实存在业务数据库或下游系统,而不是仅存在 State。