☰
Spring AI 实战:yml 配置与 ChatClient 链式调用接入大模型
2026/10/14 6:51:52 网站建设 项目流程

1. 从配置文件到对话窗口:Spring AI 到底解决了什么问题

第一次看到 Spring AI 这个模块的时候,我的反应是:Java 生态终于有人把大模型调用这件事做成"框架级"的东西了。过去一年里,我帮好几个团队做过大模型接入,每次都是同样的剧本——有人写个HttpClient工具类,把 API Key 硬编码在常量里,请求体用字符串拼接,返回结果用JSONObject一层层剥。等到要换模型、要加流式输出、要挂对话记忆的时候,整个工具类就变成了一坨谁都不敢动的泥巴。

Spring AI 的出现,本质上是把"调用大模型"这件事从"写工具类"拉到了"用框架"的层面。它的核心思路和 Spring 一贯的风格一致:约定优于配置,配置优于硬编码。你不需要关心 HTTP 请求怎么拼、鉴权头怎么加、响应怎么解析,这些都被封装在ChatClient和底层的ChatModel抽象里。你要做的,是在application.yml里写几行配置,然后通过链式调用把提示词、参数、上下文串起来。

这个项目标题里提到的"配好 yml 就能聊",说的就是这件事。Spring AI 通过spring.ai.openai这类配置前缀,把模型地址、API Key、默认模型名、温度参数全部外置到配置文件。你换一个模型供应商,改的是 yml,不是 Java 代码。而"ChatClient 四步链式调用"指的是它提供的 Fluent API——prompt()起手,中间挂user()、system()、options(),最后call()或stream()收尾。这套 API 设计明显借鉴了 WebClient 和 RestClient 的风格,写过 Spring 的人上手几乎没有成本。

这篇文章适合两类人看。一类是 Java 后端开发者,手里有 Spring Boot 项目,想快速接一个大模型能力进去,但不想引入 Python 那一套技术栈;另一类是做企业级应用的工程师,需要把大模型调用做成可配置、可替换、可测试的模块,而不是散落在业务代码里的临时脚本。我会从配置结构讲到链式调用的每一步,再补充参数调优、流式输出、对话记忆这些实际项目里绕不开的细节,最后把我踩过的坑整理成排查表。

需要提前说明的是,Spring AI 目前仍在快速迭代,不同小版本之间 API 有调整。我下面讲的内容基于当前主流的稳定版本实践,如果你用的是更早的里程碑版本,部分类名和方法签名可能对不上,以你项目实际依赖的版本为准。

2. 配置先行:yml 里到底该写什么,为什么这么写

2.1 最小可用配置的结构拆解

很多人第一次配 Spring AI 的时候,直接抄了一段配置进去,跑起来报错,然后开始怀疑人生。问题往往出在没搞清楚配置的层级关系。Spring AI 的配置是按"模型供应商"分组的,OpenAI 相关的配置全部挂在spring.ai.openai下面,而不是散在spring.ai根节点。

一个最小可用的配置大概长这样:

spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://api.example.com chat: options: model: gpt-4o-mini temperature: 0.7

这里有几个点值得展开说。api-key我强烈建议用环境变量占位符,而不是直接写明文。原因很简单——配置文件是会进 Git 仓库的,一旦密钥泄露,轻则被人盗刷额度,重则产生真实账单。用${AI_API_KEY}这种写法,本地开发时在 IDE 的运行配置里注入,线上部署时通过环境变量或配置中心下发,密钥永远不落盘。

base-url这个配置项是很多人忽略的。它的作用是覆盖默认的请求地址,让你可以把请求指向自己的网关或中转服务。在企业环境里,这个配置几乎是必须的,因为出口流量通常要走统一的网关做审计和限流。如果你用的是官方默认地址,这一项可以不写,框架会用内置的默认值。

chat.options下面的model和temperature是聊天场景的核心参数。model决定用哪个模型,temperature决定输出的随机性。这里要注意,options是可以分层配置的——你可以在spring.ai.openai.chat.options里写全局默认值,也可以在代码里针对单次请求覆盖。这个设计很实用,比如你的应用大部分场景用低温度做严谨问答,但某个创意生成功能需要高温度,就可以在调用时单独指定。

2.2 为什么参数要放在 yml 而不是代码里

我见过不少项目把模型名、温度值直接写在 Java 代码里,理由是"反正也不怎么改"。这种做法的代价在第一次需要调整时就会显现——改一个参数要重新编译、打包、发版,而实际上这只是个配置变更。

把参数外置到 yml 的好处至少有三层。第一层是环境隔离:开发环境用便宜的小模型,生产环境用能力强的大模型,同一套代码,不同的配置文件,切换零成本。第二层是快速调优:温度、最大 token 数这些参数需要反复试,放在配置文件里改完重启即可,不用走完整的发版流程。第三层是可观测性:所有影响模型行为的参数集中在一处,排查问题时一眼就能看到当前生效的配置是什么,不用在代码里到处翻。

