Spring AI Function Calling 实战——让大模型安全可控地调用后端服务

📅 2026/7/9 9:38:49
Spring AI Function Calling 实战——让大模型安全可控地调用后端服务
Spring AI Function Calling 实战——让大模型安全可控地调用后端服务一、Function Calling 的设计动机与架构选型大语言模型LLM本质上是一个文本生成器它无法直接查询数据库、调用 API 或执行业务操作。在生产场景中用户通过自然语言描述查询最近三天的订单状态时模型需要一种机制将意图转化为实际的服务调用并将结果整合到对话中。这就是 Function Calling 存在的根本原因。在设计方案选型上业界目前存在三种主流思路。第一种是基于 Prompt Engineering 的方案通过精心构造 System Prompt 来限定模型输出 JSON 格式再由应用层解析并调用。这种方案的优点是实现简单、不依赖特定模型能力但缺点是稳定性差模型可能输出格式不符合预期且无法利用模型的推理能力做参数填充。第二种是基于 Agent 框架的方案如 LangChain 的 Agent 或 AutoGPT让模型自主规划调用步骤。这种方案灵活性最高但延迟不可控且容易产生幻觉调用。第三种是基于原生 Function Calling 的方案利用模型原生的 Tool Use 能力由服务端注册函数声明模型根据对话上下文自主决定是否调用、调用哪个函数、传递什么参数。Spring AI 采用的就是第三种方案它在 Spring 生态内提供了一套完整的 Function Calling 抽象层。flowchart TB User[用户输入自然语言] -- Controller[Spring Controller] Controller -- ChatClient[ChatClient 接口] ChatClient -- LLM[大语言模型] subgraph FunctionRegistry[函数注册中心] F1[订单查询函数] F2[库存查询函数] F3[物流追踪函数] end LLM -- |解析意图| Decision{是否需要调用函数?} Decision -- |是| FCall[返回 Function Call 请求] Decision -- |否| Response[直接生成回复] FCall -- Dispatcher[函数分发器] Dispatcher -- FunctionRegistry FunctionRegistry -- BusinessService[业务服务层] BusinessService -- DB[(数据库)] BusinessService -- Result[结构化结果] Result -- LLM2[LLM 整合结果] LLM2 -- FinalResponse[最终自然语言回复]选型时的关键考量在于是否需要模型具备推理式的参数填充能力。举例来说用户说查一下我昨天的订单模型需要推断昨天对应的日期范围。这种场景下原生 Function Calling 远优于正则解析方案。二、Spring AI Function Calling 核心实现机制Spring AI 的 Function Calling 实现基于java.util.function.Function接口通过Description注解向模型提供函数签名和参数描述。核心流程如下首先定义业务函数。Spring AI 要求函数实现FunctionRequest, Response接口其中 Request 和 Response 需要显式声明字段类型及描述信息。/** * 订单查询函数定义 * 设计要点 * 1. 使用 Description 注解该描述会作为函数声明发送给模型 * 2. 参数类型使用 Java recordSpring AI 会自动提取字段类型与描述 * 3. 返回值类型同样需要 Description 标注帮助模型理解如何解读结果 */ Component Description(根据时间范围和状态查询用户的订单列表支持分页) public class OrderQueryFunction implements FunctionOrderQueryFunction.Request, OrderQueryFunction.Response { private final OrderService orderService; public OrderQueryFunction(OrderService orderService) { this.orderService orderService; } /** * 参数使用 record 类型Spring AI 会自动解析字段类型和描述 * 为什么用 record字段不可变、自动生成 getter、减少样板代码 * 与 Spring AI 的参数序列化机制天然契合 */ public record Request( JsonProperty(required true) JsonPropertyDescription(查询的起始日期格式为 yyyy-MM-dd) String startDate, JsonProperty(required true) JsonPropertyDescription(查询的截止日期格式为 yyyy-MM-dd) String endDate, JsonPropertyDescription(订单状态ALL/PENDING/SHIPPED/DELIVERED/CANCELLED默认为 ALL) String status, JsonPropertyDescription(页码从 1 开始默认为 1) int page ) {} public record Response( JsonPropertyDescription(订单列表) ListOrderSummary orders, JsonPropertyDescription(符合条件的订单总数) int total, JsonPropertyDescription(当前页码) int currentPage ) {} public record OrderSummary( JsonPropertyDescription(订单编号) String orderId, JsonPropertyDescription(订单金额单位为元) String amount, JsonPropertyDescription(订单状态) String status, JsonPropertyDescription(下单时间) String createTime ) {} Override public Response apply(Request request) { // 参数校验函数入口处必须做防御性编程 // 大模型传入的参数可能为空或异常值不能完全信任 if (request.startDate null || request.startDate.isBlank()) { throw new IllegalArgumentException(开始日期不能为空); } if (request.endDate null || request.endDate.isBlank()) { throw new IllegalArgumentException(截止日期不能为空); } String safeStatus request.status ! null ? request.status : ALL; int safePage Math.max(request.page, 1); // 页码至少为 1 // 调用底层业务服务 ListOrderService.OrderDTO orders orderService.queryOrders( request.startDate, request.endDate, safeStatus, safePage ); // 转换为函数响应对象 ListOrderSummary summaries orders.stream() .map(o - new OrderSummary(o.getOrderId(), o.getAmount(), o.getStatus(), o.getCreateTime())) .toList(); return new Response(summaries, orderService.countOrders(request.startDate, request.endDate, safeStatus), safePage); } }注册阶段通过ChatClient构建器注入函数/** * ChatClient 配置示例 * * 为什么使用 RequestScope * 每个请求独立创建 ChatClient 实例避免多线程安全问题和上下文污染 */ Configuration public class AIChatConfig { private final OrderQueryFunction orderQueryFunction; private final InventoryQueryFunction inventoryQueryFunction; public AIChatConfig(OrderQueryFunction orderQueryFunction, InventoryQueryFunction inventoryQueryFunction) { this.orderQueryFunction orderQueryFunction; this.inventoryQueryFunction inventoryQueryFunction; } Bean RequestScope public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultFunctions( // 注册函数函数名会自动转换为 snake_case 发送给模型 // 这里注册的函数越多单次调用的 token 消耗越大 // 生产环境建议按业务域分组注册减少无关函数干扰 orderQuery, orderQueryFunction, inventoryQuery, inventoryQueryFunction ) .build(); } }三、生产环境安全防护策略Function Calling 的核心风险在于大模型的输出本质上是不可控的。模型可能被 Prompt 注入攻击诱导调用不应被调用的函数或传递恶意参数。安全防护需要从多个层面构建。第一层函数白名单与最小权限原则。只向模型暴露必要的函数绝不多暴露一个。例如查询类场景只注册查询函数删除、修改类函数应通过独立的系统 Prompt 和会话上下文控制而非全局注册。第二层参数级白名单校验。函数内部必须校验传入的每一个参数/** * 订单查询函数中的安全校验增强版 * * 为什么每个字段都要做白名单校验 * 模型可能被诱导传递 SQL 注入 payload 或越权查询参数 * 函数层是最后一道防线必须在参数进入业务逻辑之前拦截 */ Override public Response apply(Request request) { // 日期格式白名单校验防止路径穿越和注入 if (!request.startDate.matches(\\d{4}-\\d{2}-\\d{2})) { throw new IllegalArgumentException(日期格式不合法); } if (!request.endDate.matches(\\d{4}-\\d{2}-\\d{2})) { throw new IllegalArgumentException(日期格式不合法); } // 状态值白名单校验只允许枚举中的值 SetString VALID_STATUSES Set.of(ALL, PENDING, SHIPPED, DELIVERED, CANCELLED); if (request.status ! null !VALID_STATUSES.contains(request.status)) { throw new IllegalArgumentException(无效的订单状态: request.status); } // 日期范围限制防止查询过大数据量 LocalDate start LocalDate.parse(request.startDate); LocalDate end LocalDate.parse(request.endDate); if (start.isAfter(end)) { throw new IllegalArgumentException(起始日期不能晚于截止日期); } if (start.until(end).getDays() 90) { throw new IllegalArgumentException(查询时间范围不能超过 90 天); } // 页码限制 if (request.page 1 || request.page 100) { throw new IllegalArgumentException(页码超出范围); } // 以下调用业务逻辑... return new Response(Collections.emptyList(), 0, 1); }第三层调用频率控制。每个函数应设置调用频率上限防止模型在解析失败时重复疯狂调用。这需要与 Resilience4j 的 RateLimiter 结合。第四层审计日志。每次 Function Call 需要记录用户 ID、会话 ID、调用的函数名、传入的参数、返回结果摘要。这条审计链是排障和安全审查的基础。四、实战构建安全的订单查询服务完整展示一个经过安全加固的 Function Calling 端点实现/** * AI 对话控制器 * * 设计考量 * 1. 使用异步返回StreamingResponseBody提升用户体验 * 2. 用户上下文通过 SecurityContext 注入不从 Prompt 中解析 * 3. 响应校验检查模型是否泄露了不该返回的敏感信息 */ RestController RequestMapping(/api/ai) public class AIChatController { private final ChatClient chatClient; private final AuditLogger auditLogger; public AIChatController(ChatClient chatClient, AuditLogger auditLogger) { this.chatClient chatClient; this.auditLogger auditLogger; } PostMapping(/chat) public ResponseEntityStreamingResponseBody chat( RequestBody ChatRequest chatRequest, Authentication authentication) { // 从认证上下文获取真实用户 ID而不是从 Prompt 中获取 // 为什么这样设计Prompt 中的用户身份可以被伪造 // SecurityContext 是服务端认证后的可信信息 String realUserId authentication.getName(); StreamingResponseBody stream outputStream - { try { // 注入用户上下文到 Advisors // 将真实 userId 通过 Advisor 追加到每次请求中 // 而不是拼接到用户 Prompt防止 User Impersonation 攻击 FluxString flux chatClient.prompt() .user(chatRequest.getMessage()) .advisors(a - a.param(userId, realUserId)) .stream() .content(); // 流式输出同时做审计 flux.doOnNext(content - { try { outputStream.write(content.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } catch (IOException e) { throw new RuntimeException(流式输出失败, e); } }).doOnComplete(() - { auditLogger.log( AuditEvent.builder() .userId(realUserId) .eventType(AI_CHAT_COMPLETE) .prompt(chatRequest.getMessage()) .timestamp(Instant.now()) .build() ); }).blockLast(); } catch (Exception e) { auditLogger.logError(realUserId, AI_CHAT_ERROR, e.getMessage()); // 不向客户端暴露内部错误细节防止信息泄露 try { outputStream.write(服务暂时不可用请稍后重试.getBytes(StandardCharsets.UTF_8)); } catch (IOException ignored) { // 连接可能已关闭忽略写入错误 } } }; return ResponseEntity.ok() .contentType(MediaType.TEXT_EVENT_STREAM) .body(stream); } }五、总结Function Calling 是连接大模型与业务系统的关键桥梁但它引入了一个新的安全隐患模型的不可控输出被赋予了服务调用能力。安全防护的核心理念是不信任模型的一切输入函数层必须做好参数白名单校验、调用频率控制、审计日志三条防线。在生产实践中建议将查询类函数和操作类函数分离管理操作类函数应通过确认机制如要求用户显式确认来降低风险。架构上Spring AI 提供的 Function 抽象与 Spring 生态天然集成配合 Resilience4j 和 Prometheus 监控可以构建一套可控、可观测、可审计的大模型服务调用体系。