前期准备
- 本文所需的环境和平台 >jdk 21 > >node 22.21.1 > >ollama server (本地服务器均可) > >阿里百炼平台api-key (避免明文使用,建议加入到环境变量) > >向量数据库 redis-stack (拥有redis所有功能 拓展了向量存储功能) > >向量数据库 pgvector (拥有pgsql所有功能 拓展了向量存储功能)
pgvector docker 快速安装
CodeBlock Loading...或者 docker compose
CodeBlock Loading...redis-stack docker 快速安装
CodeBlock Loading...或者docker compose
CodeBlock Loading...
一、基础问答
这里的基础问答指的是 无状态 的、一次性的问答交互。 在这种模式下,每一次用户请求都被视为一个独立的会话,模型在处理当前问题时,不会保留或记住之前任何一次交互的内容和上下文。
1.1 依赖
基础对话所需依赖项目
1.2 配置api key
获取api key : 获取地址
1.3 ChatModel 方式
ChatModel 是 Spring AI 提供的最低级别、最基础的抽象接口,用于与底层大语言模型(LLM)进行交互。
1.3.1 创建ChatModel
1.3.2 基础使用
使用 ChatModel 实现消息的流式返回和阻塞返回
1.4 ChatClient 使用
ChatClient 是 Spring AI 应用程序中最常用的交互接口。它在底层 ChatModel 之上提供了易用性、集成性和扩展性,让开发者能够专注于业务逻辑,而不是底层 API 的细节。
1.4.1 官方解释
ChatClient 提供了与 AI 模型通信的 Fluent API,它支持同步和反应式(Reactive)编程模型。与 ChatModel、Message、ChatMemory 等原子 API 相比,使用 ChatClient 可以将与 LLM 及其他组件交互的复杂性隐藏在背后,因为基于 LLM 的应用程序通常要多个组件协同工作(例如,提示词模板、聊天记忆、LLM Model、输出解析器、RAG 组件:嵌入模型和存储),并且通常涉及多个交互,因此协调它们会让编码变得繁琐。当然使用 ChatModel 等原子 API 可以为应用程序带来更多的灵活性,成本就是您需要编写大量样板代码。
ChatClient 类似于应用程序开发中的服务层,它为应用程序直接提供 AI 服务,开发者可以使用 ChatClient Fluent API 快速完成一整套 AI 交互流程的组装。
包括一些基础功能,如:
- 定制和组装模型的输入(Prompt)
- 格式化解析模型的输出(Structured Output)
- 调整模型交互参数(ChatOptions)
还支持更多高级功能:
- 聊天记忆(Chat Memory)
- 工具/函数调用(Function Calling)
- RAG
1.4.2 创建ChatClient
ChatClient 默认情况下不会被 Spring Boot 自动配置为可直接 @Autowired 或 @Resource 注入的 Bean。 这是因为 ChatClient 需要通过其 Builder 模式来构建,并且必须依赖于一个或多个底层 ChatModel 实例。
有两种注入方式
自定义
@Bean(推荐方式)这是最常用且最符合 Spring 规范的做法。通过定义一个
@Bean方法,您可以清晰地指定ChatClient依赖于哪个具体的ChatModel(例如,这里依赖于dashscopeChatModel),并可以在构建时应用默认配置(如默认系统指令、默认工具等)。CodeBlock Loading...构造方法注入依赖并手动构建
这种方式适用于您只想在特定组件中使用
ChatClient,而不希望将其暴露为全局Bean的场景。您可以在组件的构造函数中注入所需的ChatModel,然后手动完成ChatClient的构建。CodeBlock Loading...
1.4.3 基础使用
使用 ChatClient 实现消息的流式返回和阻塞返回
1.5 使用本地模型(ollama) 实现基础对话
选择本地模型是为了在数据隐私、离线运行、长期免费和低延迟方面取得优势,特别适用于不能联网或对敏感数据有严格要求的项目。
1.5.1 引入依赖
1.5.2 编辑配置
1.5.3 基础使用
ChatModel && ChatClient 流试阻塞方式调用。
1.6 多模型共存
多模型共存是为了实现成本、性能和合规性的最佳平衡,允许项目根据任务需求灵活选择和切换本地、线上或不同厂商的模型。
ChatClient 通过 ChatModel 创建,ChatModel(对于spring-ai-alibaba来说)也可通过DashScopeChatModel 创建不同模型的ChatModel,外部的依赖也可以引入相关的ChatModel如ollama。
这样我门就可以在一个项目中使用多个模型。下面是示例:
二、记忆存储
记忆存储是为了打破基础对话的无状态限制,使 AI Agent 能够记住先前交互的上下文,从而实现连贯且自然的多轮对话。
这里主要介绍 redis 和 postgres 两种方式作为记忆存储方式,其他类型的存储方式请参考 spring-ai 文档,这不需要向量库,普通的redis和postgres就可以。
2.1 引入依赖
2.2 添加配置
2.3 构造Bean
文档中主要介绍ollama + Jdbc存储方式,其他方式chat-memory模块中有示例代码。
2.3.1 配置 JDBC 聊天记忆仓库(PostgresMemoryConfig)
这个配置类负责创建并注册 JdbcChatMemoryRepository 实例,它依赖于 Spring Boot 自动配置的 JdbcTemplate 来与 PostgreSQL 数据库交互。
关键点解释:
@Primary: 该注解用于解决歧义性。当应用中存在多个ChatMemoryRepository接口的实现(例如,同时存在JDBC和Redis实现)时,@Primary告诉 Spring,这是默认或首选的实现。
2.3.2 集成到 ChatClient(SaaLLMConfig)
这个配置类展示了如何将上一步定义的聊天记忆仓库连接到特定的 ChatModel(此处为 Ollama),从而为该模型提供有状态(带记忆)的会话能力。
关键组件解释:
MessageWindowChatMemory: 记忆策略。它定义了如何管理对话历史,例如只保留最近的 N 条消息(此处为 100 条)。jdbcChatMemoryRepository: 记忆仓库 (Repository)。它是MessageWindowChatMemory实际存储和读取数据的地方(PostgreSQL 数据库)。MessageChatMemoryAdvisor: AOP 切面/建议器。它是 Spring AI 机制的核心,负责在 ChatModel 调用前后自动执行记忆的读取和写入操作,从而将无状态的模型调用转化为有状态的对话。
2.4 记忆对话交互
为了确保多用户和多会话之间记忆的隔离性和准确性,我们需要在每次调用时,向 ChatClient 明确传递一个唯一的会话标识符。Spring AI 通过 ConversationHistoryAdvisor(在底层自动启用)和 CONVERSATION_ID 参数来实现这一点。
2.5 其他记忆方式说明
除了 JDBC 外,chat-memory 模块还支持其他几种持久化方式。
除了 JDBC 外,chat-memory 模块还支持其他几种持久化方式。您可以参考项目中的示例代码来配置:
| 记忆仓库 | 描述 | 适用场景 |
|---|---|---|
RedisChatMemoryRepository | 使用 Redis 存储。 | 高并发、需要低延迟读写、数据量适中。 |
InMemoryChatMemoryRepository | 数据存储在应用程序内存中。 | 测试、开发环境;不适合生产环境(数据不持久化)。 |
您只需要替换 jdbcChatMemoryRepository 为相应的实现,并在其 Bean 上添加 @Primary (如果需要) 即可
三、提示词
提示词(Prompt)是与大语言模型(LLM)交互的核心载体和指令集。它本质上是提供给模型的一段输入文本,用于引导模型执行特定的任务、遵循特定的规则,并产生期望的输出。
在 Spring AI 中,与大型语言模型(LLM)的交互是通过 Prompt 对象完成的,而 Prompt 内部由一系列不同角色的 消息 (Message) 组成。掌握这些消息类型是精确控制模型行为的关键。
3.1 消息类型
| 消息类型 | 角色 (Role) | 目的 | 使用场景 |
|---|---|---|---|
SystemMessage | System (系统) | 设定模型的身份、行为、约束和通用指令。它定义了 LLM 的“人设”。 | 设定角色: “你是一个法律助手”。 格式要求: “请以 JSON 格式返回”。 |
UserMessage | User (用户) | 包含用户的实际问题、请求或输入。 | 用户的提问、指令。 |
AssistantMessage | Assistant (助手) | 包含模型历史的回复。 | 用于在构建 Prompt 时,手动加入历史对话(通常由 ChatMemory 自动管理)。 |
ToolMessage | Tool (工具) | 包含 LLM 请求调用的外部工具执行后的结果或输出。 | LLM 决定调用一个函数(例如:get_weather(location))。应用执行该函数得到结果(例如: {"temperature": "25°C"})。ToolMessage 将这个结果反馈给 LLM,让其基于此结果生成最终的用户回复。 |
3.2 使用示例
下面分别是 ChatClient 和 ChatModel 的使用方式:
3.3 提示词模板
提示词模板是 Spring AI 中用于创建动态、可复用提示词的核心工具。它允许开发者将固定的指令(如角色设定、格式要求)与动态的运行时参数(如用户输入、主题、字数)结合起来,从而灵活地构造出发送给 LLM 的 Prompt 对象。
| 类名 | 描述 | 作用 |
|---|---|---|
PromptTemplate | 最基础的模板类,用于构造 UserMessage 或简单的单条 Message。 | 接收一个包含占位符(如 {topic})的字符串,并根据 Map 参数填充。 |
SystemPromptTemplate | 专用于构造 SystemMessage 的模板类。 | 确保生成的 Message 具有系统角色的语义,用于设置模型身份或约束。 |
@Value("classpath:/...") | Spring Resource Loader。 | 允许将复杂的提示词内容存储在外部文件(如 .txt)中,使代码更清晰。 |
3.3.1 使用示例
3.3.1.1 字符串内联模板
特点: 最直接的使用方式,适用于模板内容较短的场景。
- 调用方式: 使用
ChatClient.prompt(Prompt)进行流式调用 (.stream().content()),简洁高效。 ##### 3.3.1.2 外部文件模板 此示例展示了如何将模板内容外置到文件 (template.txt) 中,并通过 Spring Resource 加载。java @Value("classpath:/prompt-template/template.txt") private org.springframework.core.io.Resource userTemplate; public Flux<String> story() { // 使用 Resource 构造 PromptTemplate PromptTemplate promptTemplate = new PromptTemplate(userTemplate); // 创建 Prompt 对象 Prompt prompt = promptTemplate.create(Map.of("topic", topic, "output_format", outputFormat, "word_count", wordCount)); // 流式调用 (使用 ChatModel) return deepseekChatModel.stream(prompt).mapNotNull( chatResponse -> chatResponse.getResult().getOutput().getText() ); } 特点: 模板内容(如故事格式、字数要求)存储在外部文件,便于维护和修改,代码更整洁。
调用方式: 使用
ChatModel.stream(Prompt),需要手动通过.mapNotNull(...)提取文本内容。3.3.1.3 系统模板 + 用户模板
此示例是最完整的提示词构建模式,它分离了系统指令和用户输入,分别使用对应的模板类构建消息,最后组合成一个
Prompt。CodeBlock Loading...
四、结构化输出
结构化输出功能允许开发者定义 Java 对象(如 Record 或 POJO)的结构,并让 LLM 的回复直接映射到该对象实例上。这通过将 JSON Schema 注入到提示词中实现,有效地将 LLM 的文本生成任务转化为数据生成任务
4.1 核心机制与优势
| 机制 | 描述 | 优势 |
|---|---|---|
| JSON Schema 注入 | Spring AI 库将目标 Java 类(例如 StudentRecord.class)转化为 JSON Schema 定义,并将其作为 SystemMessage 的一部分发送给 LLM。 | 强制模型输出符合该 Schema 的 JSON 字符串。 |
| 数据绑定 | LLM 返回 JSON 字符串后,Spring AI 自动将该 JSON 字符串反序列化(Deserialization)绑定到您指定的 Java 类实例上。 | 无需手动编写 JSON 解析代码,确保数据的类型安全和准确性。 |
Record 类支持 | 推荐使用 Java 14+ 的 Record 类型,它们简洁、不可变且自动提供了构造函数和访问器。 | 简化数据模型的创建。 |
4.2 使用示例
前提:数据模型定义 (StudentRecord)
假设您有一个用于接收结构化数据的 Java Record 类(或其他 POJO):
4.3 注意事项
- 模型能力依赖:结构化输出的成功率高度依赖于底层 LLM(如 Qwen-Plus/GPT-4 等)对 JSON Schema 和 函数调用(Function Calling) 的支持及理解能力。对于较弱的模型,结果可能不准确或返回非 JSON 文本。
- 提示词质量:用户提示词(
stringTemplate)应包含足够且明确的信息,以便模型能够准确地将信息提取并映射到StudentRecord的字段上。
五、文本向量化 存储 检索
文本向量化(也称为文本嵌入,Embedding)是将人类可读的文本信息转换为计算机可以理解和处理的数值形式(即高维向量)的过程。 文本向量化将文本的含义(语义)映射到向量空间中的位置。在向量空间中,语义相似的词语或句子(例如“狗”和“宠物”)会彼此靠近,而语义不相关的会彼此远离。 传统的全文检索(如关键词匹配)无法理解“意思”。通过向量化,我们可以计算用户问题向量与知识库中文本向量的距离(相似度),从而实现基于含义的检索,这是 RAG 机制的基石。 将文本转换为向量后,计算机可以使用成熟的线性代数和距离算法(如余弦相似度)进行快速、大规模的量化分析和搜索,极大地提高了处理效率。
5.1 前提条件
- 准备向量数据库实例:
- 你需要准备至少一种向量数据库 您需要准备一个正在运行的向量数据库实例,例如:
- PostgreSQL + PgVector 扩展
- Redis + RediSearch 模块
- Chroma、Milvus 等专用向量数据库。
- 安装要求: 请确保您已按照先前文档或官方指南完成了所选数据库的安装和配置。
- 准备 Embedding 模型 (Embedding Model):
- 向量存储要求将文本数据转换为数值向量(即嵌入)。因此,您必须配置一个可用的 Embedding 模型:
- Ollama Embedding Model: 适合本地开发和测试。
- DashScope Embedding Model: 阿里云通义千问的 Embedding 服务。
- OpenAI、Mistral 等其他供应商的 Embedding 模型。
- 向量存储要求将文本数据转换为数值向量(即嵌入)。因此,您必须配置一个可用的 Embedding 模型:
- 配置要求: 确保您的
application.properties/application.yml中已配置相应的 API Key 或 Base URL。
5.2 相关依赖
5.3 相关配置
5.4 解决多 Vector Store 冲突的方案
Spring AI 为每个支持的向量数据库提供了 自动配置(AutoConfiguration) 类。在配置了相应的连接信息后,这些自动配置类会尝试创建一个名为 vectorStore 的 Bean。
如果您的项目中同时引入了多个向量数据库的依赖(例如,同时引入了 Redis 和 PgVector 的依赖),Spring Boot 将尝试执行以下操作:
PgVector 自动配置 尝试注册一个名为
vectorStore的 Bean。Redis 自动配置 随后也尝试注册一个名为
vectorStore的 Bean。
由于 Spring Boot 默认禁止覆盖已注册的 Bean 定义,这会导致底层的 Bean 名称注册冲突,应用程序会抛出错误并启动失败:
错误信息示例: The bean 'vectorStore' ... could not be registered. A bean with that name has already been defined... and overriding is disabled.
Bean 定义名称冲突会直接阻止 Spring 容器完成初始化。
解决方案:手动排除自动配置并创建 Bean
为了避免这种底层的名称冲突,最健壮的方法是阻止 Spring Boot 自动加载所有冲突的 VectorStore 自动配置类,然后手动创建您需要的那个 Bean。
- 禁用自动配置: 在主启动类上使用
@SpringBootApplication(exclude = ...)注解,明确禁用冲突的AutoConfiguration类。
手动创建 Bean: 在您的自定义配置类中,手动创建并注册您想使用的
VectorStore实例(例如 PgVectorStore)。因为引入了阿里的DashScope有多个
EmbeddingModel,需要手动指定一些用哪个EmbeddingModelCodeBlock Loading...5.5 文本向量化实现
文本向量化的实现原理是通过深度学习模型(即 Embedding Model)将文本输入映射到高维空间中的一个密集数值向量,确保语义相似的文本在向量空间中彼此靠近。 示例代码:
CodeBlock Loading...
5.6 向量存储
为什么需要向量存储?向量存储是为了高效地存储和检索大量的高维文本向量,它是实现 RAG(检索增强生成)机制中语义检索功能的核心基础设施。
如果你是 pgvector,需要如下检查。需要检查你创建的 vector 类型和 embedding 模型所使用的向量维度是否一致。
示例代码:
5.7 相似度检索
相似度检索的原理是计算用户查询文本(已向量化)与向量存储中所有文档向量之间的距离或夹角(如余弦相似度),以找出向量空间中距离最近(即语义最相关)的 个文档片段。
使用示例:
该示例展示了如何使用 SearchRequest 向配置好的 Redis 和 PgVector 向量存储发起检索请求,并获取最相似的 Top K 文档。
六、RAG增加检索
上面我们已经实现了向量存储和向量检索的基础,下面我们将核心实现RAG(Retrieval-Augmented Generation),即给大模型外挂一个自定义“知识库”。
RAG的流程是:用户提出问题后,系统先从知识库中检索出最相关的内容片段(即Context),然后将这些内容和用户的问题一起喂给大模型(LLM),引导大模型基于这些上下文来生成回答。
依赖和配置与之前相同,此处不再赘述。
6.1 知识库内容示例
我们将使用以下自定义运维错误码作为我们的知识库:
classpath:/knowledge-base/ops.txt位置的文本内容如下
正常情况下,大模型并不知道我们自定义的这些错误码的含义。通过 RAG 机制,我们可以确保大模型在回答相关问题时能够查阅并引用这些专业知识。
6.2 知识库加载与向量存储初始化
这一步骤负责将本地的知识文本(ops.txt)读取、分割、并将其嵌入(Embedding)后存入向量数据库(这里是 PostgreSQL 和 Redis),以便后续进行语义检索。
核心组件:
TextReader:读取文件内容。
TokenTextSplitter:将大块文本分割成适合嵌入的小块Document。
VectorStore:存储嵌入后的向量。防重复加载机制: 利用 Redis 的
setIfAbsent确保在服务重启时不会重复加载相同的知识库。
6.3 实现 RAG 检索增强
我们使用 Spring AI 提供的 RetrievalAugmentationAdvisor 来实现 RAG 机制。这个 Advisor 会在发送请求给大模型之前自动执行检索,并将检索结果作为上下文(Context)注入到最终的 Prompt 中。
当用户调用 redisRag("A0100") 时:
RetrievalAugmentationAdvisor会根据用户输入的"A0100"到redisVectorStore中进行语义检索。检索结果(例如:
A0100 用户注册错误二级宏观错误码)被提取出来,作为 Context。Advisor 将 Context、
systemInfo和用户问题"A0100"组合成一个最终的 Prompt 发送给大模型。大模型根据这个增强后的 Prompt,结合 Context 给出准确的回答。
七、工具调用
工具调用机制允许大语言模型(LLM)在理解用户的意图后,识别并调用外部定义的函数或服务,从而获取实时信息、执行特定操作或访问外部系统。它极大地扩展了 LLM 的能力边界。
7.1 为什么需要工具调用?
大模型的知识通常截止于其训练数据的截止日期,它无法:
- 获取实时信息: 例如,当前时间、天气、实时股价等。
- 执行外部操作: 例如,发送邮件、调用支付接口、查询数据库等。
工具调用机制通过以下方式解决了这些限制:
| 机制 | 描述 | 示例 |
|---|---|---|
| 功能描述暴露 | 开发者将外部函数(工具)的名称、用途和参数通过 JSON Schema 格式描述给 LLM。 | 告诉 LLM 有一个名为 getCurrentTime 的函数,用来“获取当前时间”。 |
| 意图推理 | LLM 根据用户输入,推理出用户是否需要调用某个工具,并决定调用哪个工具及其所需参数。 | 用户问“现在几点?”,LLM 推理出需要调用 getCurrentTime()。 |
| 执行与生成 | 应用接收到 LLM 的调用指令后,执行该工具,将工具的结果作为新的上下文返回给 LLM,LLM 再基于结果生成最终回复。 | 应用执行 getCurrentTime() 返回 2025-12-02T17:28:23.123,LLM 回复:“现在是 2025 年 12 月 2 日下午 5 点 28 分。” |
7.2 定义工具类
在 Spring AI 中,您只需要在 Java 方法上使用 @Tool 注解,即可将其暴露为 LLM 可以理解的工具。
7.3 调用工具
Spring AI 提供了两种主要方式来集成和调用工具:基于低级 ChatModel 和基于高级 ChatClient。
7.3.1 基于 ChatModel 实现的工具调用
这种方式需要手动构造 ToolCallbacks 数组,并将其注入到 ChatOptions 中,然后传递给 ChatModel。
7.3.2 基于 ChatClient 实现的工具调用
ChatClient 提供了更简洁的流式 API,它在底层自动封装了工具注册和配置的细节。
八、MCP
MCP (Model-Powered Control Plane) 是一种架构,利用 AI Agent 的推理能力,将自然语言指令转化为对复杂系统的编排和管理。工具调用 (Tool Calling) 则是实现 MCP 的底层机制,它为 LLM 提供了调用外部函数的能力;简单来说,工具调用是 LLM 的“手脚”,而 MCP 是利用这些“手脚”去管理复杂系统的 “大脑”和框架。
8.1 MCP 服务端
8.1.1依赖
8.1.2 配置文件
8.1.3 工具定义与暴露
在模型驱动控制平面(MCP)架构中,服务端负责定义并暴露一系列具体的业务功能或操作(即工具)。这些工具将被 AI Agent 或 MCP 客户端调用,以执行复杂任务。
8.1.3.1 编写服务类并暴露工具方法
我们通过在服务类方法上使用 Spring AI 的 @Tool 注解,将方法标记为可供大模型调用的工具。
8.1.3.2 示例:城市旅游新闻服务
以下是一个模拟的在线旅游新闻服务,它接受城市名称作为参数,并返回该城市的今日旅游新闻摘要。
8.1.4 将工具注册到 MCP 框架
为了让 MCP 客户端(即调用大模型的应用)能够访问和使用这些工具,我们需要通过 Spring AI 的配置机制,将这些服务实例注册为一个工具回调提供者(ToolCallbackProvider)。
核心组件:
ToolCallbackProvider: 这是 Spring AI 中用于集中管理和暴露所有工具定义的接口。
MethodToolCallbackProvider: 默认实现,它通过反射机制扫描传入的 Java 对象(toolObjects)中所有带有@Tool注解的方法,并将它们转换成大模型能够理解的 JSON Schema 格式。CodeBlock Loading...
8.2 MCP 客户端
MCP 客户端是 AI Agent 侧的应用程序,它负责连接到 MCP 服务端,获取工具的定义(JSON Schema),并在需要时发起对远程工具的实际调用。
8.2.1 客户端依赖
8.2.2 客户端配置文件
8.2.3 客户端配置:集成远程工具到 ChatClient
为了让 ChatClient 在每次调用时都知晓并携带可用的工具定义,我们需要将 MCP 客户端获取到的远程工具注入为 ChatClient 的默认工具回调。
8.2.4 调用MCP服务
一旦 ChatClient 被配置了 MCP 提供的远程工具,客户端的调用代码就变得极其简洁。开发者无需关心工具在本地还是远程,只需像调用普通 LLM 一样发起请求。
8.3 调用其他的MCP服务(postgres mcp)
除了连接远程 HTTP/SSE 服务外,Spring AI 的 MCP 客户端还支持通过标准输入/输出(stdio)方式,启动并连接本地进程运行的 MCP 服务(如用于数据库查询的 MCP)。
这将赋予大模型直接查询本地数据库结构和数据的能力,无需编写传统的 SQL 代码。
依赖用上面mcp 客户端依赖
8.3.1 准备服务 JSON 文件 (classpath:/mcp.json)
我们使用一个 JSON 文件来定义需要通过本地进程启动和管理的 MCP 服务。这里我们配置一个名为 postgres 的服务。
注意: 这里的配置是指示 Spring AI 客户端如何通过 Node.js (
npx) 启动并连接 PostgreSQL MCP 服务。你需要确保本地环境有 Node.js 和npx可用。
这里可以添加多个mcp,可访问mcp server获取更多mcp服务。
8.3.2 配置文件
8.3.3 注册到 ChatClient
与集成远程 HTTP/SSE 服务一样,我们需要将 MCP 客户端获取到的所有工具(包括 PostgreSQL 查询工具)注册给 ChatClient。
8.3.4 调用 MCP 服务(数据库查询)
一旦配置完成,大模型就获得了查询数据库的能力。您可以向它提出关于数据库结构的自然语言问题,它将通过调用 PostgreSQL MCP 工具来获取数据并生成回答。