☰
Spring AI接入实战:yml配置与ChatClient四步链式调用
2026/10/20 6:48:49 网站建设 项目流程

这阵子把 Spring AI 新版本摸了一遍,有个感受特别深:官方这套 yml 配置加 ChatClient 的链式调用设计,确实把接入门槛降到了很低的程度。以前写代码对接大模型接口,光是鉴权、超时、重试、流式响应这几件事就能耗掉大半天,现在 Spring AI 把这一切收敛到配置文件和一个小而美的客户端类里。你只要把模型服务商的地址、密钥、模型名在 yml 里配好,代码里用 ChatClient 按四个步骤链式调用,一句“帮我写一份产品介绍”就能直接拿到结果。这篇就从一个没接触过 Spring AI 的普通后端视角出发,把这个过程完整拆开,给想快速上手的同学一份可以照着抄的参考。

1. 从 yml 到对话:Spring AI 整体思路拆解

1.1 它把“模型接入”从苦力活变成了配置项

在没有 Spring AI 这类框架之前,后端要接入一个大模型,常规路径是这样的:先去模型服务商那边申请密钥,然后看一眼他们家的 API 文档,不同服务商的请求路径、鉴权头、消息体格式各不一样。有的一次返回 JSON 里嵌着答案,有的需要手动处理 SSE 流式事件,有的还要按角色拆成 system、user、assistant 三段历史消息。这套东西写一遍不难,难的是每换一个服务商就要跟着改一遍,改到后面基本上就是复制粘贴加删删改改,维护成本全堆在业务代码里。

Spring AI 做的事情,是把“和大模型对话”抽象成一套统一接口。模型服务商的具体协议被隔离在底层实现里,业务代码只需要面对一个 ChatClient 对象:给它一句用户消息,它把请求翻译成对应服务商的格式,发出去,再把响应翻译回来。也就是说,以前那些签名、路径拼接、消息体转换、超时重试,全部下沉到框架内部,开发者只需要知道“我要和模型聊什么”,而不需要关心“模型服务商在哪、怎么握手”。

这种抽象带来的直接好处有两个。第一,接入速度变快。很多同学第一个跑通的聊天接口,从建工程到拿到回复,基本就是在几分钟内完成的,大头时间都花在等依赖下载上。第二,切换成本变低。假如今天用的是 A 服务商,明天想换成 B 服务商,只要改 yml 里的地址、密钥、模型名,代码几乎不用动。这种“面向接口开发、细节交给配置”的思路,正是 Spring 生态一贯的风格。

1.2 为什么“配好 yml 就能聊”

Spring AI 不是把模型接入能力塞进一个私有工具类里,而是继承了 Spring Boot 的自动配置机制。换句话说,它靠的不是代码里手写一个“模型客户端初始化器”,而是通过 starter 依赖把配置类加载进来。当你在 pom 或 gradle 里引入对应模块后,Spring 容器启动时会自动扫描 classpath 下的配置属性类,把 spring.ai.xxx 前缀下的 yml 配置读取出来,组装成一个可以注入的 ChatModel 实例,再封装成 ChatClient 放进容器里。

所以你写业务代码的时候,根本不需要自己 new 一个 HttpClient,也不需要去手写连接池和序列化器。只需要做三步:第一步,在工程里引入 Spring AI 的 starter;第二步,在 yml 里写上模型服务商的地址、密钥、模型名;第三步,在 Service 或 Controller 里注入 ChatClient。这套流程和 Spring Boot 里配数据源、配 Redis 几乎是同一个套路。对这种“配置驱动”的设计,我的理解是:它把模型服务商当成一种外部资源,和数据库、缓存、消息队列放在同一个抽象层次上。既然是资源,那就应该用配置去描述链接方式和参数,而不是散落在代码里。

有一点要提前说一下:不同模型服务商对应的 starter 模块不同,配置前缀也会不同。有的用 spring.ai.xxx.chat,有的用 spring.ai.yyy.chat,但结构大同小异。刚上手的时候不用纠结每一个字段,先把 api-key、base-url、model 这三个配对,就能跑通。

1.3 ChatClient:承担“聊”的统一入口

ChatClient 是 Spring AI 里最核心的业务入口。它负责三件事:组装 Prompt,调用模型,解析结果。这三个动作被设计成一套链式调用风格,写出来非常直观:

String result = chatClient.prompt() .system("你是资深技术顾问") .user("解释一下什么是锁") .call() .content();

这段代码读起来就像自然语言:“发起一个对话,设定系统角色,传入用户问题,发起调用,取出内容”。整个链路被拆成四个动作,也就是标题里说的“四步链式调用”:第一步从 prompt() 开始构建一次对话,第二步通过 system()、user() 等方法填充对话内容,第三步用 call() 真正去调用模型,第四步用 content() 从响应里拿出最终文本。

这种设计的好处是让 API 的可读性特别高。团队里其他成员看到这段代码,不用翻文档也能猜到大概意思。ChatClient 内部其实有两种构建方式:一种是直接用 Spring 容器里的自动配置对象,适合绝大多数场景;另一种是通过 ChatClient.builder(chatModel) 手动创建,适合需要自定义 prompt 增强器、自定义内存策略的高级玩法。新手阶段用自动配置就完全够了。

2. 配置层与核心细节解析

2.1 一份可用的 yml 长什么样

以我本地验证过的一份配置为例,大致长这样:

spring: application: name: ai-demo ai: demo: api-key: ${LLM_API_KEY} base-url: https://api.example.com/v1 chat: options: model: demo-chat temperature: 0.7 max-tokens: 2048

这里我特意把服务商前缀写成 demo,因为实际接入时,你需要根据自己的 starter 模块名替换成对应前缀。重点不是前缀叫什么,而是配置结构体现出来的三个维度:

  • 连接维度:api-key、base-url 解决“连到谁、怎么认证”的问题。
  • 模型维度:model 指定用哪个模型,temperature、max-tokens 控制生成风格和长度。
  • 运行维度:有些版本还支持 timeout、retry 等参数,解决“连不上怎么办”的问题。

api-key 这里用的是${LLM_API_KEY},意思是读取环境变量里的值。这个习惯一定要养成,千万别把密钥直接写死在 yml 里。否则代码仓库一泄露,密钥跟着完蛋。即使项目只是内部测试,也建议至少用一个本地的 .env 文件或者 IDE 的环境变量配置来管理。

2.2 多环境配置与密钥管理

实际操作里,我们通常会拆出 dev、test、prod 三份配置。Spring Boot 的多 profile 机制在这里非常管用。你可以在 application.yml 里只写公共项,比如模型名、温度;然后在 application-dev.yml、application-prod.yml 里分别写不同环境的 base-url 和 api-key。

举个例子,开发环境可能连的是某个内网网关或测试环境模型端点,生产环境连的是高可用模型服务端点。如果这些地址和密钥全写在同一个文件里,切环境的时候要么注释、要么改配置,很容易手滑。拆开之后,通过spring.profiles.active=dev启动即可,和 Spring Boot 本身的设计完全一致。

除了按环境拆文件,密钥管理还可以借助配置中心或云上的密钥服务,不过那是后话。至少新手阶段做到“密钥不进代码仓库”这一条,就已经规避了最常见的安全隐患。

2.3 配置项的优先级与默认值

Spring AI 和其他 Spring Boot starter 类似,配置项是有优先级和默认值的。框架在内部对模型服务商的一些参数预设了默认值,比如部分模型默认温度是 0.7,默认最大 token 数可能是 1024 或者 2048。如果你在 yml 里写了,就以 yml 为准;如果没写,框架就用默认。

这个机制有两个实际影响。第一,排查问题的时候要分清“这个参数是不是被默认值影响了”。我遇到过一位同学,觉得模型回答“太机械”,怎么改提示词都没用,后来发现是 yml 里把 temperature 写成了 0.1,接近完全确定性输出。第二,不同服务商的默认差异很大,不要拿 A 服务商的默认行为去预期 B 服务商。写配置的时候,尽量把 model、temperature、max-tokens 这三个关键参数显式补充完整,避免隐式依赖默认值。

另外还有一个容易被忽略的细节:HTTP 客户端的超时设置。Spring AI 底层用的是 RestClient 或 WebClient,默认的超时时间在部分版本里并不长。如果你的模型服务商响应慢,或者模型处理长文本,就可能出现“调用没报错但等了很久”或者“直接超时”的现象。这个参数在不同版本的配置 key 略有差异,建议跑通第一版之后就查一下当前版本的超时配置项,手动调大一些。

3. ChatClient 四步链式调用实操

3.1 第一步:创建 ChatClient 实例

