第 12 章 · 生产工程
前十二章把 Agent 建出来了,这一章把它规模化:并发形态、队列化、模型路由、成本治理。这些主题的共同点:技术不难,难在"从 10 个用户到 1 万个用户"时所有默认假设的失效。
12.1 Agent 服务的并发形态
Agent 服务与普通 CRUD API 的本质差异,三个"最":
- 最长连接:WebSocket(聊天流)与 SSE(执行事件)连接以小时计——连接数本身就是稀缺资源(文件描述符、内存),网关/代理的超时与缓冲配置(SSE 关闭 proxy buffering)都是容量参数;
- 最长任务:一次执行跑几分钟到几小时,横跨多次部署。部署即杀死在途任务——滚动发布的 Pod 驱逐会让长任务尸骨无存,除非有恢复机制(第 8 章的持久化);
- 最有状态:会话、执行中状态、流缓冲。多副本部署立刻撞上"用户的下一个请求到了另一个副本"——状态要么外置(Redis/PG),要么粘性路由,要么把"执行"本身外置成独立 worker(12.2)。
容量预估的思考框架(面试高频):
并发会话数 N,每会话平均 8 轮,每轮 1 次 LLM 调用(2s)+ 2 次工具(0.3s)
→ 每会话生命周期 ≈ 8 × 2.6s ≈ 21s,稳态并发 = QPS × 21s(Little's Law)
瓶颈排查顺序:
1. LLM 供应商的 TPM/RPM 限额(几乎总是第一个天花板,且是外部约束)
2. 事件广播 fan-out(一条消息广播给所有连接,O(连接数))
3. 数据库连接池(长事务 + 高并发 = 池耗尽)
4. CPU(加密/JSON 序列化)反而通常最后
12.2 队列化:把执行从 API 进程里拿出来
默认形态里 Agent 执行跑在 API 进程的协程里——进程即宇宙。队列化改造:
API 进程:接收请求 → 校验 → 任务写入队列 → 返回 task_id
Worker 池:从队列领取 → 执行 Agent 循环 → 状态/事件写回
前端:用 task_id 订阅事件流(不再关心哪个 worker 在跑)
收益:worker 池独立扩缩容(LLM 调用是 IO 密集,worker 可以远多于 CPU 核);部署 API 不杀任务;故障域隔离(一个 worker OOM 只死一个任务)。
Redis Stream 消费者组是这个形态的标准实现(比 List 队列多出的正是可靠性):
# 生产者
redis.xadd("task_stream", {"payload": json.dumps(task)})
# 消费者组(多 worker 共用组名,同一条消息只投给组内一个 worker)
redis.xgroup_create("task_stream", "workers", id="0", mkstream=True)
# 消费循环
while True:
msgs = redis.xreadgroup("workers", consumer_name,
{"task_stream": ">"}, count=1, block=5000)
for stream, entries in msgs:
for msg_id, fields in entries:
try:
execute(fields["payload"])
redis.xack("task_stream", "workers", msg_id) # 处理完才 ACK
except Exception:
pass # 不 ACK → 消息留在 PEL(pending 列表)
# 崩溃恢复:别的 worker 认领超时未 ACK 的消息(幂等前提下安全重投)
stale = redis.xautoclaim("task_stream", "workers", other_consumer,
min_idle_time=600_000, start_id="0")
可靠性链条:XADD → XREADGROUP → 处理 → XACK,未 ACK 的消息在 PEL 里可被 XAUTOCLAIM 认领重投。前提是任务幂等(第 8 章 8.3 的结论在这里复用)——所以幂等性不是洁癖,是队列化改造的准入条件。
12.3 模型路由与降级
单一模型是单点:贵、慢、会挂。路由把"选哪个模型"变成策略问题:
请求 → [路由器] → 主力模型(质量优先)
↘ 轻量模型(简单请求:分类/改写,成本 1/10)
↘ 备用模型(主力故障/限流时)
路由依据(按工程成熟度排序):
- 请求特征:token 长度、任务类型(第 2 章 Router 模式的静态版);
- 实时健康:滑动窗口错误率,超阈值熔断:
class CircuitBreaker:
def __init__(self, window=60, threshold=0.5, min_calls=10):
self.events = [] # (ts, ok)
self.state = "closed" # closed → open → half_open
def allow(self) -> bool:
if self.state == "open":
return False # half_open 探测由定时放行少量请求实现
return True
def record(self, ok: bool):
self.events.append((time.time(), ok)); self._evict()
recent = [e for e in self.events if self._recent(e)]
if len(recent) >= self.min_calls and self._err_rate(recent) > self.threshold:
self.state = "open" # 熔断:后续请求直接走备用模型
- 语义缓存:相同/语义相近的请求直接返回缓存结果——命中率高度依赖场景(FAQ 类高、开放对话低),且要警惕"缓存了个性化答案"的逻辑事故。
降级的层次(与第 9 章配额联动):主模型超时 → 同供应商换模型 → 跨供应商备用模型 → 静态兜底话术。每一层都要在 trace 里标注实际生效的模型(gen_ai.response.model ≠ request.model 时就是一次降级),成本与质量账才能分开算。
12.4 成本治理五层
成本是 Agent 生产化的第一约束(LLM 账单是运营支出的绝对大头),五层架构:
| 层 | 做什么 | 关键设计 |
|---|---|---|
| 1 埋点 | 每次调用记 token/费用/延迟 | 在 LLM 调用层 hook(业务零侵入),usage 缺失时估算回退;归因用 contextvars + trace 双通道 |
| 2 账本 | 按用户/Agent/会话聚合 | 时序表 + 每日汇总;单价是配置(model_prices),缺价只记 token 不估费(宁可少算不错算) |
| 3 配额 | 用户/Agent 级日/月预算 | 超限动作分级:reject(入口 429,硬限)/ degrade(切备用模型,软限);检查失败 fail-open(账本故障不该阻断业务) |
| 4 优化 | 主动降本 | 前缀缓存(第 1 章 1.4,聊天流量普遍 50-90% 节省)、上下文瘦身(第 3 章)、小模型路由(12.3)、工具结果分页(第 5 章) |
| 5 看板 | 让花钱可见 | 按维度下钻(哪类 Agent/哪天/哪个模型),P95 单任务成本(均值被长尾污染) |
成本治理的账本故障语义值得单独记:埋点与配额检查失败时 fail-open(放行并告警),因为它们是旁路系统——但要看板数据的"不可信窗口"要在告警里体现。相反,计费对账(面向客户收费)场景要 fail-closed。旁路与主链路的失败语义相反,混淆即事故。
12.5 灰度发布
Agent 的"发布"不只是代码——提示词、工具描述、模型、技能全部是配置,全部需要灰度。配置级灰度的基础设施就是第 9 章的 A/B 实验:分桶逻辑复用、生效百分比可调、指标自动产出、一键回滚。
一套完整的变更流程:
改提示词 → 候选门禁评测(离线,9.5)→ 通过 → A/B 灰度 10%(在线,9.6)
→ 观察 3-7 天(点踩率/成本/通过率,显著性检验)→ 全量 或 回滚
→ 变更历史入审计(谁/何时/改了什么/依据什么数据全量)
工程要点:配置要有版本号(回滚 = 切回版本,不是手工改回去);灰度键与实验分桶一致(同一用户不被两边打架);"全量"不是删除实验配置而是状态流转(可审计、可再回滚)。
实现作业
- 把第 2-11 章的 Agent 从"进程内执行"改成 Redis Stream 队列版:API 返回 task_id,独立 worker 进程消费,前端轮询/订阅进度——人为 kill worker 验证 XAUTOCLAIM 重投(任务做成幂等);
- 实现熔断器 + 双模型:主模型端点故意配错,验证 open → 走备用 → 恢复探测回切的全过程,trace 里能读到降级路径;
- 实现成本账本 + 配额:hook 记账(单价表)、按用户日预算、reject 与 degrade 两种动作各验证一遍;故意把账本表删掉,确认业务不被阻断且告警触发;
- 压测:Locust 三场景(REST / WebSocket 流 / SSE 事件流),产出 P50/P95/P99 + 错误率报告,找到你系统的第一个天花板(大概率是 LLM 供应商限额——这本身就是重要发现)。
深入材料
- Redis Streams 与消费者组官方指南(redis.io,XAUTOCLAIM 语义)
- Google SRE Book / Workbook(容量规划与降级思想)
- NVIDIA Dynamo / llm-d 文档(cache-aware routing 的生产形态)
- OpenAI / Anthropic 的速率限制与批处理文档(TPM/RPM 是容量规划的外部约束)
读者留言
COMMENTS 暂无还没有留言,来说第一句?