前言
因为这几周在公司实习,需要接触新的语言和框架,每天都在看大量的技术文档和公司的项目代码,于是乎想着能不能摸一个可以使用的流程出来快速入手一个项目和背后需要使用的组件(框架/语言/中间件)的工作流出来。
又刚好最近想看看SpringAI相关的文档,于是拿了鱼皮的其中一个项目进行练手,总用时不到一天(一个下午加一个晚上快速看完了一个项目和SpringAI的使用方法(项目中有使用到的东西和一些衍生的内容),又话了一个晚上把内容整理如下。
本教程只是作为快速学习使用,不涉及到任何敲打码的部分,要对一个东西熟悉还是得老老实实的敲代码进行熟悉!但是本教程可以让你快速掌握你所需要的知识逻辑(我自己认为的)
基础工具需求
- 任何一个你使用的ai工具(gemini-cli,claude-code,cursor,argument…)
- 因为他可以自动调用工具来查看你的内容就不需要你手动的cv了
- 博主使用gemin-cli 原因是 可以白嫖哈哈哈哈(当然他自带的google搜索也是很好用的)
- 如果有钱肯定是claude-code更好用!
- 具体安装教程参考网上其他文章。
- 你对你需要了解的项目整体架构有一定的了解(如springboot项目你肯定需要知道MVC是什么之类的东西,当然如果你不知道也不是不可以,就是你可以根据我下面的技巧先让ai给你讲清楚基础的东西然后再进一步学习(一步一个脚印才是正道,不可能一下子一步到位的!)
- 理解大模型幻觉、上下文、提示词技巧及其相关概念
开始!
以gemin-cli为例讲一下一开始的使用技巧
/init
:这个命令可以让gemini了解你的项目并且再你的项目下面添加GEMINI.md
文件/chat save <tag>
:这个命令可以帮助你需要的时候保存上文对话进行分支处理学习/chat resume <tag>
:这个命令可以让你回到之前的某个对话当中继续问答
/memory add <your_prompt>
:这个命令可以让你给Gemini每次都有特定的提示词:记录在GEMINI.md
中 ,并且这个提示词可以分为 全局、项目、个人- 全局文件位置:
~/.gemini/GEMINI.md
- 项目文件位置:项目根目录下面的
GEMINI.md
(使用/init
会自动生成,也可以手动添加) /memory reflash
:手动更新GEMINI.md
时用来刷新prompt
- 全局文件位置:
@path/to/your/file
:这个命令后面跟一个文件或者文件夹的路径,ai会一次性把这些文件里面的内容作为上下文发送给ai- 启动
yolo
模式:在启动 Gemini CLI 时,直接在命令后面加上--yolo
标志。在此模式下,你授权 Gemini CLI 无需每次都寻求你的批准,即可直接执行它认为必要的操作,比如运行 shell 命令、读写文件等。因为我们只需要让ai去读取文件,所以不会导致项目被修改,如果手动确认就很麻烦,开启这个就省事很多 /compress
:这个命令可以让ai总结一下上下文进行长上文的替换,适合聊的内容很长的情况下面- 其他内容参考官方文档:gemini-cli/docs/cli at main · google-gemini/gemini-cli
一开始的试错过程
我的目的就是让ai帮我进行讲解整个项目的运行过程和关于springai的使用技巧
但是一开始我为了省事,以为他会自己调用工具详细的为我进行查询,所以只是使用了简单的prompt询问ai相关的内容,
“详细解读一下这个项目,给出详细的的框架和实现思路供我详细学习。”
“对于 AI 部分的实现逻辑,以 `doChatWithLoveAppServerSentEvent` 为请求起点,给我详细说明这一条路上面使用到的 AI 相关的技术和实现逻辑…”
在这个过程中你会发现ai他最多只会关联其中的一两个文件(具体回答找不到了就不展示了),并且对于流程中的内容并不会详细的展开,只会蜻蜓点水式的讲解过去,如果要一个个去询问就太浪费时间了。
这时候我们需要先优化我们的提示词,这个过程可以让ai帮助你进行优化,例如:
下面是截取的我和gemin的对话过程,因为不能用md格式展示,所以用了分割修和text代替,读者只需要知道我总结这个对话有什么问题就好了,这里只是一个片段示例(包括后面也一样)
我提问
我想要使用ai帮我快速读懂一个项目,我的提示词应该要怎么修改?,目前是:
详细解读一下这个项目,给出详细的的框架和实现思路供我详细学习:
ai: xxxx
对于ai部分的实现逻辑,以doChatWithLoveAppServerSentEvent为请求起点,给我详细说明这一条路上面使用的到的ai相关的技术和实现逻辑,不要过多的列点,详细吧技术说明清楚就可以!:
ai: xxxx
对于这里面涉及到的springai相关的api给我根据项目和官方文档进行详细说明涉及ai的相关步骤流程(务必调用mcp进行查询对应的文档,例如:https://docs.spring.io/spring-ai/reference/api/chat-memory.html,因为你的知识可能是过时的!)
ai:xxxx
但是ai的回复总是在列点,并没有徐徐渐进的教学过程,文档引用说明也很差,我的prompt应该要怎么设计?
ai回答
第一步:项目整体理解
我需要深入学习这个项目,请按照以下方式帮我理解:
**分析方式**:
- 采用"总-分-总"的结构,先给出项目的核心价值和设计理念
- 用故事化的方式描述项目的业务流程,就像在讲述一个完整的故事
- 对于技术架构,请用类比的方式解释复杂概念
- 避免简单的功能列表,而是解释"为什么这样设计"
**回答结构**:
1. 项目核心理念(用一段话概括这个项目解决了什么问题)
2. 架构设计思路(解释架构选择的原因和优势)
3. 关键技术决策(为什么选择这些技术栈)
4. 实现亮点(有哪些值得学习的设计模式或技巧)
项目代码:[你的代码]
第二步:核心流程深度解析(融合API文档研究)
现在我需要深入理解 `doChatWithLoveAppServerSentEvent` 这个核心流程,并在分析过程中同步学习相关的Spring AI技术。
**分析方式**:
- 把这个方法当作一个技术故事的主线,从用户请求开始,按时间顺序深入分析每个环节
- 遇到Spring AI相关的代码时,立即查询最新官方文档进行详细解释
- 用"代码实现 + 官方文档 + 实践解读"三位一体的方式来解析每个技术点
- 重点关注数据流转和AI处理的完整链路
**具体要求**:
1. **流程分析结构**:
- 请求接收与参数处理
- 数据预处理与上下文构建
- AI模型调用与配置
- 流式响应处理
- 结果返回与异常处理
2. **对于每个涉及Spring AI的关键步骤,请这样处理**:
- 先展示项目中的具体代码实现
- 立即查询对应的Spring AI官方文档(如 https://docs.spring.io/spring-ai/reference/)
- 引用官方文档的准确描述和参数说明
- 解释项目中的配置选择和使用技巧
- 说明这样实现的优势和注意事项
3. **重点关注的技术点**(遇到时详细展开):
- ChatClient/ChatModel的使用和配置
- Prompt模板和参数绑定
- 流式响应(Server-Sent Events)的实现
- 上下文管理和会话状态
- 异常处理和重试机制
- 性能优化策略
4. **输出格式要求**:
- 用连贯的叙述方式,就像在进行一对一的技术指导
- 每个技术点都要有"是什么 → 怎么用 → 为什么这样用"的完整解释
- 提供可以直接参考的代码示例和配置示例
- 在适当位置插入官方文档链接和关键信息引用
**开始分析这个核心方法**:[指定的方法或代码段]
**注意**:请确保查询最新的Spring AI文档,因为这个框架更新较快,过时的信息可能会误导学习。
关键改进点
- 流程驱动:以核心流程为主线,遇到API时就地深入
- 即时文档查询:在分析具体代码时立即查询相关文档
- 三位一体:代码实现 + 官方文档 + 实践解读同步进行
- 连贯叙述:避免割裂的知识点,保持学习的连续性
- 实用导向:每个技术点都要解释实际应用价值
这样的提示词会让AI在分析核心流程的同时,自然地引入相关的Spring AI知识,形成更完整的学习体验。
修改完成后的对话内容
如下:
ai回答
好的,我们开始深入分析 doChatWithLoveAppServerSentEvent
这个核心流程。这确实是学习 Spring AI 的一个绝佳切入点,因为它涵盖了从接收请求到流式返回AI结果的全过程。
我将遵循你的要求,把这次分析当作一次技术故事分享,一步步揭开它的神秘面紗。
准备工作:定位核心代码
首先,我需要找到 doChatWithLoveAppServerSentEvent
方法所在的具体文件和代码。根据命名习惯,它很可能在 AiController.java
中。
**✔ SearchText ‘doChatWithLoveAppServerSentEvent’ in /*.java
找到了!它在
src/main/java/com/yupi/yuaiagent/controller/AiController.java
。现在,我们来阅读这个文件的全部内容,以便获得完整的上下文。
✔ ReadFile
src\controller\AiController.java
很好,代码已经拿到。现在,我们正式开始我们的技术故事。
技术故事:doChatWithLoveAppServerSentEvent
的诞生之旅
想象一下,用户在前端的恋爱大师应用里输入了一个问题,点击发送。这个请求穿越网络,最终抵达了我们的 Spring Boot 后端。它的第一站,就是 AiController
。
第一站:请求接收与参数处理
我们的旅程从 AiController
的这个方法签名开始:
代码实现:
@GetMapping(value = "/love_app/chat/server_sent_event")
public Flux<ServerSentEvent<String>> doChatWithLoveAppServerSentEvent(String message, String chatId) {
// ... 方法体
}
实践解读:
@RestController
: 这个类级别的注解告诉 Spring,AiController
的所有方法都会直接返回数据(比如 JSON、XML 或我们这里的事件流),而不是视图(比如 HTML 页面)。这是构建 API 服务的标准实践。@GetMapping
:是什么:这是 Spring MVC 的核心注解之一,用于将 HTTP GET 请求映射到特定的处理方法上。
怎么用:
value = "/love_app/chat/server_sent_event"
: 定义了访问这个方法的 URL 路径。客户端需要向http://<服务器地址>/ai/love_app/chat/server_sent_event
发送 GET 请求。
为什么这样用:对于聊天这种获取信息的场景,使用 GET 请求是符合 RESTful 风格的。虽然 POST 也可以,但 GET 更简洁,且默认支持浏览器直接访问测试。
方法参数
(String message, String chatId)
:是什么:Spring MVC 会自动将请求中的查询参数(Query Parameters)绑定到这些同名的方法参数上。
怎么用:例如,一个请求
?message=你好&chatId=123
,Spring 会自动把 “你好” 赋值给message
变量,“123” 赋值给chatId
变量。为什么这样用:这是 Spring MVC 提供的最便捷的参数绑定方式,无需手动解析
HttpServletRequest
,代码非常整洁。
第二站:AI模型调用与数据流转
请求进来了,参数也拿到了。接下来,控制器将任务委托给了专门的应用逻辑层——LoveApp
。
代码实现:
// AiController.java
@Resource
private LoveApp loveApp;
// ... in doChatWithLoveAppServerSentEvent method
return loveApp.doChatByStream(message, chatId)
.map(chunk -> ServerSentEvent.<String>builder()
.data(chunk)
.build());
这里是整个流程的核心,我们看到 loveApp.doChatByStream
是实际与 AI 交互的入口。它返回了一个 Flux<String>
,这是一个响应式流(Reactive Stream),代表着未来会陆续到达的一系列字符串(AI生成的内容块)。
为了彻底搞清楚,我们必须潜入 LoveApp.java
。
✔ ReadFile
src\yupi\yuaiagent\app\LoveApp.java
LoveApp.java
的代码非常关键,我们重点看doChatByStream
方法和构造函数。
代码实现 (LoveApp.java
)
// 构造函数
public LoveApp(ChatModel dashscopeChatModel) {
// ... (ChatMemory setup)
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new MyLoggerAdvisor()
)
.build();
}
// 核心流式方法
public Flux<String> doChatByStream(String message, String chatId) {
return chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.stream()
.content();
}
这里终于出现了 Spring AI 的核心组件:ChatClient
。
Spring AI 技术点:ChatClient
是什么:
ChatClient
是 Spring AI 提供的流畅(Fluent)API,用于与 AI 模型进行交互。它是对底层ChatModel
的一个高级封装,简化了提示词构建、模型调用、结果处理等一系列操作。你可以把它想象成一个功能强大的 AI 对话助手。怎么用(官方文档解读):
根据 Spring AI 官方文档,
ChatClient
提供了一个流畅的构建器模式,允许开发者链式调用方法来构建一个完整的 AI 请求。为什么这样用(
ChatClient
的优势):链式调用,代码优雅:看
doChatByStream
方法,.prompt().user().advisors().stream().content()
一气呵成,代码可读性极高。配置与代码分离:
ChatClient
是通过ChatClient.builder(dashscopeChatModel)
创建的。dashscopeChatModel
是一个ChatModel
类型的 Bean,它的具体实现(比如是通义千问、智谱AI还是OpenAI)和配置(如API Key, model name, temperature等)都在application.yml
中定义。这使得切换底层大模型无需修改 Java 代码,实现了高度解耦。内置高级功能:构造函数中的
.defaultAdvisors(...)
是一个亮点。Advisor(建议器)是 Spring AI 的一个强大概念,它可以在不侵入核心业务逻辑的情况下,为ChatClient
添加额外功能,比如:MessageChatMemoryAdvisor
: 自动管理多轮对话历史,这是实现上下文感知对话的关键。MyLoggerAdvisor
: 这是项目自定义的 Advisor,用于记录详细的交互日志,方便调试。QuestionAnswerAdvisor
: 用于实现 RAG(检索增强生成),让 AI 能基于私有知识库回答问题。
Spring AI 技术点:上下文管理 (ChatMemory
)
是什么:在多轮对话中,AI 需要“记住”之前的交流内容,才能理解上下文。
ChatMemory
就是 Spring AI 中负责存储和管理对话历史的组件。怎么用(项目实践):
// LoveApp.java 构造函数 MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(new InMemoryChatMemoryRepository()) .maxMessages(20) .build(); // ... chatClient = ChatClient.builder(dashscopeChatModel) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build() ) .build(); // LoveApp.java doChatByStream 方法 .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
创建
ChatMemory
实例:这里创建了一个MessageWindowChatMemory
,它使用InMemoryChatMemoryRepository
将对话历史保存在内存中,并且通过maxMessages(20)
设置只保留最近的20条消息,防止上下文过长超出模型限制。注册为
Advisor
:通过MessageChatMemoryAdvisor
将chatMemory
注册到ChatClient
中。这样,ChatClient
在每次调用前,都会自动从chatMemory
加载历史消息,并在调用后保存新的问答对。指定会话ID:
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
这一行至关重要。它告诉MessageChatMemoryAdvisor
,当前的对话属于哪个会话(由chatId
标识)。这样,系统就能同时为多个不同的用户维护各自独立的对话历史,互不干扰。
为什么这样用:Spring AI 的这套
ChatMemory
+Advisor
机制,将复杂的会话状态管理完全自动化了。开发者只需要在调用时传入一个会话ID,剩下的加载、保存、截断等工作都由框架完成,极大地降低了开发复杂度。
第三站:流式响应处理 (Server-Sent Events)
我们回到 AiController
,看看 Flux<String>
是如何变成前端可以接收的事件流的。
代码实现 (AiController.java
)
@GetMapping(value = "/love_app/chat/server_sent_event")
public Flux<ServerSentEvent<String>> doChatWithLoveAppServerSentEvent(String message, String chatId) {
return loveApp.doChatByStream(message, chatId)
.map(chunk -> ServerSentEvent.<String>builder()
.data(chunk)
.build());
}
Spring 技术点:Flux
与 ServerSentEvent
(SSE)
是什么:
Flux
是 Project Reactor(Spring WebFlux 的核心)中的一个核心类,代表一个包含 0 到 N 个元素的异步序列。在这里,Flux<String>
就代表了 AI 模型陆续生成的文本块流。Server-Sent Events (SSE)
是一种服务器推送技术,允许服务器随时向客户端发送数据。它基于简单的文本协议,非常适合实现类似打字机效果的流式响应。
怎么用:
loveApp.doChatByStream(...)
返回一个Flux<String>
。.map(...)
操作符是响应式编程的关键。它会对 Flux 流中的每一个元素(这里是每个chunk
字符串)应用一个转换函数。ServerSentEvent.<String>builder().data(chunk).build()
: 这个函数将普通的字符串chunk
包装成一个标准的ServerSentEvent
对象。SSE 协议规定了事件的格式,data:
字段就是事件的内容。Spring WebFlux 检测到方法的返回类型是
Flux<ServerSentEvent<String>>
,会自动设置Content-Type
为text/event-stream
,并按照 SSE 协议将每个ServerSentEvent
对象序列化后发送给客户端。
为什么这样用:
完美契合:AI 模型的流式输出(一个一个地生成 token)和响应式流
Flux
是天作之合。而Flux
与 Spring WebFlux 对 SSE 的原生支持,使得将后端 AI 流对接到前端变成了一件非常自然和简单的事情。非阻塞:整个处理过程是完全非阻塞的。从接收请求到等待 AI 模型生成内容,再到发送给客户端,都不会阻塞服务器线程。这使得应用能够用更少的资源处理更高的并发请求,性能和伸缩性都非常好。
第四站:结果返回与异常处理
结果返回:如上所述,结果是通过
Flux
流持续不断地推送到客户端的。当loveApp.doChatByStream
返回的Flux
流正常结束时(AI 回答完毕),Spring WebFlux 会自动关闭 SSE 连接。异常处理:
代码实现:在当前的
doChatWithLoveAppServerSentEvent
方法中,没有显式的try-catch
或错误处理。这是响应式编程的一个特点。实践解读:异常是在
Flux
流中作为错误信号(error signal)传播的。如果在loveApp.doChatByStream
的执行过程中(比如调用 AI 模型时网络超时、API Key 错误等)发生了异常,这个异常会终止Flux
流,并作为一个错误信号向下游传递。Spring WebFlux 捕获到这个错误信号后,会立即中断与客户端的 SSE 连接。客户端的
EventSource
对象会触发onerror
事件。更健壮的处理:在生产环境中,可以对 Flux 流添加错误处理操作符,例如:
Java
return loveApp.doChatByStream(message, chatId) .map(chunk -> ServerSentEvent.<String>builder().data(chunk).build()) .doOnError(e -> log.error("SSE stream error", e)) // 记录日志 .onErrorResume(e -> Flux.just(ServerSentEvent.<String>builder() .event("error") // 发送一个自定义的错误事件 .data("抱歉,服务出错了:" + e.getMessage()) .build()));
这样可以在发生错误时,给前端一个明确的错误提示,而不是直接断开连接,用户体验更好。
总结与回顾
我们的 doChatWithLoveAppServerSentEvent
技术故事到此结束。我们完整地走过了这条链路:
AiController
使用@GetMapping
接收前端请求。请求被分派给
LoveApp
,并调用其doChatByStream
方法。LoveApp
内部使用ChatClient
(一个强大的 Spring AI 组件)来与 AI 模型交互。ChatClient
利用MessageChatMemoryAdvisor
自动管理基于chatId
的多轮对话上下文。ChatClient
的.stream().content()
方法返回一个Flux<String>
响应式流,代表 AI 的流式输出。回到
AiController
,.map()
操作符将 Flux 中的每个字符串块包装成ServerSentEvent
对象。Spring WebFlux 自动处理这个
Flux<ServerSentEvent<String>>
,建立 SSE 连接,并将 AI 的回答源源不断地推送给前端。
通过这个过程,我们不仅理解了业务流程,还深入学习了 Spring AI 的核心组件 ChatClient
、ChatMemory
、Advisor
,以及它们如何与 Spring WebFlux 的响应式编程模型无缝集成,最终实现了一个高效、可扩展、代码优雅的流式 AI 应用。
希望这次的深度分析对你有所帮助!
反思ai回复的问题
可以看出内容的分析还是不进入人意,并且在之后的指令跟随就很差了,所以我们需要根据实际情况进行优化我们的prompt,比如目前的回答可能还是有讲解不够深入,没有应用官方文档的问题,并且考虑怎么让这个prompt进行更好的指令跟随?
这里就需要使用我们的/memory add
的命令了(或者手动添加刷新),最终在不断测试效果下,得到了一个满意的prompt,将其写入根目录下面的GEMINI.md
文件里面,内容如下:
## Gemini Added Memories
### 规则
- 所有回复除非我特殊要求都用中文回复
- 涉及到springai相关的知识都务必搜索官方文档:https://docs.spring.io/spring-ai/reference/index.html
### 角色 **1. 你的角色 (Your Role)** 你是一位资深的软件架构师和技术导师。你的唯一专长是,以深入浅出的方式,对**`yu-ai-agent-master`**这个真实项目的功能模块,进行源码级的、端到端的深度剖析。你对该项目的架构、设计模式和代码实现有着全面的、专家级的理解。
**2. 核心使命 (Your Mission)** 我将指定项目中的**一个核心业务方法**作为分析的入口。你的任务是,对我指定的这个方法进行一次**彻底的、无死角的源码分析**。你不仅要清晰地讲解该方法的完整执行流程,更要深挖其背后依赖的所有Spring / Spring AI组件的**来源、配置、核心原理、官方文档定义和架构设计上的权衡(The "Why")**。
#### **第二部分:项目背景知识库 (Project Knowledge Base)**
在所有分析开始前,你必须始终牢记以下关于`yu-ai-agent-master`项目的核心知识。
1. **项目名称**: `yu-ai-agent-master`
2. **核心目标**: 一个基于Spring AI框架构建的全功能AI Agent应用,旨在作为学习和实践的范例。
3. **技术架构**:
- **后端 (`yu-ai-agent-master`)**: Spring Boot + Spring AI, 承载所有核心AI逻辑。
- **微服务 (`yu-image-search-mcp-server`)**: 独立的Spring Boot应用,作为可被Agent调用的“工具”服务。
4. **核心设计模式**:
- 查询文件目录列表进行判断并且调用工具写入项目的memory里面
- 比如说可能有:
- **RAG (检索增强生成)**: 在`rag/`包下实现,通过向量数据库为AI提供私有知识。
- **Agent & Tools**: 在`agent/`和`tools/`包下实现,赋予AI调用外部Java方法和微服务的能力。
- **Advisors**: 在`advisor/`包下实现,通过AOP思想为`ChatClient`调用链路添加日志、重试等额外功能。
- **渐进式功能展示**: `LoveApp.java`是核心展示类,通过其内部不同方法(`doChat`, `doChatByStream`, `doChatWithReport`, `doChatWithRag`, `doChatWithTools`)由浅入深地展示了Spring AI的各项能力。
#### **第三部分:源码分析的黄金法则 (The Golden Rules of Analysis)**
你必须将以下三条法则奉为圭臬,并在分析的每一个环节严格遵守。
**法则一:彻底的依赖溯源 (Radical Traceability) - “绝无魔法”原则**
- 项目中没有凭空出现的Bean。当分析代码遇到任何注入的依赖(如 `@Autowired`, `@Resource`, 或构造函数注入),你**必须**立即暂停主线流程,切换到“依赖揭秘”模式。
- **追溯源头**: 你必须找到并展示创建该Bean的源头——是它自身的 `@Component`/`@Service` 注解,还是某个 `@Configuration` 类中的 `@Bean` 方法。
- **深度解析创建过程**: 你必须逐行解释其构造函数或 `@Bean` 方法中的每一行代码,特别是Builder模式中的每一个链式调用(如 `.with...()`),讲清楚每个配置的意图和作用。
- **关联配置文件**: 如果创建过程依赖 `application.yml`,必须立即切换到该文件,展示并解释相关的配置块。
**法则二:主动的文档对标 (Proactive Documentation Alignment) - “官方权威”原则**
- 对于分析中遇到的每一个Spring AI核心类、接口或注解(如 `ChatClient`, `VectorStore`, `@Tool`, `QuestionAnswerAdvisor`),你**必须**主动引用最新的Spring AI官方文档。
- **引用定义**: 给出官方对其功能、用途的权威定义。
- **对比实现**: 对比项目中的实际用法,解释这是标准实现、扩展实现,还是针对特定场景的变通,并阐述这样做的原因。
**法则三:解释“为什么” (Explain the 'Why') - “架构师思维”原则**
- 超越代码的表面含义。不仅要说明代码“**做了什么**”,更要从架构师的视角解释“**为什么这么做**”。
- **设计决策**: 解释选择某个特定组件或实现方式背后的设计考量。例如,为什么RAG流程中需要`QueryRewriter`?它的作用是什么?
- **权衡利弊**: 探讨不同方案的优劣。例如,为什么在开发阶段使用内存向量库(`SimpleVectorStore`),它与`PgVector`等生产级方案相比有何利弊?生产部署时需要注意什么?
#### **第四部分:标准执行流程 (Standard Operating Procedure)**
**第一步:上下文扫描与依赖映射 (Pre-Analysis Scan)**
- 在你正式开始分析前,**必须**以我指定的核心方法为起点,进行一次完整的**依赖扫描**。
- **识别并一次性读取**所有与该功能链路相关的Java类和配置文件。这**必须**包括:
- 入口所在的Controller层。
- 所有被调用的Service/App/Manager层。
- **所有相关的 `@Configuration` 配置类**。
- **所有被用作工具的 `@Component` 或 `@Tool` 类**。
- `application.yml` 中所有相关的配置项。
- **分析前公示**: 在分析开始前,先简要列出你识别出的所有相关文件,让我知道你已经掌握了完整的上下文。并且把重要的部分调用工具写入memory里面
**第二步:深度剖析 (The Deep Dive)**
1. **技术故事主线**: 始终保持从用户请求开始,按代码执行顺序讲解的“技术故事”风格。
2. **分析结构**: 严格按照以下逻辑层次展开:
- 请求接收与参数处理
- 数据预处理与上下文构建
- **AI模型调用及其所有依赖组件的深度揭秘 (核心)**
- 流式/同步响应处理
- 结果返回与异常处理
3. **重点关注的技术点** (遇到时按黄金法则详细展开):
- `ChatClient` / `ChatModel` / `EmbeddingModel` 的构建与配置。
- `Prompt` / `PromptTemplate` 的使用和高级技巧。
- **RAG全链路**: `VectorStore` 配置、`DocumentLoader`、`TextSplitter`、`QueryTransformer` 和 `QuestionAnswerAdvisor`。
- **Agent/Tools**: `@Tool` 注解用法,`ToolCallback` 注册管理,`ToolCallbackProvider` 微服务工具配置。
- **上下文管理**: `ChatMemory` 的不同实现及其配置。
- **输出处理**: `.stream()` (流式) vs `.entity()` (结构化输出) 的区别、原理和场景。
#### **第五部分:输出格式与风格 (Output Format & Style)**
- **对话风格**: 保持一对一技术导师的叙述方式,循循善诱。
- **解释闭环**: 对每个技术点,保证“**是什么 → 从哪来/怎么配 → 为什么这样用**”的完整解释闭环。
- **代码为王**: 大量使用格式化后的代码和配置示例,并附上清晰的注释。
- **权威引用**: 在适当位置插入官方文档链接和关键引述,增加你分析的权威性。
- **主动探索**: 你的回答必须是**主动的、探索性的**,而不是被动的、等待追问的。默认所有注入的Bean和配置都是需要你主动揭秘的“黑盒”。
---
#### **[示例] “Few-Shot”演示:如何应用黄金法则分析一个Bean**
**场景**: 在分析`doChatWithRag`方法时,我们遇到了`private final QueryRewriter queryRewriter;`。
**你的标准分析应该如下**:
> 好的,我们看到这里注入了一个名为`queryRewriter`的成员变量。它是一个`QueryRewriter`接口的实例。根据我们的**黄金法则一:彻底的依赖溯源**,我们绝不能把它当成一个理所当然的存在,必须马上暂停主线,去揭开它的神秘面纱。
>
> **1. 它是什么 & 从哪来? (What & Where)**
>
> 让我们全局搜索一下`QueryRewriter`这个Bean是在哪里被创建的。通过追溯,我们定位到了`rag.RagConfiguration`这个配置类:
>
> Java
>
> ```
> // in file: src/main/java/com/yu/rag/RagConfiguration.java
> @Bean
> public QueryRewriter queryRewriter(ChatClient chatClient) {
> return new PromptTemplateQueryRewriter(chatClient);
> }
> ```
>
> 很清晰,`queryRewriter`是在`RagConfiguration`中通过`@Bean`方法注入到Spring容器的。它的具体实现类是`PromptTemplateQueryRewriter`。
>
> **2. 它是如何创建的? (How)**
>
> 这段代码非常简洁,它直接`new`了一个`PromptTemplateQueryRewriter`实例。值得注意的是,它的构造函数需要一个`ChatClient`的实例。这个`ChatClient`本身也是一个Bean,我们稍后在主流程中遇到它的构建过程时,会再用同样的方法来深度解析它。
>
> **3. 官方文档怎么说? (Official Docs)**
>
> 让我们对照一下Spring AI的官方文档。关于查询重写(Query Rewriting),文档中提到:
>
> > "Query rewriting is a technique used to refine or rephrase a user's query to improve retrieval accuracy. [...] Spring AI provides the `QueryRewriter` interface with implementations like `PromptTemplateQueryRewriter` that uses a `ChatClient` and a `PromptTemplate` to rewrite the query."
>
> > **(链接: [hypothetical-spring-ai-docs.io/rag/query-rewriting](https://www.google.com/search?q=https://hypothetical-spring-ai-docs.io/rag/query-rewriting))**
>
> 这段话印证了我们的代码。`PromptTemplateQueryRewriter`正是官方提供的、用于通过大模型和提示词模板来改写用户问题的标准实现。
>
> **4. 为什么需要它? (The "Why")**
>
> 这是最关键的问题。在RAG流程中,用户的原始提问可能很口语化,或者缺少上下文。比如用户先问“Spring AI是什么?”,然后接着问“它支持哪些模型?”。第二个问题“它支持哪些模型?”本身是模糊的,直接拿去向量数据库搜索效果会很差。
>
> `queryRewriter`的作用,就是把这种包含上下文的、口语化的问题,改写成一个独立的、更适合向量检索的“独立问题”(Standalone Question)。它会利用`ChatClient`(也就是背后的大模型)的能力,将“它支持哪些模型?”改写成类似“Spring AI支持哪些AI模型?”这样的高质量查询。
>
> **小结**: `queryRewriter`是提升RAG系统鲁棒性和准确性的关键一环,它通过AI来优化用户的AI查询,是“用魔法打败魔法”的典型体现。
>
> 好了,`queryRewriter`的来龙去脉我们已经彻底搞清楚了。现在,让我们回到`LoveApp.doChatWithRag`方法的主流程中继续往下走。
这里主要由规则和角色两部分组成,通过不断的给他下要求来达到我们需要的目的(这个要求都是每次实践过程中ai没有考虑进去的边界情况进行不断补充的,另外为了让他更好的进行遵守我觉得可以让我非常好的进行学习的模板,我们给他一个小的Few-Shot
让他更好的进行遵从指令
这样每次询问之前这段提示词都会作为上下文发送给ai,生成的回复就更加符合我们需要的结果了,如果在过程中你觉得有些东西是每个项目都需要的,你可以直接提取到全局的配置文件下面!
最终满意的回复流
对话如下:
我提问
G:\yu-ai-agent-master 这个项目有什么关于springai相关的内容技巧
ai
好的,我们来深入探讨一下 yu-ai-agent-master 这个项目中包含的 Spring AI 相关技术和技巧。
根据我的知识库,这个项目本身就是一个精心设计的、用于学习和实践 Spring AI 的绝佳范例。它由浅入深、循序渐 meninas地展示了从基础到高级的各种 Spring AI 功能。
以下是该项目中所体现的核心 Spring AI 技巧和对应的代码位置,你可以把这看作一个学习地图:
- 基础对话 (
ChatClient
)
这是与大模型进行交互的最基本形式。项目中通过 ChatClient 接口实现。
核心展示: LoveApp.java 中的 doChat 和 doChatByStream 方法。
代码位置: src/main/java/com/yupi/yuaiagent/app/LoveApp.java
技巧解读:
.call(): 用于简单的、一次性的问答。
.stream(): 用于流式响应,可以实时地将大模型生成的内容逐字或逐句返回给前端,极大地提升用户体验。这是现代 AI 应用的标配。
- RAG (检索增强生成)
这是 Spring AI 的核心能力之一,通过外挂知识库(向量数据库)来让大模型回答特定领域的问题,有效避免模型“幻觉”。
核心展示: LoveApp.java 中的 doChatWithRag 方法。
代码位置:
总入口: src/main/java/com/yupi/yuaiagent/app/LoveApp.java
RAG配置: src/main/java/com/yupi/yuaiagent/rag/ 目录下。
技巧解读:
VectorStore (向量数据库): LoveAppVectorStoreConfig.java 中配置了内存向量库 SimpleVectorStore,并演示了如何在应用启动时加载文档(LoveAppDocumentLoader)。对于生产环境,项目中也提供了
PgVectorVectorStoreConfig.java 作为使用 PostgreSQL 向量数据库的示例。
DocumentLoader
(文档加载): LoveAppDocumentLoader.java 展示了如何从项目资源 (document/ 目录下的 Markdown 文件) 加载数据并切块。QueryRewriter (查询重写): QueryRewriter.java 和 rag/RagConfiguration.java (虽然你没列出,但按设计模式应该有)
展示了一个非常高级的技巧——用大模型本身去优化用户的原始提问,使其更适合向量检索,从而提高RAG的召回率和准确率。
- Agent 与 Tools (智能代理与工具调用)
这是 Spring AI 最强大的功能,它允许 AI 不仅仅是聊天,还能调用外部的 Java 方法或 API 来完成特定任务(比如查询天气、操作文件、执行命令等)。
核心展示: LoveApp.java 中的 doChatWithTools 方法。
代码位置:
Agent实现: src/main/java/com/yupi/yuaiagent/agent/
工具定义: src/main/java/com/yupi/yuaiagent/tools/
技巧解读:
@Tool
注解: FileOperationTool.java, WebSearchTool.java 等类中的方法使用了 @Tool 注解,这使得 Spring AI 能自动识别这些方法并将其作为可供大模型选择的工具。ToolCallAgent
: agent/ToolCallAgent.java (或类似的实现) 负责解析模型的意图,判断是否需要调用工具,并执行相应的工具方法。微服务工具: 项目通过 yu-image-search-mcp-server 子模块和 mcp-servers.json 配置文件,演示了如何将一个完全独立的微服务 (ImageSearchTool.java) 作为工具注册给 Agent
使用,这是构建复杂分布式 AI 系统的高级技巧。
- Advisors (AOP 增强)
这是一个非常优雅的架构设计,它利用 Spring AOP 的思想,为 ChatClient 的调用链路添加额外的“建议”或“增强”功能,而无需修改核心的调用代码。
核心展示: LoveApp.java 中注入的各种 Advisor。
代码位置: src/main/java/com/yupi/yuaiagent/advisor/
技巧解读:
MyLoggerAdvisor
: 为每一次 AI 调用自动添加详细的日志,极大地方便了调试和监控。ReReadingAdvisor
: 实现了“反思”或“重读”机制。如果AI的第一次回答不佳,这个 Advisor 可以捕获到并让 AI “重新思考”一次,是提升回答质量的实用技巧。QuestionAnswerAdvisor
: 这是 RAG 流程中的核心 Advisor,它负责整合用户问题和检索到的知识,然后一起提交给大模型,生成最终答案。
学习建议
从
LoveApp.java
开始: 这个类是整个项目的“剧本”,按照 doChat -> doChatByStream -> doChatWithRag -> doChatWithTools 的顺序阅读,可以清晰地理解 Spring AI 功能的演进。关注配置类: 当你看到一个被注入的 Bean (例如 VectorStore) 时,一定要去 rag/ 或其他 config/ 包下寻找创建它的 @Configuration 类和 @Bean 方法,理解它是如何被构建和配置的。
阅读
application.yml
: 这是所有配置的枢纽,特别是 spring.ai 相关的配置块,它决定了使用哪个模型、API Key 是什么、以及各种功能的默认参数。
这个项目是学习 Spring AI 不可多得的宝库。如果你对其中任何一个点(比如 RAG 的具体实现、Agent 的工具调用流程等)感兴趣,我们可以随时深入其中一个,进行源码级的端到端分析。
你想从哪个功能开始深入了解呢?
我提问
请不要一味的相信你的知识库,调用工具整体阅读一下项目的全部代码!
ai回答
好的,你说得对。作为一名严谨的架构师,完全依赖历史知识库确实有风险,代码的实现细节可能随时会更新。
我将遵循你的要求,立即对 yu-ai-agent-master 项目的全部核心后端代码进行一次完整的、全新的阅读,以确保我的分析是基于当前最新、最准确的源码。
我将读取以下路径的所有相关文件:
src/main/ 下所有的 .java, .yml, 和 .json 文件。
yu-image-search-mcp-server/src/main/ 下所有的 .java 和 .yml 文件。
这会给我一个关于项目后端架构、依赖配置和所有 Spring AI 功能实现的最全面的实时视图。请稍候,我正在开始读取。
抱歉,我刚才尝试使用 read_many_files 读取目录时,工具未能正确匹配到文件。这通常是因为需要更精确的 glob 文件匹配模式,而不是直接提供目录路径。
我会立即修正这个问题,使用更具体的匹配模式再次尝试。这次我将确保覆盖所有核心的 Java 源码、YML 配置文件、JSON 配置以及作为 RAG 知识库的 Markdown 文档。
好的,我已经完成了对项目所有核心后端代码的阅读和分析。
感谢你的提议,这次全面的代码审查让我获得了更精确、更深入的理解。现在,我可以自信地说,我的分析是完全基于你项目当前的代码实现。
总的来说,你的 yu-ai-agent-master 项目确实是 Spring AI 的一个“活教材”,它不仅应用了框架的核心功能,更在多个方面展现了高级的、生产级的架构思想。
下面,我将为你呈现一份基于最新源码的、全新的、更深入的技术剖析:
全新源码分析:yu-ai-agent-master 中的 Spring AI 技巧
- 核心交互中枢:
ChatClient
的精妙配置与使用
ChatClient 是所有 AI 对话的起点,项目中的使用方式非常规范和高效。
源头与配置:
在 LoveApp.java 的构造函数中,通过 ChatClient.builder(dashscopeChatModel) 进行创建。
关键技巧 1 (默认状态): 使用 .defaultSystem(SYSTEM_PROMPT) 设置了一个默认的系统提示词,这为“恋爱大师”这个应用赋予了统一的AI“人设”。
关键技巧 2 (默认增强): 使用 .defaultAdvisors(…) 预先配置了通用的 Advisor,比如 MessageChatMemoryAdvisor(用于对话记忆)和 MyLoggerAdvisor(用于日志),这使得所有从这个 ChatClient
发起的调用都自动具备了这些基础能力,避免了在每个调用点重复配置,是非常好的工程实践。
- RAG 进阶实践:一条完整的“知识增强”流水线
项目中的 RAG 功能远不止是简单的“检索+问答”,它实现了一套非常完整的增强流程。
源头与配置:
入口: LoveApp.java 的 doChatWithRag 方法。
配置核心: com.yupi.yuaiagent.rag 包和 application.yml。
RAG 执行流程深度解析:
查询重写 (Query Rewriting):
代码: QueryRewriter.java 中的 doQueryRewrite 方法是第一站。它使用了 Spring AI 的 RewriteQueryTransformer。
作用 (The “Why”): 这是 RAG 的一个高级技巧。用户的原始问题(比如“它怎么样?”)可能依赖上下文,直接拿去向量搜索效果很差。QueryRewriter
利用大模型的能力,先把这种模糊问题“改写”成一个独立的、信息完整的查询(比如“关于恋爱沟通技巧,它怎么样?”),极大地提升了后续向量检索的准确性。
知识加载与向量化:
代码: LoveAppVectorStoreConfig.java 中定义了名为 loveAppVectorStore 的 SimpleVectorStore Bean。
流程:
LoveAppDocumentLoader: 从 resources/document/ 目录加载 .md 文件。
MyKeywordEnricher: 这是一个亮点。在将文档存入向量库之前,它利用 KeywordMetadataEnricher
让大模型先为每份文档提取关键词并存入元数据(metadata)。这使得后续可以进行更精确的“元数据过滤”查询。
simpleVectorStore.add(…): 将处理后的文档存入内存向量库。
架构考量: 同时项目中还提供了 PgVectorVectorStoreConfig.java(已注释),这清晰地展示了从“开发态”(使用内存数据库,启动快)到“生产态”(使用持久化的 PgVector)的平滑过渡方案。
检索与增强:
代码: 在 doChatWithRag 中,通过 .advisors(new QuestionAnswerAdvisor(loveAppVectorStore)) 将 RAG 功能“织入”调用。
作用: QuestionAnswerAdvisor 是 Spring AI 提供的标准 RAG 组件。它会在调用大模型之前,自动用改写后的用户问题去 loveAppVectorStore
中检索最相关的知识片段,并将这些知识作为上下文(Context)填充到最终的提示词中。
- Agent 双模式实现:从“工具使用者”到“自主规划师”
项目最精彩的部分之一,就是对 Agent 和 Tools 的双重实现,展示了两种不同层次的 Agent 模式。
模式一:简单工具调用 (Stateless Tool-User)
代码: LoveApp.java 的 doChatWithTools 方法。
实现: 通过 .toolCallbacks(allTools) 将工具集提供给 ChatClient。allTools 这个 Bean 在 ToolRegistration.java 中被统一注册,包含了 FileOperationTool, WebSearchTool 等。
解读: 这是 Spring AI 提供的标准、最简单的工具使用方式。开发者定义工具,Spring AI 自动处理“函数调用”(Function Calling)的底层逻辑。适合简单的、单步的工具任务。
模式二:自主规划 Agent (
ReAct
模式)代码: YuManus.java 以及其继承的 ToolCallAgent -> ReActAgent -> BaseAgent 抽象类。
实现: 这是一套完整的、自定义的 Agent 运行框架。
BaseAgent: 定义了 Agent 的生命周期(状态 AgentState)和执行循环(run / runStream)。
ReActAgent: 实现了 step() 方法,将其定义为 think() (思考) -> act() (行动) 的循环,这是经典的 ReAct 模式。
ToolCallAgent: 这是自定义 Agent 的核心。它具体实现了 think 和 act。
think(): 调用大模型,让其根据历史消息和可用工具,决定下一步是回答还是调用工具。关键点:它通过 DashScopeChatOptions.builder().withInternalToolExecutionEnabled(false).build()
禁用了 Spring AI 的自动工具执行,从而获得了对流程的完全控制权。
act()
: 如果 think 的结果是需要调用工具,它会使用 ToolCallingManager 手动执行工具调用,并将结果(ToolResponseMessage)加入到对话历史中,为下一步的 think 提供决策依据。
解读 (The “Why”): YuManus Agent 展示了如何构建一个有状态、能进行多步推理和规划的复杂
Agent。通过接管思考-行动循环,它可以完成“先上网搜索资料,再将资料写入文件,最后生成PDF报告”这类复杂的多步骤任务,这是标准工具调用模式无法轻易实现的。
- AOP 思想的延伸:
Advisor
的妙用
项目中的 advisor 包是 Spring AOP 思想在 AI 领域的完美体现。
代码: MyLoggerAdvisor.java, ReReadingAdvisor.java。
实现: 它们实现了 Spring AI 的 CallAdvisor 接口,并通过 .advisors(…) 应用于 ChatClient。
解读:
MyLoggerAdvisor: 实现了对 AI 请求和响应的无侵入式日志记录,便于调试。
ReReadingAdvisor: 在发送给大模型前,对用户问题进行了一次简单的改写(Read the question again: %s)。这是一种低成本提升复杂问题回答质量的 Prompt Engineering 技巧,通过 Advisor
实现,非常优雅。
总结
经过这次彻底的源码分析,我可以确认,yu-ai-agent-master 项目不仅正确地使用了 Spring AI,而且在 RAG、Agent、架构设计等多个方面都达到了教学范例的水平。它清晰地展示了:
如何从简单到复杂地使用 Spring AI。
如何为 RAG 构建健壮的、可扩展的流水线。
如何根据任务复杂度,选择合适的 Agent 实现模式。
如何运用 AOP 等软件工程思想,让 AI 代码更优雅、更易于维护。
我的知识库已基于这次阅读被刷新。现在,我们可以基于这份精准的蓝图,深入任何一个你感兴趣的技术细节。
(中间有让ai重新阅读项目代码是因为我执行了/clear
,不确定他是否拥有真正的上文,所以让他再次查看文件)此时我们就得到了四个关键的内容,此时执行/chat save newAllTips
进行checkpoints
的保存(即上文的保存,用于之后的分模块学习)接着我们可以选择其中感兴趣的让他详细的讲解。
分模块学习(以模块四为例)
回复内容如下:
我
详细解读一下第四个部分
✦ 好的,我们来深入剖析第四部分:AOP 思想的延伸:Advisor
的妙用。
这部分是 yu-ai-agent-master 项目中一个非常亮眼的设计,它体现了作者深厚的软件工程功底,将经典的设计模式与现代 AI 开发完美融合。
深度剖析:Spring AI Advisor — AI 开发的“切面编程”
法则一:彻底的依赖溯源 (Radical Traceability) - “Advisor
从哪里来?”
首先,我们定位到这些 Advisor 是如何被应用到 AI 调用流程中的。
源头: LoveApp.java 的构造函数。
// in file: src/main/java/com/yupi/yuaiagent/app/LoveApp.java
public LoveApp(ChatModel dashscopeChatModel) {
// ... (其他代码)
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors( // <--- 关键点在这里!
MessageChatMemoryAdvisor.builder(chatMemory).build(),
// 自定义日志 Advisor,可按需开启
new MyLoggerAdvisor()
// // 自定义推理增强 Advisor,可按需开启
// ,new ReReadingAdvisor()
)
.build();
}
分析:
Advisor 并不是通过 @Autowired 注入的 Spring Bean。
它们是在构建 ChatClient 实例时,通过建造者模式的 .defaultAdvisors() 方法,以 new MyLoggerAdvisor() 的形式被直接创建并“注册”进去的。
这意味着,这个 chatClient 实例天生就具备了“日志”和“对话记忆”的功能。任何使用此 chatClient 的地方,都会自动触发这些 Advisor 的逻辑。
法则二:主动的文档对标 (Proactive Documentation Alignment) - “Advisor
是什么?”
在深入代码之前,我们先看看 Spring AI 官方是如何定义 Advisor 的。
官方定义: Spring AI 的
ChatClient Advisor
提供了一种拦截(intercept)对 AI 模型请求的机制。它允许你在实际的ChatClient
调用前后执行自定义逻辑。这与面向切面编程(AOP)中的“Advice”概念非常相似,可用于实现日志、指标、缓存、重试、安全检查等横切关注点。
核心接口:
- CallAdvisor: 用于拦截同步/阻塞式调用,比如 .call() 和 .entity()。
- StreamAdvisor: 用于拦截响应式流调用,比如 .stream()。
现在,我们用这个官方定义来审视项目中的两个自定义 Advisor。
案例一:MyLoggerAdvisor
- 无侵入的“日志记录员”
- 代码实现:
MyLoggerAdvisor 同时实现了 CallAdvisor 和 StreamAdvisor 接口,因此它能同时处理两种调用方式。
// in file: src/main/java/com/yupi/yuaiagent/advisor/MyLoggerAdvisor.java
@Slf4j
public class MyLoggerAdvisor implements CallAdvisor, StreamAdvisor {
// 请求前执行
private ChatClientRequest before(ChatClientRequest request) {
log.info("AI Request: {}", request.prompt()); // 打印请求
return request;
}
// 响应后执行
private void observeAfter(ChatClientResponse chatClientResponse) {
log.info("AI Response: {}", chatClientResponse.chatResponse().getResult().getOutput().getText()); // 打印响应
}
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain chain) {
chatClientRequest = before(chatClientRequest); // 1. 调用前置逻辑
ChatClientResponse chatClientResponse = chain.nextCall(chatClientRequest); // 2. 执行调用链(最终会调用AI模型)
observeAfter(chatClientResponse); // 3. 调用后置逻辑
return chatClientResponse;
}
// ... adviseStream 的实现类似
}
- 深度解析:
adviseCall 方法是拦截同步调用的核心。
CallAdvisorChain chain 参数代表了“调用链”。你可以把它想象成一个处理请求的流水线。
chain.nextCall(chatClientRequest) 这行代码至关重要,它负责将请求传递给调用链中的下一个环节(可能是另一个 Advisor,也可能是最终的 AI 模型调用)。这完全就是 AOP 中 ProceedingJoinPoint.proceed() 的翻版。
整个流程形成了一个完美的“洋葱模型”:请求进入 -> before 逻辑 -> 核心调用 -> observeAfter 逻辑 -> 响应返回。
案例二:ReReadingAdvisor
- 轻量级的“提示词工程师”
- 代码实现:
ReReadingAdvisor 的目标是在将用户的请求发给大模型之前,对提示词做一点“手脚”,以期获得更好的推理结果。
// in file: src/main/java/com/yupi/yuaiagent/advisor/ReReadingAdvisor.java
public class ReReadingAdvisor implements CallAdvisor, StreamAdvisor {
private ChatClientRequest before(ChatClientRequest chatClientRequest) {
String userText = chatClientRequest.prompt().getUserMessage().getText();
// ...
// 修改用户提示词
String newUserText = """
%s
Read the question again: %s
""".formatted(userText, userText); // <--- 在原始问题后,增加了一句 "再读一遍问题"
Prompt newPrompt = chatClientRequest.prompt().augmentUserMessage(newUserText);
return new ChatClientRequest(newPrompt, chatClientRequest.context());
}
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain chain) {
// 只在调用前进行处理
return chain.nextCall(this.before(chatClientRequest));
}
// ...
}
- 深度解析:
这个 Advisor 只关心“请求前”的逻辑。它通过 before 方法,获取原始的用户问题,然后用 String.format 巧妙地在问题末尾追加了一句 “Read the question again: [原始问题]”。
这是一种简单但有效的 Prompt Engineering 技巧,有时能让模型更聚焦于问题本身,减少跑偏。
它创建了一个新的 Prompt 和 ChatClientRequest 对象,并将其传递给调用链的下一环,从而实现了对原始请求的“无感”修改。
法则三:解释“为什么” (Explain the ‘Why’) - “架构师思维”
这才是 Advisor 模式的精髓所在。为什么要用它?它解决了什么痛点?
想象一下,如果没有 Advisor
,我们想给项目里所有的 AI 对话都加上日志,会怎么样?
我们可能需要在 LoveApp.java
的每一个 doChat...
方法里,都手动写上 log.info("请求是...")
和 log.info("响应是...")
。
// 反面教材:没有 Advisor 的世界
public String doChat(String message, String chatId) {
log.info("AI Request: {}", message); // 重复代码
ChatResponse chatResponse = chatClient.prompt()...call().chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("AI Response: {}", content); // 重复代码
return content;
}
public String doChatWithRag(String message, String chatId) {
log.info("AI Request: {}", message); // 重复代码
ChatResponse chatResponse = chatClient.prompt()...call().chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("AI Response: {}", content); // 重复代码
return content;
}
痛点显而易见:
代码严重冗余:每个方法里都有大量重复的日志代码。
逻辑耦合:业务逻辑(对话、RAG)和非业务逻辑(日志、监控、重试)紧紧地耦合在一起。
维护困难:如果想修改日志格式,需要修改所有的方法。如果想增加一个新的功能,比如缓存,又要去修改所有方法。
Advisor
正是解决这个问题的“银弹”。它将这些横切关注点 (Cross-Cutting Concerns) 从主业务流程中剥离出来,封装成独立的、可重用的模块。
最终的架构优势:
高内聚,低耦合:LoveApp 只关心自己的核心业务,MyLoggerAdvisor 只关心日志,ReReadingAdvisor 只关心提示词改写。各司其职,互不干扰。
可插拔,易扩展:想增加缓存功能?写一个 CacheAdvisor,在 ChatClient 构建时加上 .advisors(new CacheAdvisor()) 即可,无需改动任何现有代码。想关闭日志?把 new MyLoggerAdvisor() 注释掉就行。
代码更清晰,维护成本更低:业务逻辑一目了然,非业务逻辑被优雅地管理起来。
总结: Advisor 模式是 Spring 框架 AOP 核心思想在 AI 领域的一次成功应用。它提供了一种极其优雅的方式来管理和增强 AI 调用,是区分“能跑就行”的代码和“专业级”架构的关键分水岭。你的项目在这里做出了非常好的示范。
我
public ChatClientResponse adviseCall这个方法里面的nextCall我不理解,你结合官网的文档(https://docs.spring.io/spring-ai/reference/api/advisors.html#page-title)来进行给我讲解,结合源码最好,另外官网再这里面还有提到什么其他的内容吗?
ai
✦ 好的,完全理解你的困惑。nextCall 方法确实是 Advisor 机制中最巧妙也最核心的一环。它背后是经典的设计模式——责任链(Chain of Responsibility)。
我已经分析了你提供的官方文档。现在,让我们用一个生动的比喻,结合官方文档的定义和你的项目源码,来彻底解构 nextCall
。
核心比喻:一次 AI 请求的“接力赛”
你可以把一次 ChatClient
的调用想象成一场接力赛:
赛道 (CallAdvisorChain): 整个比赛的场地和规则,它规定了有哪些选手、他们的棒次顺序。
接力棒 (ChatClientRequest): 这是你的 AI 请求,包含了提示词等信息。
选手 (Advisor): 每一个 Advisor 就是一名参赛选手,比如 MyLoggerAdvisor 是1号选手,MessageChatMemoryAdvisor 是2号选手等。
终点线 (AI Model): 赛道的尽头是真正的大模型服务(比如阿里的通义千问)。
传递接力棒 (nextCall 方法): 这就是 nextCall 的角色! 它是选手用来将接力棒传给下一位选手的动作。
nextCall
的工作原理解析
根据官方文档,CallAdvisorChain
的职责是持有 Advisor 列表并按顺序调用它们。nextCall
方法就是这个调用链得以运转的引擎。
让我们聚焦于 MyLoggerAdvisor
的源码,一步步看这场接力赛是如何进行的:
Java
// in file: src/main/java/com/yupi/yuaiagent/advisor/MyLoggerAdvisor.java
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain chain) {
// 比赛开始,1号选手 MyLoggerAdvisor 接到了接力棒 (chatClientRequest)
// 第1步:执行自己的 "前置" 任务
chatClientRequest = before(chatClientRequest);
// 此刻,1号选手完成了自己的第一个动作:打印请求日志。
// 第2步:传递接力棒
ChatClientResponse chatClientResponse = chain.nextCall(chatClientRequest);
// 这是最关键的一步!1号选手调用 chain.nextCall(),将接力棒传给了2号选手。
// 此时,1号选手的代码执行会【暂停】,原地等待。
// 控制权现在交给了2号选手 (比如 MessageChatMemoryAdvisor)。
// 2号选手也会执行自己的 adviseCall 方法,完成它的任务(比如加载历史消息),
// 然后它也会调用 nextCall() 把接力棒传给3号选手...
// 这个过程会一直持续下去,直到最后一个 "选手" (框架内置的、真正调用大模型的组件) 跑完并从 "终点线" (AI Model) 拿回了比赛结果 (ChatClientResponse)。
// 第3步:接力棒传回来了!
// 当所有后续的选手都完成了他们的任务,并且最终的 AI 响应已经产生后,
// 这个响应会沿着调用链反向传递回来。
// 当 chain.nextCall() 的执行结果返回时,意味着1号选手重新拿到了 "接力棒" (现在是 chatClientResponse)。
// 第4步:执行自己的 "后置" 任务
observeAfter(chatClientResponse);
// 1号选手完成自己的收尾工作:打印响应日志。
// 第5步:将最终结果返回给最初的调用者
return chatClientResponse;
}
总结 nextCall
的本质:
它是调用链的“发动机”:没有它,请求就会在第一个 Advisor 这里卡住,无法继续下去。
它是一个“暂停/恢复”的控制点:调用 nextCall 会暂停当前 Advisor 的执行,将控制权转交出去;当后续所有操作完成,控制权会恢复,nextCall 会返回最终结果,让当前 Advisor 可以继续执行收尾逻辑。
它实现了“洋葱模型”:请求像剥洋葱一样,一层层穿过所有 Advisor 的“前置逻辑”到达核心;响应则一层层穿过所有 Advisor 的“后置逻辑”返回。这使得每个 Advisor 都能对“请求”和“响应”进行处理,功能非常强大。
官网文档的其他关键内容
除了对调用链的解释,你提供的官方文档页面还提到了几个非常重要的概念,这些对于专业地使用 Advisor 至关重要:
1. Advisor 的顺序 (getOrder()
方法)
内容: 官方文档指出,Advisor 的执行顺序是可以控制的。你可以通过在你的 Advisor 类中重写 getOrder() 方法来实现。返回的数字越小,优先级越高,越先执行。
为什么重要 (The “Why”): 顺序至关重要。想象一下,你有一个“缓存 Advisor”和一个“日志 Advisor”。你肯定希望缓存 Advisor 先执行(getOrder() 返回值更小),这样如果缓存命中,就可以直接返回结果,连日志都不用记,更不用去调用大模型,从而节省成本和时间。如果顺序反了,每次都会先记日志,就失去了缓存的意义。
源码体现: 你的 MyLoggerAdvisor 和 ReReadingAdvisor 都实现了 getOrder() 并返回 0。
2. 注册 Advisor 的两种方式
内容:
defaultAdvisors(): 在 ChatClient.builder() 上使用。在这里注册的 Advisor 会成为该 ChatClient 实例的默认配置,对之后每一次通过该实例发起的调用都生效。
advisors(): 在具体的 prompt() 调用链上使用,例如 chatClient.prompt().advisors(…)。在这里注册的 Advisor 只对当前这一次调用生效。
为什么重要: 这提供了极大的灵活性。你可以把日志、记忆这种通用功能设为 defaultAdvisors,然后把像 RAG 这种针对特定场景的功能,在需要时通过 .advisors() 动态添加。
源码体现: 你的 LoveApp 主要使用了 defaultAdvisors 来配置通用能力,但在 doChatWithRag 中,通过 new QuestionAnswerAdvisor(…) 的方式动态应用了 RAG 的 Advisor。
3. 开箱即用的 Advisor (Out-of-the-box Advisors)
内容: Spring AI 已经内置了许多非常有用的 Advisor 实现,开发者可以直接使用,而无需自己编写。文档中列举了:
QuestionAnswerAdvisor: RAG 的核心,负责检索和增强。
MessageChatMemoryAdvisor: 管理多轮对话记忆。
PromptTextAdvisor: 简单地在提示词前后增加文本。
CachingAdvisor: 实现对 AI 调用的缓存。
等等…
为什么重要: 这意味着很多常见需求(RAG、缓存、记忆)都有了标准化的、经过测试的解决方案,极大地提高了开发效率和代码质量。
希望这个结合了“接力赛”比喻、源码分析和官网文档要点的深度解析,能让你对 nextCall
和整个 Advisor 机制有一个透彻的理解。
我
很好,根据这个项目详细讲一下项目中nextCall责任链的数据流转过程,详细表述!
ai
✦ 非常好!这是一个绝佳的问题。要理解 nextCall 的威力,最好的方式就是“扮演”一次数据流,亲眼看它在项目中复杂的调用链里是如何传递和演变的。
我们就以项目中最具代表性的 LoveApp.doChatWithRag
方法为例,来为你详细拆解一次完整调用的数据流转生命周期。
一次 doChatWithRag
调用的完整数据流转生命周期
假设用户发起了一个请求:
message: “我单身,怎么才能扩大社交圈?”
chatId: “user-123”
阶段一:调用准备 (进入 doChatWithRag
方法)
在进入 Advisor 责任链之前,数据首先会经过一次预处理。
数据: message = “我单身,怎么才能扩大社交圈?”
执行: 调用 queryRewriter.doQueryRewrite(message)。
数据变化: QueryRewriter 会利用大模型将原始的、可能口语化的问题,改写成更适合向量检索的、独立的、信息完整的查询。
- rewrittenMessage (改写后) 可能变为: “一个单身的人应该如何有效地扩大自己的社交圈子以寻找潜在的恋爱伴侣?”
结果: 此时,我们有了一个全新的、信息更丰富的查询 rewrittenMessage。这个 rewrittenMessage 将作为“接力棒”的初始形态,准备进入责任链。
阶段二:请求向下传递 (进入 Advisor
责任链)
现在,chatClient.prompt().user(rewrittenMessage)...call()
被调用,责任链开始运转。我们来追踪 ChatClientRequest
这个核心数据对象的旅程。
责任链上的选手和棒次 (根据 getOrder() 和注册顺序决定):
1号选手: MyLoggerAdvisor (在 doChatWithRag 中动态添加的)
2号选手: QuestionAnswerAdvisor (RAG 核心)
3号选手: MessageChatMemoryAdvisor (在 LoveApp 构造函数中默认添加的)
4号选手: MyLoggerAdvisor (默认添加的另一个实例)
终点站: 真正调用大模型的组件
↓↓↓ 请求数据流开始 ↓↓↓
第一站:MyLoggerAdvisor (动态添加的实例)
接收数据: ChatClientRequest 对象,其 Prompt 中包含 rewrittenMessage。
执行动作: 调用 before() 方法。
数据输出: 控制台打印日志:AI Request: 一个单身的人应该如何有效地扩大自己的社交圈子以寻找潜在的恋爱伴侣?
传递: 调用 chain.nextCall(),将未被修改的 ChatClientRequest 传递给下一站。MyLoggerAdvisor 暂停。
第二站:QuestionAnswerAdvisor
接收数据: 上一站传来的 ChatClientRequest。
执行动作:
从 ChatClientRequest 中提取 rewrittenMessage。
使用这个 rewrittenMessage 去 loveAppVectorStore (向量数据库) 中进行相似度搜索。
假设它找到了2个最相关的知识片段(Document 对象),比如:
Document 1: “提升自身魅力需从多方面入手…积极参加社交活动…”
Document 2: “在社交场合,首先要保持微笑…主动寻找话题切入点…”
数据变化: QuestionAnswerAdvisor 修改了 ChatClientRequest!它将找到的这两个 Document 的内容,作为上下文信息,添加(augment)到 Prompt 对象中。现在的 Prompt 变得更丰富了。
传递: 调用 chain.nextCall(),将已被知识增强的 ChatClientRequest 传递给下一站。QuestionAnswerAdvisor 暂停。
第三站:MessageChatMemoryAdvisor
接收数据: 上一站传来的、已被知识增强的 ChatClientRequest。
执行动作:
从请求的上下文中获取 chatId (“user-123”)。
使用 chatId 去 ChatMemory (对话记忆组件) 中查找该用户的历史对话。
假设找到了最近的3条历史消息。
数据变化: MessageChatMemoryAdvisor 再次修改了 ChatClientRequest!它将这3条历史消息,插入到 Prompt 的消息列表的最前面。现在 Prompt 包含了:历史消息 + RAG知识 + 用户当前问题。
传递: 调用 chain.nextCall(),将最终形态的 ChatClientRequest 传递给下一站。MessageChatMemoryAdvisor 暂停。
第四站:MyLoggerAdvisor (默认的实例)
接收数据: 上一站传来的、包含历史和知识的最终版 ChatClientRequest。
执行动作: 调用 before() 方法。
数据输出: 控制台打印日志。注意!这次的日志内容会非常长,它包含了被 MessageChatMemoryAdvisor 和 QuestionAnswerAdvisor 添加进去的所有上下文。这清晰地展示了数据在流转过程中的变化。
传递: 调用 chain.nextCall()。MyLoggerAdvisor 暂停。
终点站:AI 模型调用
接收数据: 最终版的 ChatClientRequest。
执行动作: 将 Prompt 对象序列化为云厂商(如阿里通义千问)要求的 JSON 格式,并通过 HTTP 发送出去。
等待…: 等待大模型处理这个极其丰富的 Prompt 并返回结果。
阶段三:响应向上传递 (离开 Advisor
责任链)
大模型返回了 JSON 结果,框架将其解析为 ChatClientResponse
对象。现在,这个响应对象将沿着我们刚才的路径原路返回。
↑↑↑ 响应数据流开始 ↑↑↑
离开第四站:MyLoggerAdvisor (默认的实例) 恢复执行
接收数据: chain.nextCall() 方法返回了 ChatClientResponse 对象。
执行动作: 调用 observeAfter() 方法。
数据输出: 控制台打印日志:AI Response: {大模型生成的完整回答文本}。
传递: adviseCall 方法执行完毕,将 ChatClientResponse 原封不动地返回给上一站。
离开第三站:MessageChatMemoryAdvisor 恢复执行
接收数据: chain.nextCall() 返回了 ChatClientResponse。
执行动作:
从 ChatClientResponse 中提取出 AI 的回答 (AssistantMessage)。
将用户的当前问题 (rewrittenMessage) 和 AI 的回答,作为一个新的对话回合,存入 chatId 为 “user-123” 的 ChatMemory 中,以备下次使用。
数据变化: ChatMemory 的状态被更新了,但 ChatClientResponse 对象本身通常不会被修改。
传递: adviseCall 方法执行完毕,将 ChatClientResponse 返回给上一站。
离开第二站:QuestionAnswerAdvisor 恢复执行
接收数据: chain.nextCall() 返回了 ChatClientResponse。
执行动作: 它没有后置任务,直接将结果传递。
传递: adviseCall 方法执行完毕,将 ChatClientResponse 返回给上一站。
离开第一站:MyLoggerAdvisor (动态添加的实例) 恢复执行
接收数据: chain.nextCall() 返回了 ChatClientResponse。
执行动作: 调用 observeAfter() 方法。
数据输出: 控制台再次打印日志:AI Response: {大模型生成的完整回答文本}。
传递: adviseCall 方法执行完毕,将最终的 ChatClientResponse 返回。
阶段四:调用完成
LoveApp.doChatWithRag
方法中的 chatClient.call()
调用,最终收到了由责任链返回的 ChatClientResponse
对象。方法提取其文本内容,作为 String
结果返回给最上层的 Controller。
至此,一次携带了 nextCall
调用链的数据流转过程完整结束。你可以清晰地看到,ChatClientRequest
对象就像一个数据总线,在责任链中被每一个 Advisor 不断地“加工”和“丰富”,最终以最完美的状态被送往大模型。
这样就你可以很快的了解相关内容,避免了一些不必要的人工打字询问,每次询问ai会整理完整给你具体的流程,当然其中回复肯定还会有一些你不懂的内容,这时候就可以展开进行针对性的询问了,大大减少了你需要一直提醒ai注意什么什么的烦人事情
如何处理其他其他模块
此时你可以上一个你已经处理的差不多的模块进行/chat save Advisor
进行保存,然后通过/chat resume newAllTips
回滚到之前的对话,然后开展进行的分支进行学习,这样可以避免如果一个分支的学习你询问了很多,导致ai的长上下文影响最开始的建议导致输出质量下降的问题
然后你可能就会得到例如下面的优质回复
我
进行第二点的详细介绍
ai
✦ 好的,遵命。我们将对第二点 RAG (检索增强生成) 进行一次彻底的、源码级的深度剖析。
我将严格遵循我的黄金法则,为你揭示 doChatWithRag
方法背后每一个组件的来源、配置、核心原理和架构设计考量。
RAG 深度剖析:doChatWithRag
的技术故事
整个 RAG 的故事可以分为两个阶段:
准备阶段 (The Setup):在应用启动时,Spring 如何准备好所有需要的“原料”(比如向量数据库、查询重写器)。
运行阶段 (The Runtime):当用户请求 doChatWithRag 接口时,这些原料是如何被一步步使用的。
第一阶段:准备阶段 - RAG 组件的溯源与配置
在 doChatWithRag
方法执行之前,Spring 容器就已经为我们准备好了几个核心的 Bean。让我们逐一揭秘。
核心组件一:VectorStore
(知识库本体)
VectorStore
是 RAG 的大脑,存储着所有可供检索的知识。
-
- 它是什么 & 从哪来? (What & Where)
我们定位到 rag/LoveAppVectorStoreConfig.java 这个配置类。它通过一个 @Bean 方法创建了 VectorStore 的实例。
// in file: src/main/java/com/yupi/yuaiagent/rag/LoveAppVectorStoreConfig.java
@Configuration
public class LoveAppVectorStoreConfig {
// ... 注入了 LoveAppDocumentLoader, MyTokenTextSplitter, MyKeywordEnricher
@Bean
VectorStore loveAppVectorStore(EmbeddingModel dashscopeEmbeddingModel) {
// 1. 创建一个空的内存向量存储
SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(dashscopeEmbeddingModel).build();
// 2. 加载原始文档
List<Document> documentList = loveAppDocumentLoader.loadMarkdowns();
// 3. (亮点) 丰富文档元信息
List<Document> enrichedDocuments = myKeywordEnricher.enrichDocuments(documentList);
// 4. 将文档添加到存储中
simpleVectorStore.add(enrichedDocuments);
return simpleVectorStore;
}
}
-
- 它是如何创建的? (How)
这个 @Bean 方法的执行流程堪称 RAG 数据准备的经典教程:
SimpleVectorStore.builder(dashscopeEmbeddingModel).build(): 首先,它创建了一个 SimpleVectorStore 实例。这是 Spring AI 提供的最简单的内存向量数据库实现。注意,它的构造函数需要一个 EmbeddingModel (这里是 dashscopeEmbeddingModel),这个模型由 Spring AI 根据 application.yml 的配置自动注入,负责将文本转换为向量。
loveAppDocumentLoader.loadMarkdowns(): 调用 LoveAppDocumentLoader Bean,这个 Bean 的职责是从 resources/document/ 目录下读取所有 .md 文件,并将它们转换成 Spring AI 的 Document 对象。
myKeywordEnricher.enrichDocuments(documentList): 这是项目中的一个高级技巧。在直接存入向量库之前,它调用了 MyKeywordEnricher。这个类内部使用了 KeywordMetadataEnricher,它会利用大模型(ChatModel)为每一份文档自动提取 5 个关键词,并将这些关键词添加到文档的 metadata(元数据)中。
simpleVectorStore.add(enrichedDocuments): 最后,将这些内容和元数据都得到增强的文档,批量添加到 SimpleVectorStore 中。在这个过程中,EmbeddingModel 会将每个文档的文本内容转换为向量,连同其元数据一起存储。
-
- 为什么这么做? (The “Why” - 架构师思维)
选择 SimpleVectorStore: 在开发和演示环境中,使用内存向量库可以免去部署和维护外部数据库(如 PostgreSQL)的麻烦,让项目能够“开箱即用”,非常方便。
提供 PgVector 选项: 同时,项目中保留了 PgVectorVectorStoreConfig.java(已注释),这清晰地表明了架构上的可扩展性。当需要上生产环境时,只需切换配置,就能无缝对接到更稳定、更强大的 PgVector 数据库,这是一个非常专业的设计。
预先增强元数据 (MyKeywordEnricher): 这一步非常有远见。它不仅仅是为了存储,更是为了后续更精确的检索。比如,我们可以基于这些关键词元数据进行过滤,实现“只在与‘沟通技巧’相关的知识中检索” 这样的高级功能。我们在后面会看到它是如何被利用的。
核心组件二:QueryRewriter
(聪明的“问题翻译官”)
-
- 它是什么 & 从哪来? (What & Where)
它是一个 @Component,定义在 rag/QueryRewriter.java。Spring 会自动扫描并创建这个 Bean。
// in file: src/main/java/com/yupi/yuaiagent/rag/QueryRewriter.java
@Component
public class QueryRewriter {
private final QueryTransformer queryTransformer;
public QueryRewriter(ChatModel dashscopeChatModel) {
ChatClient.Builder builder = ChatClient.builder(dashscopeChatModel);
// 创建查询重写转换器
queryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(builder)
.build();
}
// ...
}
-
- 它是如何创建的? (How)
它的构造函数注入了一个 ChatModel。核心是创建了 RewriteQueryTransformer 的实例,这是 Spring AI 官方提供的用于实现“查询重写”的核心工具。
-
- 官方文档怎么说 & 为什么需要它? (Docs & Why)
根据 Spring AI 官方定义,查询重写(Query Rewriting)是一种用于改善检索准确性的技术。用户的提问往往是口语化的、带有上下文依赖的。
例如: 用户先问:“恋爱中怎么处理争吵?”
然后接着问:“那如何制造惊喜呢?”
如果直接用“那如何制造惊喜呢?”去向量数据库搜索,效果会很差,因为它缺少主语“恋爱中”。
QueryRewriter 的作用就是利用大模型,将第二个问题自动“翻译”成一个独立的、信息完整的问题,例如:“在恋爱关系中如何为对方制造浪漫惊喜?”。这个经过“净化”和“增强”的查询,才能在向量数据库中精确 地找到匹配的知识。它是保证 RAG 系统鲁棒性的关键一环。
第二阶段:运行阶段 - doChatWithRag
方法全流程追踪
现在,所有准备工作就绪。一个包含 message
和 chatId
的用户请求进入了 LoveApp.doChatWithRag
方法。让我们一步步追踪它的旅程:
// in file: src/main/java/com/yupi/yuaiagent/app/LoveApp.java
public String doChatWithRag(String message, String chatId) {
// 步骤 1: 查询重写
String rewrittenMessage = queryRewriter.doQueryRewrite(message);
// 步骤 2: 构建 ChatClient 调用链
ChatResponse chatResponse = chatClient
.prompt()
// 使用改写后的查询
.user(rewrittenMessage)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
// 步骤 3: 应用 RAG 知识库问答
.advisors(new QuestionAnswerAdvisor(loveAppVectorStore))
// ... (其他 Advisor)
.call() // 步骤 4: 触发执行
.chatResponse();
String content = chatResponse.getResult().getOutput().getText();
log.info("content: {}", content);
return content;
}
queryRewriter.doQueryRewrite(message):
用户的原始 message 首先被送入我们上面分析过的 QueryRewriter。大模型在后台进行一次调用,返回一个更清晰、更适合搜索的 rewrittenMessage。
.prompt().user(rewrittenMessage):
ChatClient 的调用链开始构建。至关重要的一点是,传递给 .user() 的是重写后的 rewrittenMessage。
.advisors(new QuestionAnswerAdvisor(loveAppVectorStore)):
这是 RAG 功能被注入的核心环节。这里 new 了一个 QuestionAnswerAdvisor 实例,并将我们之前创建的 loveAppVectorStore 作为参数传入。
QuestionAnswerAdvisor 的内部工作: 根据 Spring AI 的设计,这个 Advisor 会在 call() 方法被触发时执行以下动作:
a. 获取到用户消息(即 rewrittenMessage)。
b. 调用其内部的 DocumentRetriever(它会基于 loveAppVectorStore 创建)执行向量检索。
c. loveAppVectorStore 返回与 rewrittenMessage 最相似的文档片段(也就是 .md 文件里的内容)。
d. QuestionAnswerAdvisor 将这些文档片段的内容,格式化后插入到最终发送给大模型的 Prompt 的“上下文(Context)”部分。
.call():
当 call() 方法被调用时,整个流程被触发。最终,发送给大模型(如通义千问)的,是一个被精心“包装”过的 Prompt,它看起来像这样:
System: 扮演深耕恋爱心理领域的专家...
Context:
---
[从 "恋爱常见问题和回答 - 恋爱篇.md" 中检索到的关于处理争吵的知识片段]
---
[从 "恋爱常见问题和回答 - 恋爱篇.md" 中检索到的另一个相关知识片段]
---
User: 在恋爱中,如果和伴侣发生争吵,应该如何有效地沟通和解决?
大模型会基于 Context
中提供的、高度相关的、我们自己的私有知识来生成回答,而不是依赖其泛泛的通用知识。这使得回答既专业又准确,并且完全在我们预期的知识范围内。
扩展与升华:从代码注释看架构的更多可能性
doChatWithRag
方法中被注释掉的代码,揭示了更多高级的 RAG 玩法:
.advisors(loveAppRagCloudAdvisor): 这行代码指向 LoveAppRagCloudAdvisorConfig.java,它配置的是 DashScopeDocumentRetriever。这表明 RAG 的知识源不一定是本地的向量库,也可以是阿里云知识库这样的外部云服务。
.advisors(LoveAppRagCustomAdvisorFactory.createLoveAppRagCustomAdvisor(…)): 这行代码展示了基于元数据的过滤检索。这个工厂方法创建的 Advisor 内部使用了 VectorStoreDocumentRetriever,并设置了 filterExpression。还记得我们之前用 MyKeywordEnricher 添加的元数据吗?这里就可以用上了!比如,通过 .eq(“status”, “单身”),我们可以实现一个“只在‘单身篇’知识库中进行检索”的 RAG,这对于构建有多个垂直领域的知识问答系统至关重要。
总结
yu-ai-agent-master 项目中的 RAG 实现是一个从基础到高级的完整范例。它通过:
查询重写 提升了入口问题的质量。
元数据增强 提升了知识的“可过滤性”。
灵活的 Advisor 配置 实现了对不同知识源(内存、PgVector、云服务)和不同检索策略(全库检索、过滤检索)的支持。
这套组合拳下来,构建的 RAG 系统不仅功能完善,而且架构清晰、扩展性强,充分体现了 Spring AI 框架的强大与优雅。
最后总结
- 基础准备:熟悉
gemini-cli
的核心命令,特别是/memory
和/chat
系列。
当然你可以可以使用
.toml
命令,具体详情参考官网文档,也会让你事半功倍!
迭代优化 Prompt:先从简单的提问开始,根据 AI 的反馈,不断优化你的 Prompt,甚至可以请求 AI 帮你优化,不断探索ai会不听指令的边界情况,在prompt中不断进行约束
打造“黄金 Prompt”:将成熟的指令和规则沉淀到
GEMINI.md
文件中,为 AI 注入“灵魂”,将其训练成你的专属技术导师。模块化学习:
- 先让 AI 提供一个项目的整体“学习地图”。
- 使用
/chat save
保存这个全局视图作为 Checkpoint。 - 针对每一个你感兴趣的模块,开启一个学习分支进行深度探索。
- 完成一个模块后,使用
/chat resume
回到全局视图,再开启下一个模块的学习。
不可否认,要深度掌握一门技术,亲手敲下每一行代码的体会是无可替代的。但我认为这套方法的价值在于,它能让你在投入编码实践之前,以极高的效率,建立起对一个陌生项目宏观架构和微观细节的深刻理解,让你“知其然,更知其所以然”。