在 Spring Boot 工程里,最省事的做法就是直接注入自动配置好的 ChatClient:

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

这里用的是构造器注入,Spring 容器会自动把配置好的 ChatClient 传进来。之所以推荐构造器注入而不是 @Autowired 字段注入,一是方便后续写单元测试时手动 mock,二是避免字段注入带来的循环依赖隐患。

某些场景下,你不想直接用容器里那个全局默认 ChatClient,而是希望这个服务里使用的模型参数、系统提示词和别处不一样。这时候可以通过 ChatClient.builder 手动创建:

ChatClient customClient = ChatClient.builder(chatModel) .defaultSystem("你是一个严谨的代码评审专家") .build();

builder 方式的好处是可以在创建时指定一些默认配置,后续链式调用时如果没覆盖,就用这些默认值。比如某个模块专门做代码分析,每次请求都希望加上“请用中文回答”的系统提示,这样就不用每次调用时都重复写 system()。

3.2 第二步:用 Prompt 和消息方法定义对话内容

ChatClient 的第二步是从 prompt() 方法开始的。这个方法是整条链的起点,它返回一个 PromptSpec 对象,用于继续装配本次对话的细节。最常用的方法有三个:

chatClient.prompt() .system("你是资深运维工程师,回答要简短") .user("请给我三条排查服务器负载过高的命令") .options(ChatOptions.builder() .temperature(0.3) .build()) .call() .content();

这里 system() 设置了大模型的“人设”,user() 是用户这次输入的正文,options() 则可以针对本次单次请求临时覆盖 yml 里的模型参数。注意这个覆盖是细粒度的,只影响当前这次链式调用,不会改掉全局配置。

有一点要注意:链式调用里连续写多个 user() 并不等于多轮对话。比如:

chatClient.prompt() .user("你好") .user("你是谁")

这种写法不会把“你好”和“你是谁”当作两轮历史消息传给模型,更大的可能只是后一条覆盖前一条,或者框架按单条消息处理。真正做多轮对话,需要构造带有 List 的 Prompt 对象,手动维护历史记录列表。新手阶段最容易在这里踩坑,以为聊天上下文能自动累积。实际上,ChatClient 的每次链式调用都是无状态的,要让模型记得上下文,你得把之前的消息带进来。

3.3 第三步:调用模型并处理同步与流式返回

第三步是 call() 方法。call() 是同步调用,会阻塞当前线程直到模型把完整响应生成完毕并返回。对于大多数接口场景,这个方法最直接,也最容易理解。

ChatClient.CallResponseSpec callResponseSpec = chatClient.prompt() .system("用一句话介绍自己") .user("你好") .call();

这里返回的是 CallResponseSpec,它还不是最终的结果,而是一个包含响应结果和后续处理方法的对象。你可以从它身上继续调用 content() 拿到纯文本,也可以调用 entity() 把结果映射到自定义对象。

如果你希望接口不阻塞太久,或者模型生成时间较长,可以考虑用异步或流式方式:

// 流式返回,适合打字机效果 chatClient.prompt() .user("写一篇关于缓存设计的文章大纲") .stream() .content() .doOnNext(System.out::println) .subscribe();

流式这块在不同版本里 API 差异比较大,有的版本用 Flux ,有的封装了自己的类型。实际使用的时候,建议先确认当前版本文档里的 Reactive 支持方式。流式的优点是可以边生成边返回,用户等待的体感会好很多,尤其适合生成文章、代码、长报告这类耗时操作。

同步和流式怎么选?我的判断标准是:如果业务接口本身是同步返回给前端,而且模型生成速度快,用 call() 最省事;如果模型输出很长,或者想在前端实现打字机效果,就走 stream()。新手先掌握 call() 同步链路,跑通以后再尝试流式。

3.4 第四步:解析响应并提取内容

第四步是响应解析。最常见的做法是直接拿文本:

String answer = chatClient.prompt() .user("解释一下什么是幂等性") .call() .content();

content() 返回的是模型生成的主体文本。对于简单问答、生成文案、写摘要这类需求,这一步就够了。

如果你想拿到的不是字符串,而是一个结构化对象,Spring AI 也提供了 entity() 方法。比如让模型返回 JSON,再反序列化成用户对象:

ChatClient.CallResponseSpec resp = chatClient.prompt() .system("请用JSON格式返回,字段包括:title、summary、keywords") .user("帮我总结一下Spring AI的核心特性") .call(); SummaryResult result = resp.entity(SummaryResult.class);

