实现一个简单的工具调用DEMO
本文围绕 Page9 的 Java Demo,梳理一个工具调用程序从启动、注册工具到处理模型请求并返回结果的整体设计。重点放在各个组件如何衔接,以及应用怎样控制工具的校验和执行。
一、程序设计整体流程图
先从程序运行顺序看整体结构。启动时,应用创建工具定义并注册到 Agent;每轮交互中,模型可以直接回答,也可以请求调用工具。工具结果回到模型后,流程继续循环。
注册表是流程的起点:应用启动时创建工具定义,交给 Agent 按名称保存。此后同一份注册表既用于向模型提供候选,也用于应用查找模型请求的本地执行函数。
二、按流程拆解关键设计
1. 定义工具:把说明和 Java 执行函数绑定
每个 ToolSpec 包含工具名称、用途说明、参数 JSON Schema 和本地执行函数。Demo 定义了计算器和本机日期两个工具,前者接收算式,后者不需要参数。
工具定义 = 名称 + 用途描述 + 参数 Schema + Java 执行函数 calculator(expression: string) get_current_date()
以计算器为例,工具名 calculator 与 Java 函数 Calculator.calculate(...) 在注册时绑定。模型只看到工具说明和参数定义,不会拿到这个 Java 函数。
2. 注册工具:应用维护唯一的工具表
程序启动时把工具列表传给 Agent。Agent 按工具名把它们放入 Map<String, ToolSpec>,形成注册表;重复的工具名会被拒绝。这个表有两个用途:提供本轮候选,以及在模型提出调用后查找对应的本地执行函数。
3. 提供候选:把工具定义放进模型请求
每轮调用模型时,程序从注册表取出可用工具列表。百炼适配器将每个工具转换为 Chat Completions 的 tools 项,包含 name、description 和 parameters,并设置 tool_choice 为 auto,表示让模型自行判断是否需要工具。
{
"type": "function",
"function": {
"name": "calculator",
"description": "计算只包含数字、括号和 + - * / 的算式。",
"parameters": {
"type": "object",
"properties": { "expression": { "type": "string" } },
"required": ["expression"],
"additionalProperties": false
}
}
}
这是模型可见的“工具菜单”。参数 Schema 告诉模型要提供怎样的输入,也供应用在执行前校验参数。
4. 读取模型响应:判断是否调用以及调用什么
程序读取 choices[0].message.tool_calls。如果没有工具调用,模型返回的文本就是最终答复;如果有调用,程序解析每项中的工具名、参数和调用 ID。
{
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "calculator",
"arguments": "{\"expression\":\"123 * 456\"}"
}
}]
}
| 返回字段 | 应用怎样使用 |
|---|---|
function.name | 作为注册表查找键,决定请求哪个本地工具。 |
function.arguments | 解析为参数对象,校验后传给执行函数。 |
id | 作为 call_id 保存,之后用于关联工具结果。 |
5. 应用校验并执行:模型请求不等于允许执行
runTool() 先检查调用 ID,再按工具名在注册表查找。未注册的工具不会执行。找到工具后,validateArguments() 根据 Schema 检查必填字段、额外字段和基础类型;校验通过才调用注册时绑定的 Java 函数。
模型提出
“调用 calculator,参数是 expression=123*456。”这是调用请求,不是 Java 命令。
应用执行
查到注册项、校验参数后,应用调用 Calculator.calculate() 并取得结果。
未知工具、参数校验失败或执行过程发生异常时,程序不会把请求当成任意函数执行;它会生成一段错误结果,让模型知道本次工具调用没有成功。
6. 返回结果:用 tool_call_id 对应原调用
执行结果被包装成一条 role=tool 消息,并带回原来的调用 ID。这样模型能知道结果对应哪一次工具请求。
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "56088"
}
应用把这条消息追加到对话历史,再次请求模型。模型可以根据结果继续提出下一项工具调用,也可以直接组织最终答复。若一轮返回多个工具调用,程序会分别执行,并用各自的 tool_call_id 返回结果。
三、关键代码位置
| 流程环节 | 关键实现 |
|---|---|
| 工具定义 | ToolSpec、CALCULATOR、CURRENT_DATE |
| 建立工具注册表 | Agent 构造函数按工具名保存定义 |
| 提供候选工具 | Agent.run()、toApiTools()、DashScopeModel.complete() |
| 解析模型调用 | parseApiReply() 生成 ToolCall |
| 校验与执行 | runTool()、validateArguments()、ToolExecutor.execute() |
| 把结果交回模型 | Message.toolResult()、toApiMessages()、Agent.run() 下一轮循环 |
四、运行 Demo
先运行离线模式,可以在不调用模型 API 的情况下观察程序闭环。离线模型会固定模拟一次计算器调用;它不会根据你输入的问题进行推理。
cd "D:\data\ai学习\ai\Page9" mvn compile exec:java "-Dexec.args=--demo"
体验模型实际选择工具时,在 PowerShell 中设置百炼 API Key 和控制台里可用的模型名,再启动真实模式:
$env:DASHSCOPE_API_KEY = "你的百炼 API Key" $env:DASHSCOPE_MODEL = "控制台中可用的模型名" mvn compile exec:java
可以尝试“计算 123 × 456”“今天的日期是什么”或分成多步的算式。终端会显示提供给模型的候选工具、模型的调用请求、应用执行结果和后续模型轮次。输入 /reset 清空本次进程内对话,输入 /quit 退出。
五、Demo 与实际项目
这个 Demo 把工具调用核心闭环压缩在一个 Java 文件中,便于沿着数据流学习。实际项目的工具不一定是本地计算函数,也可能连接其他子系统的 API、企业服务或操作系统能力。变化的是工具背后的执行方式,整体闭环仍然相同。
调用其他子系统的 API
例如把订单查询封装为 get_order_status(orderId)。模型提出调用后,应用检查工具是否已注册、参数是否有效,以及当前用户是否有权查看该订单;随后由 Java API 适配器调用订单子系统,把响应整理成适合模型读取的结果,再作为工具消息交回模型。Java 可以用 HttpClient 发送 HTTP 请求,也可以使用目标系统提供的 SDK。
模型请求 get_order_status(orderId)
↓
应用校验工具、参数与用户权限
↓
Java API 适配器请求订单子系统
↓
整理响应并关联 tool_call_id
↓
结果交回模型
API 密钥和服务凭据由应用在服务端安全保存并附加到请求中,不应放进提示词或让模型生成。生产代码还要处理超时、服务错误、限流和重试;对可能重复产生副作用的请求,需要设计幂等保护。
调用操作系统工具
应用也可以把有限的系统能力包装成工具,例如查询磁盘空间、读取允许目录中的文件列表。能用 Java 标准库完成时,优先直接调用 Java API;确实需要启动外部程序时,可使用 ProcessBuilder,并由应用限定可运行的程序和参数。
不要开放一个让模型传入任意命令字符串的通用执行入口。应使用名称明确、范围有限的工具,校验每个参数,并设置执行超时、工作目录、输出大小和操作系统权限;高影响或不可逆操作还应要求用户确认。
系统和工具变多后使用 MCP
当需要连接多个独立服务时,可以通过 MCP 等协议统一工具的发现和调用。MCP Server 暴露工具名称、说明和输入 Schema,客户端先获取可用工具,再通过 tools/call 请求执行;这能统一连接方式,但不会自动替代应用的身份认证、授权和业务校验。可参考 MCP 工具规范。
当前 Demo 的 JSON Schema 校验只覆盖必填字段、额外字段和基础类型,并不是完整的 Schema 校验器。实际项目还要按具体 API 和系统能力补齐业务规则、权限检查、失败处理、超时、重试、审计和并发控制。
最重要的设计边界是:模型能够提出工具调用,但应用保留工具注册、权限校验和执行控制权。无论工具在本机、另一个子系统,还是 MCP Server 后面,应用都应在执行前做自己的检查。
总结
用户提出问题后,应用先把注册好的工具说明和对话一起交给模型。模型若返回调用请求,应用就按工具名查注册表、校验参数并运行对应的 Java 函数;工具结果带着原 call_id 回到对话中,模型再决定继续调用还是回答用户。
因此,工具调用不是模型直接执行本地代码,而是一种由模型提出请求、应用受控执行、模型读取结果并继续处理的协作闭环。Page9 的计算器和日期工具,就是这套机制的最小可运行例子。