作者:架构源启-12年OTA公司资深程序员
技术栈:Spring Boot 3.5.9 + Spring AI 1.1.4 + GPT-5.5
前置知识:已完成前五篇博客


在这里插入图片描述

📖 前言

在之前的文章中,我们学习了如何让 AI 聊天、生成图片、流式输出。但这些都是"被动"的——AI 只能回答问题,不能执行实际操作。

想象一下这些场景

  • 🏨 用户说:“帮我退1201房间”,AI 直接调用退房接口
  • 📅 用户说:“预约明天上午打扫”,AI 自动创建清洁任务
  • 🔍 用户说:“查询我的订单”,AI 实时查询数据库并返回结果
  • 💰 用户说:“续住2天”,AI 计算费用并办理续住

这就是 Function Calling(函数调用) 的魅力!它让 AI 从"问答机器人"升级为"智能助手"。

本文你将学到

✅ Function Calling 的核心原理
✅ Spring AI 函数注册机制
✅ 酒店预订场景完整实战
✅ 意图识别与参数提取
✅ 多函数组合调用
✅ 错误处理与重试机制
✅ 实战:智能酒店助手

准备好了吗?让我们开始吧!🚀


🎯 一、Function Calling 是什么?

1.1 传统方式的局限

传统流程

用户输入 → AI 理解 → 返回文本 → 程序解析 → 执行业务

问题

  • ❌ 需要复杂的自然语言处理
  • ❌ 意图识别准确率低
  • ❌ 参数提取困难
  • ❌ 容易出错

1.2 Function Calling 的优势

新流程

用户输入 → AI 理解 → 选择函数 → 提取参数 → 调用函数 → 返回结果

优势

  • ✅ AI 自动选择要调用的函数
  • ✅ AI 自动提取参数
  • ✅ 类型安全
  • ✅ 结构化输出
  • ✅ 更准确的意图识别

1.3 工作原理:四步闭环

Function Calling 并不是模型直接“运行”了你的代码,而是一个**“建议-执行-反馈”**的协作过程:

第一步:定义工具 (Definition)

开发者需要向模型提供一组可用的“工具”描述。这些描述通常包括:

  • 函数名:如 get_weather
  • 功能描述:如“获取指定城市的当前天气”。
  • 参数结构:如 location (字符串, 必填), unit (枚举: celsius/fahrenheit)。
  • 注意:此时模型只知道有哪些工具可用,但不知道如何执行。
第二步:模型决策 (Decision)

当用户输入一个问题(例如:“孝感今天冷吗?”)时,模型会结合上下文和工具描述进行推理:

  • 如果模型认为自己无法直接回答(因为它没有实时天气数据),它会决定调用工具。
  • 模型不会直接返回文本,而是返回一个特殊的 JSON 对象,包含它建议调用的函数名和提取出的参数。
  • 输出示例{"name": "get_weather", "arguments": {"location": "Xiaogan", "unit": "celsius"}}
第三步:外部执行 (Execution)

这是最关键的一步:模型本身不执行代码。

  • 你的应用程序(后端代码)接收到模型返回的 JSON。
  • 程序解析 JSON,识别出要调用 get_weather 函数。
  • 程序在本地或通过 API 真正执行这个函数,并获取结果(例如:“北京,15°C,多云”)。
第四步:结果回填 (Response)
  • 程序将函数的执行结果作为一个新的“消息”发送回给模型。
  • 模型看到这个结果后,结合原始问题,生成最终的自然语言回复给用户:“北京今天气温 15°C,有点凉,建议穿件外套。”

1.4 为什么需要 Function Calling?

在没有 Function Calling 之前,模型存在以下局限:

  1. 知识截止:模型无法知道训练数据之后的新闻、股价或天气。
  2. 幻觉问题:模型可能会编造事实。通过调用权威 API,可以确保信息的准确性。
  3. 行动能力弱:模型只能“说”,不能“做”。Function Calling 让模型能操作数据库、发送邮件或控制智能家居。

