企业级 Agent 的七层底座
很多 Agent 项目的第一版都差不多:一个模型、一段系统提示词、几个工具,再套一层循环。模型决定下一步做什么,程序执行工具,把结果放回上下文,直到模型说任务完成。
这段代码也许不到两百行,演示时甚至相当惊艳。真正接入业务以后,问题才会一起出现:
- 进程重启后,已经执行到一半的任务从哪里继续?
- 超时重试会不会重复发邮件、重复退款、重复创建工单?
- 同一用户连续提交两次任务,两个 Agent 会不会同时修改同一份数据?
- 模型说“完成了”,系统凭什么相信它真的完成了?
- 工具返回 HTTP 200,但正文是业务错误,应该记成功还是失败?
- 一条任务跑了四十分钟,谁能说清时间花在模型、工具、队列还是数据库?
这些都不是换一个更强模型就会消失的问题。模型负责在不确定信息里做判断,基础设施负责让判断受到约束、让动作可以恢复、让结果能够核验。
本文不绑定某个 Agent 框架。我们从三个常见案例出发,搭一套由七层组成的参考架构,再把其中最容易踩坑的状态、幂等、容量和评测问题展开。
先看三个难度完全不同的任务
“Agent 能调用工具”这句话掩盖了很大的风险差异。下面三个任务都需要模型和工具,但它们对基础设施的要求并不一样。
案例一:研究报告 Agent
用户给出一个主题,Agent 搜索资料、读取网页和文档、整理证据,最后生成带引用的报告。它大部分时间在做只读操作,执行几十分钟也可以接受。
这个场景最怕的不是重复执行,而是证据丢失、引用错位和上下文越跑越脏。底座应优先保证:
- 原始资料与摘要分开保存;
- 每个结论能追溯到证据;
- 中途失败可以从最近的研究阶段恢复;
- 搜索、抓取、解析和写作分别计时;
- 最终结果通过引用覆盖率和事实核验,而不是只看文笔。
案例二:售后处理 Agent
Agent 读取订单和客服政策,判断是否符合退款条件,必要时创建退款单、发通知并更新工单。
它的步骤不一定多,但会修改外部状态。一次错误重试可能造成重复退款;一次并发冲突可能让两个客服同时处理同一订单;一段被文档污染的提示词可能诱导 Agent 调用本不该调用的高风险工具。
这里最重要的是:
- 读工具与写工具分级;
- 写操作使用幂等键;
- “判断可退款”和“真正退款”分成两个步骤;
- 金额、权限、审批和预算由确定性代码校验;
- 任务状态与账务状态不能只存在模型上下文里。
案例三:运维处置 Agent
告警触发后,Agent 查询指标、日志和变更记录,提出诊断;低风险动作可以自动执行,高风险动作需要审批。
这类任务会遇到故障中的系统:指标平台可能慢,日志工具可能部分不可用,目标服务可能正在抖动。Agent 本身不能成为第二个故障源。
它需要:
- 每个依赖独立的超时、限流和熔断;
- 查询失败与“查询结果为空”严格区分;
- 处置动作有前置条件、影响范围和回滚步骤;
- 人工审批期间任务可挂起数小时,重启后仍能继续;
- 所有命令、参数、输出摘要和审批人都进入审计记录。
这三个案例说明,Agent 基础设施不是一套固定中间件清单。真正稳定的部分,是任务契约、状态机、工具协议、持久执行、权限控制和评测闭环。

