Spring AI 学习文档

王梦婵 7 阅读 技术分享

一、基础概念

1.1 系统提示词(System Prompt)

每次对话都会带上的一段隐藏指令,用户看不到,用来给 AI 定位:"你是智慧能源管理系统(EMS)的智能助手,请用中文简洁、专业的回答用户问题",这就是本项目 AiProperties 里的 systemPrompt。它决定了 AI 的"人设"和回答风格。

1.2 对话记忆

大模型天生没有记忆。这次问"我叫张三",下次问"我叫什么",它答不上来——因为每次调用都是独立的,它不记得上一轮说了啥。

ChatGPT 网页版之所以"记得住",是因为网页程序每次都把之前的对话记录重新发一遍给模型。Spring AI 的对话记忆干的就是这件事。

1.3 温度(Temperature)

模型的"发挥程度"参数,浮点数,一般范围 0~2,数值越低:越保守规矩,数值越高:越有创造性、表现欲强

实战建议0.5~0.8作为日常生产起点。

temperature

业务场景

输出风格

0.0 ~ 0.2

严谨问答、代码补全、数学答题

严格、确定、标准

0.3 ~ 0.6

聊天机器人、摘要、辅助写作

稍有变化、稳妥

0.7 ~ 1.0

创作内容、广告文案、标题生成

丰富、有创意

1.1 ~ 1.5

头脑风暴、灵感碰撞

大开脑洞、变化极强

1.4 向量化(Embedding)

把一段文字变成一串数字(比如 1024 个小数),语义相近的文字,数字串也相近。

比如:"变压器故障" → [0.12, -0.35, 0.88, ...] "变压器维修" → [0.11, -0.33, 0.90, ...](很接近) "今天天气不错" → [-0.5, 0.72, -0.1, ...](差很远)

这样"找语义相近的内容"就变成了数学计算。这是 RAG 检索的核心原理

二、Spring AI 概述

2.1 定义

Spring AI 是 Spring 官方推出的 AI 应用开发框架,目标是把 AI 能力以 Spring 一贯的风格(依赖注入、自动配置、可移植抽象)带给 Java 开发者。它解决的核心问题是:

  • 统一抽象:通过 ChatModel、EmbeddingModel、ImageModel、VectorStore 等接口,屏蔽不同厂商(OpenAI、Anthropic、Ollama、国内大模型等)的 API 差异,切换模型只需改配置、换依赖,几乎不改业务代码。

  • 可移植性:同一个 ChatClient 调用,可跑在本地 Ollama,也可切到云端 GPT,只需改一行配置。

  • 工程化能力:内置 Function Calling、RAG、对话记忆、结构化输出、Advisors 扩展链、MCP、可观测性等,让「能跑」变成「可维护、可观测」。

2.2 特性定义

特性

定义

1

提示词工厂

大模型应用中最简单也最核心的技术——与模型交互的媒介,提示词给得好模型才能按想要的方式响应

2

对话拦截 Advisors

面向切面思想,对模型对话和响应进行增强

3

对话记忆

一个 Bean 组件就让大模型拥有记忆,开箱即用

4

Tools(Function Calling)

让大模型与企业业务 API 互联,实现非常优雅

5

RAG 技术下的 ETL

让大模型与企业业务数据互联(读文件、分隔、向量化),支持 20+ 种向量数据库

6

MCP

让 Tools 外部化,形成公共工具供外部开箱即用(MCP 协议的 Java SDK 就是 Spring AI 团队提供的)

7

模型评估

测试大模型的幻觉反应

8

可观察性

把 AI 运行时关键指标暴露出来,可接 Spring Boot Actuator 观测

9

Agent 应用

官方提供 5 种 Agent 模式示例(路由/链式/并行/编排/评估优化)

三、依赖与配置

3.1 版本

组件

版本

Spring Boot

3.5.15

Spring AI

1.1.2

JDK

21

大模型

智谱 GLM

3.2 依赖

引入Spring AI 依赖

<properties>
    <spring-ai.version>1.1.2</spring-ai.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- BOM:Spring AI 全家桶的版本清单,引它之后再引具体模块就不用写版本号 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

模块配置引入模型依赖

<!-- 智谱大模型-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>

<!-- 文档解析(RAG 导入 pdf/docx 用) -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>

3.3 配置详情

配置智谱大模型

