《AI Agent 工程实战》18.1 容量与架构评审清单

收官章先把全卷散落的工程决策收成一份可执行清单:接口与契约、数据与一致性、并发与资源、可观测与告警、安全与合规五组,每条给出可判定的通过标准。再给一套容量估算方法,用一个真跑的脚本代入具体参数,算出每日 token、模型成本、DB 增速、Redis 内存与 worker 数。

18.1 容量与架构评审清单

前 17 章,我们一节一节地把 ResearchCopilot 从骨架搭成服务:配置、鉴权、数据层、缓存、队列、并发、记忆、评测、可观测、安全、容器化、发布。可当有人问「这套系统能上线了吗」,你没法指着一堆章节说「都写了」。上线需要一个可判定的答案:哪些项必须过、过的标准是什么、撑得住多少量。

本节把 ResearchCopilot 推进到「可评审」:把全卷的工程决策收成五组可执行清单,每条给出通过标准;再用一个真跑的脚本,从日活一路推出每日 token、成本、DB 增速、Redis 内存与 worker 数。

18.1.1 清单的用法:把「感觉没问题」变成「逐条打勾」

评审清单最怕写成空话——「要有缓存」「要能观测」谁都会说,但没人能判定过没过。所以本节每条都配一个通过标准:一个可观测、可执行、可判定的条件。判据的写法有三条铁律:

  • 能指向具体产物:不是「要有缓存」,而是「检索缓存命中率有监控,key 含 workspace_id 与语料版本,TTL 有依据」。
  • 能指向具体动作:不是「要能回滚」,而是「回滚脚本演练过,从触发到恢复 ≤ 5 分钟」。
  • 能指向具体数字:不是「要限流」,而是「按租户 token 配额,超限返回 429 且带 retry_after」。

一句话:每一条都必须能在评审会上被追问「怎么证明」,并且答得出来。下面五组共三十条,逐条打勾,全绿才算可以进入 18.3 的上线流程。

清单还有个常被忽略的用法:它不是一次性检查,而是回归资产。把它存进仓库、跟代码一起版本化,每次大改动(换模型、加缓存、改队列)都重跑一遍——上一版全绿、这一版冒红,红的那条就是你改动引入的风险。这和 12.3 的质量门是同一个思路:把「对不对」变成「有没有变差」。

18.1.2 清单分三级:阻断、警告、建议

不是每条都一样重。上线评审最忌讳「一票全否」——把「没接 OpenTelemetry」和「密钥写死在代码里」当成同等严重,评审就会沦为扯皮。所以每条检查项还要带一个级别,决定它在评审会上是什么声音:

级别含义处理
阻断(P0)不满足不许上线必须当场给出修复计划与时间
警告(P1)可以上,但限期内补记入待办,下次评审复查
建议(P2)锦上添花排进常规迭代

判定 P0 的标准很朴素:出事了会不会伤到钱、伤到数据、伤到用户信任。密钥落盘、租户隔离失效、没有超时保护、迁移不可回滚——这些是 P0;看板不够漂亮、日志字段不全——这些是 P1 或 P2。下面五组表里的检查项,默认按「能伤到什么」自行定级,评审时逐条标注。

18.1.3 第一组:接口与契约

检查项通过标准
接口 schema 版本化对外接口与事件都有 schema,新增字段可选、删除走 deprecation 周期,CI 有兼容性检查
错误契约统一所有非 2xx 返回 code / message / details / request_id 四字段,不泄露栈或内部路径
幂等键写接口接受 Idempotency-Key,重复提交返回同一结果,不重复扣费
流式可续传SSE 事件带 id,支持 Last-Event-ID 断线续传,不丢不重
内部边界清晰MCP 工具、记忆服务以契约描述,跨进程不共享内存对象
时间与分页时间统一 UTC 带时区,列表接口用游标分页,不用 offset

契约是最先冻住的东西。第 3 章的 REST 与 SSE、第 9 章的事件协议、第 10 章的 MCP 工具,都是契约。契约一变,前端、SDK、评测夹具全要跟着动——所以评审时对契约最严,宁可现在加字段,也别上线后改形状。

