资讯动态

Spring AI Alibaba实战:ChatModel让Java后端轻松接入大模型对话

发布时间:2026/10/18 1:52:17 来源:尧图企业网站定制
上周有个朋友问我Java 后端想接大模型除了自己拼 HTTP 请求还有没有更体面的写法。我的第一反应是 Spring AI Alibaba——这个框架已经把跟通义千问这类模型的对话封装成了非常“Spring”的方式核心入口就是 ChatModel。你用 Spring Boot 的方式声明一个接口、注入一个 Bean就能把大模型聊天能力接进业务系统不用自己处理鉴权、重试、JSON 结构拼接这些脏活。这篇文章是我最近用 Spring AI Alibaba 做 ChatModel 相关功能时沉淀下来的一条完整记录从依赖配置、首次调用到多轮会话、流式输出、函数调用再到参数调优和踩坑排查适合刚接触 Spring AI、又想快速在 Java 项目里落地大模型对话能力的开发者。我默认你是已经会 Spring Boot 的 Java 开发者但未必熟悉 Spring AI 的抽象。下面的代码我不追求面面俱到只挑那些“真正决定你能不能跑通”的细节讲。每个部分我都会说明为什么这样做以及我实际踩过的坑。1. 从“自己拼 HTTP”到“被 Spring 托管”ChatModel 解决了什么问题1.1 为什么我在 Spring 生态里找聊天方案先说背景。之前我在某个内部管理系统的需求里要做一个“智能助手”入口核心功能就是用户发一段话系统返回一段模型生成的内容。听起来简单但真拿 HTTP 直接调大模型接口时你会发现自己要处理的事情比想象中多得多请求体要按照模型要求的格式组装流式响应要处理事件流鉴权信息不能写死还要考虑超时、重试、并发。更麻烦的是这套代码和具体的模型厂商绑死了今天用通义千问明天想换其他模型几乎等于重写一版。Spring AI Alibaba 让我改观的核心点是它把“和大模型对话”这件事抽象成了一个 Java 接口。这个接口不关心你在背后用的是哪家模型也不关心你是同步等结果还是异步收流式数据。业务代码只需要依赖这个抽象剩下的事情交给框架去适配。对于我们这些常年写 Spring 的人来说这种方式天然具备亲和力——依赖注入、自动配置、统一配置项全是熟悉的味道。1.2 ChatModel 接口以及它背后的核心抽象在 Spring AI 的世界里ChatModel 是聊天模型的核心接口。你可以把它理解成一个“会说话的方法”你传入一段文本它返回一段文本。但大模型对话没那么简单因为它天然是“多消息”的有系统设定、用户输入、模型回复、历史上下文所以调用时你传递的不是一个字符串而是一个 Prompt 对象。Prompt 内部其实是一个消息列表。Spring AI 里常见的消息类型有三种SystemMessage 用来设定人设或规则UserMessage 是用户输入AssistantMessage 是模型的回复。ChatModel 的入参是 Prompt出参是 ChatResponseChatResponse 里面又包含生成结果和相关的元数据。如果你需要精细控制就去操作这个消息列表如果你只是快速验证ChatModel 也提供了直接传 String 的简单方法。Spring AI Alibaba 在这个抽象之上接入了阿里云的大模型服务目前常用的有 qwen-plus、qwen-max、qwen-turbo 这些通义千问系模型。也就是说你不需要自己去实现某个 Client只需要引入对应的 starter配置好密钥Spring 容器里就会自动出现一个可用的 ChatModel Bean。2. 搭一个能跑的 ChatModel 项目依赖、配置与第一次调用2.1 最小依赖清单如果你的项目是 Maven 构建引入 Spring AI Alibaba 的方式很简单。核心依赖就是这个它会自动把 Spring AI 的核心抽象和阿里云模型适配都带进来dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version你选择的版本/version /dependency版本号我这里不写具体数字因为这类框架迭代快不同版本之间的配置项可能略有差异。一个比较稳妥的做法是去 Spring Initializr 或仓库页面查对应的版本选择跟你 Spring Boot 主版本兼容的版本。我建议优先选较新的稳定版因为早期版本里 ChatModel 的 API 有过调整网上很多旧教程代码直接粘到新版本会编译不过。2.2 配置项逐个说清api-key 别写死在配置里依赖引入后最关键的配置就是两个API 密钥和模型名。以通义千问为例配置如下spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus这里有个很重要的习惯API 密钥不要直接写在 application.yml 里。一方面代码仓库会被很多人看到一旦提交有泄密风险另一方面密钥会经常轮换写在配置里就意味着每次轮换都要改文件、重启。我建议用环境变量或者配置中心管理${DASHSCOPE_API_KEY}这种占位符方式就是读写环境变量。你可以把变量配置在 IDEA 的运行配置里也可以写在系统的环境变量里反正别硬编码。除了这两个核心配置项chat.options 下面还可以配置 temperature、top_p、max_tokens 等模型生成参数。这些参数我后面单开一节细讲因为它们对输出质量的影响方式很微妙并不像很多人想象的那样“调大调小就行”。2.3 第一条会“说话”的代码配置完成后最快的验证方式就是写一个接口注入 ChatModel直接调用。Spring 容器会在项目启动时自动装配好 ChatModel 的 Bean你只需要在构造函数里声明它RestController public class ChatController { private final ChatModel chatModel; public ChatController(ChatModel chatModel) { this.chatModel chatModel; } GetMapping(/chat) public String chat(RequestParam String message) { return chatModel.call(message); } }启动项目访问/chat?message你好如果配置没问题你会看到模型返回的文本。这里有两个值得注意的观察点。第一个是首次调用的耗时模型接口响应通常是秒级而不是毫秒级这很正常和本地方法调用完全是两码事。第二个是返回值的形态如果你只调用chatModel.call(message)得到的就只有纯文本内容但如果你需要拿到 token 消耗、结束原因、模型信息这些元数据就要用call(new Prompt(message))的方式去拿完整的 ChatResponse 对象。我第一次跑通这段代码时最大的感受是“这也太普通了”。没有手写 HTTP 客户端没有 JSON 序列化没有鉴权逻辑就像一个普通的 Service 方法。但恰恰是这个“普通”背后框架帮你吞掉了所有和具体模型厂商相关的复杂度这就是抽象的价值。3. 让对话“记得住”多轮会话背后的消息模型与上下文管理3.1 为什么第二次调用会“失忆”很多人第一次写完基础调用后会立刻遇到一个困惑我连问两句“我叫小明”“我叫什么名字”模型第二句居然答不上来。原因很简单每一次chatModel.call都是一次独立的请求模型本身不保存任何状态。你上一次问过什么它根本不知道。这跟你和朋友聊天完全不同大模型端到端是无状态的所谓的“记忆”完全靠调用方把历史消息一起发过去。这个设计其实很合理。无状态意味着模型服务不需要维护每个用户的长连接上下文弹性扩展和并发处理都简单得多。代价就是把上下文管理的责任交给了应用层。所以你要做的是自己维护一个消息列表每次请求时把历史消息和当前问题一起打包成 Prompt 发送给模型。3.2 用消息列表还原多轮对话多轮对话的代码模式非常固定。先准备一个消息列表把系统设定放进去然后把用户消息和模型回执交替追加ListMessage messages new ArrayList(); messages.add(new SystemMessage(你是一个简洁的AI助手回答不超过50个字)); // 用户第一轮提问 messages.add(new UserMessage(我叫小明)); Prompt prompt1 new Prompt(messages); ChatResponse response1 chatModel.call(prompt1); AssistantMessage assistantMessage1 (AssistantMessage) response1.getResult().getOutput(); messages.add(assistantMessage1); // 用户第二轮提问 messages.add(new UserMessage(我叫什么名字)); Prompt prompt2 new Prompt(messages); ChatResponse response2 chatModel.call(prompt2); // 此时模型能根据历史消息知道用户叫小明这里有一个容易被忽略的细节模型回复的消息对象必须原封不动放回消息列表里而不是把文本内容重新包一个 AssistantMessage 塞进去。原因在于消息中可能带有模型生成时附带的元信息比如拒绝原因、函数调用标记等。你虽然暂时用不到但把这些完整信息传回去模型服务才能正确处理上下文。我见过有人图省事只把getText()存起来再包一层结果多轮对话偶尔出现逻辑混乱就是这个原因。3.3 上下文长度控制不要无脑塞历史多轮对话能跑通之后下一个问题就是上下文长度。模型输入有 token 上限qwen-plus 这类模型虽然支持较长的上下文但你把累计上万字的聊天记录每次都原封不动发过去第一个到达的就是 token 限制第二个是费用上升第三个是模型响应变慢、注意力被无关历史干扰。我目前用的策略有两种可以组合使用。第一种是滑动窗口裁剪只保留最近 N 轮对话比如最近 10 轮更早的直接丢弃。实现起来就是维护消息列表时超过阈值就从头部移除。第二种是摘要压缩如果业务要求保留长期记忆就写一个额外的摘要逻辑把超出窗口部分交给模型总结成几个要点再作为 SystemMessage 放进去。这两种方式都不复杂但一定要在业务层面提前想清楚否则等线上聊天记录真的多起来再改造成本会高出不少。4. 两种生产级用法流式输出与函数调用4.1 流式输出Flux 带来的“打字机”体验如果你做的功能是聊天助手同步等待模型返回再一次性展示体验会很差。模型生成几百字往往需要好几秒用户盯着空白界面会以为系统挂了。这时候需要流式输出。Spring AI 对流式的支持很优雅ChatClient 提供了 stream 方法返回的是一个响应式流对象ChatClient chatClient ChatClient.builder(chatModel).build(); FluxString content chatClient.prompt() .user(用三句话介绍杭州) .stream() .content(); content.subscribe(text - System.out.print(text));这里的FluxString是响应式编程里的概念你可以把它理解成一个异步的、会持续产生多个值的管道。模型每生成一段文本管道里就会流出一个片段你把它实时推给前端用户看到的效果就是文字一个一个字地蹦出来。如果你的项目用的是 WebFlux甚至可以直接把FluxString作为接口的返回类型框架会自动按流式数据输出到 HTTP 响应前端用 EventSource 或流式 fetch 就能接收。我实际用下来流式输出遇到的坑主要不在框架本身而在“要不要等待结束”。有些场景比如让模型生成一段 JSON 然后直接解析是不能用流式的因为你需要拿到完整文本才能解析。所以我的经验是纯聊天、面向人的输出用流式机器解析、固定格式输出用同步。两者各司其职不要混用。4.2 函数调用让 ChatModel 学会调用你的 Spring BeanChatModel 真正让我觉得“这值得学”的地方是函数调用。简单说你可以把一些业务方法暴露给模型让模型在回答过程中判断“为了完成用户请求我需要调用某个函数”然后框架会替你完成调用并把调用结果交给模型继续生成最终回复。举个例子。假设 ChatGPT 场景之外你还想让助手查一下天气。模型本身不知道实时天气但它可以调用你的天气查询方法。在 Spring AI Alibaba 里定义这种工具方法非常方便用一个注解标注成可调用函数就行Component public class WeatherTools { Tool(description 查询指定城市的当前天气) public String getWeather(String city) { // 这里往真实天气服务发请求或者读取模拟数据 return city 晴26摄氏度东南风3级; } }Spring 启动时会扫描这些带 Tool 注解的方法把它们的名称、参数、描述登记成“工具清单”下发给模型。当用户问“北京今天穿什么合适”模型会判断自己需要知道北京天气于是触发 getWeather 函数框架执行后把结果返回给模型模型再结合天气信息给出穿衣建议。整个过程对业务代码来说是透明的你要做的只是写好这个工具方法然后把这个 Bean 所在的包放进组件扫描路径即可。函数调用最关键的写作技巧是 description 要尽量写清楚。模型靠描述来决定“什么场景该调用这个函数”描述得越明确触发越准确。比如同一个城市参数你写“城市名”和写“中国城市的行政区划名称如北京、上海”效果差别很大。我在模拟项目里就是因为 description 写得太泛模型经常在无关问题里也去调天气白白浪费一次请求。5. 参数调优与输出稳定性temperature、top_p、max_tokens 的真相5.1 temperature 和 top_p不是随便填的数字配置里最常被人问到的两个参数是 temperature 和 top_p它们都控制模型的随机性。temperature 越低模型越倾向于选择高概率的 token输出更稳定、更保守temperature 越高输出更多样但也更可能跑偏。top_p 则是另一种采样方式它限制模型只在累计概率达到 p 的 token 集合里做选择同样是越接近 0 越保守。我整理了一个经验对照表方便你按场景快速选择场景temperaturetop_p说明代码生成、JSON 提取、分类任务0.1 - 0.30.1 - 0.5要求准确越保守越好知识问答、客服助手0.3 - 0.60.5 - 0.8平衡稳定性和自然度文案创作、头脑风暴0.7 - 1.00.8 - 1.0希望多样性和创意需要特别提醒的是temperature 和 top_p 一般建议只调其中一个不要两个同时大幅调整。同时调高两者对输出的影响是叠加的很容易让结果变得失控。这和大模型本身的特性有关你可以理解成两种不同的“随机性调节旋钮”同时拧到底输出不确定性自然急剧上升。5.2 max_tokens 的“截断陷阱”max_tokens 限制了模型在单次回复中最多生成的 token 数量。这里有个常见的误用场景你发现模型回答到一半突然断了以为是网络问题其实很可能是 max_tokens 设得太小回答触顶被截断。我曾经在做一个长文本总结功能时把 max_tokens 设成了 200结果模型每次只输出开头一段就停了。检查了半天才发现不是代码问题而是生成内容长度超过了上限。后来我把值调大并额外加了一个判断如果返回结果中指示“生成被截断”就在回复末尾提示用户内容过长或自动启动第二轮续写。token 数并不等于中文汉字数中文一个 token 大约能对应一个到两个汉字这个换算关系在预估容量时很有用。5.3 真正最影响质量的参数system prompt 反而更容易被忽视很多人调了半天 temperature 和 top_p输出质量还是不稳问题往往出在 system prompt 上。SystemMessage 是你在多轮对话开头设定的“人设与规则”它的约束力远比你想象的大。举个例子同一个任务如果你在 SystemMessage 里写“你是客服助手”模型输出会比较随意但如果你写“你是电商客服回答必须包含 1 个解决方案和 1 个安抚性表达总字数控制在 80 字以内禁止出现‘作为AI模型’之类的说法”输出质量会明显提升。系统提示词才是你最强大的调参工具模型参数只是辅助。我自己的习惯是先写一个足够明确的 system prompt明确角色、范围、格式、限制四条要素然后跑一组真实用例看输出再针对不合格的输出逐条把补充规则写回 system prompt。这比盲目改参数高效得多。6. 踩坑实录从配置无效到流式中断的完整排查链路6.1 配置项“不生效”的第一反应不应该是看代码有一次我在项目里调整 temperature改完之后连着测了几次发现输出风格一点变化都没有。一开始我怀疑是参数名拼错了查了官方文档核对无误后来怀疑是版本兼容问题又去翻 starter 源码。折腾了一个多小时最后发现原因极其低级我改的是 application.yml但项目启动时用的 profile 是 application-dev.yml后者里同样有一份配置把前者覆盖了。这件事给我一个很深的教训在 Spring 项目里排查配置不生效先确认加载的到底是哪个配置文件再看配置优先级。Spring Boot 的配置加载顺序本来就有很明确的规则参数文件、环境变量、命令行参数都可能覆盖 yml 内容。你辛辛苦苦改的配置被某个不起眼的地方覆盖掉是极常见的坑。6.2 输出内容出现乱码或异常符号的排查路径另一个让我印象深刻的坑是流式输出内容出现乱码。现象是控制台输出的中文每隔几个字就出现一个黑色方块像是编码错乱。我最初怀疑是模型输出问题了结果查了一圈发现是本地 IDE 控制台的编码和流式输出编码不一致。我用的终端区域语言设置是 UTF-8但控制台默认编码是 GBK 之类的导致打印时出现乱码。处理方式很简单把 IDE 控制台的编码改成 UTF-8或者在发送请求时确认 HTTP 客户端默认使用 UTF-8 解码响应字节流。Spring AI 的 starter 通常默认按 UTF-8 处理但如果你在链路中手动介入过 HttpHeaders 或文件读写就要留意编码是否被改过。这类问题不在框架本身而在你项目运行环境和代码链路里。6.3 高并发调用下的超时以及流式输出中断真实上线后遇到的一类问题是并发超时和流式中断。我手里的一个模拟项目接口没有做并发控制结果几个同事同时测试很快就有人反馈请求超时。排查后发现默认的超时配置对 AI 接口来说太短了。大模型接口本身就是秒级响应某些复杂任务甚至要十几秒用普通 HTTP 接口的超时策略来约束它一定会出问题。解决方案是在配置里显式调大连接、读取相关的超时时间同时针对流式连接设置更加宽松的空闲超时。如果你用的是异步线程池还要注意线程池大小和任务队列长度。大模型接口的耗时比普通业务接口高一个数量级并发场景下线程池很容易被占满此时其它普通接口也会跟着变慢。这一点一定要提前压测不要等到上线后被流量教育。关于流式中断我遇到最多的情况是服务端返回了连接重置或连接被对端关闭。排查链路大概是先看是不是服务端主动断连模型侧超时或内部错误再看是不是反向代理或网关空闲超时把长连接给掐了。如果你在本地直连模型没问题但部署到服务器后流式经常断优先检查代理层对长连接、缓冲流的处理配置。这是分布式环境的通病Spring AI 本身反而没有问题。6.4 一个可复用的排查顺序经历这些坑之后我总结了一套自己的排查顺序分享出来供你参考。碰到 ChatModel 相关异常按这个顺序排查能省很多时间先看启动日志和配置加载情况确认你改的配置真的生效再看请求是否真正到达模型服务可以开启框架的日志输出或临时打印请求参数然后看响应结果和异常类型是超时、限流、认证失败还是内容截断最后才怀疑框架本身的 bug这种概率在成熟版本里其实很低。如果你把每一层都排查清楚还找不到原因一个很实用的办法是写一个最小复现 Demo只保留 starter 依赖、最简配置、一句调用代码跑一遍试试。很多问题在复杂项目里很难定位但在最小 Demo 里会立刻暴露要么是配置被覆盖要么是依赖版本冲突。这段学习过程走下来我最深的体会是Spring AI Alibaba 把“调用大模型”这件事收编进了 Spring 生态但真正决定上线质量的仍然是工程层面的细节——上下文怎么管理、参数怎么调、超时怎么设、函数描述怎么写。ChatModel 只是一个入口它背后是一整套需要认真对待的工程实践。如果你也准备在项目里接这类功能我建议先从一个最小可用的对话接口开始跑通后再逐步加流式、函数调用和上下文策略。别一上来就把所有能力都堆上否则出了问题你真的会不知道是该查配置、查代码还是查模型。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