spring:
  ai:
    zhipuai:
      api-key: ${ZHIPUAI_API_KEY:}
      chat:
        options:
          model: glm-4.5-air        # 聊天模型
          temperature: 0.7          # 发挥程度 0~1
      embedding:
        options:
          model: embedding-2        # 向量模型(RAG 用)

3.4 自定义业务配置

@ConfigurationProperties——把 yml 里的一段配置映射成 Java 对象,字段有默认值,yml 里写 ai.memory-window: 50 就能覆盖。

@Data
@ConfigurationProperties(prefix = "ai")   // 绑定 yml 里 ai.* 前缀
public class AiProperties {
    private String systemPrompt = "你是智慧能源管理系统(EMS)的智能助手..."; // 系统提示词
    private String ragSystemPrompt = "...知识库问答助手...";                // RAG 提示词
    private Integer memoryWindow = 20;   // 记忆窗口:最多记 20 条消息
    private Integer ragTopK = 4;         // RAG 检索最多取 4 个片段
    private String vectorStoreFile = "./data/ai/vector-store.json"; // 向量库落盘位置
}

3.5 直连ChatModel测试

直接注入 starter 自动配好的 ChatModel:

@SpringBootTest
public class AiTest {

    @Test
    public void testChat(@Autowired ZhiPuAiChatModel chatModel) {
        String content = chatModel.call("你是谁");
        System.out.println(content);
    }

    @Test
    public void testStream(@Autowired ZhiPuAiChatModel chatModel) {
        Flux<String> stream = chatModel.stream("你是谁");
        stream.toIterable().forEach(System.out::print);   // 阻塞逐段输出
    }
}

3.6 ChatOptions 按次覆盖配置

yml 里的 spring.ai.zhipuai.chat.options.* 是全局默认。某次调用想用不同参数,可以按次覆盖(构造 Prompt 时传入)

ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
        .temperature(0.2)      // 这次要严谨
        .build();
ChatResponse res = chatModel.call(new Prompt("请写一句诗描述清晨。", options));
System.out.println(res.getResult().getOutput().getText());

三个常用参数:

  • temperature 温度

  • maxTokens 限制生成的最大token数

  • stop 截断序列:输出中出现这些字符串就立刻停止,如下所示

stop序列的四种用法:

spring:
  ai:
    zhipuai:
      chat:
        options:
          max-tokens: 20
          stop:
            - "\n"        # 只要一行
            - "。"        # 只要一句话
            - "政治"       # 敏感词,出现即停
            - "最后总结一下"  # AI 惯用模板词,截掉让文风更拟人

3.7 深度思考模型

思考的内容有个专业名词:Chain of Thought(CoT,思维链)。推理模型(如 DeepSeek 的 deepseek-reasoner)会把思考过程单独放在 reasoningContent 里返回,不过缺点是推理模型更慢更贵,复杂任务(数据分析、逻辑推理)才值得用,日常问答用普通模型。

ZhipuAiChatOptions options = ZhipuAiChatOptions.builder()
                .model("glm-4.5")
                .build();
        ChatResponse res = zhipuAiChatModel.call(new Prompt("请写一句诗描述清晨。", options));
        ZhipuAiAssistantMessage msg = (ZhipuAiAssistantMessage) res.getResult().getOutput();
        String reasoning = msg.getReasoningContent(); // 思考过程(CoT)
        String content   = msg.getText();               // 最终回答

四、对话开发

4.1 ChatModel与ChatClient的关系

ChatClient 提供了通用的 API,适用于所有大模型——面向它编程,就不需要为每一种模型学一套 API。系统提示词、格式化响应、聊天记忆、Tools 这些能力在 ChatClient 上都更易用、更优雅。ChatModel 更底层,只在做某些模型特有的个性化操作时才直接用它。

ChatModel

ChatClient

定位

底层引擎(直接和厂商 API 通信)

门面封装(基于 ChatModel 封装)

来源

starter 自动配置创建

ChatClient.builder(chatModel).build()

职责

发请求、收响应、协议细节

拼提示词、挂记忆、挂 RAG、格式化输出

类比

JDBC 的 Connection

MyBatis-Plus

4.2 创建ChatClient的两种方式

方式一:注入ChatClient.Builder(自动装配)

ChatClient 自动配置与注入:ChatClient.Builder 由 Spring AI 自动配置并注入(基于 classpath 上的模型 starter),无需手动 new。ChatClient 是线程安全、可复用的,推荐在构造函数里用 Builder 构建一次、作为单例复用,避免每次请求重复创建。

