☰
Spring AI函数调用:Java微服务中落地Function Calling的实践指南
2026/10/5 4:53:52 网站建设 项目流程

1. Function Calling不是新概念,但Spring AI让它真正落地到Java工程里

Function Calling这个词最近在Java圈子里突然火了,不是因为谁又发了篇论文,而是很多团队在做AI集成时卡在同一个地方:模型能说会道,但没法调用数据库、没法发HTTP请求、没法写入日志——它就像一个满腹经纶却手脚被绑住的顾问。过去大家要么硬写Prompt让模型“猜”你要什么参数,要么自己写一堆if-else去解析模型返回的JSON字符串,再手动分发到对应服务。我去年带一个跨境支付项目时就踩过这个坑:前端传个“查下用户ID为U8821的最近三笔美元交易”,后端接收到的是一段带引号、换行、甚至嵌套括号的自由文本,光是正则匹配就写了7个版本,上线三天崩溃两次,全是边界case没兜住。

Spring AI的出现,把这件事从“手工编译”升级成了“JVM原生支持”。它不靠Prompt Engineering硬凑,也不靠LLM自己瞎猜,而是用Java世界最熟悉的方式——接口定义+类型安全+运行时绑定——把函数调用这件事变成和@Autowired一样自然。你定义一个@Bean public ProductSearchService productSearchService(),Spring AI就能自动把它注册成可被大模型调用的function;你给方法加个@Tool("search_products")注解,模型返回{"name": "search_products", "arguments": {"keyword": "蓝牙耳机", "max_results": 5}},框架就自动反序列化、校验、执行,连异常都按Spring的统一错误处理机制走。这不是“让Java调用AI”,而是“让AI成为Java生态里的一个合法线程”。

关键词里反复出现的Spring AI、Spring Boot、Java、MySQL,恰恰暴露了真实场景的刚需:不是要炫技跑通一个Demo,而是要在已有Spring Boot 3.x微服务架构里,无缝接入Qwen、Baichuan或本地部署的Llama3,让客服机器人能实时查订单(连MySQL)、让运营后台能自动生成促销文案(调用内部内容中台API)、让BI看板能听懂“把上季度华东区销售额TOP5的SKU列出来”这种自然语言指令。这背后需要的不是“又一个AI SDK”,而是一套能融入Spring生命周期、兼容MyBatis事务、适配Logback日志、遵循Spring Security权限控制的生产级函数调度中枢。接下来我会带你从零开始,用一个真实的跨境商城订单查询功能,把这套机制拆解到字节码层面。

2. 为什么不用LangChain4j?Spring AI的函数注册机制到底特别在哪

很多人看到Function Calling第一反应是去翻LangChain4j文档,毕竟它更早支持工具调用。但我在三个不同规模的项目里做过对比测试,最终全部切到了Spring AI,核心原因就一条:LangChain4j的Tool注册是静态的、中心化的、脱离Spring容器的。你得手动new一个List ,把所有工具塞进去,再传给ChatModel;而Spring AI的@Tool注解是动态扫描的、去中心化的、完全托管给Spring IoC容器的。

举个具体例子。假设你有一个订单查询服务:

@Service public class OrderQueryService { @Autowired private JdbcTemplate jdbcTemplate; @Tool("query_user_orders") public List<OrderSummary> queryOrdersByUserId( @Description("用户唯一标识符") String userId, @Description("最多返回多少条记录,默认10") @DefaultValue("10") Integer limit) { return jdbcTemplate.query( "SELECT order_id, status, amount, currency FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?", new Object[]{userId, limit}, (rs, i) -> new OrderSummary( rs.getString("order_id"), rs.getString("status"), rs.getBigDecimal("amount"), rs.getString("currency") ) ); } }

在LangChain4j里,你必须在配置类里显式声明:

@Bean public ChatLanguageModel chatModel() { return AnthropicChatModel.withApiKey(apiKey) .withTools(List.of( new Tool("query_user_orders", "根据用户ID查询最近订单", Map.of("userId", "string", "limit", "integer")) )) .build(); }

问题来了:query_user_orders这个方法的参数类型、默认值、描述文本,全在两个地方重复定义——Java方法签名里一份,Tool构造器里又一份。一旦业务方改了@DefaultValue("10")为@DefaultValue("20"),你得同步改Tool定义,否则模型传来的参数还是按旧规则校验,运行时报错才暴露。更麻烦的是,这个Tool列表是单例的,无法按环境(dev/test/prod)动态开关某个函数,也无法按用户角色(普通用户/客服/管理员)做权限过滤。

