让 Agent 长出双手:一个注解搞定工具调用,AgentScope-Java @Tool 实战

让 Agent 长出双手:一个注解搞定工具调用,AgentScope-Java @Tool 实战

上篇文章我们跑通了第一个 HarnessAgent,但它还只会"动嘴"------你问什么它答什么,碰不到你的数据,改不了你的文件。这一篇让它真正"动手":用 @Tool 注解把普通 Java 方法变成 Agent 的工具,并完整跟踪一次工具调用在 ReAct 循环里的生命周期。本文基于 agentscope-harness 2.0.1。

你能获得什么

  • 用 @Tool 注解把 Java 方法变成 Agent 工具的完整写法
  • 框架自动生成 JSON Schema 的幕后机制(含探针验证输出)
  • 一次工具调用从发起到回传的完整事件链路
  • 工具设计的五条实战经验与三个踩坑

一、没有工具的 Agent,只是个聊天框

大模型本身是个"无状态的概率引擎":它不知道今天几号、读不到你的数据库、存不了一篇草稿。Agent 和 Chatbot 的分水岭,就在于工具(Tool)------模型在推理中判断"这事我得调个工具",框架执行工具,结果回传给模型继续思考。这就是 ReAct 循环里 Act 那一环。

AgentScope-Java 开箱自带 21 个工具(文件读写、Shell 执行、记忆检索、子智能体编排等),我上篇文章的探针输出里能看到:

csharp 复制代码
[agent_send, agent_spawn, agent_list, task_list, task_output, task_cancel,
 wait_async_results, memory_save, memory_get, memory_search, read_file,
 write_file, edit_file, list_files, glob_files, grep_files, execute, ...]

但这些通用工具不懂你的业务。让 Agent 帮你运营写作流程,得给它定制工具------好消息是,在 AgentScope-Java 里这只需要一个注解。

二、最小示例:一个方法就是一个工具

看最简单的例子:

java 复制代码
@Tool(name = "word_count", description = "统计一段文字的字数(含标点)",
        readOnly = true, concurrencySafe = true)
public String wordCount(
        @ToolParam(name = "text", description = "待统计的文字内容") String text) {
    return "共 " + text.length() + " 字。";
}

三个要点:

  1. @Tool 标注方法 :name 是模型看到的工具名(必填),description 告诉模型"什么时候该用我";
  2. @ToolParam 标注参数:每个参数的名称和描述同样会进 Schema,模型靠它决定传什么;
  3. 返回 String:工具结果统一以文本形式回喂给模型,复杂对象框架会帮你序列化。

注册一行搞定:

java 复制代码
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new DraftTools(draftsDir)); // 反射扫描全部 @Tool 方法

框架会扫描对象上所有带 @Tool 的 public 方法,自动生成工具名、描述、参数 Schema------不用手写 JSON,不用实现任何接口。

三、完整工具集:写作助手三件套

上篇文章的项目里,我给写作团队写了三个工具(src/main/java/com/mk/agentscope/tool/DraftTools.java):

java 复制代码
public class DraftTools {

    private final Path draftsDir;

    public DraftTools(Path draftsDir) {
        this.draftsDir = draftsDir;
    }

    @Tool(name = "save_draft", description = "把一段文字保存为草稿文件,返回保存结果")
    public String saveDraft(
            @ToolParam(name = "title", description = "草稿标题,将作为文件名(无需扩展名)") String title,
            @ToolParam(name = "content", description = "草稿正文内容") String content) {
        try {
            Files.createDirectories(draftsDir);
            Path target = draftsDir.resolve(sanitize(title) + ".md");
            Files.writeString(target, content, StandardCharsets.UTF_8);
            return "已保存草稿《" + title + "》,共 " + content.length() + " 字";
        } catch (IOException e) {
            return "保存失败:" + e.getMessage();
        }
    }

    @Tool(name = "list_drafts", description = "列出草稿箱中所有已保存的草稿标题")
    public String listDrafts() {
        // 无参数工具:模型直接调用,括号里什么都不用传
        ...
    }