提示:temperature的取值范围通常是 0 到 2,但实际使用中建议控制在 0 到 1 之间。超过 1 之后输出会变得非常发散,容易出现答非所问的情况。做事实性问答用 0 到 0.3,做创意写作用 0.7 到 1.0,这个区间划分是我实测下来比较稳的经验值。

2.3 多模型配置与切换策略

实际项目里很少只用一个模型。常见的情况是:简单任务用便宜的小模型,复杂推理用贵的大模型,或者主模型不可用时要有备用模型。Spring AI 支持在配置里定义多个模型配置,然后通过ChatModel的不同实例来区分。

一种实用的做法是利用 Spring 的 Profile 机制。你可以准备application-dev.yml、application-prod.yml等多套配置,每套里指定不同的模型和参数。启动时通过--spring.profiles.active=prod来激活对应配置。这样开发同学本地跑的时候不会误用生产的高价模型,生产环境也不会因为配置错误跑到测试地址上去。

另一种做法是在同一个配置文件里定义多个供应商的配置块,比如同时配好 OpenAI 和另一个兼容接口的供应商,然后在代码里根据业务场景注入不同的ChatModel。这种方式的灵活性更高,但需要你对 Spring 的 Bean 注入机制比较熟悉,知道怎么通过@Qualifier来区分同类型的多个 Bean。

3. ChatClient 四步链式调用:每一步在做什么

3.1 第一步 prompt():会话的起点

ChatClient的链式调用从prompt()开始。这个方法返回的是一个ChatClientRequestSpec,你可以把它理解成"一次对话请求的构建器"。它本身不发起任何网络请求,只是收集你接下来要设置的各项参数。

为什么要有这么一个起手方法?因为一次完整的对话请求包含多个维度的信息——系统提示词、用户输入、对话历史、模型参数、工具定义等等。如果把这些都塞进一个方法的参数列表里,签名会长到没法看。用链式调用的方式,每个维度一个方法,按需调用,代码可读性会好很多。

prompt()本身不接受参数,它只是一个入口。真正的内容填充在后面的步骤里。这个设计模式和StringBuilder、StreamAPI 是一个思路——先拿到构建器,再逐步添加内容,最后触发执行。

3.2 第二步 user() 与 system():角色分离的意义

user()方法设置的是用户侧的消息,也就是你实际想问的问题。system()方法设置的是系统提示词,用来定义模型的角色、行为边界和输出格式要求。

这两个方法分开,对应的是大模型对话协议里的角色概念。系统提示词的优先级高于用户输入,模型会把它当作"设定"来遵循。举个例子,你在system()里写"你是一个只回答技术问题的助手,遇到非技术问题直接回复'这个问题我无法回答'",那么即使用户在user()里问天气,模型也会按系统提示词的约束来回应。

实际项目里,系统提示词往往是产品质量的关键。我见过同一个模型,系统提示词写得粗糙和写得精细,输出质量差距能到天壤之别。写系统提示词有几个经验:明确角色定位,说清楚输出格式,给出边界条件,必要时给一两个示例。这些内容放在system()里,比混在用户问题里效果稳定得多。

String answer = chatClient.prompt() .system("你是一名严谨的技术文档助手,回答只基于事实,不确定的内容要明确说明。") .user("解释一下什么是链式调用") .call() .content();

上面这段代码展示了最基本的两次内容设置。注意system()和user()的调用顺序不影响结果,框架内部会按角色正确组装消息列表。

3.3 第三步 options():单次请求的参数覆盖

options()方法允许你针对当前这次请求覆盖 yml 里的默认参数。比如全局配置温度是 0.7,但这次要做严谨的事实核查,就可以在链式调用里把温度降到 0.1。

ChatResponse response = chatClient.prompt() .user("把这段话翻译成英文") .options(ChatOptions.builder() .model("gpt-4o") .temperature(0.2) .maxTokens(500) .build()) .call() .chatResponse();

这里maxTokens控制的是输出的最大长度。设置这个值有两个作用:一是控制成本,输出越长消耗的 token 越多;二是防止模型跑偏,有些模型在开放式的提示下会一直生成下去,设个上限能兜底。

注意:maxTokens设得太小会导致回答被截断,出现"话说一半"的情况。如果你不确定该设多少,可以先不设,观察几次实际输出的长度,再定一个略高于平均值的数。

3.4 第四步 call() 与 stream():两种消费方式

链式调用的最后一步是触发执行。call()是同步阻塞方式,等模型完整生成后一次性返回结果。stream()是流式方式,返回一个Flux<String>,模型每生成一小段就推送给客户端。

