P220分钟

工具与行动

工具是 Agent 与外部世界交互的方式。学习如何设计、实现和使用 Agent 工具。
SDK Focustools configcanUseToolAskUserQuestiontool_use_summaryresult

什么是工具?

工具是让 Agent 执行真实操作的接口。没有工具,Agent 只能生成文本;有了工具,Agent 可以:

  • 读写文件
  • 执行代码
  • 调用 API
  • 搜索信息
  • 与数据库交互
  • 发送消息

函数调用

现代 LLM 支持工具使用,这是 Agent 行动能力的基础。在 Anthropic 的原始 Messages API 中,流程如下:

text
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_usetool_result content block。Claude Agent SDK 在其上暴露更高层的 流式消息,例如 tool_use_summaryresult。如果你需要精确的工具输入或输出,请检查 stream_event 或底层 message content block。tool_use_summary 只包含自然语言摘要和工具调用 ID, 不是完整审计记录。

tool-calling.ts
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
typescript
// 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 内建工具使用 ReadWriteBashGrepGlob 等名称;自定义与 MCP 工具需要真实实现或服务器。

文件工具

工具功能
read_file读取文件内容
write_file创建或覆盖文件
edit_file编辑文件部分
list_directory列出目录内容

Shell 工具

typescript
bash({
  command: "npm install",
  working_dir: "/project",
  timeout: 60,
});

搜索工具

  • grep - 文本搜索
  • glob - 文件名匹配
  • semantic_search - 语义搜索
  • web_search - 网页搜索

API 工具

  • HTTP 请求 (GET/POST/PUT/DELETE)
  • 数据库查询
  • 第三方服务集成(GitHub、Slack 等)

工具设计原则

原子性

每个工具只做一件事:

typescript
// 不好:职责过多
create_and_run_test(file, test_content, run_args);

// 好:拆分为原子操作
write_file(path, content);
run_command("pytest " + path);

清晰的描述

描述需要足够详细,让 LLM 知道何时使用:

typescript
// 不好
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.`;

合理的参数

json
{
  "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"
  }
}

有帮助的错误信息

jsonc
// 不好
{"error": "Failed"}

// 好
{
  "error": "FileNotFoundError",
  "message": "File 'config.json' not found. Available files in current directory: package.json, tsconfig.json, src/"
}
can-use-tool.ts
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
typescript
// 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 白名单 + 每次调用做决策回调。

Schema: TypeBox Type.Object(…) vs JSON Schema input_schema

SDK 洞察:两种权限行为

canUseTool 返回两种决策之一:allow(自动执行,可选 updatedInput)或 deny(带 message 阻止)。 它恰好在本应弹出权限提示时触发,因此「询问」需通过权限规则或 PreToolUse hook 配置——将在 P11(人机协作)中继续展开。

模型上下文协议 (MCP)

MCP 是 Anthropic 提出的工具标准化协议,让不同的 Agent 可以共享工具:

jsonc
// 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。

  1. 创建一个 Agent,配置 tools: ["Read", "Bash", "Glob"]allowedTools: ["Read", "Glob"]
  2. 将动作分为只读、可逆写入、不可逆,并定义各级由谁审批
  3. 实现 canUseTool 记录每次请求,并以 rmsudo 为例阻止不可逆操作
  4. 让它检查 .ts 文件定位已报告问题,再测试一个安全请求和一个受阻请求
  5. 检查消息流,统计误拦截与误放行,并判断能否安全试点

关卡:P2 完成——工具调用已记录,canUseTool 阻止危险操作,在流中观察到多工具序列。

登录后可保存课程进度、解锁闪卡,并从上次阅读的位置继续