@SpringBootTest
public class ChatClientTest {
    @Test
    public void testChatClient(ChatClient.Builder builder) {
        ChatClient chatClient = builder.build();
        String content = chatClient.prompt()
                .user("Hello")
                .call()
                .content();
        System.out.println(content);
    }
}

需要注意:如果同时引入了多个模型的starter,容器里有多条ChatModel,自动注入会失败,必须改用方式二。

方式二:手动指定ChatModel

@Test
public void testChatOptions(@Autowired ZhiPuAiChatModel chatModel) {
    ChatClient chatClient = ChatClient.builder(chatModel).build();
    String content = chatClient.prompt()
            .user("Hello")
            .call()
            .content();
    System.out.println(content);
}

4.3 多模型动态切换管理

按请求选择用哪个模型,比如日常问答用便宜的flash模型,复杂分析用推理模型。

@Configuration
public class MultiModelConfig {

    // 智谱推理模型,适合复杂分析
    @Bean
    public ChatClient zhipuReason() {
        ZhipuAiApi zhipuAiApi = ZhipuAiApi.builder()
                .apiKey(System.getenv("ZHIPUAI_KEY"))
                .build();
        ZhipuAiChatModel zhipuAiChatModel = ZhipuAiChatModel.builder()
                .zhipuAiApi(zhipuAiApi)
                .defaultOptions(ZhipuAiChatOptions.builder()
                        // glm-4.5 支持思维链
                        .model("glm-4.5")
                        .build())
                .build();
        return ChatClient.builder(zhipuAiChatModel).build();
    }

    // 智谱普通对话模型
    @Bean
    public ChatClient zhipuChat() {
        ZhipuAiApi zhipuAiApi = ZhipuAiApi.builder()
                .apiKey(System.getenv("ZHIPUAI_KEY"))
                .build();
        ZhipuAiChatModel zhipuAiChatModel = ZhipuAiChatModel.builder()
                .zhipuAiApi(zhipuAiApi)
                .defaultOptions(ZhipuAiChatOptions.builder()
                        // glm-4-flash 免费高速,日常对话首选
                        .model("glm-4-flash")
                        .build())
                .build();
        return ChatClient.builder(zhipuAiChatModel).build();
    }
}

请求时按参数路由,Spring会把容器里所有ChatClient Bean按Bean名收进Map

@RestController
public class MultiModelsController {

    @Autowired
    private Map<String, ChatClient> chatClientMap;  // key = Bean 名

    @GetMapping("/chat")
    String generation(@RequestParam String message,
                      @RequestParam String model) {
        ChatClient chatClient = chatClientMap.get(model);
        return chatClient.prompt().user(message).call().content();
    }
}

五、同步对话与流式对话

5.1 同步对话

特点:

  • 简单:一行拿到完整结果

  • 慢:模型生成500字要好几秒

  • 适合:后台任务,对响应速度不敏感的内部接口

环节

作用

类比

.prompt()

创建一次"提问构建器"

开始拼 SQL

.user(...)

设置用户消息

设置查询条件

.call()

同步发起 HTTP 请求给智谱,死等回复(几秒)

执行查询

.content()

从响应对象里抽出回复文本

取结果集

5.2 流式对话

SSE(Server-Sent Events,服务器推送事件)基于 HTTP 协议,是一条单向长连接——由服务端持续向客户端推送数据。特点是:一次 HTTP 连接、服务器分多次往回写数据。与之对比,本例中 /chat/stream 通过 produces = text/event-stream 声明 SSE,配合 Flux<String> 逐条推送生成内容。想完整展示成可阅读的文本,前端需要手动拼接返回的分片内容。

@RestController
public class ChatController {

    private final ChatClient chatClient;