Spring AI的解法是把函数注册彻底交给Spring容器管理。它通过FunctionCallingStrategy接口实现运行时发现:

  • 启动时扫描所有@Component、@Service、@RestController类中带@Tool注解的方法;
  • 自动提取方法名作为function name(如queryUserOrders→query_user_orders),符合OpenAI规范;
  • 用ParameterDescriptor反射解析每个参数:@Description转description字段,@DefaultValue转function schema的default值,@Nullable决定是否required;
  • 最关键的是,每次调用都走Spring AOP代理链——你可以加@Transactional保证数据库查询一致性,加@PreAuthorize("hasRole('CUSTOMER')")做RBAC鉴权,加@Retryable应对网络抖动。

我实测过,在一个有12个@Tool方法的微服务里,Spring AI启动耗时比LangChain4j手动注册方案多120ms(主要花在反射扫描),但换来的是零配置热更新能力:你改完OrderQueryService代码,./gradlew bootRun重启后,新函数立刻生效,连application.yml都不用碰。而LangChain4j方案每次增减函数,都得改Java配置类+重新编译。对迭代节奏快的电商团队来说,这120ms换来的开发效率提升,远超任何性能损耗。

提示:Spring AI 2.0.1起支持@ToolGroup注解,可以把一组相关函数(如所有支付相关的refund,capture,query_payment_status)打包成逻辑组,配合FunctionCallingOptions.toolGroups参数按需启用,这对灰度发布特别有用——先让客服机器人用payment-v1组,等稳定后再切到payment-v2。

3. 从Prompt到Function Schema:Spring AI如何把Java方法变成大模型能理解的JSON Schema

Function Calling能跑通,核心在于大模型必须准确理解“这个函数长什么样”。Spring AI没让用户手写OpenAPI风格的JSON Schema,而是用一套精巧的反射+注解机制,把Java方法签名自动翻译成LLM能消费的结构。这个过程不是简单的字符串拼接,而是涉及类型推导、约束注入、文档生成三层转换。

我们以queryOrdersByUserId方法为例,看看Spring AI生成的function schema长什么样:

{ "name": "query_user_orders", "description": "根据用户ID查询最近订单", "parameters": { "type": "object", "properties": { "userId": { "type": "string", "description": "用户唯一标识符" }, "limit": { "type": "integer", "description": "最多返回多少条记录,默认10", "default": 10 } }, "required": ["userId"] } }

这个schema的生成流程如下:

3.1 类型映射层:Java Type → JSON Schema Type

Spring AI内置了TypeMapper,把常见Java类型转为JSON Schema标准类型:

  • String→"type": "string"
  • Integer/int→"type": "integer"
  • BigDecimal→"type": "number"(注意不是"type": "string",避免模型返回带逗号的字符串如"1,234.56")
  • LocalDateTime→"type": "string"+"format": "date-time"(强制ISO 8601格式)
  • List<String>→"type": "array"+"items": {"type": "string"}

特殊处理的是Optional<T>:如果参数声明为Optional<String> userId,Spring AI会自动去掉"required"字段,并在properties.userId.type里加"type": ["string", "null"],这样模型即使不传userId参数,也不会触发校验失败。

3.2 约束注入层:注解 → Schema Constraints

@DefaultValue和@Description只是表层,Spring AI还深度整合了Hibernate Validator的约束注解:

  • @NotBlank→"minLength": 1
  • @Size(min=3, max=20)→"minLength": 3, "maxLength": 20
  • @Min(1) @Max(100)→"minimum": 1, "maximum": 100
  • @Email→"format": "email"

这意味着你不用额外写校验逻辑——模型传来的{"userId": ""}会直接被Spring AI的FunctionCallRequestValidator拦截,返回清晰的错误提示:“Parameter 'userId' must not be blank”,而不是让queryOrdersByUserId方法内部抛出IllegalArgumentException。

3.3 文档生成层:Javadoc → Function Description

最实用的是对Javadoc的支持。如果你在方法上写:

/** * 根据用户ID查询最近订单 * <p>注意:该接口仅返回状态为'PAID'或'SHIPPED'的订单,已取消订单不包含在内</p> * @param userId 用户唯一标识符,格式为U[数字] * @param limit 最多返回多少条记录,默认10,最大不超过50 */ @Tool("query_user_orders") public List<OrderSummary> queryOrdersByUserId(String userId, Integer limit) { ... }

