切换主题
TIP
本页内容由本地原稿 03-工具调用.md 同步生成。 如果你要长期修改站点内容,优先回原稿修改,再执行 npm run docs:sync。
第 3 章:工具不是外挂,而是给 Agent 装手脚
这章要解决什么问题
如果一个 Agent 只能“根据训练数据聊天”,那它再聪明也只能停留在“会说”。
一旦你要它:
- 查商品价格
- 调公司内部接口
- 查库存
- 触发某个业务动作
它就必须有工具。
这章我们要讲清三件事:
- 工具调用到底是怎么发生的
- Spring AI Alibaba 里工具怎么写
- 你们公司项目里工具是怎么工程化接进去的
1. 先用一句人话理解工具调用
工具调用不是:
- 模型直接自己执行 Java 方法
- 模型真的“会查数据库”
- 模型自己连 HTTP 接口
工具调用的真实过程是:
- 模型读到你的问题
- 模型判断“这事我应该用某个工具”
- 框架根据模型返回的 tool call 信息,去执行真正的 Java 方法或函数
- 工具结果再回到模型上下文里
- 模型基于工具结果生成最终回答
也就是说:
模型负责“决定调不调工具”,框架负责“真的去执行工具”。
2. 把整条链画出来
这张图非常重要。
你以后无论学内置工具、HTTP 工具、MCP 工具,都还是这条基本链路。
3. 先写一个最小工具
我们先做一个很贴地气的例子:
让 Agent 能查商品价格、库存和发货信息。
3.1 先定义工具入参和出参
java
public record ProductQuery(String productName) {
}
public record ProductResult(String productName, String price, String stock, String delivery) {
}3.2 再写真正的业务方法
java
package com.example.saastudy.tool;
import org.springframework.stereotype.Service;
@Service
public class ProductToolService {
public ProductResult queryProductInfo(ProductQuery query) {
String productName = query.productName() == null ? "unknown" : query.productName();
if (productName.toLowerCase().contains("mac")) {
return new ProductResult(productName, "12999 CNY", "in stock", "Ships in 24 hours");
}
if (productName.contains("咖啡") || productName.toLowerCase().contains("coffee")) {
return new ProductResult(productName, "39 CNY", "in stock", "Same-day delivery");
}
return new ProductResult(productName, "unknown", "need manual check", "unknown");
}
}3.3 把它包成 ToolCallback
java
package com.example.saastudy.config;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.example.saastudy.tool.ProductQuery;
import com.example.saastudy.tool.ProductResult;
import com.example.saastudy.tool.ProductToolService;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
@Configuration
public class ToolAgentConfig {
@Bean
public ToolCallback productTool(ProductToolService productToolService) {
return FunctionToolCallback.builder(
"query_product_info",
(Function<ProductQuery, ProductResult>) productToolService::queryProductInfo)
.description("根据商品名查询价格、库存和发货信息。")
.inputType(ProductQuery.class)
.build();
}
@Bean
public ReactAgent shoppingAgent(ChatModel chatModel, ToolCallback productTool, MemorySaver memorySaver) {
return ReactAgent.builder()
.name("shopping_agent")
.model(chatModel)
.instruction("""
你是一个电商客服 Agent。
如果用户在问价格、库存、发货时间,优先调用 query_product_info。
回答使用中文。
""")
.tools(productTool)
.saver(memorySaver)
.build();
}
}这一段你要重点看 3 个地方:
name:工具名description:工具说明inputType:输入结构
这三个信息,模型都会“看见”。
4. 为什么 description 很重要
很多人第一次写工具,最容易轻视的就是 description。
它不是给程序员看的注释,而是给模型看的“工具使用说明书”。
如果你写成:
java
.description("查询商品")就很可能太模糊。
模型会不知道:
- 这个工具到底能查什么
- 什么情况下该调它
- 需要传什么参数
所以更好的写法是:
java
.description("根据商品名查询价格、库存和发货信息。")再进一层,你甚至可以把边界也写进去,比如:
- 只能按商品名查
- 不能查历史订单
- 返回的数据字段有哪些
5. inputType(...) 到底在帮你做什么
inputType(ProductQuery.class) 的意思不是“只是做个类型声明”。
更关键的是:
框架会根据这个 Java 类型,帮你生成工具的输入 Schema。
所以你写工具入参时,字段名要清楚,结构要稳定。
比如:
java
public record ProductQuery(String productName) {
}这比写一个含糊的 Map<String, Object> 更适合给模型用。
原因很简单:
模型也需要一个“好理解的输入结构”。
6. 什么时候用 FunctionToolCallback,什么时候用 MethodToolCallback
在学习阶段,你先把规则记成这样:
6.1 FunctionToolCallback
适合:
- 你手上已经有一个函数式方法
- 你想快速做最小 demo
- 你要手工定义输入输出类型
6.2 MethodToolCallback
适合:
- 你已经有一个现成的 Spring Bean
- 你希望把对象方法直接暴露成工具
- 团队更强调工具的统一注册和管理
你们公司的 AiAgentBuildBizImpl.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/biz/impl/AiAgentBuildBizImpl.java) 就同时覆盖了这两类思路:
- 内置工具:偏
MethodToolCallback - 自定义外部接口工具:偏
FunctionToolCallback
这非常值得你观察,因为它说明:
工具不只是写一个 demo 方法,而是可以从平台配置动态组装进 Agent。
7. 对照你本地练手项目
你自己的 MinimalAgentService.java(源码路径:D:/idea_space/og_ai_test/chat-demo/src/main/java/com/hy/bigdata/chatdemo/concept/MinimalAgentService.java) 就是一个非常好的最小工具示例。
它做了几件很典型的事:
- 定义
ProductQuery和ProductResult - 用
FunctionToolCallback.builder(...)包装工具 - 用
ReactAgent.builder().tools(productTool)把工具挂进去 - 让 Agent 根据用户问题决定是否调用工具
这说明你的练手方向其实是对的。
你现在缺的不是“是不是懂原理”,而是把这条链看得更透。
8. 工具结果为什么不总是直接出现在最终回答里
这是初学者很容易困惑的一点。
工具返回的是结构化结果,比如:
json
{
"productName": "MacBook Pro",
"price": "12999 CNY",
"stock": "in stock",
"delivery": "Ships in 24 hours"
}但最终给用户的话,往往是模型重新组织过的自然语言。
比如:
“这台 MacBook Pro 当前价格是 12999 CNY,库存充足,预计 24 小时内发货。”
所以你要记住:
工具结果是模型的依据,不一定是最终输出的原样。
9. 你们公司项目里的工具链,比最小 demo 多了什么
先看 AiAgentBuildBizImpl.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/biz/impl/AiAgentBuildBizImpl.java)。
这个类说明了生产级工具链和教程 demo 的差别:
- 工具不是写死在代码里,而是从业务配置里读出来
- 不同工具类型走不同包装逻辑
- 工具执行前后可能还会带日志、鉴权、审计
再看 ToolCallAdvisorV2.java(源码路径:D:/idea_space/ai-center-server/ai-center-api-server/src/main/java/com/hy/bigdata/ai/config/advisor/ToolCallAdvisorV2.java)。
它说明团队已经不满足于“能调工具”了,而是进一步去处理:
- 流式场景下的工具响应
- 工具调用过程的可观测性
- 多轮工具回调中的中间状态
这就是一个非常重要的认知升级:
学工具调用,第一步是会写;第二步是会控;第三步是会管。
10. 常见误区
10.1 以为工具一定会被调用
不会。
模型只有在“觉得这个工具合适”时,才会返回 tool call。
所以:
- 工具描述要写清
- instruction 要写清
- 用户问题也要真的触发工具使用场景
10.2 以为工具名随便取
最好别太随便。
工具名要让模型一眼能猜到用途,比如:
query_product_infoquery_weatherbook_flight
10.3 以为工具写完,业务就结束了
真正的生产工程里,工具后面经常还跟着:
- 鉴权
- 限流
- 超时控制
- 日志记录
- 失败重试
你们项目已经开始往这条路上走了。
11. 本章小结
你现在应该已经把工具调用的本质想明白了:
- 模型负责“决定调哪个工具”
- 框架负责“真的去执行工具”
- 工具结果回到模型上下文,模型再组织最终回答
一旦你把这三句话吃透,后面再学 HTTP 工具、MCP 工具、工具选择优化,就不会乱。
下一章我们把另一件最容易被说玄乎的东西讲透:记忆。