1. 从“只会聊天”到“能干活”ReActAgent 为什么需要工具如果你刚接触 AgentScope Java大概率已经跑通过一个只会回话的 ReActAgent你问它问题它给你一段文字。但只要需求稍微真实一点比如“现在北京几点了”“帮我算一下 123 乘 456”“查一下这个订单的状态”它就开始一本正经地胡说——因为它没有获取实时信息、执行计算、访问业务系统的能力。工具系统就是给 Agent 装上的“手”。在 AgentScope Java 里Tool注解负责把普通 Java 方法注册成 ReActAgent 可调用的能力Agent 在 ReAct 循环里自己判断这个问题要不要调工具、调哪个、参数填什么。整个过程不需要你写 if-else 去路由模型根据工具的名称和描述做决策。这篇是新手村系列第三篇聚焦工具系统入门。我会从零讲清Tool注解怎么用、参数怎么描述、返回值有什么约定、调用链路长什么样然后给一份可以直接复制的工具类骨架和 Agent 注册配置最后带你本地跑一次亲眼看到工具被正确触发。适合已经能跑起 ReActAgent、想让它真正“动手”的 Java 开发者。2. 前置准备TaoToken 接入与依赖确认在写工具之前先把模型接入这块理顺。AgentScope Java 本身不绑定某一家模型它通过OpenAIChatModel这类适配器对接兼容 OpenAI 协议的服务。我这边习惯用 TaoToken 来做统一接入原因是它同时提供对话和编码两类能力后面从工具调用过渡到 Coding Plan 不用换一套配置。你需要先拿到一个 API Key。打开 https://taotoken.net/api-keys 创建然后把它放进环境变量别硬编码在代码里export TAOTOKEN_API_KEYsk-你的key模型服务的基础地址用 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为baseUrl使用。如果你后面要跑 Claude Code 这类编码场景可以看 https://taotoken.net/claude-code 不过本篇还是聚焦纯 Java 工具调用。依赖方面确认你的pom.xml里有 AgentScope 的核心包。版本号按你项目实际用的填这里只示意结构dependency groupIdio.agentscope/groupId artifactIdagentscope-core/artifactId version你的版本/version /dependency注意工具调用依赖模型本身支持 function calling。选模型时确认它具备工具调用能力否则 Agent 不会触发任何工具只会用文字回答。3. 可复制配置Tool 工具类骨架与 Agent 注册3.1 最小工具类一个方法就是一把扳手先看最核心的写法。一个普通 Java 方法加上Tool就变成 Agent 可调用的工具参数上加ToolParam描述含义。下面这个类包含两个工具查时间和做加法。package com.example.tools; import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; public class BasicTools { Tool(name get_current_time, description 获取指定时区的当前时间。当用户询问某个地点的当前时间时使用此工具。) public String getCurrentTime( ToolParam(name timezone, description 时区名称例如 Asia/Shanghai、America/New_York) String timezone) { try { ZoneId zoneId ZoneId.of(timezone); LocalDateTime now LocalDateTime.now(zoneId); DateTimeFormatter fmt DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); return String.format(Current time in %s: %s, timezone, now.format(fmt)); } catch (Exception e) { return Error: Invalid timezone. Try Asia/Shanghai or America/New_York; } } Tool(name add_numbers, description 计算两个数字的和。当用户需要做加法运算时使用此工具。) public String addNumbers( ToolParam(name a, description 第一个加数) double a, ToolParam(name b, description 第二个加数) double b) { return String.format(%.2f %.2f %.2f, a, b, a b); } }这里有几个关键点值得单独说。Tool的name是工具的唯一标识Agent 调用时用的就是这个名字所以别起重复的。description是模型判断“要不要调这个工具”的唯一依据写得越清楚触发越准。ToolParam标注在参数上描述参数含义和格式模型据此填参数。3.2 注册到 Toolkit 并挂到 ReActAgent工具类写好后创建Toolkit实例把工具对象注册进去再传给 Agent 的 builder。import io.agentscope.core.ReActAgent; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.formatter.openai.OpenAIChatFormatter; import io.agentscope.core.message.UserMessage; import io.agentscope.core.model.OpenAIChatModel; import io.agentscope.core.tool.Toolkit; import com.example.tools.BasicTools; public class ToolAgentDemo { public static void main(String[] args) { String apiKey System.getenv(TAOTOKEN_API_KEY); Toolkit toolkit new Toolkit(); toolkit.registerTool(new BasicTools()); ReActAgent agent ReActAgent.builder() .name(ToolAgent) .sysPrompt(你是一个可以使用工具的助手。需要准确信息时请调用工具 并在调用前简要说明你在做什么。) .model(OpenAIChatModel.builder() .apiKey(apiKey) .modelName(你的模型名) .baseUrl(https://taotoken.net/api) .stream(true) .formatter(new OpenAIChatFormatter()) .build()) .toolkit(toolkit) .build(); String reply agent.call( new UserMessage(现在北京几点了), RuntimeContext.empty()) .block() .getTextContent(); System.out.println(reply); } }sysPrompt里加一句“需要准确信息时请调用工具”是有用的它给模型一个行为倾向。但真正决定调不调的还是工具描述本身系统提示只是辅助。3.3 返回值约定能返回什么工具方法的返回值会被自动转成字符串交给 Agent。常见几种返回类型// 1. 直接返回字符串 Tool(name echo, description 回显输入内容) public String echo(ToolParam(name input, description 要回显的文本) String input) { return Echo: input; } // 2. 返回对象自动序列化为 JSON Tool(name get_user, description 根据用户 ID 查询用户信息) public MapString, Object getUser( ToolParam(name userId, description 用户 ID) String userId) { return Map.of(id, userId, name, Alice, age, 30); } // 3. 返回 voidAgent 收到空结果 Tool(name log_message, description 记录一条日志) public void logMessage( ToolParam(name message, description 日志内容) String message) { System.out.println([LOG] message); }实测下来返回 JSON 对象在需要结构化数据时最方便模型能直接读到字段。返回 void 适合纯副作用操作但模型拿不到反馈容易重复调用慎用。4. 验证请求本地跑一次看工具被触发代码就绪后直接运行ToolAgentDemo。你会在控制台看到类似这样的输出具体措辞因模型而异我需要查一下北京时间调用 get_current_time 工具。 Current time in Asia/Shanghai: 2025-01-15 14:32:08 现在是北京时间 2025-01-15 14:32:08。这条链路值得拆开看。第一步模型读到用户问题“现在北京几点了”结合工具描述判断需要调用get_current_time。第二步模型生成工具调用请求参数timezone填Asia/Shanghai。第三步框架执行你的 Java 方法拿到返回值。第四步返回值作为工具结果回传给模型模型整理成自然语言回复。想确认工具真的被调用了而不是模型瞎编时间可以在工具方法里加一行打印Tool(name get_current_time, description 获取指定时区的当前时间) public String getCurrentTime( ToolParam(name timezone, description 时区名称) String timezone) { System.out.println([TOOL CALLED] get_current_time, timezone timezone); // ... 原有逻辑 }再跑一次如果控制台出现[TOOL CALLED]说明工具确实被触发了。这一步是新手最容易忽略的验证很多人看到回复里有时间就以为成功了其实可能是模型自己编的。5. 本篇常见错排查5.1 工具完全没被调用Agent 直接文字回答最常见的原因是工具描述太模糊。比如description 做事情模型根本不知道什么时候该用。改成明确的功能边界加使用场景比如“获取指定城市的当前天气当用户询问天气时使用”。另一个原因是模型不支持 function calling换一个支持工具调用的模型再试。5.2 参数填错或类型不匹配ToolParam的description要写清格式和取值范围。如果参数是double模型可能传字符串123框架一般会做转换但保险起见在方法里加 try-catch出错时返回明确的错误提示字符串让模型有机会纠正重试。5.3 工具名重复导致注册失败同一个Toolkit里注册两个name相同的工具会冲突。检查所有Tool(name ...)确保全局唯一。工具多的时候建议按业务前缀命名比如order_query、user_query。5.4 返回值太大把上下文撑爆工具返回的内容会进入对话上下文。如果返回一个几万字的 JSON会挤占后续推理空间。返回前做裁剪只给模型需要的字段。比如查数据库只返回关键列别SELECT *全丢回去。5.5 时区、编码这类环境问题ZoneId.of(Asia/Shanghai)在标准 JDK 上没问题但如果你的运行环境时区数据不全可能抛异常。工具方法里捕获异常并返回可读错误比让整个调用链崩掉要好。编码问题同理涉及文件读写时显式指定 UTF-8。6. 下一步从单工具到工具系统到这里你已经能让 ReActAgent 调用第一个 Java 工具了。但真实项目里工具会越来越多十几个甚至几十个全塞给模型会让工具描述占满上下文触发准确率也会下降。这时候就需要工具分组和元工具机制让 Agent 自己决定激活哪一组工具。如果你打算把工具调用用到长期编码或 Agent 场景建议了解一下 Coding Plan它在工具编排和上下文管理上做了更多优化https://taotoken.net/coding-plan 。想先验证不同模型对工具调用的支持情况可以直接在模型对话里试https://taotoken.net/chat 。接入过程中遇到工具注册或参数传递的问题接入文档里有更细的说明https://taotoken.net/doc 。我自己的经验是工具描述值得反复打磨。同一个工具描述改三遍触发准确率能差出一大截。别指望一次写对跑起来看日志看模型在什么情况下没调、什么情况下调错再回去改描述。这个迭代过程本身就是工具系统调优的核心工作。