    // 注入 Builder,构建后可多次复用同一个 client
    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    // 普通单轮对话
    @GetMapping("/chat")
    public String chat(@RequestParam("message") String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    // SSE 流式输出(逐 token 返回)
    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(@RequestParam("message") String message) {
        return chatClient.prompt()
                .user(message)
                .stream()
                .content()
                .doOnNext(chunk -> System.out.println("chunk===[" + chunk + "]"))
                .concatWithValues("[DONE]");
    }
//使用stream()方法,也可以不指定produces = text/event-stream,这样它就不是SSE流式输出了。后端内部是分片(大模型),HTTP 对外不是分片推送。即Spring Web 会把整个 Flux 收集完毕,把所有 chunk 拼接,HTTP 一次性返回完整字符串给前端。

}

六、对话记忆

多轮对话需要把历史上下文回传给模型。Spring AI 把记忆拆成两层:逻辑层 ChatMemory + 存储层 ChatMemoryRepository,通过 MessageChatMemoryAdvisor 自动读写。

记忆的工作原理:所谓「让模型记住对话」,本质是——把全部历史多轮对话(用户提问和模型回答成对保存),加上当前最新提问,整体一起传给大模型;不是只带上一轮的回答。

第 1 轮   用户:「我叫张三」      → 模型回答「你好张三」(记忆存下 user + assistant 两条)
第 2 轮   用户:「我叫什么?」    → 请求携带:第1轮user + 第1轮assistant + 第2轮user
                                    模型回答「你叫张三」    (再存下这一轮 assistant)
第 3 轮   用户:「帮我写邮件」    → 请求携带:前面所有历史 + 第3轮user

每一轮交互结束,要把模型输出的 assistant 消息存入记忆,下一次请求就携带这一整套消息。缺点也由此而来:对话越多 token 越大,容易触发上下文超限,因此需要做记忆截断(滑动窗口 maxMessages)或摘要压缩。

6.1 内存记忆

// 逻辑层:滑动窗口记忆,最多保留最近 10 条消息
 ChatMemory chatMemory = MessageWindowChatMemory.builder()
          .maxMessages(10) 
          .build(); 
// 挂载 Advisor,自动「读历史 → 调模型 → 存本轮」 
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
        .build();

补充两点:

① 「取多少」与「存多少」是两回事——MessageWindowChatMemory.maxMessages 控制存多少,MessageChatMemoryAdvisor 的 chatHistoryWindowSize 参数控制每次取多少条历史注入;

② 裁剪历史时,system 消息会被保留,不会随窗口滑动被丢弃。

6.2 conversationId隔离

绝不能把 conversationId 写死在 Bean 里,否则所有用户共享同一份记忆。正确做法是每次请求覆盖,记忆按 conversationId 分格子存:

@GetMapping("/chat")
public String chat(@RequestParam("userId") String userId, @RequestParam("message") String message) {
    return chatClient.prompt()
            .user(message)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, bo.getConversationId())))  // 按conversationId隔离
            .call()
            .content();
}

6.3 持久化记忆

1、数据库存储对话记忆

默认情况, 对话内容会存在jvm内存会导致:

  1. 一直存最终会撑爆JVM导致OOM。

  2. 重启就丢了, 如果已想存储到第三方存储进行持久化

springAi内置提供了以下几种方式(例如 Cassandra、JDBC 或 Neo4j), 这里演示下JDBC方式

(1)添加依赖


        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
        </dependency>

        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

(2)添加配置\建表

spring.ai.chat.memory.repository.jdbc.initialize-schema=always
spring.ai.chat.memory.repository.jdbc.schema=classpath:/schema-mysql.sql

//application.yml
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/springai?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&
    driver-class-name: com.mysql.cj.jdbc.Driver

(3)配置类

@Configuration
public class ChatMemoryConfig {


    @Bean
    ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) {
        return MessageWindowChatMemory
        .builder()
        .maxMessages(1)
        .chatMemoryRepository(chatMemoryRepository).build();
    }

}
2、Redis存储

若要用Redis存记忆,则需要自己实现ChatMemoryRepository接口,ChatMemoryRepository可以选择Cassandra、Neo4j、MongoDB、CosmosDB、Redis 等实现,按现有基础设施选型即可,上层 ChatMemory 抽象不变。

@Bean
    public RedisChatMemoryRepository redisChatMemoryRepository() {
        return RedisChatMemoryRepository._builder_()
                .host(redisHost)
                .port(redisPort)
                .password(redisPassword)
                .timeout(redisTimeout)
                .build();
    }

6.4 多层次记忆架构

记忆多 = 更像人,但记忆多 = token 超限。无论换什么存储,发给模型的窗口就那么大。想"记更多"就要模仿人类,分层:

  • 近期记忆:保留在上下文窗口中的最近几轮对话,每轮对话完成后立即存储(可通过ChatMemory); 10 条

  • 中期记忆:通过RAG检索的相关历史对话(每轮对话完成后,异步将对话内容转换为向量并存入向量数据库) 5条

  • 长期记忆:关键信息的固化总结

