摘要:本文从一张企业 Agent 架构图出发,拆解「不是更多 Agent,而是可控执行系统」这一命题的工程含义。全文给出九层主线与两翼控制面的完整设计,涵盖 Runtime 状态机、意图路由与规划、Workflow 与 Agent 的统一编排、Skill 与 MCP 工具的治理、业务语义底座、唯一副作用闸门 Action,以及超时、审计、可观测性与评估四面治理,并附可运行的 Python 参考实现与三张架构流程图。
一、被误判的瓶颈:不是 Agent 不够多,而是不够可控
先说一个不太讨喜的现场结论。过去一年我参与评审过的企业 Agent 项目里,真正卡在「模型能力」上的不到两成。剩下八成的失败,与模型是否聪明几乎没有关系,它们死在三件很朴素的事情上。
第一件是权限失控。演示阶段,Agent 拿着数据库读写权限和所有内网接口,出了问题只需要重启进程。生产阶段,同一个 Agent 的一次误删,触发的可能是财务对账失败、客户投诉和合规问询。团队的第一反应通常是「再包一层提示词」,写下「你不得删除数据,除非用户明确要求」。这条规则在七成情况下有效,在剩下三成里,恰好是在它最该生效的那一次失效。原因是提示词是软约束,而权限是硬约束;用软约束去实现硬约束,等价于没有约束。
第二件是无法复现。用户投诉「昨天这个订单它给错了答复」,工程师打开日志,只看到一段对话记录。当时检索到了哪几篇知识、走了哪个分支、调用了哪几个工具、模型为什么选那个工具,全部不可知。没有轨迹就没有修复,只能靠「再试一次」,而再试一次的代价是用户站在那里等。
第三件是成本不可控。一个 Planner 为了「稳妥」,把任务拆成十二步,每步做一次检索加一次反思,单次会话消耗四十万 token。上线三天,账单比预算高一个数量级,产品经理被迫把入口关掉。
三件事有一个共同点:它们都不是智能问题,而是系统问题。模型决定 Agent 能想多远,工程决定它敢不敢被放进生产。这句话在架构图上被写成了一行小字,我认为它是整张图里最值钱的一句------模型决定能想多远,控制边界决定它能走多远。
为什么「多 Agent」是一个诱人的错误方向
面对上述问题,最常见的解法是加 Agent。客服 Agent 判不准,就加一个质检 Agent;质检 Agent 搞不定,再加一个仲裁 Agent。这套结构在汇报材料上看起来很像一家真实的公司:有前台、有中台、有风控、有审计,层级分明。它的问题在于,组织架构的成本被完整地搬进了系统里,而组织架构之所以存在,是因为人类的沟通带宽有限、责任必须落到人头上------这两个前提在软件里都不成立。
每增加一个 Agent,你就增加了一条通信链路、一份提示词、一个上下文窗口的开销,以及一个新的不确定性来源。更麻烦的是,Agent 之间的对话本身是极不可靠的信道:上游的结论以自然语言传给下游,下游的理解偏差不会被任何类型检查捕获。三个 Agent 串联,端到端成功率是各自成功率的乘积而不是平均值。一个单段 0.9 的三段链路,端到端只剩 0.73,而你把三个 0.9 换成一个 0.95 的单体加两个确定性校验节点,端到端能到 0.94。这不是理论推演,是我们做 A/B 时反复看到的结果。
真正缺的从来不是 Agent 的数量,而是承载 Agent 的那层系统:它有状态、有权限、有预算、有轨迹、有回滚。业界把这一层笼统地称为 Harness 或 Runtime。名字不重要,重要的是它必须存在,而且必须比 Agent 更早存在。先有跑车再有赛道是常态,但在 Agent 工程里,顺序经常被搞反。
把问题重新定义一次
所以本文不讨论「如何写出更好的提示词」,也不讨论「该选哪个更强的模型」。本文讨论一个更工程化的问题:假设模型的能力是一个我们无法完全控制的随机变量,那么围绕它的系统应该长什么形状,才能让端到端的行为成为一个我们敢向业务方承诺的确定量?
架构图给出的答案是:一条从意图到企业系统的九层主线,加两侧的控制面。主线负责「把事情做成」,两翼负责「让事情可控」。前者是能力问题,后者是治理问题。企业级 Agent 的分水岭,几乎永远落在后者------因为能力可以通过换模型追上,治理只能靠设计补上,而设计一旦错了,后面每个新能力都会变成新的风险敞口。
二、架构总览:一条主线,两翼护航
先看全图的主线骨架。它自上而下只有九个环节,任何一个真实的 Agent 请求都必须依次穿过,不允许跳层。
#mermaid-svg-Wqfo0EEP5RxxU8x7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .error-icon{fill:#552222;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .marker.cross{stroke:#333333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 p{margin:0;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .cluster-label text{fill:#333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .cluster-label span{color:#333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .cluster-label span p{background-color:transparent;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .label text,#mermaid-svg-Wqfo0EEP5RxxU8x7 span{fill:#333;color:#333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .node rect,#mermaid-svg-Wqfo0EEP5RxxU8x7 .node circle,#mermaid-svg-Wqfo0EEP5RxxU8x7 .node ellipse,#mermaid-svg-Wqfo0EEP5RxxU8x7 .node polygon,#mermaid-svg-Wqfo0EEP5RxxU8x7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .rough-node .label text,#mermaid-svg-Wqfo0EEP5RxxU8x7 .node .label text,#mermaid-svg-Wqfo0EEP5RxxU8x7 .image-shape .label,#mermaid-svg-Wqfo0EEP5RxxU8x7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .rough-node .label,#mermaid-svg-Wqfo0EEP5RxxU8x7 .node .label,#mermaid-svg-Wqfo0EEP5RxxU8x7 .image-shape .label,#mermaid-svg-Wqfo0EEP5RxxU8x7 .icon-shape .label{text-align:center;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .node.clickable{cursor:pointer;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .arrowheadPath{fill:#333333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Wqfo0EEP5RxxU8x7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wqfo0EEP5RxxU8x7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Wqfo0EEP5RxxU8x7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .cluster text{fill:#333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .cluster span{color:#333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Wqfo0EEP5RxxU8x7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .icon-shape,#mermaid-svg-Wqfo0EEP5RxxU8x7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .icon-shape p,#mermaid-svg-Wqfo0EEP5RxxU8x7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .icon-shape .label rect,#mermaid-svg-Wqfo0EEP5RxxU8x7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wqfo0EEP5RxxU8x7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Wqfo0EEP5RxxU8x7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Wqfo0EEP5RxxU8x7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 反馈回路
User 自然语言 / 任务
Intent Router 意图理解 · 分类 · 分发
Planner 任务规划 · 步骤拆解 · 预算分配
Workflow 确定性流程 · 规则驱动
Agent 动态决策 · 自主执行
Skill 专业能力 · 领域知识
Tool / MCP 工具调用 · 外部服务
Business Context / Ontology / Rule 语义底座
Action 业务动作 · 权限校验 · 合规控制
Enterprise Systems CRM · ERP · 订单 · 财务
State / Audit / Eval 状态管理 · 审计追踪 · 效果评估
左右两翼是控制面。左翼管运行时韧性:State 管任务状态与上下文记忆,Context 管用户、业务、知识三类上下文的装载与裁剪,Permission 管身份、访问与操作授权,Retry 管异常、失败与补偿。右翼管治理观测:Timeout 管任务、工具、执行的超时,Audit 管操作、变更与合规留痕,Observability 管日志、指标与链路,Evaluation 管准确性、质量与价值。
这条主线与两翼的关系,可以借用一个更老的类比。网络协议栈里,数据面负责把包送到,控制面负责告诉数据面该怎么送、能送多少、送失败了怎么办。Agent 系统完全是同一个结构:中间那条链是数据面,两侧是控制面,而且控制面必须对数据面的每一次越界都有否决权。
读这张图要抓住的三个判断
第一,权限与合规校验被前移到了 Action 层。 注意它没有出现在 Planner,也没有出现在 Skill,而是紧贴着企业系统的入口。这是一个非常刻意的位置选择。模型可以规划、可以推理、可以提议,但真正落地写数据的动作只有 Action 这一处,把授权、校验、幂等、补偿全部压在这一个闸门上,只需要治理一个点,而不是治理一整条链。这和我们做网络安全的思路一致:不在每个应用里各写一遍鉴权,而是在统一的网关层收口。
第二,Workflow 与 Agent 是并列的执行体,不是替代关系。 图上它们从 Planner 分叉,又在 Skill 处汇合。这个画法容易被误读成「二选一」,实际上它表达的是编排策略:可枚举的业务走 Workflow,靠规则驱动、结果可预测;长尾场景走 Agent,靠动态决策、接受一定的不确定性。两者共用下游的 Skill、Tool、Action,所以治理成本被复用而不是翻倍。真实系统里,一个请求往往前半段走 Workflow、中段遇到歧义下沉给 Agent、得出结论后再回到 Workflow 的确定性收尾------这种混合编排才是常态。
第三,语义底座是一个三元组,而不是一段检索结果。 Business Context、Ontology/Object、Relationship/Rule 三者并列摆在 Tool 与 Action 之间,这个位置意味深长:它说明企业知识不是被「塞进提示词」的素材,而是被「查询和求值」的对象。提示词里的知识会随上下文漂移,结构化的对象和规则不会。这条线区分了企业级方案与演示级方案------演示可以靠向量检索拼凑上下文,生产必须靠对象模型和规则引擎保证语义一致性。
到这里,架构的形状已经清楚了。接下来的问题是:每一层具体怎么实现,边界怎么划,代码长什么样。下面按主线顺序逐层拆解,每层给出一个最小但可运行的设计,并说明它挡掉了哪一类真实故障。
三、Harness / Runtime:先把控制面从 Agent 里剥出来
九层里最容易被低估的是那个没有画成节点的东西:包裹一切的外框。图中用一条细线把整条主线围在 HARNESS / RUNTIME 里,它不在链路上,却在所有层之上。工程上它的含义很明确:Agent 是纯函数,Runtime 是唯一持有状态、权限与预算的地方。
这个分工不是洁癖,而是三个硬需求逼出来的。一是可恢复:长任务跑十几分钟,中间要经历网络抖动、模型超时、人工审批,进程随时可能被杀;如果状态长在 Agent 的对话上下文里,进程一挂,整条任务就没了。二是可审计:审计需要的是一份结构化的事件流,而不是一段自然语言对话。三是可并行:同一个用户的两个请求、同一个企业的几十个并发会话,如果状态散落在 Agent 内部,你就没法做隔离和限流。
所以第一段代码不是 Agent 循环,而是任务状态机。企业 Agent 的失败模式里,有相当一部分不是「做错了」,而是「状态乱了」------任务在 EXECUTING 里被重试了三次,又插入一次补偿,最后数据库里既有扣款又有退款。用一张显式的迁移表把所有非法路径堵死,比在提示词里写「请谨慎操作」有效得多。
python
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
class Phase(str, Enum):
ROUTING = "routing"
PLANNING = "planning"
EXECUTING = "executing"
AWAITING_APPROVAL = "awaiting_approval"
COMMITTED = "committed"
FAILED = "failed"
COMPENSATING = "compensating"
# 合法迁移表:不在表内的迁移一律抛异常,不做任何「尽力而为」的兜底
TRANSITIONS = {
Phase.ROUTING: {Phase.PLANNING, Phase.FAILED},
Phase.PLANNING: {Phase.EXECUTING, Phase.FAILED},
Phase.EXECUTING: {Phase.AWAITING_APPROVAL, Phase.COMMITTED,
Phase.COMPENSATING, Phase.FAILED},
Phase.AWAITING_APPROVAL: {Phase.EXECUTING, Phase.COMPENSATING, Phase.FAILED},
Phase.COMPENSATING: {Phase.COMMITTED, Phase.FAILED},
Phase.COMMITTED: set(), # 终态,不可再迁移
Phase.FAILED: set(),
}
@dataclass
class Budget:
max_tokens: int = 120_000
max_tool_calls: int = 40
deadline_ms: int = 90_000
used_tokens: int = 0
used_tool_calls: int = 0
def spend(self, tokens: int) -> None:
self.used_tokens += tokens
if self.used_tokens > self.max_tokens:
raise RuntimeError("token 预算超限,任务终止")
if self.used_tool_calls > self.max_tool_calls:
raise RuntimeError("工具调用次数超限,任务终止")
@dataclass
class Runtime:
task_id: str
phase: Phase = Phase.ROUTING
budget: Budget = field(default_factory=Budget)
trail: list[dict] = field(default_factory=list)
def transit(self, to: Phase, reason: str = "") -> None:
if to not in TRANSITIONS[self.phase]:
raise RuntimeError(f"非法迁移 {self.phase.value} -> {to.value}")
self.trail.append({"from": self.phase.value, "to": to.value, "why": reason})
self.phase = to
def assert_alive(self) -> None:
if self.phase in (Phase.COMMITTED, Phase.FAILED):
raise RuntimeError("任务已终结,拒绝继续执行")
这段代码有三处值得说明。第一,COMMITTED 是终态而不是「成功态」,因为一个已经提交的任务仍然可能需要在审计里被标记、在评估里被归因,但它绝不能再被同一个流程继续推进。第二,预算写在 Runtime 里而不是写在 Agent 里,Agent 每花一次 token 都要问 Runtime 报账,这样超支是在发生的那一刻被拦住,而不是在任务结束后被账单发现。第三,trail 记录的是状态迁移而不是消息内容,这条轨迹是后续审计与回放的最小骨架,比存一份完整对话便宜一个数量级,也更好查。
Runtime 还承担上下文的装载职责。注意图中左侧的 Context 层,它的关键设计不是「能装多少」,而是「按什么顺序装、什么时候丢」。我们线上用的策略是四段装载:系统约束与工具声明常驻,业务对象通过 Ontology 按需注入,历史对话按轮次摘要压缩,检索结果只在当前步骤有效。装载顺序决定了前缀稳定性,而前缀稳定性直接决定了缓存命中率------这一点在成本章节还会回来。
四、Intent / Router 与 Planner:在概率之上建确定性
第一层要解决的是「这是什么任务」。看起来是个分类问题,实际是个分流问题:分类错了,后面的规划越精确越有害。Router 的设计原则只有一条------低置信度不猜,转澄清。
这条原则听起来保守,但它是从线上事故里换来的。早期的实现里,Router 永远返回一个意图,置信度 0.3 也照单全收,结果是用户问「我的钱怎么还没到」,系统判定为「退款申请」,直接走退款流程,用户实际只是在催查询。一次误判带来的不只是答错,还有一笔真实的资金操作。
python
@dataclass
class Route:
intent: str
confidence: float
# 意图 -> (执行体, 最低置信度)。低于底线一律转澄清,不进入执行链路
ROUTES = {
"refund.request": ("workflow", 0.75),
"order.query": ("workflow", 0.70),
"complaint": ("agent", 0.55),
"unknown": ("clarify", 0.0),
}
def route(r: Route) -> tuple[str, str]:
executor, floor = ROUTES.get(r.intent, ROUTES["unknown"])
if r.confidence < floor:
return "clarify", f"置信度 {r.confidence:.2f} 低于 {floor},请用户确认意图"
return executor, ""
代码很短,但有两个容易被忽略的点。其一,置信度门槛按意图分别设置,因为不同意图误判的代价不同:退款误判直接动钱,门槛就高;投诉误判只是走一条更慢的路径,门槛可以低。其二,clarify 是一个正式的执行体,有自己的话术、上下文和降级路径,不是一句「抱歉我没听懂」的敷衍。它需要带着候选意图去问:「您是查询退款进度,还是要申请退款?」------把不确定性用一次成本极低的交互消除,这是整个系统里性价比最高的动作。
Planner 的职责是把意图变成一个可校验的计划。这里的关键词是「可校验」:如果计划是一段自然语言,你就无法在动手之前发现它要花四十万 token 或者存在循环依赖。所以计划必须是数据结构。
python
@dataclass
class Step:
sid: str
kind: str # tool | skill | agent | action
payload: dict
deps: tuple[str, ...] = ()
budget_tokens: int = 2_000
def validate(plan: list[Step], cap: int = 120_000) -> list[str]:
errs: list[str] = []
ids = [s.sid for s in plan]
for i in set(ids):
if ids.count(i) > 1:
errs.append(f"步骤 id 重复:{i}")
for s in plan:
for d in s.deps:
if d not in ids:
errs.append(f"步骤 {s.sid} 依赖不存在的 {d}")
if has_cycle(plan): # 拓扑排序检测,实现略
errs.append("计划存在环,拒绝执行")
if sum(s.budget_tokens for s in plan) > cap:
errs.append(f"计划预算 {sum(s.budget_tokens for s in plan)} 超过上限 {cap}")
return errs
校验通过之后才允许进入执行阶段,失败则打回重新规划,重新规划的次数同样受 Runtime 预算约束。这里有一个工程细节值得强调:kind 字段把「这一步用什么执行体」显式化了,因此一个计划里可以同时出现确定性步骤与自主步骤,Planner 的输出天然支持混合编排,而不是先决定「用 Workflow 还是用 Agent」再规划。
五、Workflow 与 Agent:两个执行体,一套接口
图上 Planner 分叉出 Workflow 与 Agent 两个执行体,又在 Skill 处汇合。这个结构在实际实现里对应一条很重要的设计约束:它们必须共享同一套执行接口,否则下游的 Skill、Tool、Action 就要各写两遍适配层,治理成本直接翻倍。
用协议(Protocol)而不是抽象基类,是因为 Python 的鸭子类型在这里正好够用:任何实现了 run(ctx) 的对象都能被编排器调度,无论是写死的业务流程、一次 LLM 循环,还是一个外部 HTTP 服务。
python
from typing import Protocol
@dataclass
class StepResult:
ok: bool
output: dict
tokens: int = 0
hint: str = "" # 只对 Agent 有意义,Workflow 直接忽略
class Executable(Protocol):
def run(self, ctx: "ExecCtx") -> StepResult: ...
Workflow 执行器本质是一个带预算与超时的 DAG 遍历器。它对每一层做拓扑排序,逐个执行,任一节点失败就按预定义的策略决定「整体失败」还是「局部降级」。它的确定性来自两点:节点由代码定义而非模型生成,分支条件是可求值的表达式而非自然语言判断。
python
def run_workflow(plan: list[Step], ctx: "ExecCtx") -> StepResult:
done: dict[str, StepResult] = {}
for step in topo_sort(plan):
ctx.runtime.assert_alive()
if not all(done.get(d, StepResult(False, {})).ok for d in step.deps):
return StepResult(False, {"reason": f"{step.sid} 前置未满足"})
res = ctx.executors[step.kind].run(step, ctx) # 派发到具体执行体
ctx.runtime.budget.spend(res.tokens)
done[step.sid] = res
if not res.ok and step.kind == "action":
return StepResult(False, {"failed_at": step.sid, "detail": res.output})
return StepResult(True, {k: v.output for k, v in done.items()})
Agent 执行器则是标准的「观察---决策---行动」循环,但它有一处和常见实现不同的地方:Agent 产出的不是动作,而是动作提议。读操作可以直接执行,写操作必须交给下游的 Action 闸门裁决。这个 read/write 的分离,是整个架构里最便宜的防线,因为它在模型与真实世界之间插了一个永远不会被提示词说服的东西。
python
def run_agent(ctx: "ExecCtx", max_turns: int = 8) -> StepResult:
for turn in range(max_turns):
ctx.runtime.assert_alive()
obs = ctx.observe() # 当前状态 + 上一步结果 + 可用能力
decision = ctx.policy(obs) # 模型调用,纯函数,不持有状态
ctx.runtime.budget.spend(decision.tokens)
if decision.kind == "read_tool":
ctx.trace(turn, decision)
ctx.state.last = ctx.tools.call(decision.name, decision.args)
continue
if decision.kind == "propose_action":
return ctx.gateway.attempt(decision.action, ctx) # 交给闸门,不在循环里做
if decision.kind == "finish":
return StepResult(True, {"answer": decision.text})
return StepResult(False, {"reason": "回合上限耗尽,交还人工"})
注意最后一行:回合耗尽不是崩溃,而是一个显式的、可预期的失败路径,它会被转成人工工单而不是抛出异常。生产系统里,「可控地失败」和「成功地完成」同等重要,前者决定了故障的半径。
两种执行体可以互相嵌套:一个 Workflow 的某个步骤可以让 Agent 去处理模糊子任务,Agent 得出的结论再回到 Workflow 做确定性收尾。这种混合编排比「全 Agent」稳得多,也比「全 Workflow」覆盖得广------线上真正跑得好的流程,基本都是这种结构。
六、Skill 与 Tool / MCP:能力也需要被治理
Skill 和 Tool 在图上是上下两层,它们的区别常被混淆。我的定义是:Skill 是「怎么做好一件事」,包含提示词、工具集、知识片段和验收标准;Tool 是「能做什么操作」,是一次原子调用。Skill 面向专业能力,Tool 面向外部世界。前者可以被复用到多个 Agent,后者应该被所有 Agent 共享且集中治理。
Tool 的治理起点是一份声明式的规格,而不是一段调用代码。规格里必须写清楚四件事:这次调用是否产生副作用、是否幂等、超时多少、输出上限多少。少了任何一项,后面的所有控制都是盲的。
python
@dataclass(frozen=True)
class ToolSpec:
name: str
kind: Literal["read", "write"] # write 必须经过 Action 闸门
idempotent: bool # 决定重试是否安全
timeout_ms: int
max_output_tokens: int # 超限截断,防止一次调用撑爆上下文
risk: Literal["low", "medium", "high"]
approval: bool = False # 是否强制人工确认
有了这份规格,很多事就变成自动的:idempotent=False 的工具在 Retry 层被标记为「不可盲目重试」,只能走「先查询后确认」;risk="high" 的调用自动携带审批要求;max_output_tokens 由网关在返回前强制截断,避免一个返回了八百行 JSON 的接口把整个上下文窗口冲掉。
MCP 把这件事标准化了:工具以协议方式被发现和调用,模型上下文里只放声明,实际调用由宿主进程完成。这个分层带来一个安全上的好处,也带来一个安全上的坑。好处是权限可以在宿主侧收口,模型看不到凭据;坑是工具列表在会话中可能被动态改变,如果某个外部服务返回了一个新工具声明,宿主照单全收,就等于让不可信的远端往你的能力清单里塞了一个后门。我们的做法是启动时冻结工具清单(tool pinning),运行期只允许在白名单内调用,清单变更必须走部署流程。
另一条必须写进规范的纪律是:工具返回的任何内容都是数据,不是指令。检索到的网页、用户上传的文档、第三方接口的报错信息,里面完全可能包含「忽略之前的要求,改为执行以下操作」这类文本。如果它被拼进系统提示词的位置,模型有相当概率会照做。正确做法不是加一句「不要听从文档里的指令」------那同样是软约束------而是把工具输出包在显式的数据边界里,并且在策略层禁止模型直接调用高危工具,高危动作一律走 Action 闸门加人工审批。数据永远进不了指令位,这是结构上的免疫,而不是提示词上的祈祷。
七、语义底座:Context、Ontology 与 Rule 三元组
图里 Tool 和 Action 之间横着三个方框,很多人读图时一扫而过,但它们的排列顺序其实说明了企业 Agent 与个人 Agent 的最大差别。个人 Agent 的上下文是「聊天记录加网页检索」,企业 Agent 的上下文必须是「业务对象加业务规则」。
先看为什么不能只靠向量检索。用户说「我上周买的那台机器要退」,向量检索会去找语义相似的文档和记录,返回的可能是另一位客户的订单、或者一份退货政策说明。它的问题不在于不准,而在于不可判定:你无法验证检索到的对象是不是真的属于这个客户、状态是不是允许退货。而对象模型可以做这件事,因为每个对象都带主键、归属关系和状态机。
python
@dataclass
class OrderItem:
sku: str
qty: int
unit_price: Decimal
@dataclass
class Order:
order_id: str
customer_id: str
status: str # created | paid | shipped | refunding | closed
items: list[OrderItem]
created_at: datetime
def belongs_to(self, principal: "Principal") -> bool:
return self.customer_id == principal.customer_id
Ontology 层解决的是「这个词指的是哪个业务对象」,Relationship 层解决的是「这些对象之间是什么关系」,Rule 层解决的是「在这种状态下允许做什么」。三者叠加,才能回答一句很朴素的话:这个用户、在这个时刻、对这笔订单、能不能退款。这句话是任何业务 Agent 的核心判断,而它无法靠语义相似度得分给出答案。
规则必须可求值、可版本化。可求值意味着它是一段代码或一个表达式,而不是一句「原则上允许七天无理由」的自然语言;可版本化意味着审计能够回答「当时生效的是哪一版规则」。这一点在合规场景里是硬要求,因为一年后追责时,「当时用的是 v3.2 版规则」和「当时的模型觉得可以」是两种性质完全不同的答复。
python
@dataclass(frozen=True)
class Rule:
rid: str
version: str
when: Callable[[dict], bool] # 输入是对象快照,不依赖模型
then: str # 受限动作标识,不是自由文本
REFUND_RULE = Rule(
rid="refund.window",
version="3.2.0",
when=lambda ctx: ctx["order"].status in ("paid", "shipped")
and (ctx["now"] - ctx["order"].created_at).days <= 7
and ctx["order"].belongs_to(ctx["principal"]),
then="ALLOW_REFUND",
)
这三个方框的工程价值在于,它们把「业务知识」从提示词里搬到了可测试的代码里。提示词里的知识会随上下文长度漂移、随模型版本变化、随措辞微调而失效;对象和规则则是可以写单测、可以回归、可以 code review 的资产。一家公司真正沉淀下来的 Agent 能力,多半就在这一层。
八、Action:唯一的副作用闸门
如果整张图只允许保留一个节点,我会留 Action。它是模型与真实世界之间唯一的通道,也是所有治理逻辑的收口点。前面所有的层都只影响「怎么想」,只有这一层决定「做不做」。
一次 Action 要同时满足四个条件才能落地:有权限、没重复、可回滚、留痕迹。四者缺一都会在生产上出问题,而且出问题的形态各不相同------缺权限是越权操作,缺幂等是重复扣款,缺回滚是数据永久损坏,缺痕迹是无法追责。下面这段实现把四件事压在一次调用里。
python
@dataclass(frozen=True)
class ActionIntent:
action: str # refund.create / order.cancel ...
target: str # 业务对象主键
params: dict
idempotency_key: str # plan_id + step_id + 参数哈希
risk: Literal["low", "medium", "high"]
def attempt(intent: ActionIntent, ctx: "ExecCtx") -> StepResult:
# 1 授权:主体 × 动作 × 对象,三者一起判,缺一不可
if not ctx.permission.check(ctx.principal, intent.action, intent.target):
return StepResult(False, {"denied": intent.action, "who": ctx.principal.uid})
# 2 幂等:命中历史记录直接返回,绝不重复执行
if hit := ctx.ledger.find(intent.idempotency_key):
return StepResult(True, {**hit.output, "replayed": True})
# 3 高风险动作挂起等人工,任务进入 AWAITING_APPROVAL 而不是阻塞线程
if intent.risk == "high" and not ctx.approval.granted(intent.idempotency_key):
ctx.runtime.transit(Phase.AWAITING_APPROVAL, reason=intent.action)
return StepResult(False, {"await_approval": intent.idempotency_key})
# 4 预演:先算一遍预期变更,闸门自己先看一眼后果
preview = ctx.executor.simulate(intent)
if not preview.consistent():
return StepResult(False, {"preview_failed": preview.reason})
# 5 执行与记账在同一事务边界内,要么都有,要么都没有
with ctx.ledger.transaction(intent.idempotency_key):
out = ctx.executor.apply(intent)
ctx.ledger.record(intent, out, ctx.runtime.task_id)
ctx.audit.append({"action": intent.action, "target": intent.target,
"result": out, "task": ctx.runtime.task_id})
return StepResult(True, out)
这段代码里有三处值得单独说。幂等键 由计划标识、步骤标识与参数哈希共同生成,保证了「同一次逻辑意图重试多次」只会产生一次真实副作用,这比在工具层做去重可靠得多,因为它锚定的是业务意图而不是调用参数。预演 是一个经常被省略但收益极高的步骤,它让闸门在动用真实写权限之前先拿到一份「如果执行会怎样」的推演,很多越界操作在这一步就被拦下了,成本几乎为零。事务边界把执行与记账绑成原子操作,避免出现「钱扣了但账本没记」这种最糟糕的中间态。
高风险动作的审批不是阻塞等待,而是把任务切到 AWAITING_APPROVAL 并持久化。这一点极其重要:企业里真正需要 Agent 干的高价值动作,往往都需要人签一次字,如果审批是同步阻塞的,一个 Agent 进程会被占住几十分钟;如果是异步挂起,进程可以立即去处理下一个任务,审批通过后再从持久化的状态恢复执行。这也是第三章那个状态机存在的直接原因。
整条链路串起来看是这样的:
Audit 轨迹 幂等账本 业务执行器 人工审批 Permission Action 闸门 Agent 策略 Audit 轨迹 幂等账本 业务执行器 人工审批 Permission Action 闸门 Agent 策略 #mermaid-svg-9Tz5kP6yEzYnZfFB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9Tz5kP6yEzYnZfFB .error-icon{fill:#552222;}#mermaid-svg-9Tz5kP6yEzYnZfFB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9Tz5kP6yEzYnZfFB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9Tz5kP6yEzYnZfFB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9Tz5kP6yEzYnZfFB .marker.cross{stroke:#333333;}#mermaid-svg-9Tz5kP6yEzYnZfFB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9Tz5kP6yEzYnZfFB p{margin:0;}#mermaid-svg-9Tz5kP6yEzYnZfFB .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9Tz5kP6yEzYnZfFB text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9Tz5kP6yEzYnZfFB .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9Tz5kP6yEzYnZfFB .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9Tz5kP6yEzYnZfFB .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9Tz5kP6yEzYnZfFB .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9Tz5kP6yEzYnZfFB #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9Tz5kP6yEzYnZfFB .sequenceNumber{fill:white;}#mermaid-svg-9Tz5kP6yEzYnZfFB #sequencenumber{fill:#333;}#mermaid-svg-9Tz5kP6yEzYnZfFB #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9Tz5kP6yEzYnZfFB .messageText{fill:#333;stroke:none;}#mermaid-svg-9Tz5kP6yEzYnZfFB .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9Tz5kP6yEzYnZfFB .labelText,#mermaid-svg-9Tz5kP6yEzYnZfFB .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9Tz5kP6yEzYnZfFB .loopText,#mermaid-svg-9Tz5kP6yEzYnZfFB .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9Tz5kP6yEzYnZfFB .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9Tz5kP6yEzYnZfFB .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9Tz5kP6yEzYnZfFB .noteText,#mermaid-svg-9Tz5kP6yEzYnZfFB .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9Tz5kP6yEzYnZfFB .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9Tz5kP6yEzYnZfFB .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9Tz5kP6yEzYnZfFB .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9Tz5kP6yEzYnZfFB .actorPopupMenu{position:absolute;}#mermaid-svg-9Tz5kP6yEzYnZfFB .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9Tz5kP6yEzYnZfFB .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9Tz5kP6yEzYnZfFB .actor-man circle,#mermaid-svg-9Tz5kP6yEzYnZfFB line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9Tz5kP6yEzYnZfFB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt高风险动作 提交 ActionIntent校验 主体 × 动作 × 对象拒绝 / 放行查询幂等键命中则回放历史结果挂起任务,写入待审批批准 / 驳回预演 dry-run预期变更与一致性正式执行记账(与执行同事务)追加审计事件StepResult
从图上看,Agent 在这一层被彻底降权了:它只能提交意图,不能执行。这个降权是刻意的,因为 Agent 的输出是概率性的,而真实世界的写操作必须是确定性的。把两者隔开,才可能既享受模型的灵活性,又不承担它失控的代价。
九、治理四面:超时、审计、可观测与评估
右侧那一列四个方框看起来最像「运维配套」,实际上它们是决定 Agent 能否通过生产验收的关键。左侧的 State、Context、Permission、Retry 保证系统在正常路径上不出错,右侧这四面保证系统在异常路径上不失控。
Timeout:不是设一个数,而是预算的逐层切分
最常见的错误做法是在每个调用点各设一个超时值:模型 30 秒、检索 5 秒、写接口 10 秒。这样做的结果是总时长不可预测------一个计划有十二步,最坏情况是十二个 30 秒,用户等了六分钟。正确的做法是让截止时间(deadline)沿调用链向下传递,并且每一层只能拿到剩余预算的一部分。
python
@dataclass(frozen=True)
class Deadline:
expires_at: float # 单调时钟,不受系统时间调整影响
def remaining(self) -> float:
return self.expires_at - time.monotonic()
def child(self, share: float = 0.6) -> "Deadline":
# 子调用只能拿剩余预算的一部分,防止单个慢节点吃光后续所有步骤
return Deadline(time.monotonic() + self.remaining() * share)
share 取 0.6 是一个经验值。它留出 40% 给后续步骤,同时保证当前步骤不至于因为分到的时间太短而频繁超时。更重要的设计是「超时之后做什么」:超时必须带着一份明确的降级策略,要么返回部分结果并标注「未完成」,要么转入人工,绝不能静默重试------静默重试在超时场景里几乎总是放大问题,因为超时的原因往往是下游已经过载。
Audit:审计不是日志,是可回放的事件流
日志是给人看的,审计是给合规和追责用的,两者要求完全不同。日志可以采样、可以滚动删除、可以是自由文本;审计必须完整、必须持久、必须结构化到可以回答「谁在什么时候对哪个对象做了什么,依据是哪条规则、哪一版模型」。
我们的事件结构固定为五个字段:主体、动作、对象、结果、依据。依据这一项最容易被忽略,但它恰恰是追责时的核心------没有它,你只能说「系统执行了退款」,无法说「系统依据 v3.2.0 版退款规则、在模型置信度 0.87 的情况下执行了退款」。前者是事故,后者是可辩护的工程决策。
Observability:按任务、步骤、工具三层打 span
链路的粒度要跟治理的单元对齐。我们按三层打点:任务级 span 记录整体耗时与结果,步骤级 span 记录每个规划步骤的输入输出摘要与 token 消耗,工具级 span 记录每次外部调用的参数、耗时、返回码与截断量。三层对不上就是 bug,比如某个任务的耗时远大于其全部子 span 之和,说明有隐藏的同步阻塞。
python
@contextmanager
def span(name: str, ctx: "ExecCtx", **attrs):
s = ctx.tracer.start(name, attrs | {"task": ctx.runtime.task_id,
"phase": ctx.runtime.phase.value})
st = time.monotonic()
try:
yield s
except Exception as e: # 异常也要落 span,不能只记成功路径
s.set("error", repr(e)); s.set("ms", int((time.monotonic() - st) * 1000))
raise
finally:
s.end()
这套打点的直接收益是归因。当用户投诉「昨天答得不对」,你能在三十秒内调出那条任务的完整轨迹:检索到了什么、走了哪个分支、模型在第几轮改主意、闸门为什么放行。没有这条链路,所有排障都退化成猜测。
Evaluation:把上线决策变成一次回归测试
评估是企业 Agent 里最容易被做成摆设的一环。多数团队的做法是攒几十条问答案例,跑一遍人工看,觉得「还行」就上线。这套流程的问题在于它不可复现、不可比较、不可阻断。
可行的做法是三层评估加一道门禁。第一层是离线集,覆盖高频意图与边界用例,指标包括任务成功率、动作正确率、平均工具调用次数、平均 token、危险动作拦截率。第二层是金标回放,把生产上的真实请求(脱敏后)重放一遍,比较新旧版本的行为差异,重点看「原来对的变错了」这一类回归。第三层是线上抽样,按比例抽样人工复核,用于发现集外的新问题,并定期把这些新问题回流进离线集。
门禁的规则要做成硬性的:提示词、模型版本、规则库、工具规格,任何一项变更都必须跑完前两层,任一项核心指标退化超过阈值就阻断发布。这条规则的价值不在于它能抓住多少问题,而在于它把「要不要上线」从主观判断变成了数据判断。
十、闭环与代价:状态、重试与成本预算
主线的最后一段是 State、Audit、Eval,图上只有一行字,但它决定了系统是不是真的在「变好」。没有闭环的 Agent 系统只能靠人肉迭代:每周复盘一次 badcase,改一轮提示词,下周再复盘。有闭环的系统则把这件事写进了数据流。
State 要分三层存,混在一起是后期最难还的债。任务态是持久化的执行进度,用于恢复与接管;会话态是短期记忆,带 TTL,用于多轮上下文;领域态是业务事实,存在业务库里,Agent 只能读不能臆造。很多团队把三者都塞进对话历史,结果是任务一长就爆上下文,而且业务事实被语言转述后失真,后续无法核验。
Retry 的关键不在退避算法,而在先判断这次调用值不值得重试 。第三章的工具规格里有一个 idempotent 字段,它在这里派上用场:幂等调用可以安全盲重试,非幂等调用必须先查询实际状态再决定,否则一次网络超时后的重试可能变成重复扣款。补偿则是另一层保障,每一步写操作都要登记一个可逆的补偿动作,整条流程失败时按逆序执行补偿,这就是 Saga 的思路。
python
@dataclass
class RetryPolicy:
max_attempts: int = 3
base_ms: int = 200
factor: float = 2.0
jitter: float = 0.2
breaker_threshold: int = 5 # 连续失败多少次后熔断
def delay(self, attempt: int) -> float:
raw = self.base_ms * (self.factor ** attempt)
return raw * (1 + random.uniform(-self.jitter, self.jitter)) / 1000
def call_with_retry(fn, spec: ToolSpec, policy: RetryPolicy, ctx) -> StepResult:
for attempt in range(policy.max_attempts):
try:
return fn()
except TransientError:
if not spec.idempotent: # 非幂等:不盲重试,先对账
return ctx.ledger.reconcile(spec.name)
if ctx.breaker.opened(spec.name, policy.breaker_threshold):
return StepResult(False, {"circuit_open": spec.name})
time.sleep(policy.delay(attempt))
return StepResult(False, {"retry_exhausted": spec.name})
成本是这一章必须谈的另一半,因为它往往比正确性更早压垮项目。我们的做法是三层限额加三条降级路径。三层限额是:单任务上限(防失控)、单租户日上限(防一个客户吃掉全部配额)、全局日上限(防整体账单击穿)。三条降级路径按代价从低到高:减少反思轮次、切换到更小的模型、缩短检索范围。降级要由 Runtime 自动触发,而不是等财务月底发现。把成本当作一等指标,意味着每次发布都要回答一个问题:这次改动让单次会话的中位成本变化多少。并且这个数字要与成功率并列出现在发布报告里。
还有一条与成本直接相关但很少被提起的工程细节:上下文的前缀稳定性。模型侧的缓存命中是按前缀匹配的,如果你每次请求都在最前面插入时间戳、随机 ID 或排序不稳定的检索结果,缓存命中率会掉到接近零。把常驻部分(系统约束、工具声明、规则摘要)放在最前面且保持字节稳定,把易变部分放在后面,这一条改动在我们线上把单次成本降了三成以上,而它不需要换模型,也不需要改任何业务逻辑。
什么时候才真的需要多 Agent
讲到这里,可以正面回答开头那个问题了:多 Agent 到底在什么条件下才合理。我的判断标准有三条,全部满足才值得引入第二个 Agent。
第一条是职责边界可以被数据切分。比如检索与写作、审核与生成,两者的输入输出能用结构化契约描述清楚,而不是靠自然语言互相商量。如果两个 Agent 必须通过对话协商才能推进,那它们本质上是同一段逻辑被切成了两半,拆开只会增加不可控性。
第二条是不确定性可以相互隔离。一个 Agent 的出错不会污染另一个的输入,或者即便污染了也能被下游的校验拦住。反例是「规划 Agent 生成计划、执行 Agent 照着做」:执行质量完全取决于规划质量,拆开之后你还要额外花力气保证两者对同一份计划的理解一致,而这恰恰是自然语言通信最不可靠的地方。
第三条是并发收益大于协调成本。让三个 Agent 并行处理三个独立子任务,收益是可观的;让三个 Agent 串行接力完成同一个任务,收益通常是负的,因为串联放大误差而不放大能力。
按这三条筛下来,企业场景里真正需要多 Agent 的地方并不多:并行检索与汇总,不同专业域的独立处理(法务、财务、技术各自有自己的工具与知识),以及由一个监督者控制多个执行者的审阅式结构。除此之外,多数所谓多 Agent 系统,用单个 Agent 加确定性编排就能做得更好、更便宜、也更容易审计。
而且即便引入多 Agent,前面的九层一层都不能少。每个 Agent 都要有自己的预算、自己的轨迹、自己的工具白名单;它们之间的消息传递要走结构化契约而不是自由文本;共享状态必须放在 Runtime 里,而不是放在某个 Agent 的上下文里。换句话说,多 Agent 不是对本架构的替代,而是它在横向上的一次复制------先把单 Agent 的控制面做扎实,复制才有意义。
最后把闭环画完整:
#mermaid-svg-Wodtf7ghehSwe49l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Wodtf7ghehSwe49l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wodtf7ghehSwe49l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wodtf7ghehSwe49l .error-icon{fill:#552222;}#mermaid-svg-Wodtf7ghehSwe49l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wodtf7ghehSwe49l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wodtf7ghehSwe49l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wodtf7ghehSwe49l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wodtf7ghehSwe49l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wodtf7ghehSwe49l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wodtf7ghehSwe49l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wodtf7ghehSwe49l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wodtf7ghehSwe49l .marker.cross{stroke:#333333;}#mermaid-svg-Wodtf7ghehSwe49l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wodtf7ghehSwe49l p{margin:0;}#mermaid-svg-Wodtf7ghehSwe49l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wodtf7ghehSwe49l .cluster-label text{fill:#333;}#mermaid-svg-Wodtf7ghehSwe49l .cluster-label span{color:#333;}#mermaid-svg-Wodtf7ghehSwe49l .cluster-label span p{background-color:transparent;}#mermaid-svg-Wodtf7ghehSwe49l .label text,#mermaid-svg-Wodtf7ghehSwe49l span{fill:#333;color:#333;}#mermaid-svg-Wodtf7ghehSwe49l .node rect,#mermaid-svg-Wodtf7ghehSwe49l .node circle,#mermaid-svg-Wodtf7ghehSwe49l .node ellipse,#mermaid-svg-Wodtf7ghehSwe49l .node polygon,#mermaid-svg-Wodtf7ghehSwe49l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wodtf7ghehSwe49l .rough-node .label text,#mermaid-svg-Wodtf7ghehSwe49l .node .label text,#mermaid-svg-Wodtf7ghehSwe49l .image-shape .label,#mermaid-svg-Wodtf7ghehSwe49l .icon-shape .label{text-anchor:middle;}#mermaid-svg-Wodtf7ghehSwe49l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Wodtf7ghehSwe49l .rough-node .label,#mermaid-svg-Wodtf7ghehSwe49l .node .label,#mermaid-svg-Wodtf7ghehSwe49l .image-shape .label,#mermaid-svg-Wodtf7ghehSwe49l .icon-shape .label{text-align:center;}#mermaid-svg-Wodtf7ghehSwe49l .node.clickable{cursor:pointer;}#mermaid-svg-Wodtf7ghehSwe49l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Wodtf7ghehSwe49l .arrowheadPath{fill:#333333;}#mermaid-svg-Wodtf7ghehSwe49l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wodtf7ghehSwe49l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wodtf7ghehSwe49l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wodtf7ghehSwe49l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Wodtf7ghehSwe49l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wodtf7ghehSwe49l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Wodtf7ghehSwe49l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wodtf7ghehSwe49l .cluster text{fill:#333;}#mermaid-svg-Wodtf7ghehSwe49l .cluster span{color:#333;}#mermaid-svg-Wodtf7ghehSwe49l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wodtf7ghehSwe49l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Wodtf7ghehSwe49l rect.text{fill:none;stroke-width:0;}#mermaid-svg-Wodtf7ghehSwe49l .icon-shape,#mermaid-svg-Wodtf7ghehSwe49l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wodtf7ghehSwe49l .icon-shape p,#mermaid-svg-Wodtf7ghehSwe49l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Wodtf7ghehSwe49l .icon-shape .label rect,#mermaid-svg-Wodtf7ghehSwe49l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wodtf7ghehSwe49l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Wodtf7ghehSwe49l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Wodtf7ghehSwe49l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
退化
新 badcase
任务执行
Runtime + Planner
审计事件流
主体 · 动作 · 对象 · 结果 · 依据
评估器
成功率 · 动作正确率 · 成本
回归门禁
离线集 + 金标回放
发布
提示词 · 规则 · 工具规格
阻断并归因
样本回流
脱敏后进入离线集
这张图里最值得注意的箭头是「评估器到样本回流再到回归门禁」。它把线上发现的每一个问题都变成了下一次上线前的一条测试用例,这是唯一能让系统随时间变好的机制。缺了这条箭头,前面所有的治理都只是防止变坏,而不是推动变好。
十一、落地路线图:从最小闭环开始
讲完九层,最后一个问题是怎么落地。常见失败是把整张图一次性建完,三个月后交付一个谁也不敢用的系统。我的建议是按四个阶段推进,每个阶段都以「能上线、能观测、能回滚」为交付标准。
| 阶段 | 建设内容 | 交付标准 | 典型周期 |
|---|---|---|---|
| P0 骨架 | Runtime 状态机、Action 闸门、审计事件流、三层 span | 只读场景上线,所有写操作被闸门拦截并转人工 | 3 到 4 周 |
| P1 确定性 | Workflow 引擎、20% 高频场景的规则化流程 | 覆盖主要业务量,成功率与人工基线可比 | 6 到 8 周 |
| P2 自主性 | Agent 循环、语义底座、混合编排 | 长尾场景接管率达标,人工介入率可控 | 8 到 12 周 |
| P3 治理 | 评估门禁、成本三层限额、降级策略 | 每次发布跑回归,月度成本可预测 | 持续 |
顺序不能反。P0 看起来最不产生业务价值,但它决定了后面所有阶段的天花板。我见过不止一个项目跳过 P0 直接做 P2,结果在演示时惊艳、在上线时被安全评审一票否决,回头补闸门的成本是当初的十倍,因为那时候已经有二十个工具和十五条流程绕过了它。
还有一个反直觉的经验:先做只读,再做写入。只读场景没有副作用,可以用最低的治理成本验证意图识别、规划质量与工具链稳定性;等这些稳了,再逐步放开写操作,而且每放开一类写操作,都要单独配一套审批与补偿策略。按这个节奏,团队对系统的信任是逐步建立的,而不是在一次大爆炸式上线后靠祈祷维持。
十二、结语
回到开头那个判断:企业 Agent 的瓶颈不是 Agent 不够多,而是执行系统不够可控。这张架构图之所以值得认真读,是因为它把「智能」和「控制」放在了两个不同的位置上------模型在链路的中间,控制面在链路的四周,两者的边界清晰且不可互相侵入。
如果把整篇文章压缩成三句话,我会这样说。第一,把副作用收敛到唯一的闸门,模型可以提议,但只有系统能执行。第二,把不确定的部分显式化:低置信度就问,高风险就审批,超时就降级,不要用提示词去假装确定性。第三,把治理做成数据流而不是文档:审计事件、评估指标、回归门禁,让系统自己证明它变好了。
模型决定能想多远,控制边界决定它能走多远。这句话反过来读同样成立:控制边界画得越清楚,模型能安全覆盖的范围就越大。所以「加约束」和「放能力」并不矛盾,它们是一件事的两面------你越能把失控的代价压到可接受,就越敢把更重的业务交给它。
这也是我在所有企业 Agent 项目里坚持的第一条原则:先造赛道,再放跑车。