做RAG项目快两年了,我发现一个现象:很多人第一次接触RAG,都是从“把文档扔进知识库,然后问AI”开始的。真正做生产级应用时才发现,切分、嵌入、检索、评测,每一环都能把项目拖垮。这篇内容没有高大上的理论迷阵,我用13张图的节奏,从向量的直观理解一直讲到Spring AI 2.0的完整落地,最后给你一套直接能用的评测体系。如果你是Java技术栈,想用Spring AI快速构建RAG知识库,或者你在Dify这类工具里画过流程但想回归代码控制,这篇文章应该能省你不少弯路。
1. 项目整体设计:13张图先画给谁看
1.1 聊RAG之前,先说清我们的目标
RAG(Retrieval-Augmented Generation,检索增强生成)要解决的是LLM的两个天然短板:一是知识有截止时间,公司内部文档它没见过;二是它喜欢一本正经地编答案,也就是幻觉。RAG的做法简单直接——先根据用户问题去知识库里检索相关资料,把找到的资料塞进Prompt,再让模型基于这些资料作答。相当于从“闭卷考试”变成“开卷考试”,模型手里有了参考书,胡说八道的概率自然低很多。
做这个项目前,我先把13张图规划了出来,它们分别对应RAG从原理到落地的13个关键问题。这13张图既是文章的骨架,也是我当时给团队做技术分享时用的主线:
| 图号 | 主题 | 要解决的疑问 |
|---|---|---|
| 图1 | RAG全景流程 | RAG整体是怎么串起来的 |
| 图2 | 向量的二维示意 | 向量为什么能表示语义 |
| 图3 | 文本嵌入链路 | 从文档到向量的完整过程 |
| 图4 | 相似度计算 | RAG检索背后的打分逻辑 |
| 图5 | 向量数据库索引结构 | 百万级向量为什么还能秒回 |
| 图6 | 向量数据库选型对比 | 什么场景选什么库 |
| 图7 | Spring AI核心组件 | RAG在Java里的实现骨架 |
| 图8 | 文档切分策略 | chunk size与overlap怎么定 |
| 图9 | 项目目录与依赖 | 一个可跑的工程长什么样 |
| 图10 | 检索排序调优 | 怎么让正确结果排到前面 |
| 图11 | 四类RAG瓶颈 | 回答质量差,问题出在哪一环 |
| 图12 | 评测指标体系 | 除了肉眼判断,还能怎么测 |
| 图13 | 自动评测闭环 | 评测如何沉淀成持续流程 |
如果你也准备在公司内部推RAG项目,我建议先照着这个框架把图1画出来,不要一上来就写代码。RAG看似简单,其实文档处理、向量化、检索、模型问答、评测五条链路都有各自的坑,先把全局图画清楚,后面踩坑时至少知道坑在哪个环节。
1.2 为什么选Spring AI而不是别的框架
项目选型时,有人推荐LangChain4j,也有人推荐直接用Python的LlamaIndex。我们团队全是Java后端,如果用Python那套,后续要单独维护一套Python服务,跨语言联调和部署都很痛苦。Spring AI的价值在于它是Spring官方主导的AI抽象层,把EmbeddingModel、VectorStore、ChatClient这些概念统一成了Spring风格,Java开发者学习成本很低。
我当时也对比过Dify这类可视化平台。Dify上手快,适合业务同学快速搭原型,但到了生产环境,发现剂约力不够——你要定制检索逻辑、加权限过滤、接公司内部认证体系、做性能监控,可视化节点反而成了瓶颈。所以这次项目定的是“Dify验证可行性,Spring AI做最终落地”,用代码接管完整链路,Dify工作流只用来快速验证Prompt和流程是否合理。
2. 从“向量是什么”开始:RAG的第一块地基
2.1 向量就是一组语义坐标
很多人一听“向量”就发怵,觉得是线性代数内容。其实你完全可以把它理解成坐标。图2里我画了一个二维坐标系,横轴可以理解为“水果程度”,纵轴是“事物具体程度”,那么“苹果”可能落在(8, 6),“香蕉”在(8, 5),“汽车”却在(1, 3)。坐标越接近,表示这两个词语义越接近。
真实世界的文档不会只有两个特征,所以向量通常是768维、1024维甚至1536维。维度越多,能区分的语义维度就越丰富。Embedding模型做的工作,就是把“苹果”“香蕉”这样的人类语言,映射成高维空间里的一个点。RAG的整个检索逻辑,本质上是在这个高维空间里做“找邻居”。
理解这一点很重要,因为它决定了后面所有调优方向:一个Embedding模型不好用,往往不是模型本身差,而是它表达语义的方式和你业务文档的特征不匹配。比如法律文书和客服问答,最好用不同的Embedding模型分别实验。
2.2 从文档到向量的完整链路
图3展示的是文本嵌入的完整过程:原始文档(PDF、Word、Markdown)先被解析成纯文本,然后按规则切成一段段小块,每段文本交给Embedding模型转成向量,最后连同文本和元数据一起写进向量数据库。这个过程看似简单,但“切分”这一步直接决定检索上限。
切分切得太粗,一块文本里混了好几个主题,检索时虽然能命中,但上下文里塞了太多无关内容;切得太细,单个块语义不完整,检索到的片段往往答非所问。我在项目里实测的通用规则是:通用FAQ类文档用500-800个token,合同、法律文件用200-300个token,因为合同一个条款就是一个完整语义单元,切开语义就碎了。
Embedding模型这里也踩过坑。中文场景我推荐两套:开源的本地方案用BGE系列(比如bge-m3),不需要联网,成本低;线上生产如果有预算,用云厂商的text-embedding系列,效果通常更好。需要特别强调:测试环境和生产环境的Embedding模型必须保持一致,一旦换了模型,向量空间就变了,旧向量会全部失效,必须全量重建。这个坑我在第5节评测部分还会展开。
2.3 相似度计算:点积、余弦与向量范数
检索的本质是拿用户提问的向量和库里所有文档向量做相似度计算,选最接近的返回。图4里最常用的三种计算方式:
- 余弦相似度:公式是 cos(A,B) = (A·B) / (|A|×|B|),只看方向不考虑长度,文本向量场景最常用,数值范围在[-1,1]。
- 点积(内积):A·B = Σ(a_i × b_i),同时受方向和长度影响,适合向量已经做过归一化的场景。
- 欧氏距离:计算的是空间中的直线距离,在RAG里不常用,因为向量维度很高时距离反差不明显。
这里涉及“向量范数”的概念,通俗理解就是向量的长度(模长)。我见过不少人在代码里直接比较点积结果,却不看向量是否做过归一化。Spring AI的多数Embedding模型返回的向量默认是归一化的,此时余弦相似度和点积是等价的,你选哪个都行;但如果模型没做归一化,直接比点积就会出现“长向量天然得分高”的问题,结果会很奇怪。
从工程角度,我建议在项目里统一用余弦相似度,并且在写入向量库前确认向量是否L2归一化过。多数向量数据库在构建索引时也有默认的相似度度量,选型和建索引时就要定好,中途改metric很可能要重建全量索引,耗时很长。
2.4 复向量、多向量:什么时候才需要
搜索热词里有个“复向量”,很容易让人误解成数学里的复数。这里大家真正搜的其实是有两套方案:向量数据库处理多向量,以及多向量检索。简单说,就是一段文本不再是只生成一个向量,而是生成多个向量。
典型代表是ColBERT这种late interaction模型,它给文本里每个token都生成一个向量,检索时拿查询向量和文档的token级向量做细粒度匹配,能捕捉“苹果公司”里的“苹果”到底是水果还是公司这种上下文差异。这种方案对小段精确匹配效果更好,但存储量会放大10倍以上,计算也更重。
我做项目的经验是:如果你的文档是短文本问答(比如客服FAQ),单向量完全够用;如果你是长文档、法律条款、论文这类需要“在长文里定位关键句子”的场景,才值得认真考虑多向量。切勿一上来就上多向量,先跑通单向量基线,评测发现检索精准度不够,再考虑升级。
3. 向量数据库集成与优化:检索层才是工程重心
3.1 向量数据库选型:别只看跑分
图5我画的是向量数据库索引的简化结构:一棵多层图(HNSW)或倒排文件(IVF),排在前面的是“粗略索引”,后面再精排。图6则是选型对比。你会发现向量数据库的领域非常热闹,pgvector、Milvus、Qdrant、Redis、Elasticsearch都在做,选型标准其实不在“谁能跑100万分”,而在“谁和你现有架构匹配”。
| 方案 | 适合场景 | 规模能力 | 运维成本 | 混合检索 | 备注 |
|---|---|---|---|---|---|
| pgvector | 已有PostgreSQL的小团队 | 千万级以内 | 低 | 支持 | 无需新引入中间件 |
| Milvus | 大规模独立向量服务 | 亿级 | 高 | 支持 | 功能全面、社区活跃 |
| Qdrant | Rust实现的独立向量库 | 千万到亿级 | 中 | 支持 | 性能不错、API友好 |
| Redis | 低延迟缓存型检索 | 百万级以内 | 低 | 弱 | 适合轻量场景 |
| Elasticsearch | 需要全文检索强耦合 | 千万级 | 高 | 强 | 已有ES团队才推荐 |
我在生产里优先选了pgvector,原因是团队太多中间件了,能少一个就少一个。向量量级在千万级以内、每天增量不大时,pgvector完全顶得住,而且可以和业务数据放在同一个库里做事务和权限过滤。等到数据量真涨到需要独立向量服务,再平移到Milvus也不迟,因为Spring AI的VectorStore接口是抽象封装的,替换实现只需要改配置。
3.2 HNSW与IVF:索引参数到底怎么调
图5里HNSW的结构像一个多层跳表,越上层节点越稀疏,检索时从顶层往下走,快速定位到近似最近邻。HNSW的核心参数有三个:
- M:每个节点的最大连接数,默认16。M越大,检索精度越高,但内存和构建时间也越高。
- efConstruction:构建索引时的动态列表大小,默认100。越大索引质量越高,构建越慢。
- efSearch:查询时的动态列表大小,默认40。越大检索越准,但延迟越高。
我一般建议从M=16、efConstruction=100、efSearch=40起步,然后用评测集把efSearch从40调到200,看在hit rate的提升和P99延迟之间找平衡点。IVF则是“先聚类再搜索”:nlist决定聚成多少个桶,nprobe决定查询时搜多少个桶。nlist设为数据量的sqrt左右,nprobe从1开始往上调。
有一个容易被忽略的点:索引参数的作用范围和你的数据分布高度相关。如果你的知识库文本类型差异很大(既有短标题又有长正文),tuning一个通用参数不如先给文档打类型标签,然后在查询时用元数据过滤缩小搜索范围。
3.3 元数据过滤与混合检索:别把整个库都拿来排序
纯向量检索的痛点是:语义相近不代表业务相关。比如用户问“退货政策”,向量库里可能有“商品退货说明”也有“员工离职退电脑流程”,语义都沾边,但你只想要商品中心的数据。解决办法是给每个Document写入元数据,检索时用过滤条件把范围圈起来。
Spring AI的SearchRequest支持类似filterExpression("docType == 'product' && status == 'active'")的表达。这个才是我认为向量检索工程化的核心:真正上生产时,RAG的检索一定不是“全库语义搜索”,而是“有权限、有时间范围、有业务类型的语义搜索”。数据权限甚至可以映射到查询过滤条件里,不同角色的用户检索到不同的数据集。
混合检索(BM25全文检索 + 向量检索)也值得关注。某些情况下,用户输入的是产品型号“A-100-X”,向量检索可能找不到,但BM25的精确关键词匹配效果反而稳定。Spring AI 2.0的VectorStore接口里可以同时保留多个存储实现,手动合并两套结果再重排,这是RAG进阶必须掌握的技巧。
3.4 RAG知识库能存图片吗
搜索里高频出现的“rag知识库能存储图片嘛”,答案是可以,但要区分清楚。图片进RAG有两种路线:
第一,多模态Embedding。用CLIP、SigLIP这类多模态模型,把图片和文本映射到同一个向量空间,查询时拿文字向量直接检索图片向量。适合“给一张图找类似图”“用文字描述找设计稿”的场景。
第二,先用多模态大模型把图片转成文字描述,再走常规文本RAG。比如给产品截图生成一段说明文字,入库后用户问“那个截图里的按钮功能是什么”,纯文本检索也能命中。这种方式实现简单,成本低,适合绝大多数企业内部知识库场景。
我给业务方做技术方案时通常推荐第二条路线。原因很现实:企业内部图片资产大多是产品截图、流程图、单据扫描件,用户要的是“根据图片内容回答问题”,而不是“按图搜图”。用图片描述文本入库,流程可复用现有链路,也方便人工审核。只有真正需要图像检索的业务(比如以图搜图的设计素材库),才值得上多模态向量。
4. Spring AI 2.0实战:把RAG跑在Java服务里
4.1 Spring AI核心组件长什么样
图7是Spring AI 2.0里和RAG相关的核心组件,本质上就四个:EmbeddingModel负责向量化,VectorStore负责存取向量,DocumentReader负责读文档,TextSplitter负责切块。把这些配好之后,问答链路就是:用户问题 → 向量化 → 检索 → 组装Prompt → LLM回答。
Spring AI 2.0相比1.x,最大的变化是把很多模型接入方式改成了spring.ai.model.*自动配置,配置简化了不少。比如OpenAI兼容协议的模型,在application.yml里配好base-url和api-key,ChatModel和EmbeddingModel的Bean就自动生成。这对国内云厂商的模型服务特别友好,因为大多数云服务都暴露了兼容OpenAI协议的接口。
依赖方面,一个典型的Spring Boot 3项目只需要引入spring-ai-starter-model-openai、spring-ai-starter-vector-store-pgvector这类starter就行。我强烈建议用Spring Initializr或者Maven仓库里明确标注的版本,Spring AI版本迭代很快,不同版本之间的API偶尔会有调整,不要直接照抄网上的旧代码。
4.2 接入通义模型或本地Ollama
对接通义大模型或者本地Ollama,配置方式几乎相同。下面这套配置在Spring AI 2.0里实测可用:
spring: ai: model: chat: openai: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 options: model: qwen-plus embedding: openai: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 options: model: text-embedding-v3如果团队偏好本地部署,直接用Ollama也行,配置文件改成:
spring: ai: model: chat: ollama: base-url: http://localhost:11434 options: model: qwen2.5:7b embedding: ollama: base-url: http://localhost:11434 options: model: bge-m3:latest这里有一个值得注意的兼容性问题:模型服务的model名要以实际开通为准。云厂商的模型ID经常更新,比如qwen-plus、qwen3.x系列在控制台上以实际开通值为准。本地Ollama则要看拉取过的模型列表,代码里写错模型名,启动不会报错,但运行时调用就会出现模型不存在之类的错误,排查起来容易绕圈子。
4.3 文档切分策略实操
图8的主题是切分。我一直觉得切分是整个RAG里最“看着简单、做起来全是坑”的环节。Spring AI里常用的TokenTextSplitter实现如下:
TextSplitter splitter = TokenTextSplitter.builder() .withDefaultChunkSize(500) .withDefaultOverlap(50) .build(); List<Document> chunks = splitter.apply(reader.get());chunk size我建议结合你的模型最大上下文来定。如果模型上下文是128k,chunk设大一点没太大问题,因为可以多塞几个段落到Prompt里;如果模型上下文只有8k,chunk设500但检索返回4个段落,再加上系统提示词和用户问题,上下文就被塞满了。
overlap(重叠)的作用是避免“一句话被从中间切开”导致语义断裂。50-100个token的overlap比较稳妥。但重叠也带来重复内容,会稍微增加存储和Prompt成本,具体表现为检索结果里相邻块经常同时命中。这个现象不算bug,但如果重复过多,说明chunk size设小了。
我做合同类项目时会配合一个自定义切分:先按章节标题定位,再按段落切,最后用TokenTextSplitter兜底。这样做的好处是不会把不同法律条款混在一个chunk里。Spring AI 2.0支持自定义DocumentReader和TextSplitter,团队里有Java功底,写个扩展并不难。
4.4 一个最简可跑的RAG Pipeline
图9是一个最简可运行的项目结构:
src/main/java/com/example/ragdemo ├── RagDemoApplication.java ├── config/VectorStoreConfig.java ├── service/RagService.java └── controller/RagController.java先说初始化。EmbeddingModel的Bean由Spring Boot自动注入,VectorStore这里用pgvector:
@Bean VectorStore vectorStore(EmbeddingModel embeddingModel, DataSource dataSource) { PgVectorStore store = new PgVectorStore(EmbeddingModel converter, dataSource); return store; }写入文档的Service方法:
public void ingest(String filePath) { DocumentReader reader = new PagePdfDocumentReader(filePath); List<Document> docs = splitter.apply(reader.get()); vectorStore.add(docs); }查询链路是Spring AI里最有价值的部分,用QuestionAnswerAdvisor实现了“检索+增强生成”的整个包装:
@RestController public class RagController { private final ChatClient chatClient; public RagController(ChatClient.Builder builder, VectorStore vectorStore) { this.chatClient = builder .defaultAdvisors(QuestionAnswerAdvisor.builder() .vectorStore(vectorStore) .build()) .build(); } @PostMapping("/chat") public String chat(@RequestBody String question) { return chatClient.prompt() .user(question) .call() .content(); } }这段代码写到这一步,一个RAG服务就能跑了。用户提问后,QuestionAnswerAdvisor自动完成向量化、检索、把上下文塞进Prompt,最后让模型基于检索内容回答。我特别欣赏Spring AI这一点,它不像别的框架把RAG流程写在框架内部黑盒里,而是通过Advisor机制暴露出来,你可以随时加一个新的Advisor做权限过滤、对话历史、Prompt模板改写,组合起来非常灵活。
我再贴一段更精细的检索参数控制:
SearchRequest request = SearchRequest.builder() .query(question) .topK(4) .similarityThreshold(0.5) .filterExpression("docType == 'contract'") .build(); List<Document> docs = vectorStore.similaritySearch(request);这里topK决定取多少片段,similarityThreshold决定最低相似度门槛。如果检索结果里有一堆低分内容混进来,先检查threshold是不是没设;但threshold设太高又会漏检,一般0.4-0.6之间需要反复试。
4.5 从Dify工作流到Spring AI代码:一张映射表
很多团队先用Dify把RAG原型做出来,后面希望用Java代码接管。这时候你会发现,Dify里每个可视化节点和Spring AI的组件都有对应关系。图10我用映射表把它们对应起来,照着迁移就行:
| Dify节点 | Spring AI对应组件 | 说明 |
|---|---|---|
| 知识库检索节点 | VectorStore.similaritySearch | 配置过滤条件、topK |
| 问题分类节点 | ChatClient + LLM Router | 写成switch分支或单独Model调用 |
| Prompt模板节点 | Advisor + PromptTemplate | 定制System Prompt |
| Agent节点 | ChatClient + ToolCalling | Spring AI的Function Calling |
| 知识库添加 | DocumentReader + TextSplitter | 导入方向由节点改成代码任务 |
从Dify迁到Spring AI,本质是把可视化流程翻译成代码流程。迁移过程中建议不要一次性全搬,先把“知识检索+LLM回答”的最小链路跑通,再逐步添加工作流里那些条件分支。Spring AI对复杂流程的支持是足够的,只不过它不提供拖拽画布,你得接受用代码来表达流程这个事实。
5. RAG评测体系:从“感觉不错”到“可量化”
5.1 先诊断瓶颈:四类问题对应四个环节
搜索里“rag瓶颈”出现频率很高,说明不少项目都卡在“效果不够好但不知道哪里不好”。图11给出了四类典型瓶颈,正好对应RAG的四个环节:
- 文档处理环节:PDF解析乱码、表格丢失、扫描件没有OCR。表现是所有问题答非所问。
- 切分环节:chunk太小或太大,语义被切断,检索返回内容不完整。
- 检索环节:该命中的没命中,或者命中了但排序靠后。
- 生成环节:检索结果都是对的,但模型没按检索内容回答,或者原样复述了一堆废话。
诊断方法是把RAG基座拆开:第一步单独打印检索结果,用“带检索结果的离线测试集”直接看top5里有没有正确答案;第二步把正确答案硬塞进Prompt,看生成质量。如果第一步过了第二步没过,问题在生成层;如果第一步就没过,问题在检索层或文档处理层。
5.2 评测指标:hit rate、MRR与NDCG怎么算
图12是评测指标体系。肉眼判断问题不好量化,我们需要一组可计算、可对比的指标。RAG评测里最常用的三个检索指标:
- hit rate(命中率):用户提问后,top-N检索结果里是否包含目标文档。计算方式是:命中数量 / 总提问数。这指标最简单,适合先卡基线。
- MRR(平均倒数排名):只关心第一个正确答案排在哪个位置。公式是 MRR = (1/N) × Σ(1 / rank_i),rank_i是第i个问题第一个正确答案的排名。如果没命中,该问题计0。
- NDCG(归一化折损累计增益):考虑整个排序质量。公式是 DCG = Σ(rel_i / log2(i+1)),NDCG = DCG / IDCG。rel_i是相关性打分,比如正确答案给1,相关但不完全匹配给0.5,无关给0。
对应的Java实现并不复杂:
public double hitRate(List<Boolean> hitFlags) { long hit = hitFlags.stream().filter(Boolean::booleanValue).count(); return (double) hit / hitFlags.size(); } public double mrr(List<Integer> ranks) { double sum = 0.0; for (int rank : ranks) { if (rank > 0) sum += 1.0 / rank; } return sum / ranks.size(); }生成侧还有一个“忠实度”指标,用来判断模型回答是否忠于检索上下文,这要和LLM-as-Judge结合。做法是把“用户问题、检索上下文、模型回答”一起发给裁判模型,让裁判打1-5分,并输出理由。打分脚本我在下游评测时常用Python跑,因为统计分析方便:
def faithfulness_score(context, answer): prompt = f"""判断回答是否完全由给定上下文支撑。 上下文:{context} 回答:{answer} 只输出 0 或 1,并给出理由。""" # 调用裁判LLM,解析分数 return parse_score(llm(prompt))5.3 搭一套可落地的自动评测闭环
图13是自动评测闭环,这套流程在我项目里稳定跑了好几个月。步骤如下:
- 构造测试集:从真实用户问题里挑20-50条,每条标注它对应的标准文档ID和一份标准答案。测试集要覆盖常见问题、模糊问题、长尾问题,规模不在大,而在于每一条都能暴露问题。
- 批量跑RAG:写个离线脚本,逐条调用检索接口,记录检索到的文档ID、排序、最终生成的答案。
- 自动计算检索指标:对照标注的文档ID,直接算出hit rate、MRR、NDCG。
- 生成质量打分:用裁判LLM给忠实度、答案相关性打分。
- 回归对比:把每次改动前后的指标记录下来,形成一个简单趋势表。改切分参数、换Embedding模型、加过滤条件,都靠这个表拍板。
我的经验是:第一次跑这个闭环时,指标难看是正常的,千万别慌。你只需要记录基线,然后每次只改一个变量。比如这周改chunk size从300到500,下周再改overlap,每次对比hit rate和MRR的变化,你很快就能找到当前瓶颈。
还要提醒一个常被忽略的坑:不要用生产环境的同一个LLM既做生成又做裁判。生成和裁判如果共用同一个模型,会在“是否认可自己回答”这件事上产生主观偏向。最好裁判模型用能力更强或者至少不同温度参数的大模型,把temperature调低到0.1以下,打分结果才稳定。
5.4 评测之后:向Agentic RAG与GraphRAG进阶
当你把上面这套评测指标跑出来后,可能会发现一些单纯调参解决不了的问题。比如单轮RAG无法回答“对比A部门和B部门去年Q3的报销差异”这类多跳问题,因为信息分布在多个文档块,一次检索很难覆盖全。这时候就要考虑Agentic RAG,让模型具备多轮检索能力:先检索出A部门数据,再检索B部门数据,最后汇总对比。
如果业务问题强依赖实体关系,比如“这个供应商和哪些项目有关联”,GraphRAG或Ontology RAG值得调研。GraphRAG把文档里的实体和关系抽取成知识图谱,Ontology RAG更进一步,用业务本体约束图谱结构。它们的共同点是前期建设成本高,但评测数据显示在关系密集场景下,回答准确率提升非常明显。
回到热门问题“rag和llm wiki”以及“解决了知识割裂”,其实很多团队的知识库不是没有资料,而是资料之间缺乏关联,导致检索时只能召回孤立片段。GraphRAG就是针对这种“知识割裂”的解法之一。但我的建议很直接:先把单轮RAG的评测跑好,确认瓶颈确实在“跨文档推理”后再上图谱。不要因为热词就盲目引入重方案,成本会高到你怀疑人生。
最后说一个我个人的实操心得。做这个项目时踩过最大的坑,就是中途换了一次Embedding模型,却没有重建向量库,结果线上检索返回了一堆风马牛不相及的片段。从那以后,所有模型升级都强制走“离线评测 + 全量重建向量”流程。还有一个实际的小技巧:评测集一开始只有5条也没关系,先把整个闭环跑通,再去扩充到50条。评测体系的建立,比评测集的规模重要得多。