1.5 进阶视野:Function Calling 与 MCP 的关联

如果你关注 AI 领域的最新动态,可能听说过 MCP (Model Context Protocol)。它与 Function Calling 有着千丝万缕的联系:

  • Function Calling 是“基石”:它是大模型与外部世界交互的底层技术标准。无论是 OpenAI 还是 Anthropic,都在使用这种机制。
  • MCP 是“通用插座”:MCP 是一个开放协议,旨在标准化模型如何连接到各种数据源(如数据库、文件系统)和工具。在 MCP 架构中,模型正是通过 Function Calling 来触发 MCP 服务器提供的各种能力。
  • 从“私有”到“生态”:目前的 Function Calling 通常需要开发者为每个应用手动定义工具。而 MCP 的目标是建立一个庞大的工具生态,让你的 Spring AI 应用可以像插 USB 一样,即插即用各种现成的 MCP 服务(如 GitHub、PostgreSQL、Slack 等)。

总结:你现在学习的 Function Calling 是通往更高级 AI Agent 架构的必经之路。Spring AI 社区也在积极跟进 MCP 标准,未来你将能通过更简洁的方式集成这些强大的外部能力。


🔧 二、Spring AI 函数注册

2.1 定义函数

方式一:使用 @Bean 注册
@Configuration
public class HotelFunctionConfig {
    
    /**
     * 退房函数
     */
    @Bean
    public FunctionCallback checkOutFunction() {
        return FunctionCallback.builder()
            .name("checkOut")
            .description("办理酒店退房手续")
            .function("checkOut", (String roomNo) -> {
                // 调用业务逻辑
                System.out.println("办理退房,房间号: " + roomNo);
                return "房间 " + roomNo + " 已成功退房";
            })
            .inputType(String.class)
            .build();
    }
    
    /**
     * 续住函数
     */
    @Bean
    public FunctionCallback extendStayFunction() {
        return FunctionCallback.builder()
            .name("extendStay")
            .description("办理酒店续住")
            .function("extendStay", (ExtendStayRequest request) -> {
                System.out.println("办理续住,房间号: " + request.getRoomNo() 
                    + ", 天数: " + request.getDays());
                return "房间 " + request.getRoomNo() 
                    + " 已续住 " + request.getDays() + " 天";
            })
            .inputType(ExtendStayRequest.class)
            .build();
    }
}
方式二:使用注解
@Component
public class HotelFunctions {
    
    @Tool(description = "办理酒店退房手续")
    public String checkOut(
            @ToolParam(description = "房间号") String roomNo) {
        // 业务逻辑
        return "房间 " + roomNo + " 已成功退房";
    }
    
    @Tool(description = "办理酒店续住")
    public String extendStay(
            @ToolParam(description = "房间号") String roomNo,
            @ToolParam(description = "续住天数") int days) {
        // 业务逻辑
        return "房间 " + roomNo + " 已续住 " + days + " 天";
    }
    
    @Tool(description = "查询房间状态")
    public RoomStatus queryRoomStatus(
            @ToolParam(description = "房间号") String roomNo) {
        // 查询数据库
        return new RoomStatus(roomNo, "occupied", "2026-05-05");
    }
}

2.2 配置 ChatClient

@RestController
@RequestMapping("/hotel")
public class HotelAssistantController {
    
    private final ChatClient chatClient;
    
    public HotelAssistantController(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("""
                你是酒店智能助手小智。
                你可以帮客人办理:退房、续住、查询、打扫、预订。
                当需要执行操作时,使用相应的函数。
                """)
            .build();
    }
}

💡 三、酒店预订场景实战

3.1 需求分析

我们要实现一个酒店智能助手,支持以下功能:

  1. 退房checkOut(roomNo)
  2. 续住extendStay(roomNo, days)
  3. 查询queryRoomStatus(roomNo)
  4. 打扫scheduleCleaning(roomNo, time)
  5. 预订bookRoom(roomType, checkInDate, days)

