从 Function Calling 到 MCP:企业级 AI 智能体的工具调用架构与 Fallback 策略

在趣玩搭平台中,我基于 Spring AI 开发了一个"AI 主理人"智能体,它不仅能回答问题,还能实际操作业务系统——查订单、退款、改活动信息。本文记录 Tool Calling 和 MCP 的落地实践,以及工具调用失败时的 Fallback 设计。

一、业务需求:从"会聊天"到"会干活"

1.1 智能客服的局限

前面博客中介绍的 RAG 智能客服解决了"回答问题"的需求。但运营团队很快提出了新需求:能不能让 AI 不只是告诉用户"退款政策是什么",而是直接帮用户"查一下退款状态"甚至"发起退款申请"?

运营同事每天花大量时间做重复性操作:查订单状态、查退款进度、修改活动信息、回复用户投诉。这些操作都是"查数据库 → 判断条件 → 执行操作"的固定模式,非常适合交给 AI 来做。

1.2 AI 主理人的能力设计

我设计的"AI 主理人"定位是一个能对话、能查询、能操作的企业级智能体。它能做三类事:

信息查询类: 查订单详情、查退款进度、查活动报名人数、查用户积分余额。

业务操作类: 发起退款申请、修改活动时间/地点、给用户补发积分、发送通知消息。

分析建议类: 基于 RAG 知识库回答业务规则问题、基于数据给出运营建议。

二、Tool Calling 基础实现

2.1 Spring AI 的 Tool Calling 机制

Spring AI 对 Function Calling(工具调用)有原生支持。核心概念是:告诉大模型"你有哪些工具可以用",模型根据用户意图自主决定是否调用工具、调用哪个工具、传什么参数。

// 定义一个工具:查询订单详情
@Component
public class OrderQueryTool implements Function<OrderQueryRequest, OrderQueryResponse> {
​
    @Override
    @Description("根据订单号查询订单详情,包括活动名称、支付金额、订单状态、创建时间等信息")
    public OrderQueryResponse apply(OrderQueryRequest request) {
        Order order = orderService.getByOrderNo(request.getOrderNo());
        if (order == null) {
            return new OrderQueryResponse(false, "未找到该订单", null);
        }
        return new OrderQueryResponse(true, "查询成功", convertToVO(order));
    }
}
​
public record OrderQueryRequest(
    @JsonPropertyDescription("订单编号,格式如 QWD202501150001") String orderNo
) {}
// 定义另一个工具:发起退款
@Component
public class RefundApplyTool implements Function<RefundApplyRequest, RefundApplyResponse> {
​
    @Override
    @Description("为指定订单发起退款申请。需要订单号和退款原因。只有已支付且未使用的订单可以退款。")
    public RefundApplyResponse apply(RefundApplyRequest request) {
        // 1. 权限校验
        if (!permissionService.canOperate(getCurrentOperator(), "REFUND")) {
            return new RefundApplyResponse(false, "您没有退款操作权限");
        }
​
        // 2. 业务校验
        Order order = orderService.getByOrderNo(request.getOrderNo());
        if (order == null) {
            return new RefundApplyResponse(false, "订单不存在");
        }
        if (order.getStatus() != OrderStatus.PAID) {
            return new RefundApplyResponse(false, "订单状态不可退款,当前状态: " + order.getStatus());
        }
​
        // 3. 执行退款
        RefundResult result = refundService.applyRefund(order.getId(), request.getReason());
        return new RefundApplyResponse(result.isSuccess(), result.getMessage());
    }
}

2.2 注册工具到 ChatClient

@Configuration
public class AiAgentConfig {
​
    @Bean
    public ChatClient agentChatClient(
            ChatModel chatModel,
            OrderQueryTool orderQueryTool,
            RefundApplyTool refundApplyTool,
            ActivityInfoTool activityInfoTool,
            UserPointsTool userPointsTool,
            SendNotificationTool sendNotificationTool) {
​
        return ChatClient.builder(chatModel)
            .defaultSystem(loadSystemPrompt())
            .defaultFunctions(
                "queryOrder", orderQueryTool,
                "applyRefund", refundApplyTool,
                "queryActivity", activityInfoTool,
                "queryUserPoints", userPointsTool,
                "sendNotification", sendNotificationTool
            )
            .build();
    }
}

