模型说“执行”,系统不能立刻照做
模型输出一句“删除临时文件”,距离操作系统真正删除文件,中间应该隔着一条清楚的工程边界。
这条边界要检查工具是否存在、参数是否合法、当前任务是否允许、操作是否需要审批、应该在哪个环境执行,以及结果要怎样返回给模型。它还要隐藏凭据、处理超时、记录审计信息,并保证同一工具在不同 Agent 中可以有不同权限。
这个边界就是 Harness 的第四个核心部分:工具网关。
模型负责表达意图,工具网关负责把意图转换为受控的现实操作。如果把 Agent 比作一家公司,模型是提出工作请求的员工,工具网关则是统一办事大厅:每项业务都有表单、权限、办理流程和回执。
本文是《把 Harness 讲清楚:Agent 背后的运行系统》八个核心部分的第四篇。
工具不是一个普通函数
在代码里,工具最终可能确实是一个函数。但对 Agent 来说,一个合格的工具至少包含这些信息:
{
"name": "read_file",
"description": "读取工作区内一个文本文件的指定行范围",
"input_schema": {
"path": "string",
"offset": "positive integer",
"limit": "integer between 1 and 500"
},
"execution_mode": "parallel",
"risk": "read_only",
"timeout_ms": 10000,
"output": "带行号的文本窗口,并标记是否截断"
}
名称和描述帮助模型选择工具,schema 限制参数形状,执行模式帮助调度器判断并发,风险标签交给策略层,输出约定让后续处理保持稳定。
只有函数实现而没有这些契约,模型就只能靠猜。
一个工具调用经过哪些阶段
模型生成工具名与参数
↓
查找当前 Agent 可见的工具
↓
解析并校验参数
↓
应用任务范围、权限和审批策略
↓
选择执行环境与并发模式
↓
执行工具,处理取消、超时和异常
↓
规范化结果并过滤敏感信息
↓
记录审计事件
↓
把适量结果返回模型和用户界面
任意一步失败都应该返回明确语义。例如 UNKNOWN_TOOL、INVALID_ARGUMENTS、PERMISSION_DENIED、TIMEOUT 和 RESULT_TOO_LARGE,比统一返回“工具调用失败”更有用。
工具设计首先要窄
下面两个工具都能执行命令:
工具 A:run_anything(command: string)
工具 B:run_tests(target: string, timeout_seconds: integer)
工具 A 灵活,但模型需要自己拼接 Shell 字符串,权限范围巨大,也难以判断副作用。工具 B 能力较窄,却更容易描述、校验、审计和授权。
不是所有场景都必须禁用通用终端,但高频业务动作最好提供语义明确的窄工具。一个好的工具通常具备:
- 名称说明动作,不依赖隐含上下文;
- 参数少而清楚,枚举优于自由文本;
- 一次调用只承担一个主要职责;
- 读操作和写操作分开;
- 错误告诉调用方下一步可以怎么做;
- 返回结构稳定,并明确是否截断。
参数校验必须在模型之外
提示词可以告诉模型“路径必须在工作区”,但真正执行前仍然要由代码验证。
def validate_read_file(args, workspace):
path = resolve_path(workspace, args["path"])
if not path.is_inside(workspace):
raise ToolError("PATH_OUTSIDE_WORKSPACE")
if args["limit"] < 1 or args["limit"] > 500:
raise ToolError("INVALID_LIMIT")
if not path.is_file():
raise ToolError("FILE_NOT_FOUND")
return path
模型生成结构化参数能够减少错误,却不能取代服务端校验。模型输出始终是不可信输入。
只暴露当前任务需要的工具
如果系统注册了 200 个工具,不代表每次请求都要全部提供给模型。
过多工具有三类成本:
- 每个 schema 都占用上下文;
- 相似工具增加误选概率;
- 不必要的高风险能力扩大攻击面。
工具网关可以根据任务契约、Agent 身份和当前阶段生成可见集合:
def visible_tools(agent, task, step):
tools = registry.for_scope(agent.scope)
tools = tools.allowed_by(task.allowed_actions)
tools = tools.allowed_by(agent.identity)
tools = tools.relevant_to(step.kind)
return tools
例如分析阶段只提供读取和搜索,确认需要修改后才开放写文件,只有用户明确要求发布时才开放外部发布工具。
凭据不能进入模型上下文
模型可能需要调用带认证的 API,但它通常不需要知道令牌本身。
正确流程是:
模型调用 create_issue({ title, body })
↓
工具网关识别当前用户与目标服务
↓
凭据服务在执行侧注入访问令牌
↓
向外部 API 发起请求
↓
过滤响应中的敏感字段后返回结果
令牌不应出现在工具描述、参数、日志、报错或模型可见文本中。否则上下文记录、遥测和第三方模型请求都可能扩大泄露范围。
读写分类与并发调度
多个只读调用通常可以并行,例如同时读取三个互不相关的文件。写操作则需要更谨慎。
一种实用分类是:
| 模式 | 适用工具 | 调度方式 |
|---|---|---|
| Parallel | 纯读取、无共享可变状态 | 进入有界并发池 |
| Exclusive | 写文件、执行命令、外部写入 | 作为顺序屏障单独运行 |
并发安全应该“默认关闭”。只有工具明确定义为安全,分类器明确返回允许时才并行。不确定、异常或未声明都回退到独占模式。
即使工具主体并行,结果也最好按照模型原始调用顺序提交。这样历史稳定、回放确定,也避免先完成的后置上下文改变后续结果顺序。
返回结果需要两种形态
程序和模型对结果的需求不同。
数据库查询的规范结果可能包含 5000 行结构化 JSON,程序可以继续过滤;模型可能只需要总数、前 20 条和下一页游标。用户界面又可能希望展示一张表格。
因此可以拆成:
- 规范值:完整、类型稳定,供程序和后续工具使用;
- 模型内容:有长度控制的文本或结构化摘要;
- 展示元数据:供 UI 渲染表格、差异或终端卡片。
这种分离能避免为了模型上下文而破坏程序需要的数据,也不会为了保留完整数据而把上下文撑爆。
MCP 在工具网关中的位置
Model Context Protocol 提供了外部工具发现和调用的标准方式。Harness 可以把 MCP Server 暴露的工具映射进自己的注册表,例如:
MCP 原始工具:search
服务器名称:docs
Harness 可见名称:mcp__docs__search
但“来自 MCP”不等于可以绕过本地策略。映射后的工具仍应经过参数验证、作用域限制、审批、调度、超时和结果过滤。
MCP 解决的是连接协议,工具网关解决的是整个 Agent 产品中的准入和执行治理。两者互补,不是替代关系。
工具执行器伪代码
async def execute_tool(agent, task, call):
tool = registry.resolve(agent.scope, call.name)
if tool is None:
return error("UNKNOWN_TOOL")
args = tool.schema.parse(call.arguments)
decision = policy.authorize(
identity=agent.identity,
task=task,
tool=tool,
arguments=args,
)
if decision.requires_approval:
decision = await approval.ask(decision.prompt)
if not decision.allowed:
return error("PERMISSION_DENIED", decision.reason)
try:
value = await with_timeout(
tool.execute(args, credentials.for_tool(tool)),
tool.timeout,
)
return tool.present(tool.redact(value))
except Timeout:
return error("TIMEOUT")
注意:超时后是否可以重试,要由工具的幂等性和副作用语义决定,不能在这个函数里统一重放。
常见的失败方式
工具描述过于相似
search、find、lookup 都写着“查找信息”,模型很难稳定选择。名称和适用边界应有明显区别。
允许工具自行读取全局凭据
凭据分散在环境变量和日志里,难以审计和轮换。应该由统一凭据服务按调用注入。
把工具异常原样返回模型
堆栈可能泄露路径、SQL、令牌或内部结构。内部日志可以详细,模型可见错误应安全且可行动。
写工具没有幂等键
发布、付款和创建工单在网络超时后重试,可能生成重复结果。外部写操作应支持稳定请求键或先查询状态。
把超时声明当成已经执行
schema 里写了 timeout_ms,不代表底层一定会取消。运行时必须真正传播信号并执行截止策略。
工具网关检查清单
- 工具名称、描述和适用场景是否清楚?
- 参数是否有严格 schema 和服务端校验?
- 是否只向当前 Agent 暴露必要工具?
- 读、写和破坏性操作是否明确分类?
- 凭据是否只在执行侧注入?
- 工具是否支持取消、超时和结构化错误?
- 并发是否默认保守,并设置上限?
- 结果是否区分规范值、模型内容和 UI 展示?
- 是否标记截断、总量和完整数据位置?
- 外部写入是否具备幂等或状态核验机制?
最后理解工具网关
工具让 Agent 拥有行动能力,工具网关让这种能力变得可控。
成熟的系统不会把模型输出直接接到操作系统和外部 API,而是在中间建立一条稳定边界:先理解调用,再验证参数和权限,选择正确环境执行,最后返回可审计的结果。
一句话总结:Agent 决定想做什么,工具网关决定这件事能不能做、在哪里做,以及怎样安全地做。