方式一:定时批处理

通过定时任务(如每天或每周)对积累的对话进行总结和提炼

提取关键信息、用户偏好、重要事实等

批处理方式降低计算成本,适合大规模处理

方式二:关键点实时处理

在对话中识别出关键信息点时立即提取并存储

例如,当用户明确表达偏好、提供个人信息或设置持久性指令时

采用"写入触发器"机制,在特定条件下自动更新长期记忆

6.5 会话管理与手动API

除了 Advisor 自动读写,ChatMemory 接口本身也提供手动操作,用于会话生命周期管理

// 手动预置上下文(对话开始前注入业务背景)
chatMemory.add(conversationId, List.of(new UserMessage("对方是储能用户,请在其储能专业的角度回答")));

// 读取历史
List<Message> history = chatMemory.get(conversationId);

// 清空会话(「开始新对话」按钮)
chatMemory.clear(conversationId);

6.6 摘要压缩

maxMessages 截断是「丢弃」策略,会丢掉早期信息。需要保留更长历史时,用摘要压缩:定期把历史对话用模型总结成摘要,用摘要替代原始历史注入,大幅减少 token。

Spring AI 无内置摘要功能,可自定义实现,核心思路:

// 用模型生成摘要
String summary = chatClient.prompt()
        .system("把下面的对话历史总结成 200 字以内的摘要,保留关键信息:")
        .user(historyText)   // historyText = 拼接后的历史对话
        .call()
        .content();

七、提示词

提示词(Prompt)是引导模型行为的输入。Spring AI 把 Prompt 拆成消息(Message)与选项(Options)两部分,支持角色、模板、参数化。

7.1 角色消息

角色

谁在说

作用

SYSTEM

开发者设定

引导 AI 的行为和响应方式、设置规则。相当于对话前给 AI 的指令

USER

用户

用户的问题、命令或语句,构成 AI 响应的基础

ASSISTANT

AI

AI 的历史回复。维持对话连贯性、回放记忆全靠它;也可能携带工具调用请求信息

TOOL

工具

响应工具调用、返回附加信息(Function Calling 场景用,见进阶路线)

本项目中配置了systemPrompt为system消息,装配时通过.defaultSystem(...)挂载,每次调用自动携带

ChatClient.builder(chatModel)
    .defaultSystem("你是智慧能源管理系统(EMS)的智能助手,请用中文简洁、专业地回答用户问题。")

7.2 模板占位符

用 {变量} 占位,运行时注入,避免字符串拼接。ST4(StringTemplate v4)模板占位符 {变量名} 的变量标识符只能英文、数字、下划线,不能中文、不能空格

ChatClient 写法——.text() 放模板,.param() 填充占位符:

String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("列出5个{domain}行业常见风险点,并简单说明。")
            .param("domain", "电化学储能电站"))
    .call()
    .content();

ChatModel 写法——用 SystemPromptTemplate 手动渲染再组装:

String systemText = """
  你是一个友好的 AI 助手,帮助人们寻找信息。
  你的名字是 {name}。
  你应该以 {voice} 的风格回复用户。
  """;
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "小助手", "voice", "专业"));

Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
List<Generation> response = chatModel.call(prompt).getResults();

7.3 自定义模板分隔符

默认占位符是 {xxx}。如果模板文本本身含大量花括号(比如要嵌 JSON 代码片段),会和占位符冲突,可以换成分隔符 <xxx>

// ChatClient 写法:指定 templateRenderer
String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("告诉我5部<composer>的电影")
            .param("composer", "John Williams"))
    .templateRenderer(StTemplateRenderer.builder()
            .startDelimiterToken('<')
            .endDelimiterToken('>')
            .build())
    .call()
    .content();
// ChatModel 写法:PromptTemplate.builder() 指定 renderer
PromptTemplate promptTemplate = PromptTemplate.builder()
    .renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
    .template("告诉我5部<composer>的电影.")
    .build();
String prompt = promptTemplate.render(Map.of("composer", "John Williams"));

StTemplateRenderer——基于 StringTemplate 的渲染器(模块 spring-ai-template-st),默认 {xxx},可自定义起止分隔符。

八、RAG知识库

RAG = 先从知识库检索相关文档,再把这些文档作为上下文注入提示词,让模型基于真实资料回答,减少幻觉。

