部署、扩容与生产边界

本章目标
使用 PostgreSQL 和容器启动服务,并明确自托管还需要哪些平台能力。
启动生产形态环境

cp .env.example .env
docker compose up --build
.env.example 只提供本地基线。共享环境必须替换数据库密码,并由 Secret Manager 或部署平台注入,不能提交真实密钥。Dockerfile 使用非 root 用户运行 API;镜像扫描、基础镜像更新和依赖锁定仍应进入 CI。
验证:
curl -s http://localhost:8000/health/live
curl -s http://localhost:8000/health/ready
live 只证明进程存活;ready 还会在两秒超时内读取一次 Checkpointer。它不会重新编译或执行完整业务图,但能在 PostgreSQL 不可访问时返回 503。
异步调用
FastAPI 路由使用:
result = await graph.ainvoke(...)
不要在 async def 路由中直接调用同步 graph.invoke(),否则阻塞事件循环。同步 SDK 也应放到线程池,或替换为异步客户端。
并发控制
一个进程内的 dict[str, asyncio.Lock] 不能解决多进程和多实例并发,还可能无限增长。生产环境应选择一种明确策略:
- 使用 Agent Server 管理线程和运行。
- 将每个
thread_id的运行串行投递到队列。 - 使用数据库锁或分布式锁,并配置租约与故障恢复。
无论采用哪种方式,业务副作用仍必须幂等。
本教程推荐把“同一 thread_id 同时只能有一个活动运行”作为不变量。实现可以是带唯一约束的运行表:创建运行时插入 (thread_id, active=true),冲突时返回现有运行;运行终止后原子更新状态。数据库锁只保护调度,不替代下游退款幂等。
限流和背压
限流优先放在 API Gateway 或共享存储支持的限流组件中,而不是每个进程独立计数。至少设置:
- 租户级并发数。
- 模型提供商并发和 QPS。
- 单线程最大运行数。
- 队列长度和拒绝策略。
- 外部 API 超时与熔断。
所有限制都应来自集中配置并可观测。拒绝请求时返回稳定错误码、建议重试时间和已有 run_id,不要让客户端把限流错误误判为业务失败。
缓存
LangGraph 节点缓存需要同时配置 Cache 和 CachePolicy:
from langgraph.cache.memory import InMemoryCache
from langgraph.types import CachePolicy
builder.add_node(
"load_public_policy",
load_public_policy,
cache_policy=CachePolicy(ttl=300),
)
graph = builder.compile(cache=InMemoryCache(), checkpointer=checkpointer)
只缓存稳定、可重复计算且不会跨租户泄漏的数据。权限判断、支付状态和实时库存不应使用仅按输入文本构造的全局缓存键。
成本和超时预算
生产 State 可以累计以下字段:
model_callsinput_tokensoutput_tokensstarted_atdeadlineattempts
路由函数在进入下一轮前检查预算。超限后返回已完成内容、终止原因和人工接管入口,不要静默截断。
预算检查应落到图中,而不是只写在监控面板:
from datetime import datetime, timezone
def route_budget(state: SupportState) -> Literal["continue", "manual"]:
if state["model_calls"] >= 8:
return "manual"
if datetime.now(timezone.utc) >= datetime.fromisoformat(state["deadline_at"]):
return "manual"
return "continue"
持久化带时区的 UTC deadline,并在每个进程转换后检查;不要保存 monotonic() 值,因为它不能跨进程比较。同时让 API Gateway 和客户端拥有更外层超时。
数据库迁移、升级与回滚
Checkpointer setup() 适合首次实验,正式发布应由独立 Job 执行迁移。滚动升级前验证新旧版本能否同时读取正在运行的 State,并为 State 增加版本字段。若新版本不能兼容旧 Checkpoint,应先排空运行或提供迁移程序。
回滚不仅是换回旧镜像,还要回答:数据库结构是否向后兼容、旧代码能否读取新 State、已经创建的审批能否继续、队列中消息是否仍可消费。每次发布记录镜像摘要、迁移版本和回滚命令。
备份、恢复和灾难演练
PostgreSQL 至少需要自动备份、时间点恢复和定期恢复演练。恢复目标应同时覆盖业务审批/退款数据库与 Checkpointer;只恢复其中一个可能造成图状态与业务事实不一致。定义 RPO、RTO,并演练从备份恢复后如何核对未完成线程和未知退款结果。
容量与负载测试
容量测试不能只打 /health。测试脚本应模拟:普通问题、并行查询、等待审批、批准恢复、SSE 慢客户端和外部服务延迟。逐步增加并发,观察 API P95、数据库连接、队列深度、Checkpoint 写延迟和模型限流。
# 示例:先准备不触发真实资金副作用的测试环境
k6 run loadtests/support-flow.js
仓库没有伪造一份固定吞吐报告。团队应把负载脚本、环境规格、数据量、结果和瓶颈分析作为发布证据提交。
安全基线
- API 使用 OIDC/JWT 或服务身份认证,从声明推导租户。
- 工具凭证按 Worker 最小授权并定期轮换。
- 出站网络使用 allowlist,防止模型诱导访问任意地址。
- 输入、工具输出和模型响应设置大小上限与内容策略。
- 审批、退款和权限拒绝写入不可抵赖审计日志。
- 镜像以非 root 用户运行,文件系统和 Linux capability 按需收紧。
- 依赖、镜像和 IaC 进入漏洞扫描与发布门禁。
上线检查清单

