ACP大模型应用开发工程师-从Function Calling到ReAct再到MCP

答疑机器人接上 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):

你需要:

  1. 解析 tool_calls 中的函数名和参数
  2. 在你的代码中实际执行对应的函数
  3. 将函数执行结果包装成一条 role: "tool" 的 message
  4. 再次调用 API,将工具执行结果发送给模型
  5. 模型基于工具返回的结果,生成最终的用户回复

标准流程与用户侧职责可概括为下图(与用户、应用、模型、工具四元交互一致):


image.png

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 是怎么实现刚才的工具调用的:

image.png

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\"}";
        }
    }

问答成功了:


image.png

与之前手动实现的 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;定义与使用解耦,解决的是工具「如何被发现、如何统一管理」的规模化问题。
区别对比:

image.png

理解了 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 类似——选择工具、生成参数、执行调用、返回结果。

image.png

**Java 侧 ReActAgent + DashScopeChatModel 通常无需再显式配置 DashScopeChatFormatter;远程 MCP 的 URL、路径与鉴权头格式以 阿里云 DashScope / 百炼 最新文档为准。

若更希望 与 Spring Boot 主工程统一,同一远程 MCP 也可由 spring-ai-starter-mcp-clientStreamable HTTP / SSE 连接 + ChatClient.toolCallbacks 实现(见 Spring AI MCP Client 文档)。

AgentScope Java(简述)

若更希望 Agent API 采用 AgentScope 命名ReActAgentToolkit),可使用 AgentScope Java 自带的 MCP 能力与 DashScopeChatModel 组合;与 Spring 主工程的集成需自行处理配置与 Bean 边界,见 AgentScope Java 文档

七、小结

阶段 要解决的问题 Java 抓手
硬编码 先跑通「工具→模型」 直接调方法 + ChatClient
路由 谁被调用 尽量别长期靠 if/关键词
结构化 可靠解析 JSON + Jackson + 重试
Function Calling 协议与多轮 tool 对话 @ToolMethodToolCallbackProvidertoolCallbacks(见 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)
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容