3.2 定义数据模型

@Data
public class ExtendStayRequest {
    private String roomNo;
    private Integer days;
}

@Data
public class RoomStatus {
    private String roomNo;
    private String status;  // occupied, vacant, cleaning
    private String checkOutDate;
}

@Data
public class CleaningRequest {
    private String roomNo;
    private String time;  // 如 "明天上午10点"
}

@Data
public class BookingRequest {
    private String roomType;  // single, double, suite
    private String checkInDate;
    private Integer days;
}

3.3 实现函数

@Component
@Slf4j
public class HotelFunctions {
    
    @Autowired
    private HotelService hotelService;
    
    @Tool(description = "办理酒店退房手续")
    public String checkOut(
            @ToolParam(description = "房间号,如 1201") String roomNo) {
        try {
            log.info("办理退房,房间号: {}", roomNo);
            hotelService.checkOut(roomNo);
            return "房间 " + roomNo + " 已成功退房。祝您旅途愉快!";
        } catch (Exception e) {
            log.error("退房失败", e);
            return "退房失败: " + e.getMessage();
        }
    }
    
    @Tool(description = "办理酒店续住")
    public String extendStay(
            @ToolParam(description = "房间号") String roomNo,
            @ToolParam(description = "续住天数") int days) {
        try {
            log.info("办理续住,房间号: {}, 天数: {}", roomNo, days);
            
            // 查询当前退房日期
            RoomStatus status = hotelService.queryRoomStatus(roomNo);
            
            // 计算新的退房日期
            LocalDate newCheckOut = LocalDate.parse(status.getCheckOutDate())
                .plusDays(days);
            
            // 办理续住
            hotelService.extendStay(roomNo, days);
            
            return String.format("房间 %s 已续住 %d 天,新的退房日期是 %s",
                roomNo, days, newCheckOut);
                
        } catch (Exception e) {
            log.error("续住失败", e);
            return "续住失败: " + e.getMessage();
        }
    }
    
    @Tool(description = "查询房间状态")
    public RoomStatus queryRoomStatus(
            @ToolParam(description = "房间号") String roomNo) {
        try {
            log.info("查询房间状态,房间号: {}", roomNo);
            return hotelService.queryRoomStatus(roomNo);
        } catch (Exception e) {
            log.error("查询失败", e);
            throw new RuntimeException("查询房间状态失败");
        }
    }
    
    @Tool(description = "预约房间打扫")
    public String scheduleCleaning(
            @ToolParam(description = "房间号") String roomNo,
            @ToolParam(description = "打扫时间,如 明天上午10点") String time) {
        try {
            log.info("预约打扫,房间号: {}, 时间: {}", roomNo, time);
            hotelService.scheduleCleaning(roomNo, time);
            return String.format("已预约房间 %s 在 %s 进行打扫", roomNo, time);
        } catch (Exception e) {
            log.error("预约打扫失败", e);
            return "预约打扫失败: " + e.getMessage();
        }
    }
    
    @Tool(description = "预订酒店房间")
    public String bookRoom(
            @ToolParam(description = "房型:single, double, suite") String roomType,
            @ToolParam(description = "入住日期,格式 YYYY-MM-DD") String checkInDate,
            @ToolParam(description = "入住天数") int days) {
        try {
            log.info("预订房间,房型: {}, 入住日期: {}, 天数: {}", 
                roomType, checkInDate, days);
            
            // 查询可用房间
            List<String> availableRooms = hotelService.findAvailableRooms(
                roomType, checkInDate, days);
            
            if (availableRooms.isEmpty()) {
                return "抱歉,该时间段没有可用的" + getRoomTypeName(roomType);
            }
            
            // 预订第一个可用房间
            String roomNo = availableRooms.get(0);
            hotelService.bookRoom(roomNo, checkInDate, days);
            
            return String.format("已成功预订 %s(%s),入住日期 %s,共 %d 天",
                roomNo, getRoomTypeName(roomType), checkInDate, days);
                
        } catch (Exception e) {
            log.error("预订失败", e);
            return "预订失败: " + e.getMessage();
        }
    }
    
