SpringAI系列|第2篇:模型集成与对话基础
Spring AI 实战系列|第 2 篇 模型集成与对话基础(ChatClient 深入理解 + 多模型接入 + Prompt 工程化)
📊 本文约 11000 字,阅读时间约 38 分钟。
系列说明:本文是《Spring AI 实战系列》第 2 篇,深入讲解 ChatClient 的完整用法、多模型接入配置,以及 Prompt 工程化实践。
前置知识:已完成第 1 篇,能跑通基础 Spring AI 对话项目。
文章目录
前言
上一篇以十余行代码完成了首个 AI 对话接口的搭建。然而,该示例距离生产环境的要求仍有差距。
实际项目中,开发者通常会面临以下问题:
- 同一系统需同时接入 OpenAI 与 DeepSeek,应如何配置?
- 若希望 AI 回答趋于保守,temperature 参数应如何设置?
- 流式输出的前端对接方式为何?SSE 应如何防范代理缓存?
- Prompt 直接写入代码导致维护困难,应如何进行工程化管理?
- 对话历史持续增长,Token 超限时应如何处理?
上述问题可归纳为三类核心议题:模型接入、对话控制与 Prompt 管理。本文将逐一展开论述。
一、ChatClient 深入理解
1.1 ChatClient vs ChatModel
二者的关系需先予以明确:
// ChatModel — 底层接口,直接对接模型 API
public interface ChatModel {
ChatResponse call(Prompt prompt);
}
// ChatClient — 上层封装,提供 Fluent API
public interface ChatClient {
ChatClientRequestSpec prompt();
}
| 维度 | ChatModel | ChatClient |
|---|---|---|
| 定位 | 底层抽象,直接调模型 API | 上层封装,面向开发者 |
| 功能 | 发送 Prompt,接收响应 | Fluent 链式调用 + 默认配置 |
| 使用场景 | 需要底层控制时 | 日常开发(99% 场景) |
日常开发中建议直接使用 ChatClient,它是 Spring AI 为开发者提供的上层封装接口。仅在需要绕过默认行为、直接操控底层模型时才使用 ChatModel。
1.2 创建方式
方式一:自动注入 Builder(最常用)
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
}
Spring AI 根据 application.yml 配置自动创建 ChatModel,再包装为 ChatClient.Builder 注入。开发者只需调用 .build() 即可完成构建。
方式二:构建时预设默认值
public ChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你是一位资深 Java 开发工程师,回答简洁,必要时提供代码示例")
.defaultOptions(ChatOptions.builder()
.temperature(0.5)
.maxTokens(2048)
.build())
.build();
}
defaultSystem() 与 defaultOptions() 设定的值在每次调用时自动生效,无需重复配置。此为生产环境中最推荐的实践方式:将不变的内容在构建时固定,调用时仅传递变化的部分。
方式三:手动指定 Model(多模型场景)
@Bean
public ChatClient deepSeekClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
@Bean
public ChatClient ollamaClient(@Qualifier("ollamaChatModel") ChatModel ollamaModel) {
return ChatClient.builder(ollamaModel).build();
}
同一项目中接入多个模型时,需手动创建多个 ChatClient Bean,具体方法详见后文。
1.3 核心调用链路
无论以何种方式创建 ChatClient,其调用链路均保持一致:
ChatClient.prompt()
→ .system() / .user() / .messages() // 构建请求内容
→ .options() // 可选:覆盖默认参数
→ .call() 或 .stream() // 选择同步或流式
→ .content() / .chatResponse() // 提取结果

每一步的作用:
| 方法 | 作用 | 返回类型 |
|---|---|---|
.prompt() |
开始构建请求 | ChatClientRequestSpec |
.system(msg) |
设置 System Message(AI 人设) | 同上(链式) |
.user(msg) |
设置 User Message(用户输入) | 同上(链式) |
.messages(list) |
批量设置多条 Message(多轮对话) | 同上(链式) |
.options(opts) |
临时覆盖 temperature/maxTokens 等 | 同上(链式) |
.call() |
同步调用,阻塞等待完整响应 | ChatClientRequest |
.stream() |
流式调用,返回响应式流 | StreamChatClientRequest |
.content() |
提取纯文本内容 | String / Flux<String> |
.chatResponse() |
提取完整响应(含元数据) | ChatResponse / Flux<ChatResponse> |
二、多模型提供商接入
2.1 统一抽象层设计
Spring AI 最核心的设计之一即为统一抽象层。无论底层接入的是 OpenAI、DeepSeek、通义千问还是 Ollama,上层调用代码几乎完全一致:
// 这段代码不管用哪个模型都能跑
String reply = chatClient.prompt()
.user("你好")
.call()
.content();
切换模型仅需修改 application.yml,Java 代码无需任何改动。此即上一篇所述"供应商锁定"问题的解决方案。

