news 2026/10/14 6:51:52

Spring AI 实战:ChatClient 四步链式调用与 yml 配置详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 实战:ChatClient 四步链式调用与 yml 配置详解

Spring AI 这两年在 Java 圈子里讨论度一直不低,但真正动手跑通一个能对话的 Demo,很多人卡在第一步——配置文件怎么写、依赖怎么引、ChatClient 到底怎么串起来。我前阵子在一个模拟项目里完整走了一遍这套流程,从零到能对话大概花了不到半小时,核心代码加起来不超过二十行。这篇文章就把整个过程拆开讲清楚,包括 yml 里每个参数为什么这么配、ChatClient 的四步链式调用每一步在干什么、以及我踩过的几个坑。不管你是刚接触 Spring AI 的新手,还是已经用过其他 AI 框架想快速迁移的老手,应该都能从里面找到能直接抄的部分。

1. 项目整体设计与思路拆解

1.1 为什么选 Spring AI 而不是自己封装 HTTP 调用

很多人第一反应是:调个对话接口而已,用 RestTemplate 或者 WebClient 直接发 POST 不就行了,为什么要引入一个框架?我一开始也是这么想的,但实际写下来发现,自己封装至少要处理这几件事:请求体的 JSON 结构拼装、不同模型厂商接口协议的差异、流式响应的解析、对话上下文的维护、异常重试和超时控制。这些活儿单看每一件都不难,但加在一起就是几百行胶水代码,而且每换一个模型供应商就得改一遍。

Spring AI 的价值在于它把这些东西抽象成了统一的接口。ChatClient 负责对话交互,ChatModel 负责底层模型通信,Advisor 负责拦截和增强,你切换模型供应商的时候,大部分业务代码不用动,改一下依赖和 yml 配置就行。这跟当年 Spring 对 JDBC 做的抽象是一个思路——不是说你不能自己写,而是框架帮你把重复劳动吃掉了。

注意:Spring AI 目前仍在快速迭代中,不同小版本之间 API 有调整的可能。建议在 pom 里锁定具体版本号,不要用范围版本,否则某天重新构建可能直接编译不过。

1.2 整体架构:三层结构各管什么

我把这个 Demo 的结构拆成三层来理解,这样后面看代码的时候不容易迷路。

第一层是配置层,也就是 application.yml。这一层负责告诉 Spring AI 用哪个模型供应商、API 地址在哪、用哪个模型、温度参数多少、超时时间多长。这一层是纯声明式的,不写 Java 代码。

第二层是模型层,由 Spring AI 的 Starter 自动装配出 ChatModel 实例。你引入不同的 starter,它就装配不同的实现。比如引入某个云厂商的 starter,它装配的就是对应厂商的 ChatModel。这一层你基本不用管,Spring 容器启动时自动完成。

第三层是交互层,也就是 ChatClient。这是你写业务代码时直接打交道的对象。它提供了一套链式 API,让你用 fluent 风格构建请求、发送消息、拿到响应。四步链式调用说的就是这一层。

三层之间的关系是:配置层驱动模型层,模型层注入交互层。你改配置,模型层跟着变,但交互层的代码写法基本不变。这就是抽象带来的好处。

1.3 四步链式调用的设计哲学

ChatClient 的链式调用我概括为四步:创建实例 → 设置系统提示 → 传入用户消息 → 调用获取响应。这个顺序不是随便定的,它对应了一次对话请求的完整生命周期。

创建实例是确定"用哪个模型、带哪些默认行为";设置系统提示是确定"AI 扮演什么角色、遵守什么规则";传入用户消息是确定"这次问什么";调用获取响应是"真正发起请求并拿到结果"。每一步都是可选的(除了创建实例和调用),你可以只创建实例然后直接 call,也可以叠加多个提示词。

这种设计的好处是可读性强。你一眼就能看出这次对话的系统提示是什么、用户问了什么,不用去翻请求体构造代码。而且链式调用天然支持复用——你可以先创建一个配置好系统提示的 ChatClient 实例,然后在多个地方复用它,每次只换用户消息。