当用户对 AI 主理人说"帮我查一下订单 QWD202501150001 的退款进度"时,大模型会:

  1. 分析意图 → 需要查询订单信息

  2. 选择工具 → queryOrder

  3. 提取参数 → orderNo = "QWD202501150001"

  4. 调用工具 → 得到订单详情

  5. 组织回答 → "订单 QWD202501150001 的退款申请已于 1 月 16 日提交,当前状态为"退款处理中",预计 1-3 个工作日到账。"

三、MCP 的引入:为什么 Function Calling 不够用

3.1 Function Calling 的局限

随着工具数量增长到十几个,Function Calling 的几个问题开始暴露:

工具注册是硬编码的。 每增加一个工具,都要在 AiAgentConfig 中手动注册,重新部署。运营想加一个"查活动评价"的工具,要等开发排期。

工具之间的调用是扁平的。 所有工具直接暴露给大模型,大模型需要从十几个工具中选择。工具越多,模型选错工具的概率越大。

跨系统的工具集成困难。 趣玩搭的后台管理系统、财务系统、CRM 系统分属不同的代码仓库和团队,把所有系统的能力都封装成 Function 注册到一个 ChatClient 中,架构上非常耦合。

3.2 MCP 是什么

MCP(Model Context Protocol)是 Anthropic 提出的一个标准协议,定义了 AI 模型和外部工具/数据源之间的通信规范。简单来说,MCP 把"工具提供方"和"AI 应用方"解耦了:

传统 Function Calling:
AI 应用 ←→ [工具A, 工具B, 工具C, ...] (全部硬编码在同一个应用中)
​
MCP 架构:
AI 应用 ←→ MCP Client ←→ MCP Server A(订单相关工具)
                       ←→ MCP Server B(财务相关工具)
                       ←→ MCP Server C(CRM 相关工具)

每个 MCP Server 独立部署、独立开发、独立注册工具。AI 应用通过 MCP Client 动态发现和调用这些工具,不需要硬编码。

3.3 MCP vs Function Calling 的核心区别

维度

Function Calling

MCP

工具注册方式

硬编码在应用中

通过协议动态发现

工具提供方

必须在同一个应用内

可以是独立的远程服务

新增工具

修改代码 + 重新部署

MCP Server 独立部署,AI 应用无需变更

跨系统集成

困难(需要在 AI 应用中封装所有系统的 SDK)

自然(每个系统提供自己的 MCP Server)

标准化

各 LLM 厂商格式不同

统一协议,与 LLM 无关

适用场景

工具少、单体应用

工具多、多系统、需要动态扩展

四、MCP 落地实现

4.1 MCP Server:按业务域拆分

我将工具按业务域拆分为三个 MCP Server:

趣玩搭 AI 主理人
    │
    ├── MCP Server: order-tools(订单域)
    │   ├── queryOrder        查询订单详情
    │   ├── queryOrderList    查询订单列表
    │   ├── applyRefund       发起退款
    │   └── queryRefundStatus 查询退款进度
    │
    ├── MCP Server: activity-tools(活动域)
    │   ├── queryActivity     查询活动详情
    │   ├── updateActivity    修改活动信息
    │   ├── querySignupList   查询报名列表
    │   └── closeActivity     关闭活动
    │
    └── MCP Server: user-tools(用户域)
        ├── queryUserPoints   查询积分
        ├── adjustPoints      积分调整
        └── sendNotification  发送通知

每个 MCP Server 是一个独立的 Spring Boot 应用,由对应业务域的开发同事维护。

以 order-tools 为例:

@SpringBootApplication
public class OrderMcpServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderMcpServerApplication.class, args);
    }
}
​
@McpServer
@Component
public class OrderToolProvider {
​
    @Tool(description = "根据订单号查询订单详情,包括活动名称、支付金额、订单状态等")
    public OrderDetail queryOrder(
            @Param(description = "订单编号") String orderNo) {
        Order order = orderService.getByOrderNo(orderNo);
        if (order == null) throw new ToolException("未找到订单: " + orderNo);
        return convertToDetail(order);
    }
​
    @Tool(description = "为已支付订单发起退款申请,需提供退款原因")
    public RefundResult applyRefund(
            @Param(description = "订单编号") String orderNo,
            @Param(description = "退款原因") String reason) {
        // 权限校验 + 业务校验 + 执行退款
        // ...
    }
}

4.2 AI 主理人(MCP Client)