选择哪种方式取决于你的交互场景。如果是后台批处理任务,比如批量给文章生成摘要,用call()就够了,代码简单,逻辑清晰。如果是面向用户的对话界面,强烈建议用stream(),因为大模型生成一段完整回答可能需要好几秒,同步等待会让用户觉得界面卡死了,而流式输出能让用户看到文字一个个蹦出来,体验好很多。

Flux<String> stream = chatClient.prompt() .user("写一段关于春天的散文") .stream() .content(); stream.subscribe(chunk -> System.out.print(chunk));

流式输出的实现依赖底层模型接口的支持。目前主流的模型供应商都提供流式接口,Spring AI 在框架层面做了统一封装,你切换供应商时这部分代码基本不用改。

4. 对话记忆与上下文管理:让模型记住前面说过的话

4.1 为什么默认情况下模型"没有记忆"

大模型的 API 调用本质上是无状态的。每次你发一个请求过去,模型看到的只是这一次请求里包含的消息列表,它不知道上一次请求说了什么。这就是为什么你连续问两个相关的问题,第二个问题的回答可能完全不接第一个的茬。

要让模型"记住"上下文,做法是在每次请求时把之前的对话历史一起发过去。消息列表里包含多轮 user 和 assistant 的交替内容,模型就能基于完整的历史来生成回复。这个机制叫对话记忆,实现方式有几种,复杂度各不相同。

4.2 基于内存的对话记忆实现

最简单的方式是用 Spring AI 提供的ChatMemory抽象。它会在内存里维护一个消息列表,每次调用时自动把历史消息拼进去。

ChatMemory memory = MessageWindowChatMemory.builder() .maxMessages(20) .build(); ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(memory)) .build();

这里的maxMessages控制保留多少条历史消息。为什么不无限保留?因为模型的上下文窗口是有限的,消息越多消耗的 token 越多,成本越高,而且过长的上下文反而会稀释模型对近期内容的注意力。保留最近 20 条是个比较常见的折中值,具体设多少要看你的对话轮次和单条消息的长度。

MessageChatMemoryAdvisor是一个拦截器,它会在请求发出前把记忆里的消息注入进去,在响应返回后把新的对话追加到记忆里。这种 Advisor 机制是 Spring AI 的一个亮点,它让你可以在不改动业务代码的情况下,给所有对话统一挂上记忆能力。

4.3 记忆的持久化与多用户隔离

内存记忆的问题很明显——应用重启就没了,而且多个用户之间会串。生产环境里通常需要把记忆持久化到数据库或缓存里,并且按用户 ID 或会话 ID 做隔离。

实现思路是自定义一个ChatMemory的实现类,把消息的读写指向你的存储层。Spring AI 的接口设计允许你替换默认实现,只要实现add、get、clear这几个核心方法即可。存储选型上,Redis 适合做短期会话记忆,关系型数据库适合做需要长期保留的对话记录。

多用户隔离的关键是给每个会话一个唯一的conversationId,在读写记忆时带上这个 ID 作为分区键。Spring AI 的 Advisor 支持通过ChatClient的请求参数传递会话标识,你可以在调用时指定,框架会把它透传给记忆实现。

提示:做多用户隔离时,一定要在服务端校验会话 ID 的归属,不能直接信任客户端传来的 ID。否则 A 用户构造一个 B 用户的会话 ID,就能读到别人的对话历史,这是很典型的安全漏洞。

5. 常见问题与排查技巧实录

5.1 启动就报错的几类原因

配置类问题占了新手报错的一大半。下面这张表是我整理的高频问题速查:

报错现象可能原因排查方向
启动时提示找不到 API Key环境变量未注入或名称拼写错误检查${}占位符名称与运行环境变量是否一致
请求返回 401密钥无效或已过期确认密钥有效性,检查是否有前后空格
请求返回 404base-url 配置错误确认地址是否包含正确的路径前缀
连接超时网络不通或地址不可达用 curl 测试地址连通性
返回内容为空maxTokens 设得过小或模型名错误检查模型名拼写,适当调大 maxTokens

5.2 流式输出不生效的排查

流式输出不生效,最常见的原因是中间有缓冲层把数据攒住了。比如你用了某个网关或反向代理,它默认会等响应完整再转发。这种情况下,需要在代理层开启流式转发支持,关闭响应缓冲。

另一个原因是客户端消费方式不对。如果你拿到Flux之后调用了collectList()再处理,那就等于把流式变成了同步等待,失去了流式的意义。正确的做法是订阅Flux,在onNext回调里逐块处理。

还有一种情况是模型本身不支持流式,或者你调用的接口路径不对。有些兼容接口的流式和非流式走的是不同路径,配置时需要确认清楚。

5.3 对话记忆导致的内存增长问题