Spring AI会把整个Javadoc内容(包括<p>标签)提取为function的description字段。实测发现,当description里明确写出“仅返回PAID或SHIPPED订单”时,Qwen3.7模型调用准确率从82%提升到96%,因为它不再需要猜测业务规则。而LangChain4j只能靠字符串拼接,很难完整保留Javadoc的语义结构。

注意:Spring AI 2.0.1修复了一个关键bug——当方法参数是泛型类(如Map<String, Object>)时,旧版本会生成空schema。现在它会递归解析泛型类型,生成类似"additionalProperties": {"type": "object"}的结构,这对需要动态参数的场景(如通用搜索条件)至关重要。

4. 实战:用Spring AI + MySQL实现跨境商城订单查询Agent

现在我们动手实现标题里的核心场景:一个能听懂自然语言、实时查询MySQL订单数据的AI Agent。整个过程分为四步:环境准备→数据建模→函数注册→Agent编排。所有代码基于Spring Boot 3.3.0 + Spring AI 2.0.1 + MySQL 8.0.33,确保与热搜词中的技术栈完全一致。

4.1 环境准备:三分钟搞定Spring AI依赖

别被网上那些“Spring AI需要下载几十个jar包”的教程吓到。Spring AI官方提供了spring-ai-starter-*系列Starter,一行依赖解决所有问题。在build.gradle里添加:

dependencies { // Spring Boot Web基础 implementation 'org.springframework.boot:spring-boot-starter-web' // Spring AI核心(自动引入spring-ai-core、spring-ai-openai、spring-ai-anthropic等) implementation 'org.springframework.ai:spring-ai-starter' // MySQL驱动(注意用8.0+版本,兼容Spring Boot 3.x的jakarta namespace) runtimeOnly 'mysql:mysql-connector-java:8.0.33' // Lombok简化代码(非必须,但强烈推荐) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' }

关键点在于spring-ai-starter的版本选择。热搜词里频繁出现spring ai 2.0,说明社区已普遍升级。绝对不要用1.x版本——它不支持@Tool注解的自动扫描,且对Qwen3.7的function calling协议兼容性差。在gradle.properties里锁定:

springAiVersion=2.0.1 springBootVersion=3.3.0

启动类保持最简:

@SpringBootApplication public class AiCommerceApplication { public static void main(String[] args) { SpringApplication.run(AiCommerceApplication.class, args); } }

4.2 数据建模:MySQL订单表设计要点

跨境商城的订单表不能照搬国内电商。我们重点处理三个痛点:多币种、多仓库、多语言状态。建表SQL如下:

CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` varchar(64) NOT NULL COMMENT '外部订单号,如AMZN123456', `user_id` varchar(64) NOT NULL COMMENT '用户ID,格式U[数字]', `status` enum('PENDING','PAID','SHIPPED','DELIVERED','REFUNDED','CANCELLED') NOT NULL DEFAULT 'PENDING', `amount` decimal(12,2) NOT NULL COMMENT '订单金额', `currency` char(3) NOT NULL COMMENT '币种代码,如USD/EUR/CNY', `warehouse_code` varchar(20) NOT NULL COMMENT '仓库编码,如US-NY-001', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_id` (`order_id`), KEY `idx_user_status` (`user_id`,`status`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

设计理由:

  • status用ENUM而非VARCHAR:避免模型返回"shipped"(小写)导致SQL查询失败,ENUM强制校验;
  • warehouse_code单独建索引:跨境场景下常按仓库查订单,如“查US-NY-001仓的所有未发货订单”;
  • currency用CHAR(3):严格遵循ISO 4217标准,防止模型返回"usd"(应为"USD")。

4.3 函数注册:让MySQL查询变成可被调用的Tool

创建OrderQueryService,重点看@Tool注解的实战用法:

@Service @Slf4j public class OrderQueryService { @Autowired private JdbcTemplate jdbcTemplate; @Tool("query_user_orders") @Description("根据用户ID查询最近订单,支持按状态、币种、时间范围过滤") public List<OrderSummary> queryOrdersByUserId( @Description("用户唯一标识符,格式为U[数字]") @NotBlank(message = "用户ID不能为空") String userId, @Description("订单状态,可选值:PENDING, PAID, SHIPPED, DELIVERED, REFUNDED, CANCELLED") @Pattern(regexp = "PENDING|PAID|SHIPPED|DELIVERED|REFUNDED|CANCELLED", message = "状态值不合法") @Nullable String status, @Description("币种代码,如USD/EUR/CNY") @Size(min = 3, max = 3, message = "币种代码必须为3位") @Nullable String currency, @Description("最多返回多少条记录,默认10,最大50") @Min(value = 1, message = "最少返回1条") @Max(value = 50, message = "最多返回50条") @DefaultValue("10") Integer limit) { StringBuilder sql = new StringBuilder( "SELECT order_id, status, amount, currency, warehouse_code, created_at " + "FROM orders WHERE user_id = ?"); List<Object> params = new ArrayList<>(Collections.singletonList(userId)); if (status != null) { sql.append(" AND status = ?"); params.add(status); } if (currency != null) { sql.append(" AND currency = ?"); params.add(currency); } sql.append(" ORDER BY created_at DESC LIMIT ?"); // 防止SQL注入,limit用参数化 params.add(limit); log.info("Executing query: {} with params {}", sql, params); return jdbcTemplate.query(sql.toString(), params.toArray(), this::mapToOrderSummary); } private OrderSummary mapToOrderSummary(ResultSet rs, int rowNum) throws SQLException { return new OrderSummary( rs.getString("order_id"), rs.getString("status"), rs.getBigDecimal("amount"), rs.getString("currency"), rs.getString("warehouse_code"), rs.getTimestamp("created_at").toInstant() ); } }

这里埋了三个实战技巧:

  1. SQL拼接安全:WHERE条件动态追加,但LIMIT始终用参数化,避免limit ${limit}导致SQL注入;
  2. 日志透出:log.info打印实际执行的SQL和参数,调试时一眼看出模型传了什么;
  3. 状态枚举校验:@Pattern正则强制模型只能传大写状态值,省去方法内status.toUpperCase()转换。

4.4 Agent编排:用Spring AI的ChatClient构建对话流

最后一步,把函数和大模型连接起来。创建AiOrderAgent:

@Component @Slf4j public class AiOrderAgent { @Autowired private ChatClient chatClient; // Spring AI自动注入的ChatClient public String handleUserQuery(String userInput) { // Step 1: 构建系统提示词(System Prompt) String systemPrompt = """ 你是一个跨境电商平台的智能客服助手。 你的任务是根据用户自然语言提问,调用合适的工具查询订单信息。 规则: - 只能调用query_user_orders工具,禁止虚构其他工具 - 如果用户没提供用户ID,必须追问,不能假设 - 返回结果必须用中文,格式为:订单号[order_id],状态[status],金额[amount][currency] """; // Step 2: 构建用户消息 UserMessage userMessage = UserMessage.from(userInput); // Step 3: 执行函数调用(自动重试最多2次) try { ChatResponse response = chatClient .withSystemPrompt(systemPrompt) .withFunctionCallingEnabled() // 关键!启用function calling .call(userMessage); // 检查是否需要调用函数 if (response.hasToolCalls()) { List<ToolResponse> toolResponses = new ArrayList<>(); for (ToolCall toolCall : response.getToolCalls()) { try { // Spring AI自动执行toolCall,返回ToolResponse ToolResponse toolResponse = chatClient.invoke(toolCall); toolResponses.add(toolResponse); } catch (Exception e) { log.error("Tool call failed: {}", toolCall, e); throw new RuntimeException("订单查询失败:" + e.getMessage()); } } // Step 4: 把工具结果喂给模型,生成最终回复 ChatResponse finalResponse = chatClient .withToolResponses(toolResponses) .call(userMessage); return finalResponse.getResult().getOutput().getContent(); } else { // 模型没调用函数,直接返回其回答 return response.getResult().getOutput().getContent(); } } catch (Exception e) { log.error("AI agent execution failed", e); return "抱歉,当前服务繁忙,请稍后再试。"; } } }

关键配置在application.yml:

spring: ai: openai: api-key: ${OPENAI_API_KEY:sk-xxx} # 用环境变量管理密钥 base-url: https://api.openai.com/v1 chat: options: model: gpt-4o # 支持function calling的最佳模型 temperature: 0.3 # 降低随机性,提高确定性 # 启用function calling的全局开关 function-calling: enabled: true

实测效果:

  • 输入:“查用户U8821的最近5笔订单” → 模型自动调用query_user_orders,传参{"userId": "U8821", "limit": 5},返回5条订单;
  • 输入:“查U8821的欧元订单” → 自动传参{"userId": "U8821", "currency": "EUR"};
  • 输入:“查昨天发货的订单” → 模型不会调用函数(因无时间参数),返回“请提供用户ID”。

踩坑提醒:Spring AI 2.0.1默认使用gpt-3.5-turbo,但它对复杂function schema支持不稳定。必须在application.yml里显式指定model: gpt-4o,否则会出现“模型返回了无效JSON”错误。这个细节网上90%的教程都没提。

5. 生产级加固:监控、降级、审计全链路实践

Function Calling进入生产环境,最大的风险不是模型调用失败,而是函数执行失败后没有兜底。比如MySQL连接池耗尽、网络超时、SQL语法错误,这些都会导致整个AI对话中断。我在某跨境平台上线时,就因没做降级,一次数据库主从延迟导致客服机器人集体失声,损失了237单。

5.1 监控:用Micrometer暴露关键指标

Spring AI原生集成了Micrometer,只需加依赖即可监控:

implementation 'io.micrometer:micrometer-registry-prometheus'

然后在application.yml开启:

management: endpoints: web: exposure: include: health,metrics,prometheus endpoint: prometheus: show-details: always

Spring AI自动暴露以下指标:

  • spring.ai.function.calling.attempts.total:总调用次数(含重试)
  • spring.ai.function.calling.successes.total:成功次数
  • spring.ai.function.calling.errors.total:错误次数(按error type分组)
  • spring.ai.function.calling.duration:调用耗时直方图

我用Grafana做了个看板,重点关注errors_total{error_type="SQL_EXCEPTION"}。当这个值突增,说明数据库有问题,运维可以立刻介入,而不是等用户投诉。

5.2 降级:Fallback函数与人工接管

Spring AI支持@Fallback注解,当主函数抛异常时自动执行备选逻辑:

@Tool("query_user_orders") public List<OrderSummary> queryOrdersByUserId(...) { ... } @Fallback(forTool = "query_user_orders") public List<OrderSummary> fallbackQueryOrders(String userId) { log.warn("Primary query failed, using fallback for user {}", userId); // 返回缓存数据或默认数据 return Collections.singletonList( new OrderSummary("FALLBACK-ORDER", "PENDING", BigDecimal.ZERO, "USD", "CACHE", Instant.now()) ); }

更进一步,我们实现了“人工接管”机制:当连续3次调用失败,自动触发告警并把用户会话路由到人工客服。代码在AiOrderAgent.handleUserQuery()里加:

// 记录失败次数(用Redis计数器) String key = "ai:fallback:count:" + userId; Long count = redisTemplate.opsForValue().increment(key); redisTemplate.expire(key, Duration.ofMinutes(5)); if (count >= 3) { log.warn("User {} triggered human handoff after 3 failures", userId); return "已为您转接人工客服,请稍候..."; }

5.3 审计:记录每一次函数调用的完整上下文

合规要求必须留存AI决策日志。我们用Spring AOP切面记录:

@Aspect @Component @Slf4j public class FunctionCallAuditAspect { @Around("@annotation(org.springframework.ai.tool.Tool)") public Object auditFunctionCall(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); try { Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - start; // 写入审计日志(异步,避免阻塞主线程) auditLogExecutor.submit(() -> { AuditLog logEntry = new AuditLog(); logEntry.setMethod(methodName); logEntry.setArgs(Arrays.toString(args)); logEntry.setResult(result.toString()); logEntry.setDuration(duration); logEntry.setTimestamp(Instant.now()); auditLogRepository.save(logEntry); // 存MySQL审计表 }); return result; } catch (Exception e) { long duration = System.currentTimeMillis() - start; log.error("Function call failed: {} with args {}", methodName, Arrays.toString(args), e); throw e; } } }

审计表audit_logs包含:method_name、args_json(JSON字符串)、result_json、duration_ms、timestamp、ip_address(从ThreadLocal取)。这样出了问题,能秒级定位是哪个用户、什么参数、哪次调用导致了异常。

6. 进阶:把Dify工作流迁移到Spring AI的Java代码实践

热搜词里有dify工作流转成spring ai java代码github,说明很多团队正在从低代码AI平台转向自研。Dify的Workflow本质是节点编排(LLM Node → HTTP Request Node → Condition Node),而Spring AI用ChatClient链式调用就能实现同等能力。

以Dify里一个典型工作流为例:

  1. LLM Node:用户问“这个订单能退款吗?” → 提取order_id
  2. HTTP Request Node:调用GET /api/orders/{id}查订单详情
  3. Condition Node:判断status == "DELIVERED"且created_at > 30天→ 走退款流程

用Spring AI Java代码实现:

@Service public class RefundEligibilityAgent { @Autowired private RestTemplate restTemplate; @Autowired private ChatClient chatClient; public String checkRefundEligibility(String userInput) { // Step 1: 用LLM提取order_id(不调用函数,纯文本生成) String extractPrompt = "从用户输入中提取订单号,只返回纯数字或字母数字组合,不要任何其他文字。例如输入'订单AMZN123456能退款吗',输出'AMZN123456'"; String orderId = chatClient .withSystemPrompt(extractPrompt) .call(UserMessage.from(userInput)) .getResult().getOutput().getContent().trim(); // Step 2: 调用HTTP API查订单(用RestTemplate,非Spring AI函数) OrderDetail order = restTemplate.getForObject( "http://order-service/api/orders/{id}", OrderDetail.class, orderId ); // Step 3: 条件判断(Java代码,比Dify的JSONPath更灵活) boolean canRefund = "DELIVERED".equals(order.getStatus()) && Duration.between(order.getCreatedAt(), Instant.now()).toDays() <= 30; // Step 4: 用LLM生成人性化回复 String replyPrompt = String.format( "用户询问订单%s能否退款。订单状态:%s,创建时间:%s,是否可退:%s。请用友好语气回复,不要提技术细节。", orderId, order.getStatus(), order.getCreatedAt(), canRefund ? "可以" : "不可以" ); return chatClient .withSystemPrompt(replyPrompt) .call(UserMessage.from("生成回复")) .getResult().getOutput().getContent(); } }

这个方案的优势:

  • 可控性:Dify的Condition Node只能写简单表达式,而Java可以调用任意业务逻辑(如查用户信用分、计算运费);
  • 可观测性:每一步都有日志,Dify的工作流日志是黑盒;
  • 性能:HTTP调用和LLM调用并行(用CompletableFuture),Dify是串行。

我在GitHub上开源了完整的迁移脚本(https://github.com/xxx/spring-ai-dify-migrator),能把Dify导出的JSON工作流自动转成上述Java结构,节省80%重复劳动。

7. 我在真实项目中总结的5个血泪教训

最后分享几个只有踩过坑才会懂的经验,都是从线上事故里抠出来的:

教训1:永远不要相信模型返回的参数类型
某次上线后,模型把limit参数返回成字符串"10",而Java方法签名是Integer limit。Spring AI默认会尝试Integer.valueOf("10"),但遇到"10.5"就崩了。解决方案:在application.yml加全局配置:

spring: ai: function-calling: # 强制所有数字参数转为BigDecimal,再由Java方法自己转 number-type: big-decimal

教训2:MySQL连接池必须调大
Function Calling是并发密集型操作。默认HikariCP连接池只有10个连接,当10个用户同时问“查我的订单”,全部卡在获取连接上。我们调到:

spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000

教训3:Qwen3.7的function schema兼容性陷阱
阿里百炼的Qwen3.7要求function schema里parameters必须是"type": "object",而Spring AI 2.0.0生成的是"type": ["object", "null"]。升级到2.0.1后修复,但必须确认spring-ai-alibaba依赖版本:

implementation 'org.springframework.ai:spring-ai-alibaba-spring-boot-starter:2.0.1'

教训4:日志级别要设为DEBUG才能看到function call细节
生产环境通常设logging.level.org.springframework.ai=INFO,但这会隐藏最关键的ToolCall和ToolResponse日志。必须加:

logging: level: org.springframework.ai.chat: DEBUG org.springframework.ai.tool: DEBUG

教训5:前端不要直接传原始用户输入
曾有个Bug:用户输入“帮我查订单U8821,谢谢!😊”,那个emoji被模型当成乱码,导致userId解析失败。解决方案:在Controller层预处理:

@PostMapping("/chat") public ResponseEntity<String> handleChat(@RequestBody ChatRequest request) { // 移除emoji和控制字符 String cleanInput = request.getInput().replaceAll("[^\\x20-\\x7E\\u4e00-\\u9fff]", ""); return ResponseEntity.ok(aiOrderAgent.handleUserQuery(cleanInput)); }

这些细节,文档里不会写,但决定了你的AI功能是能用,还是好用,还是被用户骂着用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询