Skip to content

TIP

本页内容由本地原稿 03-工具调用.md 同步生成。 如果你要长期修改站点内容,优先回原稿修改,再执行 npm run docs:sync

第 3 章:工具不是外挂,而是给 Agent 装手脚

这章要解决什么问题

如果一个 Agent 只能“根据训练数据聊天”,那它再聪明也只能停留在“会说”。
一旦你要它:

  • 查商品价格
  • 调公司内部接口
  • 查库存
  • 触发某个业务动作

它就必须有工具。

这章我们要讲清三件事:

  1. 工具调用到底是怎么发生的
  2. Spring AI Alibaba 里工具怎么写
  3. 你们公司项目里工具是怎么工程化接进去的

1. 先用一句人话理解工具调用

工具调用不是:

  • 模型直接自己执行 Java 方法
  • 模型真的“会查数据库”
  • 模型自己连 HTTP 接口

工具调用的真实过程是:

  1. 模型读到你的问题
  2. 模型判断“这事我应该用某个工具”
  3. 框架根据模型返回的 tool call 信息,去执行真正的 Java 方法或函数
  4. 工具结果再回到模型上下文里
  5. 模型基于工具结果生成最终回答

也就是说:

模型负责“决定调不调工具”,框架负责“真的去执行工具”。

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) 就是一个非常好的最小工具示例。

它做了几件很典型的事:

  • 定义 ProductQueryProductResult
  • 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_info
  • query_weather
  • book_flight

10.3 以为工具写完,业务就结束了

真正的生产工程里,工具后面经常还跟着:

  • 鉴权
  • 限流
  • 超时控制
  • 日志记录
  • 失败重试

你们项目已经开始往这条路上走了。

11. 本章小结

你现在应该已经把工具调用的本质想明白了:

  1. 模型负责“决定调哪个工具”
  2. 框架负责“真的去执行工具”
  3. 工具结果回到模型上下文,模型再组织最终回答

一旦你把这三句话吃透,后面再学 HTTP 工具、MCP 工具、工具选择优化,就不会乱。

下一章我们把另一件最容易被说玄乎的东西讲透:记忆。

Built with VitePress. Deployed on Cloudflare Pages.