    @Tool(name = "word_count", description = "统计一段文字的字数(含标点)",
            readOnly = true, concurrencySafe = true)
    public String wordCount(
            @ToolParam(name = "text", description = "待统计的文字内容") String text) {
        return "共 " + text.length() + " 字。";
    }
}

【图1:DraftTools 源码中 @Tool 注解特写】

三个工具覆盖了三种典型形态:有副作用 (save_draft 写文件)、纯查询 (list_drafts 无参)、纯计算(word_count 只读)。写业务工具时先想清楚你的工具属于哪类,这决定了下面要讲的声明属性怎么配。

四、幕后机制:Schema 是自动生成的

模型怎么知道该调哪个工具、传什么参数?答案是框架为每个工具生成的 JSON Schema。我写了个探针(不需要 API Key)直接验证:

java 复制代码
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new DraftTools(Paths.get(".agentscope/workspace/drafts")));
System.out.println(toolkit.getToolNames());
toolkit.getToolSchemas().forEach(s -> System.out.println(s.getParameters()));

控制台输出:

csharp 复制代码
INFO  io.agentscope.core.tool.Toolkit -- Registered tool 'save_draft' in group 'ungrouped'
INFO  io.agentscope.core.tool.Toolkit -- Registered tool 'list_drafts' in group 'ungrouped'
INFO  io.agentscope.core.tool.Toolkit -- Registered tool 'word_count' in group 'ungrouped'
TOOL_NAMES=[save_draft, word_count, list_drafts]
=== save_draft ===
params: {type=object, properties={title={type=string, description=草稿标题,将作为文件名(无需扩展名)},
        content={type=string, description=草稿正文内容}}, required=[title, content]}

【图2:探针输出------注册日志 + 自动生成的 JSON Schema】

看到没:@ToolParam 里的描述原文进了 Schema 的 description 字段,required 列表自动生成(@ToolParam 的 required 默认 true)。这套 Schema 随每次请求发给模型,模型据此决定调用时机和参数------所以 description 不是写给编译器的注释,是写给模型的 prompt,值得像写需求一样认真。

@Tool 还有几个声明属性值得知道:

属性 默认 语义
readOnly false 只读工具,无副作用,可安全重试
concurrencySafe false 线程安全,可并行调用
strict false 严格参数校验模式
dangerousFiles / dangerousDirectories 空 声明危险路径,触发权限确认

五、一次工具调用的完整生命周期

工具挂到 HarnessAgent 上,发起一个必然触发工具的请求(agent/ToolCallDemo.java):

java 复制代码
HarnessAgent agent = HarnessAgent.builder()
        .name("writing-assistant")
        .sysPrompt("你是一个写作助手。用户要求保存草稿时必须调用 save_draft 工具...")
        .model(ModelFactory.build())
        .workspace(Paths.get(".agentscope/workspace"))
        .toolkit(toolkit)
        .build();

UserMessage message = new UserMessage(
        "帮我把下面这段存成草稿,标题「Java多智能体入门」,内容:「...」。"
        + "存完告诉我草稿箱里现在有哪些草稿,这段内容一共多少字。");

agent.streamEvents(message, ctx)
        .doOnNext(event -> {
            switch (event.getType()) {
                case TEXT_BLOCK_DELTA -> System.out.print(((TextBlockDeltaEvent) event).getDelta());
                case TOOL_CALL_START -> System.out.println("[调用工具] " + ((ToolCallStartEvent) event).getToolCallName());
                case TOOL_CALL_END -> System.out.println("[参数就绪] " + ((ToolCallEndEvent) event).getToolCallName());
                case TOOL_RESULT_TEXT_DELTA -> System.out.print(((ToolResultTextDeltaEvent) event).getDelta());
                case TOOL_RESULT_END -> System.out.println("[工具返回] " + ((ToolResultEndEvent) event).getToolCallName());
                default -> { }
            }
        }).blockLast();

运行后控制台会看到完整链路:

css 复制代码
[调用工具] save_draft
[参数就绪] save_draft
已保存草稿《Java多智能体入门》,共 42 字
[工具返回] save_draft 状态=SUCCESS
[调用工具] list_drafts
...
[调用工具] word_count
...
共 42 字。
[工具返回] word_count 状态=SUCCESS
好的,已为你保存草稿《Java多智能体入门》...(模型基于三次工具结果生成最终回复)