用内存记忆做长会话时,如果不设上限,消息列表会一直增长,最终可能导致内存溢出。MessageWindowChatMemory的maxMessages就是用来兜底的,但要注意它限制的是消息条数,不是 token 数。如果单条消息特别长,20 条也可能撑爆上下文窗口。

更稳妥的做法是在记忆实现里同时做条数限制和 token 估算限制。token 估算不需要特别精确,按字符数除以一个经验系数粗略计算即可,目的是防止极端情况。我一般会在记忆写入前做一次检查,超过阈值就丢弃最早的消息。

5.4 参数调优的实操心得

温度这个参数,我的经验是不要迷信网上的推荐值,要针对你的具体任务实测。同一个温度值,在不同模型上的表现可能差异很大。做事实问答时,我会从 0 开始试,如果发现回答太死板、缺乏必要的灵活性,再往上加 0.1。做创意任务时,从 0.8 开始试,如果发现跑题严重就往下调。

maxTokens的设置要结合你的业务场景。如果是生成摘要,先统计一批真实摘要的长度分布,取 95 分位数再上浮 20% 作为上限。如果是开放式对话,可以设得宽松一些,但一定要设,不能留空。

还有一个容易被忽略的参数是超时时间。大模型生成慢的时候,单次请求可能超过 30 秒。如果你的 HTTP 客户端默认超时是 10 秒,就会频繁报超时错误。这个超时时间需要在底层客户端或框架配置里单独调整,不能沿用普通接口的默认值。

6. 从能跑到好用:几个提升工程质量的做法

6.1 把提示词模板化管理

系统提示词和用户提示词模板如果散落在代码各处,维护起来会很痛苦。我的做法是把它们抽到独立的资源文件里,用模板引擎做变量替换。Spring AI 本身支持PromptTemplate,你可以把带占位符的模板定义在文件里,运行时填充变量。

这样做的好处是提示词的修改不需要动 Java 代码,产品经理或运营同学经过简单培训也能参与调整。同时,模板集中管理后,做 A/B 测试也方便,换一个模板文件就能对比效果。

6.2 给模型调用加上可观测性

生产环境里,你需要知道每次调用的耗时、消耗的 token 数、成功失败情况。这些数据是成本核算和性能优化的基础。Spring AI 提供了调用链路的拦截点,你可以通过自定义 Advisor 或监听器来采集这些指标。

我通常会在 Advisor 里记录请求开始时间、结束时间、输入输出的大致长度,然后打到日志或监控系统里。有了这些数据,你才能回答"这个月模型调用花了多少钱""哪个接口最慢""失败率是多少"这类问题。

6.3 降级与重试策略

模型服务不是百分之百可用的,网络抖动、服务限流、临时故障都会导致调用失败。在关键业务路径上,需要有降级方案。最简单的降级是重试,但要注意重试次数不能太多,否则会放大故障。一般重试一到两次,配合指数退避的间隔。

如果重试后仍然失败,要有兜底逻辑。比如返回一个缓存的默认回答,或者提示用户稍后再试,而不是直接把异常抛到前端。对于非关键路径,可以考虑降级到更小更快的模型,牺牲一点质量换取可用性。

6.4 成本控制的几个抓手

模型调用的成本主要取决于输入输出 token 总量和模型单价。控制成本的手段有几个:一是选对模型,简单任务不要用最贵的;二是精简提示词,去掉不必要的冗余描述;三是控制上下文长度,记忆不要无限增长;四是设置合理的 maxTokens,避免生成过长的无用内容。

我还会定期分析调用日志,找出那些消耗高但价值低的调用,针对性优化。比如有些接口的提示词里塞了大量固定说明,其实可以精简;有些场景的上下文带了很多无关历史,其实可以裁剪。

7. 我对这套技术栈的实际体会

用 Spring AI 做项目接入,最大的感受是"省心"。以前每次接新模型都要重写一遍 HTTP 调用逻辑,现在换供应商基本只改配置。ChatClient 的链式 API 写起来也顺手,代码读起来像自然语言,review 的时候一眼能看懂在干什么。

但它也不是银弹。框架封装得越深,出问题时排查的链路就越长。有一次流式输出不工作,我从业务代码一路查到框架源码,最后发现是底层 HTTP 客户端的一个配置项没开。这种时候,理解框架的分层结构就很重要,知道每一层负责什么,才能快速定位问题出在哪一层。

另外,Spring AI 的版本迭代比较快,升级时要注意看变更日志。有些方法签名会调整,有些配置项会改名。我的建议是锁定一个稳定版本,不要盲目追新,等版本稳定了再升级。

最后分享一个小技巧:调试提示词的时候,我会先把temperature设成 0,这样同样的输入每次输出基本一致,方便对比不同提示词的效果。等提示词调好了,再根据实际需要调整温度。这个习惯帮我省了很多"这次怎么和上次不一样"的困惑。

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

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

立即咨询