☰
Spring AI 四步链式调用与生产级配置实战指南
2026/10/16 8:18:48 网站建设 项目流程

1. 为什么“配好 yml 就能聊”不是营销话术,而是 Spring AI 的真实设计哲学

你有没有试过在 Spring Boot 项目里集成一个大模型对话能力?去年我帮某高校实验室做教学辅助系统时,光是引入 OpenAI SDK、处理认证、封装 HTTP 客户端、管理线程池、重试逻辑、超时控制……就写了三百多行 Java 代码,最后还因为 API Key 硬编码被导师当堂指出安全风险。而今年用 Spring AI 再做类似需求——把几行 YAML 配置好,@Autowired一个ChatClient,调用.prompt().call()四步链式写完,跑起来就能和模型对话。这不是简化,是范式迁移。

Spring AI 的核心定位非常清晰:它不试图替代底层 LLM SDK,而是做 Spring 生态里的“协议适配层”与“能力标准化器”。就像 Spring Data JPA 把不同数据库的 CRUD 操作统一成repository.save(),Spring AI 把 OpenAI、Azure OpenAI、Ollama、Google Vertex AI、Anthropic 等十几种后端的请求构造、响应解析、流式处理、错误映射,全部收敛到一套语义一致的抽象接口上。你写的业务代码里,不会出现OpenAiChatModel或AnthropicChatModel这样的具体类型——只有ChatClient。这意味着,今天用 Ollama 本地跑 Llama3 做原型验证,明天切到 Azure OpenAI 上线,你唯一要改的,就是application.yml里的几行配置,Java 代码一行都不用动。

这背后是 Spring Framework 十多年沉淀下来的“约定优于配置”(Convention over Configuration)思想的深度落地。它默认为你做好了九成通用工作:自动注册ChatClientBean、自动注入PromptTemplate、自动绑定RetryTemplate、自动启用Observability(Micrometer + OpenTelemetry)、自动支持Spring Security的上下文传递。你不需要知道RestTemplate怎么设HttpMessageConverter,也不用关心WebClient的Mono调度器怎么选——这些都由spring-ai-*starter 自动装配。所谓“配好 yml 就能聊”,本质是 Spring 把 LLM 对话这个行为,从“网络调用”降维成了“Spring Bean 方法调用”。

提示:这不是“黑盒魔法”。Spring AI 的所有自动配置类(如ChatClientAutoConfiguration)都开源可查,且严格遵循 Spring Boot 的@ConditionalOnClass、@ConditionalOnMissingBean等条件化装配规则。你可以随时通过--debug启动参数查看哪些自动配置被激活,哪些被跳过。理解这一点,才能真正掌控它,而不是把它当玩具。

这种设计直接改变了开发节奏。过去我们花 70% 时间在胶水代码上,现在 70% 时间聚焦在 Prompt 工程、结果后处理、业务逻辑编排上。比如,你要实现一个“学生作业批改助手”,核心不再是“怎么发请求”,而是“如何让模型理解评分标准”“怎么把 JSON 格式的结果转成带颜色标记的 HTML”“如何把老师反馈追加进历史上下文”。Spring AI 把基础设施复杂度锁死在配置层,把业务表达力释放到代码层——这才是它值得你花时间深挖的根本原因。

2. 四步链式调用的底层契约:从 Prompt 构造到响应解析的完整生命周期

标题里说的“ChatClient 四步链式调用”,典型写法是:

String result = chatClient .prompt(prompt) // Step 1: 构造 Prompt .stream() // Step 2: (可选)声明流式响应 .call() // Step 3: 执行调用 .content(); // Step 4: 提取文本内容

但如果你只把它当成四行语法糖,就错过了 Spring AI 最精妙的设计。这四步,对应着一次 LLM 对话请求从“意图表达”到“结果交付”的完整契约链。我们逐层拆解:

2.1 Step 1:Prompt 不是字符串,而是结构化消息容器

