先说两句题外话。Spring AI 这个系列我写了第 1 篇的整体认知和第 2 篇的环境准备之后,后台一直有读者催更第 3 篇,问的几乎全是同一个问题:Spring AI 里的 Model API 到底怎么用?坦白讲,Model API 在 Spring AI 里既是基础中的基础,也是整个框架中最容易被误解的部分。很多人一上来就去找所谓"最全配置",结果被各种参数绕晕,连 ChatModel 和 EmbeddingModel 的区别都没搞清楚就开始抄代码,最后连最简单的对话请求都跑不通。这篇我打算把 Spring AI 的 Model API 从头到尾掰开揉碎,按照我实际项目里的使用顺序来讲,尽量让你看完就能直接上手。
从第 1 篇开始我就提过一个观点:Spring AI 并不是在发明一套新的人工智能技术,而是把"怎么跟大模型对话"这件事标准化了。它要解决的痛点和当年 Spring JDBC 解决数据库连接一样——不同厂商的 SDK 各写各的,接口风格千差万别,每换一家大模型服务商就要把业务代码推翻重写。Spring AI 把这一切抽象成统一的 Model API 之后,你只需要关心自己业务里要的是什么能力,底层是哪家公司提供的模型并不重要。这篇第 3 篇的内容,我会重点围绕这个抽象层展开,从 API 全景讲到实际配置,再到多模型切换和排障技巧,全程使用可运行的最小示例,保证你能照着落地。
1. Model API 的全景认识:Spring AI 到底抽象了哪些模型能力
先说一个最直观的问题:Spring AI 的 Model API 一共包含哪几类模型能力?如果你去看官方文档,会看到一大堆名字,比如 ChatModel、EmbeddingModel、ImageModel、AudioModel、TranscriptionModel,甚至还有 ModerationModel。新手第一反应总是想全都学一遍,但我建议你改变这个思路:不要按模型种类去学,而要按你项目里的需求去学。
Model API 本质上是 Spring AI 封装的"模型能力接口层"。它把每家服务商的模型访问协议、鉴权方式、参数命名、返回结构全部屏蔽掉,对外暴露一套统一的方法签名。打个比方:你没用 Spring AI 之前,接 A 厂商的模型要学 A 的 SDK,接 B 厂商的模型要学 B 的 SDK,这些 SDK 连请求超时设置都不一样;用了 Spring AI 之后,你接触到的就是几个明确定义的 Java 接口,传入一个统一风格的请求对象,拿回一个统一风格的响应对象。
在实际项目中,我用得最多的是三个:
- ChatModel:对话补全,也就是"你问我答"。这是绝大多数业务场景的入口,也是本文的核心。
- EmbeddingModel:向量化,把文本变成稠密向量,用于语义检索、知识库、聚类等场景。
- ImageModel:文生图,根据提示词生成图片,适合做封面图、海报素材等。
其他的模型类型,比如音乐生成、语音转文字、内容审核,都属于特定业务才会碰到的分支。Spring AI 把它们都纳入统一的 Model API 门面之下,但上手顺序应该从 ChatModel 开始,然后根据项目需要逐步扩展。
为什么会这样设计?我个人的理解是:Spring AI 团队从一开始就希望这个框架能"面向接口编程"。如果你的业务代码直接依赖某个厂商的 SDK,那你就被厂商绑死了;但如果你依赖的是 Spring AI 抽象出来的 ChatModel 接口,那么你在配置里切换一个属性,就能从 A 厂商换成 B 厂商,甚至从云端模型换成本地模型,业务代码一行都不用改。这种解耦带来的直接收益是:你在项目初期可以用便宜甚至免费的本地模型做开发测试,确定功能稳定之后再切换到云端生产环境,两者之间的切换成本几乎为零。
2. 快速打通第一个 ChatModel 调用链
2.1 最小依赖与自动装配
如果你是第一次写 Spring AI 的代码,最简单的路线是创建一个 Spring Boot 项目,然后引入对应的 starter。以我惯用的 Maven 配置为例,第一步只需要在 pom.xml 里加入 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>然后加入你需要的 starter。假设你先用某云厂商的对话接口,就引入一个对应的 Chat 依赖;如果你本地已经用开源工具拉起了一个模型服务,也可以引入本地推理的 starter,两者接口完全一样。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency>我不太建议直接手写一个 ChatClient 出来。Spring AI 的自动装配机制会帮你把 ChatModel 的实现类创建好,你只需要在 application.yml 里提供基础配置。这一步也是很多新人卡住的地方:看到 NPE 就以为是代码写错了,实际上是配置没给全。
spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL} chat: options: model: gpt-4o-mini temperature: 0.8注意,真实业务的配置绝不能把密钥明文写在 yml 里,我一般用环境变量或配置中心注入。这里写${OPENAI_API_KEY}就是让你在启动时通过环境变量传入。
2.2 从 ChatClient 到 ChatModel:两种写法的各自定位
自动装配完成之后,你有两个入口可以使用。第一个是 Spring AI 1.0 之后主推的 ChatClient,流式 API,用起来最直观。第二个是底层的 ChatModel,如果你需要精细控制请求参数,或者要写一套自定义的模型调用层,ChatModel 更直接。
我的习惯是:日常业务一律走 ChatClient,因为它对 prompt、参数、输出类型的封装更好用;只有在需要访问底层模型独有的能力时才绕过 ChatClient,直接用 ChatModel。下面的代码是 ChatClient 的最小示例:
@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String ask(String question) { return chatClient.prompt(question) .call() .content(); } }这里有个细节:在 Spring AI 较新的版本中,ChatClient.Builder 会自动被注入,你甚至可以不用手动 build,直接在配置类里定义一个 ChatClient 的 Bean。我保留 builder 的原因是,我经常需要在不同的业务场景里用不同的默认参数,比如一个用于普通问答,一个用于严格JSON输出的场景。
如果你要用底层的 ChatModel,写法类似,但返回体多了一层结构:
@Service public class RawChatService { private final ChatModel chatModel; public RawChatService(ChatModel chatModel) { this.chatModel = chatModel; } public String ask(String question) { ChatResponse response = chatModel.call( new Prompt(question)); return response.getResult().getOutput().getText(); } }上面的Prompt是 Spring AI 中的核心请求对象,它不只包含用户消息,还可以包含系统消息、历史消息和模型参数。当你需要多轮对话时,Prompt会带上多个Message;当你需要调整模型参数时,Prompt里塞进一个ChatOptions实例。
2.3 请求参数背后都发生了什么
新手往往对temperature、topP这些参数又爱又怕。爱是因为好像调一调就能让回答变聪明,怕是因为不知道背后的逻辑。我用自己的理解做一个简化说明。
大模型生成文本本质上是"按概率挑选下一个词"。每个词都有一个被选中的概率,temperature正是控制这个概率分布的熵——值越低,模型越倾向于选择概率最高的那个词,输出就更确定、更保守;值越高,低概率词也有机会被选中,输出就更随机、更具创造性。常见的场景里,代码生成、数据抽取、JSON 格式化最好把temperature调到 0 到 0.3,而文案创作、头脑风暴、想法生成环节调到 0.7 到 1.0 反而效果更好。
topP是另一个筛选机制。它不按温度缩放概率,而是从累积概率达到某个阈值的最小词集合里采样。topP=0.9的意思是只从累积概率最大的前 90% 的候选词里选。实际项目里我很少同时大幅调两个参数,一般固定一个,调另一个就够了。否则两个机制叠加,你很难判断输出变化到底是谁引起的。
Spring AI 里参数的传递方式有三种,优先级从低到高分别是:application.yml 中的全局配置、ChatOptions中的默认配置、每次调用时在Prompt中临时指定的配置。这个优先级设计在排障时非常重要——遇到"我明明改了参数模型怎么不听话"的情况,先看看是不是某个地方用默认配置覆盖了你的显式配置。
3. ChatModel 的高级玩法:多轮对话与结构化输出
3.1 多轮对话的 Message 结构
用一个无状态的prompt(question)调用大模型,是入门级玩法。真实业务里几乎都会遇到多轮对话的需求:用户先问"Spring AI 是什么",再追问"它和 LangChain 有什么区别",这时候模型需要知道第一轮聊过什么。
Spring AI 里多轮对话的实现方式是把历史消息一起放进Prompt。为了搞明白这个机制,你需要先理解Message的角色划分。它有四种典型类型:
SystemMessage:系统指令,声明模型的身份和行为准则。UserMessage:用户消息,也就是你真正想问的问题。AssistantMessage:模型的历史回复。ToolResponseMessage:函数调用返回的结果,涉及 Tool Calling 时才会用到。
看起来很简单,但实际项目里最容易犯的错误是无限累加历史消息。如果你把每一轮对话都原封不动地追加到Prompt里,很快 Token 数就会爆掉。我一般的做法是:只保留最近 N 轮消息,并对超长历史做摘要窗口。这个窗口大小需要根据模型上下文长度和业务诉求平衡,没有万能的固定值,但思路必须是"消息有去有留"。
下面这段代码演示了如何在业务层维护一个简单的对话历史:
public class ConversationWindow { private final List<Message> history = new ArrayList<>(); public String chat(String userMessage, int maxRounds) { history.add(new UserMessage(userMessage)); if (history.size() > maxRounds * 2) { history.subList(0, history.size() - maxRounds * 2).clear(); } ChatResponse response = chatModel.call(new Prompt(history)); String answer = response.getResult().getOutput().getText(); history.add(new AssistantMessage(answer)); return answer; } }注意我维护的是一个List<Message>,而不是单纯的字符串列表。这个细节很重要,因为 Spring AI 的Prompt接受的就是消息对象列表,它内部要以此判断消息角色,从而构造出符合模型要求的对话格式。
3.2 结构化输出:让模型返回可靠的 JSON
聊天式的字符串输出用起来方便,但一旦到了企业应用场景,你需要的是程序能直接消费的数据。比如让模型从一段合同文本里抽取"甲方、乙方、金额、生效日期",你还让模型随意输出一段话,那下游解析代码就要被逼疯了。
Spring AI 对结构化输出的支持做得挺到位的。它可以基于你定义的 Java 类自动生成格式说明,在请求时要求模型按指定格式返回,随后自动完成反序列化。我在代码里最常用的是ChatClient的.entity()方法:
public record ContractInfo(String partyA, String partyB, BigDecimal amount, LocalDate effectiveDate) { } public ContractInfo extractContract(String text) { return chatClient.prompt() .system("你是一个严谨的合同信息抽取专家。") .user(text) .call() .entity(ContractInfo.class); }看上去仿佛变魔术,但这里其实有两件值得说的事。第一,Spring AI 会在底层为这个 record 生成 JSON Schema 描述,把它塞给模型,模型必须输出符合该 Schema 的 JSON。第二,如果模型输出的 JSON 缺字段或者多字段,Spring AI 的反序列化器会尽量做容错处理。在实际生产中,我强烈建议你对关键抽取结果做二次校验,不要盲目相信模型的输出,哪怕它在测试集上表现得再好。
还有一类接口只支持返回单条 JSON,不适用流式输出。如果你发现某个模型接口在结构化模式下无法启动流式,不用惊讶,这是部分模型厂商的限制。你需要退回到非流式调用,换取输出的规范性。
3.3 流式输出:从"等回答"到"边收边显示"
有过调用大模型接口经验的人应该有体会:一次完整的对话生成可能需要几秒甚至十几秒。如果用户在前端盯着一片空白等十几秒,体验是很糟糕的。流式输出的价值在于把一段较长的响应拆成若干小块,边生成边推送,用户看到的效果就是"打字机式"的逐字输出。
Spring AI 的流式调用方法名是stream,它的返回类型是Flux<String>,这正是响应式编程中的数据流。配合 WebFlux 或者 Spring MVC 的异步支持,你可以非常方便地将内容推送到前端。一个最基础的流式调用长这样:
public Flux<String> streamChat(String question) { return chatClient.prompt(question) .stream() .content(); }如果你之前只写过同步接口,第一次接触Flux可能会觉得有点突兀,但它的心智模型并不难:想象一条河流,模型吐出一块内容,河流就流过来一块;当模型输出结束时,河流就关闭。你要做的只是在上游定义一个Flux<String>,下游负责消费。消费的方式可以是直接返回给 Web 层,也可以是逐个处理后再聚合。
我踩过一个坑:流式输出与结构化输出同时使用时,某些模型提供商的兼容性并不好。模型可能默认要完整生成完才能输出 JSON,强行流式只会造成前端收到半截 JSON 片段。所以你需要在项目设计阶段就决定好:是追求实时性,还是追求可解析的完整结果。不要等代码写到一半再去纠结。
4. EmbeddingModel 与向量化检索实战
4.1 Embedding 到底在干什么
说完 ChatModel,接下来是另一块我项目中不可或缺的能力:EmbeddingModel。先说一个生活中的类比。假如你要在一个巨大的书库里找到与某一句话意思最相近的一段话,靠搜索引擎的关键词匹配常常会漏掉同义表达。Embedding 做的就是把"一句话"转换成一串高维数值,这个数值能体现出语义的远近——意思相近的句子在向量空间中距离更近。
Spring AI 的 EmbeddingModel 将各家厂商的向量化接口统一封装。你需要做的不多:准备一个文本列表,调用方法,拿到一个EmbeddingResponse,里面是一串float[]或者double[]。代码异常简单:
@Service public class EmbeddingService { private final EmbeddingModel embeddingModel; public EmbeddingService(EmbeddingModel embeddingModel) { this.embeddingModel = embeddingModel; } public float[] embed(String text) { EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text)); return response.getResult().getOutput(); } }这里需要注意一个维度问题:不同模型产出的向量维度不一样,有 384 维的,有 768 维的,也有 1536 维甚至更高的。这个维度不是越高越好,高维度意味着更强的表达能力,但也意味着更高的存储和计算成本。如果你要把向量存进数据库,维度过高会对索引大小和检索速度产生明显影响。
4.2 手搓一个最小知识库检索
EmbeddingModel 单独用价值有限,但它一旦和向量数据库结合起来,就能做出一个非常有用的东西:知识库问答。整体链路是这样的:先把你的文档切成小段,每段通过 EmbeddingModel 转成向量,存入向量数据库;当用户提问时,把问题也转成向量,去数据库里找语义上最相似的若干文档片段;最后把这些片段作为上下文交给 ChatModel,让它基于上下文作答。
这个架构在 Spring AI 生态里被称为 RAG,本质是"先检索,后生成"。虽然 Spring AI 提供了很多高级组件来简化 RAG,但如果你刚开始接触,我建议先用最原始的方式把链路跑通一次,用代码感受每个环节发生了什么。伪代码如下:
public String askWithKnowledgeBase(String question) { float[] questionVector = embeddingModel.embed(question); List<Document> similarDocs = vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(4) .build()); String context = similarDocs.stream() .map(Document::getText) .collect(Collectors.joining("\n")); return chatClient.prompt() .system("请基于以下资料回答问题,如果资料中找不到答案,请直接说明不知道。\\n" + context) .user(question) .call() .content(); }这里我用vectorStore.similaritySearch做了向量检索,但事实上不同向量数据库的客户端 API 差异很大。Spring AI 封装了一个统一的VectorStore接口,但如果你直接配合某一种数据库的 Spring Boot Starter 使用,配置方式会有所不同。你要有心理准备:向量数据库这块不像 ChatModel 那样换厂商零成本,因为不同数据库在索引策略、量化方式上各有差异,但 Spring AI 至少已经把最常用的存储操作收敛到同一套语义之下了。
4.3 为什么说 Embedding 模型要在同一家里面选
这里有一个很多人踩过坑后才明白的点:向量不是随随便便可以混用的。你用 A 模型产出的向量,必须也用 A 模型来编码你的查询,才能去做相似度比较。原因很简单,每个模型的向量空间映射方式都不相同,B 模型产出的向量在 A 模型的向量空间里毫无意义。
所以,当你在公司里搭知识库时,一定要把"向量化模型"当作一个全局统一的基础设施来管理。不要文档入库时用这个模型,查出来以后又因为升级模型导致新旧向量不一致。模型换版本,所有存量向量必须重新计算。我曾经在一个项目里吃过这个亏:某开源向量模型的版本从 v1 升到 v2,我为了省事只对新文档跑了新模型,结果线上检索质量明显下降,排查了三天才意识到新旧向量不能混用。
5. ImageModel 与多模态扩展思路
5.1 文生图的 API 结构
ImageModel 是 Model API 里用法相对独立的一块。它的核心方法是call,传入一个ImagePrompt,返回一个ImageResponse。每个响应里包含生成图片的 URL 或者 Base64 编码数据。代码上很直接:
@Service public class ImageService { private final ImageModel imageModel; public ImageService(ImageModel imageModel) { this.imageModel = imageModel; } public String generate(String prompt, int width, int height) { ImageOptions options = OpenAiImageOptions.builder() .withWidth(width) .withHeight(height) .build(); ImageResponse response = imageModel.call(new ImagePrompt(prompt, options)); return response.getResult().getOutput().getUrl(); } }要提醒的是,文生图请求通常比较耗时,动辄十几秒甚至更长。如果是同步调接口,一定要把调用放在异步任务里,不要让用户请求一直挂着。我习惯的做法是:把生成任务丢到线程池里,前端轮询任务状态,生成完成后再展示图片。
5.2 从单模态走向多模态
随着模型能力增强,你会发现"文本对话"远不能满足所有业务。比如用户上传一张产品图,问你"这个包装上的文字是什么";或者用户上传一段音频,让你总结内容。这类需求通常需要多模态模型同时理解文本和另一类输入。
Spring AI 对多模态的支持力度也在持续增强。在 ChatModel 的请求里,你可以传入携带图片等数据的UserMessage,模型会尝试解析图片内容并输出回答。如果你在项目里已经建好了统一的 ChatModel 入口,扩展多模态的时候就不需要推翻重来,只需要在消息内容上多加几个类型。这对长期维护来说是巨大的红利。
不过,多模态请求对 Token 消耗和响应延迟的冲击很大。图片会按像素切块折算成 Token,成本比纯文本高一个量级。你要在代码里做好资源开销的监控,避免被几个高频的图片理解请求把账单打爆。
6. 多模型切换与自定义配置的完整经验
6.1 一套代码,多环境切换
我开头说过,Spring AI 最吸引我的地方是解耦带来的切换成本极低。但"切换成本低"并不等同于"随便换"。不同的模型提供商之间,能力边界、参数支持范围、价格策略都不一样。比如有的模型不支持temperature=0,有些模型不支持response_format={json_object},你在切换后可能遇到微妙的行为差异。
我处理这类问题的方法是在业务层再包一层自己的模型抽象。不直接用 Spring AI 的 ChatModel 满天飞,而是定义项目内的AiService接口,在实现类里选择具体调用 Spring AI 哪个 Model API。这样一来,万一某一家的模型能力不满足需求,我只需要替换实现的内部,上游业务代码完全感知不到变化。
public interface BusinessAiService { String chat(String sceneId, String userInput); } @Component public class BusinessAiServiceImpl implements BusinessAiService { private final ChatClient chatClient; @Override public String chat(String sceneId, String userInput) { ChatClient request = switch (sceneId) { case "code-review" -> chatClient.mutate() .defaultOptions(ChatOptions.builder().temperature(0.1).build()) .build(); case "copywriting" -> chatClient.mutate() .defaultOptions(ChatOptions.builder().temperature(0.9).build()) .build(); default -> chatClient; }; return request.prompt(userInput).call().content(); } }这种设计的好处是:底层模型参数、甚至底层模型厂商的变化都被局部化了。你的业务代码只依赖BusinessAiService这个接口,而不依赖任何具体模型。
6.2 自定义 Model 实例与超时控制
Spring AI 的自动装配省了很多事,但总有需要精细微调的时候。比如你希望同一套应用里根据不同租户走不同的模型 Endpoint,又比如你要调整连接池和超时时间。
在 Spring AI 中,你可以通过手动创建 Model 实例来覆盖自动装配的默认 Bean。通常的做法是在配置类里定义一个新的ChatModelBean,并使用@Qualifier区分。如果你不想覆盖全局,而是给某个模型单独指定不同的baseUrl,你需要构造一个独立的ChatOptions和对应的 RestClient 实例。
下面是设置超时时间的一个经典配置片段,我用的是内部的OpenAiChatModel构建器。注意,这只是一种示意,在具体项目中你要看你引入的是哪一家的 Starter,方法签名会略有差异。
@Bean @Qualifier("fastChatModel") public ChatModel fastChatModel(ChatModel.Builder builder) { ChatOptions options = ChatOptions.builder() .model("gpt-4o-mini") .temperature(0.3) .build(); return builder.defaultOptions(options).build(); }超时问题我想多说一句。大模型响应慢是常态,但慢和卡死是两码事。如果某个上游模型接口一直没有返回,你的应用线程会被长时间占用,在高并发场景下这是致命的。我在生产环境会单独为模型调用配置较短的连接超时和较长的读取超时,同时用 2 到 3 秒的限流和熔断机制兜底。Spring AI 本身不强制你做这些,但作为有经验的使用者,你应该自己补上这一层防护。
7. 常见问题与排查技巧实录
7.1 配置正确却报 401 鉴权失败
这是提到次数最多的问题。大多数人第一反应是"密钥错了",但实际情况往往是环境变量没生效,或者 yml 里的变量被 IDE 的启动配置覆盖了。排查步骤我建议按这个顺序来:先确认环境变量是否在当前 Shell 里确实有值;再确认 Spring 配置文件里读到的 key 不是空字符串;最后确认密钥前缀是否正确。
有时候你会发现用同一个 key 在手写 HTTP 请求时没问题,换了 Spring AI 就报 401。这通常是因为 Spring AI 的 Starter 依赖了对 key 前缀的固定格式要求,或者你误把多个 key 用逗号拼接传了进去。检查一下配置里是否有多余的空格,这类隐性问题最耗时间。
7.2 返回内容被截断或者突然中断
如果你发现模型回答在一半位置戛然而止,先不要怀疑是网络问题。最常见的元凶是 Token 限制——输出长度触顶后模型会强制终止。Spring AI 里对应的配置项是maxTokens或者maxOutputTokens。你需要根据业务场景设置足够大的值,但同时要意识到输出越长,成本和延迟也越高。
另一个可能导致中断的原因是流式环境下客户端提前关闭了连接。我之前排查过一个现象:网页端几乎每次回复都快结尾时断开,后来发现是网关层把连接空闲超时设成了很短的时间,流式输出的长连接被误杀。检查链路中的代理和网关超时配置,往往比反复调模型参数更管用。
7.3 返回 JSON 但解析失败
结构化输出是最容易出问题的地方。Spring AI 帮你做了类型映射,但「模型输出的 JSON 不符合预期结构」这种事情在低版本语言模型上依然会发生。应对策略有几个:第一,在系统提示词里给出明确的 JSON 字段定义,甚至给出一个示例,不要只依赖底层固定的 Schema;第二,在实体类中使用宽松类型,比如枚举字段不要直接映射,先用字符串接收再手动转换;第三,对关键数据做入库前的校验,发现异常时主动重试一次或者让模型自我纠正。
我还遇到过一种反直觉的情况:record类中字段名称是 Java 风格的小驼峰,但模型的输出却是 snake_case 风格,反序列化后所有字段都是空值。Spring AI 的映射组件通常会处理这种转换,但稳妥起见我会在请求参数里声明字段别名,而不是依赖默认行为。
7.4 常见问题速查表
| 现象 | 可能原因 | 快速处置 |
|---|---|---|
| 启动时 NPE,提示找不到 ChatModel Bean | 忘记引入对应 Starter 或配置了不匹配的依赖 | 检查依赖是否完整,查看自动装配条件 |
| 请求报 429 限流 | 并发请求超过模型服务限额 | 引入重试与降级策略,或提高服务套餐额度 |
| 返回内容都是重复的字或词 | 温度过低或模型上下文窗口设置不当 | 适当提高 temperature,检查提示词是否有诱导循环 |
| 中文内容乱码 | 编码不一致,通常是 HTTP 消息转换器默认字符集问题 | 在接口配置中强制 UTF-8,统一响应头 |
| 第一次响应极慢 | 冷启动、网络握手或模型加载耗时 | 使用预热请求,调整读取超时 |
| 流式输出不稳定,偶尔断流 | 网关或代理空闲超时过短 | 检查长连接相关的网关超时,并做断点续试 |
7.5 三个让排障效率翻倍的小工具
排查大模型调用问题时,我不太喜欢单纯在业务代码里到处打日志,有几个"土办法"效率很高。
第一,在接入 Spring AI 之前,先用原始的 HTTP 客户端直接调用一次模型厂商的接口,确保密钥、参数、网络都没问题。这相当于把问题的嫌疑范围缩小到"框架使用"这一层。
第二,开启 Spring AI 的调试日志。在application.yml里把对应包名的日志级别调到 DEBUG,能看到每次请求实际发送的 URL、请求头和部分请求体,很多配置错误一目了然。
第三,做一个极简的测试接口,不经过任何业务包装,直接暴露ChatClient.call的结果。当我怀疑是业务代码的缓存、事务、线程池导致问题时,这个直连接口能迅速帮我验证"框架本身没问题"。
8. 我的一点心得体会
Spring AI 的 Model API 学起来并不难,尤其是你有过手写模型调用经验之后,反而会觉得这份抽象帮你省下了很多琐碎工作。但框架顺手不意味着你可以跳过基础概念。建议你把ChatModel、EmbeddingModel、ImageModel三者分别做一个小 Demo,自己亲手打印一次输入输出结构,真正理解Prompt、Message、ChatOptions是怎么组装出来的。
项目里如果要用多模型切换,一定要在第一天就把"面向接口编程"的防线建立起来,不要等到业务写完了再回头抽象。还有一点,和生产环境对接时,把超时、限流、熔断、重试这些非功能需求放到和业务代码同等重要的位置——模型调用比普通数据库查询慢得多,稳定性的问题会被成倍放大。
最后说一个最容易被忽略的经验:不要一上来就追求"最先进"的模型参数和 RAG 流程。先把一个最简单的 ChatClient 跑通,把返回结构打印出来,把结构化输出跑明白,再逐步叠加记忆、检索、工具调用这些能力。底子稳了,后面拓展功能会顺手很多。希望这篇第 3 篇能帮你少走一些弯路,下一篇我可以讲讲 Tool Calling 和 Agent 的实战设计思路。