@Service
public class AiAgentService {
​
    private final ChatClient chatClient;
    private final McpSyncClient orderMcpClient;
    private final McpSyncClient activityMcpClient;
    private final McpSyncClient userMcpClient;
​
    /**
     * 处理用户对话
     */
    public String chat(String userId, String message, List<ChatMessage> history) {
        // 1. 动态获取所有可用工具(从各 MCP Server 发现)
        List<McpTool> allTools = new ArrayList<>();
        allTools.addAll(orderMcpClient.listTools());
        allTools.addAll(activityMcpClient.listTools());
        allTools.addAll(userMcpClient.listTools());
​
        // 2. 构建带上下文的请求
        String response = chatClient.prompt()
            .system(buildSystemPrompt(userId))
            .messages(history)
            .user(message)
            .functions(convertToFunctions(allTools))  // 动态注册工具
            .call()
            .content();
​
        return response;
    }
}

关键区别在于:工具列表不是启动时硬编码的,而是每次对话时动态从各 MCP Server 获取。如果活动域团队新增了一个 queryActivityReview(查询活动评价)工具,只需要在 activity-tools 这个 MCP Server 中加代码并部署,AI 主理人会自动发现并使用这个新工具,完全不需要改 AI 应用的代码。

4.3 实际交互示例

运营同事对 AI 主理人说:

"用户 13912345678 投诉说报名了 1 月 20 号的露营活动但收到退款了,帮我查一下怎么回事"

AI 主理人的内部调用链:

1. 意图分析:需要查订单 + 查退款状态
2. 调用 order-tools/queryOrderList(userId=关联用户ID, keyword="露营")
   → 返回订单 QWD202501180032,状态: TIMEOUT_CLOSED
3. 调用 order-tools/queryRefundStatus(orderNo="QWD202501180032")
   → 返回退款记录:超时自动退款,已到账
4. 调用 activity-tools/queryActivity(activityId=从订单中获取)
   → 返回活动信息:1月20号露营,已满员
5. 组织回答

返回给运营同事:

"查到了。该用户于 1 月 18 日报名了'1 月 20 日周末露营'活动(订单号 QWD202501180032),但超过 15 分钟未完成支付,系统自动关闭了订单并退款。退款 ¥199 已于 1 月 18 日到账。活动目前已满员。建议联系用户确认是否支付过程中遇到了问题,如需重新报名需等待有人退出后释放名额。"

一次对话中智能体自主决定调用了三个不同域的工具,拼接出了完整的事件脉络,并给出了处理建议。

五、工具调用失败的 Fallback 策略

5.1 失败场景分类

工具调用失败在生产环境中是常见的:MCP Server 临时不可用、数据库查询超时、外部 API 限流、参数校验失败等。我将失败场景分为三类,分别设计了不同的处理策略。

5.2 Fallback 策略矩阵

@Component
public class ToolCallFallbackHandler {

    /**
     * 工具调用异常的统一处理
     */
    public ToolCallResult handleFailure(String toolName, Object params, Throwable ex) {
        FailureType type = classifyFailure(ex);

        return switch (type) {
            case TRANSIENT -> handleTransientFailure(toolName, params, ex);
            case PERMISSION -> handlePermissionFailure(toolName, ex);
            case PERMANENT -> handlePermanentFailure(toolName, ex);
        };
    }

    /**
     * 临时性失败(网络超时、服务暂时不可用):重试一次
     */
    private ToolCallResult handleTransientFailure(String toolName, Object params, Throwable ex) {
        log.warn("工具 {} 调用失败(临时性),准备重试: {}", toolName, ex.getMessage());

        try {
            Thread.sleep(1000);
            // 重试一次
            return retryToolCall(toolName, params);
        } catch (Exception retryEx) {
            // 重试也失败了,降级为提示用户
            log.error("工具 {} 重试失败", toolName, retryEx);
            return ToolCallResult.degraded(
                String.format("抱歉,%s 功能暂时不可用,请稍后再试。如急需处理,建议联系人工客服。",
                    getToolDisplayName(toolName)));
        }
    }

    /**
     * 权限不足:直接告知,不重试
     */
    private ToolCallResult handlePermissionFailure(String toolName, Throwable ex) {
        return ToolCallResult.denied(
            String.format("您没有执行 %s 的权限,请联系管理员。",
                getToolDisplayName(toolName)));
    }

    /**
     * 永久性失败(参数错误、业务规则不满足):告知原因
     */
    private ToolCallResult handlePermanentFailure(String toolName, Throwable ex) {
        return ToolCallResult.failed(
            String.format("操作无法完成:%s", ex.getMessage()));
    }

