AI Agent 工程化实战 #03:工具与外部能力------接口化(本文) | 承接 #02 的工具层;下一篇讲状态与记忆
上一篇把工具放进了「工具与知识」这一层:负责真去读数据、写数据,失败时把原因告诉编排,不要自己编给用户看的句子。这一篇继续往下挖:一个工具怎样才算能进门。
契约里那份副作用清单,是上限。工具规格,是把上限写成可检查的条目。例子仍用变更说明 Agent:现在只允许读 diff、读提交说明。文末有一份空白 Tool Spec,还有一张准入清单。
1. 工具在这里指什么
工具是编排能调用的外部能力:读接口、写接口、查库、跑命令、调 MCP。模型不能直接摸这些能力,只能通过编排去调。
「接口化」不是多写几个 HTTP 路径,而是约定清楚四件事:
- 这个工具对应契约里哪一条允许的副作用
- 超时了、空了、没权限了,怎么告诉编排
- 重复调用会不会多写一次
- 谁有资格叫它,下线时怎么收
没有规格的工具,看起来像插件,用起来像暗门。今天能读,明天顺手加了写,契约还没改,线上已经能改数据了。
2. 工具没管住时,现场是什么样
变更说明如果只图「模型能调到」,通常会出这些事。
用户说「顺便合并」。模型选中了一个写接口,因为工具列表里本来就挂着它。契约写了不许写,工具箱却没收干净。
拉 diff 超时了。工具把一段 HTML 错误页塞回上下文,模型据此写出「本次优化了稳定性」。失败原因没有分类,编排没法返回「变更内容暂不可用」那句固定话。
网络抖动,同一次「生成说明」重试了两次。如果以后加了「写回评审描述」,没有幂等约定,描述会被写两遍,或者第二遍把第一遍盖掉。
下线一个废弃的读接口。编排和提示词里还留着旧名字,模型继续选它,整条成功路径变成随机失败。
这几类问题,换更强的模型补不上。要补的是:进门有清单,调用有规格,失败有固定原因,下线有收口。
3. 先定一条总规矩
契约没写进「允许」的写操作,工具层根本不要有这个方法。
读操作也要登记。不是「能调的都可以挂上去」,而是「挂上去的每一项,都能在契约里找到对应允许」。
对照变更说明 Agent:
| 契约怎么写 | 工具箱里该有什么 | 不该有什么 |
|---|---|---|
| 允许读本次 diff、读本次提交说明 | read_diff、read_commit_message |
merge、push、发评论 |
| 写操作一律禁止 | 无写方法 | 「写回描述」「创建评论」哪怕只是实验开关 |
以后要加「写回评审描述」,顺序是:先改契约(允许这个写操作,并且必须人工确认)→ 再写 Tool Spec → 再实现方法 → 最后挂进编排。反过来做,工具会比契约多一只手。
模型不直接拿工具列表。上一篇已经说过:该拒绝的、材料不够的,编排根本不会走到调工具。工具规格再严,也替不了那一层判断。
4. 一份 Tool Spec 最少写什么
一个工具一页短规格就够。写不下,往往是一个名字下塞了好几件事,该拆开。
4.1 名称与对应契约
写清工具名、一句话做什么、对应契约里哪一条允许的副作用。对不上契约,就不要实现。
4.2 输入与输出
输入写必填、选填、禁止传入。输出写成功时返回什么结构。不要返回「已经帮用户写好的说明」------那是编排和模型的事。工具成功时返回材料;失败时返回原因分类。再写清一句:是否面向用户可见,默认否。 面向用户可见的句子(拒绝、失败说明、最终变更说明)由编排按契约映射,或由模型出草稿后再经编排检查。工具默认只交出材料或执行回执,不直接对用户说话。
4.3 超时
写默认超时、最长多重试、超时后告诉编排的原因名。原因名要稳定,例如 TIMEOUT,不要把供应商的英文长句原样丢上去。编排靠原因名去配用户看到的那句话。
超时毫秒数可以放在配置里调。规格里要钉死的是:超时之后算哪一种失败,以及会不会自动重试。读接口可以有限重试;写接口默认不自动重试,除非规格写明幂等键怎么用。
4.4 失败原因
最少分清这几类,名字可以按项目统一,但含义要固定:
| 原因 | 含义 | 编排通常怎么处理 |
|---|---|---|
EMPTY |
读到了,但内容是空的 | 按契约走「缺输入」失败 |
TIMEOUT |
规定时间内没返回 | 有限重试或直接失败 |
FORBIDDEN |
身份过了编排,工具侧仍无权限 | 失败,不让模型圆 |
NOT_FOUND |
指定对象不存在 | 失败 |
DOWNSTREAM |
下游乱报错、非预期响应 | 失败,可记日志,不把原文塞给模型 |
CONFLICT |
写操作撞上版本或重复 | 写工具才用;读工具一般没有 |
工具不要在失败时返回一段自然语言说明。自然语言一变,编排的固定句式就对不上了。
4.5 幂等
读工具:同样的参数,重复调用结果应一致(数据本身变了除外)。写工具:必须写明幂等键是什么。没有幂等键的写工具,编排侧不要做自动重试。
幂等键由编排生成,再传给工具,不要让工具自己猜。常见做法是拼业务标识,例如 change_id + 操作类型,需要区分稿件版本时再加上草稿摘要。同一键第二次提交,工具应返回与第一次一致的回执,而不是再写一遍。
变更说明现在只有读,也要在规格里写一句「本工具无写副作用,重复调用安全」。以后加写回时,不能靠记忆,要靠规格。
4.6 权限与确认
谁能调用:只允许编排调用,不允许模型层直接持有客户端。模型如果自己拿着工具客户端,就可以绕过编排里的边界判断:该拒绝的请求可能直接打到写接口,人工确认也会被跳过。所以「模型拿不到客户端」不是风格偏好,是拒绝语义和确认闸门能不能立住的前提。
要不要人确认:读操作通常不需要。写操作默认需要,除非契约明文写了「可自动执行」------多数对外写操作不要开这个口子。
4.7 版本与下线
工具有版本。行为变了就升版本,旧版本给出下线日期。编排和门禁用的是登记过的名字与版本,不靠提示词里的口语别名。
5. 变更说明 Agent:两份读工具怎么写
下面是填写粒度的示例,不是某个仓库的真实类名。
# Tool Spec
名称:read_diff
版本:v1
对应契约:允许读取本次 diff
一句话:按变更标识读取统一 diff 文本
输入
必填:change_id
选填:无
禁止传入:要求执行 merge/push 的指令字段
输出(成功)
字段:diff_text、charset
说明:只返回文本材料,不返回「变更说明」正文
是否面向用户可见:否
超时
默认:3s
自动重试:最多 1 次,仅针对 TIMEOUT
超时原因名:TIMEOUT
失败原因
EMPTY:diff 去空白后长度为 0
TIMEOUT:超时
FORBIDDEN:调用方无权读该 change_id
NOT_FOUND:change_id 不存在
DOWNSTREAM:上游返回无法解析的内容(不把原文塞进模型上下文)
幂等:只读,重复调用安全
权限:仅编排可调用;模型不可持有该客户端
人工确认:不需要
下线:无计划;若替换实现,保持原因名不变
# Tool Spec
名称:read_commit_message
版本:v1
对应契约:允许读取本次提交说明
一句话:按变更标识读取提交说明原文
输入
必填:change_id
选填:无
禁止传入:同 read_diff
输出(成功)
字段:message_text
是否面向用户可见:否
超时:2s;TIMEOUT 可重试 1 次
失败原因:EMPTY / TIMEOUT / FORBIDDEN / NOT_FOUND / DOWNSTREAM(含义同 read_diff)
幂等:只读,重复调用安全
权限:仅编排可调用
人工确认:不需要
编排侧可以约定:两个都是空,才走「缺少 diff 或提交说明」;只有一个有内容,仍可进入成功路径。这是编排规则,不要写进工具返回的自然语言里。
如果以后加 write_review_description,规格里至少多几行:对应契约新版本、是否面向用户可见仍为否(回执不是说明正文)、幂等键由编排生成(例如 change_id + write_review_description + draft_hash)、需人工确认:是。确认前编排不得调用。
6. 带走物一:空白 Tool Spec
# Tool Spec
名称:
版本:
对应契约条目:
一句话:
## 输入
必填:
选填:
禁止传入:
## 输出(成功)
字段:
说明:成功时只返回材料或执行回执,不返回给用户看的业务长文
是否面向用户可见:是 / 否(默认否)
## 超时
默认:
自动重试:次数 / 仅针对哪些原因
超时原因名:
## 失败原因(名称固定)
EMPTY:
TIMEOUT:
FORBIDDEN:
NOT_FOUND:
DOWNSTREAM:
CONFLICT:(写工具才填)
## 幂等
是否只读:
写操作幂等键:由编排生成;常见拼法为业务标识 + 操作类型(必要时加草稿摘要)
无幂等键时禁止自动重试:是 / 否
## 权限与确认
调用方:仅编排 / 其他(不建议)
说明:模型直接持有客户端会绕过边界判断,拒绝语义与人工确认都会失效
人工确认:是 / 否
依据契约哪一条:
## 版本与下线
当前版本:
兼容策略:
计划下线日期:
填不出「对应契约条目」,就先停。先改契约,再写规格。
7. 带走物二:工具准入清单
新工具上线,或给模型「多挂一个能力」之前,对着问。有一条是否,不准进箱。
| # | 问什么 | 答「否」说明什么 |
|---|---|---|
| 1 | 契约「允许」里能否找到对应条目? | 工具比契约多一只手 |
| 2 | 是否有一页 Tool Spec,且失败原因名写全了? | 编排没法配固定句式 |
| 3 | 失败时是否只返回原因分类,不返回业务长文? | 失败会被模型圆成成功语气 |
| 4 | 写工具是否有幂等键,且键由编排按业务标识生成?无键时编排是否禁止自动重试? | 重试可能写两次 |
| 5 | 写工具是否要求人工确认(除非契约明文可自动)? | 副作用闸门不在 |
| 6 | 模型层是否拿不到该工具的客户端?拿得到就会绕过拒绝判断和人工确认 | 分层又糊回去了 |
| 7 | 输出是否默认「不对用户可见」,成功时只交材料或回执? | 工具在替编排/模型说话 |
| 8 | 超时与重试是否写清,并且读/写策略不同? | 写操作被当成读操作重试 |
| 9 | 下线或改名时,编排与门禁是否只认登记名? | 提示词里的别名会变成幽灵调用 |
| 10 | 门禁里是否至少有一条:该工具失败时走契约规定的失败句? | 工具挂了却仍可能「成功」 |
第 1 条和第 6 条最容易烂。前者是契约没改就加能力;后者是图上分层了,模型却仍能直接调工具。
实验环境可以宽松,但实验工具不要进生产清单。名称上分开,比靠口头「先试一下」可靠。
8. 几种看起来省事、后面很贵的做法
把供应商 SDK 的原始错误丢给模型。模型会开始解释 HTML 错误页,用户看到的话每天不一样。先收成原因名,再让编排配句。
一个工具名干两件事:又读 diff 又写评论。规格写不清,权限也写不清。拆开,读写分开登记。
用提示词约束「不要调用写工具」。提示词不是准入。写方法还在列表里,就总会被选中。没收掉方法,比多写一句「请小心」有效。
全公司共用一把「万能工具箱」。不同 Agent 的契约不同,共用箱会把最宽的那份权限传给最窄的那个 Agent。按 Agent 或按契约版本挂工具,不要挂全局大杂烩。
9. 和前后篇怎么接
#01 规定哪些副作用允许。#02 规定工具待在「工具与知识」层,失败只回报原因。本篇把「允许」落成 Tool Spec,并用准入清单挡住没登记的能力。
状态怎么存、记什么不记什么,下一篇讲。工具读到的材料进了会话之后怎么处理,和记忆策略有关,但不改变本篇的进门规则。
发布前用契约例子回归时,应覆盖「工具返回 TIMEOUT / EMPTY」的路径。那是 #05 的事;本篇先保证这些原因名存在且稳定。
下一篇
AI Agent 工程化实战 #04:状态与记忆------工程视角
工具读回来的内容、对话里说过的话,哪些只活在这一次调用,哪些可以留下。先问该不该记,再问能不能存。
系列导航
| 编号 | 完整标题 | 状态 |
|---|---|---|
| #00 | AI Agent 工程化实战 #00:工程化到底在工程什么 | 已出 |
| #01 | AI Agent 工程化实战 #01:边界先行------Agent 的产品契约 | 已出 |
| #02 | AI Agent 工程化实战 #02:分层交付------别把智能糊进一锅 | 上一篇 |
| #03 | AI Agent 工程化实战 #03:工具与外部能力------接口化 | 本文 |
| #04 | AI Agent 工程化实战 #04:状态与记忆------工程视角 | 下一篇 |
| #05 | AI Agent 工程化实战 #05:质量门禁------嵌进流水线 | 待更 |
| #06 | AI Agent 工程化实战 #06:可观测与运行手册 | 待更 |
| #07 | AI Agent 工程化实战 #07:发布与演进------版本、灰度、回滚 | 待更 |
| #08 | AI Agent 工程化实战 #08:协作与所有权 | 待更 |
