第 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. 架构图 / 流程图
6.1 Tool 调用链路
6.2 工具设计闭环
6.3 工具风险分级
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 工具名过于抽象
execute、query、process 会增加模型误用概率。
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。
核心结论:
- Tool 是 Agent 与外部环境交互的接口。
- 工具定义本身就是给模型看的 Prompt,需要认真设计。
- 好工具应高内聚、命名清晰、Schema 明确、返回稳定、错误可恢复。
- 工具结果必须 token-efficient,避免污染上下文。
- 企业工具必须有权限、审计、风险分级和测试。
一句话总结:
一个 Agent 能否可靠行动,往往不取决于它有没有工具,而取决于工具是否为 Agent 正确设计。
12. 实践任务
任务 1:重构坏工具
把下面工具:
execute(action, params)
拆成 5 个高内聚工具,并写出每个工具的描述。
任务 2:设计 search_code 工具
定义:
- name;
- description;
- input schema;
- output schema;
- 错误码;
- 使用示例。
任务 3:实现 ToolRegistry
在 Java 中实现一个简单 ToolRegistry,支持注册、查询、按权限列出工具。
任务 4:工具误用测试
让 Agent 使用你设计的工具完成一个任务,记录它在哪些地方误用工具,然后改进工具描述和 Schema。