从 Chroma 到 Milvus:一套 Agentic RAG 知识库的工程实践

前言

很多 RAG 演示项目看起来很简单:上传文档、切成文本块、生成向量、查询向量数据库,再把检索结果交给大模型回答。真正落到生产环境后才会发现,向量数据库只是整个链路中的一环。文档解析是否完整、切分是否破坏语义、用户问题是否被过度改写、候选结果如何融合、重排序接口是否正确,都会直接影响最终回答。

本文记录我在知识库系统中同时接入 Chroma 与 Milvus 的过程。系统既支持轻量的本地 Chroma,也支持 Milvus 的向量与 BM25 混合检索,并在此基础上加入查询扩展、原问题保留、RRF 融合、重排序和 Agent 多轮检索。文章会先介绍 RAG、BM25、向量数据库等基础概念,再说明项目的真实流程、测试结果以及开发中遇到的坑。

一、RAG 到底解决什么问题

大语言模型的知识来自训练数据,存在知识截止时间、私有资料不可见、答案无法溯源以及幻觉等问题。RAG(Retrieval-Augmented Generation,检索增强生成)的核心思想,是在回答前先从外部知识库中找到相关资料,再让模型依据这些资料生成答案。

一条典型链路可以概括为:

text 复制代码
文档上传
  -> 文档解析
  -> 文本切分
  -> Embedding 向量化
  -> 写入向量数据库
  -> 用户提问
  -> 检索相关文本块
  -> 重排序
  -> 将上下文交给大模型
  -> 生成带依据的答案

RAG 并不保证模型自动变得准确,它只是把"模型凭记忆回答"变成"模型依据检索材料回答"。如果检索阶段找错了资料,后面的模型再强,也可能一本正经地总结错误上下文。因此,RAG 工程的重点不是单纯接入一个向量数据库,而是持续提高有效召回率和前几名结果的相关性。

这里需要区分两个概念:

  • 召回率:真正有用的内容,有多少被找进了候选集合。
  • 排序准确度:找回的内容中,最相关的内容能否排在前面。

我们采用的策略是"宽召回、精排序":先通过向量、BM25 和查询扩展尽量找全,再用重排序模型把最适合回答问题的文本块排到前面。

二、向量检索、BM25 与混合检索

1. 向量检索

Embedding 模型会把问题和文本映射为高维向量。语义接近的文本,其向量在空间中的距离也更接近。本项目使用 BAAI/bge-m3 生成向量。

常见的向量相似度包括:

  • 余弦相似度(Cosine Similarity):比较两个向量方向是否一致,值越大通常越相似。
  • 点积(Inner Product):同时受向量方向和模长影响,常用于已归一化的向量。
  • 欧氏距离(L2):比较空间距离,距离越小越相似。

Milvus 检索使用的是 COSINE。余弦相似度可写为:

text 复制代码
cos(A, B) = (A · B) / (|A| × |B|)

向量检索的优势是能理解同义表达。例如用户问"哪些工厂进行了数字化改造",即使原文写的是"智能制造升级",仍可能被召回。它的弱点也很明显:语义看起来相似、事实却不属于目标范围的段落,也可能获得较高分数。

2. BM25 关键词检索

BM25 是经典的稀疏检索算法。它基于词频、逆文档频率和文档长度,对查询词与文档的匹配程度进行评分。直观理解如下:

  • 查询词在某段文本中出现,相关性会上升;
  • 越少见、越有区分度的词,权重越高;
  • 同一个词重复很多次后,收益会逐渐饱和;
  • 对过长文档进行长度归一化,避免长文本天然占优。

BM25 对产品型号、项目名称、专有名词、缩写和精确关键词尤其有效。例如"中国灯塔工厂""胶州空调互联工厂"等词,关键词检索通常比纯向量检索更稳定。但 BM25 不真正理解语义,如果用户换了一种说法,原文又没有对应词汇,就可能漏掉答案。

3. 为什么采用混合检索

向量检索擅长"意思相近",BM25 擅长"字面命中",二者是互补关系。本项目的 Milvus 知识库会对同一查询同时执行:

text 复制代码
查询文本
  ├─ BGE-M3 向量 -> Dense Search
  └─ 中文分词分析 -> BM25 Sparse Search
                    ↓
             WeightedRanker 融合

当前向量权重为 0.7,BM25 权重为 0.3。这个比例不是放之四海皆准的标准答案,而是当前资料和测试问题下的折中:以语义召回为主,用关键词命中补强专有名词和项目名称。

三、Chroma、Milvus 与常见向量数据库

Chroma 是一个轻量、易用的向量数据库,非常适合本地开发、单机应用和中小规模知识库。本项目将 Chroma 数据持久化到服务器本地目录,部署简单,不需要额外维护一套分布式服务。

