
1. 从“对话”到“行动”为什么Function Calling是AI应用的关键一跃最近在捣鼓SpringAI发现一个挺有意思的现象很多开发者包括我自己一开始都把它当成了一个更智能的“聊天机器人”框架。你给它一段提示词它返回一段文本然后你再手动解析这段文本去调用你的业务系统。这个流程听起来很自然对吧但做多了你就会发现这中间有个巨大的鸿沟——AI模型并不知道你的业务世界是什么样子它只能“说”不能“做”。举个例子你想让AI帮你查一下明天的天气。传统的做法是你问“明天北京天气怎么样”AI回答“明天北京晴气温15-25度”。然后呢没了。如果你想让这个信息自动显示在你的App首页或者触发一个给用户发送提醒邮件的流程你就得自己写代码去解析“晴”、“15-25度”这些关键词再调用你的天气服务接口和邮件服务。这个过程繁琐、脆弱而且极度依赖对AI输出格式的严格约定比如必须包含“晴”这个字一旦AI换了个说法比如“明日京城天气晴朗”你的解析逻辑可能就崩了。这就是Function Calling要解决的核心问题。它不是一个简单的“调用函数”的技术而是一套让大语言模型LLM理解并能够“调度”外部工具和系统的协议。通过Function CallingAI从一个只能“动嘴”的顾问变成了一个可以“动手”的智能调度中心。它理解了“查天气”这个意图背后需要调用一个名为getWeather的函数并且能自动从对话中提取出location: “北京”、date: “明天”这些准确的参数然后以结构化的JSON格式告诉你“嘿你需要调用这个函数参数我都帮你准备好了。” 你的程序拿到这个结构化请求直接调用对应的业务方法即可完全跳过了脆弱且不稳定的文本解析环节。SpringAI 1.0.0-M5及之后的版本将Function Calling作为一等公民进行了深度集成。这意味着在Spring这个庞大的Java生态里你可以用熟悉的Bean、Service注解就把任何一个Spring管理的Bean方法暴露给AI模型作为可调用的“工具”。这不仅仅是技术上的便利更是一种范式的转变你的业务逻辑不再需要为AI做适配AI主动来理解和调度你的业务逻辑。接下来我们就深入看看在SpringAI的生态里如何把我们的系统能力“喂”给AI让它真正为我们干活。2. 核心概念拆解Function Calling在SpringAI中的三层抽象要玩转SpringAI的Function Calling不能只停留在“怎么配”的层面得先理解它设计的几层抽象。这能帮你避免很多配置上的困惑尤其是在处理复杂参数和返回类型的时候。2.1 第一层FunctionCallback- 能力的封装器这是最底层、最核心的接口。FunctionCallback定义了一个“AI可调用功能”的契约。它不关心这个功能具体是什么只关心三件事身份我这个功能叫什么名字getName()描述我有什么用AI模型会根据这个描述来决定在什么场景下调用我。getDescription()执行当AI决定调用我时具体要执行什么逻辑call()方法在SpringAI中FunctionCallback的实现类CallbackFunction是连接你的业务方法和AI模型的桥梁。但通常我们不会直接去实现它而是通过更高级的抽象来创建。2.2 第二层Function与Tool- OpenAI协议与Spring的适配这里有个关键点SpringAI需要与不同的AI模型提供商如OpenAI、Azure OpenAI、Anthropic等对接。而Function Calling这个概念和背后的数据格式很大程度上是由OpenAI的API标准定义的。因此SpringAI设计了两套并行的抽象来应对spring-ai-core中的Function这是一个与提供商无关的通用抽象。它定义了功能的名称、描述、输入参数模式InputSchema和返回类型。它的目标是提供一种统一的方式来描述一个“功能”。spring-ai-openai等具体模块中的Tool这是为了兼容OpenAI API的tools参数而设计的。一个Tool本质上是对一个Function的封装并添加了提供商特定的元数据。当你使用OpenAI的ChatClient时你注册的FunctionCallback最终会被包装成Tool然后序列化成JSON数组填入API请求的tools字段。为什么需要这两层为了可移植性。理论上你的Function定义名称、描述、参数模式应该是相对稳定的。如果未来你想从OpenAI切换到另一个也支持类似功能的模型比如Google Gemini你只需要换一个ChatClient实现并确保它支持将Function转换为其自身的协议格式而你的业务逻辑(FunctionCallback)可能无需改动。2.3 第三层RegisterFunction与FunctionRegistration- 声明式的便捷之道手动创建和注册每一个FunctionCallback很麻烦。SpringAI提供了两种更Spring风格的方式注解驱动RegisterFunction这是最简单直接的方式。你可以在任何Spring Bean的方法上添加RegisterFunction注解。SpringAI会在应用启动时自动扫描这些注解并将该方法包装成一个FunctionCallback注册到上下文中。你只需要在注解里提供功能的名称和描述即可参数和返回类型通过方法签名自动推断。Service public class WeatherService { RegisterFunction(name getWeather, description 获取指定城市未来几天的天气预报) public WeatherInfo getWeather(Parameter(description 城市名称例如北京、上海) String city, Parameter(description 查询的天数默认为1) Nullable Integer days) { // ... 调用真实天气API return new WeatherInfo(...); } }这种方式极其简洁适合大多数标准场景。Parameter注解用来增强参数的描述帮助AI更准确地理解如何填充它。编程式注册FunctionRegistration当你需要对功能的行为进行更精细的控制时比如动态生成描述、或者处理非常复杂的参数类型可以使用FunctionRegistration。它是一个包装了FunctionCallback和其对应Function定义的容器。你可以在配置类中通过Bean方法显式地创建并返回它。Configuration public class FunctionConfig { Bean public FunctionRegistrationWeatherInfo weatherFunctionRegistration(WeatherService weatherService) { Function function Function.builder() .name(getWeather) .description(获取天气预报) .inputSchema(/* 可以在这里详细定义JSON Schema */) .build(); // 将Bean方法包装成CallbackFunction CallbackFunction callbackFunction CallbackFunction.builder() .name(getWeather) .description(获取天气预报) .inputType(WeatherRequest.class) // 指定输入类型 .outputType(WeatherInfo.class) // 指定输出类型 .function(weatherService::getWeather) // 方法引用 .build(); return new FunctionRegistration(callbackFunction, function); } }这种方式更灵活但代码量也更多。它允许你完全掌控Function的schema定义和CallbackFunction的构建过程。理解这三层抽象后我们就知道无论是用注解还是编程式最终目标都是生成一个符合FunctionCallback契约的实例并将其注册到Spring的ApplicationContext中以便ChatClient在对话时能够发现并使用它们。3. 实战将数据库查询暴露为AI工具光说不练假把式。我们来看一个比“查天气”更贴近企业开发的例子让AI能够查询公司内部的员工信息。假设我们有一个简单的Employee实体和对应的JPA Repository。3.1 定义领域模型与数据层首先是基础的领域对象和数据访问层。// 实体类 Entity Data public class Employee { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private String department; private String email; private LocalDate hireDate; } // 仓库接口 public interface EmployeeRepository extends JpaRepositoryEmployee, Long { ListEmployee findByNameContaining(String name); ListEmployee findByDepartment(String department); ListEmployee findByHireDateAfter(LocalDate date); }3.2 创建AI可调用的服务层这里我们创建一个服务它提供两个AI可调用的功能按姓名模糊查找员工以及查找某个日期之后入职的员工。Service public class EmployeeQueryService { Autowired private EmployeeRepository employeeRepository; /** * 根据员工姓名查找模糊匹配 * 使用RegisterFunction注解让SpringAI自动注册此功能。 * 描述写得详细些AI理解更准确。 */ RegisterFunction( name findEmployeesByName, description 根据员工姓名进行模糊查询。当用户想找某个具体的人或者只知道名字的一部分时使用此功能。 ) public ListEmployee findByName( Parameter(required true, description 员工姓名或姓名的一部分例如张伟 或 伟) String nameFragment) { return employeeRepository.findByNameContaining(nameFragment); } /** * 查找在指定日期之后入职的员工 * 注意AI模型对日期格式可能比较敏感这里我们接受字符串在方法内进行转换。 * 更健壮的做法是定义一个专用的请求对象。 */ RegisterFunction( name findEmployeesHiredAfter, description 查找在某个特定日期之后入职的所有员工。用于分析团队新鲜血液或进行入职周年纪念等。 ) public ListEmployee findHiredAfter( Parameter(required true, description 入职日期格式为YYYY-MM-DD例如2023-01-01) String date) { LocalDate localDate; try { localDate LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException(日期格式不正确请使用 YYYY-MM-DD 格式例如2023-01-01); } return employeeRepository.findByHireDateAfter(localDate); } }关键点与避坑经验参数描述至关重要Parameter注解里的description是AI模型理解如何从用户问题中提取参数的主要依据。务必清晰、无歧义。例如“姓名的一部分”比“姓名”更好。类型处理AI模型传递的参数永远是字符串或基础类型的JSON表示。对于复杂类型如LocalDate需要在方法内部进行转换和校验并提供清晰的错误提示。另一种更优雅的方案是使用Converter或自定义InputSchema但注解方式下在方法内处理是最直接的。返回类型方法返回ListEmployee。SpringAI和底层的Jackson会自动将其序列化为JSON数组。确保你的实体类可以被正常序列化比如避免循环引用。3.3 配置与调用让ChatClient知晓这些工具仅仅定义了服务还不够我们需要在配置中启用Function Calling并确保ChatClient能使用这些工具。Configuration public class AIConfig { Bean public ChatClient chatClient(OpenAiChatClient openAiClient) { // 实际上OpenAiChatClient已经自动注册了所有FunctionCallback。 // 这里我们主要进行一些全局配置比如指定使用的模型。 return openAiClient; } // 如果你需要更细粒度的控制可以像下面这样配置ChatClient Bean // Bean // public ChatClient customChatClient(OpenAiApi openAiApi, ListFunctionCallback toolCallbacks) { // OpenAiChatOptions options OpenAiChatOptions.builder() // .model(“gpt-4-turbo-preview”) // 建议使用较新的模型对Function Calling支持更好 // .functionCallingType(FunctionCallingType.AUTO) // 关键设置为AUTO让模型决定何时调用函数 // .build(); // // // 将找到的所有FunctionCallback注册到ChatClient // OpenAiChatClient client new OpenAiChatClient(openAiApi, options); // toolCallbacks.forEach(client::addFunctionCallback); // // return client; // } }在application.yml中配置你的OpenAI API密钥和基础URL如果你用的是Azure OpenAI或代理spring: ai: openai: api-key: ${OPENAI_API_KEY} # base-url: https://your-resource.openai.azure.com/openai/deployments/your-deployment-name # Azure OpenAI chat: options: model: gpt-4o-mini # 根据实际情况选择模型gpt-3.5-turbo也支持但能力较弱 temperature: 0.2 # 低温度使输出更确定更适合工具调用场景3.4 编写交互控制器最后我们创建一个简单的REST端点来体验整个流程。RestController RequestMapping(/api/ai/employee) public class EmployeeAIController { Autowired private ChatClient chatClient; PostMapping(/query) public String queryEmployee(RequestBody UserQuery userQuery) { // 构建系统提示词设定AI的角色和能力范围 String systemPrompt 你是一个公司内部的人力资源助手专门帮助员工查询同事信息。 你可以根据姓名查找员工也可以查找在特定日期之后入职的员工。 请根据用户的问题判断是否需要调用工具以及调用哪个工具。 如果查询到结果请用友好、清晰的方式总结并告知用户。 如果用户的问题无法通过现有工具解决请直接告知用户你的能力范围。 ; // 构建消息列表 ListMessage messages new ArrayList(); messages.add(new SystemMessage(systemPrompt)); messages.add(new UserMessage(userQuery.getQuestion())); // 发起对话请求。ChatClient会自动处理Function Calling的流程 // 1. 将注册的FunctionCallback作为tools信息发送给AI。 // 2. AI返回一个包含tool_calls的响应。 // 3. ChatClient自动执行对应的工具函数。 // 4. 将工具执行结果作为上下文再次请求AI生成最终回复。 // 这个过程对开发者是透明的。 ChatResponse response chatClient.call(new Prompt(messages)); // 返回AI生成的最终文本回复 return response.getResult().getOutput().getContent(); } Data public static class UserQuery { private String question; } }现在你可以用Postman或curl测试了POST /api/ai/employee/query Content-Type: application/json { question: 帮我找一下名字里带‘伟’字的同事 }AI的回复可能不再是简单的“找到了张三、李四”而会是“根据查询找到2位姓名中包含‘伟’字的同事张伟技术部zhangweicompany.com和李伟市场部liweicompany.com。需要我为您提供其中某位的详细信息吗”4. 进阶处理复杂参数、流式响应与错误处理基础功能跑通后我们会遇到更实际的问题。单一参数的服务很简单但现实中的业务接口往往复杂得多。4.1 复杂参数与JSON Schema定义当你的函数需要接收一个结构复杂的对象时仅靠RegisterFunction和基本类型参数就不够了。例如一个创建项目任务的功能需要任务标题、描述、负责人、截止日期等多个字段。方案一使用专用请求对象推荐这是最清晰、类型安全的方式。SpringAI可以很好地处理简单POJO对象。Data public class CreateTaskRequest { Parameter(description “任务标题简要概述任务内容”) NotBlank private String title; Parameter(description “任务的详细描述和要求”) private String description; Parameter(description “负责人的员工邮箱”) Email private String assigneeEmail; Parameter(description “任务的截止日期格式YYYY-MM-DD”) Future private String dueDate; } Service public class TaskService { RegisterFunction(name “createTask”, description “在项目管理系统中创建一个新的任务”) public Task createTask(CreateTaskRequest request) { // 参数校验、业务逻辑... return taskRepository.save(...); } }SpringAI会利用Jackson库分析CreateTaskRequest类的结构自动生成对应的JSON Schema给AI模型。Parameter注解可以加在字段上。方案二自定义InputSchema更灵活控制如果你需要完全控制生成的Schema或者你的参数类型无法被Jackson自动推导比如使用了一些自定义的泛型你可以实现InputSchema接口。public class CustomTaskSchema implements InputSchema { Override public JsonNode getSchema() { // 使用JsonNodeFactory或ObjectMapper手动构建一个复杂的JSON Schema对象 ObjectMapper mapper new ObjectMapper(); ObjectNode schema mapper.createObjectNode(); schema.put(“type”, “object”); schema.put(“description”, “创建任务的请求参数”); // ... 详细定义properties, required等字段 return schema; } }然后在创建FunctionRegistration时通过.inputSchema(new CustomTaskSchema())来指定。实操心得对于绝大多数业务场景方案一专用请求对象完全够用且是最佳实践。它保持了代码的简洁性和可维护性。只有当你需要动态生成Schema或者与已有的、格式特殊的API对接时才需要考虑方案二。手动维护JSON Schema容易出错且与Java代码不同步。4.2 支持流式响应StreamingFunction Calling本身是支持流式响应的。在流式模式下AI会先流式返回一个包含tool_calls的消息块然后你的客户端需要执行对应的函数并将结果以tool类型的消息发送回去AI再继续流式生成最终的文本回复。在SpringAI中使用StreamingChatClient即可。Autowired private StreamingChatClient streamingChatClient; public FluxString streamQuery(String userQuestion) { ListMessage messages ... // 构建消息包含系统提示和用户问题 Prompt prompt new Prompt(messages); return streamingChatClient.stream(prompt) .map(ChatResponse::getResult) // 这里得到的是Delta即流式片段 .map(Generation::getOutput) .map(AssistantMessage::getContent) .filter(content - content ! null); // 过滤掉tool call本身的内容可能是null }对于前端你需要处理两种类型的流式事件一种是AI决定调用工具时的特殊消息通常需要前端解析并调用后端接口另一种是常规的文本流。SpringAI的流式响应包含了完整的元数据你可以从中解析出tool_calls。不过更常见的做法是在后端将整个“AI决策 - 调用工具 - AI总结”的流程封装好只将最终的文本流式推送给前端这样前端逻辑会简单很多。4.3 错误处理与重试策略Function Calling流程中可能出错的地方很多AI模型错误没有正确理解用户意图调用了错误的函数或提取的参数完全不对。函数执行错误你的业务方法抛出了异常如参数校验失败、数据库连接失败等。网络或超时错误与AI API或你的内部服务通信失败。SpringAI的默认行为当工具执行抛出异常时ChatClient会捕获这个异常并将其信息如异常消息作为工具执行结果的一部分再次发送给AI模型。AI模型通常会向用户道歉并解释出了问题。这有时是可行的但暴露内部错误信息可能不安全。更健壮的错误处理策略在FunctionCallback内部捕获异常这是最直接的方式。在你的服务方法或CallbackFunction的包装逻辑里用try-catch包裹核心业务返回一个结构化的错误信息对象而不是抛出异常。RegisterFunction(...) public Object findByName(String name) { try { return employeeRepository.findByNameContaining(name); } catch (Exception e) { log.error(“查询员工失败”, e); // 返回一个AI能理解的错误描述 return Map.of(“error”, true, “message”, “系统繁忙查询失败请稍后再试。”); } }使用Spring的全局异常处理通过ControllerAdvice定义一个全局异常处理器将特定的异常转换为友好的错误消息。但注意这需要确保异常在到达ChatClient之前被处理。配置重试机制对于网络抖动等暂时性错误可以使用Spring Retry或Resilience4j为ChatClient的调用添加重试逻辑。Bean public ChatClient retryableChatClient(OpenAiChatClient delegate) { // 使用Spring Retry模板 RetryTemplate retryTemplate RetryTemplate.builder() .maxAttempts(3) .exponentialBackoff(1000, 2, 5000) // 初始1秒倍数2最大5秒 .retryOn(ResourceAccessException.class) // 只在网络异常时重试 .build(); return new RetryableChatClient(delegate, retryTemplate); }最重要的经验永远不要相信AI提取的参数是完美的。必须在你的业务方法入口处进行严格的参数校验使用JSR-303注解如NotBlank、Valid或手动校验并给出清晰、友好的校验失败信息。AI可能会尝试“猜测”缺失的参数或者提供格式完全错误的值。5. 架构思考Function Calling在微服务中的集成模式当你的系统从单体应用扩展到微服务架构时如何让AI调度跨服务的功能直接让AI模型感知所有微服务的所有函数是不现实也不安全的。这里有几个可行的架构模式。5.1 模式一AI网关聚合推荐这是最清晰、最可控的模式。引入一个独立的“AI网关”服务可以是SpringAI应用。这个网关的职责是聚合工具通过服务发现如Eureka或直接配置知晓所有业务微服务暴露的“AI可调用端点”。统一注册在网关内为每个远端功能定义一个本地的FunctionCallback。这个Callback的实现不是直接执行业务逻辑而是通过HTTP客户端如Feign、RestTemplate或消息队列去调用对应的业务微服务。协议转换与适配负责将AI模型传来的参数转换成后端服务需要的API格式并将后端服务的响应转换成AI模型能理解的格式。优点关注点分离业务微服务无需引入SpringAI依赖只需提供标准的REST API或消息接口。安全控制网关可以作为统一的认证、授权、限流、审计入口。灵活性高可以方便地对工具进行编排、组合或添加缓存层。缺点增加了新的服务组件和网络跳数。网关可能成为性能瓶颈需要精心设计。5.2 模式二Sidecar代理在每个业务微服务旁边部署一个轻量级的“AI Sidecar”代理可以是一个独立的进程或容器。这个Sidecar负责与本业务服务通过本地进程间通信IPC或localhost HTTP进行交互。向中心的AI协调服务或直接向AI网关注册本服务提供的功能。接收来自AI协调服务的工具调用请求转发给本地业务服务并返回结果。优点业务服务仍然保持纯净无AI框架依赖。功能注册可以更动态。通信延迟较低本地调用。缺点部署和运维复杂度高需要管理多个Sidecar实例。需要一套中心化的协调机制来管理所有Sidecar注册的工具。5.3 模式三消息驱动的事件总线所有工具调用和结果返回都通过一个中心化的事件总线如Kafka、RabbitMQ来完成。AI网关将工具调用请求发布到特定的主题如tool-call.employee.query。订阅了该主题的业务微服务消费消息执行逻辑然后将结果发布到另一个主题如tool-result.request-id。AI网关订阅结果主题收到结果后继续与AI模型的对话。优点完全解耦异步处理适合高并发或耗时较长的工具调用。具备很好的可扩展性和弹性。缺点系统复杂度最高。需要处理消息的序列化、路由、错误处理和超时。整体链路延迟可能较高。选择建议对于大多数中小型项目或刚开始引入AI能力的团队模式一AI网关聚合是最务实的选择。它平衡了复杂度、控制力和灵活性。你可以从一个简单的Spring Boot应用开始随着业务增长再逐步将其演进为更强大的API网关。6. 性能优化、监控与安全考量将系统能力开放给AI调用在享受便利的同时也带来了新的挑战。6.1 性能优化减少Token消耗与延迟每一次Function Calling都意味着多次与AI API的交互用户消息 - AI决定调用工具 - 执行工具 - AI生成最终回复这会消耗更多的Token并增加整体响应延迟。精简工具描述description字段会被发送给AI模型占用Token。在保证清晰的前提下尽量精简。避免在描述中写入大量示例或不必要的信息。合并细粒度工具如果两个工具总是被连续调用考虑将它们合并成一个。例如getUserProfile和getUserRecentOrders可以合并为getUserDetails返回一个包含用户基本信息和最近订单的复合对象。但这需要权衡合并后工具可能变得臃肿AI也可能无法准确调用。使用更高效的模型对于工具调用场景gpt-4o-mini或gpt-3.5-turbo在成本和速度上通常优于gpt-4且工具调用能力足够。可以在非关键路径使用小模型。实现客户端缓存对于一些返回结果变化不频繁的工具如查询部门列表、产品目录可以在客户端AI网关或业务服务实现缓存避免重复执行和重复消耗AI Token来传递相同的结果。注意设置合理的缓存过期策略。6.2 监控与可观测性你需要知道AI在什么时候、调用了什么工具、结果如何。结构化日志在FunctionCallback的call方法入口和出口打上结构化日志使用MDC记录工具名称、输入参数、执行耗时、成功/失败状态。这便于后续通过ELK等日志系统进行分析。Slf4j Component public class LoggingFunctionCallbackWrapper implements FunctionCallback { private final FunctionCallback delegate; public LoggingFunctionCallbackWrapper(FunctionCallback delegate) { this.delegate delegate;} Override public Object call(ConversationContext context) { long start System.currentTimeMillis(); String toolName delegate.getName(); Object input context.getArguments(); log.info(“AI工具调用开始: {}, 参数: {}”, toolName, input); try { Object result delegate.call(context); long duration System.currentTimeMillis() - start; log.info(“AI工具调用成功: {}, 耗时: {}ms”, toolName, duration); return result; } catch (Exception e) { long duration System.currentTimeMillis() - start; log.error(“AI工具调用失败: {}, 耗时: {}ms, 错误:”, toolName, duration, e); throw e; } } // ... 其他方法委托给delegate }你可以通过Spring的BeanPostProcessor自动包装所有FunctionCallback。Metrics指标集成Micrometer为工具调用创建计数器counter、计时器timer和异常计数器。这样可以在Grafana等监控面板上实时查看各工具的调用频率、平均延迟和错误率。分布式追踪集成Sleuth或OpenTelemetry确保一次用户请求引发的AI对话和后续的工具调用都在同一条追踪链路上方便排查问题。6.3 安全与权限控制这是重中之重。不能让AI拥有不受限制的系统访问权。工具级别的权限不是所有注册的函数都应该对所有AI对话开放。例如createTask创建任务和deleteUser删除用户的权限级别显然不同。可以在FunctionCallback的call方法开始时检查当前对话的上下文例如从ConversationContext中获取用户身份Token进行权限校验。如果无权限则抛出特定异常或返回权限错误信息。RegisterFunction(name “createTask”) public Task createTask(CreateTaskRequest request, Parameter(hidden true) Header(“X-User-Id”) String userId) { // 1. 根据userId查询用户权限 // 2. 校验用户是否有创建任务的权限 if (!permissionService.canCreateTask(userId)) { throw new AccessDeniedException(“用户无权创建任务”); } // 3. 执行业务逻辑... }注意Parameter(hidden true)表示这个参数不会暴露给AI模型而是由调用者你的控制器在构建ConversationContext时传入。输入验证与净化AI提取的参数必须视为不可信输入。除了常规的类型和格式校验还要防范注入攻击。如果参数用于数据库查询务必使用参数化查询或JPA的Criteria API避免SQL注入。如果参数用于构造系统命令或文件路径必须进行严格的净化。输出过滤从工具返回给AI的数据可能包含敏感信息如用户手机号、身份证号、内部系统ID。在返回前应该进行脱敏处理。可以定义一个统一的JacksonJsonSerializer在序列化特定字段时进行脱敏。审计日志所有通过AI发起的工具调用无论成功失败都应记录详尽的审计日志包括调用时间、调用者会话ID或用户ID、工具名、参数、结果状态等以满足合规性要求。通过SpringAI的Function Calling我们成功地将AI的“语言理解”能力与Spring生态中丰富的业务系统“执行”能力连接了起来。这不仅仅是技术集成更是一种思维方式的转变——从“如何让AI回答得更好”到“如何让AI更好地调度我的系统来解决问题”。在这个过程中清晰的抽象设计、严谨的错误处理、全面的安全考量和可观测性建设是确保项目成功落地并稳定运行的关键。开始动手把你的第一个业务服务变成AI的“手”和“脚”吧你会发现自动化与智能化的边界又被拓宽了一大步。