04-让 Agent 会"用工具": Tool Calling 实战

系列第 4 篇 · 能力篇

上篇我们讲清了 ReAct 循环:模型在"推理(Reasoning)"和"行动(Acting)"之间反复横跳,直到产出答案。但有个绕不开的问题------

模型本身只会"生成文本",它怎么可能真的去查天气、算账单、读数据库?

答案就是本篇的主角:Tool Calling(工具调用)。它把 ReAct 循环里那个 "Acting" 步骤,接到你写的 Java 方法上。理解它,Agent 才第一次真正"动手"干活。

读完你会做到:用 @Tool 注解把任意 Java 方法变成 Agent 的能力,注册进 Agent,然后让模型自己决定什么时候调、怎么调。


一、先厘清:Tool 在 ReAct 里站在哪

回顾第 3 篇的循环图,"行动"那一步就是工具调用的位置:

sql 复制代码
flowchart TD
    Think[Reasoning:模型推理] --> Decide{需要外部数据?}
    Decide -->|否| Answer([直接回答])
    Decide -->|是| Action[Acting:调用 Tool]
    Action --> Observe[Observation:工具返回结果]
    Observe --> Think

整个过程对你来说是零硬编码的:你不用写 if (用户问天气) 调 getWeather(),模型读着工具描述自己判断。这正是 ReAct 比"写死流程图"聪明的地方。

而"工具描述"怎么写,直接决定了模型调得准不准------这点放到后面"避坑"细讲,它是实战里最常被忽略的命门。


二、三步走:定义 → 注册 → 使用

2.1 用 @Tool 定义工具

任意 Java 方法,加上 AgentScope 自己的 @Tool 注解,就成了 Agent 可调用的能力:

typescript 复制代码
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
 
public class WeatherTools {
 
    @Tool(name = "get_weather",
          description = "查询指定城市的当前天气,返回天气状况和温度")
    public String getWeather(
            @ToolParam(name = "city", description = "城市名称,例如 '北京' 或 'Shanghai'")
            String city) {
        // 这里可以接真实天气 API;示例先返回模拟数据
        return city + " 今天:晴,25℃,微风";
    }
 
    @Tool(name = "calculate",
          description = "计算一个数学表达式,例如 '23 * 7 + 4'")
    public double calculate(
            @ToolParam(name = "expression", description = "数学表达式字符串")
            String expression) {
        // 示例:真实场景可接表达式引擎(如 FelEvaluator / Janino)
        return 42.0;
    }
}

几个关键点(后面会反复用到):

@Tool

的name是工具的唯一标识,模型调用时用的就是它;

@Tool

的description告诉模型"这个工具能干嘛、什么时候该用",模型靠它决策;

@ToolParam

必须显式写name------因为 Java 编译后会丢掉参数名,不写框架拿不到;

方法返回值就是工具的执行结果,会被塞回 ReAct 循环当作 Observation。

注意:这是 AgentScope 自己的io.agentscope.core.tool.Tool,不是 Spring AI 的 @Tool。混用注解是新手高频报错来源。

2.2 注册到 Toolkit

定义了方法 ≠ 模型看得到它。还要把工具对象注册进 Toolkit:

java 复制代码
import io.agentscope.core.tool.Toolkit;
 
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new WeatherTools());   // 反射扫描 @Tool 方法并注册

职责划分要清楚:

WeatherTools

:工具真正执行的业务逻辑;

Toolkit

:收集、描述、分发多个工具;

HarnessAgent

:把模型、提示词、工具、Harness 能力组合起来。

2.3 交给 Agent 并调用

把 Toolkit 传给 HarnessAgent.builder().toolkit(...),然后用普通 call 触发:

java 复制代码
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.harness.HarnessAgent;
 
import java.nio.file.Paths;
 
public class ToolDemo {
    public static void main(String[] args) {
        Toolkit toolkit = new Toolkit();
        toolkit.registerTool(new WeatherTools());
 
        HarnessAgent agent = HarnessAgent.builder()
                .name("assistant")
                .sysPrompt("你是一个有用的助手,需要查天气或计算时请调用对应工具,不要编造数据。")
                .model("dashscope:qwen-plus")          // ModelRegistry 解析
                .toolkit(toolkit)                        // 接上工具
                .workspace(Paths.get(".agentscope/workspace"))
                .build();
 
        UserMessage userMsg = new UserMessage("北京今天天气怎么样?另外帮我算一下 23 乘以 7 加 4。");
        String reply = agent.call(userMsg, RuntimeContext.empty())
                .block()
                .getTextContent();
 
        System.out.println(reply);
    }
}

RuntimeContext.empty() 表示不区分用户/会话(演示用);真实业务请用 RuntimeContext.builder().sessionId(...).userId(...).build() 做多租户隔离(见第 2 篇)。


三、运行预期:模型自己"决定"调不调

跑起来后,你不会看到代码里任何 if 去判断"要不要查天气"。实际发生的是:

  1. 模型读到 get_weather / calculate 的描述;

  2. 判断用户这句话需要外部数据 → 返回工具调用请求(带参数 city="北京"、expression="23 * 7 + 4");

  3. 框架把请求分发到 WeatherTools.getWeather(...) / calculate(...),拿到结果;

  4. 结果作为 Observation 塞回循环,模型基于真实数据生成最终回复。