这里用了一个自定义类 SummaryResult,对应 JSON 里的字段。框架会尝试让模型按指定格式返回,再帮你做反序列化。不过要提醒一句:大模型返回的 JSON 不一定每次都完美符合 schema,尤其模型能力较弱或提示词约束不够明确的时候,字段可能多、可能少,类型也可能错。所以用 entity() 做严格解析时,最好在 system 提示词里把格式要求说清楚,并在业务代码里做好兜底,比如字段缺省时给默认值。

3.5 一个可以直接跑的问答接口

把前面四步串起来,最简单的 Demo 接口可以写成这样:

@RestController @RequestMapping("/ai") public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient = chatClient; } @GetMapping("/chat") public String chat(@RequestParam String question) { return chatClient.prompt() .system("你是技术领域的助手,回答精准且简洁") .user(question) .call() .content(); } }

启动应用后,访问/ai/chat?question=什么是依赖注入,就能拿到模型返回的文本。整个过程没有任何手写的 HTTP 请求代码,连接信息全部来自 yml 配置。这就是“配好 yml 就能聊”最直观的体现。

如果你还想在接口里记录调用日志,或者统一处理异常,建议把 chatClient 的调用封装到一个 Service 方法里,而不是直接在 Controller 里链式调用。这样将来加日志、埋点、限流都方便,Controller 只负责接收参数和返回结果。

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

4.1 常见异常速查表

跑 Spring AI 的时候,新手遇到最多的坑集中在这几个地方:

现象可能原因排查手段
启动报错找不到 ChatClient 类型的 Bean没引入对应 starter,或自动配置没生效确认依赖坐标和 Spring Boot 版本兼容性
调用时提示 api-key 未配置或为空环境变量没设置,或 yml 里占位符没被解析检查环境变量名,确认 spring.ai.xxx.api-key 有值
请求一直卡住直到超时模型端点网络不通,或读取超时时间太短手动 curl 测试端点,调大超时配置
返回内容被截断max-tokens 设置偏小,或模型输出长度超限增大 max-tokens,减少提示词中的冗余要求
返回 JSON 解析失败模型返回内容夹带 Markdown 代码块标记在 system 提示词中要求“不要输出任何多余文字,直接输出JSON”
连续对话答非所问没有维护多轮历史消息使用包含消息列表的 Prompt 对象传递上下文

这张表只是起点,实际项目里遇到的问题往往更复杂。但大多数问题的根源就集中在三类:配置不对、版本不匹配、上下文处理不当。

4.2 配置正确但请求超时

有一次我在本地跑通了一段代码,换到服务器上却频繁超时。第一反应是网络问题,可测了一下服务器到模型服务端的连通性又是通的。后来才发现是服务器环境变量没有同步过去,api-key 为空,框架拿了空值去请求,模型服务端直接 hang 住,直到客户端读超时才抛出异常。这个坑很隐蔽,因为日志里不会直接说“api-key is empty”,而是一个很笼统的超时异常。

排查这类问题,我给自己的固定流程是:第一步,用环境变量名回显确认值在不在;第二步,直接在服务器上发起一个最简 HTTP 请求,验证端点可访问;第三步,把 yml 里的读取超时临时调大,比如 60 秒,排除超时阈值太紧;第四步,如果还是不行,就把框架日志级别调到 DEBUG,看底层实际请求的 URL 和 header 长什么样,定位是鉴权问题还是路径问题。这套流程基本能干掉九成以上的“配置对了但还是不行”。

4.3 返回内容被截断或格式混乱

模型返回的内容被截断,通常不是网络问题,而是模型的输出长度上限被卡住了。这个上限由 yml 里的 max-tokens 控制。要注意,max-tokens 限制的是生成的 token 数量,不是中文字数。一行代码可能只占二三十个 token,一段中文可能占上百个 token。如果你在做代码生成或者长文总结,建议先把这个参数调大。

格式混乱的问题则多出在提示词不明确。你让模型“总结一下”,它可能给你一段散文;你要求“用 JSON 格式返回”,它可能额外输出一段解释或者把 JSON 包在 markdown 代码块里。解决方式是在 system 消息里把约束写死,例如:

你是一个数据输出助手。你只能输出JSON,不要输出任何解释、不要使用Markdown代码块。JSON格式如下:{"title": "...", "summary": "..."}

