答疑机器人接上 RAG 后,能解决「手册里写过」的问题,但遇到实时联网、查库、调业务 API 时,仅靠生成式模型不够——需要让模型「指挥」外部工具。
我把课程里 1~3 节的路演进给捋清了,并在 第四节自然衔接到行业标准 Function Calling,全部用 Java + Spring AI / Spring AI Alibaba(通义 OpenAI 兼容) 写示例。
前言:为什么需要工具
大模型本质是在给定上下文里续写文本。RAG 解决的是「把知识塞进上下文」,但模型不能凭空发起 HTTP、查数据库、下单。要实现「搜集最新 Transformer 资料」「搜 Arxiv」这类任务,必须在应用侧提供可被调用的函数,并让模型(或路由器)决定在何时、以何种参数调用它们。
本节路线:硬编码单工具 → 脆弱路由 → 结构化 JSON + 校验重试 → Function Calling → ReAct 循环。
四、主流方案:Function Calling(行业标准)
云平台把「工具 Schema + 模型是否调用 + 第二轮带 tool 结果再生成」封装进 API——你不必自己拼上面的 JSON 协议(但仍要执行函数并把结果回传)。
以 OpenAI SDK 的函数调用为例:
工具定义 (Tool Definition): 你需要在 API 的 tools 参数中定义可用的工具,使用 JSON Schema 描述每个工具的 name、description 以及 parameters(函数所需的输入参数结构)。
调用决策 (Call Decision): 模型根据用户输入和工具定义,自动决策是否需要调用工具。如果需要,模型会在响应中返回 tool_calls 字段,包含要调用的函数名和符合 Schema 的参数 JSON。
执行与返回 (Execute & Return):
你需要:
- 解析 tool_calls 中的函数名和参数
- 在你的代码中实际执行对应的函数
- 将函数执行结果包装成一条 role: "tool" 的 message
- 再次调用 API,将工具执行结果发送给模型
- 模型基于工具返回的结果,生成最终的用户回复
标准流程与用户侧职责可概括为下图(与用户、应用、模型、工具四元交互一致):

4.1 Spring AI Alibaba:@Tool + MethodToolCallbackProvider
我的个人项目中 Mother Agent 已采用该模式:方法上 @Tool,运行时通过 MethodToolCallbackProvider 注册为回调,再在 ChatClient.prompt()...toolCallbacks(...).call() 中交给框架与模型交互(框架会在内部完成多轮 tool 协议,等价于手写「第二次请求」)。
方法定义可参考:
@Tool(name = "getPointsBalance", description = "查询当前用户积分余额")
public String getPointsBalance() {
long uid = MotherAgentUserContext.requireUserId();
long b = pointsDomainService.getBalance(uid);
return json(Map.of("userId", uid, "balance", b));
}
编排侧:
var toolProvider = MethodToolCallbackProvider.builder().toolObjects(motherAgentTools).build();
return llmRuntimeResolver.forBurstAgent()
.prompt()
.system(systemPrompt)
.user(augmentedUser)
.toolCallbacks(toolProvider)
.call()
.content();
还需要做的:把「联网搜索」「Arxiv」等真正实现成带 @Tool 的 Java 方法,描述写清楚,ChatModel 指向通义(DashScope OpenAI 兼容)即可沿用同一套路。
五、ReAct:思考 → 调用 → 观察(循环)
你会发现,工具调用的结果是通过又一次调用传递给大模型的,大模型会观察工具调用的结果,然后思考任务是否完成,从而回复你最终答案或继续行动(调用工具)。这和你之前学过的"多轮对话"很相似。
我们把这种思考——行动——观察的循环模式称为 ReAct,按照此模式工作的 Agent 称为 ReAct Agent。
手动实现 ReAct Agent 的逻辑比较复杂。为了简化开发流程,我们将使用 AgentScope 这一生产级 Agent 框架——它已经帮你封装好了 ReAct Agent 和工具调用的完整逻辑。
AgentScope 是一套为开发者设计的、生产级别的 Agent 框架。它通过规范化的方式定义智能体的通信、记忆和工具调用,让你能专注于业务逻辑而非底层实现。AgentScope 的核心优势包括:
- 开箱即用的 ReAct Agent:内置了完整的"思考-行动-观察"循环逻辑
- 灵活的工具管理:通过
Toolkit类统一管理工具函数,支持自动解析工具的 JSON Schema- 多模型支持:兼容 OpenAI、DashScope(通义千问)、Anthropic 等主流 LLM API
- 状态管理:自动处理对话历史、工具调用记录等状态
- 异步支持:所有核心功能都支持异步调用,提升性能
让我们来看一下 AgentScope 是怎么实现刚才的工具调用的:

AgentScope Java代码样例实现:
依赖(与 AgentScope Java 安装文档 一致;版本号请按 Central 最新修订):
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
<version>1.0.12</version>
</dependency>
示例代码(工具名、用户问句与课件一致;Arxiv 结果仍为模拟 JSON,与课件 tool_result 对齐,便于对照输出):
@Test
@DisplayName("AgentScope:search_arxiv_paper 模拟工具 + ReAct 最终答复")
void reactAgentCallsArxivToolAndReplies() {
// 与仓库内其他 debug 测试一致:多环境变量兜底,便于本机 / CI 未配 key 时用 Assumptions 跳过而非失败
String apiKey = firstNonBlank(
System.getenv("AGENTSCOPE_DEBUG_API_KEY"),
System.getenv("REASONING_DEBUG_API_KEY"),
System.getenv("PROMPT_DEBUG_API_KEY"),
System.getenv("DASHSCOPE_API_KEY")
);
Assumptions.assumeTrue(
apiKey != null && !apiKey.isBlank(),
"未配置密钥:请设置 DASHSCOPE_API_KEY(或 REASONING_DEBUG_API_KEY / PROMPT_DEBUG_API_KEY / AGENTSCOPE_DEBUG_API_KEY)后重跑"
);
// 默认 qwen-plus,与博客示例一致;可按账号开通情况改为 qwen-max 等
String modelName = firstNonBlank(System.getenv("AGENTSCOPE_ARXIV_DEMO_MODEL"), "qwen-plus");
// Toolkit:注册带 @Tool 的 Bean;框架据此生成 JSON Schema 并在对话中下发给模型
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new PaperSearchTools());
// DashScope 模型 + 可选代理/非流式等(见 buildDashScopeChatModel),避免 VPN 下 JDK HttpClient 握手失败时无排查手段
DashScopeChatModel model = buildDashScopeChatModel(apiKey, modelName);
// ReActAgent:绑定 sysPrompt、模型与 toolkit;后续 call 时由框架驱动 tool_calls 与结果回灌
ReActAgent agent = ReActAgent.builder()
.name("ResearchAssistant")
.sysPrompt("你是课程研究助理;请根据工具返回的 JSON 准确、简洁地回答用户。")
.model(model)
.toolkit(toolkit)
.build();
// 用户轮:仅文本;多模态场景可在此扩展 Msg 的 content blocks
Msg userMsg = Msg.builder()
.textContent("帮我找一下那篇经典的 Transformer 论文 'Attention Is All You Need'")
.build();
// block():同步等待整条 ReAct 管线结束(底层仍为 Reactor);未配网络或 VPN 干扰时可能抛 SSL/超时异常
Msg response = Objects.requireNonNull(agent.call(userMsg).block(), "Agent 返回为空");
// 最终助手可见回复;中间轮次的 tool 结果在框架内部已写入对话状态
String text = Objects.requireNonNullElse(response.getTextContent(), "");
System.out.println("=== AgentScope 最终文本 ===");
System.out.println(text);
// 断言刻意宽松:不同模型措辞可能略变,只要包含 arXiv id 或标题关键词即视为工具链路生效
Assertions.assertFalse(text.isBlank(), "模型未返回可见文本");
Assertions.assertTrue(
text.contains("1706.03762") || text.toLowerCase().contains("attention"),
"预期答复中应出现论文 id 或标题关键词,实际: " + text
);
}
/**
* 对应课件 tools[].function:name / description / parameters(由注解生成 Schema)。
*/
static class PaperSearchTools {
@Tool(name = "search_arxiv_paper", description = "在 Arxiv.org 上搜索学术论文")
public String searchArxivPaper(
@ToolParam(name = "query", description = "论文的标题或关键词") String query) {
if (query != null && query.toLowerCase().contains("attention")) {
return "{\"paper_id\":\"1706.03762\","
+ "\"url\":\"https://arxiv.org/abs/1706.03762\","
+ "\"title\":\"Attention Is All You Need\"}";
}
return "{\"paper_id\":\"unknown\",\"url\":\"\",\"title\":\"not found\"}";
}
}
问答成功了:

与之前手动实现的 OpenAI Function Calling 相比,AgentScope 的优势在于:
- 工具定义更简洁:只需要写带文档字符串的普通 java 函数,框架会自动解析生成 JSON Schema
- 无需手动解析:ReActAgent 内部自动处理 tool_calls 的解析、函数执行、结果包装等繁琐步骤
- 自动管理对话历史:框架会自动记录用户消息、工具调用、工具结果等,无需手动维护
- 支持多轮工具调用:如果一次工具调用不够,Agent 会自动继续思考并调用更多工具,直到完成任务。
看到 AgentScope 的简洁实现,你可能会疑惑:既然有现成框架,为什么还要学习前面那套繁琐的手动实现?
这是因为:
1.理解底层原理:框架内部就是在执行"调用模型 → 解析 tool_calls → 执行函数 → 再次调用模型"这套流程。了解机制才能调试问题。
2.自定义需求:生产环境常需实现权限验证、缓存、重试、日志监控等特殊逻辑,理解底层才能扩展框架。
3.兼容性保障:部分模型或平台不支持标准 Function Calling 格式时,手动实现可作为降级方案。
六、MCP 协议:工具的规模化管理
你已经用 Function Calling 实现了
web_search,并在tools参数里写好 JSON Schema,模型按需调用。到这一步,「单次对话里如何调工具」已经跑通;但当工具数量变多、版本迭代变快、多方复用同一能力时,谁在维护 Schema、谁保证描述质量会变成新的瓶颈——这就自然过渡到 MCP(Model Context Protocol,模型上下文协议)。
6.1 工具复用的挑战
在 Agent 侧硬编码某一版 web_search 的 Schema,会带来明显维护成本:字段变更、描述优化、多环境 endpoint 都要改应用代码并重新发布。更麻烦的是,工具提供方无法主动进入 AI 应用生态:只能等每个 Agent 开发者自行拷贝 Schema,且各家的 description 质量参差不齐,模型路由效果不稳定。
简言之:Function Calling 解决了「调用格式」;没有 MCP 时,「工具的发现、版本与归属」仍散落在各 Agent 仓库里,难以规模化。
6.2 MCP 的解耦思想
为缓解上述问题,Anthropic 推动的 MCP (Model Context Protocol,模型上下文协议),把核心原则说得很直白:谁提供工具,谁定义工具。工具定义的职责从 Agent(消费方) 转移到 工具服务(提供方)。
在 MCP 术语里:
- MCP Server:对外声明工具(名称、描述、参数 Schema),并负责执行或转发真实能力;
- MCP Client:连接 Server,动态发现(list tools) 并拉取最新定义,再把这些工具挂到 Agent 可用的「工具面」上。
这样,Agent 只需集成 Client,而不必在代码里长期硬编码每个工具的 JSON Schema;定义与使用解耦,解决的是工具「如何被发现、如何统一管理」的规模化问题。
区别对比:

理解了 MCP 的解耦思想后,你希望用 MCP 重构之前的 web_search,以降低后续的维护成本。
6.3 用 MCP 重构 web_search 工具调用
运行此代码前,请先前往阿里云百炼官网开通联网搜索 MCP 服务,并了解其计费详情。
代码实现:
pom.xml(版本请与团队 Spring Boot / Spring AI BOM 对齐,下以 3.5.x / 1.1.2 为例):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
配置文件resources/application.yml:
src/main/resources/application.properties:
spring:
ai:
mcp:
client:
enabled: true
type: SYNC
toolcallback:
enabled: true
stdio:
connections:
mock-web-search:
command: java
args:
- -Dspring.main.web-application-type=none
- -Dspring.main.banner-mode=off
- -Dlogging.pattern.console=
- -jar
- /absolute/path/to/mock-web-search-mcp-server-0.0.1-SNAPSHOT.jar
env: {}
dashscope:
api-key: ${DASHSCOPE_API_KEY}
代码实现--目前直接引用云端实现:
完整示例(main 可独立运行;需有效 DASHSCOPE_API_KEY 且网络能访问 DashScope;若遇 TLS/代理问题参见 ArxivToolCallingAgentScopeTest 类注释中的环境变量说明):
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.mcp.McpClientBuilder;
import io.agentscope.core.tool.mcp.McpClientWrapper;
import java.util.Objects;
public class DashScopeWebSearchMcpAgentScopeDemo {
private static final String WEB_SEARCH_MCP_URL =
"https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp";
public static void main(String[] args) {
String apiKey = System.getenv("DASHSCOPE_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("请设置环境变量 DASHSCOPE_API_KEY");
}
// 1. Streamable HTTP MCP 客户端(对标样例实现中的 HttpStatelessClient + streamable_http)
McpClientWrapper webSearchClient = McpClientBuilder.create("web_search_service")
.streamableHttpTransport(WEB_SEARCH_MCP_URL)
.header("Authorization", "Bearer " + apiKey)
.buildAsync()
.block();
// 2. 注册到 Toolkit,由框架向 Server 发现工具并映射为可调工具
Toolkit toolkit = new Toolkit();
toolkit.registerMcpClient(webSearchClient).block();
DashScopeChatModel model = DashScopeChatModel.builder()
.apiKey(apiKey)
.modelName("qwen-plus")
.build();
ReActAgent agent = ReActAgent.builder()
.name("Research Assistant Agent")
.sysPrompt("你是一个课程研究助理,擅长使用工具搜集和整理最新的教学素材。")
.model(model)
.toolkit(toolkit)
.build();
String userRequest = """
我正在为'大模型原理'课程搜集素材,需要一个调用外部实时数据的例子,比如帮我搜索一下最近关于'大型语言模型'的最新进展。
""";
Msg msg = Msg.builder().textContent(userRequest).build();
System.out.println("用户请求: " + userRequest + "\n");
Msg response = Objects.requireNonNull(agent.call(msg).block(), "Agent 返回为空");
System.out.println(response.getTextContent());
toolkit.removeMcpClient("web_search_service").block();
}
}
Agent 启动时,MCP Client 自动连接 MCP Server,拉取工具清单(名称、描述、参数)。拿到清单后,后续流程与 1.2 的 Function Calling 类似——选择工具、生成参数、执行调用、返回结果。

**Java 侧 ReActAgent + DashScopeChatModel 通常无需再显式配置 DashScopeChatFormatter;远程 MCP 的 URL、路径与鉴权头格式以 阿里云 DashScope / 百炼 最新文档为准。
若更希望 与 Spring Boot 主工程统一,同一远程 MCP 也可由 spring-ai-starter-mcp-client 的 Streamable HTTP / SSE 连接 + ChatClient.toolCallbacks 实现(见 Spring AI MCP Client 文档)。
AgentScope Java(简述)
若更希望 Agent API 采用 AgentScope 命名(ReActAgent、Toolkit),可使用 AgentScope Java 自带的 MCP 能力与 DashScopeChatModel 组合;与 Spring 主工程的集成需自行处理配置与 Bean 边界,见 AgentScope Java 文档。
七、小结
| 阶段 | 要解决的问题 | Java 抓手 |
|---|---|---|
| 硬编码 | 先跑通「工具→模型」 | 直接调方法 + ChatClient |
| 路由 | 谁被调用 | 尽量别长期靠 if/关键词 |
| 结构化 | 可靠解析 | JSON + Jackson + 重试 |
| Function Calling | 协议与多轮 tool 对话 |
@Tool、MethodToolCallbackProvider、toolCallbacks(见 4.1~4.3);或 AgentScope Toolkit + ReActAgent(见 4.4) |
| ReAct | 多步工具 | Spring AI 内建多轮;AgentScope Java 内建 ReAct(或手写循环) |
| MCP | 工具定义与发现的规模化、多应用复用 | MCP Server 维护 Schema;Java 用 spring-ai-starter-mcp-client(§6) |