2. 核心细节解析与实操要点

2.1 yml 配置里每个参数的含义

先看一份最小可用的配置。我以 OpenAI 兼容协议为例,因为大部分云厂商都提供兼容接口,改个 base-url 就能切换。

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

逐行拆解一下。api-key是身份凭证,我强烈建议用环境变量注入而不是硬编码在 yml 里,原因不用多说,代码一旦提交到仓库,密钥就泄露了。base-url是接口地址,如果你用的是官方服务可以省略这一行,用兼容接口的云厂商就必须显式指定。

model指定具体用哪个模型。这里有个经验:开发调试阶段用便宜的小模型,比如 mini 系列,等提示词调好了再换大模型跑正式效果。我见过有人一上来就用最贵的模型调试,一天烧掉几十块,其实前期的调试根本不需要那么强的能力。

temperature控制输出的随机性,取值 0 到 2 之间。0 表示每次输出几乎一样,适合需要稳定结果的场景比如信息抽取;0.7 左右适合日常对话;1.5 以上会变得很有创意但容易跑偏。做客服类应用建议 0.3 到 0.5,做创意写作可以到 1.0 以上。

max-tokens限制单次响应的最大长度。这个参数很关键,设太小会导致回答被截断,设太大又浪费额度。一般对话场景 1024 到 2048 够用,长文生成场景需要 4096 以上。注意这个值不是越大越好,有些模型对输出长度有硬上限,超过会直接报错。

2.2 依赖引入的取舍

pom 里需要引两个东西:Spring AI 的 BOM 和具体的 starter。

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> </dependencies>

用 BOM 的好处是统一管理版本,starter 不用写 version,避免版本冲突。这是 Spring 生态的标准做法,没什么好纠结的。

这里有个坑要提醒:Spring AI 的仓库地址可能不在 Maven 中央仓库里,如果拉不下来依赖,需要在 pom 里额外配置 milestone 或 snapshot 仓库。具体地址以官方文档为准,我这里不贴了,因为地址会变。判断方法很简单,如果mvn dependency:resolve报找不到 artifact,八成就是仓库没配。

2.3 ChatClient 的两种创建方式

ChatClient 有两种拿到实例的方式,我两种都用过,各有适用场景。

第一种是自动注入 Builder。Spring 启动时会自动装配一个ChatClient.Builder,你直接注入进来,然后.build()就得到实例。

@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder.build(); } }

第二种是手动创建。你自己 new 一个 Builder,传入 ChatModel。

ChatClient client = ChatClient.builder(chatModel).build();

什么时候用哪种?如果你整个应用只用一个模型,用自动注入最省事。如果你需要同时对接多个模型(比如一个用于对话、一个用于摘要),那就手动创建多个实例,每个绑定不同的 ChatModel。我建议新手先用自动注入跑通,等有明确的多模型需求再改手动。

提示:自动注入的 Builder 是原型作用域的,每次注入都是新实例,所以你可以放心地在构造函数里 build,不用担心共享状态问题。

3. 实操过程与核心环节实现

3.1 第一步:创建 ChatClient 实例

这一步是整个链路的起点。我用自动注入的方式,代码最简洁。

@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个专业的技术助手,回答简洁准确。") .build(); } }

注意这里我在 build 之前调了defaultSystem。这是设置默认系统提示,意味着这个实例后续所有对话都会带上这个系统提示,不用每次重复设置。这是链式调用的第一个优化点——把不变的配置固化在实例上。

为什么把系统提示放在创建阶段而不是每次调用时设置?因为系统提示通常在一个服务里是固定的。比如客服服务永远是"你是客服",翻译服务永远是"你是翻译"。把它固化在实例上,调用代码就干净了。当然如果你需要动态切换系统提示,也可以在调用时覆盖,后面会讲。

3.2 第二步:设置系统提示(可选但推荐)