Milvus 更适合数据规模增大、并发提高、需要独立检索服务或混合检索的场景。它支持向量字段、标量过滤和稀疏检索。本项目把 Milvus 当作第三方独立服务使用,只保存文本块、向量、BM25 索引和定位字段。原始文件与解析后的 Markdown 仍保存在应用服务器本地,不依赖 MinIO。

当前系统通过配置开关控制是否启用 Milvus:

ini 复制代码
[milvus]
enabled = true

关闭后,前端只能创建 Chroma 知识库;打开后,创建知识库时可以选择 Milvus 双路检索。一个知识库创建后,其存储后端固定,不在运行中随意切换。

除了 Chroma 和 Milvus,常见方案还有:

方案 特点 适合场景
FAISS 高性能向量检索库,但不是完整数据库 单机算法验证、离线检索
pgvector 在 PostgreSQL 中保存和查询向量 已有 PostgreSQL、数据规模适中
Qdrant 向量检索与过滤能力完善,部署较直接 独立向量服务
Weaviate 支持向量检索和结构化对象管理 需要较完整的数据模型
Elasticsearch/OpenSearch 关键词检索成熟,也可支持向量与混合查询 已有搜索基础设施
Pinecone 托管式向量数据库 希望减少基础设施运维

技术选型不应只看性能榜单。小型内部知识库使用 Chroma 足够省心;需要 BM25、独立服务和扩展能力时,再选择 Milvus 这样的服务型数据库,通常更合理。

四、文档解析与切分:检索质量的地基

1. 解析后先保存 Markdown

系统上传文档后,不会直接把文件粗暴地按字符截断。PDF 优先通过 PyMuPDF 解析,并根据字号等信息恢复 Markdown 标题层级;解析失败时使用 pypdf 回退。DOCX 会转换为 Markdown,扫描件则可通过 RapidOCR 识别。

解析结果会保存为:

text 复制代码
src/data/knowledge/uploads/<kb_id>/<file_id>.parsed.md

原文件也保存在同一知识库的本地目录。解析结果与向量索引分离有两个好处:一是调整切分参数后可以直接重新索引,不必重新上传;二是可以打开原文或解析全文,为 Agent 补充相邻上下文。

2. 为什么采用 Token 切分

早期常见做法是按字符数切分,例如每 1000 个字符切一块。但中文、英文、数字和标点对应的模型 Token 数量并不相同。最终上下文限制以 Token 计算,因此按字符切分容易出现块大小不稳定的问题。

当前实现基于 tiktokencl100k_base 编码,默认每块约 512 tokens,重叠 64 tokens。旧配置中的 chunk_sizechunk_overlap 仍为兼容字段,但不再驱动新的切分逻辑。

系统提供三种切分预设:

  • heading:按 Markdown 标题层级切分,并将标题路径写入文本块;没有标题时自动回退到 general。
  • general:以段落为基础进行 Token 贪心组合,超长段落再硬切。
  • separator:先按指定分隔符划分,再按 Token 预算组合。

默认使用 heading。它的价值在于保留语义结构。例如"第三章 > 灯塔工厂 > 应用案例"会成为文本块的上下文,即使块内没有重复完整标题,检索模型也能知道这段内容属于什么主题。

3. 切分中的坑

块太大时,一个 Chunk 会混入多个主题,向量表达变得模糊,也会浪费大模型上下文;块太小时,一个事实可能被拆散,检索到其中一半仍无法回答问题。重叠能缓解边界断裂,但重叠过大会产生大量重复结果。

扫描件还有一个特殊问题:OCR 结果通常没有可靠的标题层级,因此 heading 会退化为 general。对表格密集、双栏排版或扫描质量差的 PDF,解析质量往往比向量数据库选型更值得优先检查。

五、两条真实的检索链路

1. Chroma 检索流程

在 Agent 对话中,Chroma 当前的主要流程是:

text 复制代码
用户原始问题
  -> Agent 生成短关键词或扩展查询
  -> BGE-M3 生成查询向量
  -> Chroma 向量召回 recall_k 条
  -> 可选 Rerank
  -> 返回 top_k 条
  -> Agent 组织答案

Chroma 路线轻量、延迟低,但服务端不会自动融合"用户原问题"和"Agent 扩展词"。Agent 可以在结果不足时换一组关键词再次调用工具,因此它仍具有多轮检索能力,只是每次调用本质上是一次向量查询。

2. Milvus 检索流程

Milvus 路线会始终保留用户原始问题,同时允许 Agent 提供更短、更适合搜索的扩展查询:

text 复制代码
原始问题 ──> 向量 + BM25 ──> 混合结果 A
扩展查询 ──> 向量 + BM25 ──> 混合结果 B
                         ↓
                  加权 RRF 融合
                         ↓
                      Rerank
                         ↓
                    最终 TopK