架构分层:
- 业务层:只依赖
ChatClient,不关心底层是哪个模型 - 抽象层:
ChatModel接口定义统一契约 - 适配层:各模型提供商的实现类(
OpenAiChatModel、OllamaChatModel等) - 配置层:通过 YAML 切换模型,零代码改动
2.2 OpenAI 接入
OpenAI 作为业界标杆,其 API 格式被后续多个国产模型所兼容,故优先予以介绍。
依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
配置:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.7
max-tokens: 2048
API Key 需在 platform.openai.com 申请。国内用户可能需使用海外信用卡,如不便操作,可参考下文国产替代方案。
完整配置参数参考:OpenAI Chat Properties
2.3 DeepSeek 接入(推荐)
DeepSeek 是实际项目中较为常用的国产替代方案。其 API 完全兼容 OpenAI 格式,价格约为 OpenAI 的 1/10,且中文理解能力更为突出。
依赖:与 OpenAI 使用同一 starter,无需额外添加。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
配置:
spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7
与 OpenAI 配置相比,仅需修改两处:
base-url换成 DeepSeek 的地址model换成deepseek-chat(推理模型用deepseek-reasoner)
申请 Key:前往 platform.deepseek.com 注册,新用户可获得赠送额度。在「API Keys」页面创建 key 后,复制至环境变量即可。
⚠️ API Key 仅显示一次,请妥善保存。如遗失,只能重新创建。
参考:DeepSeek API 文档
2.4 通义千问接入
阿里云的通义千问同样兼容 OpenAI 格式:
spring:
ai:
openai:
api-key: ${DASHSCOPE_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
chat:
options:
model: qwen-plus
模型选择参考:
| 模型 | 特点 | 适用场景 |
|---|---|---|
| qwen-turbo | 速度快,成本低 | 简单对话、高并发场景 |
| qwen-plus | 综合能力均衡 | 日常开发首选 ← 推荐 |
| qwen-max | 能力最强,价格较高 | 复杂推理、高质量输出 |
参考:DashScope 文档
2.5 智谱 GLM 接入
spring:
ai:
openai:
api-key: ${ZHIPU_API_KEY}
base-url: https://open.bigmodel.cn/api/paas/v4
chat:
options:
model: glm-4
前往 open.bigmodel.cn 注册获取 API Key。
2.6 Ollama 本地模型接入
Ollama 是本地运行开源模型的方案,适合内网环境或对数据隐私要求高的场景。它走的是独立协议,不兼容 OpenAI 格式,所以需要单独的 starter。
安装 Ollama:前往 ollama.com 下载安装包,支持 Windows/Mac/Linux。
# 验证安装
ollama --version
# 拉取模型(首次下载需等待)
ollama pull llama3 # Llama 3
ollama pull qwen2 # 通义千问 2
ollama pull deepseek-r1:7b # DeepSeek 推理模型 7B 版本
# 启动服务(默认监听 localhost:11434)
ollama serve
依赖(注意不是 openai 的 starter):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
配置:
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3
⚠️ 常见问题:首次配置 Ollama 时较易引错依赖。若误引 spring-ai-openai-spring-boot-starter,启动时将报找不到 OllamaChatModel Bean 的错误。
本地模型客观评价:
| 优势 | 劣势 |
|---|---|
| 完全免费,无 API 费用 | 效果不如云端大模型 |
| 数据不出本地,隐私安全 | 硬件要求高(建议 16G+ 内存) |
| 不依赖网络,离线可用 | 小模型复杂任务容易翻车 |
| 适合个人学习和内网场景 | 首次下载模型耗时,占用磁盘空间 |
建议:开发测试阶段使用 DeepSeek(成本较低),复杂任务选用 GPT-4o,离线或隐私场景再考虑 Ollama。
2.7 各模型提供商对比

| 模型 | Starter | API 兼容 | 参考价格 | 特色 |
|---|---|---|---|---|
| GPT-4o | openai-starter | 原生 | ~$0.005/1K tokens | 综合最强 |
| DeepSeek-V3 | openai-starter | 兼容 | ~¥0.001/1K tokens | 性价比之王 |
| 通义千问 | openai-starter | 兼容 | ~¥0.002/1K tokens | 中文优化好 |
| 智谱 GLM-4 | openai-starter | 兼容 | ~¥0.002/1K tokens | 国产头部 |
| Ollama/Llama3 | ollama-starter | 独立协议 | 免费(硬件成本) | 本地部署 |
三、多模型共存与切换
3.1 同时配置多个模型
实际项目中经常需要同时接入多个模型。例如:日常对话使用 DeepSeek 以降低成本,复杂推理切换至 GPT-4o 以保证质量,网络故障时 fallback 到 Ollama。
spring:
ai:
# 主力模型 - DeepSeek
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
# 本地备用 - Ollama
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3
3.2 定义多个 ChatClient Bean
@Configuration
public class AiConfig {
/**
* DeepSeek 客户端 — 使用默认装配的 OpenAiChatModel
*/
@Bean("deepSeekClient")
public ChatClient deepSeekClient(ChatClient.Builder builder) {
return builder.build();
}
/**
* Ollama 客户端 — 需要显式指定 OllamaChatModel
*/
@Bean("ollamaClient")
public ChatClient ollamaClient(@Qualifier("ollamaChatModel") ChatModel ollamaModel) {
return ChatClient.builder(ollamaModel).build();
}
}
关键点:Ollama 必须使用 @Qualifier("ollamaChatModel") 显式指定,否则 Spring 会注入默认的 OpenAiChatModel。
3.3 按场景选择模型
@Service
public class AiService {
private final ChatClient deepSeekClient;
private final ChatClient ollamaClient;
public AiService(
@Qualifier("deepSeekClient") ChatClient deepSeekClient,
@Qualifier("ollamaClient") ChatClient ollamaClient) {
this.deepSeekClient = deepSeekClient;
this.ollamaClient = ollamaClient;
}
/** 在线对话 — 高质量回复 */
public String chatOnline(String message) {
return deepSeekClient.prompt().user(message).call().content();
}
/** 离线对话 — 内网/隐私场景 */
public String chatOffline(String message) {
return ollamaClient.prompt().user(message).call().content();
}
}
3.4 动态路由(运行时切换)
有些场景需要在运行时根据用户选择动态切换模型:
@Service
public class DynamicAiService {
private final Map<String, ChatClient> clients;
// Spring 自动将所有 ChatClient Bean 收集到 Map 中,key 是 Bean 名称
public DynamicAiService(Map<String, ChatClient> clients) {
this.clients = clients;
}
public String chat(String modelType, String message) {
ChatClient client = clients.get(modelType);
if (client == null) {
throw new IllegalArgumentException(
"不支持的模型: " + modelType + "。可用模型: " + clients.keySet());
}
return client.prompt().user(message).call().content();
}
}
Controller 层调用:
@RestController
@RequestMapping("/ai")
public class ChatController {
@Autowired
private DynamicAiService dynamicAiService;
// GET /ai/chat?model=deepSeekClient&message=你好
@GetMapping("/chat")
public String chat(@RequestParam String model, @RequestParam String message) {
return dynamicAiService.chat(model, message);
}
}

四、对话参数详解
4.1 temperature(随机性/创造性)
控制 AI 回复的随机程度,取值范围 0 ~ 2:
spring:
ai:
openai:
chat:
options:
temperature: 0.7
| 值域 | 效果 | 适用场景 |
|---|---|---|
| 0.0 ~ 0.3 | 保守、确定、事实性强 | 代码生成、数据提取、格式化输出 |
| 0.4 ~ 0.8 | 平衡 ← 推荐 | 日常对话、通用问答 |
| 0.9 ~ 2.0 | 创意性强 | 写诗、头脑风暴、创意写作 |
个人习惯参考值:业务问答 0.5,代码生成 0.2,创意写作 0.9。
4.2 maxTokens(最大输出长度)
spring:
ai:
openai:
chat:
options:
max-tokens: 2048
控制 AI 单次回复最多生成的 Token 数量。若发现回复被截断,大概率是该值设置过小。
不同模型的上限:
- GPT-4o:单次输出最多 4096 Token
- DeepSeek-V3:单次输出最多 8192 Token
- Claude 3.5 Sonnet:单次输出最多 8192 Token
4.3 topP(核采样)
与 temperature 类似,均用于控制随机性。通常二者只需调整其一:
spring:
ai:
openai:
chat:
options:
top-p: 1.0 # 默认值,从所有 Token 中选取
| 值 | 效果 |
|---|---|
| 0.1 | 只从概率最高的 10% Token 里选,回答非常确定 |
| 1.0 | 从全部 Token 里选,最随机 |
建议:调整 temperature 即可满足大部分需求,topP 保持默认 1.0。
4.4 代码中动态覆盖
有时需要在某次调用时临时修改参数,而无需调整配置文件:
// 创意写作场景 — 临时提高温度
String creative = chatClient.prompt()
.user("写一首关于 Java 的诗")
.options(ChatOptions.builder()
.temperature(1.2)
.maxTokens(500)
.build())
.call()
.content();
// 下一次调用恢复正常默认值
String normal = chatClient.prompt()
.user("什么是 Spring Boot?")
.call()
.content(); // temperature 还是构建时的 0.5
.options() 只影响当前这一次调用,不会改变全局默认值。
五、同步调用与流式输出
5.1 同步调用
String content = chatClient.prompt()
.user(message)
.call() // 阻塞等待完整响应
.content(); // 提取文本
适用场景:短文本回复、后端处理、批量任务。
不足:长文本生成时用户需等待数秒,体验欠佳。
5.2 流式输出
把 .call() 换成 .stream(),返回类型从 String 变成 Flux<String>:
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream() // 关键改动:同步 → 流式
.content(); // 返回 Flux<String>
}
前端用浏览器原生 EventSource API 接收:
const eventSource = new EventSource('/ai/chat/stream?message=你好');
const output = document.getElementById('output');
eventSource.onmessage = (event) => {
output.textContent += event.data; // 文字逐步显示
};
eventSource.onerror = () => {
eventSource.close(); // 流结束时关闭连接
};
效果:文字逐步显示,类似 ChatGPT 的打字机效果。长文本场景下,体验差异颇为显著。
5.3 流式输出获取完整元数据
若需在流式输出的同时获取 Token 消耗等元数据:
@GetMapping(value = "/chat/stream-full", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatResponse> chatStreamFull(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.chatResponse(); // 返回 Flux<ChatResponse>,每个元素包含一段内容 + 元数据
}
前端处理方式稍复杂,需解析 JSON 结构:
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
const content = data.result.output.text;
const usage = data.metadata.usage;
// 实时展示内容,同时记录 Token 用量
};
5.4 流式输出常见问题排查
| 症状 | 排查方向 | 解决方法 |
|---|---|---|
| 前端收不到任何数据 | Controller produces 类型 | 确认是 TEXT_EVENT_STREAM_VALUE |
| 数据一次性全出来 | Nginx/网关缓冲 | 加 proxy_buffering off; |
| 连接中断 | 超时设置 | 增大 readTimeout |
| 中文乱码 | 编码问题 | 确认 charset=UTF-8 |
六、响应元数据获取
AI 返回的不只是文字,还有很有用的元数据。
6.1 Token 消耗统计
ChatResponse response = chatClient.prompt()
.user("介绍一下 Spring AI")
.call()
.chatResponse();
Usage usage = response.getMetadata().getUsage();
System.out.println("输入 Token: " + usage.getPromptTokens());
System.out.println("输出 Token: " + usage.getGenerationTokens());
System.out.println("总 Token: " + usage.getTotalTokens());
按 Token 计费的场景下,该信息尤为重要。可据此进行成本监控和预算告警。
6.2 模型信息确认
String modelName = response.getMetadata().getModel(); // 实际使用的模型名
String responseId = response.getMetadata().getId(); // 本次响应的唯一 ID
多模型切换时,使用 modelName 确认实际调用的模型,防止配置错误。
6.3 完整工具类示例
将上述内容整合,构建一个完整的 Service:
@Service
public class AiChatService {
private final ChatClient chatClient;
public AiChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你是一位资深 Java 开发工程师,回答简洁,必要时提供代码示例")
.defaultOptions(ChatOptions.builder()
.temperature(0.5)
.maxTokens(2048)
.build())
.build();
}
/** 普通对话 */
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
/** 带自定义角色的对话 */
public String chatWithRole(String role, String message) {
return chatClient.prompt()
.system("你是一位" + role)
.user(message)
.call()
.content();
}
/** 流式输出 */
public Flux<String> chatStream(String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
/** 带 Token 统计的对话 */
public ChatResult chatWithMetrics(String message) {
ChatResponse response = chatClient.prompt()
.user(message)
.call()
.chatResponse();
String content = response.getResult().getOutput().getText();
Usage usage = response.getMetadata().getUsage();
return new ChatResult(content, usage.getPromptTokens(), usage.getGenerationTokens());
}
public record ChatResult(String content, int promptTokens, int generationTokens) {}
}
七、Prompt 工程化
7.1 Message 类型体系
Spring AI 的 Prompt 并非简单字符串,而是由多种角色组成的结构化对象:

| Message 类型 | 角色 | 用途 | 是否必填 |
|---|---|---|---|
| SystemMessage | system | 设定 AI 行为、背景、约束 | 强烈建议有 |
| UserMessage | user | 用户的问题或指令 | ✅ 必须 |
| AssistantMessage | assistant | AI 的历史回复(多轮对话) | 按需 |
| FunctionMessage | function | 工具调用的返回结果 | Tool Calling 时使用 |
SystemMessage 的质量决定输出稳定性。一个优质的 System Message 应包含三要素:
var systemMsg = new SystemMessage("""
你是一位资深 Java 架构师,回答技术问题时:
1. 先给出结论
2. 再提供代码示例
3. 最后说明注意事项
""");
| 要素 | 说明 | 示例 |
|---|---|---|
| 角色定义 | 你是谁 | “你是 Java 架构师” |
| 输出格式 | 怎么回答 | “先结论后代码” |
| 约束条件 | 什么不能做 | “不超过 200 字” |
同样的 User Message,配合不同的 System Message,效果差异显著:
| System Message | 同一个问题「什么是 Spring AI?」的回答风格 |
|---|---|
| “你是严谨的技术文档作者” | 正式的定义 + 架构说明 |
| “你是幽默的程序员博主” | 口语化比喻 + 个人经验 |
| “你是小学老师” | 通俗类比 + 简单例子 |
7.2 多轮对话
聊天机器人必须携带历史对话,否则 AI 每次都会"失忆":
List<Message> history = new ArrayList<>();
history.add(new SystemMessage("你是一位 Java 技术专家"));
history.add(new UserMessage("什么是 Spring AI?"));
history.add(new AssistantMessage("Spring AI 是 Spring 官方推出的 AI 应用开发框架..."));
history.add(new UserMessage("它支持哪些模型?"));
String response = chatClient.prompt()
.messages(history) // 传入完整对话历史
.call()
.content();
⚠️ 注意:历史越长,Token 消耗越大。一般保留最近 5-10 轮就够了,更早的可以做摘要压缩(后续 Advisor 篇会详细讲 ChatMemory)。
7.3 PromptTemplate 动态模板
实际项目中 Prompt 往往包含动态变量,比如翻译接口的源语言和目标语言是用户传参的:
PromptTemplate template = new PromptTemplate("""
请将以下文本从 {sourceLang} 翻译成 {targetLang}。
要求:保留原文格式,专业术语用括号标注原文。
文本内容:
{text}
""");
Prompt prompt = template.create(Map.of(
"text", "Spring AI is a framework for building AI applications.",
"sourceLang", "英文",
"targetLang", "中文"
));
String response = chatClient.prompt(prompt).call().content();
PromptTemplate vs 字符串拼接对比:
| 对比项 | 字符串拼接 | PromptTemplate |
|---|---|---|
| 可读性 | 差,变量混杂在文本中 | 好,模板与数据分离 |
| 安全性 | 有注入风险 | 占位符替换,安全 |
| 复用性 | 低,每次重新组装 | 高,同一模板多次调用 |
| 团队协作 | 困难 | 方便,模板放文件 |
7.4 外部文件加载模板
Prompt 内容较长或涉及团队协作时,建议将其置于外部文件统一管理。
@Value("classpath:prompts/translate.st")
private Resource translatePromptResource;
public String translate(String text) {
PromptTemplate template = new PromptTemplate(translatePromptResource);
Prompt prompt = template.create(Map.of("text", text));
return chatClient.prompt(prompt).call().content();
}
模板文件 src/main/resources/prompts/translate.st:
你是一位专业翻译,请将以下文本翻译成中文。
要求:
1. 保持原文格式
2. 专业术语保留英文并在括号标注
3. 语句通顺自然
原文:
{text}
推荐目录结构:
resources/prompts/
├── customer-service/
│ ├── greeting.st
│ └── complaint-handling.st
├── code-review/
│ ├── security-check.st
│ └── performance-check.st
└── common/
└── output-format.st
采用外部文件管理 Prompt 的优势在于:修改无需重新编译部署,产品经理可参与调优,且 Git 版本变更记录可供追溯。
7.5 Prompt 最佳实践

