完成可审批客服工单系统

本章目标
把前九章能力组合成一个真实闭环,并验证通过、拒绝、重试耗尽和幂等路径。
核心代码: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_result 为 success,next 为空列表。
自动化验证矩阵
| 测试 | 验证结论 |
|---|---|
| 审批通过 | 执行退款并完成 |
| 审批拒绝 | 不调用退款服务 |
| 暂时失败一次 | 第二次业务尝试成功 |
| 连续失败 | 三次后转人工 |
| 重复幂等键 | 业务服务只执行一次 |
| 非业务问题 | 不进入审批,直接转人工 |
| 不同租户相同工单 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 定义 KnowledgeGateway 和 OrderGateway 协议,使生产适配器与测试替身遵守同一最小接口。生产适配器还必须补齐超时、连接池、认证、可观测性和稳定错误映射。
API 运行协议
创建运行应返回稳定 run_id、thread_id、当前状态和可轮询地址。相同业务请求重发时,可以用客户端请求键返回已有运行,避免同一工单并发启动两张图。审批接口必须验证当前状态确实等待审批;执行中的运行应返回冲突而不是覆盖状态。
建议把外部错误统一成领域结果:
SUCCESS
TEMPORARY_FAILURE
PERMANENT_REJECTION
UNKNOWN_RESULT
UNKNOWN_RESULT 表示请求可能已经在下游成功,必须通过幂等键查询,不能直接当作失败再次退款。
资金副作用的提交顺序
一个更稳妥的流程是:
- 业务数据库原子写入已批准的执行任务和幂等键。
- Outbox 或可靠队列投递退款命令。
- Worker 调用退款服务并按幂等键查询结果。
- 把最终业务结果写回业务数据库。
- 图读取业务事实并继续完成或转人工。
这样即使应用在任一步崩溃,也能从业务任务恢复。不要把 Checkpoint 是否写成功当作资金动作是否成功的唯一证据。
端到端故障演练
在宣布项目可上线前,依次注入以下故障并保存证据:知识库超时、订单 API 429、审批重复提交、审批超时、退款成功但响应丢失、PostgreSQL 短暂断连、应用在审批后重启、两个实例同时处理同一线程。每次演练都记录最终业务状态、退款调用次数、Checkpoint 状态、告警和恢复耗时。
本章验收
- 本地 API 能完成创建、暂停、批准、状态查询和 SSE 流程。
- 通过、拒绝、技术重试、业务重试耗尽和重复审批均有测试。
- 能指出测试替身不能提供哪些生产保证。
- 为真实知识库、订单和退款服务写出适配器契约及错误映射。
- 资金动作的最终事实存在业务数据库或下游系统,而不是仅存在 State。