如果你没有在创建时设置 defaultSystem,或者需要针对某次调用临时改系统提示,可以在链式调用里加.system()。

String response = chatClient.prompt() .system("你是一个幽默的段子手,用轻松的语气回答。") .user("解释一下什么是依赖注入") .call() .content();

.system()和创建时的defaultSystem是什么关系?后者是默认值,前者是本次调用的覆盖值。如果两者都设置了,.system()优先。这个优先级设计很合理,符合"就近原则"。

系统提示的写法有讲究。我总结了几条经验:第一,明确角色,不要只说"你是一个助手",要说"你是一个有十年经验的 Java 架构师";第二,明确输出格式,比如"用不超过三句话回答";第三,明确禁止事项,比如"不要编造不存在的 API"。这三条加上去,输出质量会有明显提升。

3.3 第三步:传入用户消息

.user()就是传入用户消息,这是链式调用里最核心的一步。

String response = chatClient.prompt() .user("Spring AI 的 ChatClient 怎么用?") .call() .content();

.user()接受字符串,也接受其他类型。如果你要做多轮对话,需要传入消息列表,可以用.messages()方法。但大多数单轮场景,.user()就够了。

这里有个细节:.user()可以调用多次吗?可以,但效果是追加消息,不是覆盖。如果你连续调两次.user(),会变成两条用户消息。这个行为在构造多轮对话时有用,但单轮场景下容易误用。我建议单轮就用一次,多轮用.messages()显式传列表,语义更清晰。

3.4 第四步:调用并获取响应

.call()是真正发起请求的方法,.content()是从响应里提取文本内容。

String content = chatClient.prompt() .user("你好") .call() .content();

.call()返回的是一个ChatResponse对象,里面除了文本内容,还有元数据比如 token 消耗、模型名称、结束原因等。如果你需要这些信息,就不要调.content(),直接拿ChatResponse。

ChatResponse response = chatClient.prompt() .user("你好") .call() .chatResponse(); String text = response.getResult().getOutput().getContent();

什么时候需要完整响应?做用量统计、做成本核算、做调试的时候。日常业务代码里,.content()直接拿字符串就够了。

3.5 完整可运行的最小示例

把四步串起来,一个完整的 Controller 长这样:

@RestController @RequestMapping("/chat") public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个简洁的技术助手。") .build(); } @GetMapping("/ask") public String ask(@RequestParam String question) { return chatClient.prompt() .user(question) .call() .content(); } }

启动项目,访问/chat/ask?question=你好,就能看到模型返回的内容。整个核心代码不到二十行,这就是 Spring AI 抽象带来的效率。

3.6 流式响应怎么改

上面是同步阻塞的写法,用户要等模型全部生成完才能看到结果。如果响应内容长,等待体验很差。改成流式只需要把.call()换成.stream()。

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> stream(@RequestParam String question) { return chatClient.prompt() .user(question) .stream() .content(); }

.stream()返回的是Flux<String>,配合 SSE 就能实现打字机效果。注意produces要设成text/event-stream,否则浏览器不会按流式处理。这个改动很小,但用户体验提升很大,我建议对话类应用默认就用流式。

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

4.1 启动报错找不到 ChatModel Bean

这是新手最常见的问题。报错信息大概是No qualifying bean of type 'ChatModel'。原因通常是三种:一是 starter 没引对,二是 yml 配置的层级写错了,三是 api-key 为空导致自动装配跳过。

排查顺序:先看 pom 里 starter 的 artifactId 对不对,再看 yml 里spring.ai下面的层级是不是和 starter 匹配,最后检查 api-key 环境变量有没有真的注入进去。我遇到过有人 yml 写对了但环境变量名拼错,结果 api-key 是空字符串,自动装配直接跳过,报的就是找不到 Bean。

4.2 调用返回 401 或 403

认证失败。先确认 api-key 有没有多余的空格或换行,从环境变量读的时候特别容易带进来。再确认 base-url 有没有写错,有些兼容接口的路径需要带/v1,有些不需要,这个要看具体厂商的文档。最后确认模型名称对不对,有些厂商的模型名和官方不一样。