技巧一:结构化 Prompt
优质的 Prompt 应具备清晰的结构,避免冗长堆砌:
## 角色
你是一位...
## 任务
请...
## 输入数据
{userData}
## 输出要求
1. 格式:JSON
2. 字段:name, age, skills
3. 约束:skills 最多 5 个
技巧二:Few-Shot(少样本提示)
通过提供示例引导模型按预期格式输出:
String fewShotPrompt = """
请将用户输入分类为:技术、产品、运营、其他。
示例 1:
输入:Spring Boot 怎么配置数据库连接池?
输出:技术
示例 2:
输入:这个新功能什么时候上线?
输出:产品
现在请分类:
输入:{userInput}
输出:
""";
技巧三:Chain-of-Thought(思维链)
引导模型逐步推理,可显著提升复杂问题的准确率:
String cotPrompt = """
请一步一步思考,然后给出最终答案。
问题:一个仓库有 150 台服务器,其中 30% 是数据库服务器,
剩余的服务器中 40% 是应用服务器,其余是缓存服务器。
请问缓存服务器有多少台?
请按步骤分析:
1. 数据库服务器数量 = ?
2. 剩余服务器数量 = ?
3. 应用服务器数量 = ?
4. 缓存服务器数量 = ?
""";
技巧四:输出格式约束
明确指定输出格式,降低解析失败的概率:
String formatPrompt = """
请分析以下代码质量,以严格 JSON 格式返回:
{"score": "0-100整数", "issues": ["问题列表"], "suggestions": ["建议列表"]}
代码:
{code}
""";
实测效果对比:
| 技巧 | 无技巧基准 | 使用后提升 |
|---|---|---|
| 结构化 Prompt | 输出格式混乱 | 准确率约 +40% |
| Few-Shot | 分类错误率高 | 准确率约 +35% |
| Chain-of-Thought | 直接给错答案 | 准确率约 +50% |
| 输出格式约束 | JSON 解析常失败 | 成功率约 +60% |
7.6 Prompt 版本管理
生产环境中,Prompt 的变更需具备可追溯与可回滚能力。

