Agent 工程 · 第 12 章|生产工程:并发模型、队列化、模型路由、成本治理

第 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)
              ↘ 备用模型(主力故障/限流时)

路由依据(按工程成熟度排序):

  1. 请求特征:token 长度、任务类型(第 2 章 Router 模式的静态版);
  2. 实时健康:滑动窗口错误率,超阈值熔断:
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"             # 熔断:后续请求直接走备用模型
  1. 语义缓存:相同/语义相近的请求直接返回缓存结果——命中率高度依赖场景(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 天(点踩率/成本/通过率,显著性检验)→ 全量 或 回滚
→ 变更历史入审计(谁/何时/改了什么/依据什么数据全量)

工程要点:配置要有版本号(回滚 = 切回版本,不是手工改回去);灰度键与实验分桶一致(同一用户不被两边打架);"全量"不是删除实验配置而是状态流转(可审计、可再回滚)。

实现作业

  1. 把第 2-11 章的 Agent 从"进程内执行"改成 Redis Stream 队列版:API 返回 task_id,独立 worker 进程消费,前端轮询/订阅进度——人为 kill worker 验证 XAUTOCLAIM 重投(任务做成幂等);
  2. 实现熔断器 + 双模型:主模型端点故意配错,验证 open → 走备用 → 恢复探测回切的全过程,trace 里能读到降级路径;
  3. 实现成本账本 + 配额:hook 记账(单价表)、按用户日预算、reject 与 degrade 两种动作各验证一遍;故意把账本表删掉,确认业务不被阻断且告警触发;
  4. 压测: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 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?