4.3 响应被截断

如果回答说到一半就没了,八成是max-tokens设小了。把它调大试试。但要注意,调大之后如果还是截断,可能是模型本身的输出上限到了,这时候需要在提示词里要求模型分点回答或者精简表达。

4.4 中文乱码

这个在流式响应里偶尔遇到。检查两处:一是produces有没有设对,二是响应头的字符集。Spring 默认用 UTF-8,一般不会有问题,但如果前面有网关或者反向代理,可能会被改掉。排查方法是直接看原始响应字节,确认是编码问题还是内容问题。

4.5 常见问题速查表

现象可能原因排查方向
找不到 ChatModel Beanstarter 未引入或配置层级错误检查 pom 和 yml 层级
401/403api-key 无效或 base-url 错误检查密钥和地址
响应截断max-tokens 过小调大参数或优化提示词
中文乱码字符集配置问题检查 produces 和响应头
启动慢首次拉取依赖或模型列表检查网络和仓库配置
流式无输出produces 未设 event-stream检查注解配置

4.6 几个我踩过的坑

第一个坑是在循环里反复创建 ChatClient。我一开始图省事,每次请求都 new 一个 Builder 再 build,结果发现性能很差。后来改成在构造函数里创建一次,复用实例,性能立刻上来了。ChatClient 是线程安全的,可以放心复用。

第二个坑是系统提示写太长。我一开始把一大堆规则塞进 defaultSystem,结果每次请求都带着这一大段,token 消耗很高。后来把固定规则精简,动态规则放到调用时设置,成本降了不少。

第三个坑是忽略超时配置。默认超时可能很长,模型响应慢的时候请求会一直挂着。建议在 yml 里显式配置连接超时和读取超时,具体参数名看 starter 文档。我一般设连接 10 秒、读取 60 秒,大部分场景够用。

第四个坑是把 api-key 提交到了仓库。这个不用多说了,血的教训。现在我的做法是本地用.env文件,仓库里只放.env.example,CI 环境用密钥管理服务注入。

4.7 提示词调优的实操心得

系统提示的质量直接决定输出质量。我总结了一个三段式模板:角色定义 + 任务约束 + 输出格式。举个例子:

你是一个资深 Java 后端工程师。 回答技术问题时,优先给出可运行的代码示例,代码要带注释。 输出格式:先用一句话总结,再给代码,最后补充注意事项。

这个模板比单纯写"你是一个助手"效果好很多。角色定义让模型知道用什么知识库,任务约束让它知道重点在哪,输出格式让结果结构化。我实测下来,同样的模型,加上这三段之后,回答的可用性提升很明显。

另外,温度参数和提示词要配合调。提示词约束越严格,温度可以适当调高一点增加灵活性;提示词比较宽松,温度就要调低避免跑偏。这两个参数不是独立的,要一起看。

5. 扩展方向与进阶思路

5.1 多轮对话怎么实现

单轮对话跑通之后,下一步自然是多轮。核心是把历史消息带上。Spring AI 提供了ChatMemory抽象,可以自动维护对话历史。

ChatClient client = builder .defaultSystem("你是一个技术助手。") .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())) .build();

加上这个 Advisor 之后,同一个会话 ID 的对话会自动带上历史。会话 ID 通过.advisors()传入。这个机制比自己手动维护消息列表省事很多,但要注意内存版的历史存在内存里,重启就没了,生产环境需要换成持久化实现。

5.2 结构化输出怎么拿

有时候你不想拿字符串,想直接拿对象。Spring AI 支持.entity()方法,传入目标类型,它会自动把模型输出解析成对象。

record Answer(String summary, List<String> points) {} Answer answer = chatClient.prompt() .user("总结一下 Spring AI 的三个优点") .call() .entity(Answer.class);

