
1. 项目拆解Spring AI如何让大模型“听懂”并调用你的API先把这个项目的核心说透。标题里三个词Spring AI、大模型、自己的API串起来之后干的其实是一件很多团队都在琢磨的事让用户用大白话指挥大模型大模型再把话翻译成对后端已有接口的调用。说白了就是把大模型当调度中枢把你自己写的Service当作可被调用的工具。1.1 为什么会产生这种需求传统后端接口的调用路径非常固定前端页面点按钮请求打到Controller然后进Service查数据库最后把结果渲染回页面。问题在于用户能触发什么操作完全取决于页面上做了哪些按钮、哪些表单。你做了一个订单查询页面用户就只能通过这个页面查订单想用自然语言问一句“我前天买的那个东西发货了没”页面根本接不住。企业里的老系统更是如此订单、库存、物流、会员这些接口早就写好了但入口都是固定的。要让这些接口被自然语言驱动传统做法是自己做语义解析写一堆意图识别规则效果还很差。大模型出现以后自然语言理解这个环节直接变成了现成能力缺的只是“理解之后如何安全、准确地调用你已经写好的API”。Spring AI在这里扮演的角色就是把大模型和已有代码之间的这条通路铺好。你不用自己去处理模型返回的原始JSON不用自己维护工具清单只需要给方法加一个注解剩下的交给框架。1.2 为什么不自己拼Prompt让模型输出JSON很多人听到这个需求的第一反应是那简单我写一个Prompt告诉大模型“你有以下工具可用请输出JSON格式的调用参数”然后我用Jackson解析这个JSON再去调自己的API。这个思路做Demo完全没问题但做生产级系统会遇到一连串的坑。首先是输出不稳定模型的回复经常在JSON上下加Markdown代码块偶尔还会带着几句多余的解释你得写一堆容错解析逻辑。其次是参数容易出错模型可能把订单号字段拼错可能漏掉必填参数可能把字符串类型的数字写成了数值类型。最要命的是Prompt膨胀每加一个工具你就得往Prompt里塞一段工具描述工具一多上下文被占掉一大块Token成本居高不下模型反而容易混淆。Function Calling官方叫法Tool Calling是模型厂商专门为这个场景设计的机制。模型在训练阶段就对“工具调用”这个动作做了对齐它知道什么时候该输出工具调用输出的结构也是标准化的tool_calls对象不是自由文本。Spring AI做的事情就是把这套机制封装成Java开发者熟悉的注解和组件。1.3 用生活化的比喻理解整个链路把大模型想象成一个客服前台你自己写的API想象成各个业务部门。用户走进来问“我订单到哪了”这个客服前台不会自己去仓库翻货它要做的是填一张工单把“查订单”这个诉求和订单号一起转给订单部门。订单部门处理完把结果退回前台前台再组织成一句用户能听懂的话回复出去。Function Calling就是那张工单的结构Spring AI是负责把工单转到正确部门、再把结果拿回来的中间人系统。Java开发者要做的事情就是告诉这个中间人“我们有一个订单部门它接收订单号参数能返回订单状态”剩下的流转环节框架替你处理掉了。2. 方案选型与前置准备模型、依赖、配置一步到位落地之前先把技术选型和环境准备想清楚能少走很多弯路。2.1 选哪个大模型、怎么连Spring AI不绑定任何一家模型厂商它抽象了模型接口。OpenAI、Azure OpenAI、通义千问、DeepSeek、Ollama本地模型甚至是公司自己用vLLM部署的开源模型原理上都能接入。核心判断标准是API协议是否兼容兼容OpenAI协议的基本都能用Spring AI的OpenAI Starter直接连。我这次用的是DeepSeek原因很实在价格低国内访问稳定Function Calling能力足够应付工具调用场景。另外它兼容OpenAI的API协议所以Spring AI的OpenAI驱动可以直接拿着DeepSeek的Key和地址用。如果你在别的平台上跑配置核心就两个值base-url和model name。这里顺便提一下自建模型的情况。如果你用的是公司内网部署的模型比如vLLM部署了Qwen系列那Spring AI照样能接只需要把base-url指向自建服务的地址前提是那个服务提供OpenAI兼容的接口。Spring AI在模型适配层做得很省心换模型往往只是改配置的事。2.2 Spring AI和LangChain4j怎么选Java生态里接入大模型绕不开这两个名字。LangChain4j社区也很活跃热词里也出现了langchain4j说明不少人在关注。我的选择逻辑很简单如果你本来就在Spring Boot生态里优先用Spring AI。理由有两个。第一是自动配置和Starter机制Spring AI和Spring Boot是一家人依赖引入、配置加载、Actuator监控都是现成的。第二是注解化程度高我在后面要讲的Tool注解用起来非常接近Spring MVC里GetMapping那种熟悉感Java开发者基本零学习成本。LangChain4j的优势则是功能覆盖更广抽象风格更接近Python版LangChain。如果你以前写过Python的LangChain代码换到Java生态用LangChain4j会更顺手。两个框架的底层原理是一样的都是把工具描述传给模型模型返回结构化调用指令框架负责执行。殊途同归选哪个盯准自己的技术栈就行不必纠结太久。2.3 构建一个最小项目需要哪些依赖环境要求是JDK 17以上Spring Boot 3.2以上。Maven的pom.xml里加入Spring AI的OpenAI驱动dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependencySpring AI的版本迭代节奏比较快1.0.0是GA版本API已经趋于稳定。如果你的Spring Boot版本比较新通常兼容性没有问题。然后是配置文件这是整个项目里我踩过最多坑的地方spring: ai: openai: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com chat: options: model: deepseek-chat temperature: 0.3几个关键点值得啰嗦一遍。api-key千万别硬编码在yml里从环境变量读是最基本的底线后面接配置中心也行。base-url写到域名级就够不要自己脑补加/v1什么的后缀不同的服务方要求不一样以官方文档为准。temperature在工具调用场景建议调到0.3以下太高模型容易发散该调工具的时候不调反而给你编一段回答出来。3. 实操落地用Tool把订单查询API暴露给大模型下面进入核心环节我以一个订单查询助手为例子把完整实现串一遍。这个例子的业务逻辑不复杂但足够说明Spring AI工具调用的完整套路。3.1 先定义好返回结构假设你的系统里早就有一个OrderService里面有现成的订单查询方法。OrderInfo我用的是一个Java record字段保持简单清晰public record OrderInfo( String orderNo, String status, BigDecimal amount, String productName ) {}选择record而不是传统JavaBean一方面代码简洁另一方面Spring AI在把方法签名转化成JSON Schema的时候对record的解析更干净不会出现无参构造、getter setter这种干扰项。3.2 写一个带Tool注解的服务门面直接在业务方法上加Tool注解这是Spring AI 1.0以后的推荐写法Service public class OrderAssistantService { private final ChatClient chatClient; private final OrderService orderService; public OrderAssistantService(ChatClient.Builder builder, OrderService orderService) { this.orderService orderService; this.chatClient builder .defaultSystem(你是智能订单助手。用户询问订单相关信息时必须调用queryOrderByNo工具获取不要编造订单数据。) .build(); } Tool(description 根据订单号查询订单信息返回订单状态、金额、商品名称) public OrderInfo queryOrderByNo(String orderNo) { return orderService.getOrderInfo(orderNo); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }这段代码里有几个细节需要展开讲。Tool注解的description字段非常重要模型完全依赖这段文字来判断“这个工具是干什么的、什么时候该用它”。写得太笼统模型不知道该不该调用写得太复杂模型抓不住重点。我的习惯是遵守“动作输入输出”三层结构比如“根据订单号查询订单信息返回订单状态、金额、商品名称”模型看到就知道该传什么参数、能拿回什么结果。ChatClient的创建方式也值得注意。通过ChatClient.Builder来构建构建的时候可以设置默认系统提示词。这个系统提示词相当于给模型立规矩比如“必须调用工具”“不要编造数据”在工具调用场景里这行提示词的作用非常明显。Spring AI会自动把当前Spring容器里所有标注了Tool的方法收集起来注册成工具集不需要手动去写注册逻辑。3.3 写一个入口Controller验证效果Controller保持简单接收用户消息转给服务层处理RestController public class ChatController { private final OrderAssistantService assistant; public ChatController(OrderAssistantService assistant) { this.assistant assistant; } PostMapping(/chat) public String chat(RequestBody ChatRequest request) { return assistant.chat(request.message()); } }启动项目后用curl做一次冒烟测试curl -X POST http://localhost:8080/chat \ -H Content-Type: application/json \ -d {message: 帮我查一下订单20240513001现在是什么状态}如果一切正常返回的内容会类似“订单20240513001已发货商品是无线机械键盘金额299元”这说明大模型正确识别了意图、提取了订单号参数、调用了你的查询接口、再根据接口结果组织了回答。3.4 工具多了怎么办实际项目里不可能只有一个接口。一个助手可能同时有订单查询、物流跟踪、退款申请、优惠券查询好几个工具你可以把Tool方法分散到不同的Service类里Spring AI会扫描所有被Spring管理的Bean。但是需要注意工具越多每次请求带过去的工具描述Token就越多模型在选择工具时也更容易迷糊。我实践下来的办法是按业务域拆分成多个ChatClient实例。订单问答用一个只挂订单工具的ChatClient库存问答用另一个只挂库存工具的ChatClient。这样每个模型请求的上下文更精简调用准确率明显提升。如果工具数量在几十个量级就需要考虑动态工具选择的方案了比如先让模型做一次粗分类再加载对应工具集具体做法根据业务复杂度灵活决定。4. 调用链路拆解一次问答背后到底发生了什么跑通了Demo之后很多人对内部机制还是一团黑。我建议把一次调用的完整链路彻底搞明白后面排查问题才有方向。4.1 完整流程的逐步拆解一次“帮我查订单”的请求背后其实发生了八步。第一步用户输入消息进入ChatClient。第二步Spring AI把系统提示词、所有Tool方法生成的工具描述、用户消息拼装成一个符合OpenAI协议格式的请求。第三步请求到达大模型服务端模型判断“用户需要查询订单这应该调用queryOrderByNo工具参数orderNo是20240513001”。第四步模型返回的结果不是最终答案而是一个tool_calls结构里面包含了工具名和参数JSON。第五步Spring AI解析tool_calls通过代理机制调用本地方法也就是执行你自己的queryOrderByNo代码。第六步方法返回OrderInfo对象框架把它序列化成一个工具结果消息。第七步这个结果消息再次发给大模型模型看到“订单已发货”这样的工具返回值。第八步模型用自然语言组织最终回答返回给Controller。这八步里第二步到第七步对开发者来说都是透明的Spring AI全部封在框架内部。我建议你在学习阶段把日志级别调到DEBUG实际看一下框架发送出去的请求体和模型的返回体对理解原理帮助极大。4.2 工具定义和JSON Schema的关系当年我刚开始用Function Calling的时候一直好奇Spring AI是怎么把我Java方法的签名变成模型能看懂的JSON结构。后来抓包看到请求体一下就明白了。你的方法签名经过框架处理会变成类似这样的函数定义{ type: function, function: { name: queryOrderByNo, description: 根据订单号查询订单信息返回订单状态、金额、商品名称, parameters: { type: object, properties: { orderNo: { type: string, description: 订单号 } }, required: [orderNo] } } }这个JSON Schema是模型决定“怎么调用”的唯一依据。方法名、参数名、参数类型、描述信息全都映射到schema里。所以参数命名必须见名知义我见过有人把订单号参数写成a模型死活填不对参数改成orderNo之后一次就通了。4.3 返回类型的序列化问题Tool方法的返回类型也会被序列化后发给模型。返回一个record模型看到的就是一个清晰的JSON对象比如{ orderNo: 20240513001, status: SHIPPED, amount: 299.00, productName: 无线机械键盘 }模型就是根据这段JSON来组织最终回答的。字段名清晰模型总结得就准确。如果字段名是缩写或者无意义的字符模型可能理解不了回答就会牛头不对马嘴。这也是我一直强调返回结构要设计清楚的原因。5. 高频报错与排查实录400、模型名、工具不生效工具调用这个功能跑通不难但生产环境里各种问题都有。我把实际项目里遇到的高频问题整理成速查按频率排序。5.1 api error: 400 invalid schema for function这个报错在热词里出现了估计困扰了不少人。核心信息是invalid schema也就是说你传给模型的函数定义没有通过模型服务端的校验。我遇到过的触发原因主要有四类。第一Tool方法的参数用了Map或者Object这种无法推断类型的声明Spring AI没法生成明确的properties结构直接导致schema非法。第二返回对象存在循环引用Jackson序列化时卡死或者生成递归结构。第三参数对象上用了Pattern这类校验注解里面的正则表达式格式不合法或者包含了服务端schema校验器不支持的转义字符。第四返回类型用了匿名内部类或者lambda表达式生成的类型类名里带着$符号schema生成阶段直接异常。排查思路也别绕弯子先把返回类型换成String参数换成基本类型跑通最小链路然后逐步把字段加回来定位到具体是哪一层导致schema生成失败。5.2 模型名称配置错误“The supported api model names are...”这类的报错基本就是model name写错了。不同平台的模型名差异很大DeepSeek官方是deepseek-chat和deepseek-reasonerOpenAI是类似gpt-4o-mini这样的命名通义百炼则是qwen-plus、qwen-max。如果你的服务方是第三方网关或者内部平台模型名可能被重新定义过这时候千万别拿网上教程里的模型名硬套直接查你所用平台的最新文档。5.3 模型死活不调用工具回答还瞎编这个问题的排查顺位我建议先从prompt入手。系统提示词里如果没有强调“必须使用工具获取信息”模型很大概率会直接用自己的知识库回答。比如你让它查订单它可能一本正经告诉你“请确认您的订单号后重试”实际上根本没去查你的数据库。其次是工具描述写得含糊模型不知道这个工具和用户问题的对应关系。最后检查temperature太高了模型会发散建议工具调用场景控制在0.1到0.3之间。我用过的一段比较有效的系统提示词模板是你是XX业务助手。涉及XX信息时必须调用对应工具获取真实数据。如果工具没有返回结果直接告诉用户无法查询严禁根据常识猜想或编造。这段提示词在多个项目里实测下来工具调用率提升明显。5.4 Spring AI能不能替代Python写代码这个疑问在热词里出现频率不低。我的看法很明确Spring AI不是要替代Python它是Java技术栈接入大模型的最优解。如果你的业务本身就是Spring Boot写的为了接一个大模型去额外搞一套Python服务维护两套系统的成本太高了。直接在现有工程里加依赖把已有的Service方法暴露成工具这是最平滑的路径。反过来如果你要做的是数据分析、模型微调这类Python生态优势明显的任务那就没必要硬换Java。工具选型跟着技术栈走跟着团队能力走别为了“跟风AI”牺牲工程效率。6. 项目经验总结与后续扩展建议项目落地之后我复盘了整个开发过程有一些经验值得单独拎出来说。6.1 我在实操中最看重的几个细节API密钥的管理是第一优先级。本地开发的时候用环境变量注入部署到测试环境就上配置中心无论如何不要把key提交到代码仓库。我见过不止一次因为key泄露被刷爆账单的案例。其次是日志的重要性。把每次请求的tool_calls调用记录打到日志里哪个工具被调用了、参数是什么、返回是什么这些信息在排查问题的时候价值极高。Spring AI本身有trace级别的日志建议至少保留一条工具调用的关键链路日志。再就是兜底设计。大模型接口随时可能超时、限流、返回异常你的业务接口调用层必须有超时控制和重试机制。工具调用链路里模型是不可控的一环你的代码必须有足够的健壮性来应对这种不可控。6.2 接下来可以扩展的方向订单助手只是一个起点顺着这个思路可以做企业内部的人工智能助手门户把IT工单、审批流程、知识库查询全部接入统一通过自然语言入口对外服务。如果你的场景是几十个工具以上的规模工具调用准确率会明显下降。这时你可以考虑给历史对话加入few-shot示例也就是在系统提示词里给出几组“用户问题-正确工具”的示例让模型照着例子学。再不行的话工具描述需要做A/B测试看看哪种描述风格能提升调用准确率。还有一个实用的扩展思路工具调用记录本身就是很好的数据资产。哪些工具被频繁调用哪些描述导致模型选错工具这些都是可以反馈到模型优化中的宝贵数据。真到了需要微调模型的阶段这些积累的调用数据就是最有价值的训练语料。最后再分享一个我个人的习惯每次新接入一个Tool方法我都会先做一个单独的联调测试输入对应的自然语言问题看模型是否准确命中工具、参数是否完整。跑通了再合入主代码。这样能把问题掐死在最早环节而不是等到整个系统联调时再面对一堆不确定的故障。