12-Tool-Calling原理-大模型如何决定调用你的Java方法
很多刚接触 Agent 的 Java 同学会有个误会:“大模型不是只能聊天吗?它怎么可能去调我的 saveMemory 方法、去改我的数据库?”
答案就藏在一个叫 Tool Calling(工具调用) 的机制里。它不是让模型直接"执行"你的 Java 代码,而是让模型先决定"我要调哪个函数、参数填什么",然后由 LangChain4j 框架真的去跑你的方法,再把结果喂回去。你的方法永远跑在你的 JVM 里,模型只是个"指手画脚的指挥官"。
这篇文章把工具调用的完整回合拆成 7 步讲透,然后用 AI 伙伴里一句最日常的"明天八点提醒我吃药",串起那 13 个工具到底谁会被叫醒。最后聊聊 @Tool 描述文本有多要命,以及并行/多轮调用这些进阶玩法。
一、一次工具调用的完整 7 步回合
下面这张图是 AI 伙伴里 Agent 调用任意工具的通用流程。请把它刻进脑子里,后面所有分析都基于它。
┌──────────────────────────────────────────────────────────────────┐
│ 1. 注册:6 个 @Component 工具类被注入 AiServices.builder() │
│ 2. 转 Schema:LangChain4j 把每个 @Tool 方法抽成 JSON Schema │
│ (名字、描述、参数名、参数类型、是否必填) │
└───────────────────────────┬──────────────────────────────────────┘
│ (把这些 schema 随请求发给模型)
▼
┌──────────────────────────────────────────────────────────────────┐
│ 3. 模型决策:LLM 读完 System/用户消息 + 工具列表,决定"调哪个" │
│ 4. 参数填充:模型吐出 tool_call,内含方法名 + 填好的参数 JSON │
└───────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 5. Java 执行:LangChain4j 拿到 tool_call,反射调用你的真实方法 │
│ (如 ReminderTool.createReminder → ReminderService → 落库) │
└───────────────────────────┬──────────────────────────────────────┘
│ (方法返回值 = 工具结果)
▼
┌──────────────────────────────────────────────────────────────────┐
│ 6. 结果回灌:工具结果作为 assistant 的 tool 消息,再次发回模型 │
│ 7. 最终回答:模型结合工具结果,生成给用户的自然语言回复 │
└──────────────────────────────────────────────────────────────────┘
我逐条展开:
第 1 步 注册。 AI 伙伴在 LangChain4jConfig 里用 AiServices.builder(CompanionAssistant.class).tools(memoryTool, reminderTool, ...) 把 6 个工具类一次性注册进 Agent。这 6 个类都是 @Component,由 Spring 管理,方法上标了 @Tool。
第 2 步 转 Schema。 框架会扫描每个 @Tool 方法,生成一段机器可读的"函数说明书"(JSON Schema),描述:函数叫什么、干啥用(来自 @Tool 的描述文本)、有哪些参数、每个参数什么类型、必填还是可选。模型读的是这份说明书,而不是你的 Java 源码。
第 3~4 步 模型决策 + 参数填充。 这是大模型的活。它根据 System 消息(人设 + 记忆)、用户这句话、以及工具说明书,判断"现在该不该调工具、调哪个、参数怎么填"。如果决定调,它会返回一个 tool_call 结构,里面是方法名和填好的参数(比如 {"title":"吃药","type":"medication","remindTimeStr":"明天8点"})。
第 5 步 Java 执行。 框架接到 tool_call,用反射真正调用你的方法。你的方法内部再去调 service、落库、发 MQTT——所有副作用都在这一步发生,完全在本地 JVM,模型碰不到你的数据库。
第 6~7 步 结果回灌 + 最终回答。 方法返回的那个 String(比如"提醒已创建,ID=12")会被包成一条工具消息,再次发给模型。模型看到"哦,提醒建好了",于是用自然语言回复用户:“好的,已经帮你记下明天早上8点吃药~”
二、用"明天八点提醒我吃药"串起 13 个工具
现在上主菜。假设老人对着 AI 伙伴说了一句:
“明天八点提醒我吃药。”
我们跟着第 3~4 步走一遍,看看那 13 个 @Tool 方法里,到底谁会被叫醒。
先亮结论: 这句话最直接命中的是 ReminderTool.createReminder,其余 12 个工具在这一句里基本不会被调用。但这中间有个有意思的细节。
模型的内心戏(示意还原):
- 用户说"明天八点提醒我吃药"。意图很明确:建一个提醒。
- 模型扫一眼 13 个工具说明书,发现
ReminderTool.createReminder的描述是"为用户创建一条提醒(吃药/日程/喝水/生日/自定义)"——完美匹配。 - 参数怎么填?
title→ “吃药”(或更具体的"吃降压药")content→ 可空,或填"吃药"remindTimeStr→ “明天8点”(自然语言,交给NaturalTimeParser去算)type→ “medication”(吃药对应 medication 类型)repeatCron→ 空(一次性提醒)deviceCode→ 空(没指定哪个设备播报,调度器会自己找在线设备)userId→ 由框架从@ToolMemoryId自动注入,模型不用管
为什么不是别的工具?
TimeTool.getCurrentTime:理论上模型可能想先确认"今天周几"以便定位"明天"。但NaturalTimeParser在计算"明天8点"时是直接基于LocalDate.now()的,不需要模型先问时间,所以多数情况下模型会跳过这一步,直接调createReminder。MemoryTool.*:这句话没有透露稳定偏好或事实,不需要存记忆。EmotionTool.*:没涉及情绪。HealthTool.*:虽然"吃药"沾健康边,但用户说的是"提醒吃药"不是"记录已服药",语义上是提醒不是记录,所以不调recordHealth。DeviceTool.*:没要求现在就播报或控制设备。ReminderTool的另外两个(listReminders/cancelReminder):用户是"创建"不是"查询/取消",不匹配。
执行阶段(第 5 步)真发生了什么?
// 项目源码:ReminderTool.createReminder(节选)
Long reminderId = reminderService.createFromAgent(
userId, title, content, remindTimeStr, type, repeatCron, deviceCode);
return "提醒已创建,ID=" + reminderId + ":" + title + "(" + remindTimeStr + ")";
createFromAgent 内部会用 NaturalTimeParser.parse("明天8点") 把自然语言变成明早 8 点的 LocalDateTime,落进 t_reminder 表,返回一个 ID。这个 ID 连同"提醒已创建"会作为工具结果回灌(第 6 步),模型据此生成最终那句温暖的回复。
一句话总结这个案例:一句话往往只唤醒 1 个工具,工具选择高度依赖"用户意图 × 工具描述"的匹配度。13 个工具是 Agent 的"技能栏",但大多数时候它只点其中一个。
三、@Tool 描述文本为什么直接决定调用准确率
回到第 2 步转成的那份"函数说明书"。模型选不选这个工具、参数填得对不对,九成取决于 @Tool 括号里那段描述文字。
看 AI 伙伴里的真实写法:
// 项目源码:ReminderTool.createReminder
@Tool("为用户创建一条提醒(吃药/日程/喝水/生日/自定义),返回创建结果和提醒 ID")
public String createReminder(...) { ... }
// 项目源码:MemoryTool.saveMemory
@Tool("保存一条关于用户的长期记忆,例如:用户喜欢喝绿茶、用户有一只叫团团的猫、用户本周三要去医院复查。返回保存结果")
public String saveMemory(...) { ... }
注意 saveMemory 的描述里举了三个具体例子:喜欢绿茶、有只叫团团的猫、周三复查。这不是凑字数,而是给模型"打样"——告诉它"什么样的输入算一条记忆"。例子越贴近真实场景,模型越容易在用户说出类似话时,正确联想到该调 saveMemory 而不是别的。
反过来说,如果描述写得太含糊,比如只写"保存数据",模型就会犯迷糊:这到底是存记忆、存健康、还是存提醒?调用准确率直接跳水。所以给工具写描述,是 Agent 工程里性价比最高的一件事:把"这个工具干什么、什么时候用、参数是啥意思"用大白话写清楚,最好带 1~3 个真实例子。
参数名和参数注释同样重要。比如 remindTimeStr 的 JavaDoc 写着"触发时间,自然语言,例如’明天早上8点’‘每天晚上9点’",模型读了就知道该传自然语言而不是 ISO 时间串——因为后面有 NaturalTimeParser 兜底解析。
四、并行工具调用:一句话能同时点两个技能
刚才的"吃药"案例只唤醒了 1 个工具,但有些话会同时命中多个。比如用户说:
“记住我爱吃甜食,顺便看看我最近血压怎么样。”
这句话里有两个独立意图:存一条偏好记忆 + 查健康趋势。现代大模型(DeepSeek、通义等 OpenAI 兼容模型都支持)可以在同一个回合里并行返回多个 tool_call:
tool_call #1 → MemoryTool.saveMemory(content="爱吃甜食", type="preference", ...)
tool_call #2 → HealthTool.getHealthTrend(type="blood_pressure", ...)
框架会并行(或快速串行)执行这两个方法,把两个结果一起回灌,模型再综合生成回复。这对用户体验很关键:一句复合指令不用拆成两轮对话。
AI 伙伴的 6 个工具之间没有强制的先后依赖,因此天然支持这种并行。但要注意:并行执行时,如果两个工具都去写库且涉及同一行数据,才可能撞车——在陪伴场景里各写各的表,冲突概率极低。
五、多轮工具调用:工具调完还不够,再调一次
还有一种更"绕"的情况:模型调了一个工具,看了结果,发现信息不够,于是主动再调一次甚至调另一个工具。这叫多轮(multi-step)工具调用。
举个例子(示意):
用户:“帮我把明天的吃药提醒取消。”
模型可能这样走:
- 第一回合:先调
ReminderTool.listReminders(userId),看看用户有哪些待办提醒。 - 看到列表里有个"吃药"提醒 ID=12,但用户没说取消哪个。模型可能直接选 ID=12,或反问确认。
- 第二回合:调
ReminderTool.cancelReminder(reminderId=12, userId),完成取消。
这种"先查再改"的两段式,就是多轮工具调用的典型形态。框架层面,LangChain4j 会在"工具结果回灌 → 模型再决策"这一步循环,直到模型认为信息齐全、不再需要调工具,才输出最终自然语言回答。
需要提醒的是:AI 伙伴目前只有 ReminderTool.createReminder 一个方法有 try-catch。这意味着如果某个工具方法在 Java 执行阶段(第 5 步)抛了异常,而又没有 try-catch 兜住,这次 Agent 调用就可能整体失败,被 ChatService 捕获后返回"AI 服务暂时不可用"。这也是第 14 篇要专门聊的"统一异常处理"话题。
六、把原理收口:工具调用不是魔法,是"说明书写得好"
回顾一下,Tool Calling 的本质就三句话:
- 模型不碰你的代码,它只吐出"调哪个函数、参数填啥"的 JSON;
- 框架拿着 JSON 真的去调你的 Java 方法,副作用(落库、发消息)全在你的 JVM 里发生;
- 方法返回值会被回灌给模型,模型再组织成人类能懂的话。
所以,想让 AI 伙伴更聪明,功夫往往不在模型本身,而在两处:把 @Tool 描述写清楚(让模型选得准),以及把工具方法写稳健(让执行不翻车)。下一讲我们把那 13 个工具逐个摊开,看看 AI 伙伴到底有哪些"技能栏"。

279

被折叠的 条评论
为什么被折叠?