8.1 ETL数据摄入

三步走:读取 → 切分 → 写入向量库。其中「读取」用 TikaDocumentReader(底层 Apache Tika)解析 PDF/Word/PPT/HTML 等文件——RAG 知识库导入文件必须,需要先引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>

TikaDocumentReader 这类普通解析器只提取文本,碰到图片既不报错、也不会识别,而是直接跳过——图片里承载的信息就丢了。框架不会自动帮助判断文档里有没有图片,需要业务层自己处理:图片带业务信息(如架构图、流程图、截图里的关键内容)时,要手动把图片抽出来,调用多模态模型转成文字描述,再参与切块和向量化;纯装饰的配图则无需处理。

// 1. 读取(支持 PDF/Word/HTML 等,底层用 Apache Tika)
var reader = new TikaDocumentReader("file:./docs/handbook.pdf");

// 2. 切分(按 token 分块,避免单块过长)
TokenTextSplitter splitter = new TokenTextSplitter(800, 350, 5, 10000, true);

// 3. 写入向量库(自动 embedding)
List<Document> chunks = splitter.apply(reader.get());
vectorStore.add(chunks);

切分策略:长文档必须切成小块再入库,块大小直接决定检索质量。上面 TokenTextSplitter(800, 350, 5, 10000, true) 五个参数:

参数

默认值

含义

chunkSize

800

每块目标大小(按 token),切块的基本单位

minChunkSizeChars

350

块超过此字符数时,在块内最后一个句末标点(. ! ? \n)处截断,保证块在句子边界结束;没超过就保留整块

minChunkLengthToEmbed

5

丢弃短于此长度的块(去掉换行后只剩几个字的废块不向量化)

maxNumChunks

10000

最多分多少块,超出不管(防意外)

keepSeparator

true

块内是否保留 \r\n 换行

8.2 问答链路:QuestionAnswerAdvisor

最简单的方式,检索 + 注入 + 问答一气呵成。需要引入 spring-ai-advisors-vector-store 依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(SearchRequest.builder()
                .topK(5)
                .similarityThreshold(0.45)
                .build())
        .build();

ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(qaAdvisor)
        .build();

// 提问时会自动检索并注入上下文
String answer = chatClient.prompt()
        .user("我们的产品支持哪些支付方式?")
        .call()
        .content();

8.3 RetrievalAugmentationAdvisor(RAG 流水线)

QuestionAnswerAdvisor 是"一步到位"的简化版。想精细控制 RAG 每个环节,用 RetrievalAugmentationAdvisor。

Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
        // 查(相当于 QuestionAnswerAdvisor 的检索部分)
        .documentRetriever(VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .similarityThreshold(0.5)
                .build())
        // 可选:检索为空时自定义兜底话术(默认直接把问题原样发给模型)
        .queryAugmenter(ContextualQueryAugmenter.builder()
                .allowEmptyContext(false)
                .emptyContextPromptTemplate(PromptTemplate.builder()
                        .template("用户查询位于知识库之外,请礼貌告知无法回答").build())
                .build())
        // 可选:查询重写——用户表述模糊时,先让大模型改写成更适合检索的查询
        .queryTransformers(RewriteQueryTransformer.builder()
                .chatClientBuilder(ChatClient.builder(chatModel))
                .targetSearchSystem("能源管理知识库助手")
                .build())
        // 可选:检索后处理——拿到文档后还可以过滤、打日志、重排序
        .documentPostProcessors((query, documents) -> {
            return documents;
        })
        .build();

扩展点

解决什么问题

queryAugmenter

知识库检索不到时的兜底策略(拒绝回答 or 直接回答)

queryTransformers

查询侧优化:用户问法口语化/关键词缺失 → 改写、翻译(TranslationQueryTransformer)后再检索

documentPostProcessors

结果侧优化:对检索结果过滤、监控、重排序

8.4 元数据

切分后还可以用大模型给每个 chunk"加料",提升检索精度,按文档元数据缩小检索范围

String answer = chatClient.prompt()
        .user("策略的政策是什么?")
        .advisors(a -> a.param(
                QuestionAnswerAdvisor.FILTER_EXPRESSION, "source == 'policy.md'"))
        .call()
        .content();

技术手册(前后章节依赖)、教程、法律条款——单块语义不完整时,靠前后块摘要补上下文,为当前块及前后相邻块生成摘要存入元数据(PREVIOUS/CURRENT/NEXT 三种)