    private String getRoomTypeName(String roomType) {
        return switch (roomType.toLowerCase()) {
            case "single" -> "单人间";
            case "double" -> "双人间";
            case "suite" -> "套房";
            default -> roomType;
        };
    }
}

3.4 控制器

@RestController
@RequestMapping("/hotel")
public class HotelAssistantController {
    
    private final ChatClient chatClient;
    
    public HotelAssistantController(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("""
                你是酒店智能助手小智。
                你可以帮客人办理:退房、续住、查询、打扫、预订。
                当需要执行操作时,使用相应的函数。
                回答要友好、专业、简洁。
                """)
            .build();
    }
    
    @PostMapping("/chat")
    public Map<String, Object> chat(@RequestBody ChatRequest request) {
        try {
            // 调用 AI(自动处理函数调用)
            ChatResponse response = chatClient.prompt()
                .user(request.getMessage())
                .call();
            
            // 获取回复
            String reply = response.getResult().getOutput().getContent();
            
            // 返回结果
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("reply", reply);
            result.put("sessionId", request.getSessionId());
            
            return result;
            
        } catch (Exception e) {
            log.error("聊天失败", e);
            Map<String, Object> error = new HashMap<>();
            error.put("success", false);
            error.put("message", "服务暂时不可用");
            return error;
        }
    }
}

注意:Spring AI 会自动处理函数调用,无需手动干预!


🎨 四、高级功能

4.1 多轮对话中的函数调用

@PostMapping("/conversation")
public Map<String, Object> conversation(@RequestBody ConversationRequest request) {
    // 构建消息历史
    List<Message> messages = new ArrayList<>();
    messages.add(new SystemMessage("你是酒店智能助手"));
    
    // 添加历史消息
    for (MessageHistory history : request.getHistory()) {
        if ("user".equals(history.getRole())) {
            messages.add(new UserMessage(history.getContent()));
        } else if ("assistant".equals(history.getRole())) {
            messages.add(new AssistantMessage(history.getContent()));
        }
    }
    
    // 添加当前消息
    messages.add(new UserMessage(request.getCurrentMessage()));
    
    // 调用 AI(支持函数调用)
    ChatResponse response = chatClient.prompt()
        .messages(messages)
        .call();
    
    String reply = response.getResult().getOutput().getContent();
    
    // 保存对话历史
    saveConversation(request.getSessionId(), messages, reply);
    
    Map<String, Object> result = new HashMap<>();
    result.put("success", true);
    result.put("reply", reply);
    
    return result;
}

4.2 并行函数调用

某些场景下,AI 可能需要同时调用多个函数:

// 用户说:"帮我查1201和1202房间的状态"
// AI 会并行调用两次 queryRoomStatus

@Tool(description = "批量查询房间状态")
public List<RoomStatus> queryMultipleRooms(
        @ToolParam(description = "房间号列表") List<String> roomNos) {
    return roomNos.stream()
        .map(roomNo -> hotelService.queryRoomStatus(roomNo))
        .collect(Collectors.toList());
}

4.3 条件函数调用

@Tool(description = "根据情况选择合适的操作")
public String handleGuestRequest(
        @ToolParam(description = "用户需求描述") String request) {
    
    // 分析需求
    if (request.contains("退房")) {
        return checkOut(extractRoomNo(request));
    } else if (request.contains("续住")) {
        return extendStay(extractRoomNo(request), extractDays(request));
    } else if (request.contains("查询")) {
        return queryRoomStatus(extractRoomNo(request)).toString();
    } else {
        return "抱歉,我不太理解您的需求。您可以这样说:\n" +
               "- 帮我退1201房间\n" +
               "- 我想续住2天\n" +
               "- 查询1201房间状态";
    }
}

4.4 函数调用日志

@Component
public class FunctionCallLogger {
    