文件版本管理:
resources/prompts/
├── translate/
│ ├── v1.st # 初始版本
│ ├── v2.st # 优化后的版本
│ └── current -> v2.st # 软链接指向当前版本
└── summarize/
├── v1.st
└── current -> v1.st
配置化选择版本:
app:
prompts:
translate:
version: v2
path: classpath:prompts/translate/v2.st
灰度发布支持:
@Service
public class PromptGrayReleaseService {
@Value("${prompt.gray-release.percentage:0}")
private int grayPercentage; // 灰度比例 0-100
public Prompt selectPrompt(String task, Map<String, Object> vars) {
boolean useNewVersion = ThreadLocalRandom.current().nextInt(100) < grayPercentage;
String version = useNewVersion ? "v2" : "v1";
return promptManager.getPrompt(task, version, vars);
}
}
发布流程建议:开发验证 → 5% 灰度观察 24 小时 → 25% → 50% → 100%,保留旧版本配置以便快速回滚。
7.7 Token 优化

Prompt 中的 Token 同样计入费用,且过长的上下文会降低模型响应速度。
Token 用量估算:
| 内容类型 | GPT-4o 约 Token 数 | 国内模型约 Token 数 |
|---|---|---|
| 1 个汉字 | ~1 Token | 1-2 Token |
| 1 个英文单词 | ~1.3 Token | 1-2 Token |
| 1 行代码 | 5-15 Token | 5-20 Token |
| 500 字 System Message | ~700 Token | 500-1000 Token |
优化技巧:
// ❌ 低效:啰嗦重复
String badPrompt = """
你是一位专家。请记住你是专家。作为专家你应该...
""";
// ✅ 高效:简洁明确
String goodPrompt = """
角色:Java 架构师
约束:先结论 → 代码 → 注意事项
""";
优化原则:
- System Message 控制在 500 Token 以内
- 历史对话保留最近 5–10 轮,超出部分做摘要处理
- 去除冗余表述,确保每个字均有价值
八、常见问题
Q1:ChatClient 和 ChatModel 到底什么时候用哪个?
绝大多数场景应使用 ChatClient。仅在需要绕过 Fluent API、直接操控底层行为(如自定义请求拦截器)时,才考虑使用 ChatModel。
Q2:切换模型后代码要改吗?
只要目标模型兼容 OpenAI 格式(如 DeepSeek、通义千问、智谱),仅需修改 YAML 配置,Java 代码无需变动。Ollama 不兼容该格式,需更换 starter 并调整配置。
Q3:多模型配置报循环依赖怎么办?
确保每个 ChatClient Bean 使用不同的 @Bean 名称,注入时通过 @Qualifier("beanName") 明确指定。若未加限定符,Spring 将无法确定注入目标。
Q4:对话历史太长 Token 超限怎么办?
两种处理思路:
- 截断:仅保留最近 N 轮对话
- 摘要:将早期对话交由模型生成摘要,以摘要替代原始内容
Spring AI 的 ChatMemory(Advisor 篇将详述)可自动管理此问题。
Q5:流式输出前端收不到数据?
按以下顺序排查:
- Controller 的
produces是否为TEXT_EVENT_STREAM_VALUE - 前端是否使用
EventSource(而非 fetch/ajax) - 中间是否存在 Nginx 代理缓冲?需配置
proxy_buffering off;
Q6:Ollama 本地模型效果不好怎么办?
建议先尝试更大参数的模型(如 deepseek-r1:14b 相比 7b 效果显著)。若仍不满足需求,建议采用云端模型。本地模型适用于简单任务与离线场景,不宜期望其替代 GPT-4o 等商业模型。
Q7:AI 不按要求格式输出 JSON 怎么办?
建议按以下四步处理:
- Prompt 中明确写入"请严格按 JSON 格式输出"
- 提供 Few-Shot 示例展示期望格式
- 追加一句"不要输出任何额外内容"
- 使用下一篇将要介绍的结构化输出(BeanOutputConverter),从根本上解决该问题
参考资源
官方资源:
- Spring AI 官方文档 — 最权威的技术参考
- Spring AI GitHub — 源码与 Issue
- OpenAI Chat Properties — 完整配置项列表
模型平台:
- OpenAI Platform — GPT 系列
- DeepSeek 开放平台 — 国产高性价比
- DashScope — 通义千问
- 智谱开放平台 — GLM 系列
- Ollama — 本地模型运行
Prompt 工程:
基础知识:
- Project Reactor — Flux/Mono 响应式编程
- StringTemplate — PromptTemplate 引擎基础
写在最后
本文系统梳理了 Spring AI 对话编程的核心内容:从 ChatClient 的创建与使用,到主流模型的一键接入,再到 Prompt 的工程化管理。上述知识足以应对日常开发中约 80% 的场景。
下一篇将深入探讨结构化输出与多模态,阐述如何让 AI 稳定返回 POJO、List 等强类型对象,以及如何处理图片输入输出。这是告别字符串解析困扰的关键一步。
系列目录:
- 第一篇-SpringAI概述与快速上手
- 第二篇-模型集成与对话基础 ✅(本文)
- 第三篇-结构化输出与多模态
- 第四篇-Embedding与向量数据库
- 第五篇-RAG检索增强生成
- 第六篇-Tool-Calling工具调用
- 第七篇-Advisor机制与对话管理
- 第八篇-MCP模型上下文协议
- 第九篇-AI-Agent开发
- 第十篇-企业级应用与最佳实践
若本文对您有所帮助,欢迎点赞、收藏与关注,系列文章将持续更新。
更多推荐



所有评论(0)