// 示例:给每个块生成前/中/后三段摘要
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel,
        List.of(SummaryMetadataEnricher.SummaryType.PREVIOUS,
                SummaryMetadataEnricher.SummaryType.CURRENT,
                SummaryMetadataEnricher.SummaryType.NEXT));
chunks = enricher.apply(chunks);

九、Advisor机制

Advisor 是 Spring AI 的统一扩展点:它拦截每一次 ChatClient 调用,在请求发出前、响应返回后做增强。接下来要用的对话记忆、RAG、日志,本质都是 Advisor——理解它之后,这些能力就是「一条可插拔的链」。

9.1 请求/响应拦截链

每次 chatClient.prompt().call() 都会经过一条 Advisor 链:

请求(Prompt) ──► [ Advisor 链 ] ──► 调用大模型 ──► 响应(ChatResponse)
                     ▲                                │
                     └──── 响应再反向经过 Advisor ◄────┘

每个 Advisor 可以在两个时机介入:

  • 请求阶段(adviseCall):改写用户输入、注入历史/检索结果、附加系统提示等;

  • 响应阶段(adviseResponse):改写模型输出、记录日志、写入记忆等。

通过 ChatClient 的 .defaultAdvisors(...)(默认)或 .advisors(...)(按请求)挂载。

9.2 Advisor 接口层级

自定义 Advisor 优先用 BaseAdvisor:只实现 before/after 就同时支持同步与流式。若只实现 CallAdvisor,.stream() 流式场景不会生效——这是最容易踩的坑。

类/接口

作用

Advisor

顶层接口(getName + getOrder)Advisor 必须结合 ChatClient 才能用

CallAdvisor / CallAdvisorChain

非流式(call)场景的拦截链

StreamAdvisor / StreamAdvisorChain

流式(stream)场景的拦截链

BaseAdvisor

模板方法基类,同时适配 call/stream,自定义 Advisor 从它继承最省事

ChatClientRequest

还未发出的 Prompt 请求(可修改)

ChatClientResponse

聊天完成响应

9.3 内置Advisor:SimpleLoggerAdvisor

整个对话过程是个"黑盒",不利于调试。SimpleLoggerAdvisor 把实际发给模型的请求和收到的响应全部打进日志,配合日志级别

chatClient = ChatClient
        .builder(chatModel)
        .defaultAdvisors(new SimpleLoggerAdvisor())
        .build();

9.4 组合与顺序

典型生产配置:记忆 → RAG → 日志,用 order 明确先后:

ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(chatMemory).order(10).build(), // 1. 先注入历史
                qaAdvisor,                                                       // 2. 再检索注入
                new SimpleLoggerAdvisor())                                       // 3. 最后打印日志
        .build();

9.5 自定义Advisor

基于 BaseAdvisor 实现:请求阶段追加用户消息后缀,响应阶段可做后处理。

public class AppendSuffixAdvisor implements BaseAdvisor {

    @Override
    public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
        // 给用户消息追加后缀(ChatClientRequest 不可变,用 mutate 生成新对象)
        return request.mutate()
                .prompt(request.prompt().augmentUserMessage("\n\n请用中文回答。"))
                .build();
    }

    @Override
    public ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {
        // 响应阶段可在此记录日志、改写输出等
        return response;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

augmentUserMessage,每一轮都会追加,会造成 prompt 越来越长。这里要特别注意:augmentUserMessage 如果设定不合理会容易改变prompt上下文,干扰大模型的判断,输出与预期结果不符的情况。只要是做分类、抽取、JSON 输出这类强格式约束业务,尽量不要用全局 Advisor 自动修改 prompt,极易破坏格式指令。

ChatClientRequest 是不可变 record,必须用 mutate().xxx().build() 生成新对象再返回;BaseAdvisor 同时覆盖同步与流式,无需再单独写 StreamAdvisor。

十、结语

在整理这份笔记的过程中,我系统学习了 Spring AI 的整套功能。我明白了大模型基础概念、模型配置、ChatClient 使用、会话记忆管理、提示词模板、RAG 检索增强以及 Advisor 扩展链。我发现,接入大模型不只是简单调用接口,上下文控制、幻觉处理、模型选型、token 开销都是生产环境需要重点考虑的。后续我会继续在储能 EMS 场景做验证,持续补充这份笔记。