从 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 的退款进度"时,大模型会:
分析意图 → 需要查询订单信息
选择工具 →
queryOrder提取参数 →
orderNo = "QWD202501150001"调用工具 → 得到订单详情
组织回答 → "订单 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 的核心区别
四、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 主理人上线后的运营数据:
七、经验总结
Function Calling 适合工具少的单体应用,MCP 适合多系统集成的企业场景。 如果只有 3-5 个工具且不会频繁新增,Function Calling 完全够用。超过 10 个工具且涉及多个团队的系统,MCP 的解耦价值就体现出来了。
工具描述(description)比代码实现更重要。 大模型是根据工具的文本描述来决定调不调、怎么调的。描述写得好,模型选对工具的概率就高。我们花了大量时间反复调优每个工具的 description。
Fallback 不是可选项。 在生产环境中,工具调用失败是常态而不是异常。没有 Fallback 的智能体就是一个"晴天娃娃"——天气好时很可爱,一下雨就垮了。
权限控制在工具层而不是在 AI 层。 不要指望通过 Prompt 告诉大模型"这个用户没有退款权限"——Prompt 可以被绕过。权限校验必须在每个工具的代码中硬编码。
如果这篇文章对你有帮助,欢迎访问我的博客 robinzhu.top 获取更多实战分享。