    @EventListener
    public void onFunctionCall(FunctionCallEvent event) {
        log.info("函数调用: {}({}), 耗时: {}ms",
            event.getFunctionName(),
            event.getArguments(),
            event.getDuration());
    }
}

⚠️ 五、常见问题与解决方案

问题1:函数未被调用

现象:AI 没有调用注册的函数

原因

  • 函数描述不清晰
  • 提示词未说明可以使用函数
  • 模型不支持 Function Calling

解决方案

// 1. 优化函数描述
@Tool(description = "办理酒店退房手续。当用户想要退房时使用此函数。")

// 2. 在系统提示词中说明
.defaultSystem("""
    你是酒店智能助手。
    你可以使用以下函数来帮助用户:
    - checkOut: 办理退房
    - extendStay: 办理续住
    - queryRoomStatus: 查询房间状态
    
    当用户请求相关服务时,请调用相应的函数。
    """)

// 3. 使用支持 Function Calling 的模型
.model("gpt-4o")  // 或 gpt-3.5-turbo

问题2:参数提取错误

现象:AI 提取的参数不正确

解决方案

// 1. 提供更详细的参数描述
@ToolParam(description = "房间号,必须是4位数字,如 1201")

// 2. 添加参数验证
@Tool(description = "办理退房")
public String checkOut(@ToolParam(description = "房间号") String roomNo) {
    // 验证房间号格式
    if (!roomNo.matches("\\d{4}")) {
        return "房间号格式错误,请输入4位数字,如 1201";
    }
    
    // 业务逻辑
    return hotelService.checkOut(roomNo);
}

问题3:函数执行失败

现象:函数调用后返回错误

解决方案

@Tool(description = "办理退房")
public String checkOut(@ToolParam(description = "房间号") String roomNo) {
    try {
        // 业务逻辑
        hotelService.checkOut(roomNo);
        return "退房成功";
        
    } catch (RoomNotFoundException e) {
        return "房间 " + roomNo + " 不存在,请检查房间号";
    } catch (RoomNotOccupiedException e) {
        return "房间 " + roomNo + " 当前无人入住,无法退房";
    } catch (Exception e) {
        log.error("退房失败", e);
        return "退房失败,请稍后重试或联系前台";
    }
}

问题4:多次调用同一函数

现象:AI 反复调用同一个函数

原因:返回值不够明确

解决方案

// 返回更明确的结果
@Tool(description = "办理退房")
public String checkOut(@ToolParam(description = "房间号") String roomNo) {
    hotelService.checkOut(roomNo);
    
    // 明确告知已完成
    return String.format(
        "✅ 退房已完成\n" +
        "房间号:%s\n" +
        "退房时间:%s\n" +
        "祝您旅途愉快!",
        roomNo,
        LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
    );
}

📊 六、性能优化建议

6.1 函数缓存

对于查询类函数,可以使用缓存减少重复调用。

@Component
public class HotelFunctions {
    
    @Autowired
    private HotelService hotelService;
    
    /**
     * 查询房间状态(带缓存)
     * 缓存5分钟,避免频繁查询数据库
     */
    @Tool(description = "查询房间状态")
    @Cacheable(value = "room-status", key = "#roomNo", unless = "#result == null")
    public RoomStatus queryRoomStatus(
            @ToolParam(description = "房间号") String roomNo) {
        
        log.info("Querying room status for: {}", roomNo);
        return hotelService.queryRoomStatus(roomNo);
    }
    
    /**
     * 清除房间状态缓存
     * 在退房、续住等操作后调用
     */
    @CacheEvict(value = "room-status", key = "#roomNo")
    public void clearRoomStatusCache(String roomNo) {
        log.info("Cleared cache for room: {}", roomNo);
    }
}

缓存配置

spring:
  cache:
    type: redis
    redis:
      time-to-live: 300000  # 5分钟过期
      key-prefix: "hotel:room:"

效果对比