当前原始问题权重为 0.7,扩展查询权重为 0.3。这样既不会因为模型改写丢失用户限定条件,又能利用关键词扩展提高召回。如果原问题与扩展词完全相同,系统只查询一次;某一路失败时,也会回退到另一路结果。

跨查询融合使用加权 RRF(Reciprocal Rank Fusion):

text 复制代码
RRF_score(d) = Σ weight_i / (k + rank_i(d))

其中 query_fusion_rrf_k=60 是排名平滑常数,不是召回数量。它越大,不同名次之间的分差越平缓。当前融合按 kb_id + file_id + chunk_index 去重,避免同一个文本块因命中两路查询而重复进入上下文。

六、Rerank:从"找得到"到"排得准"

首轮向量或混合检索追求召回,因此会先取 recall_k=12 条候选。随后使用 BAAI/bge-reranker-v2-m3 对"问题---候选文本"逐对打分,最后保留 top_k=8 条。

Embedding 是把问题和文档分别编码后进行近似搜索,速度快,适合海量召回;Reranker 会联合阅读问题和候选文本,计算更精细的相关性,成本更高但排序更准。因此不能用 Reranker 替代数据库的第一阶段召回,而应让两者各司其职。

这里踩过一个接口坑:硅基流动的 Embedding 接口是 /v1/embeddings,重排序接口必须使用 /v1/rerank。模型返回值也不一定都是 0 到 1 的概率,有些模型返回 Logit。当前实现采用自动归一化:0 到 1 的分数直接使用,超出范围时经过 Sigmoid 处理,并在接口异常时回退到初始排序。

similarity_threshold=0.0 也是有意设置的。Chroma 距离分、Milvus 混合分、RRF 分和 Rerank 分并不是同一量纲。如果在重排前使用一个未经标定的高阈值,很可能先把真正有用的候选删除。当前策略是先宽松召回,再由 Reranker 完成精排。

七、一次真实的对照测试

为了减少变量,我建立了两个知识库,上传完全相同的 PDF,并确认文件哈希和 142 个 Chunk 的内容一致,唯一差别是检索后端。测试问题是:

中国灯塔工厂在哪里项目上有过应用?

人工标注结果如下:

检索方式 相关块/Top8 噪声块 NDCG 代理指标
Chroma,未重排 6/8 2 0.8368
Milvus,未重排 7/8 1 0.8894
Chroma + Rerank 8/8 0 0.9599
Milvus + Rerank 8/8 0 0.9891

未重排时,一些只包含"工业应用案例"但并非灯塔工厂的段落被向量检索误召回。Milvus 通过 BM25 对"灯塔工厂"等关键词进行补强,噪声更少。加入 Rerank 后,两条链路的 Top8 都变成相关内容,而 Milvus 的关键案例排序更靠前。

这组结果说明当前问题上 Milvus 混合检索更好,但不能简单宣布"准确率永久提升了多少"。这是单文档、单问题、人工标注的代理测试,只能证明方案方向有效。严谨评估还需要建立包含事实问答、列表问答、同义改写、长问题、否定条件和无答案问题的测试集,并长期记录 Recall@K、MRR、NDCG、答案正确率与延迟。

测试还暴露出 top_k 过小的问题。部分案例分散在不同 Chunk 中,top_k=4 会漏掉排名稍后的事实,因此列表型问题当前采用 top_k=8。但 TopK 也不是越大越好,过多上下文会增加费用,并可能让模型被噪声干扰。

八、Agentic RAG:让检索成为一个推理过程

传统 RAG 通常只有一次固定查询,而 Agentic RAG 把知识库检索封装成工具,让 Agent 根据问题决定何时检索、查哪个知识库、使用什么关键词,以及结果不足时是否继续。

本项目中,每个已创建知识库都会对应一个独立的 knowledge_query_<kb_id> 工具。Agent 可以先查看知识库,再生成短关键词调用检索;若结果重复、证据不足或问题包含多个子任务,还可以更换关键词继续查询。必要时,Agent 能打开文档全文或相关段落窗口,而不是只依赖孤立 Chunk。

以灯塔工厂问题为例,Agent 可能执行:

text 复制代码
原问题:中国灯塔工厂在哪里项目上有过应用
扩展词:中国灯塔工厂 项目应用 案例

第一次检索:获取灯塔工厂及项目案例
第二次检索:如证据不足,尝试"灯塔工厂 行业 数字化改造"
打开文档:查看某个命中块的前后文
最终回答:按汽车、家电、钢铁等行业归纳,并引用知识库证据

Milvus 的原问题保留机制尤其重要。Agent 可以自由扩展查询,但系统仍会把用户原始问题送入检索并融合两路结果,避免扩展词删掉"中国""在哪里""哪些项目"等限制条件。

