企业级 Agent 的七层底座
原创 · 约 39 分钟阅读 · 阅读 --

企业级 Agent 的七层底座

作者: Alex Xiang


很多 Agent 项目的第一版都差不多:一个模型、一段系统提示词、几个工具,再套一层循环。模型决定下一步做什么,程序执行工具,把结果放回上下文,直到模型说任务完成。

这段代码也许不到两百行,演示时甚至相当惊艳。真正接入业务以后,问题才会一起出现:

  • 进程重启后,已经执行到一半的任务从哪里继续?
  • 超时重试会不会重复发邮件、重复退款、重复创建工单?
  • 同一用户连续提交两次任务,两个 Agent 会不会同时修改同一份数据?
  • 模型说“完成了”,系统凭什么相信它真的完成了?
  • 工具返回 HTTP 200,但正文是业务错误,应该记成功还是失败?
  • 一条任务跑了四十分钟,谁能说清时间花在模型、工具、队列还是数据库?

这些都不是换一个更强模型就会消失的问题。模型负责在不确定信息里做判断,基础设施负责让判断受到约束、让动作可以恢复、让结果能够核验。

本文不绑定某个 Agent 框架。我们从三个常见案例出发,搭一套由七层组成的参考架构,再把其中最容易踩坑的状态、幂等、容量和评测问题展开。

先看三个难度完全不同的任务

“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_refoutput_refattemptstarted_atfinished_aterror_typeidempotency_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,而是先做三件事:

  1. 合并只有参数不同的重复工具;
  2. 把适用条件、禁用条件和反例写进描述;
  3. 根据任务阶段只暴露当前需要的工具集合。

工具发现、参数生成、执行和结果解释也应分别记录。否则“选错工具”和“工具执行失败”最终都会变成一条模糊的 Agent 失败。

第四层:状态、记忆和证据不要混在一个向量库里

长期记忆很容易被设计成“把所有对话都做 Embedding”。这样上线快,几个月后会遇到重复、过期、互相矛盾和权限泄漏。

至少应分成四类数据:

类型保存什么主要存储生命周期
运行状态当前步骤、租约、重试次数、审批状态事务数据库任务结束后归档
工作记忆当前计划、最近 observation、临时摘要KV/数据库 + 上下文分钟到小时
事实记忆用户偏好、稳定业务事实、已验证知识结构化库/向量库有版本和失效规则
证据与审计原始工具结果、决策依据、外部回执对象存储/日志库按合规策略保留

“能再次检索”不代表“应该成为长期记忆”。写入前至少判断:

  • 这条信息是否稳定?
  • 它是否来自可信来源?
  • 新信息与旧信息冲突时谁优先?
  • 用户是否有权让系统长期保存它?
  • 什么时候过期,谁负责删除?

上下文是一次编译结果

模型每一轮看到的上下文,应该由 Context Builder 根据当前 Step 动态组装:

固定区:系统规则、租户策略、不可变约束
任务区:目标、成功条件、当前计划、最近 checkpoint
证据区:本步骤必需的原文片段和工具摘要
记忆区:按用户、权限、时间过滤后的少量相关事实
预算区:为工具定义、模型输出和突发结果预留空间

原始网页、日志和数据库结果应存到外部,只把引用、摘要和必要片段放进上下文。上下文不是数据库,也不是审计日志;它是针对“下一步决策”临时编译出来的输入。

第五层:持久执行、幂等和并发要一起设计

Agent 的外部调用天然是不可靠的。模型 API 会限流,浏览器会卡住,第三方接口会超时,Worker 会重启。可靠性设计的核心不是“永不失败”,而是失败后知道哪些事情已经发生、哪些可以重试。

Agent 状态迁移与副作用边界:先记录意图,再执行带幂等键的动作,最后用外部证据确认

不要把慢调用包在数据库事务里

下面这种代码很危险:

async with db.begin():
    step = await lock_step(task_id)
    result = await external_refund_api(step.payload)
    await mark_step_done(step.id, result)

外部接口慢十秒,数据库锁就持有十秒;接口卡一分钟,连接池和等待事务都会堆积。更稳妥的流程是:

  1. 短事务写入“准备执行”及幂等键;
  2. 事务提交;
  3. 调外部接口;
  4. 用另一个短事务写回结果;
  5. 如果响应未知,进入 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大额支付、生产环境破坏性操作默认禁止或双人审批

风险级别不是模型给出的建议字段,而应由工具元数据固定。金额、目标环境、影响资源数等运行时参数可以进一步提高风险等级,不能降低工具的基础等级。

预算也是一种权限

每个任务应有明确预算:

  • 最大模型轮数;
  • 最大工具调用次数;
  • 最大运行时间;
  • 最大费用;
  • 最大写操作次数;
  • 单次和累计影响范围。