18.1.4 第二组:数据与一致性

检查项通过标准
迁移不中断expand-contract 两步走,回滚脚本可用,且演练过(17.3)
事务边界明确一次 run 的落库在单事务或明确的补偿流程内,无跨服务长事务
租户隔离所有查询带 workspace_id,应用层与行级策略双重保证(4.2)
缓存一致性缓存 key 含 workspace_id 与语料版本,TTL 有依据,失效路径明确
幂等写消息/运行写入用唯一约束防重,重放不产生重复行
备份可恢复有备份策略,且做过恢复演练,RPO/RTO 已知

这一组的关键词是「双重保证」和「演练过」。租户隔离不能只靠应用层 SQL 拼接,还要有数据库行级策略兜底——一层写错,另一层拦住。迁移与备份同理:没演练过的迁移脚本和没演练过的备份,等于没有。

18.1.5 第三组:并发与资源

检查项通过标准
下游独立限并发模型/DB/检索各有独立 Semaphore,上限按 QPS × 耗时 估且留余量(8.2)
连接池匹配pool.max_size × 副本数 < 数据库 max_connections,留安全余量
超时与取消每次模型/工具调用有超时,超时触发取消并走降级链(8.3)
背压生产-消费之间是有界队列,积压转阻塞而非 OOM
无资源泄漏超时后连接/任务/文件句柄被回收,压测后内存与连接数不增长
优雅退出SIGTERM 后停止取新任务,等在途完成再退出(16.3)

并发这一组是最容易出事、也最难事后补的。第 8 章讲透了原理,评审时只需验证「有没有落地」:每一类下游是不是各有一个闸门、超时是不是配了、队列是不是有界。这三问答不上来,压测必崩。

18.1.6 第四组:可观测与告警

检查项通过标准
每次 run 可追踪有 run_id,span 树覆盖模型调用与工具调用(13.1)
关键指标有看板延迟分位、成功率、队列深度、成本、缓存命中率同屏可见(13.2)
告警可行动每条告警有阈值、负责人、runbook,不留「无主告警」(13.3)
成本可归因token/成本按 run/租户/工具三维可查(14.2)
日志结构化JSON 日志带 trace_id,可按租户与 run 检索
探针分离liveness 与 readiness 分开,readiness 依赖 DB/Redis 真实连通

可观测的价值不在「有数据」,而在「能回答一个具体问题」:这次 run 为什么慢、这个租户为什么超预算、这条告警该谁处理。三条 run_id 串起来的链路(追踪 → 日志 → 成本)就是排查的骨架,评审时验证它是否闭环。

18.1.7 第五组:安全与合规

检查项通过标准
输入护栏提示注入与越权工具调用被拦截或降权(15.1)
输出护栏敏感信息与幻觉引用被检出并标注(15.2)
密钥不落盘密钥只从环境注入,日志与错误信息脱敏
高危工具审批写操作类工具需策略或人工审批(4.3)
审计留痕谁在何时调用了什么工具、参数是什么,可审计(15.3)
数据可删除按用户删除端到端可达,含缓存与向量索引(11.3)
配额与限流按租户 token 配额,超限明确拒绝且不静默失败(6.3)

安全这一组的底线是**「密钥永不出现」**。本卷从头到尾正文里没有一处密钥字面量,代码里只有 os.environ["ANTHROPIC_API_KEY"] 这一个引用——评审时这条要当硬指标查:翻代码、翻配置、翻日志,确认没有密钥泄漏。

18.1.8 容量估算:从日活推出基础设施

清单回答「做没做对」,容量估算回答「撑不撑得住」。估算的链条很短:日活 → run 数 → 模型调用数 → token 与成本 → 存储增速 → 缓存内存 → worker 数。每一环都是上一环乘一个系数,唯一需要你填的是假设:

输入假设取值依据
日活用户 DAU2000产品目标
每用户每天 run 数10运营观察
每 run 模型调用次数5多跳检索 + 工具 + 收尾
每次调用 input/output token2500 / 300含历史累积
单价(input/output)$3 / $15 每百万示例价,以实际为准
单 run 落库30 KB消息 + 步骤 + 索引
数据保留天数90合规要求

把假设填进脚本,就能推出一张基础设施账。链条的每一环都是一个乘法,值得单独看清:

  • 每日 run 数 = 日活 × 每用户 run 数 = 2000 × 10 = 20000。
  • 每日模型调用 = run 数 × 每 run 调用数 = 20000 × 5 = 100000。
  • 每日 token = 调用数 × 单次 token;成本 = token / 百万 × 单价。
  • DB 日增 = run 数 × 单 run 落库大小;累计 = 日增 × 保留天数。
  • Redis 内存 = 每小时新增缓存条数 × TTL 小时 × 单条大小(稳态在途量)。
  • 峰时并发 = 峰时 QPS × 单 run 耗时(利特尔法则,8.2 已推过)。

下面这段是真跑的估算脚本。

18.1.9 真跑:一份容量估算表

"""18.1 容量估算:从日活一路推出每日 token、存储增速、Redis 内存与 worker 数。"""
import math

DAU, RUNS_PER_USER, CALLS_PER_RUN = 2000, 10, 5      # 日活 / 每用户 run 数 / 每 run 调用数
IN_TOK, OUT_TOK = 2500, 300                          # 每次调用 token
PRICE_IN, PRICE_OUT = 3.0, 15.0                      # 美元 / 百万 token
KB_PER_RUN, RETENTION_DAYS = 30, 90                  # 单 run 落库 KB / 保留天数
CACHE_KB, CACHE_TTL_H = 4, 6                         # 缓存单条 KB / TTL 小时
QUEUE_KB = 0.5                                       # 队列单条 KB
PEAK_HOURS, PEAK_FACTOR, RUN_SEC, PER_WORKER = 10, 3.0, 12, 8

runs_day = DAU * RUNS_PER_USER
calls_day = runs_day * CALLS_PER_RUN
in_tok, out_tok = calls_day * IN_TOK, calls_day * OUT_TOK
cost_day = in_tok / 1e6 * PRICE_IN + out_tok / 1e6 * PRICE_OUT
db_mb_day = runs_day * KB_PER_RUN / 1024
db_gb = db_mb_day * RETENTION_DAYS / 1024
redis_mb = (runs_day / 24 * CACHE_TTL_H * CACHE_KB
            + runs_day / PEAK_HOURS * 0.1 * QUEUE_KB) / 1024
peak_qps = runs_day / PEAK_HOURS / 3600 * PEAK_FACTOR
concurrent = peak_qps * RUN_SEC
workers = math.ceil(concurrent / PER_WORKER) + 1     # +1 冗余

rows = [
    ("每日 run 数", f"{runs_day:,}"),
    ("每日模型调用", f"{calls_day:,}"),
    ("每日 input token", f"{in_tok/1e6:,.1f} M"),
    ("每日 output token", f"{out_tok/1e6:,.1f} M"),
    ("每日模型成本", f"${cost_day:,.2f}"),
    ("每月模型成本", f"${cost_day*30:,.0f}"),
    ("DB 日增", f"{db_mb_day:,.0f} MB"),
    (f"DB {RETENTION_DAYS} 天累计", f"{db_gb:,.1f} GB"),
    ("Redis 内存合计", f"{redis_mb:,.1f} MB"),
    ("峰时 QPS", f"{peak_qps:,.2f} run/s"),
    ("峰时并发 run", f"{concurrent:,.1f}"),
    ("worker 数(含 1 冗余)", f"{workers}"),
]
print(f"{'指标':<22}{'值':>16}")
print("-" * 38)
for k, v in rows:
    print(f"{k:<22}{v:>16}")

用 venv 里的 Python 3.14.6 跑出来(参数是假设的,计算过程是真实的):

