第 9 章:Tool Engineering

本章导读

  • 核心问题:如何把普通 API 设计成模型真正能稳定使用的 Tool。
  • 关键词:Tool Definition、ToolRegistry、Schema、ToolResult、权限、错误恢复
  • 学习产出:设计 CI 失败分析的只读工具集,例如读取日志、Git Diff、代码片段和项目规则。

1. 本章要解决什么问题

前面几章讨论 Context。

但 Agent 不能只“看见”信息,它还必须能“行动”。

这就需要 Tool。

没有工具的 Agent,本质上只能生成文本。

有了工具,Agent 才能:

  • 搜索代码;
  • 读取文件;
  • 修改文件;
  • 运行测试;
  • 查询数据库;
  • 调用 API;
  • 创建工单;
  • 操作 Git;
  • 访问企业系统。

但是,工具不是简单把一个函数暴露给模型。

很多 Agent 系统失败,是因为工具设计太差:

工具名太抽象
参数含义不清
返回结果太长
错误信息不可恢复
权限边界不明确
一个工具做太多事
工具描述面向程序员,不面向模型

Anthropic 在工具设计文章中强调:工具接口会显著影响 Agent 的表现。

本章核心观点是:

Tool 是 Agent 与外部世界的交互接口。工具设计的质量,决定了 Agent 能否可靠行动。

本章回答:

  • 什么是 Agent Tool;
  • 工具与普通 API 有什么区别;
  • 好工具应该如何命名、描述、设计输入和输出;
  • 为什么工具返回值要面向模型消费;
  • 如何设计 Java ToolRegistry;
  • 如何测试和治理工具。

2. 对应 Anthropic 文章

本章主要对应:

  • Writing effective tools for agents — with agents

Anthropic 的重要观点是:

Tool definitions and specifications deserve the same care as prompts.

也就是说,工具定义本身也是一种 Prompt。

模型能否正确使用工具,取决于:

  • 工具名;
  • 描述;
  • 参数 Schema;
  • 返回格式;
  • 错误信息;
  • 示例;
  • 约束。

这就是 Tool Engineering。

3. Tool 与普通 API 的区别

普通 API 面向程序员。

Agent Tool 面向模型。

这带来几个差异。

3.1 API 可以依赖隐含知识,Tool 不行

程序员看到 query(),可能会去读文档。

模型看到 query(),可能不知道它到底查什么。

工具名应该表达意图:

差:query()
好:search_code_symbols()
好:get_ci_failure_summary()
好:read_file_range()

3.2 API 返回可以复杂,Tool 返回要可推理

普通 API 可以返回庞大 JSON。

但工具返回给模型时,应该清晰、结构化、可操作。

例如数据库查询工具,不应该直接返回 10 万行。

应该返回:

rowCount
sampleRows
aggregates
warnings
nextPageToken

3.3 API 错误可以给程序处理,Tool 错误要帮助模型恢复

普通 API 错误:

{"error":"Bad Request"}

对 Agent 没帮助。

更好的 Tool 错误:

{
  "errorCode": "FILE_NOT_FOUND",
  "message": "File src/main/java/OrderService.java does not exist.",
  "suggestions": [
    "Call search_files with pattern '*OrderService*'.",
    "Check repository root path."
  ]
}

工具错误应该告诉 Agent 下一步怎么恢复。

4. 好工具的设计原则

4.1 工具高内聚

一个工具应该完成一个清晰动作。

坏例子:

project_tool(action="search_or_read_or_write_or_test", payload=...)

好例子:

search_code
read_file
apply_patch
run_tests
get_git_diff

高内聚工具更容易被模型理解和组合。

4.2 工具命名表达意图

工具名应该是动词 + 对象。

例如:

read_file
search_code
run_test_command
get_pull_request_diff
create_jira_issue
summarize_ci_log

避免:

execute
handle
process
query
call_api

除非上下文非常明确。

4.3 工具描述要告诉模型何时使用

工具描述不只是解释函数做什么,还要解释:

什么时候用
什么时候不用
输入怎么填
输出是什么
失败怎么办

例如:

read_file_range:
Reads a specific line range from a text file in the current repository.
Use this when you know the file path and only need part of the file.
Do not use this to search for files; use search_code first.
Returns the requested lines with line numbers.

这比 “Read file” 更有效。

4.4 输入 Schema 要明确

工具参数应避免歧义。

坏例子:

{
  "path": "string",
  "options": "object"
}

好例子:

{
  "filePath": "src/main/java/com/example/OrderService.java",
  "startLine": 40,
  "endLine": 120
}

输入 Schema 应包含:

  • 字段名;
  • 类型;
  • 是否必填;
  • 取值范围;
  • 示例;
  • 约束。