这个功能很实用,但有个前提:模型输出必须是合法 JSON。所以提示词里要明确要求输出 JSON 格式,否则解析会失败。我一般会在系统提示里加一句"输出必须是合法 JSON,不要包含 markdown 代码块标记"。

5.3 后续可以怎么扩展

跑通基础对话之后,可以往几个方向扩展。一是接入 RAG,把私有文档向量化后作为上下文注入,让模型基于你的资料回答。二是加工具调用,让模型能调用外部 API 查天气、查数据库。三是做多模型路由,简单问题用小模型、复杂问题用大模型,控制成本。这几个方向 Spring AI 都有对应的抽象,但每个都值得单独写一篇,这里就不展开了。

我个人在实际操作中的体会是,Spring AI 最大的价值不是帮你省了多少代码,而是帮你建立了一套统一的思维模型。你理解了 ChatClient、ChatModel、Advisor 这几个概念之后,换任何模型供应商都是换配置的事,业务代码基本不动。这种可迁移性,比省下来的那几十行胶水代码值钱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:51:50

从碎片化到系统化:AIGC创作训练营的完整工作流实战

刷了两个月的免费AI教程&#xff0c;收藏夹里躺着上百条“AI绘画入门”“提示词大全”&#xff0c;真打开创作界面的时候&#xff0c;我还是两眼一抹黑。后来朋友把闪学it-AI-AIGC创作训练营的课程安排丢给我&#xff0c;我抱着“就当找人逼我交作业”的心态入了营。五周下来收…

作者头像 李华
网站建设 2026/10/14 6:51:15

无锁环(Lock-Free Ring Buffer)实现原理与代码详解

1. 引言 在高并发系统中&#xff0c;生产者与消费者之间频繁交换数据的场景随处可见&#xff1a;日志采集、网络收发包、消息队列、音视频流处理等。用一个简单的「加锁队列」能满足功能需求&#xff0c;但锁带来的上下文切换、线程阻塞与竞争开销&#xff0c;往往成为吞吐量瓶…

作者头像 李华
网站建设 2026/10/14 6:51:08

生产级网络工程:从连得上到连得稳、连得智的实战体系

1. 这不是教科书里的“网络工程”&#xff0c;而是我踩过坑后画出的系统架构神经图“系统架构设计笔记&#xff08;18&#xff09;—— 网络工程”这个标题乍看平平无奇&#xff0c;像极了某本被翻烂的教材目录页。但如果你真在大型分布式系统里调过跨机房延迟、查过凌晨三点的…

作者头像 李华
网站建设 2026/10/14 6:50:59

从高精地图到BEV实时感知:自动驾驶无图方案工程落地全解析

1. 从“背着地图跑”到“睁眼看世界”&#xff1a;自动驾驶感知路线的底层逻辑切换早几年入行做自动驾驶的朋友&#xff0c;应该都经历过那个“高精度地图崇拜”的阶段。当时大家默认一个前提&#xff1a;车要自己开&#xff0c;必须先有一张厘米级的“底图”&#xff0c;把车道…

作者头像 李华
网站建设 2026/10/14 6:50:58

Plotly数据可视化:交互式图表开发与实战应用

1. 为什么选择Plotly进行数据可视化第一次接触Plotly是在三年前的一个客户项目中&#xff0c;当时需要展示一组复杂的销售数据趋势。用Matplotlib生成的静态图表让客户频频皱眉&#xff0c;而当我切换到Plotly制作的交互式图表时&#xff0c;会议室的气氛立刻活跃起来——客户可…

作者头像 李华
网站建设 2026/10/14 6:50:37

排污权交易与全要素生产率:DID实证复现全流程解析

最近一直在做环境经济学里几篇经典论文的复现&#xff0c;其中最值得把全过程写出来的&#xff0c;就是“排污权交易机制是否提高了企业全要素生产率——来自中国上市公司的证据”这个题目。它看起来只是评估一项环保政策的效果&#xff0c;实际上是一套完整的微观计量实战&…

作者头像 李华