指标                                   值
--------------------------------------
每日 run 数                        20,000
每日模型调用                         100,000
每日 input token                 250.0 M
每日 output token                 30.0 M
每日模型成本                       $1,200.00
每月模型成本                         $36,000
DB 日增                           586 MB
DB 90 天累计                      51.5 GB
Redis 内存合计                     19.6 MB
峰时 QPS                      1.67 run/s
峰时并发 run                          20.0
worker 数(含 1 冗余)                     4

这张表一眼读出四个决策点:

  • 成本是第一约束:每月 3.6 万美元几乎全在 token 上(output token 单价是 input 的 5 倍,30 M output 就占了 45% 的钱)。要降本,第 14.2 章的缓存与路由才是主战场,而不是省存储。
  • 存储很便宜:90 天才 51.5 GB,一张 Postgres 卷绰绰有余。所以别为了省存储去删运行历史——它正是评测回放(12.2)与故障复盘的原料。
  • Redis 内存小得意外:稳态常驻不到 20 MB。这说明「加缓存」的成本瓶颈从来不是内存,而是命中率——缓存不命中,省下的内存换不来任何延迟收益。
  • worker 只需 4 台(含冗余):峰时并发 20 个 run,每台扛 8 个并发,3 台就够,多留 1 台冗余。真实上线时按这个数的 2 倍开,把拐点(14.3)留出余量。

18.1.10 从估算到动作:把数字变成预案

估算表本身不会扩容,能扩容的是写进预案的触发条件。每个数字都要配一条「到多少就做什么」:

指标阈值动作
峰时并发 run> 12(估算 20 的六成)worker 从 4 扩到 6,观察 30 分钟
队列深度> 500(持续 2 分钟)触发 18.2 的积压告警,人工判断是否扩 worker
DB 磁盘> 70%归档超过保留期的运行数据,或扩卷
月成本> 预算的 80%收紧配额、提高缓存命中率、下调模型档位(14.2)
Redis 内存> 60%缩短缓存 TTL 或清理冷 key

这张表是容量估算与 13.3 告警的接缝:估算给出「正常应该是多少」,告警负责「偏离了多少」。没有估算,你不知道该把阈值定在哪;没有告警,估算就只是一张纸。两者合起来,才叫「容量有数」。

18.1.11 验收清单与真实环境

本节的验收:

  • 五组清单逐条打勾,每条能答出「怎么证明」
  • 容量估算的假设写进文档,可随运营数据更新
  • 估算出的成本、存储、worker 数有对应的监控与扩容预案
  • 每个数字都能追到公式,而不是拍脑袋
  • 清单已版本化入库,大改动后重跑一遍

真实环境里会怎样:估算表的每一个系数都会漂移。日活可能翻倍,单 run 调用次数会因为模型「更爱多跳」而涨到 8 次,input token 会因为记忆注入而膨胀。所以这份表的价值不在那几个数字,而在公式本身——运营数据一变,改几个系数就能重算。另外,真实的峰谷比远不止 3 倍,突发流量(一次营销、一条爆款)能在几分钟内把并发推到均值的十倍,这正是 18.2 要演练的「队列积压」场景。

还有一处估算会低估:token 单价会变。模型降价、缓存命中(prompt caching)打折、路由到小模型,都会让实际成本低于按标准价算出的数。所以容量表要写「按当前价目」并定期复核,而不是当成永久承诺。宁可先按贵价估、留出预算余量,也别乐观低估——超支比浪费难处理得多。

清单与容量都过了,下一节开始动手:主动把系统弄坏,看它会不会优雅地坏。先看 18.2 故障演练:模型超时/DB 宕机/队列积压 。

阅读导航:上一节:17.3 版本与迁移:不中断 schema 迁移演练 · 下一节:18.2 故障演练:模型超时/DB 宕机/队列积压 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「agent」更多文章

  1. 《AI Agent 高级编程》目录
  2. 《AI Agent 高级编程》11.3 全景回顾:从零到框架的完整路径与展望
  3. 《AI Agent 高级编程》11.2 真实 API 冒烟:假模型 vs 真模型量化差距