    private FailureType classifyFailure(Throwable ex) {
        if (ex instanceof TimeoutException || ex instanceof ConnectException) {
            return FailureType.TRANSIENT;
        }
        if (ex instanceof PermissionDeniedException) {
            return FailureType.PERMISSION;
        }
        return FailureType.PERMANENT;
    }
}

5.3 MCP Server 不可用时的整体降级

如果某个 MCP Server 完全挂了(不是单次调用失败,而是连续多次失败),智能体应该感知到并主动规避:

@Component
public class McpServerHealthChecker {

    // 记录各 MCP Server 的健康状态
    private final Map<String, CircuitBreaker> circuitBreakers = new ConcurrentHashMap<>();

    /**
     * 获取当前可用的工具列表(排除不健康的 MCP Server)
     */
    public List<McpTool> getAvailableTools() {
        List<McpTool> availableTools = new ArrayList<>();

        for (Map.Entry<String, McpSyncClient> entry : mcpClients.entrySet()) {
            String serverName = entry.getKey();
            CircuitBreaker breaker = circuitBreakers.computeIfAbsent(
                serverName, k -> new CircuitBreaker(3, 60000)); // 3次失败熔断60秒

            if (breaker.isOpen()) {
                log.warn("MCP Server {} 已熔断,跳过工具注册", serverName);
                continue;
            }

            try {
                availableTools.addAll(entry.getValue().listTools());
                breaker.recordSuccess();
            } catch (Exception e) {
                breaker.recordFailure();
                log.error("MCP Server {} 不可用: {}", serverName, e.getMessage());
            }
        }

        return availableTools;
    }
}

当 order-tools 挂了时,智能体不会注册订单相关的工具。如果用户问了订单相关的问题,大模型发现没有对应的工具,会回答"订单查询功能暂时不可用,请稍后再试或联系人工客服"。这比调用一个必然失败的工具然后报错要优雅得多。

5.4 多工具链路中的部分失败

智能体经常需要调用多个工具组合完成一个任务。如果链路中间某个工具失败了怎么办?

我在 System Prompt 中明确告诉了大模型如何处理部分失败:

如果你调用了多个工具,其中某个工具返回了错误或不可用的信息:
1. 不要因为一个工具失败就放弃整个任务
2. 把已经获取到的信息告诉用户
3. 明确说明哪部分信息暂时无法获取
4. 如果关键信息缺失导致无法给出结论,建议用户联系人工客服

例如:查到了订单信息但退款服务暂时不可用时,应回复:
"您的订单 XXX 状态为已支付。关于退款进度查询功能暂时维护中,
建议您稍后再试,或联系人工客服(电话:400-xxx-xxxx)。"

六、成本与效果

6.1 工具调用的额外 LLM 成本

Tool Calling 会增加 LLM 的 token 消耗——每次对话需要在 System Prompt 中携带所有可用工具的描述(约 800-1200 token),模型返回工具调用指令后还需要一轮将工具结果喂回模型的交互。

实测每次带工具调用的对话比纯问答多消耗约 1500-2000 token(input + output),月度 LLM 成本从 ¥5,500 增加到约 ¥7,200。在 ¥8,000 的预算内。

6.2 运营效率提升

AI 主理人上线后的运营数据:

指标

上线前

上线后

运营处理单条用户投诉平均耗时

8 分钟

2 分钟

日均人工查询订单操作次数

~200 次

~40 次(80%由 AI 完成)

客服响应时效

15 分钟

实时(7×24)

七、经验总结

Function Calling 适合工具少的单体应用,MCP 适合多系统集成的企业场景。 如果只有 3-5 个工具且不会频繁新增,Function Calling 完全够用。超过 10 个工具且涉及多个团队的系统,MCP 的解耦价值就体现出来了。

工具描述(description)比代码实现更重要。 大模型是根据工具的文本描述来决定调不调、怎么调的。描述写得好,模型选对工具的概率就高。我们花了大量时间反复调优每个工具的 description。

Fallback 不是可选项。 在生产环境中,工具调用失败是常态而不是异常。没有 Fallback 的智能体就是一个"晴天娃娃"——天气好时很可爱,一下雨就垮了。

权限控制在工具层而不是在 AI 层。 不要指望通过 Prompt 告诉大模型"这个用户没有退款权限"——Prompt 可以被绕过。权限校验必须在每个工具的代码中硬编码。


如果这篇文章对你有帮助,欢迎访问我的博客 robinzhu.top 获取更多实战分享。