OpenClaw Java 工具系统详解:从工具注册到大模型 Function Calling
本文以当前 OpenClaw Java 工程为基础,介绍 Agent 工具系统的功能、组成、注入时机、调用链路和设计思路。
一、前言
在 Agent 系统中,大模型本身只能生成文本。它不能直接读取文件、执行命令、访问网页或操作浏览器。
要让模型具备这些能力,需要引入 Tool Calling(工具调用) 机制:
- Java 程序实现工具的具体功能;
- Java 程序把工具名称、描述和参数 Schema 发送给大模型;
- 大模型根据用户问题决定是否调用工具;
- Java 程序执行模型返回的工具调用;
- Java 程序把工具执行结果再次发送给大模型;
- 大模型结合工具结果生成最终答案。
当前工程的工具系统采用了“统一接口 + 注册中心 + 策略过滤 + Provider 适配”的设计,既支持内置工具,也为插件工具和沙箱策略预留了扩展点。
二、工具系统的整体架构
核心类位于 openclaw-agent 模块中:
AgentTool
├── ExecTool
├── FileTools
├── WebSearchTool
├── WebFetchTool
├── BrowserTool
├── MemoryTool
├── MessageTool
└── 其他内置工具
ToolRegistry
├── 注册内置工具
├── 注册插件工具
├── 查询工具
├── 按策略过滤
└── 转换为模型工具定义
AgentRunner
├── 注入插件工具
├── 应用沙箱策略
├── 构造 ChatRequest.tools
├── 调用模型
├── 执行工具调用
└── 进入下一轮模型调用
OpenAICompatibleProvider
└── 将工具转换为 OpenAI Function Calling 格式
工具的统一抽象定义在 AgentTool.java。
三、统一工具接口设计
所有工具都实现 AgentTool 接口。接口要求每个工具提供四类能力:
String getName();
String getDescription();
JsonNode getParameterSchema();
CompletableFuture<ToolResult> execute(ToolContext context);
1. getName():工具唯一名称
工具名称是模型返回 Tool Call 时使用的标识。例如:
exec
web_search
web_fetch
browser
sessions_spawn
工具名称必须唯一。ToolRegistry 使用工具名称作为 Map 的 Key,重复注册时可能覆盖已有工具,插件工具则会主动跳过冲突名称。
2. getDescription():告诉模型什么时候使用工具
描述不是给 Java 开发者看的,而是模型决策的重要依据。例如 web_search 的描述会告诉模型它可以搜索互联网,read_file 的描述会说明它可以读取文件。
描述应当包含:
- 工具能做什么;
- 适合在什么场景使用;
- 输入参数的大致含义;
- 使用时需要注意的限制。
3. getParameterSchema():定义工具参数
工具参数使用 JSON Schema 描述。例如文件读取工具要求一个 path 参数:
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute file path"
}
},
"required": ["path"]
}
模型会依据这个 Schema 生成参数,Java 端再从 ToolContext.parameters 中读取参数。
4. execute():实际执行逻辑
工具执行接口返回 CompletableFuture<ToolResult>,说明工具系统从设计上支持异步执行。
ToolResult 统一表示成功、失败、文本输出、结构化数据和多模态内容,避免每个工具定义不同的返回格式。
四、当前实现的工具
4.1 文件和命令工具
文件工具集中在 FileTools.java,命令工具位于 ExecTool.java。
| 工具名 | 功能 | 实现位置 |
|---|---|---|
exec | 执行 Shell 命令并返回标准输出和错误输出 | ExecTool |
read_file | 读取指定文件内容 | FileTools.readFile() |
write_file | 写入文件内容并按需创建父目录 | FileTools.writeFile() |
list_dir | 列出目录下的文件和子目录 | FileTools.listDir() |
grep_search | 在目录范围内搜索文本 | FileTools.grepSearch() |
这些工具涉及本地文件系统和进程执行,因此必须结合沙箱使用。工具本身负责业务执行,是否允许执行则由沙箱策略进一步决定。
4.2 网络和网页工具
网页工具位于:
两者职责不同:
| 工具名 | 功能 |
|---|---|
web_search | 根据关键词搜索网页,返回搜索结果摘要或链接 |
web_fetch | 根据 URL 抓取网页,并提取可读文本 |
典型调用顺序是:
用户询问实时信息
→ web_search
→ 从搜索结果中选择 URL
→ web_fetch
→ 根据网页内容回答
当前真实运行日志中,模型实际使用过的就是 web_search 和 web_fetch。
4.3 会话和 Agent 工具
会话工具位于 builtin 目录下:
| 工具名 | 功能 |
|---|---|
session_status | 查看当前会话状态、模型和用量 |
sessions_list | 查询会话列表 |
sessions_history | 查询指定会话历史 |
sessions_send | 向另一个会话发送消息 |
sessions_spawn | 创建后台子 Agent |
agents_list | 查询可以被使用的 Agent |
这些工具使 Agent 具备“管理其他 Agent 或会话”的能力,是多 Agent 协作的基础。
4.4 Gateway 和消息工具
相关实现包括:
gateway 是一个顶层工具,内部通过 action 区分重启、配置读取、配置更新等操作;message 也是一个顶层工具,内部可以处理消息发送、编辑、删除、回复等动作。
这体现了当前工具设计中的一个重要原则:相关操作合并为一个领域工具,通过参数区分具体动作,而不是把每一个动作都注册为独立工具。
4.5 浏览器和 Canvas 工具
浏览器工具位于 BrowserTool.java,Canvas 工具位于 CanvasTool.java。
browser 可以根据参数执行:
- 查看浏览器状态;
- 打开和关闭页面;
- 聚焦标签页;
- 导航到 URL;
- 获取页面快照;
- 截图;
- 执行页面交互;
- 处理弹窗和文件上传。
canvas 用于 Canvas 节点展示、隐藏、导航、脚本执行、快照和 A2UI 操作。
浏览器控制服务使用独立端口,工具通过 BrowserClient 与浏览器控制服务通信,而不是直接把浏览器逻辑写进 Agent 循环。
4.6 记忆、图片、语音和节点工具
| 工具名 | 实现类 | 功能 |
|---|---|---|
memory_search | MemoryTool.java | 搜索 MEMORY.md 和记忆目录中的内容 |
image | ImageTool.java | 使用视觉模型分析本地图片 |
tts | TtsTool.java | 文本转语音 |
nodes | NodesTool.java | 查看和操作已连接节点 |
cron | CronTool.java | 创建、更新、删除和执行定时任务 |
当前 memory_get 不是独立工具,Java 实现只注册了 memory_search。
五、工具在哪里注册
应用启动时,Spring 创建 ToolRegistry,在 AgentBeanConfig.java 中注册内置工具:
registry.register(new ExecTool());
registry.register(FileTools.readFile());
registry.register(FileTools.writeFile());
registry.register(FileTools.listDir());
registry.register(FileTools.grepSearch());
随后调用 OpenClawToolFactory.java,批量创建 OpenClaw 扩展工具:
var extensionTools = OpenClawToolFactory.createTools(options);
registry.registerAll(extensionTools);
当前静态工具数量为:
5 个基础工具 + 17 个扩展工具 = 22 个工具
注册阶段的工具只是保存在内存中,并不代表已经发送给大模型。
六、工具什么时候注入给大模型
真正注入发生在 AgentRunner.java 的 executeLoop() 中。
每一轮模型请求都会执行以下逻辑:
ToolRegistry.listAll()
→ 按沙箱策略过滤
→ 读取工具名称、描述、参数 Schema
→ 构造 ChatRequest.tools
→ 调用 Provider
核心代码的逻辑相当于:
.tools(availableTools(context).stream().map(tool -> {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("name", tool.getName());
definition.put("description", tool.getDescription());
definition.put("input_schema", tool.getParameterSchema());
return definition;
}).toList())
因此,工具不是只在 Agent 启动时注入一次,而是每次调用大模型之前重新生成工具列表。
这样设计的好处是:
- 每轮都可以重新应用沙箱策略;
- 动态注册的插件工具可以及时生效;
- 不同 Agent、不同会话可以拥有不同工具上下文;
- 后续可以根据模型能力、用户权限和会话状态动态调整工具。
七、插件工具的动态注入
在 AgentRunner.java 的 Agent 开始阶段,系统会解析插件工具:
PluginRegistry
→ PluginToolResolver.resolvePluginTools()
→ ToolRegistry.registerPluginTool()
→ 下一步 availableTools()
→ 注入模型请求
插件工具需要经过名称冲突检查。如果插件工具名称已经存在,系统不会覆盖原有内置工具,而是跳过该插件工具并记录警告。
因此,tools=22 是当前静态工具集合的数量;如果启用了插件,最终数量可能大于 22。
八、沙箱策略如何影响最终工具列表
工具注册数量不等于最终发送数量。AgentRunner.availableTools() 会调用 isToolAllowed(),根据当前 Sandbox 的工具策略过滤工具。
例如:
ToolRegistry 中有 22 个工具
→ 沙箱策略禁止 exec
→ 发给模型的列表变成 21 个工具
这是一种重要的安全设计:
- 工具实现和安全策略分离;
- 工具本身不需要知道当前用户权限;
- 不同会话可以使用不同的工具白名单;
- 沙箱可以限制文件访问和命令执行能力。
九、Provider 如何发送工具定义
当前 Provider 是 OpenAI 兼容格式实现,位于 OpenAICompatibleProvider.java。
Agent 层使用统一格式:
{
"name": "web_search",
"description": "Search the web for information...",
"input_schema": {
"type": "object"
}
}
Provider 层会转换为 OpenAI Function Calling 格式:
{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web for information...",
"parameters": {
"type": "object"
}
}
}
随后放入 /v1/chat/completions 请求体的 tools 字段。
Provider 日志中的:
tools=22
表示本次 HTTP 请求携带了 22 个工具定义,并不表示模型实际调用了 22 个工具。
十、模型调用工具后的执行流程
完整的多轮调用过程如下:
第 1 轮
Java 发送用户消息 + 22 个工具定义
→ 模型返回普通文本或 tool_calls
如果返回 tool_calls
→ AgentRunner 根据工具名从 ToolRegistry 查找工具
→ 构造 ToolContext
→ 调用 AgentTool.execute()
→ 得到 ToolResult
→ 将工具结果追加到 messages
第 2 轮
Java 发送历史消息 + 工具结果 + 22 个工具定义
→ 模型继续推理
直到模型返回最终文本,或者达到最大轮数
工具调用本身不会直接产生最终答案。工具只负责提供外部信息或执行外部动作,最终回答仍由大模型生成。
十一、为什么采用顶层工具加 action 的设计
当前很多工具不是把每个动作注册成单独工具,而是使用一个顶层工具配合 action 参数,例如:
browser.open
browser.navigate
browser.screenshot
在 Provider 看来,它们通常仍然是一个 browser 工具;具体操作通过参数区分。
这种设计有几个优点:
1. 减少工具数量
如果浏览器的每个动作都单独注册,工具数量会迅速膨胀,增加模型的选择成本和上下文长度。
2. 统一领域模型
浏览器相关能力集中在 BrowserTool,消息相关能力集中在 MessageTool,后续维护更清晰。
3. 方便权限控制
可以对整个 browser 工具进行控制,也可以在工具内部进一步控制某些 action。
4. 便于协议扩展
新增浏览器动作时,通常只需要扩展 action 枚举和执行逻辑,不必改变整个工具注册体系。
十二、已经实现但当前没有注册的工具
项目中还存在 PatchTool.java,工具名是 patch,用于对文件进行文本替换或补丁操作。
但是当前 AgentBeanConfig.java 没有注册它,因此:
PatchTool 已实现
PatchTool 未进入当前 22 个静态工具
PatchTool 当前不会被发送给模型
如果需要启用,需要在工具注册阶段显式加入,并同时评估:
- 沙箱路径限制;
- 文件写入权限;
- 补丁失败时的错误处理;
- 是否需要按模型或用户权限开放。
十三、当前设计的优点和可以改进的地方
当前优点
- 统一接口:所有工具都遵循
AgentTool,便于扩展。 - 注册中心管理:工具查找、列举、去重集中在
ToolRegistry。 - Provider 解耦:工具不依赖具体模型格式,由 Provider 负责适配。
- 支持异步执行:工具返回
CompletableFuture,适合网络和长耗时任务。 - 安全策略独立:沙箱策略在注入模型前过滤工具。
- 支持插件扩展:插件工具可以在 Agent 运行时动态加入。
- 支持多轮调用:模型可以连续执行多个工具,然后综合结果回答。
建议改进
1. 记录每轮实际发送的工具名称
当前日志主要记录 tools=22,建议增加脱敏后的工具名列表,例如:
Provider tools: count=22, names=[exec, read_file, web_search, web_fetch, ...]
这样可以直接判断是注册数量、过滤数量还是最终发送数量。
2. 区分注册工具数和可用工具数
建议记录:
registeredTools=22
pluginTools=0
filteredTools=1
providerTools=21
这比单独记录 tools=22 更容易排查权限问题。
3. 对高风险工具增加二次确认
对于 exec、write_file、gateway、message 等工具,可以根据操作类型增加用户确认或管理员权限校验。
4. 为工具增加超时、重试和熔断
网络工具、浏览器工具和子 Agent 工具可能长时间等待,建议统一提供:
- 执行超时;
- 可重试次数;
- 取消信号;
- 熔断和降级策略。
5. 统一工具调用审计
建议为每次工具调用记录:
traceId;runId;sessionId;- 工具名;
- 工具调用开始时间;
- 执行耗时;
- 成功或失败;
- 结果长度;
- 脱敏后的错误摘要。
不建议直接记录完整 Prompt、完整工具参数或网页正文,以免日志泄露敏感信息。
十四、总结
当前 OpenClaw Java 的工具系统可以概括为:
工具实现
→ ToolRegistry 注册
→ Agent 启动时注入插件工具
→ 每轮模型调用前按沙箱策略过滤
→ 构造 ChatRequest.tools
→ Provider 转换为 Function Calling 格式
→ 模型决定是否调用
→ AgentRunner 执行工具
→ 工具结果回传模型
→ 多轮循环直到最终回答
当前静态注册工具共有 22 个,覆盖:
- Shell 和文件系统;
- 网络搜索和网页抓取;
- 会话和子 Agent;
- Gateway 和消息;
- 浏览器和 Canvas;
- 记忆、图片、语音、节点和定时任务。
其中,tools=22 只表示发送给 Provider 的工具定义数量;在实际请求中,模型可能只调用其中一个或几个工具。当前真实请求已经确认实际使用的是 web_search 和 web_fetch。
更多推荐

所有评论(0)