04. 工具设计:手脚的粒度、描述与异步
工具是动作空间;设计质量决定模型能不能选对、做对、收得住
上下文解决「看见什么」,工具解决「能对世界做什么」。本篇对应书的第 4 章:在工具从十几个涨到成百上千、任务从同步问答拉到跨邮件与电话的异步世界时,动作空间该如何设计。
两个核心压力:
- 选择:工具说明本身就能撑爆窗口,模型如何找到「那一个」?
- 时间:长任务、用户打断、外部回调并存时,如何避免同步死等?
五类工具:谁发起、作用在哪
| 类型 | 调用方向 | 作用对象 | 例子 |
|---|---|---|---|
| 感知 | Agent 主动 | 获取信息 | web_search、read_file、grep |
| 执行 | Agent 主动 | 改变世界 | write_file、shell_exec、send_email |
| 协作 | Agent 主动 | 其他 Agent / 人类 | spawn_subagent、请求审批 |
| 事件触发 | Agent 注册,外部触发 | 唤醒 Agent | set_timer、monitor_shell、connect_channel |
| 用户沟通 | Agent 主动 | 触达用户 | reply_to_user、卡片消息、推送 |
前三类在同步 ReAct 里最常见;后两类在「秘书型」常驻 Agent 里变成标配——没有事件入口,系统只能等用户下一句。
能力形态:专用工具还是 Skill + 通用执行器
| 形态 | 是什么 | 适合 |
|---|---|---|
| 专用工具 | 结构化 function + 严格 schema | 嵌套参数、强权限、需审计的稳定 API |
| Skill + 通用执行器 | 自然语言流程 + bash / 代码解释器 | 变更频繁、步骤组合多、模型足够强 |
例:「部署应用」写成 Skill(build → docker → kubectl),用终端逐步执行,不必为每步挂一个 MCP 工具。
三维决策:
- 参数复杂度——联合校验多 → 专用 schema 更稳
- 变更频率——周周变的流程 → 改 Skill 文本比改代码发布便宜
- 模型能力——弱模型更吃结构化引导;强模型更能消化 Skill
默认倾向:通用执行器优先,除非安全、权限粒度或性能有明确理由。计算器十个不如沙箱 Python 一个;但生产库写操作仍应封装成可审计的专用接口。
粒度:该合就合,该分就分
过细:工具数膨胀,选择错误率升(经验上过百个后,即使强模型也明显吃力)。
过粗:单工具语义糊,参数变成大杂烩。
整合标准看功能相似性 + 场景重叠。extract_pdf / extract_docx / extract_pptx 都是「路径进、文本出」→ 合并为 read_document(file_type=...) 更清晰。OCR 与视频关键帧提取虽都叫「提取」,参数形态与延迟差太大,硬合并会让模型更懵。
高频且参数集独特的能力,可保持独立,避免被通用接口拖慢。
描述质量:告诉模型「何时用 / 不能做什么」
工具描述决定调用准确率。调试原则:频繁选错时,先改描述,再怀疑模型。
写描述时优先这几项:
- 触发条件——「当需要实时信息或未知事实时使用」,优于只写「搜索相关内容」。
- 边界与反例——「仅匹配文件名,不搜文件内容」;大多数失败来自模型不知道工具不能做什么。
- 参数用例子——
timestamp: RFC3339,如 2024-03-15T14:30:00Z;phone: E.164,如 +8613888888888。 - 返回形状——JSON 字段列表,减少后续解析幻觉。
- 代价提示——「完整下载网页可能 5~10 秒;只要元信息请用 get_page_metadata」。
- 1~5 个真实调用示例——schema 说不清的隐式约定(秒 vs 毫秒、过滤嵌套)靠例子传达。
对比两段描述,差别一目了然:
# 弱
search_files: 搜索文件
# 强
search_files:
何时用:只知道文件名片段、要在工作区内定位路径时。
不要用:需要按文件内容关键字查找时(请用 grep_file)。
参数:
pattern: glob 模式,例如 "**/*.ts"、"src/**/Button.tsx"
不含正则;匹配的是路径而非文件内容。
返回:字符串数组,每项为相对工作区的路径;最多 200 条,
超出时 truncated=true 并提示收窄 pattern。
示例:{"pattern": "**/test_*.py"}
参数保真:禁止静默改写模型意图
比缺功能更隐蔽的故障:参数传递层静默转换输入。
案例形态:读文件工具原样返回中文弯引号;编辑工具却把弯引号改成直引号 → old_string 永远匹配失败 → 模型反复重试却无法自诊断。另一类是静默注入:所有 git commit 被附加额外 flag,旧版 Git 直接报错,模型只会改 commit message 措辞。
原则:模型感知的世界与工具操作的世界不得有系统性偏差。 必须规范化时,要在描述与返回值里显式告知,而不是「好心」改写。
感知工具:控制信息量
感知只读,可缓存、可并行。设计要点:
- 搜索返回候选列表(标题、位置、摘要),不要一次倾倒全文;支持分页 / cursor。
read支持 offset/limit;截断必须显式(「已显示 1–200 / 共 5000 行」),静默截断会导致基于残缺信息的错误决策。- 输出超阈值时做上下文感知压缩或落盘 + 引导
read_file。 - 多模态:纯文字可 OCR/提取;布局敏感(复杂表、UI)保留图像给视觉模型。
执行工具:安全与验证闭环
执行会改变世界,错误代价不对称。分层防护:
- 输入验证——路径穿越、命令注入、类型范围;异常则快速失败,不「智能修正」。
- 权限——工作目录白名单、危险命令策略、API 配额;黑名单只能作一层,Shell 组合爆炸要语义层理解(第 5 章)。
- 提议者-审核者——高风险操作前,独立模型(最好不同家族、能力相近)审批;拒绝理由以 tool 结果回写轨迹。
- Sidecar——与主流式输出并行的轻量分类,只看结构化
{tool, args},不看主模型自由文本,降低话术操纵;连续拒绝则熔断到人工。 - 自动验证——
write_file后跑 linter,错误结构化返回,形成执行-验证-反馈环。 - 长输出——头尾保留 + 中间省略 + 完整路径。
- 幂等——可重试操作带 idempotency key;不可幂等(发邮件、转账)用预检-确认两段式。
沙盒不是 venv:venv 只隔离依赖。隔离强度从 OS 机制 → 容器 → microVM 递增,按威胁模型选型。
协作工具:子 Agent 与人在回路
子 Agent 价值是专业化与并行,不是为拆而拆。上下文传递四档:
| 策略 | 传什么 | 适合 |
|---|---|---|
| 最小化 | 仅调用参数 | 查天气等高频简单 |
| 手动筛选 | 显式字段 | 可控但费设计 |
| 自动裁剪 | 规则:近 N 轮 + 用户画像 | 默认中等任务 |
| LLM 生成 | 按隐私与任务动态摘要 | 复杂,多一次调用 |
提示词里标注来源([FROM_MAIN_AGENT] / [FROM_USER] / [TOOL_RESULT]),降低注入与角色混淆。HITL(Human-in-the-Loop,人在回路)要配超时与默认保守策略,并把批准/拒绝记入可检索案例或训练数据。
MCP:互操作标准与三个工程坑
MCP(Model Context Protocol) 是工具与数据源的开放协议:MCP Server 暴露工具/资源/提示模板,Client(IDE / Agent 框架)用统一方式发现与调用。价值是一次开发、多客户端复用。
实践中的三个坑:
- 同步请求-响应为主——会话内可有 progress / notification,但跨会话唤醒、多事件源离线触发,仍要在协议之上自建事件队列与运行时。
- 上下文开销——少量 MCP 服务器即可引入数万 token 的工具定义。缓解:默认只注入名称索引,按需拉 schema(与 Skills / 工具搜索同构);有实测可显著降低 MCP 相关 token。
- 信任模型——接入第三方 server ≈ 把不可信文本注入上下文,并可能交出凭证。风险包括:描述投毒、供应链篡改、同名工具遮蔽(shadowing)、凭证滥用。缓解:审计 description、锁定版本、最小权限凭证、运行时 Sidecar。
事件驱动的异步 Agent
同步 ReAct 假设:工具调用的下一条必须是结果。真实助理场景则是:用户随时插话、邮件随时到、电话中途要 OTP。
把输入统一建模为结构化事件(来源、渠道、内容、优先级、关联上下文),经队列进入同一条轨迹。处理策略按紧急度分三档:
| 策略 | 何时 | 行为 |
|---|---|---|
| 取消式 | 用户「停止」、高优告警 | 中止当前推理/同步工具,清空队列,合并事件后重推 LLM |
| 队列式 | 常规补充、工具结果 | 不打断,完成当前步后批量追加 |
| 并行式 | 与主任务无关的轻量查询 | 旁路会话快答,结果标记并行写回主轨迹 |
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Any
class Urgency(Enum):
CANCEL = "cancel" # 立即中止当前步
QUEUE = "queue" # 当前步结束后批量并入
PARALLEL = "parallel"
@dataclass
class Event:
"""统一事件:来源 + 载荷 + 紧急度,进入轨迹前先结构化。"""
source: str
channel: str
payload: dict[str, Any]
urgency: Urgency
def classify_urgency(event: Event) -> Urgency:
"""规则优先;语义模糊时再交给小模型分类(此处仅示意规则层)。"""
if event.source in {"user.interrupt", "supervisor.instruction"}:
return Urgency.CANCEL
if event.payload.get("priority") == "high" and event.channel == "pager":
return Urgency.CANCEL
if event.payload.get("independent") is True:
return Urgency.PARALLEL
return Urgency.QUEUE
def merge_into_trajectory(trajectory: list[dict], events: list[Event]) -> list[dict]:
"""把一批事件追加为 user/tool 风格消息,供下一轮 LLM 一并看见。"""
for e in events:
trajectory.append({
"role": "user",
"content": f"[event source={e.source} channel={e.channel}] {e.payload}",
})
return trajectory
if __name__ == "__main__":
e = Event("user.interrupt", "im", {"text": "停止,我说错了"}, Urgency.QUEUE)
print(classify_urgency(e))
# 典型输出:Urgency.CANCEL
事件触发工具三类:
- 定时器——一次性或 cron 循环;无推送的外部系统用轮询补洞
- 后台监控——盯 shell 新输出或关键词,避免「干等到底」或「空转刷屏」
- 外部 Channel——邮件/Webhook/电话中途事件即时推送(对比纯 Heartbeat 的分钟级延迟)
用户沟通工具:当渠道不再是「单一 session 回显 assistant 文本」时,reply_to_user / 卡片 / 推送成为显式动作。通知对象是审批者 → 归协作;是终端用户 → 归用户沟通。
常驻异步 Agent 往往还要 虚拟身份 + 隔离环境(独立邮箱/浏览器档案/工作区),避免直接持有用户全部账号;与用户真实账号交互时用 HITL 登录,会话令牌限时复用。
主动工具发现
工具上百时,平铺 schema 既贵又干扰决策。主动工具发现:上下文默认只有索引(名称、短描述、分类),模型通过 tool_search 类工具按需加载完整定义。
层次化目录帮助定位,例如:搜索 / 读取 / 解析 / 结构化查询。业界公开数字量级:按需检索可将工具使用准确率从约五成提升到七成以上(视基准与模型而异)。这与第 2 章「前缀只留简述、完整 schema 追加轨迹」一致,并要求模型训练中见过「对话中途出现工具定义」的模式。
本篇收束
- 五类工具覆盖感知、改变世界、协作、被世界唤醒、主动触达用户。
- 形态上 Skill+执行器与专用工具按复杂度/变更/模型能力选型;粒度按语义合并。
- 描述写触发与边界,参数保真禁止静默改写;执行侧多层约束与验证。
- MCP 解决互操作,不自动解决异步、token 税与供应链信任。
- 异步靠事件统一建模 + 紧急度策略;规模化靠主动发现而非无限加长 tools 字段。
下一篇进入第 5 章:Coding Agent 与代码元能力——为什么通用 Agent 常以编码运行时为内核,以及代码如何超出「写程序」本身。
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:04. 工具设计:手脚的粒度、描述与异步
本文链接:https://www.sshipanoo.com/blog/ai/ai-agent-indepth/04-工具设计/
本文最后一次更新为 天前,文章中的某些内容可能已过时!