Q1:如何实现 AI 多轮对话功能?如何解决对话记忆持久化问题?
A1:
本项目通过spring AI提供的chatmemory 和advisor实现对话记忆持久化和多轮对话功能,chatmemory用于存储历史对话,advisor类似拦截器用于在调用AI前后执行额外操作。
具体来说,首先创建chatclient用于调用AI。然后实现chatmemory接口,由于默认的实现类inmemorychatmemory将历史对话存在内存中,重启服务会丢失数据,所以我自定义filebasedchatmemory实现chatmemory接口,使用kryo序列化库将历史对话存到文件中,实现持久化存储。
最后定义messagechatmemoryadvisor,在调用AI前将历史对话作为message拼接到用户问题上。
Q2:你在智能体项目中如何实现对话记忆持久化?
A2:
使用基于文件的保存,MessageChatmemoryAdvisor拦截对话上下文,加入到对话历史消息列表,然后使用kyro序列化保存到文件,下次会话时反序列文件内容添加到对话上下文中。
- BASE_DIR
- kyro序列化
- FileBasedChatMemory:
- get(conversationID, lastN),根据对话ID获取文件,反序列为消息列表,根据lastN返回最近的N条消息
- add(conversationID, []messages)当需要添加新的消息到某个对话时,这个方法首先会根据 conversationId 获取或创建一个该对话的消息列表 (getOrCreateConversation)。然后将新消息追加到列表中,通过 saveConversation 方法将整个更新后的消息列表序列化回文件
- clear删除文件
Q3:什么是结构化输出?Spring AI 是怎么实现结构化输出的?
A3:
定义:结构化输出是指将大模型返回的文本数据转化为具有特性格式的数据,比如Json、Xml或者Java类。
总体 :Spring AI 是两个步骤来实现结构化输出的,分为调用大模型前 和调用大模型后的两个操作。
调用前,Spring AI 会将指令添加到提示器里面,明确告诉 AI 我需要你返回什么格式的数据。
调用后 ,根据返回的数据,一般是Json格式的字符串,使用转化器将其转化为指定格式的数据。Spring AI 内置了很多转换器的实现。
总结 :也就是说,首先通过修改提示词 告诉 AI 需要转化的特定格式,再调用转化器对返回的字符串进行转化,这样就实现了结构化输出。
回归项目 :在项目当中,可以使用ChatClient 的.entity来快速实现结构化输出,将结果映射到 Java类上。但是结构化输出只能说是尽可能进行转化,模型并不能保证每次都完美实现。
Q4:在 智能体项目中你是如何实现 RAG 知识库构建的?
A4:
在项目中,我主要通过Spring AI + 本地知识库的方式来构建和实现RAG知识库功能。 首先,我把知识内容整理成 Markdown 文档,然后用 Spring AI 的 MarkdownDocumentReader 读取文件内容,并为每个文档加上元数据,方便后续检索。
接着,这些文档会通过 EmbeddingModel转换成向量,存入 Spring AI 内置的内存型向量数据库 SimpleVectorStore。它可以直接接收并处理文档列表的写入请求,内部完成向量化和存储过程。
在查询阶段,我用 Spring AI 的 QuestionAnswerAdvisor 做无感集成。它会在用户提问时自动检索最相关的文档片段,把这些内容拼进用户的原始问题中形成增强 Prompt,再交给大模型生成更准确、更有知识支撑的回答。
Q5:RAG 的文档处理流程是怎样的?
A5:
核心目标: 让 AI 回答问题时,能像查菜单一样快速找到你上传的文档里的知识,而不是只靠它自己脑子里记的东西(可能记不全或记错了)。
流程步骤(通俗披萨店版):
-
准备食材 & 做菜单(文档预处理):
- 你开了一家披萨店(RAG系统),有很多秘方(你的文档:PDF、Word、网页、笔记等等)。
- 这些秘方写得乱七八糟:有的很长(一本书),有的有废话(广告、页眉页脚),格式五花八门。
- 切菜工(文档处理器)上场:
- 拆包装(格式转换): 把所有秘方都转换成统一的"厨房用纸"(纯文本)。
- 去烂叶(清理): 扔掉没用的部分(广告、特殊符号、乱码)。
- 切小块(分块/Chunking): 把大段秘方(比如整本披萨大全)按主题切成容易处理的小块。比如:
- 一块讲"面团配方"
- 一块讲"番茄酱熬制秘诀"
- 一块讲"芝士选择指南"
- 一块讲"烤炉温度控制"
- (关键!块不能太大也不能太小,得像"一口能吃下的披萨角",方便后续查找)
-
给每块食材贴智能标签(文本嵌入/Embedding):
- 光切好块还不行,厨房(数据库)太乱,找起来慢。
- 智能标签机(嵌入模型)上场:
- 拿起一块秘方(比如"面团配方"),仔细"读"一遍,理解它的核心意思(需要水、面粉、酵母、揉面时间、发酵条件...)。
- 根据理解,生成一个独特的、密密麻麻的数字密码(向量/Vector) 。这个密码神奇之处在于:
- 意思相近的块,密码也相似! 比如"面团配方"和"发酵技巧"的密码会很接近。
- 意思不同的块,密码差别大! 比如"面团配方"和"外卖电话"的密码天差地别。
- 把这个数字密码(向量) 当作"智能标签"贴在这个秘方块上。
-
把贴好标签的食材存进智能冰柜(向量数据库存储):
- 现在,所有切好、清理好、贴好智能标签(向量)的秘方块,都整整齐齐地存进一个超级智能冰柜(向量数据库)。
- 这个冰柜的牛X之处在于:
- 它不光存储秘方块本身(文本内容)。
- 它更擅长根据标签(向量)的相似度来快速查找!想象冰柜里的东西是按"意思相似度"自动排列好的。
到此为止,文档处理流程就完成了! 你的披萨店(RAG系统)现在拥有:
- 一个 智能菜单库(向量数据库),里面存好了所有处理过的、贴了智能标签的秘方块(文档块 + 向量)。
当顾客(用户)来点单(提问)时,流程继续(这才是 RAG 的检索和生成部分):
-
理解顾客想吃啥(问题嵌入):
- 顾客问:"你们家面团为啥这么筋道啊?"(用户问题)
- 智能标签机(嵌入模型)也给这个问题生成一个数字密码/标签(问题向量),代表"面团筋道的原因"。
-
翻智能菜单找秘方(检索/Retrieval):
- 拿着这个问题的标签(问题向量),跑去智能冰柜(向量数据库)里喊:
- "快!把标签意思最接近'面团筋道原因'的那几块秘方找出来!"
- 冰柜瞬间工作,比较问题向量和所有存储的向量,找出最相似(意思最相关)的几个文档块(比如"面团配方"块里提到了高筋面粉和揉面手法,"发酵技巧"块里提到了低温长时间发酵)。
- 这几块最相关的秘方被抽出来,作为"参考材料"。
- 拿着这个问题的标签(问题向量),跑去智能冰柜(向量数据库)里喊:
-
大厨结合秘方做披萨(生成/Generation):
- 大厨(AI 语言模型,如 ChatGPT)原本只知道通用披萨知识。
- 现在,大厨手上拿到了顾客的具体问题("面团为啥这么筋道")+ 刚刚翻出来的几块最相关的自家秘方("面团配方"、"发酵技巧")。
- 大厨仔细看了看这些秘方,结合自己的厨艺(通用知识)和店里的独门秘方(检索到的文档) ,现场做出一份专属回答 :
- "我们的面团筋道主要有两个秘诀:一是选用优质高筋面粉,蛋白质含量高;二是采用独特的低温长时间发酵工艺(参考咱家秘方第X条),让面筋网络充分形成。这样烤出来的饼底就特别有嚼劲啦!"
总结 RAG 文档处理流程(就是准备智能菜单的过程):
- 收食材(收文档) -> 2. 统一处理 & 切块(格式转换、清理、分块) -> 3. 贴智能标签(文本嵌入成向量) -> 4. 存智能冰柜(存入向量数据库)
这个流程让 AI 在回答问题时,能快速、精准地从你自己的文档里找到最相关的知识片段,从而给出更靠谱、更有依据的答案!就像给大厨配了一个能秒查秘方的智能菜单库。
Q6:什么是 ETL 数据处理?在构建 RAG 知识库时有什么作用?你是如何利用 Spring AI 实现 ETL 的?
A6:
ETL:抽取、转换、加载
抽取:使用DocumentReader将项目中的文档读取出来
转换:使用DocumentTransformer对加载文档进行拆分,实际上使用的是MarkdownDocumentReader中的自动拆分,文本分割器以及元数据增强功能
加载:使用DocumentWrier的子类VectorStore将数据存储到基于内存的向量数据库,使用Document列表进行存储
Q7:在 智能体项目中,你是如何实现 RAG 向量存储的?为什么这么实现?
A7:
选择 PGVector 作为 RAG 知识库向量存储方案:
实现步骤:
- 使用阿里云 PostgreSQL 实例,安装并启用 PGVector 扩展
- 在 Spring Boot 配置文件中设置数据库连接信息
- 通过
PgVectorStore.builder()创建实例,需传入JdbcTemplate和明确指定的EmbeddingModel - 解决多
EmbeddingModelBean 冲突问题,手动配置并排除自动配置
选型原因:
- 整合现有基础设施:复用已有 PostgreSQL 数据库,无需引入独立向量数据库,降低架构复杂度和运维成本
- 数据一致性和事务支持:向量与业务数据同库存储,利用 PostgreSQL 成熟的事务、备份恢复机制
- 成熟的生态系统:PostgreSQL 拥有庞大工具、驱动和社区支持
- Spring AI 支持:Spring AI 提供良好集成,接入成本低
Q8:在 智能体项目中,你为什么要设计批处理优化策略?它解决了什么问题?
A8:
在 Spring AI 中,BatchingStrategy处理 "token 数量差异大" 的文本时,核心是通过动态调整批次大小和适配模型 token 限制来避免超出模型的单次请求上限,具体实现逻辑如下:
Token 预估算与动态分组 对于 token 数量差异大的文本(如短文本仅几十 token,长文本达上千 token),BatchingStrategy会先基于文本长度(字符数或预训练的 tokenizer)估算每个文本的 token 数,再根据模型的单批最大 token 限制(如 OpenAI 的text-embedding-ada-002单批上限为 8191 token)动态调整批次内容: 短文本可按batchSize(数量)批量合并,只要总 token 不超限; 长文本可能单独成批(若单条已接近上限),或与少量短文本组合(确保总 token 不超标)。 例如,模型单批上限为 1000 token 时: 若一条长文本 800 token,则最多只能再加入 1 条 200 token 的短文本组成一批; 若一条长文本 900 token,则单独作为一批,避免超限。 处理超长文本 对于超过单条 token 上限的文本(如某片段 2000 token,模型单条最大 1500 token),BatchingStrategy会配合TextSplitter先将其拆分为更小的子片段(确保每个子片段 token 数在单条限制内),再按上述动态分组逻辑批量处理。 适配模型特性 不同嵌入模型的 token 限制和处理逻辑不同(如有的模型对单批数量有限制,有的对总 token 数有限制),BatchingStrategy的实现会根据具体模型的元数据(如通过EmbeddingModelMetadata获取限制参数)自动适配分组策略,避免硬编码。 异常兜底 若批量请求因 token 超限失败,部分策略会触发 "自动拆分重试"------ 将当前批次拆分为更小的子批次(如对半拆分),重新提交请求,确保兼容性。