4.5 输出 Schema 要稳定

模型需要根据工具返回结果继续推理。

如果输出格式不稳定,模型就很难可靠使用。

例如 search_code 返回:

{
  "matches": [
    {
      "filePath": "src/main/java/.../OrderService.java",
      "line": 42,
      "snippet": "public Order createOrder(...)"
    }
  ],
  "totalMatches": 12,
  "truncated": false
}

比返回一大段文本更可控。

4.6 返回结果要 token-efficient

工具结果也是上下文。

如果每次工具调用都返回大量原始数据,会迅速污染上下文。

因此工具应该支持:

  • 分页;
  • limit;
  • range;
  • summary;
  • fields;
  • filtering;
  • references。

例如读取日志工具应支持:

read_log_segment(buildId, startLine, endLine)
find_error_blocks(buildId, pattern)
summarize_test_failures(buildId)

而不是只提供 read_full_log()

4.7 工具要有权限边界

危险工具必须受控。

工具可以按风险分级:

风险 示例 控制方式
只读 read_file、search_code 记录审计
低风险写 apply_patch 到临时分支 diff 审核
中风险 run_command 命令白名单、超时
高风险 deploy、delete、db_update 人工审批

模型不能决定自己是否有权限。

权限必须由 ToolExecutor 强制执行。

4.8 工具要可测试

工具需要像业务代码一样测试。

测试内容包括:

  • 参数校验;
  • 成功路径;
  • 错误路径;
  • 返回格式;
  • 大结果截断;
  • 权限拒绝;
  • 模型是否能根据描述正确使用。

Anthropic 的文章也强调可以用 Agent 自己帮助测试工具:让模型尝试使用工具完成任务,观察它在哪些地方误用工具,再改进工具定义。

5. Tool Definition 模型

一个完整 Tool Definition 应包含:

name
description
inputSchema
outputSchema
examples
permissions
riskLevel
timeout
idempotent
rateLimit

示例:

{
  "name": "search_code",
  "description": "Search source files in the current repository. Use this to find classes, methods, symbols, or text before reading files. Returns file paths, line numbers, and snippets. Do not use this for shell commands.",
  "inputSchema": {
    "query": "string",
    "filePattern": "string optional",
    "maxResults": "integer default 20"
  },
  "outputSchema": {
    "matches": "array of {filePath, line, snippet}",
    "totalMatches": "integer",
    "truncated": "boolean"
  },
  "riskLevel": "READ_ONLY",
  "idempotent": true
}

6. 架构图 / 流程图

Tool Registry 与工具执行链路
图 9-1 Tool Registry 与工具执行链路 这张图说明 Tool 从注册、权限检查、执行、结果标准化到上下文压缩的完整路径。 Mermaid 源文件

6.1 Tool 调用链路

Tool 调用链路
图 9-2 Tool 调用链路 Mermaid 源文件

6.2 工具设计闭环

工具设计闭环
图 9-3 工具设计闭环 Mermaid 源文件

6.3 工具风险分级

工具风险分级
图 9-4 工具风险分级 Mermaid 源文件

7. Java / Spring Boot 落地方案

7.1 AgentTool

public interface AgentTool<I, O> {
    ToolDefinition definition();
    O execute(I input, ToolExecutionContext context);
}

7.2 ToolDefinition

public record ToolDefinition(
    String name,
    String description,
    JsonSchema inputSchema,
    JsonSchema outputSchema,
    ToolRiskLevel riskLevel,
    boolean idempotent,
    Duration timeout,
    Set<String> requiredPermissions
) {}

7.3 ToolResult

public record ToolResult(
    String toolName,
    boolean success,
    Object data,
    ToolError error,
    int tokenEstimate,
    Map<String, Object> metadata
) {}

错误:

public record ToolError(
    String code,
    String message,
    List<String> suggestions,
    boolean retryable
) {}

7.4 ToolRegistry

public interface ToolRegistry {
    void register(AgentTool<?, ?> tool);
    Optional<AgentTool<?, ?>> findByName(String name);
    List<ToolDefinition> listAvailable(ToolSelectionRequest request);
}

工具不应全部暴露给模型。

listAvailable 应根据任务类型、权限、阶段选择工具。

7.5 ToolExecutor

public class ToolExecutor {

    private final ToolRegistry registry;
    private final ToolPermissionService permissionService;
    private final ToolAuditLogger auditLogger;

    public ToolResult execute(ToolCall call, ToolExecutionContext context) {
        AgentTool tool = registry.findByName(call.name())
            .orElseThrow(() -> new ToolNotFoundException(call.name()));

        permissionService.check(context, tool.definition());

        try {
            Object result = tool.execute(call.input(), context);
            ToolResult toolResult = normalize(tool.definition(), result);
            auditLogger.success(call, toolResult, context);
            return toolResult;
        } catch (Exception ex) {
            ToolResult error = toToolError(call, ex);
            auditLogger.failure(call, error, context);
            return error;
        }
    }
}