第一层:先把自然语言变成任务契约
用户说“帮我处理一下这笔退款”,模型能理解大意,但系统不能把这句话直接当作可执行事务。入口层至少要把请求整理成一份结构化任务:
{
"task_id": "task_01J...",
"tenant_id": "tenant_42",
"actor_id": "user_17",
"task_type": "refund_review",
"goal": "判断订单是否符合退款条件,符合时创建退款申请",
"inputs": {
"order_id": "order_20260727_0081"
},
"success_criteria": [
"已读取当前订单状态",
"已引用适用的退款规则版本",
"退款申请存在且金额正确,或给出不可退款原因"
],
"constraints": {
"max_amount": 1000,
"require_approval_above": 200,
"deadline_seconds": 120
}
}
这里最容易被忽略的是 success_criteria。很多系统只有用户输入和最终答案,没有机器可核验的完成条件。模型说“退款已经提交”,运行时就把任务标为完成;但退款接口也许超时了,或者只是生成了参数,根本没有执行。
任务契约应回答四件事:
| 问题 | 需要固化的内容 |
|---|---|
| 谁在请求 | 租户、用户、角色、授权上下文 |
| 要做什么 | 任务类型、目标、结构化输入 |
| 什么算完成 | 可验证的结果、证据和外部状态 |
| 不能做什么 | 时间、费用、权限、影响范围和审批阈值 |
自然语言仍然可以保留,它适合表达意图;真正驱动执行的应是经过校验的任务对象。这个边界越早建立,后面的重试、审计和评测越容易。
第二层:运行时必须是一台状态机
最简 Agent 循环通常是:
while not done:
response = model(context)
observation = call_tool(response.tool_call)
context.append(observation)
它的问题不是代码短,而是重要状态都藏在 context 里:现在执行到哪一步、哪个动作已经发生、为什么暂停、能不能重试,只有模型和当前进程知道。
一个可恢复的运行时至少需要显式状态:
from enum import StrEnum
class TaskStatus(StrEnum):
QUEUED = "queued"
PLANNING = "planning"
RUNNING = "running"
WAITING_APPROVAL = "waiting_approval"
WAITING_EXTERNAL = "waiting_external"
VERIFYING = "verifying"
SUCCEEDED = "succeeded"
FAILED = "failed"
CANCELLED = "cancelled"
ALLOWED_TRANSITIONS = {
TaskStatus.QUEUED: {TaskStatus.PLANNING, TaskStatus.CANCELLED},
TaskStatus.PLANNING: {TaskStatus.RUNNING, TaskStatus.FAILED},
TaskStatus.RUNNING: {
TaskStatus.WAITING_APPROVAL,
TaskStatus.WAITING_EXTERNAL,
TaskStatus.VERIFYING,
TaskStatus.FAILED,
TaskStatus.CANCELLED,
},
TaskStatus.WAITING_APPROVAL: {
TaskStatus.RUNNING,
TaskStatus.CANCELLED,
TaskStatus.FAILED,
},
TaskStatus.WAITING_EXTERNAL: {TaskStatus.RUNNING, TaskStatus.FAILED},
TaskStatus.VERIFYING: {TaskStatus.SUCCEEDED, TaskStatus.RUNNING, TaskStatus.FAILED},
}
状态转换应由运行时代码执行,而不是让模型直接写数据库。模型可以提出“下一步需要审批”,运行时再检查当前状态是否允许进入 waiting_approval,并生成审批记录。
Step 比一长串消息更重要
一条任务还应拆成可持久化的 Step:
task
├── step 01: load_order
├── step 02: load_refund_policy
├── step 03: decide_eligibility
├── step 04: request_approval
├── step 05: create_refund
└── step 06: verify_refund
每个 Step 保存 input_ref、output_ref、attempt、started_at、finished_at、error_type 和 idempotency_key。模型上下文可以丢,可以重新组装;已执行的 Step 和外部副作用不能丢。
完成不是一个字符串
任务从 verifying 进入 succeeded 前,应由验证器检查成功条件。退款任务可以查询退款单状态;研究任务可以检查引用是否都指向实际证据;代码任务可以运行测试。
验证器尽量是确定性的。实在无法写规则时,再引入模型评审,并保留人工抽检。不要让“负责执行的同一个模型”同时做唯一验收者。
第三层:工具要像正式 API,而不是一段函数说明
工具层决定 Agent 能碰到多少真实世界。工具描述写得再漂亮,如果返回契约含糊,运行时仍然无法判断发生了什么。
一个工具结果至少要区分四层状态:
from typing import Any, Literal
from pydantic import BaseModel, Field
class ToolResult(BaseModel):
transport_status: Literal["ok", "timeout", "network_error"]
provider_status: Literal["success", "business_error", "unknown"]
data_status: Literal["usable", "empty", "malformed", "not_applicable"]
side_effect_status: Literal[
"none", "not_started", "committed", "unknown", "rolled_back"
]
retryable: bool
error_code: str | None = None
data: Any | None = None
evidence_refs: list[str] = Field(default_factory=list)
HTTP 200 只能说明传输层拿到了响应,不能说明业务成功;业务成功也不等于结果可用;调用超时后,副作用状态可能是 unknown,此时更不能直接重试。
写工具必须带三样东西
凡是会修改外部状态的工具,至少需要:
- 幂等键:同一业务动作重复提交时返回同一个结果;
- 前置条件:例如订单仍未退款、资源版本仍是
v7; - 执行后证据:例如退款单 ID、提交版本、消息回执。
以创建退款为例,幂等键不应随机生成,而应来自稳定业务语义:
idempotency_key = sha256(
f"{tenant_id}:{order_id}:refund:{policy_version}:{amount}".encode()
).hexdigest()
如果第一次请求在响应前超时,第二次带同一个键查询或重放,服务端就能返回第一次的执行结果。随机 UUID 只能区分请求,不能识别“这是同一个业务动作”。
工具数量不是越多越好
工具过多时,模型不仅要在名称之间做选择,还要理解相似参数和重叠边界。OpenAI 的 Agent 实践指南也特别指出,问题不只在工具数量,还在工具是否相似、是否重叠;十个边界含糊的工具,可能比十五个职责清晰的工具更难用。
解决办法不是立即拆成多 Agent,而是先做三件事:
- 合并只有参数不同的重复工具;
- 把适用条件、禁用条件和反例写进描述;
- 根据任务阶段只暴露当前需要的工具集合。
工具发现、参数生成、执行和结果解释也应分别记录。否则“选错工具”和“工具执行失败”最终都会变成一条模糊的 Agent 失败。
第四层:状态、记忆和证据不要混在一个向量库里
长期记忆很容易被设计成“把所有对话都做 Embedding”。这样上线快,几个月后会遇到重复、过期、互相矛盾和权限泄漏。
至少应分成四类数据:
| 类型 | 保存什么 | 主要存储 | 生命周期 |
|---|---|---|---|
| 运行状态 | 当前步骤、租约、重试次数、审批状态 | 事务数据库 | 任务结束后归档 |
| 工作记忆 | 当前计划、最近 observation、临时摘要 | KV/数据库 + 上下文 | 分钟到小时 |
| 事实记忆 | 用户偏好、稳定业务事实、已验证知识 | 结构化库/向量库 | 有版本和失效规则 |
| 证据与审计 | 原始工具结果、决策依据、外部回执 | 对象存储/日志库 | 按合规策略保留 |
“能再次检索”不代表“应该成为长期记忆”。写入前至少判断:
- 这条信息是否稳定?
- 它是否来自可信来源?
- 新信息与旧信息冲突时谁优先?
- 用户是否有权让系统长期保存它?
- 什么时候过期,谁负责删除?
上下文是一次编译结果
模型每一轮看到的上下文,应该由 Context Builder 根据当前 Step 动态组装:
固定区:系统规则、租户策略、不可变约束
任务区:目标、成功条件、当前计划、最近 checkpoint
证据区:本步骤必需的原文片段和工具摘要
记忆区:按用户、权限、时间过滤后的少量相关事实
预算区:为工具定义、模型输出和突发结果预留空间
原始网页、日志和数据库结果应存到外部,只把引用、摘要和必要片段放进上下文。上下文不是数据库,也不是审计日志;它是针对“下一步决策”临时编译出来的输入。
第五层:持久执行、幂等和并发要一起设计
Agent 的外部调用天然是不可靠的。模型 API 会限流,浏览器会卡住,第三方接口会超时,Worker 会重启。可靠性设计的核心不是“永不失败”,而是失败后知道哪些事情已经发生、哪些可以重试。