预算耗尽后进入 pausedfailed_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_idstep_idattempt
  • 模型与 Prompt 版本;
  • 工具名、参数摘要、结果分类;
  • 上下文引用和压缩策略;
  • token、费用和各阶段耗时;
  • 审批、权限拒绝和安全策略命中;
  • 最终验证结果,而不只是模型自报状态。

只测最终答案会漏掉很多问题

Agent 评测至少分四层:

层级检查什么例子
工具契约参数和结果是否正确非法日期是否被拒绝,空结果是否可区分
轨迹中间步骤是否合理是否选对工具,是否发生无效循环
环境结果外部状态是否正确工单是否创建,代码测试是否通过
系统指标是否达到交付要求成功率、p95 延迟、人工接管率、成本

Anthropic 在 Agent 评测实践中也强调,多轮 Agent 既要看输出,也要看它如何调用工具和改变环境。代码 Agent 最可靠的评分常常不是让另一个模型判断“代码看起来不错”,而是运行单元测试、静态检查和真实任务验证。

pass@kpass^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 个协程。

从用户任务量推导模型调用、工具 QPS、在途并发和 Worker 容量的估算图

容量估算还会推动几项设计:

  • 慢工具与快工具分队列,避免头部阻塞;
  • 每租户设置并发上限,避免一个大客户占满 Worker;
  • 模型和工具分别限流,不能只限入口请求;
  • 重试流量单独计数,故障时不能把上游压垮;
  • 长任务状态外置,Worker 扩缩容不依赖本地内存。

真正上线后,再用 Trace 里的到达率、服务时间和重试率替换假设值。

一套可以逐步落地的最小架构

不是所有团队都需要第一天部署工作流引擎、策略引擎、向量库和独立评测平台。可以按风险逐步增加:

第一阶段:只读 Agent

适合研究、问答和数据分析。

  • PostgreSQL 保存任务、Step 和结果引用;
  • 对象存储保存大结果和证据;
  • 队列负责异步执行;
  • 每步有超时、错误分类和 Trace;
  • 上线前有一组固定任务评测。

第二阶段:有限写操作

加入发消息、创建草稿、更新低风险记录。

  • 工具风险分级;
  • 写操作幂等;
  • 执行前预览;
  • 资源版本检查;
  • 用户和租户级并发限制;
  • 可撤销动作与补偿步骤。

第三阶段:长任务和高风险动作

加入审批、跨天执行、退款、发布或生产变更。

  • 持久工作流或等价的事件历史;
  • 审批和外部回调;
  • 对账与未知状态恢复;
  • 细粒度授权和短期凭证;
  • 双人审批、熔断和紧急停机;
  • 影子流量、灰度发布和回归评测。

这条路径有一个好处:每增加一层复杂度,都有明确的业务风险作为理由。不要因为“企业级”三个字就堆组件,也不要等第一次重复退款后才补幂等。

上线前,我会追问这二十个问题

最后给一份可以直接拿去做设计评审的清单:

任务与状态

  • 成功条件是否可以由系统核验?
  • 每个 Step 是否有稳定 ID、状态和尝试次数?
  • Worker 重启后从哪里恢复?
  • 用户取消任务时,正在执行的外部调用怎么处理?

工具与副作用

  • HTTP 成功、业务成功和结果可用是否分开记录?
  • 哪些动作可以安全重试?
  • 写操作是否有业务语义幂等键?
  • 超时后副作用未知时,是否有查询或对账接口?

数据与并发

  • 运行状态、工作记忆、长期事实和审计证据是否分开?
  • 同一资源被两个任务修改时如何检测冲突?
  • 是否有任务租约、心跳和失效接管?
  • 大结果是否外置,是否会把上下文撑爆?

权限与风险

  • 工具是否按读写、可逆性和影响范围分级?
  • 授权是否由确定性代码执行,而不是由模型自我约束?
  • 是否限制模型轮数、工具调用数、费用和运行时间?
  • 高风险动作是否有审批、预览和回滚?

可观测与评测

  • 能否还原一次任务每一步的输入引用、工具结果和耗时?
  • 是否同时评测最终结果、执行轨迹和外部状态?
  • 是否有按任务类型拆分的发布门禁?
  • 线上失败能否沉淀为新的回归案例?

如果这些问题大半没有答案,系统离“企业级”还很远。此时继续调 Prompt,可能会让演示更顺,却不会让系统更可靠。

模型负责判断,底座负责让判断可控

一个成熟 Agent 的价值,不在于它能连续调用多少次工具,而在于它能否在现实约束中把任务做完:失败后能继续,重复执行不出事故,越权动作做不了,结果有证据,成本和延迟算得清。

模型、Prompt 和 Agent 框架当然重要,但它们只在七层底座里占据一部分。真正决定系统能不能长期运行的,往往还是那些传统工程问题:状态机、事务边界、幂等、并发、权限、日志、测试和容量。

这也许没有“再加一个智能体”那么吸引眼球,却是 Agent 从能跑走到能交付的必经之路。

参考资料

打开原图 ↗