工具与行动
什么是工具?
工具是让 Agent 执行真实操作的接口。没有工具,Agent 只能生成文本;有了工具,Agent 可以:
- 读写文件
- 执行代码
- 调用 API
- 搜索信息
- 与数据库交互
- 发送消息
函数调用
现代 LLM 支持工具使用,这是 Agent 行动能力的基础。在 Anthropic 的原始 Messages API 中,流程如下:
1. 定义工具 schema
{
"name": "read_file",
"description": "Read the contents of a file",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file"
}
},
"required": ["path"]
}
}
2. 模型发出工具请求
{
"type": "tool_use",
"id": "toolu_123",
"name": "read_file",
"input": {"path": "config.json"}
}
3. 执行工具并返回结果
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": "{\"name\": \"my-app\", ...}"
}边界:原始 tool blocks 与 SDK 消息
原始 API 使用 tool_use 和 tool_result content block。Claude Agent SDK 在其上暴露更高层的
流式消息,例如 tool_use_summary 和 result。如果你需要精确的工具输入或输出,请检查
stream_event 或底层 message content block。tool_use_summary 只包含自然语言摘要和工具调用 ID,
不是完整审计记录。
import { query } from "@anthropic-ai/claude-agent-sdk";
const response = query({
prompt: "Read package.json and tell me the project name",
options: {
// 控制 Agent 能看见哪些工具
tools: ["Read", "Glob"],
// 自动批准这些可见工具,无需权限提示
allowedTools: ["Read", "Glob"],
// 或使用预设 + 覆盖
// tools: { type: 'preset', preset: 'claude_code' },
// disallowedTools: ["Write", "Bash"]
},
});
for await (const message of response) {
if (message.type === "tool_use_summary") {
console.log(`Tool run: ${message.summary}`);
}
if (message.type === "result" && message.subtype === "success") {
console.log(`Result: ${message.result.slice(0, 100)}...`);
}
}piPi 等效写法— 工具暴露小写,事件靠 subscribe
// tool-calling.ts
import { createAgentSession } from "@earendil-works/pi-coding-agent";
// 工具暴露使用小写名;内置工具随 session 一起暴露。
const { session } = await createAgentSession({ tools: ["read", "glob"] });
let lastText = "";
const unsubscribe = session.subscribe((event) => {
if (event.type === "tool_execution_start") {
console.log(`Tool run: ${event.toolName}`);
} else if (event.type === "agent_end") {
for (const m of event.messages) {
if (m.role !== "assistant" || !Array.isArray(m.content)) continue;
for (const b of m.content) if (b.type === "text") lastText = b.text;
}
}
});
try {
await session.prompt("Read package.json and tell me the project name");
console.log(`Result: ${lastText.slice(0, 100)}...`);
} finally {
unsubscribe();
}核心差异
Pi 的 tools 使用小写(["read", "glob"]);Claude Agent SDK 的 tools 使用
PascalCase(["Read", "Glob"]),allowedTools 只控制自动批准。
tool_execution_start / tool_execution_end(按调用)vs tool_use_summary(聚合摘要)。
agent_end 事件携带最终 assistant 消息——没有专用的 result 事件。
工具分类
下面的小写调用只是概念草图,不是 SDK 函数。SDK 内建工具使用 Read、Write、Bash、Grep、
Glob 等名称;自定义与 MCP 工具需要真实实现或服务器。
文件工具
| 工具 | 功能 |
|---|---|
read_file | 读取文件内容 |
write_file | 创建或覆盖文件 |
edit_file | 编辑文件部分 |
list_directory | 列出目录内容 |
Shell 工具
bash({
command: "npm install",
working_dir: "/project",
timeout: 60,
});搜索工具
grep- 文本搜索glob- 文件名匹配semantic_search- 语义搜索web_search- 网页搜索
API 工具
- HTTP 请求 (GET/POST/PUT/DELETE)
- 数据库查询
- 第三方服务集成(GitHub、Slack 等)
工具设计原则
原子性
每个工具只做一件事:
// 不好:职责过多
create_and_run_test(file, test_content, run_args);
// 好:拆分为原子操作
write_file(path, content);
run_command("pytest " + path);清晰的描述
描述需要足够详细,让 LLM 知道何时使用:
// 不好
const badDescription = "Search files";
// 好
const goodDescription = `Search for files by name pattern using glob syntax.
Use this when you need to find files matching a pattern like '*.py'
or 'src/**/*.ts'. Returns a list of matching file paths.`;合理的参数
{
"name": "edit_file",
"parameters": {
"path": "string (required) - File to edit",
"old_string": "string (required) - Exact text to replace",
"new_string": "string (required) - Replacement text",
"expected_count": "number (optional) - Expected match count"
}
}有帮助的错误信息
// 不好
{"error": "Failed"}
// 好
{
"error": "FileNotFoundError",
"message": "File 'config.json' not found. Available files in current directory: package.json, tsconfig.json, src/"
}declare function isSafeRead(tool: string, input: unknown): Promise<boolean>;
const response = query({
prompt: "Clean up the temp directory",
options: {
tools: ["Read", "Bash", "Glob"],
allowedTools: ["Read", "Glob"],
// 自定义权限门控
canUseTool: async (toolName, input) => {
// 只读也要校验工作区与敏感路径
if (["Read", "Glob", "Grep"].includes(toolName) && await isSafeRead(toolName, input)) {
return { behavior: "allow" };
}
// 这个只读工作流不暴露通用 shell 逃生口
if (toolName === "Bash") {
return { behavior: "deny", message: "Shell access is not permitted" };
}
// 其他情况一律拒绝,并给出明确原因
return {
behavior: "deny",
message: `${toolName} is not permitted in this workflow`
};
}
}
});piPi 等效写法— 策略写在工具边界,没有全局 hook
// can-use-tool.ts
import { createAgentSession, defineTool } from "@earendil-works/pi-coding-agent";
import { Type } from "@earendil-works/pi-ai";
import { execFile } from "node:child_process";
// 不要过滤 shell 字符串,应把意图结构化。此工具只能删除应用
// 临时目录中的普通文件名。
const cleanTemp = defineTool({
name: "clean_temp",
label: "Clean temp files",
description: "Delete named files from the application temp directory.",
parameters: Type.Object({ files: Type.Array(Type.String()) }),
async execute(_id, { files }) {
if (files.some((file) => !/^[A-Za-z0-9._-]+$/.test(file))) {
return {
content: [{ type: "text", text: "DENIED: filenames only" }],
details: null,
isError: true,
};
}
const result = await new Promise<{ text: string; isError: boolean }>((res) =>
execFile("rm", ["-f", "--", ...files], { cwd: "./tmp" }, (error, stdout, stderr) =>
res({ text: error?.message || stderr || stdout || "Done", isError: !!error })
)
);
return {
content: [{ type: "text", text: result.text }],
details: null,
isError: result.isError,
};
},
});
const { session } = await createAgentSession({
customTools: [cleanTemp],
tools: ["clean_temp"], // 读取工具需先有工作区 + 敏感路径策略
});
await session.prompt("Clean up the temp directory");核心差异
工具边界(自定义工具的 execute())vs 回调钩子(canUseTool)。
不暴露内置(如 bash)只暴露经过门控的封装版本 vs 白名单 + 每次调用做决策回调。
Type.Object(…) vs JSON Schema input_schema。SDK 洞察:两种权限行为
canUseTool 返回两种决策之一:allow(自动执行,可选 updatedInput)或 deny(带 message
阻止)。 它恰好在本应弹出权限提示时触发,因此「询问」需通过权限规则或 PreToolUse hook 配置——将在
P11(人机协作)中继续展开。
模型上下文协议 (MCP)
MCP 是 Anthropic 提出的工具标准化协议,让不同的 Agent 可以共享工具:
// MCP tools/list 返回项(服务器身份位于客户端配置中)
{
"name": "github_create_issue",
"description": "Create a GitHub issue",
"inputSchema": {
"type": "object",
"properties": {
"repo": { "type": "string" },
"title": { "type": "string" },
"body": { "type": "string" },
},
},
}MCP 优势:
- 可复用的工具
- 跨 Agent 兼容性
- 标准化接口
- 丰富的生态系统
安全考虑
- 沙箱执行 - 限制工具运行环境
- 权限控制 - 区分只读与可写工具
- 确认机制 - 危险操作需要用户确认
- 审计日志 - 记录操作者、工具、决策和脱敏证据,并设置保留期限
- 速率限制 - 防止失控循环
生产落差
本课的工具定义使用简单的 schema。生产环境的工具需要输入清理、超时处理、重试逻辑和速率限制——尤其是调用外部 API 的工具。
在公司落地 一个范围收敛的工具目录,是 agent 最安全的第一个落地面:
- 第一个试点选只读的,再谈写入。 一个「Slack 归档检索 + JIRA 关联」的 agent,配上
tools: ["search_slack", "search_jira"],再把这些只读工具放进allowedTools自动批准,可以很快上线,风险半径也足够小,还能直观地把「动作空间」这套思路讲给安全审查听。 - 把
tools当成服务契约,把allowedTools当成预批准列表。 每个工具的 scope、限流、副作用,都写进你记录 REST 接口的同一份文档里——安全 review 一定会问,而且这份规范也正是 LLM 的 API 文档。 - 第一个月不要把写操作放进
allowedTools。 让权限规则、宿主审批或canUseTool的拒绝路径触发审核。每一次审批弹窗,都是日后收紧策略时有价值的标注样本。 - 把
tool_use_summary用作可读遥测,不要当成审计。 应从工具块或 hooks 构建受保护、 已脱敏的审计记录,包含操作者、决策和时间戳。
转型为 agent 工程师 动作空间设计是核心技能:
- 资深 agent 工程师做设计时,绝大多数时间花在有哪些工具、它们的输入长什么样、各自持有多大权限,而不是 prompt 上。一个干净的 6 个工具目录,通常比一个臃肿的 30 个工具目录更可靠。
- 面试官会追问:「为什么是这一个工具,而不是拆成三个?schema 是怎么定的?工具超时了怎么办?LLM 拿到一个 4xx 形态的错误能否恢复?」 这些都得有明确答案。
- 一个成熟的工具设计应当包含:带类型的 schema、
canUseTool策略、一个 LLM 能从中恢复的错误契约,以及某个真实工作流的工具调用日志。这比多个没人使用的 MCP server 更能说明设计质量。
动手试试工具权限门控
为排查客户事故的支持团队构建权限受控的 Agent。
- 创建一个 Agent,配置
tools: ["Read", "Bash", "Glob"]和allowedTools: ["Read", "Glob"] - 将动作分为只读、可逆写入、不可逆,并定义各级由谁审批
- 实现
canUseTool记录每次请求,并以rm、sudo为例阻止不可逆操作 - 让它检查
.ts文件定位已报告问题,再测试一个安全请求和一个受阻请求 - 检查消息流,统计误拦截与误放行,并判断能否安全试点
关卡:P2 完成——工具调用已记录,canUseTool 阻止危险操作,在流中观察到多工具序列。