不要把慢调用包在数据库事务里
下面这种代码很危险:
async with db.begin():
step = await lock_step(task_id)
result = await external_refund_api(step.payload)
await mark_step_done(step.id, result)
外部接口慢十秒,数据库锁就持有十秒;接口卡一分钟,连接池和等待事务都会堆积。更稳妥的流程是:
- 短事务写入“准备执行”及幂等键;
- 事务提交;
- 调外部接口;
- 用另一个短事务写回结果;
- 如果响应未知,进入
reconciling,主动查询外部状态。
这不是严格意义上的分布式事务,却更符合真实 API 的能力边界。需要保证的是“最终能确认”和“重复执行无害”,而不是假装数据库能回滚一个已经发生的外部退款。
重试要按错误分类
| 错误 | 是否重试 | 处理 |
|---|---|---|
| 连接失败、明确未发出 | 可以 | 指数退避,加随机抖动 |
| 429、服务暂时不可用 | 可以 | 尊重 Retry-After,受总预算限制 |
| 参数错误、权限不足 | 不可以 | 立即失败或请求用户修正 |
| HTTP 200 + 业务错误 | 视错误码 | 不能按传输成功处理 |
| 写请求超时,是否提交未知 | 不能盲重试 | 用幂等键查询或对账 |
| 模型输出格式错误 | 有限重试 | 修复提示,最多一到两次 |
Temporal 的重试文档把易失败的外部操作放在 Activity 中,并提醒区分暂时性错误和永久错误。这一原则不依赖 Temporal:无论使用工作流引擎、消息队列还是自研 Worker,重试都应该发生在具体失败步骤,而不是把整条 Agent 任务从头再跑一遍。
多用户并发需要三种锁
并发控制通常不是“一把分布式锁”就能解决:
- 任务租约:保证同一 Step 同一时刻只有一个 Worker 处理;租约有过期时间和心跳。
- 资源版本:修改订单、工单或文档时使用乐观锁,避免覆盖别人刚写入的状态。
- 业务互斥:同一订单的退款、同一仓库的发布等不可并行操作,按业务键串行。
任务租约解决“谁来跑”,资源版本解决“写入是否基于最新数据”,业务互斥解决“两个合法动作是否允许同时发生”。把三者混成一把全局锁,吞吐量会很差,也很难处理 Worker 崩溃后的锁释放。
什么时候需要工作流引擎
下面几种情况出现两三项时,可以认真考虑 Temporal 一类持久工作流引擎:
- 任务会运行数小时甚至数天;
- 有大量等待审批、定时器和外部回调;
- 步骤多,补偿与重试策略复杂;
- 需要在部署、宕机后自动恢复;
- 审计要求高,希望保留完整事件历史。
如果任务只有三五步、几十秒内结束、几乎都是只读操作,一个带状态表和租约的队列系统通常够用。工作流引擎提供的是持久执行语义,不是免费的简单性。
第六层:权限和安全边界必须在模型之外
Prompt 里写“不要执行危险操作”不是权限系统。模型可能理解错,也可能受到文档中的 Prompt Injection 影响。
真正的授权判断应由工具网关或策略引擎完成:
是否允许 =
用户权限
∩ Agent 身份权限
∩ 当前任务授权范围
∩ 工具自身策略
∩ 数据资源权限
∩ 当前风险与预算限制
模型只能在这个交集里行动。
给工具做风险分级
| 级别 | 例子 | 默认策略 |
|---|---|---|
| L0 | 读取公开天气、公开网页 | 自动执行 |
| L1 | 查询用户有权访问的内部数据 | 自动执行,完整审计 |
| L2 | 发消息、创建草稿、修改非核心记录 | 预览或可撤销执行 |
| L3 | 退款、删除、发布、改权限 | 强校验,通常需要审批 |
| L4 | 大额支付、生产环境破坏性操作 | 默认禁止或双人审批 |
风险级别不是模型给出的建议字段,而应由工具元数据固定。金额、目标环境、影响资源数等运行时参数可以进一步提高风险等级,不能降低工具的基础等级。
预算也是一种权限
每个任务应有明确预算:
- 最大模型轮数;
- 最大工具调用次数;
- 最大运行时间;
- 最大费用;
- 最大写操作次数;
- 单次和累计影响范围。
预算耗尽后进入 paused 或 failed_budget,不要让模型自行决定“再试最后一次”。没有预算的自动重试,是最常见的成本和故障放大器之一。
第七层:Trace、评测和发布门禁要从第一天存在
Agent 出错时,只记录最终答案几乎没有价值。一次任务至少应有一条完整 Trace:
task span
├── context.build
├── model.plan
├── tool.search
├── tool.execute
├── approval.wait
├── model.synthesize
└── outcome.verify
OpenTelemetry Trace API提供了 Span、属性、事件和跨进程上下文传播的通用模型。Agent 不必另造一套完全孤立的链路系统,但需要补充 Agent 特有字段:
task_id、step_id、attempt;- 模型与 Prompt 版本;
- 工具名、参数摘要、结果分类;
- 上下文引用和压缩策略;
- token、费用和各阶段耗时;
- 审批、权限拒绝和安全策略命中;
- 最终验证结果,而不只是模型自报状态。
只测最终答案会漏掉很多问题
Agent 评测至少分四层:
| 层级 | 检查什么 | 例子 |
|---|---|---|
| 工具契约 | 参数和结果是否正确 | 非法日期是否被拒绝,空结果是否可区分 |
| 轨迹 | 中间步骤是否合理 | 是否选对工具,是否发生无效循环 |
| 环境结果 | 外部状态是否正确 | 工单是否创建,代码测试是否通过 |
| 系统指标 | 是否达到交付要求 | 成功率、p95 延迟、人工接管率、成本 |
Anthropic 在 Agent 评测实践中也强调,多轮 Agent 既要看输出,也要看它如何调用工具和改变环境。代码 Agent 最可靠的评分常常不是让另一个模型判断“代码看起来不错”,而是运行单元测试、静态检查和真实任务验证。
pass@k 和 pass^k 回答不同问题
假设同一任务重复运行 (k) 次,并暂时把各次运行视为相互独立:
pass@k:至少成功一次的概率,适合“可以多试几次,挑一个最好结果”的任务;pass^k:每次都成功的概率,适合用户希望稳定复现的生产任务。
一个任务单次成功率为 80%,连续五次都成功只有:
[ 0.8^5 = 32.768% ]
因此 Demo 中“十次有八次不错”并不等于生产可用。对于会修改外部状态的任务,稳定性、可恢复性和失败可见性通常比偶尔出现的最佳答案更重要。
发布门禁不要只放一个总分
比较实用的门禁可以是:
核心任务成功率 >= 95%
高风险误执行次数 = 0
不可恢复任务比例 < 0.1%
工具参数有效率 >= 99.5%
任务 p95 完成时间 不退化超过 10%
单任务 p95 成本 不退化超过 15%
人工接管后可继续率 >= 99%
门禁要按任务类型、风险级别和工具拆分。一个 97% 的总成功率,可能掩盖退款任务只有 70%,也可能被大量简单查询冲高。
做一次容量估算,很多架构争论会自然消失
容量设计不需要一开始就做得很精确,但不能完全凭感觉。
假设系统有 1000 个活跃用户,每人每天触发 1 个 Agent 任务;每个任务平均调用模型 2 次、工具 8 次。60% 的任务集中在早上半小时内执行。
半小时内的平均负载:
任务:1000 × 60% / 1800 秒 ≈ 0.33 task/s
模型:0.33 × 2 ≈ 0.67 call/s
工具:0.33 × 8 ≈ 2.67 call/s
平均值不能拿来直接配容量。按 5 倍突发系数计算,工具层要承受约 13.3 call/s。若工具调用 p95 是 3 秒,用 Little’s Law 粗估:
在途工具调用 ≈ 到达率 × 服务时间
≈ 13.3 × 3
≈ 40
也就是说,仅工具执行就需要大约 40 个并发槽位。再考虑慢工具隔离、重试和余量,Worker 并发可能要配置在 60 左右,而不是看到“每秒才两三个请求”就开 10 个协程。

