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()           // 提取结果

Spring AI ChatClient 调用链路图

每一步的作用:

方法 作用 返回类型
.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 代码无需任何改动。此即上一篇所述"供应商锁定"问题的解决方案。

Spring AI 多模型统一架构图

架构分层:

  • 业务层:只依赖 ChatClient,不关心底层是哪个模型
  • 抽象层ChatModel 接口定义统一契约
  • 适配层:各模型提供商的实现类(OpenAiChatModelOllamaChatModel 等)
  • 配置层:通过 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 对应关系

模型 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 并非简单字符串,而是由多种角色组成的结构化对象:

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

优质的 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 的变更需具备可追溯与可回滚能力。

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 优化

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 超限怎么办?

两种处理思路:

  1. 截断:仅保留最近 N 轮对话
  2. 摘要:将早期对话交由模型生成摘要,以摘要替代原始内容

Spring AI 的 ChatMemory(Advisor 篇将详述)可自动管理此问题。

Q5:流式输出前端收不到数据?

按以下顺序排查:

  1. Controller 的 produces 是否为 TEXT_EVENT_STREAM_VALUE
  2. 前端是否使用 EventSource(而非 fetch/ajax)
  3. 中间是否存在 Nginx 代理缓冲?需配置 proxy_buffering off;

Q6:Ollama 本地模型效果不好怎么办?

建议先尝试更大参数的模型(如 deepseek-r1:14b 相比 7b 效果显著)。若仍不满足需求,建议采用云端模型。本地模型适用于简单任务与离线场景,不宜期望其替代 GPT-4o 等商业模型。

Q7:AI 不按要求格式输出 JSON 怎么办?

建议按以下四步处理:

  1. Prompt 中明确写入"请严格按 JSON 格式输出"
  2. 提供 Few-Shot 示例展示期望格式
  3. 追加一句"不要输出任何额外内容"
  4. 使用下一篇将要介绍的结构化输出(BeanOutputConverter),从根本上解决该问题

参考资源

官方资源:

模型平台:

Prompt 工程:

基础知识:


写在最后

本文系统梳理了 Spring AI 对话编程的核心内容:从 ChatClient 的创建与使用,到主流模型的一键接入,再到 Prompt 的工程化管理。上述知识足以应对日常开发中约 80% 的场景。

下一篇将深入探讨结构化输出与多模态,阐述如何让 AI 稳定返回 POJO、List 等强类型对象,以及如何处理图片输入输出。这是告别字符串解析困扰的关键一步。


系列目录:


若本文对您有所帮助,欢迎点赞、收藏与关注,系列文章将持续更新。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