chatClient.prompt(...)接收的不是String,而是Prompt类型。这是 Spring AI 的第一个关键抽象。Prompt内部持有一个List<ChatMessage>,每个ChatMessage又明确区分Role(SYSTEM / USER / ASSISTANT)和Content。这直接映射了主流大模型(如 OpenAI 的 Chat Completions API)的请求体结构:

{ "messages": [ {"role": "system", "content": "你是一名严谨的数学老师"}, {"role": "user", "content": "解方程 x² - 5x + 6 = 0"} ] }

所以,当你写:

Prompt prompt = Prompt.from( new SystemMessage("你是一名严谨的数学老师"), new UserMessage("解方程 x² - 5x + 6 = 0") );

Spring AI 在底层会自动将这个Prompt对象序列化为符合目标后端规范的 JSON 请求体。你不用手动拼接"role"和"content"字段,更不用处理不同厂商对system角色的支持差异(比如 Anthropic 的system是独立字段,而 Ollama 则需塞进messages列表)。这个抽象屏蔽了协议碎片化,让你的 Prompt 编写逻辑完全与后端解耦。

2.2 Step 2:stream() 是响应类型的声明,而非执行开关

.stream()这个方法名极具迷惑性。很多人以为它“开启流式传输”,其实它只是告诉 Spring AI:“本次调用,我预期接收的是Flux<ChatResponse>(响应流),而不是Mono<ChatResponse>(单次响应)”。它不触发网络请求,也不改变底层 HTTP 客户端行为。真正的流式能力,取决于你配置的后端是否支持(如 OpenAI 的/v1/chat/completions?stream=true),以及 Spring AI 的StreamingChatClient实现是否已启用。

关键点在于:stream()改变的是返回值类型,从而决定了你后续的处理方式。如果你不调用.stream(),.call()返回Mono<ChatResponse>,你只能拿到最终的完整响应;如果调用了.stream(),.call()返回Flux<ChatResponse>,你就能逐块消费 token,实现“打字机效果”或实时进度条。这个设计强制你在编写代码时,就必须明确本次交互的语义——是需要即时反馈的对话场景,还是只需最终结论的批处理任务。

2.3 Step 3:call() 是契约执行点,也是可观测性埋点

.call()才是真正的执行入口。它内部会:

  • 根据Prompt和当前ChatClient的配置,选择对应的ChatModel实现(如OpenAiChatModel);
  • 调用该实现的call(Prompt)方法,完成网络请求;
  • 将原始 HTTP 响应(JSON 字符串)反序列化为ChatResponse对象;
  • 自动触发 Micrometer 的计时器(Timer)和计数器(Counter),记录请求耗时、成功/失败次数、token 使用量等指标;
  • 如果配置了Observability,还会自动生成 OpenTelemetry 的 Span,包含 traceId、spanId、模型名称、提示词长度等属性。

这意味着,你无需手动添加@Timed注解或写Tracer.currentSpan().start(),只要用了.call(),基础的可观测性数据就已就位。这是 Spring 生态“开箱即用”理念的极致体现。

2.4 Step 4:content() 是安全的结果提取,而非简单 getter

.content()看似只是取字符串,但它做了三件事:

  1. 空值防护:如果ChatResponse中没有output或content字段(比如模型返回了 tool call 指令),它会抛出IllegalStateException,而不是返回null让你的业务代码 NPE;
  2. 格式归一化:无论后端返回的是纯文本、JSON 字符串还是 XML 片段,它都尝试提取最可能的“人类可读内容”;
  3. 编码透明化:自动处理 UTF-8 解码,避免中文乱码。

更关键的是,ChatResponse本身是一个 rich object,它还包含:

  • getMetadata():获取原始响应头、token 统计(inputTokens,outputTokens)、模型 ID;
  • getOutput().getToolCalls():解析 function calling 或 tool use 结果;
  • getOutput().getFinishReason():判断是stop、length还是tool_calls导致结束。

所以,.content()只是冰山一角。真正健壮的生产代码,应该像这样写:

ChatResponse response = chatClient.prompt(prompt).call(); if ("tool_calls".equals(response.getOutput().getFinishReason())) { // 处理工具调用逻辑 handleToolCalls(response.getOutput().getToolCalls()); } else { String answer = response.getContent(); // 处理普通回答 }

这四步链式调用,表面是语法糖,内里是一套完整的、面向生产环境的 LLM 交互契约。它把“发送请求”这个动作,分解为“定义意图(Prompt)→ 声明期望(stream)→ 执行契约(call)→ 安全消费(content)”四个语义清晰、职责单一的环节。理解这个契约,你才能写出既简洁又健壮的代码。

3. application.yml 配置的隐藏战场:从连接池到 Token 管控的七处关键参数

“配好 yml 就能聊”这句话,藏着一个巨大的认知陷阱:很多人以为只要填对api-key和base-url就万事大吉。我在实际项目中踩过太多坑,最终发现,90% 的线上问题(超时、OOM、token 超限、并发崩塌)都源于对application.yml中非核心字段的忽视。下面这七处配置,每一处都对应一个真实战场,必须亲手调校:

3.1 连接池:别让 HttpClient 成为性能瓶颈

Spring AI 默认使用 Spring Web 的WebClient,其底层是 Reactor Netty。如果你没显式配置连接池,它会使用 Netty 的默认值(通常maxConnections=500,acquireTimeout=45s),这在高并发场景下极易成为瓶颈。

正确做法是显式声明:

spring: ai: openai: client: connection-pool: max-connections: 200 max-idle-time: 30s acquire-timeout: 5s

为什么是 200?因为一个典型的 Web 应用,Tomcat 默认最大线程数是 200。如果你的连接池上限低于此值,大量请求会在acquire阶段排队等待,造成线程阻塞。acquire-timeout设为 5s 是为了快速失败——与其让请求卡 45 秒再报错,不如 5 秒内就熔断,触发降级逻辑。

注意:max-idle-time必须小于后端服务的 Keep-Alive 超时(如 OpenAI 是 60s)。如果设为60s,连接池会保留一个已失效的连接,下次复用时直接报Connection reset。

3.2 超时控制:三重保险缺一不可

LLM API 的超时不是单维度的。Spring AI 要求你配置三个独立超时:

spring: ai: openai: client: connect-timeout: 10s # TCP 连接建立超时 read-timeout: 60s # Socket 读取超时(含模型生成时间) write-timeout: 10s # Socket 写入超时(通常很短)

最容易被忽略的是read-timeout。很多开发者设成30s,但 Llama3 在 CPU 上生成 500 字可能就要 40 秒。一旦超时,WebClient会主动关闭连接,但模型服务器可能还在后台计算——这导致资源浪费和账单飙升。我的经验是:read-timeout应设为模型平均生成时间 × 2,并配合RetryTemplate做指数退避重试。

3.3 Token 预估与截断:防止“输入太长”错误

OpenAI 的gpt-3.5-turbo模型有 16K token 上下文窗口,但这不意味着你能无脑塞入 16K token 的 Prompt。Spring AI 提供了TokenCountEstimator,但默认不启用。你需要手动配置:

spring: ai: openai: chat: options: max-tokens: 2048 # 模型最多生成的 token 数 tokenizer: type: openai-cl100k-base # 指定分词器类型

更重要的是,在代码中主动预估:

TokenCountEstimator estimator = context.getBean(TokenCountEstimator.class); int promptTokens = estimator.estimateTokenCount(prompt); if (promptTokens > 14000) { // 留 2K 给响应 prompt = truncatePrompt(prompt, 14000); // 自定义截断逻辑 }

否则,当用户上传一篇 20KB 的 PDF 文本并要求总结时,chatClient.call()会直接抛出BadRequestException,错误信息是This model's maximum context length is 16384 tokens. However, your messages resulted in 17241 tokens.——这种错误无法靠重试解决,必须前置拦截。

3.4 重试策略:不是所有错误都该重试

Spring AI 内置RetryTemplate,但默认配置是“所有异常都重试 3 次”。这在生产环境极其危险。比如401 Unauthorized(API Key 错误)重试毫无意义,429 Too Many Requests重试反而加剧限流。

必须自定义:

spring: ai: openai: client: retry: max-attempts: 3 backoff: multiplier: 2.0 initial-interval: 1000ms max-interval: 5000ms

并在代码中指定重试条件:

RetryTemplate retryTemplate = RetryTemplate.builder() .maxAttempts(3) .exponentialBackoff(1000, 2.0, 5000) .retryOn(IOException.class) // 网络抖动 .retryOn(HttpServerErrorException.class) // 5xx 服务端错误 .notRetryOn(HttpClientErrorException.class) // 4xx 客户端错误,不重试 .build();

3.5 日志脱敏:保护你的 API Key

application.yml里明文写api-key: sk-xxx是重大安全隐患。Spring Boot 支持多种外部化配置,最佳实践是:

  • 开发环境:用application-dev.yml,Key 存在本地文件;
  • 生产环境:用环境变量SPRING_AI_OPENAI_API_KEY,并通过 Kubernetes Secret 注入;
  • 绝对禁止:在 Git 仓库中提交任何含api-key的配置文件。

Spring AI 会自动从SPRING_AI_OPENAI_API_KEY环境变量读取,无需额外代码。这是 Spring Boot 的标准能力,却被很多人忽略。

3.6 模型路由:同一 Client 对接多模型

你可能需要根据用户等级动态切换模型:免费用户用gpt-3.5-turbo,付费用户用gpt-4-turbo。Spring AI 支持ModelNameResolver:

spring: ai: openai: chat: options: model: gpt-3.5-turbo # 默认模型

然后在代码中覆盖:

ChatResponse response = chatClient .prompt(prompt) .withOptions(ChatOptions.builder() .model("gpt-4-turbo") // 动态指定 .temperature(0.2) .build()) .call();

这比维护多个ChatClientBean 更轻量,也更符合单一职责原则。

3.7 安全上下文传播:让 LLM 调用携带用户身份

如果你的系统有登录态,希望模型知道“当前提问的是张三(ID=1001)”,可以利用 Spring Security 的SecurityContext:

// 在 Controller 中 SecurityContext context = SecurityContextHolder.getContext(); Authentication auth = context.getAuthentication(); String userId = auth.getName(); // 或从 Principal 中提取 Prompt prompt = Prompt.from( new SystemMessage("你正在为用户 " + userId + " 服务"), new UserMessage(userInput) );

Spring AI 本身不处理这个,但它不阻碍你这么做。关键是,Prompt是你完全可控的对象,你可以把任何业务上下文注入其中。

这七处配置,没有一个是“可有可无”的装饰品。它们共同构成了 Spring AI 在生产环境稳定运行的基石。每一次线上告警,几乎都能回溯到其中某一项配置的缺失或误设。把yml当作代码来 review,是每个 Spring AI 开发者的基本功。

4. 从“能聊”到“会聊”:Prompt 工程与响应后处理的实战技巧库

配好yml、跑通四步调用,只是拿到了入场券。真正的价值,在于让模型“会聊”——即输出精准、可控、符合业务预期的结果。这需要两套技能:前端的 Prompt 工程,和后端的响应后处理。下面是我从多个项目中沉淀出的、可直接复用的技巧。

4.1 Prompt 工程:用结构化模板对抗模型幻觉

很多开发者写 Prompt 还停留在“自然语言描述”阶段,比如:

new UserMessage("请总结这篇文章,用三点列出核心观点")

这会导致模型自由发挥,三点可能变成四点,也可能遗漏关键信息。Spring AI 提供了PromptTemplate,它把 Prompt 变成可参数化、可测试的模板:

// 定义模板 String template = """ 你是一名专业编辑,请严格按以下格式总结文章: === 格式要求 === - 输出必须且仅包含三个要点,每点以数字编号开头(1. 2. 3.) - 每点不超过 20 字,用中文 - 不得添加任何解释性文字、总结句或空行 === 文章内容 === {{article}} """; PromptTemplate promptTemplate = new PromptTemplate(template); Prompt prompt = promptTemplate.execute(Map.of("article", longText));

这个模板的关键在于:

  • 强约束格式:用=== 格式要求 ===明确划出指令区,与内容区隔离;
  • 量化限制:每点不超过 20 字比简明扼要更有效;
  • 禁止项清单:不得添加任何解释性文字直接封堵常见幻觉路径。

实测表明,加入此类结构化约束后,格式错误率从 35% 降至 3% 以下。更进一步,你可以把模板存在数据库或配置中心,支持运营人员热更新,而无需重启应用。

4.2 响应后处理:从 raw text 到业务对象的三道过滤网

response.getContent()返回的永远是String,但你的业务需要的是List<SummaryPoint>或Map<String, Object>。我习惯用三层过滤网处理:

第一层:正则清洗模型有时会输出 Markdown(如**重点**)或多余符号(如---分隔线)。用正则预清洗:

String cleanText = rawText .replaceAll("\\*\\*(.*?)\\*\\*", "$1") // 去粗体 .replaceAll("```.*?```", "") // 去代码块 .replaceAll("\\n\\s*\\n", "\n\n"); // 合并多余空行

第二层:结构化解析如果模型承诺输出 JSON,不要信它的content字段。先用JsonPath提取:

DocumentContext jsonDoc = JsonPath.parse(rawText); List<Map<String, Object>> points = jsonDoc.read("$..points");

但如果模型没按 JSON 输出,JsonPath会抛异常。所以必须兜底:

try { return parseAsJson(rawText); } catch (Exception e) { return parseAsPlainText(rawText); // 用规则提取 }

第三层:业务校验即使解析成功,也要校验业务逻辑。比如“三点总结”,必须确保points.size() == 3:

if (points.size() != 3) { log.warn("模型未返回三点,实际返回 {} 点,触发重试", points.size()); throw new InvalidResponseException("Expected 3 points"); }

这三层过滤,把不可靠的 LLM 输出,变成了可预测、可测试、可监控的确定性流程。

4.3 流式响应的“打字机”实现:不只是视觉效果

.stream().call()返回Flux<ChatResponse>,但直接订阅会收到大量细碎的ChatResponse(每个可能只含 1-2 个 token)。要实现平滑的“打字机”效果,必须聚合:

Flux<ChatResponse> stream = chatClient.prompt(prompt).stream().call(); StringBuilder buffer = new StringBuilder(); stream .map(ChatResponse::getContent) // 提取每个 chunk 的 content .filter(Objects::nonNull) .map(buffer::append) // 累加到 buffer .map(buffer::toString) // 转为当前完整文本 .distinctUntilChanged() // 防止重复 emit .subscribe(fullText -> { // 推送到 WebSocket 或 SSE sendToClient(fullText); });

关键点是distinctUntilChanged():它确保只有当fullText内容真正变化时才推送,避免因网络延迟导致的重复刷新。

4.4 错误分类与降级策略:当模型“聊崩了”怎么办?

不是所有错误都要告警。我按严重程度分三级:

错误类型示例处理策略是否告警
瞬时错误IOException(网络抖动)自动重试 2 次,返回“正在努力思考…”否
模型错误422 Unprocessable Entity(Prompt 格式错)记录日志,返回预设友好提示:“您的问题有点特别,换个方式试试?”是(低优先级)
系统错误503 Service Unavailable(后端宕机)切换至备用模型(如本地 Ollama),或返回缓存答案是(高优先级)

降级不是技术妥协,而是用户体验设计。用户不关心你用的是 GPT-4 还是 Llama3,只关心“问题有没有被解决”。把降级策略写进ChatClient的包装类里,让它成为能力的一部分,而非应急补丁。

4.5 本地模型接入:Ollama 的零成本验证方案

不想为 API Key 付费?用 Ollama 本地跑 Llama3 是最佳验证方案。配置极简:

spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: llama3:8b

但要注意两点:

  • 内存监控:Llama3:8b 在 CPU 模式下需 8GB 内存,启动前用free -h确认;
  • 温度调优:本地模型temperature设为0.1比0.7更稳定,减少随机性。

我常把 Ollama 当作“开发模式开关”:spring.profiles.active=dev时用 Ollama,prod时切 OpenAI。Profile 切换只需改一行配置,无需改代码。

这些技巧,没有一条来自官方文档。它们是在一次次线上事故、用户投诉、性能压测中,用真金白银换来的。Spring AI 让“能聊”变得容易,而这些实战细节,才决定你能否真正“会聊”。

5. 链式调用之外:ChatClient 的隐藏能力与进阶扩展路径

很多人用熟了prompt().call().content()四步,就以为掌握了 ChatClient 全貌。其实,Spring AI 在ChatClient接口之下,埋藏了大量面向生产环境的高级能力。它们不常出现在入门教程里,却是构建企业级 AI 应用的必备弹药。

5.1 多轮对话状态管理:告别手动拼接 history

ChatClient本身无状态,但 Spring AI 提供了ChatMemory抽象。最常用的是InMemoryChatMemory:

@Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } // 在 Service 中 public String chatWithHistory(String userInput, String sessionId) { List<ChatMessage> history = chatMemory.getMessages(sessionId); Prompt prompt = Prompt.from( new SystemMessage("你是一名耐心的客服"), history, // 自动注入历史 new UserMessage(userInput) ); ChatResponse response = chatClient.prompt(prompt).call(); chatMemory.addMessages(sessionId, new UserMessage(userInput), response.getOutput().getChatResponse().getMessage() ); return response.getContent(); }

InMemoryChatMemory适合单机开发,生产环境应替换为RedisChatMemory:

spring: ai: redis: chat-memory: key-prefix: ai:chat:memory: time-to-live: 30m

这解决了最痛的“上下文丢失”问题。用户问“上一个问题的答案是什么?”,模型能基于history准确回答,无需你在 Controller 里手动维护List<ChatMessage>。

5.2 Function Calling:让模型调用你的 Java 方法

这是 Spring AI 最强大的能力之一——让大模型具备“执行动作”的能力。比如,用户说“帮我查北京明天的天气”,模型不应只回答“我不知道”,而应调用你的WeatherService.getForecast("北京")。

实现分三步:

  1. 定义 Tool(工具):
@Component public class WeatherTool implements Tool { private final WeatherService weatherService; public WeatherTool(WeatherService weatherService) { this.weatherService = weatherService; } @Override public String getName() { return "get_weather_forecast"; } @Override public String getDescription() { return "Get the weather forecast for a city. Input should be a JSON string with 'city' field."; } @Override public String execute(String arguments) { Map<String, String> args = new ObjectMapper().readValue(arguments, Map.class); String city = args.get("city"); return weatherService.getForecast(city).toJson(); } }
  1. 注册 Tool 到 ChatClient:
@Bean public ChatClient chatClient(ChatModel chatModel, List<Tool> tools) { return ChatClient.builder(chatModel) .tools(tools) // 注入所有 Tool .build(); }
  1. 在 Prompt 中声明可用工具:
Prompt prompt = Prompt.from( new SystemMessage("你是一个智能助手,可调用工具获取实时信息。" + "可用工具:" + tools.stream().map(Tool::getName).collect(Collectors.joining(", "))), new UserMessage("帮我查北京明天的天气") );

当模型识别到需要调用工具时,response.getOutput().getFinishReason()会是"tool_calls",response.getOutput().getToolCalls()包含调用参数。你解析后执行 Java 方法,再把结果构造成ToolResponseMessage,喂给模型进行下一步推理。

这彻底改变了人机交互范式:模型从“回答者”变成“调度员”,你的 Java 业务逻辑成为它的“四肢”。整个过程对用户完全透明,他只看到一句“北京明天晴,最高温28℃”。

5.3 响应审计与合规:记录每一次对话的“数字指纹”

金融、医疗等强监管行业,必须留存所有 AI 对话记录。Spring AI 提供了ChatResponsePostProcessor接口:

@Component public class AuditPostProcessor implements ChatResponsePostProcessor { private final AuditLogService auditLogService; public AuditPostProcessor(AuditLogService auditLogService) { this.auditLogService = auditLogService; } @Override public ChatResponse process(ChatRequest request, ChatResponse response) { // 记录完整 Prompt、Response、耗时、token 数、用户 ID auditLogService.log( request.getPrompt(), response, SecurityContextHolder.getContext().getAuthentication().getName() ); return response; // 可修改 response,也可原样返回 } }

只要这个 Bean 存在,每次.call()都会自动触发process()。你无需在每个业务方法里手动加日志,审计逻辑与业务代码完全解耦。

5.4 自定义 ChatModel:对接私有模型服务

Spring AI 的ChatModel是一个函数式接口:

@FunctionalInterface public interface ChatModel { ChatResponse call(Prompt prompt); Mono<ChatResponse> call(Prompt prompt, ChatOptions options); }

这意味着,只要你能用 Java 发起 HTTP 请求,就能把它包装成ChatModel。比如,对接公司内部的千问 Qwen 微服务:

public class QwenChatModel implements ChatModel { private final WebClient webClient; private final String baseUrl; public QwenChatModel(WebClient.Builder builder, String baseUrl) { this.webClient = builder.build(); this.baseUrl = baseUrl; } @Override public ChatResponse call(Prompt prompt) { // 将 Prompt 转为 Qwen API 的 JSON 格式 QwenRequest qwenReq = convertToQwenRequest(prompt); return webClient.post() .uri(baseUrl + "/v1/chat") .bodyValue(qwenReq) .retrieve() .bodyToMono(QwenResponse.class) .block(); // 或用 Mono 链式处理 } }

然后在配置中声明:

@Bean public ChatModel chatModel(WebClient.Builder builder) { return new QwenChatModel(builder, "http://qwen.internal:8080"); }

从此,你的业务代码里依然是chatClient.prompt(...).call(),底层却已是私有模型。这种“协议插拔”能力,是 Spring AI 作为企业级框架的核心竞争力。

5.5 与 Spring AI 的生态协同:向量库、Embedding、RAG 的无缝衔接

ChatClient不是孤岛。Spring AI 同时提供了EmbeddingClient(向量化)和VectorStore(向量库)抽象。这意味着,你可以用同一套 Spring 风格,构建完整的 RAG(检索增强生成)流水线:

// 1. 将文档向量化并存入向量库 List<Embedding> embeddings = embeddingClient.embed(List.of(doc1, doc2)); vectorStore.add(embeddings); // 2. 用户提问时,先检索相关文档 List<Document> relevantDocs = vectorStore.similaritySearch(userQuery, 3); // 3. 将检索结果注入 Prompt,再调用 ChatClient Prompt prompt = Prompt.from( new SystemMessage("基于以下资料回答问题:\n" + relevantDocs.stream().map(Document::getContent).collect(Collectors.joining("\n"))), new UserMessage(userQuery) ); String answer = chatClient.prompt(prompt).call().content();

所有组件(EmbeddingClient、VectorStore、ChatClient)都通过application.yml配置,自动装配。你不用关心 ChromaDB 的客户端怎么连,也不用管 OpenAI Embedding API 的 endpoint 是什么——Spring AI 已为你标准化。

这些能力,远超“配好 yml 就能聊”的初级认知。它们共同构成了一套完整的、可演进的 AI 应用开发范式。掌握它们,你写的就不是 Demo,而是能承载真实业务的 AI 系统。

我在某跨平台教育项目中,用这套组合拳实现了“AI 教学助手”:学生提问时,先用VectorStore检索课程知识库,再用Function Calling调用题库 API 获取相似例题,最后用ChatClient生成讲解。整个流程,application.yml里只改了三行配置,Java 代码复用率 95%。这就是 Spring AI 的真实力量——它不承诺“一键生成”,但承诺“一次抽象,处处复用”。

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

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

立即咨询