Agentic RAG 也需要边界控制:不能无限搜索,连续得到相同结果时应停止;回答必须以工具返回内容为依据;找不到证据时应明确说明,而不是用模型常识补齐。Agent 带来了更灵活的检索策略,也带来了更多延迟和不可预测性,因此日志和离线评测必不可少。

九、开发过程中最值得记录的坑

  1. 只看最终回答,无法判断问题在哪一层。 回答错误可能来自解析、切分、召回、排序或生成。为此曾增加向量、BM25、混合以及融合后 TopK 日志,评估完成后再通过 diagnostic_logging=false 关闭,避免生产日志爆量。

  2. 模型改写查询可能丢掉关键限定。 只使用扩展查询时,模型可能把长问题压缩得过度。Milvus 改为同时检索原问题和扩展查询,再使用 RRF 融合。

  3. 关键词越多不一定越准。 "案例""应用"等宽泛词可能把普通工业案例召回。扩展查询应该保留实体和约束,而不是无节制堆词。

  4. 不同阶段的分数不能直接比较。 向量相似度、BM25、WeightedRanker、RRF 和 Rerank 分数含义不同,不能拿同一个阈值生硬过滤。

  5. 修改默认配置不会自动改变旧知识库。 top_krecall_k 等参数在创建知识库时会保存到元数据。调整 chatbot.ini 主要影响新库,旧库需要单独更新或重新索引;配置缓存变更后还应重启相关进程。

  6. 重排序不是绝对正确。 Reranker 可能把细节块降权,所以要保留足够的召回候选,并通过测试集观察关键证据是否进入最终 TopK。

  7. 长问题会稀释 BM25 关键词。 当前原问题保留能避免语义丢失,但超长问题直接进入 BM25 仍可能带入大量无效词。后续可增加条件抽取、停用词处理和动态权重,但必须保留原问题作为兜底,不能只依赖模型摘要。

  8. 解析质量决定上限。 如果表格、标题或扫描文字在解析时已经丢失,再优秀的向量数据库也检索不到不存在的内容。

  9. 安全配置不能进入文章和仓库。 API Key 应通过环境变量或密钥管理注入,示例配置只能使用占位符,不能复制真实密钥。

十、当前方案总结与后续方向

这套知识库最终形成了两种层级清晰的方案:

  • Chroma:本地持久化、部署简单、纯向量召回,适合默认知识库和中小规模应用。
  • Milvus:远程独立服务、向量与 BM25 混合召回,再融合原问题与扩展查询,适合对召回和扩展能力要求更高的场景。

两条链路共同使用 Token 级结构化切分、BGE-M3 Embedding、候选集重排序、本地原文保存和 Agent 工具调用。真正带来效果提升的并不是某一个组件,而是整个链路的组合:

text 复制代码
高质量解析
  + 合理切分
  + Dense/BM25 多路召回
  + 原问题与扩展查询融合
  + Rerank 精排
  + Agent 多轮补充检索
  + 可复现的评测与日志

后续最重要的工作不是继续盲目叠加模型,而是建立稳定的评测集,按问题类型统计效果;对长查询做结构化条件抽取;为标题、文件名、章节等元数据增加可解释的加权;同时记录检索版本、参数、耗时和最终答案。只有能够持续测量,RAG 的"感觉更准"才能逐渐变成可以验证、可以回归的工程指标。

RAG 的本质并不是让模型知道更多,而是让系统在正确的时间找到正确的证据,并让模型忠实地使用这些证据。向量数据库负责"相似",BM25 负责"命中",Reranker 负责"排序",Agent 负责"决定下一步"。把这些环节组合好,知识库才会从一个能演示的功能,变成真正可靠的问答系统。

每文一语

当迷茫之时;需要沉下心;不要盲目的前进;人世间美好的事物多的数不胜数;

相关推荐
weixin_422329313 小时前
企业级 RAG 系统实战详解
ai·rag
海棠AI实验室3 小时前
AI 时代文献综述:从检索到成稿的 RAG 五步法
windows·算法·自动化·llm·rag
啾啾Fun3 小时前
【向量数据库】Milvus:为大规模、高性能而生的企业级向量数据库
数据库·milvus
56AI13 小时前
2026 企业级AI智能体开发平台推荐:聚焦底层安全与准确率的智能体平台
人工智能·安全·智能体
黑马师兄18 小时前
RAG混合检索深度解析:让AI真正找到你要的内容
java·人工智能·ai·agent·rag·ai-native
救救孩子把1 天前
02 Milvus-Milvus整体架构
架构·milvus
Xd聊架构1 天前
为什么 OpenClaw 和 Claude Code 都使用 Node.js
node.js·agent·智能体·claudecode·openclaw
小程故事多_801 天前
RAGFlow 分块策略全景与 Book 策略深度解析
java·开发语言·rag
救救孩子把1 天前
01 Milvus-向量数据库基础
数据库·milvus