场景 无缓存 有缓存 提升
首次查询 200ms 200ms -
重复查询 200ms 5ms 40倍
数据库压力 95%降低

6.2 异步函数调用

对于耗时操作(如发送短信、邮件),使用异步执行。

@Component
public class NotificationFunctions {
    
    @Autowired
    private SmsService smsService;
    
    @Autowired
    private EmailService emailService;
    
    /**
     * 发送确认短信(异步)
     */
    @Tool(description = "发送确认短信给用户")
    @Async("notificationExecutor")
    public CompletableFuture<String> sendConfirmationSms(
            @ToolParam(description = "手机号") String phone,
            @ToolParam(description = "消息内容") String message) {
        
        log.info("Sending SMS to: {}", phone);
        
        try {
            smsService.send(phone, message);
            log.info("SMS sent successfully");
            return CompletableFuture.completedFuture("短信已发送");
        } catch (Exception e) {
            log.error("Failed to send SMS", e);
            return CompletableFuture.completedFuture("短信发送失败");
        }
    }
    
    /**
     * 发送确认邮件(异步)
     */
    @Tool(description = "发送确认邮件给用户")
    @Async("notificationExecutor")
    public CompletableFuture<String> sendConfirmationEmail(
            @ToolParam(description = "邮箱地址") String email,
            @ToolParam(description = "邮件主题") String subject,
            @ToolParam(description = "邮件内容") String content) {
        
        log.info("Sending email to: {}", email);
        
        try {
            emailService.send(email, subject, content);
            log.info("Email sent successfully");
            return CompletableFuture.completedFuture("邮件已发送");
        } catch (Exception e) {
            log.error("Failed to send email", e);
            return CompletableFuture.completedFuture("邮件发送失败");
        }
    }
}

线程池配置

@Configuration
public class AsyncConfig {
    