【图3:ToolCallDemo 控制台完整链路截图】

这张图就是 P2 文章的核心素材:一次用户请求背后,模型自主完成了三轮工具调用------保存、查询、统计,最后才综合结果作答。注意最后那段文字不是工具直接返回的,而是模型把三个工具结果"消化"后的产物,这正是 ReAct 循环(Think → Act → Observe)的含义。

【图4:.agentscope/workspace/drafts/ 下真实生成的草稿文件】

跑完去 workspace 里看,Java多智能体入门.md 真的躺在那里------Agent 不是模拟保存,是真的动了你的文件系统。

六、工具设计的五条经验

  1. 一个工具只做一件事 。save_draft 只管保存,别让它顺便统计字数------工具粒度越细,模型的组合空间越大,出错时定位越准。
  2. description 是给模型的 prompt。写"保存草稿"不如写"把一段文字保存为草稿文件,返回保存结果"------后者明确了输入输出契约。宁具体勿抽象。
  3. 工具内自己兜异常 。saveDraft 里 try-catch 返回"保存失败:原因"而不是抛异常------把错误信息回喂给模型,它有机会自己纠正(比如换个标题重试);抛异常会直接中断整个 ReAct 循环。
  4. 纯计算/查询类工具标上 readOnly = true, concurrencySafe = true,框架可以并行调度,多工具请求时延迟明显下降。
  5. 路径要清洗 。工具直接暴露文件系统时,像 sanitize(title) 那样过滤路径分隔符,否则模型一个"包含 / 的标题"就能写出目录外的文件。

七、踩坑记录

坑 1:终端中文乱码。 注册日志和 Schema 打印里的中文在 Windows GBK 终端显示为乱码,但数据本身是 UTF-8 无损的。跑探针时加 -Dfile.encoding=UTF-8 即可,或者干脆不管------发给模型的内容不受终端编码影响。

坑 2:ToolSchema 没有 toJson()。 我想打印 Schema 验证生成结果,翻了半天只有 getName()/getDescription()/getParameters() 这些 getter(getParameters() 返回的就是 Schema Map)。想自己序列化得借助 Jackson。

坑 3:@ToolParam 的 required 默认 true。 想声明可选参数必须显式写 required = false,否则模型会把它当必传,Schema 里也会进 required 列表。

八、总结与下期预告

到这儿,Agent 已经有了"双手":你给它工具,它自己决定什么时候用、怎么组合。下一篇 P3 是系列核心篇------把单个 Agent 放大成一个写作团队 :总编接需求,agent_spawn 派活给结构策划、撰稿人、审校三个子 Agent,全部用 markdown 文件声明,不写一行胶水代码。

本文完整代码:github.com/525128633/a...(tool/DraftTools.java + agent/ToolCallDemo.java) 框架官方文档:java.agentscope.io 基于 agentscope-harness 2.0.1,DeepSeek API

相关推荐
小溪学编程1 小时前
将 Bean 放入 Spring 容器中的方式有哪些?
java·后端·spring
Mk598781 小时前
一个 Markdown 文件就是一个员工 —— AgentScope-Java 多智能体编排实战(核心篇)
后端
imDwAaY1 小时前
微信运动每天晚上 10 点推送排行榜,技术上到底有多难?
后端
ServBay1 小时前
PHP 突爆 9.2 分高危漏洞,你升级了吗?
后端·php
武子康2 小时前
撤回发生在重建途中,旧索引还能切回来吗
人工智能·后端·agent
码事漫谈2 小时前
双十一,我为什么想买一台编码显示器
后端
喵个咪3 小时前
GoWind Kanban 拆解:看板拖拽背后——LexoRank、乐观锁与尽力而为广播
后端·架构·go
喵个咪3 小时前
GoWind OA 拆解:用 3000 行 Go 写一个够用的审批工作流引擎
后端·架构·go
喵个咪3 小时前
GoWind Ledger 拆解:单式记账的余额一致性——confirm、补偿,以及没进事务的那些缝隙
后端·架构·go