- [ ] 依赖锁定并通过
pip check。 - [ ] 正常、拒绝、超时、重试耗尽和恢复路径均有测试。
- [ ] 生产使用持久 Checkpointer,不使用内存或单文件 SQLite。
- [ ]
thread_id包含租户边界且经过鉴权。 - [ ] 每个副作用都有稳定幂等键。
- [ ] 中断前没有非幂等副作用。
- [ ] Web 路由和外部客户端均采用正确异步方式。
- [ ] Trace、日志和 Checkpoint 完成脱敏与保留期配置。
- [ ] 速率限制、队列背压和外部 API 超时已经配置。
- [ ] 数据库迁移、滚动升级和回滚经过演练。
- [ ] 密钥只来自 Secret Manager 或运行环境。
- [ ] 已完成容量测试和故障注入,而不是仅凭本地成功判断可上线。
本章验收
- 能在 PostgreSQL 模式启动服务,并通过 live/readiness 检查。
- 镜像以非 root 用户运行,真实密钥不进入镜像和仓库。
- 同一线程的并发策略、限流、背压和预算都有实际实现位置。
- 完成一次带数据库迁移的滚动升级与回滚演练。
- 完成数据库恢复、应用重启、慢 SSE 和外部依赖故障演练。
- 容量报告包含环境、流量模型、P95/P99、瓶颈和安全阈值。
最终验证
执行全部本地验证:
python -m pip check
python -m pytest
python -m compileall -q src examples tests
可选容器验证:
docker compose config
docker compose up --build
如果测试全部通过,你得到的是一套可复现的生产形态基线。正式上线前仍需把示例服务替换成真实模型、知识库、订单 API、企业鉴权和调度系统,并重新执行容量、安全和恢复测试。
总结
生产级 Agent 的核心不是“让模型自己决定一切”,而是让状态、路由、副作用、恢复点和故障边界都可检查。
本手册的关键结论是:
- State 是持久化数据契约,不是任意对象容器。
- 并行路径必须明确合并规则,循环必须明确退出预算。
- 动态
interrupt()、Checkpointer 和thread_id共同构成审批恢复。 - 技术重试、业务重试和幂等是三个不同问题。
- 多 Agent 是职责与权限边界,不是角色数量竞赛。
- 能运行只是起点;测试、Trace、鉴权、背压和故障恢复决定能否上线。