    @Bean("notificationExecutor")
    public Executor notificationExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("notification-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

启用异步

@SpringBootApplication
@EnableAsync  // 启用异步支持
public class HotelApplication {
    public static void main(String[] args) {
        SpringApplication.run(HotelApplication.class, args);
    }
}

6.3 批量处理

当需要处理多个相似任务时,使用批量操作提高效率。

@Component
public class BatchFunctions {
    
    @Autowired
    private HotelService hotelService;
    
    /**
     * 批量办理退房
     */
    @Tool(description = "批量办理多个房间的退房手续")
    public String batchCheckOut(
            @ToolParam(description = "房间号列表,如 [\"1201\", \"1202\", \"1203\"]") 
            List<String> roomNos) {
        
        log.info("Batch check-out for {} rooms", roomNos.size());
        
        List<String> results = roomNos.parallelStream()
            .map(roomNo -> {
                try {
                    hotelService.checkOut(roomNo);
                    return "✅ " + roomNo + " 退房成功";
                } catch (Exception e) {
                    log.error("Check-out failed for room: {}", roomNo, e);
                    return "❌ " + roomNo + " 退房失败: " + e.getMessage();
                }
            })
            .collect(Collectors.toList());
        
        int successCount = results.stream()
            .filter(r -> r.startsWith("✅"))
            .count();
        
        return String.format(
            "批量退房完成\n" +
            "总计: %d 间\n" +
            "成功: %d 间\n" +
            "失败: %d 间\n\n" +
            "详情:\n%s",
            roomNos.size(),
            successCount,
            roomNos.size() - successCount,
            String.join("\n", results)
        );
    }
    
    /**
     * 批量查询房间状态
     */
    @Tool(description = "批量查询多个房间的状态")
    public String batchQueryRoomStatus(
            @ToolParam(description = "房间号列表") List<String> roomNos) {
        
        List<RoomStatus> statuses = roomNos.parallelStream()
            .map(roomNo -> {
                try {
                    return hotelService.queryRoomStatus(roomNo);
                } catch (Exception e) {
                    log.error("Query failed for room: {}", roomNo, e);
                    return new RoomStatus(roomNo, "error", null);
                }
            })
            .collect(Collectors.toList());
        
        // 格式化为表格
        StringBuilder sb = new StringBuilder();
        sb.append("房间状态查询结果:\n\n");
        sb.append("| 房间号 | 状态 | 退房日期 |\n");
        sb.append("|--------|------|----------|\n");
        
        for (RoomStatus status : statuses) {
            sb.append(String.format("| %s | %s | %s |\n",
                status.getRoomNo(),
                getStatusText(status.getStatus()),
                status.getCheckOutDate() != null ? status.getCheckOutDate() : "-"
            ));
        }
        
        return sb.toString();
    }
    
    private String getStatusText(String status) {
        return switch (status) {
            case "occupied" -> "入住中";
            case "vacant" -> "空闲";
            case "cleaning" -> "打扫中";
            case "error" -> "查询失败";
            default -> status;
        };
    }
}

性能对比

方式 10个房间 100个房间 说明
串行处理 2s 20s 逐个处理
并行处理 0.5s 2s 并行流
批量SQL 0.2s 0.5s 一次性查询

6.4 函数调用日志

记录所有函数调用,便于审计和问题排查。

@Component
@Slf4j
public class FunctionCallLogger {
    
    @Autowired
    private AuditRepository auditRepository;
    
    /**
     * 监听函数调用事件
     */
    @EventListener
    @Async("auditExecutor")
    public void onFunctionCall(FunctionCallEvent event) {
        
        AuditLog auditLog = AuditLog.builder()
            .timestamp(LocalDateTime.now())
            .functionName(event.getFunctionName())
            .arguments(maskSensitiveData(event.getArguments()))  // 脱敏
            .duration(event.getDuration())
            .success(event.isSuccess())
            .errorMessage(event.getErrorMessage())
            .userId(getCurrentUserId())
            .build();
        
        auditRepository.save(auditLog);
        
        // 记录日志
        if (event.isSuccess()) {
            log.info("Function call: {}({}) - {}ms",
                event.getFunctionName(),
                maskSensitiveData(event.getArguments()),
                event.getDuration());
        } else {
            log.error("Function call failed: {}({}) - {}ms - {}",
                event.getFunctionName(),
                maskSensitiveData(event.getArguments()),
                event.getDuration(),
                event.getErrorMessage());
        }
    }
    
    /**
     * 脱敏敏感数据
     */
    private String maskSensitiveData(String arguments) {
        if (arguments == null) return "";
        
        // 隐藏手机号
        arguments = arguments.replaceAll("\\d{3}\\d{4}\\d{4}", "$1****$3");
        
        // 隐藏身份证
        arguments = arguments.replaceAll("\\d{6}\\d{8}\\d{4}", "$1********$3");
        
        return arguments;
    }
    
    private String getCurrentUserId() {
        // 从 SecurityContext 获取当前用户ID
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth != null ? auth.getName() : "anonymous";
    }
}

审计日志实体

@Entity
@Table(name = "audit_logs")
@Data
@Builder
public class AuditLog {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private LocalDateTime timestamp;
    
    @Column(length = 100)
    private String functionName;
    
    @Column(length = 2000)
    private String arguments;
    
    private Long duration;  // 毫秒
    
    private Boolean success;
    
    @Column(length = 500)
    private String errorMessage;
    
    @Column(length = 50)
    private String userId;
}

查询审计日志

@RestController
@RequestMapping("/admin/audit")
public class AuditController {
    
    @Autowired
    private AuditRepository auditRepository;
    
    @GetMapping("/logs")
    public Page<AuditLog> getAuditLogs(
            @RequestParam(required = false) String functionName,
            @RequestParam(required = false) Boolean success,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        Specification<AuditLog> spec = Specification.where(null);
        
        if (functionName != null) {
            spec = spec.and((root, query, cb) ->
                cb.equal(root.get("functionName"), functionName));
        }
        
        if (success != null) {
            spec = spec.and((root, query, cb) ->
                cb.equal(root.get("success"), success));
        }
        
        return auditRepository.findAll(spec,
            PageRequest.of(page, size, Sort.by("timestamp").descending()));
    }
}

6.5 熔断降级

防止某个函数故障影响整个系统。

@Component
public class ResilientHotelFunctions {
    
    @Autowired
    private HotelService hotelService;
    
    /**
     * 带熔断的退房函数
     */
    @Tool(description = "办理酒店退房手续")
    @CircuitBreaker(name = "checkOutService", fallbackMethod = "checkOutFallback")
    @Retry(name = "checkOutService", fallbackMethod = "checkOutRetryFallback")
    public String checkOut(@ToolParam(description = "房间号") String roomNo) {
        
        log.info("Processing check-out for room: {}", roomNo);
        
        try {
            hotelService.checkOut(roomNo);
            return String.format(
                "✅ 退房成功\n" +
                "房间号:%s\n" +
                "时间:%s\n" +
                "祝您旅途愉快!",
                roomNo,
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
            );
        } catch (Exception e) {
            log.error("Check-out failed", e);
            throw e;  // 抛出异常触发熔断/重试
        }
    }
    
    /**
     * 重试降级
     */
    public String checkOutRetryFallback(String roomNo, Exception e) {
        log.warn("Check-out retry failed for room: {}", roomNo, e);
        return "抱歉,系统繁忙,请稍后重试或联系前台办理退房。";
    }
    
    /**
     * 熔断降级
     */
    public String checkOutFallback(String roomNo, Exception e) {
        log.error("Check-out circuit breaker opened for room: {}", roomNo, e);
        return "抱歉,退房服务暂时不可用,请直接联系前台办理。电话:400-xxx-xxxx";
    }
}

Resilience4j 配置

resilience4j:
  circuitbreaker:
    instances:
      checkOutService:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5
  
  retry:
    instances:
      checkOutService:
        max-attempts: 3
        wait-duration: 1s
        exponential-backoff-multiplier: 2

监控面板

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,circuitbreakers
  health:
    circuitbreakers:
      enabled: true

访问 http://localhost:8080/actuator/circuitbreakers 查看熔断器状态。


📝 七、总结与最佳实践

7.1 核心要点回顾

函数注册
  • ✅ 使用 @Tool 注解或 FunctionCallback Bean
  • ✅ 提供清晰的函数描述
  • ✅ 详细说明每个参数的含义
提示词设计
  • ✅ 在系统提示词中说明可用函数
  • ✅ 指导 AI 何时使用函数
  • ✅ 提供示例对话
错误处理
  • ✅ 函数内部捕获异常
  • ✅ 返回友好的错误信息
  • ✅ 记录详细日志

7.2 设计原则

  1. 单一职责:每个函数只做一件事
  2. 明确命名:函数名和参数名要清晰
  3. 详细描述:description 要具体
  4. 健壮性:处理好异常情况
  5. 可测试:编写单元测试

7.3 安全注意事项

  • 🔒 验证所有输入参数
  • 🔒 实施权限控制
  • 🔒 记录审计日志
  • 🔒 限制函数调用频率
  • 🔒 防止注入攻击

🔮 八、下一步学习路径

恭喜你已经掌握了 Function Calling 的核心技能!接下来可以学习:

关注账号,后续持续更新内容

  1. [第7篇] Spring AI RAG 实战 - 基于知识库的智能问答
  2. [第8篇] Spring AI 多模型路由 - 动态切换多个模型
  3. [第9篇] Spring AI 微服务架构 - 生产级架构设计

实践项目

  • 🏨 完善酒店智能助手
  • 🛒 开发电商客服机器人
  • 📅 构建智能日程管理助手
  • 💼 实现企业OA智能助手

💬 互动环节

有问题?
欢迎在评论区留言!

觉得有用?

  • ⭐ 点赞支持
  • 💾 收藏备用
  • 🔄 分享给朋友

下一篇预告:《Spring AI RAG 实战:基于知识库的智能问答系统》


在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