7.6 示例:read_file_range

public class ReadFileRangeTool implements AgentTool<ReadFileRangeInput, ReadFileRangeOutput> {

    @Override
    public ToolDefinition definition() {
        return new ToolDefinition(
            "read_file_range",
            "Reads a line range from a file in the current repository. Use after you know the file path. Returns lines with line numbers.",
            JsonSchema.of(ReadFileRangeInput.class),
            JsonSchema.of(ReadFileRangeOutput.class),
            ToolRiskLevel.READ_ONLY,
            true,
            Duration.ofSeconds(5),
            Set.of("repo:read")
        );
    }

    @Override
    public ReadFileRangeOutput execute(ReadFileRangeInput input, ToolExecutionContext context) {
        // validate path, enforce repo boundary, read lines
        return null;
    }
}

输入输出:

public record ReadFileRangeInput(
    String filePath,
    int startLine,
    int endLine
) {}

public record ReadFileRangeOutput(
    String filePath,
    int startLine,
    int endLine,
    List<NumberedLine> lines,
    boolean truncated
) {}

8. 企业研发 Agent 的最小工具集

建议 MVP 先提供只读工具:

工具 作用 风险
search_code 搜索代码 只读
read_file_range 读取文件片段 只读
get_git_diff 获取当前变更 只读
get_recent_commits 获取最近提交 只读
summarize_ci_failure 摘要 CI 失败 只读
run_test_command 运行白名单测试命令 中风险

等只读分析稳定后,再引入写工具:

工具 控制
apply_patch 只允许工作区,记录 diff
revert_patch 可回滚
create_pull_request 需要人工确认
update_jira_issue 需要权限

9. 常见误区

9.1 工具名过于抽象

executequeryprocess 会增加模型误用概率。

9.2 返回完整原始结果

大结果应摘要、分页或保留引用。

9.3 错误信息不可恢复

工具错误应告诉 Agent 如何修正参数或选择下一步。

9.4 工具过大

一个工具承担搜索、读取、修改、测试,会让边界不清。

9.5 只测试函数,不测试模型使用

工具不仅要单元测试,还要测试 Agent 能否根据描述正确调用。

9.6 权限只写在 Prompt 里

权限必须由 ToolExecutor 强制执行,不能只靠模型遵守。

10. 贯穿案例:CI 失败分析 Agent

CI 失败分析 Agent 的第一版工具集应保持只读。

推荐工具包括:

Tool 作用 风险
summarize_ci_failure 获取失败摘要 只读
read_ci_log_segment 读取日志片段 只读
get_git_diff 获取 PR Diff 只读
get_recent_commits 获取最近提交 只读
search_code 搜索代码 只读
read_file_range 读取文件片段 只读
get_project_instructions 读取 AGENTS.md 只读

工具命名应表达意图。不要设计一个含糊的 execute(action, payload)

例如 read_ci_log_segment 的返回不应是完整日志,而应包含:

buildId
startLine
endLine
lines
truncated
errorBlocks

工具错误也要可恢复。例如文件不存在时,应建议 Agent 先调用 search_code

这个案例说明:Tool Engineering 的重点不是“暴露 API”,而是为模型设计清晰、可组合、可恢复、token-efficient 的行动接口。

11. 本章小结

本章讨论了 Tool Engineering。

核心结论:

  1. Tool 是 Agent 与外部环境交互的接口。
  2. 工具定义本身就是给模型看的 Prompt,需要认真设计。
  3. 好工具应高内聚、命名清晰、Schema 明确、返回稳定、错误可恢复。
  4. 工具结果必须 token-efficient,避免污染上下文。
  5. 企业工具必须有权限、审计、风险分级和测试。

一句话总结:

一个 Agent 能否可靠行动,往往不取决于它有没有工具,而取决于工具是否为 Agent 正确设计。

12. 实践任务

任务 1:重构坏工具

把下面工具:

execute(action, params)

拆成 5 个高内聚工具,并写出每个工具的描述。

任务 2:设计 search_code 工具

定义:

  • name;
  • description;
  • input schema;
  • output schema;
  • 错误码;
  • 使用示例。

任务 3:实现 ToolRegistry

在 Java 中实现一个简单 ToolRegistry,支持注册、查询、按权限列出工具。

任务 4:工具误用测试

让 Agent 使用你设计的工具完成一个任务,记录它在哪些地方误用工具,然后改进工具描述和 Schema。

results matching ""

    No results matching ""