这种强约束不能 100% 保证不变形,但能大幅降低出错概率。如果业务对结构化输出要求极高,建议在代码里做一层解析兜底,比如提取首次出现的{到最后一个}之间的内容再去反序列化,而不是直接把整个响应扔给 JSON 解析器。

4.4 链式调用里常见的“上下文丢失”问题

我见过不少刚上手的人,把 ChatClient 的链式调用误解成天然能记住上下文。实际上,每次 prompt().call() 都是独立请求。要模拟真正的多轮对话,你需要每次请求时把之前的历史消息一起带上。

Spring AI 里多轮对话的常见做法是构造一个 Prompt 对象:

List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是应用助手")); messages.add(new UserMessage("你好,我叫小明")); messages.add(new AssistantMessage("你好,小明,很高兴认识你")); messages.add(new UserMessage("我叫什么名字?")); Prompt prompt = new Prompt(messages); String answer = chatClient.call(prompt).getResult().getOutput().getText();

这里的 call(Prompt) 是一个重载方法,接收完整消息列表。如果你只是简单地连续调用两个 user(),是没有办法让模型知道你先前提过“我叫小明”的。新手阶段如果不需要多轮,只是每次独立问答,直接用最简单的链式调用就好;需要上下文时,再按上面的方式手动维护 messages。

4.5 从初体验到小规模应用,还能再加什么

跑通第一版以后,可以逐步往上加东西。比如 Spring AI 的 Advisor 机制,允许你给 ChatClient 挂上一些增强逻辑,像是“自动记录日志”“自动追加公共系统提示词”“自动做敏感词过滤”。再比如 Memory 接口,可以让模型具备跨请求的记忆能力,避免每次都要手工传历史消息。如果你做的是客服机器人或者 AI 助手类应用,这个功能非常有用。

另一个值得研究的是函数调用。Spring AI 允许你定义一个普通的 Java 方法,把它注册成模型可以调用的工具。模型收到用户请求后,会根据需要自动调用这个方法来获取实时数据。举个例子,你让模型查天气,模型自己不知道天气数据,但它会调用一个你写好的 getWeather 方法,拿到结果后再整合成自然语言回复给用户。这套机制把“模型推理”和“业务数据”打通了,是后续写智能体类应用的核心基础。

我的建议是:不要在第一版就把所有高级特性全部引入,先把 yml 配置、ChatClient 四步调用、异常处理这三件事弄透。基础链路稳了,再逐步加 memory、advisor、function calling,每一步都能基于真实问题去理解它有什么用,而不是盲目堆功能。

5. 我在实操中的几点体会

把 Spring AI 从零跑通之后,有几点体会想特别分享。第一,版本敏感度很高。Spring AI 还在快速演进阶段,不同版本的 API、配置项、类名都有可能调整。网上很多教程是基于旧版写的,直接照抄不一定会过编译。遇到报错时,先去你当前版本的官方文档或源码里确认方法签名,而不是硬套网上的旧代码。

第二,配置管理一定要从第一天就做好。不要把密钥写在 yml 文件里,也不要把密钥传给同事之后再用聊天记录翻。环境变量 + 不同 profile 拆文件的方案虽然简单,但能省掉大量后续的麻烦。我见过不止一个团队因为密钥泄到代码仓库里被迫轮换,那感觉真的很难受。

第三,不要忽略流式调用。很多业务场景看起来好像用同步 call() 就行,但一旦模型生成时间超过 30 秒,前端就会因为等太久而表现得很糟糕。提前把 stream() 的链路准备好,生产环境切过去只是迟早的事。

第四,提示词不是玄学,是可维护的资产。把 system 提示词抽到配置文件或常量枚举里,不要在 Controller 里随手写死。这样后续调优提示词时不需要重新编译代码,也方便针对不同场景复用不同的提示词模板。

最后,Spring AI 这套抽象确实让“大模型接入”这件原本很繁琐的事情变得轻量了。对我这种关注业务实现的人来说,它最大的价值不是省掉了那几十行 HTTP 代码,而是让我可以把心力放在“我们想让模型做什么”上,而不是“怎么呼叫模型”。如果你正准备上手,建议就拿一个最简单的问答接口练手,配好 yml,写完四步调用,你会很快感受到这种开发方式的顺手之处。

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

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

立即咨询