第 8 章:执行模型与工具目录¶
1. 继续看同一个支持场景,但这次进入写路径¶
继续沿用前几章的同一个场景。
用户写道:
我已经等了三天还没有开通访问权限。请检查状态,如果申请卡住了,就创建一个紧急工单。
乍一看,这个任务很简单:
- 智能体读取消息;
- 调用检查状态的工具;
- 如果申请确实卡住,再调用创建工单的工具;
- 返回答复。
在演示里,这几乎就够了。在生产环境里,最贵的错误恰恰从这里开始。
因为现在的问题已经不只是 模型想做什么。真正的问题是:
- 它究竟被允许调用哪些工具;
- 在什么租户作用域下才允许;
- 哪些参数才算有效;
- read 和 write 操作在哪里分开;
- 如果外部服务在副作用之后卡住,该怎么办;
- 事后怎么证明工单到底创建了一次还是两次。
所以,工具调用不能被设计成模型旁边的小助手,而必须被设计成平台的执行层。
2. 智能体不应该直接接触工具¶
这里最有价值的工程习惯之一其实很朴素:智能体永远不应该直接获得真实集成的访问权限。
你需要的是一个执行层,它能够:
- 知道可用工具目录;
- 校验输入参数;
- 应用策略检查;
- 区分 read 和 write 操作;
- 管理重试、超时和幂等性;
- 发出审计事件。
对于同一个支持场景,这意味着模型不应该直接调用 helpdesk API 或 IAM 服务。它应该只和执行层说话。
贯穿案例:重复工单控制
在 support-triage 案例里,执行层会变得非常具体。check_access_request_status 是带作用域的读操作,而 create_support_ticket 是受治理的写操作,需要 approval、idempotency、timeout handling 和 outcome telemetry。如果 helpdesk API 在创建工单后超时,运行时不能让模型简单重试;它需要一条 reconciliation path,用来证明副作用是否已经发生。
3. 一个请求如何穿过执行层¶
现在把同一个场景看成一条执行路径。
3.1. 先由模型提出一个读工具¶
要判断申请是否卡住,智能体需要一个状态检查工具。这是读路径:
- 它不应该改变外部世界;
- 它需要正确的租户作用域;
- 它应该返回干净、结构化的结果。
3.2. 然后系统再决定写工具是否被允许¶
如果状态显示申请确实卡住,下一步可能就是 create_support_ticket。但这已经进入写路径:
- 这里会产生副作用;
- 可能需要审批;
- 需要幂等键;
- 需要更严格的审计轨迹。
3.3. 然后执行层接手那些不体面的现实问题¶
也正是在这里,演示里很少出现的问题开始出现:
- helpdesk 在创建工单后超时;
- 工具只返回部分成功;
- 模型在重试后重复调用;
- 外部服务返回了意外的载荷;
- 运行时已经无法确定副作用是否真的发生。
这已经不是“工具调用”而已,而是执行纪律。
模型不应该直接与外部世界通信,而应该通过执行层
flowchart LR
A["提示 + 策略上下文"] --> B["Model"]
B --> C["工具请求"]
C --> D["Execution layer"]
D --> E["Catalog lookup"]
D --> F["策略 / 校验"]
D --> G["重试 / 超时 / 幂等性"]
G --> H["External system"]
H --> D
D --> I["结构化工具结果"]
I --> B 4. 工具目录是平台接口,不是随机函数的集合¶
如果你把目录看成“函数调用的文件夹”,它很快就会变成一个集成垃圾场。更有用的理解方式是:工具目录是执行层的公共接口。
在这个支持场景里,目录应该明确告诉你,智能体到底能做什么:
check_access_request_statusget_user_profilecreate_support_ticketrequest_human_approval
一个好的目录通常会保存:
- 稳定的工具名;
- 工具目的描述;
- 输入 schema;
- 风险等级;
- 副作用等级;
- 允许调用者或能力;
- 超时、重试策略和幂等性预期。
这会让执行层变得可检查:团队看到的不是“模型也许会调什么”,而是一个具体的平台契约。
4.1. 过大的工具目录会伤害选择质量,而不是扩大自由¶
另一个非常实际的问题会在目录变得过大时出现。
模型一次看到的工具越多:
- 它就要为工具描述消耗越多提示 token;
- 那些彼此相似的契约就会越长;
- 区分接近能力的难度也会越高;
- 选择阶段的注意力也会被稀释。
这也是为什么"干脆把所有工具都展示给模型"通常不是好主意。选择质量下降,不是因为模型"变笨了",而是因为候选集合本身太嘈杂。
这里一个很实用的模式通常叫作语义工具过滤:
- 完整 registry 仍然保留在 platform layer;
- 但某一次 run 只会看到一个狭窄而相关的子集;
- 通常是 3 到 5 个工具,而不是几十个。
这在重叠能力上尤其重要,因为它们之间差异很细:多个搜索工具、多个写适配器、多个相似编排动作。
还有一个很有用的实用规则:如果模型一开始看到的就是一个过于嘈杂的目录,那么重试并不能真正修复工具选择问题。
5. 必须区分读工具和写工具¶
这看上去似乎很明显,但实践里很多系统几乎把它们当成同一类对象来描述。
对于同一个支持智能体,check_access_request_status 和 create_support_ticket 不是简单的两个工具,而是两种不同的风险类别。
read tools 通常:
- 风险更低;
- 更适合自动调用;
- 对依据构建和检索更有帮助;
- 需要访问控制,但不一定总要审批。
write tools 通常:
- 会产生副作用;
- 需要更强的校验;
- 必须有明确的回滚边界;
- 经常需要幂等键和人工审批。
如果读操作和写操作都被塞进一个模糊的“工具调用”概念里,执行层很快就会失控。
5.1. 另一个有用的分类:data、action、orchestration¶
OpenAI 的实践指南里还有一个很有用的简化:工具不只适合按 read 和 write 分,也适合按它们在系统中的角色来分。2
data tools读取并返回上下文:状态检查、检索、CRM 读取;action tools改变外部世界:创建工单、发送邮件、更新记录;orchestration tools帮助运行时自身工作:请求审批、交接、调用规划器。
这两条分类轴可以一起使用:
data tools通常更接近read;action tools通常更接近write;orchestration tools可能两者都有,但它们有单独的运营含义。
Anthropic 的工作流分类在这里又补上了一层很有用的纪律。1 目录不应该只告诉模型“有哪些工具存在”,还应该说明“这些工具在哪些编排模式里才是安全可用的”。
例如:
data tool也许适合放进routing、prompt chaining或parallelization;write action tool也许只适合出现在审批中断之后,或者只适合放进边界非常紧的工作流;orchestration tool,比如request_human_approval或handoff_to_specialist,会直接改变执行图,因此需要更严格的追踪和负责人规则;orchestrator-workers模式可能需要一个显式的 worker-safe 目录子集,而不是把父级全部工具表面都暴露出去。
所以,成熟的工具目录最终不会只是一个“可调用函数列表”。它会变成执行模式与允许副作用之间的边界契约。
6. 工具契约应该无聊而严格¶
智能体系统里最糟糕的习惯之一,就是允许模型自己即兴决定调用格式。
在好的设计里,一个工具应该拥有明确契约:
- 清晰的必填字段;
- 可理解的 enum 和约束;
- 正常的错误消息;
- 显式的返回格式;
- 超时或重复请求时可预测的行为。
对于这个支持场景,它可以长这样:
tools:
check_access_request_status:
description: "Read the current status of an access request"
kind: "read"
risk: "low"
timeout_seconds: 10
input_schema:
required: ["request_id", "tenant_id"]
properties:
request_id: {type: string}
tenant_id: {type: string}
create_support_ticket:
description: "Create a support ticket in the internal helpdesk"
kind: "write"
risk: "medium"
idempotent: true
timeout_seconds: 15
input_schema:
required: ["title", "queue", "requester_id", "tenant_id", "idempotency_key"]
properties:
title: {type: string, maxLength: 200}
queue: {type: string, enum: ["support", "security", "ops"]}
requester_id: {type: string}
tenant_id: {type: string}
idempotency_key: {type: string}
description: {type: string}
它看起来很普通。很好。契约层越少魔法,工具层就越稳定。
7. 执行层应该统一错误语义¶
另一个非常常见的问题是:每个外部服务都用自己的风格返回错误,而智能体几乎原样接收。
在同一个支持场景里,这很容易变成一锅粥:
- IAM 服务返回 HTTP 500;
- helpdesk 回了
"created": true,却没有ticket_id; - 老旧适配器返回 HTML;
- 超时发生在副作用之后;
- 下游 API 返回空 body。
执行层应该把这些统一成可用的结果:
successretryable_failurevalidation_failurepermission_deniedside_effect_unknown
这会大幅提高可解释性,也让智能体能做出更成熟的动作:重试、请求审批、升级给人、或者安全停止。
8. 幂等性和重试不能事后再补¶
几乎所有真实集成最终都会给你至少一种不愉快场景:
- 副作用已经发生后才超时;
- 重试之后重复调用;
- 部分成功;
- 两次运行之间出现竞态条件;
- 外部服务响应时间远超预期。
如果幂等性没有内建进执行设计,智能体很快就会做出那些在普通系统里已经很难排查的重复动作。
对于这个支持场景,一个很实际的规则是:任何会创建工单、更新记录或发送消息的写工具,在第一次生产 rollout 之前都必须有明确的幂等策略。
9. 执行层的实用规则¶
如果要把生产里真正有用的规则压缩成一小组,通常就是这些:
- 每个工具都应该有负责人、schema、风险等级和契约生命周期。
- 读路径和写路径必须在第一次 rollout 之前就分开,而不是等第一次事故之后再补。
- 每个写工具在接触真实流量前都必须有幂等策略。
- 所有工具结果都应该先被规范化,再返回给模型。
- 只要副作用可能已经发生,停下来通常比盲目重试更安全。
10. 团队最常做错什么¶
执行层往往会在同样的地方出问题:
- 直接给模型外部 API 的访问权;
- 把读工具和写工具混成一个模糊类别;
- 让工具在不同编排模式之间随意泄漏,却没有说明 routing、parallelization、审批中断和 worker 委派到底哪些才被允许;
- 把重试藏在适配器深处,却没有审计轨迹和幂等性;
- 把原始载荷直接返回给模型,而不是给出规范化结果;
- 不给目录层设负责人和弃用策略。
11. 一个简单的执行层骨架¶
下面不是生产运行时,而是一个骨架,用来展示责任如何拆分:查找、校验、执行、规范化结果。
from dataclasses import dataclass
@dataclass
class ToolSpec:
name: str
kind: str
timeout_seconds: int
idempotent: bool
@dataclass
class ToolResult:
status: str
payload: dict
def execute_tool(spec: ToolSpec, args: dict) -> ToolResult:
if spec.kind not in {"read", "write"}:
return ToolResult(status="validation_failure", payload={"reason": "unknown tool kind"})
if spec.kind == "write" and "idempotency_key" not in args:
return ToolResult(status="validation_failure", payload={"reason": "missing idempotency key"})
# In production this call would go through policy checks, a gateway, and typed adapters.
return ToolResult(status="success", payload={"tool": spec.name})
关键不在于这个例子有多复杂,而在于工具不是直接由模型决定后立刻执行的。
12. 工具结果也需要设计¶
如果工具结果太原始,模型就又重新获得了危险的即兴空间。
好的结果应该:
- 简短;
- 结构化;
- 不夹带无关技术噪音;
- 具备机器可读状态;
- 对不确定性保持诚实。
糟糕的结果则会:
- 把完整外部载荷整段扔回来;
- 混杂面向用户的文本和系统细节;
- 无法区分“没找到”和“系统挂了”;
- 不说明副作用是否真的发生。
13. 工具目录应该缓慢演化¶
如果工具每天都变、没有兼容性和版本管理,智能体系统很快就会像是在对接一个极不稳定的私有 API。
所以目录层很适合具备这些习惯:
- 版本化契约;
- 弃用策略;
- 每个工具都有负责人;
- schema 和结果形状的测试;
- 新写工具引入前做能力评审。
这是无聊的平台工作,而不是浪漫的即兴发挥。正因为如此,它才可靠。
14. 给工具层做一次快速成熟度测试¶
团队不应该只因为工具已经能被调用,就把执行层叫做成熟。
更强的标准应该是这样:
- 目录是显式的,并且有人负责;
- read、write 和编排语义可以被清楚区分;
- 幂等性是在事故发生前设计好的;
- 不确定性不会被伪装成成功;
- 模型不会变成直接对接外部集成的表面。
如果只缺一两项,系统也许还能运行。但如果大多数都缺失,那这个工具层仍然只是原型包装层。
15. 读完这一章后先做什么¶
如果你想快速检查执行层,可以用这个短清单:
- 你有真正的工具目录,而不是一堆函数吗?
- read 和写工具分开了吗?
- 参数有 schema 校验吗?
- 外部错误被统一了吗?
- 超时、重试和幂等性都考虑了吗?
- 能看出副作用是否发生了吗?
- 每个工具都有负责人和契约生命周期吗?
如果连续几个答案都是否,那说明你的智能体虽然已经能调用工具,但执行模型还远未成熟。
16. 接下来读什么¶
这一部分接下来的自然主题是:沙箱执行、MCP 作为集成契约,以及重试和回滚边界的规则。也正是在那里,你会看到同一个支持智能体并不只是“会调工具”,而是通过成熟执行层去调工具。