ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring AI ChatModel与StreamingChatModel:同步与流式对接大模型实战

Spring AI ChatModel与StreamingChatModel:同步与流式对接大模型实战 平时用Spring Boot写后端接口对接大模型时最烦的就是两件事同步调用要等很久用户看着页面转圈又以为接口挂了流式输出倒是体验好但自己对接OpenAI或者智谱的SSE协议解析起来又得写一堆重复代码。Spring AI 1.x把这两个场景都抽象好了分别对应ChatModel和StreamingChatModel两个接口。这篇文章就专门讲清楚这两个东西怎么用、底层做了什么、实际项目里怎么选型以及我踩过的几个坑。如果你是第一次看Spring AI系列这里先交代一下背景Spring AI是Spring官方出的AI应用开发框架定位类似Java生态里的LangChain。它把对接大模型OpenAI、智谱、通义、Ollama本地模型等的细节封装成统一API你在代码里只需要面向ChatModel、EmbeddingModel这些接口写业务逻辑换模型厂商的时候改个依赖和配置就行。1.x系列目前已经发布了1.0.0 GA版本API趋于稳定可以放心用到生产项目。这一篇我聚焦在最核心的ChatModel和StreamingChatModel上把同步对话和流式对话一次讲透。1. 整体设计思路为什么Spring AI把“同步”和“流式”拆成两个接口很多第一次接触Spring AI的人都会困惑为什么ChatModel和StreamingChatModel是两个独立的接口而不是一个接口里放两个方法这背后其实是Spring AI团队对两个场景差异化的设计考量。1.1 同步与流式的本质区别不在快慢而在返回模型同步调用ChatModel时你的代码会一直阻塞在那里直到大模型把整段回复生成完一次性拿到完整结果。这个过程少则几秒多则几十秒取决于模型大小、输入长度和当前服务负载。而流式调用StreamingChatModel时模型每生成一小段token就会立刻推送给你你的程序可以边收边处理不用等全部生成完。从接口返回类型上就能一眼看出差异public interface ChatModel { ChatResponse call(Prompt prompt); } public interface StreamingChatModel { FluxChatResponse stream(Prompt prompt); }ChatModel返回的是单体的ChatResponse对象里面是一整段回复StreamingChatModel返回的是Reactor的Flux 也就是一个响应式流里面会不断emit出多个ChatResponse片段。这个差异决定了它们在编程模型上的完全不同同步是一个请求一个响应流式是一个请求一串响应。1.2 拆开设计的好处在哪我在实际项目中体会最深的一点是拆成两个接口让调用方和实现方都更清晰。对调用方来说你拿到一个ChatModel类型的Bean就知道它一定是同步返回结果的不会半路给你吐流拿到StreamingChatModel就知道它一定是流式的接口语义明确不容易误用。对实现方也就是各个模型厂商的适配器来说不是所有模型都支持流式拆开之后不支持流式的模型可以不实现StreamingChatModel不会被迫写一堆空方法。另外还有一个很务实的原因同步和流式在底层走的HTTP请求协议都不一样。同步调用是普通POST请求一次请求拿完整响应流式调用走的是SSEServer-Sent Events协议连接会保持打开服务端持续往同一个连接里写入事件流。把两个场景封装成不同接口底层的协议差异就可以隐藏在各自的实现类里对上层业务完全透明。1.3 核心实现类的结构Spring AI的模型适配器命名规则很统一基本都是厂商名模型类型Model的格式。比如OpenAIOpenAiChatModel、OpenAiStreamingChatModel实际上OpenAiChatModel同时实现了ChatModel和StreamingChatModel两个接口智谱AIZhiPuAiChatModel通义千问TongYiChatModelOllama本地模型OllamaChatModel同样同时实现了两个接口实现类通常由Spring Boot的自动装配机制创建。你引入spring-ai-openai-spring-boot-starter之后配置好API Key和model名称容器里就会自动出现ChatModel和StreamingChatModel类型的Bean。这套抽象真正解决了我的一个痛点以前项目里对接OpenAI后来要切到国产模型代码里全是OpenAI SDK的专属类型换起来几乎等于重写。现在用了Spring AI业务代码只依赖ChatModel和StreamingChatModel这两个接口换厂商基本就是改pom依赖和application.yml配置的事。2. ChatModel核心API精讲同步对话的完整链路这一节我们把ChatModel的使用彻底讲透。从最基础的调用方式到Prompt对象结构再到返回结果里各个字段的含义最后聊多模型环境下怎么管理Bean。2.1 基础调用三步跑通一次对话先看一个最简单的例子。在Spring Boot项目中注入ChatModel然后调用call方法Service public class ChatService { private final ChatModel chatModel; public ChatService(ChatModel chatModel) { this.chatModel chatModel; } public String chat(String userMessage) { Prompt prompt new Prompt(userMessage); ChatResponse response chatModel.call(prompt); return response.getResult().getOutput().getContent(); } }这段代码做了三件事创建Prompt用户消息、调用chatModel.call()发起同步请求、从ChatResponse里取出模型生成的文本。核心方法call是阻塞的调用线程会一直等待模型响应。这里有个小细节需要注意构造new Prompt(userMessage)时ChatModel会使用配置文件里的默认模型和默认参数。如果你什么都没配置OpenAI的默认模型是gpt-4o-mini。当然真实项目中不建议完全依赖默认值下面讲到Prompt参数时会说明怎么覆盖。2.2 Prompt对象结构不只是一句话很多人把Prompt简单理解成用户输入的字符串这是不够的。Prompt在Spring AI里是一个比较完整的结构它包含了一个或多个Message消息以及模型调用的参数配置。从源码看public class Prompt { private final ListMessage messages; private ChatOptions modelOptions; // ... }Message是消息的抽象Spring AI提供了几种常见实现UserMessage用户消息SystemMessage系统消息用于设定模型角色和行为AssistantMessage模型回复的消息多轮对话时用到ToolResponseMessage工具调用结果消息所以你要做多轮对话时不能只传当前用户说的话需要把历史消息一起传进去否则模型没有上下文记忆ListMessage messages List.of( new SystemMessage(你是一个乐于助人的助手), new UserMessage(你好我叫小明), new AssistantMessage(你好小明有什么可以帮你的吗), new UserMessage(我叫什么名字) ); Prompt prompt new Prompt(messages);而ChatOptions这个参数就是用来控制模型行为的。如果你的项目里需要针对不同请求使用不同温度、不同模型可以在构造Prompt时指定OpenAiChatOptions options OpenAiChatOptions.builder() .withModel(gpt-4o) .withTemperature(0.7) .withMaxTokens(1024) .build(); Prompt prompt new Prompt(messages, options); ChatResponse response chatModel.call(prompt);2.3 ChatResponse返回结果核心信息藏在这些字段里ChatResponse是同步调用的返回对象里面的结构可以理解为一个响应包着一组结果每个结果里是一条回复消息ChatResponse response chatModel.call(prompt); // 获取模型生成的文本内容 String content response.getResult().getOutput().getContent(); // 获取token使用统计注意同步调用时这里通常有值 TokenUsage usage response.getMetadata().getUsage(); System.out.println(输入token: usage.getPromptTokens()); System.out.println(输出token: usage.getCompletionTokens()); System.out.println(总token: usage.getTotalTokens());ChatResponse主要包含result一个Generation对象通过getOutput()可以拿到模型回复的AssistantMessage再调用getContent()就是文本内容。metadataChatResponseMetadata类型里面封装了TokenUsagetoken用量、模型名称、响应类型等元数据。实际开发中token用量统计有时会被忽略但它对排查问题和做成本监控很重要。建议在正式项目里记录每次调用的token数方便后续统计费用。有些模型厂商比如OpenAI会在响应里返回usage如果拿到的usage为null通常可以通过响应元数据里的finishReason或者自己数token的方式做粗略统计。2.4 多模型多Bean的场景依赖注入别用错现在很多项目不止接一个模型厂商可能是OpenAI为主、智谱做兜底或者OpenAI接GPT-4o的同时还要接一个embedding模型做向量化。这时候Spring容器里会有多个ChatModel类型的Bean直接Autowired注入ChatModel会启动失败报NoUniqueBeanDefinitionException。解决办法有几种我推荐用Qualifier显式指定Service public class MultiModelService { private final ChatModel openAiChatModel; private final ChatModel zhiPuChatModel; public MultiModelService( Qualifier(openAiChatModel) ChatModel openAiChatModel, Qualifier(zhiPuChatModel) ChatModel zhiPuChatModel) { this.openAiChatModel openAiChatModel; this.zhiPuChatModel zhiPuChatModel; } }Bean名称默认是实现类名的首字母小写形式比如OpenAiChatModel对应的Bean名称是openAiChatModel。如果两个厂商的类名恰好一样可以在配置类里自己定义Bean并用Primary标记默认实现。还有一个编程式方案直接从ApplicationContext里按类型获取所有Bean再根据配置路由到具体的模型MapString, ChatModel models applicationContext.getBeansOfType(ChatModel.class);这种方式适合做模型路由中心比如根据用户等级、请求来源动态选择模型。但大多数项目用Qualifier就足够了别过度设计。3. StreamingChatModel流式调用从原理到落地流式输出是现在聊天类应用的标配。用户发一句话模型一个字一个字地往外吐那种正在思考的体验是同步加载没法比的。这一节重点讲StreamingChatModel的正确用法。3.1 底层协议SSE事件流到底在传什么StreamingChatModel底层走的是SSE协议。简单理解就是客户端发起一个HTTP请求后连接不断开服务端不断往这个连接里推送数据每个数据块以data:开头块之间用空行分隔最后以[DONE]结束。如果直接看OpenAI接口返回的原始流大概长这样data: {id:chatcmpl-xxx,object:chat.completion.chunk,choices:[{delta:{content:你},index:0}]} data: {id:chatcmpl-xxx,object:chat.completion.chunk,choices:[{delta:{content:好},index:0}]} data: [DONE]Spring AI把这些都封装好了。你调用stream(prompt)拿到的FluxChatResponse每个ChatResponse就是一个chunk的解析结果里面放着模型刚生成的一段文本。注意是一段不是一个token——服务端可能一次推送多个token也可能一个token拆两次推送这取决于模型厂商的实现我们作为调用方不用关心这个粒度。3.2 Flux基础操作订阅、转换、聚合拿到Flux 之后怎么把它变成前端能用的流核心就是Reactor的操作符。最直接的方式是订阅它拿到每个chunk里的文本Test void testStream() { Prompt prompt new Prompt(讲个笑话); FluxChatResponse stream streamingChatModel.stream(prompt); stream.doOnNext(response - { String chunk response.getResult().getOutput().getContent(); System.out.print(chunk); }).blockLast(); // blockLast() 是为了让测试方法等流结束实际业务代码里不要这样写 }这里doOnNext会在每个chunk到达时执行一次输出当前收到的文本。注意doOnNext不会修改流里的元素只是偷看一眼。实际开发中通常需要做两件事一是把每个chunk里的文本拼接成完整回复用于保存到数据库二是把chunk实时推给前端。拼接的逻辑是这样的StringBuilder fullContent new StringBuilder(); flux.doOnNext(response - { String chunk response.getResult().getOutput().getContent(); fullContent.append(chunk); // 累加完整内容 }) .doOnComplete(() - { // 流结束时可以在这里保存 fullContent 到数据库 System.out.println(完整回复: fullContent); }) .subscribe();这里有个很隐蔽的坑ChatResponse.getResult()可能返回null。有些模型厂商在流式响应时最后一个chunk只包含usage统计和finishReason没有实际的文本内容。我在接OpenAI时就遇到过所以流式处理代码里必须判空否则最后一个chunk直接NPE。3.3 把流式输出接到Web接口SseEmitter实践在Spring Boot项目中最常见的做法是返回SseEmitter给前端前端用EventSource或者fetch的ReadableStream去读。下面是一个完整的示例。先定义ControllerRestController public class ChatController { private final StreamingChatModel streamingChatModel; public ChatController(StreamingChatModel streamingChatModel) { this.streamingChatModel streamingChatModel; } GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamChat(RequestParam String message) { SseEmitter emitter new SseEmitter(60_000L); // 60秒超时 Prompt prompt new Prompt(message); FluxChatResponse stream streamingChatModel.stream(prompt); stream.doOnNext(response - { String chunk response.getResult().getOutput().getContent(); if (chunk ! null) { emitter.send(SseEmitter.event().data(chunk)); } }) .doOnComplete(() - { emitter.complete(); }) .doOnError(error - { emitter.completeWithError(error); }) .subscribe(); return emitter; } }要点拆解produces MediaType.TEXT_EVENT_STREAM_VALUE声明这个接口返回的是SSE流。SseEmitter是Spring MVC原生支持的异步返回值底层用Servlet异步线程处理不会阻塞Tomcat线程池。通过emitter.send()把每个chunk实时推给前端前端收到一条就渲染一条。流结束时调用emitter.complete()出错时调用emitter.completeWithError()让前端知道流已经结束。subscribe()要最后调用因为它会触发整个响应式流开始执行。整条链都是惰性的没有订阅者就不会真正发起模型调用。这里提醒一点如果你的项目用的是Spring WebFlux而非Spring MVC更推荐直接返回FluxChatResponse让框架自己处理SSE序列化GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString streamChat(RequestParam String message) { return streamingChatModel.stream(new Prompt(message)) .map(response - response.getResult().getOutput().getContent()); }用Flux返回的好处是不用手动管理SseEmitter的生命周期框架会在流结束时自动完成响应。缺点是对异常的处理不如SseEmitter灵活如果你需要精细控制每个事件的结构比如增加事件类型、自定义事件idSseEmitter会更顺手。3.4 Consumer接口配置流式响应的另类用法StreamingChatModel除了stream(Prompt)之外还有一个带Consumer参数的默认方法。这个Consumer可以拿到每个chunk里的token列表。比如OpenAI的实现中chunk里的token可能通过response.getResult().getOutput().getMetadata().get(tokens)获取。这个接口主要面向那些想直接操作原始token流的场景比如做实时翻译、做流式敏感词过滤。大多数业务项目用不到知道有这个东西就行。4. 从0到1的实战全流程完整代码与配置前面讲了原理和API这一节我带你完整搭建一个既能同步对话、又能流式对话的Spring Boot服务。这里用OpenAI做示例但换成本地Ollama或国产模型也只是改依赖和配置的问题。4.1 工程初始化与依赖引入创建一个Spring Boot项目Java版本建议17。在pom.xml里加入Spring AI的依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.4/version relativePath/ /parent properties spring-ai.version1.0.0/spring-ai.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement这里最关键的坑就是Spring AI的依赖管理必须通过BOM引入否则依赖版本对不上启动时各种类找不到。而且Spring AI 1.x要求Spring Boot 3.x别用Spring Boot 2.x去尝试会直接启动失败。如果你要接本地Ollama把starter换成dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama-spring-boot-starter/artifactId /dependency4.2 application.yml配置配置文件里需要指定API Key、模型名称、基础URL。以OpenAI为例spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: https://api.openai.com chat: options: model: gpt-4o-mini temperature: 0.8如果你用的模型服务商有兼容OpenAI的API比如有些国产模型服务商提供了OpenAI兼容端点可以配置base-url指向他们的地址。这个base-url配置是我经常用的等于一套代码能对接各种兼容OpenAI协议的服务。4.3 编写对话服务完整服务类如下包含同步和流式两个核心方法Service public class ChatService { private final ChatModel chatModel; private final StreamingChatModel streamingChatModel; public ChatService(ChatModel chatModel, StreamingChatModel streamingChatModel) { this.chatModel chatModel; this.streamingChatModel streamingChatModel; } /** * 同步对话 */ public String chatSync(String message) { Prompt prompt new Prompt(message); ChatResponse response chatModel.call(prompt); return response.getResult().getOutput().getContent(); } /** * 流式对话返回Flux给Web层 */ public FluxString chatStream(String message) { Prompt prompt new Prompt(message); return streamingChatModel.stream(prompt) .mapNotNull(response - { if (response.getResult() null) { return null; } String content response.getResult().getOutput().getContent(); return (content null || content.isEmpty()) ? null : content; }); } }注意我用的是mapNotNull而不是map这一步就把那些不包含文本内容的chunk比如统计信息过滤掉了省得Controller还要再判空。4.4 测试验证启动服务后用curl测试同步接口curl -X GET http://localhost:8080/chat/sync?message用一句话介绍你自己测试流式接口curl -N -X GET http://localhost:8080/chat/stream?message讲讲Spring AI加-N参数是关键它告诉curl不要缓冲输出实时显示服务端推送的内容。你会看到文字一个词一个词地蹦出来这就是SSE流式输出的效果。5. 我踩过的坑常见问题与排查技巧这一节是全文最有价值的部分全部来自我在生产环境里实际踩过的坑。整理成速查表你再遇到类似问题可以直接对号入座。5.1 常见问题速查表现象可能原因解决方案启动报NoUniqueBeanDefinitionException容器里有多个ChatModel实现使用Qualifier指定Bean名称或用Primary标记默认实现流式接口返回503/超时忘了调subscribe()触发订阅检查是否在方法最后调用了subscribe()流式前端只收到一条完整消息浏览器EventSource不支持SSE的某些content-type确认Controller的produces值为text/event-stream且前端用的解析库正确调call()时一直卡住几秒没反应同步调用本质就是等模型完整回复确认业务场景确实需要同步如果交互场景建议改流式最后一个chunk导致NPEChatResponse.getResult()为null流式处理加判空或改用mapNotNull中文乱码响应content-type缺少charsetUTF-8SseEmitter事件手动设置MediaType.TEXT_EVENT_STREAM并确保编码UTF-8生成内容在content字段里为null部分chunk只包含metadata信息确认filter过滤逻辑只处理非空content的chunk请求401 UnauthorizedAPI Key没配置或配置错误优先使用环境变量方式注入API Key避免硬编码在配置文件中5.2 流式响应中断的排查思路实际项目中用得最多、问题也最多的就是流式响应中断。现象是前端收到一半文字连接就断了或者页面一直转圈到最后也没等来[DONE]。按照下面的顺序排查先看服务端日志有没有doOnError打印的异常。最常见的是模型服务商返回的超时或者额度错误这类错误大多是配置问题。看是不是数据库或其他IO操作阻塞了Reactor的EventLoop线程。我在真实项目里遇到过这个问题在doOnNext回调里做了数据库写入写库偶尔慢到几百毫秒导致整个流式输出被拖累。正确做法是不要把耗时操作直接放在doOnNext里可以用publishOn切换到专门的弹性线程池再执行stream.publishOn(Schedulers.boundedElastic()) .doOnNext(response - saveToDb(response)) .subscribe();检查前端有没有正确消费SSE流。很多情况下后端一直在推送是前端EventSource没正确处理message事件导致看起来像断流。5.3 流式模式下Token统计的坑同步调用直接能从response.getMetadata().getUsage()拿到token统计但流式模式下情况完全不同。有的模型把usage放在最后一个chunk里有的则完全不返回。如果你要做流式接口的token统计可以采用两种方案一是自己数流入的内容字符串长度粗略估算token数中文大约1个token一个字符英文大约4个字符一个token这个方法有误差但做成本估算够用。二是在流结束时专门捕获包含usage的chunk。OpenAI的流式API会在最后一个chunk里返回usage但该chunk的choices通常为空数组。这种情况在Spring AI的解析结果里就表现为getResult()为null恰好对应了上面的判空逻辑。做一个漏桶收集起来AtomicReferenceTokenUsage usageRef new AtomicReference(); stream.doOnNext(response - { TokenUsage usage response.getMetadata().getUsage(); if (usage ! null) { usageRef.set(usage); } }).doOnComplete(() - { System.out.println(总token统计: usageRef.get()); }).subscribe();5.4 流式Flux的线程模型和性能注意事项StreamingChatModel返回的Flux默认运行在Reactor的Netty EventLoop线程上。如果你在doOnNext里做大量CPU密集型计算或者阻塞IO这会占用EventLoop线程导致同一个JVM里的其他响应式请求卡顿。我的实践经验是凡是涉及阻塞操作如数据库写入、外部API调用都用publishOn(Schedulers.boundedElastic())把后续操作切换到弹性线程池。这样可以保证流式数据处理与响应式核心线程互不干扰。但注意publishOn不是万能的它只管切下游线程如果整条链只有一次publishOn放在链的靠前位置效率更好避免在链尾才切换线程导致链路中间耗时影响了EventLoop。5.5 关于版本选型的建议Spring AI 1.x在正式发布前经历了很长的里程碑阶段0.8.x、0.9.x、1.0.0-M1到M6API变动非常频繁。我见过不少同学的代码是从老版本博客抄来的在1.x下直接编译不过。目前生产环境建议直接使用1.0.0 GA以上版本API已经稳定官方文档也同步更新了。如果你项目里用的是0.8.x这种老版本升级到1.x要注意几个不兼容点ChatClient的构建方式从ChatClient.create()变成了ChatClient.builder(chatModel)的方式并且加入了ChatClient.Builder作为Spring Bean注入。部分类名和包名发生了变化OpenAI专属的OpenAiChatOptions从org.springframework.ai.openai迁移到了org.springframework.ai.openai.api下。各厂商starter的GAV坐标统一为spring-ai-xxx-spring-boot-starter格式老版本里的spring-ai-openai独立依赖不推荐继续使用。升级前务必看一眼官方releasenotesCHANGELOG别盲从网上老教程。6. 什么时候用ChatModel什么时候用StreamingChatModel选错模型接口是很多初学者的通病。把这个问题单独讲一下因为这是实际项目架构设计时要拍板的事情。我用一个简单的判断标准用户能不能接受等待完整回复这个交互模式。如果你的接口是给内部系统调用的比如代码生成工具、定时任务、后台批量处理用户不直接面对模型的输出那就用ChatModel同步调用。代码简单返回值清晰出错了直接抛异常调试方便。如果用户直接面对模型输出尤其是聊天对话、写作助手、AI搜索引擎这类交互场景必须用StreamingChatModel。同步模式下用户等3-5秒才看到回复心理体验差别巨大流式模式下用户能在1秒内看到第一个字后续内容逐渐展开感知延迟几乎为零。还有一种混合场景你的流式接口需要在最终回复完整内容落库同时又要实时把内容推给用户。这种时候以StreamingChatModel为主在流链的末尾把完整内容汇总后异步落库正如第3.2节演示的那样。不要试图用同步接口快而全地解决问题那只会牺牲用户体验。重要建议在一个项目中建议业务层依赖StreamingChatModel而不是ChatModel。因为如果你做的产品形态是对话式的哪怕今天某个场景只用了同步调用明天产品经理很可能要求改成流式依赖StreamingChatModel可以让你用同样的代码直接切换代价最小。7. 实际操作中我的一些体会最后分享几个我在实际项目中用出来的经验不一定写在官方文档里但都是实打实好用的小技巧。第一流式接口一定要给SseEmitter设超时时间并且超时时间要比模型最大响应时间稍微长一点点。我之前设过30秒结果模型偶尔思考久一点就提前断连前端一片红。后来统一改成60秒并配合前端的心跳重连机制稳了很多。如果你用Ollama跑本地大模型推理慢是常态超时时间建议直接120秒。第二如果做SSE推送前端只写EventSource是不够的。原生EventSource不支持自定义Header比如带Token验证很多项目会因为这个卡住。解决办法有两个要么把鉴权信息放在URL的query参数里注意别把敏感key放进去要么前端改用fetch ReadableStream手动解析SSE。第三使用Spring AI的Prompt Template可以极大减少拼接Prompt的重复代码。1.x版本里PromptTemplate依然可用它基于StringTemplate引擎支持占位符替换PromptTemplate template new PromptTemplate(请写一篇关于{topic}的短文要求{requirement}); Prompt prompt template.create(Map.of(topic, Spring AI, requirement, 简洁易懂));如果你的业务有大量固定模板的场景建议尽早用起来别在代码里手写字符串拼接。第四也是最重要的一条不要把API Key写在application.yml里提交到代码仓库。哪怕只是个人项目也建议用环境变量或配置中心管理。这不是危言耸听我见过太多因为key泄露被刷爆账单的案例。整个ChatModel和StreamingChatModel的使用就讲到这里。Spring AI 1.x系列还会继续下一篇我会写关于ChatClient这个更高层的封装工具它能让你的业务代码再少一半。如果这篇文章对你有帮助欢迎在评论区交流你踩过的坑。
返回列表