输出大致是:"北京今天晴,25℃。23 × 7 + 4 = 165。"------数字来自工具,不是模型编的。这正是 Tool Calling 的价值:把"会聊天"变成"能办事"。


四、进阶:让工具"调得准、用得稳"

4.1 工具描述写得好不好,决定一切

模型完全靠 @Tool / @ToolParam 的描述来决策。描述模糊,模型就会传错参数、甚至干脆不调。对比一下:

| 写法 | 后果 | | --- | --- | | @ToolParam(description = "城市") | 太泛,模型可能传"中国""北方"这类歧义值 | | @ToolParam(description = "城市名称,例如 '北京' 或 'Shanghai'") | 给示例,模型传参更准 | | @Tool(name = "t", description = "处理一些事") | 模型几乎不会调用(不知道何时用) |

经验法则:把工具描述当成"写给另一个工程师看的接口说明"------讲清能力边界 + 参数含义 + 给示例。

4.2 系统提示词是补充,不是替代

工具描述讲"工具能做什么",sysPrompt 讲"业务规则"。在关键场景,用 sysPrompt 给硬约束:

js 复制代码
当用户询问天气、温度时,必须调用 get_weather 工具,不得编造天气数据。

这能压住模型"幻觉式作答"的冲动,尤其在医疗、金融等不能编的领域。

4.3 工具可以是同步 / 异步、实例 / 静态

框架支持:实例方法、静态方法;同步或异步返回;流式或非流式返回。需要非阻塞时,工具方法可直接返回 Reactive 类型(如 Mono / Flux),与 AgentScope 的响应式内核天然契合------具体签名以官方文档为准。

4.4 readOnly 与计划模式(2.0 亮点)

2.0 的 HarnessAgent 支持计划模式(Plan Mode):Agent 先"只读规划"再"执行"。标注了只读性质的工具应加 readOnly = true,这样它在规划阶段也能被调用:

typescript 复制代码
@Tool(name = "getCurrentTime", description = "获取当前系统时间", readOnly = true)
public String getCurrentTime() { return LocalDateTime.now().toString(); }

4.5 工作区声明工具(进阶)

Harness 工作区模式下,还能在 workspace/tools.json 里声明 MCP server 与工具白名单,无需改 Java 代码即可扩工具------在MCP 协议相关的篇幅里面会详细介绍


五、新手最常踩的 6 个坑

@ToolParam

没写name------ Java 运行期拿不到参数名,工具 Schema 生成会出错或参数错位。务必显式写 name。

用了 Spring AI 的@Tool------ 认准 io.agentscope.core.tool.Tool,注解混用导致工具扫描不到。

忘了toolkit.registerTool(...)------ 只写了 @Tool 方法却没注册,模型根本不知道有这个工具。

工具描述太敷衍------ "处理事情""输入内容"这类描述,模型要么不调、要么乱传参。描述要讲清能力 + 参数示例。

让模型"编"数据------ 没在 sysPrompt 里约束"必须调工具",模型可能直接幻觉作答。涉及真实数据务必强约束。

非 DashScope 模型没装扩展包------ dashscope: 也要单独引 agentscope-extensions-model-dashscope,并非 harness 内置;换 DeepSeek / OpenAI 要补 agentscope-extensions-model-*。


小结

这一篇你让 Agent 第一次"动手":

Tool Calling 站在 ReAct 循环的Acting环节,把"生成文本"接到真实 Java 方法;

用@Tool+@ToolParam定义工具,Toolkit.registerTool(...)注册,.toolkit(toolkit)接入 Agent;

框架自动把注解编译成 JSON Schema,你不用手写;模型按描述自主决策何时调用;

调得准不准,七分看工具描述和sysPrompt 约束;2.0 还支持 readOnly、异步、工作区声明等进阶玩法。

工具是 Agent 能力的"手脚"。下一篇我们解决另一个 Java 工程师最痛的点:怎么把模型吐出的自由文本,稳稳地变成 Java 对象------结构化输出(Structured Output)与自纠错解析。

下篇预告:《结构化输出:把 LLM 文本变成 Java 对象》


如果这篇文章对你有帮助,欢迎关注公众号「栈知见」,我会持续输出有深度的技术实战笔记

相关推荐
吃饱了得干活2 小时前
Agent 的架构、多智能体与落地:从 Demo 到生产系统
llm·agent
VIP_CQCRE2 小时前
Coze 接入大模型太麻烦?用 Ace Data Cloud 统一 OpenAI Responses API
ai·大模型·agent·coze·acedatacloud
10年前端老司机3 小时前
实战分享:基于 PyMuPDF+Qwen-VL 实现图文兼容的 PDF RAG 方案
人工智能·python·agent
用户976104399214 小时前
8.2记忆系统:让智能体拥有记忆
agent
用户1494484813205 小时前
RAG 检索到了相似内容,为什么答案还是错的?
agent
vilya5 小时前
我怎么给手机 GUI Agent 做记忆层:事实常驻、技能按需,一条写入链
agent
zmsup6 小时前
设计 OpsArk 运维智能体:从一句需求,到一项可验收的任务
产品运营·agent·运维工具·终端运维
hpoenixf6 小时前
一个套壳MCP,为什么长成了研究决策系统
agent
骑着蜗牛撵大象3277 小时前
Agent 打字机是怎么来的:SSE 与 WebSocket 打通实时响应与中间状态
网络·websocket·网络协议·agent·sse·实时通信·流式输出