容量估算还会推动几项设计:
- 慢工具与快工具分队列,避免头部阻塞;
- 每租户设置并发上限,避免一个大客户占满 Worker;
- 模型和工具分别限流,不能只限入口请求;
- 重试流量单独计数,故障时不能把上游压垮;
- 长任务状态外置,Worker 扩缩容不依赖本地内存。
真正上线后,再用 Trace 里的到达率、服务时间和重试率替换假设值。
一套可以逐步落地的最小架构
不是所有团队都需要第一天部署工作流引擎、策略引擎、向量库和独立评测平台。可以按风险逐步增加:
第一阶段:只读 Agent
适合研究、问答和数据分析。
- PostgreSQL 保存任务、Step 和结果引用;
- 对象存储保存大结果和证据;
- 队列负责异步执行;
- 每步有超时、错误分类和 Trace;
- 上线前有一组固定任务评测。
第二阶段:有限写操作
加入发消息、创建草稿、更新低风险记录。
- 工具风险分级;
- 写操作幂等;
- 执行前预览;
- 资源版本检查;
- 用户和租户级并发限制;
- 可撤销动作与补偿步骤。
第三阶段:长任务和高风险动作
加入审批、跨天执行、退款、发布或生产变更。
- 持久工作流或等价的事件历史;
- 审批和外部回调;
- 对账与未知状态恢复;
- 细粒度授权和短期凭证;
- 双人审批、熔断和紧急停机;
- 影子流量、灰度发布和回归评测。
这条路径有一个好处:每增加一层复杂度,都有明确的业务风险作为理由。不要因为“企业级”三个字就堆组件,也不要等第一次重复退款后才补幂等。
上线前,我会追问这二十个问题
最后给一份可以直接拿去做设计评审的清单:
任务与状态
- 成功条件是否可以由系统核验?
- 每个 Step 是否有稳定 ID、状态和尝试次数?
- Worker 重启后从哪里恢复?
- 用户取消任务时,正在执行的外部调用怎么处理?
工具与副作用
- HTTP 成功、业务成功和结果可用是否分开记录?
- 哪些动作可以安全重试?
- 写操作是否有业务语义幂等键?
- 超时后副作用未知时,是否有查询或对账接口?
数据与并发
- 运行状态、工作记忆、长期事实和审计证据是否分开?
- 同一资源被两个任务修改时如何检测冲突?
- 是否有任务租约、心跳和失效接管?
- 大结果是否外置,是否会把上下文撑爆?
权限与风险
- 工具是否按读写、可逆性和影响范围分级?
- 授权是否由确定性代码执行,而不是由模型自我约束?
- 是否限制模型轮数、工具调用数、费用和运行时间?
- 高风险动作是否有审批、预览和回滚?
可观测与评测
- 能否还原一次任务每一步的输入引用、工具结果和耗时?
- 是否同时评测最终结果、执行轨迹和外部状态?
- 是否有按任务类型拆分的发布门禁?
- 线上失败能否沉淀为新的回归案例?
如果这些问题大半没有答案,系统离“企业级”还很远。此时继续调 Prompt,可能会让演示更顺,却不会让系统更可靠。
模型负责判断,底座负责让判断可控
一个成熟 Agent 的价值,不在于它能连续调用多少次工具,而在于它能否在现实约束中把任务做完:失败后能继续,重复执行不出事故,越权动作做不了,结果有证据,成本和延迟算得清。
模型、Prompt 和 Agent 框架当然重要,但它们只在七层底座里占据一部分。真正决定系统能不能长期运行的,往往还是那些传统工程问题:状态机、事务边界、幂等、并发、权限、日志、测试和容量。
这也许没有“再加一个智能体”那么吸引眼球,却是 Agent 从能跑走到能交付的必经之路。
参考资料
- OpenAI:A practical guide to building agents
- Anthropic:Building effective agents
- Anthropic:Writing effective tools for agents
- Anthropic:Demystifying evals for AI agents
- Temporal:Workflow Execution
- Temporal:Retry Policies
- OpenTelemetry:Tracing API
- AgentBench: Evaluating LLMs as Agents
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。
X / Twitter
关注 @ax2_zicode
更即时的技术观察、新文章提醒和一些短想法会发在 X 上。