news 2026/5/11 1:17:32

企业知识库RAG到底有多难:实战3:向量化与存储

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
企业知识库RAG到底有多难:实战3:向量化与存储

文章目录

  • (零)项目位置
  • (一)整体功能介绍
  • (二)程序入口与参数
  • (三)向量数据库初始化
  • (四)文档 node 构建流程
  • (五)为什么 debug 模式非常重要
  • (六)metadata 统计分析
  • (七)观察最大和最小 node
  • (八)最终索引构建
  • (九)整个代码的核心思路

文章索引

《企业知识库RAG到底有多难:理论知识部分》
《企业知识库RAG到底有多难:实战1:原始文档处理》
《企业知识库RAG到底有多难:实战2:数据内容分块》
《企业知识库RAG到底有多难:实战3:向量化与存储》

(零)项目位置

我的项目Repo:🔗 https://github.com/ShionWakanae/Llamarkdown。

现在讲的是./src/index_cli.py
这篇比较水,因为没什么技术含量。

(一)整体功能介绍

这段代码本质上是一个“文档索引构建工具”,作用是把 Markdown 文档读取进来,处理成适合向量检索的 node,然后生成 embedding,最后写入向量数据库,供后续 RAG 检索使用。

代码并没有和某一个具体的向量数据库强绑定,而是通过 LlamaIndex 的VectorStore抽象层来接入数据库。
也就是说,业务层其实只需要面对:VectorStoreIndex,至于底层到底是:

  • Chroma
  • Qdrant
  • Milvus
  • PGVector
  • Weaviate

其实都可以替换。

比如这里当前使用的是:ChromaVectorStore(chroma_collection=chroma_collection)然后交给:StorageContext.from_defaults(vector_store=vector_store)最后统一进入VectorStoreIndex(...)

整个索引流程就建立完成了。

这种结构有个很实际的好处,就是后续迁移数据库成本会低很多。开发阶段可以先使用本地 Chroma,甚至只用LlamaIndex的默认存储。部署阶段再切换 Milvus 或 Qdrant,而大部分业务代码其实都不用改。
所以这里的重点,其实不是“用了 Chroma”,而是“LlamaIndex 已经帮我们把向量数据库抽象统一了。”


(二)程序入口与参数

程序入口部分比较简单:
两个参数:文档目录,调试标识--debug

调试实际上非常照耀,因为真实项目里,大量 RAG 问题,并不是 embedding 模型的问题,也不是向量数据库的问题,而是最前面的数据处理阶段已经出了问题。

比如:

  • markdown 结构异常
  • node 长度失控
  • metadata 丢失
  • 标题层级错乱
  • 某些 section 没有被正确解析
  • 文档格式特殊导致 parser 崩溃

这些问题如果不提前发现,数据一旦入库,后面检索质量会非常差,而且问题会变得很难定位。所以这里 debug 模式的设计目标,其实是:“先人工确认 node 正常,再允许入库。”

是的是的,没有前面辛苦的人工,就无法带来后面的智能。


(三)向量数据库初始化

这里初始化的是 Chroma:chromadb.PersistentClient(path="./storage/chroma_db"),这是一个本地持久化数据库。也就是说,即使程序退出,向量数据仍然会保存在:./storage/chroma_db目录里。

接着:get_or_create_collection("docs")创建 collection,可以理解成“向量表”。
后面:metadata={"hnsw:space": "cosine"}表示使用 cosine 相似度。
随后通过:ChromaVectorStore(...)包装成 LlamaIndex 的统一 vector store 接口。
再交给:StorageContext.from_defaults(...),这样后续:VectorStoreIndex(...)就不需要知道底层数据库细节了。

这里正体现了 LlamaIndex 是一个“统一的数据接入层”。
后续如果你想切换:

  • Milvus
  • Qdrant
  • PGVector

通常只需要替换:vector_store = xxxVectorStore(...)这一层。
而 node 构建、embedding、索引流程,基本都不用改。


(四)文档 node 构建流程

真正的数据处理部分在这里:builder = IndexBuilder(),然后final_nodes = builder.build_nodes(doc_path, debug_mode)会把 markdown 文档转换成 node(上篇文章的内容)。

node 可以理解成:“最终进入向量数据库的最小检索单元”,每个 node 通常包含:

  • text
  • metadata

metadata 里可能包含:

  • 标题路径
  • topic
  • block_type
  • 是否包含 SQL
  • 是否包含 API
  • 是否包含错误码

后续检索时,其实大量能力都依赖 metadata。比如:

  • 过滤某类文档
  • 限制某种 block_type
  • 优先召回 API 文档
  • 按 topic 检索

所以 metadata 的质量其实非常重要。

这里还有一个小处理:

meta["merged_headers"] = " > ".join(meta["merged_headers"])

因为有些数据库不喜欢直接存 list 类型 metadata(会报错)。
所以这里会提前转成:一级标题 > 二级标题 > 三级标题这样的字符串结构。


(五)为什么 debug 模式非常重要

因为我发现数据问题会特别多,就算是自己精心编写的文档,到了RAG系统里,也会有结构不合理的问题。
再说我一直是做运营商软件的,怀疑一切是我的习惯。就算是正式文档写了,我也会怀疑它不生效。同时用友商文档和中国移动标准帮助工程上的同事回怼友商,也是工作的一部分。

呃,总之这部分其实是我自己现在经常用到的,因为你根本不知道:

  • 最终生成了多少 node
  • node 长度是否合理
  • metadata 是否完整
  • 有没有大量空 chunk
  • 有没有超长 chunk
  • markdown 是否解析失败

所以这里我专门写了:Show_debug_info_and_exit(final_nodes)在 debug 模式下只分析 node。
不索引,不入库。最后打印日志后直接中断程序。

这样做的目的,就是把问题尽量提前暴露。
最近才调试发现一个问题,node合并跨文档了,离谱……
如果这些 node 一旦进入向量数据库,会出现神奇的bug,再去定位问题会非常麻烦。


(六)metadata 统计分析

debug 模式下,首先会执行:print_metadata_stats(final_nodes)这里会统计 metadata 信息。例如:

  • topic
  • block_type
  • has_sql
  • has_api
  • has_error_code

比如:stats["has_sql"]["true"] += 1

最后会打印:

  • 总 node 数量
  • 每种 metadata 的数量
  • 占比百分比

这个步骤其实也容易发现问题(如果你足够了解你的文档)。

例如:“为什么 SQL 文档数量为 0?”
或者:“为什么 API 类型异常少?”

这通常意味着:

  • parser 有问题
  • metadata enrich 失败
  • markdown 结构异常
  • 某类 block 没识别出来

这些问题如果提前发现,修复成本很低。
但如果已经完成 embedding 并入库,再去查问题会困难很多。


(七)观察最大和最小 node

另一个我经常使用的调试方式,是直接观察最长和最短 node。
代码会遍历:for i, node in enumerate(final_nodes):然后记录:

  • 最大 node
  • 最小 node

最后输出:print("[min node]", final_nodes[node_min_index])
以及:print("[max node]", final_nodes[node_max_index])

这个步骤其实非常有效。
因为很多格式问题,一眼就能看出来。

例如:最短 node 如果只有:"-"或者:"```"
通常说明 markdown 被错误切碎了。
而最长 node 如果突然达到几万字符,通常意味着:

  • 标题层级失效
  • section 没切开
  • parser 出错
  • merge 逻辑异常

这些问题会直接影响 embedding 质量。
因为向量模型并不擅长处理:

  • 极短垃圾文本
  • 超长混乱文本

所以我现在基本已经形成习惯:先 debug 看 node,确认:

  • node 数量合理
  • node 长度正常
  • metadata 正确
  • 文档结构正常

之后才允许真正入库。


(八)最终索引构建

最后才是真正建立索引:VectorStoreIndex(...)
这里会传入:

  • final_nodes
  • storage_context
  • embed_model

也就是说:node -> embedding -> vector store 整个流程由 LlamaIndex 自动完成。
这里:show_progress=True我一般都会打开。
因为真实项目里,文档量可能非常大。不开进度条时,很容易误以为程序卡死。
并且这里其实是默认用的CUDA,如果没有N卡,暂时只能改代码换成CPU,但是会慢10多倍……


(九)整个代码的核心思路

这份代码其实没有特别复杂的算法。
重点更多在于:

  • 使用 LlamaIndex 解耦向量数据库
  • 在正式入库前先观察数据质量
  • 提前发现 markdown 与 metadata 问题
  • 尽量避免低质量 node 进入向量数据库

因为实际 RAG 项目里,很多召回质量问题,本质上都来自:“数据本身已经坏了”。
而 debug 阶段其实是最容易发现这些问题的时候。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/11 1:10:42

PostgreSQL:企业级全能开源数据库

PostgreSQL(常简称 Postgres 或 PG)是一款免费、开源、功能极其强大的对象 - 关系型数据库管理系统(ORDBMS)。 一、基本定位与历史本质:它是数据库,用于安全存储、高效管理和快速查询数据。出身&#xff1a…

作者头像 李华
网站建设 2026/5/11 1:09:51

面阵圆阵二维DOA估计MUSIC算法与FPGA实现【附代码】

✨ 本团队擅长数据搜集与处理、建模仿真、程序设计、仿真代码、EI、SCI写作与指导,毕业论文、期刊论文经验交流。 ✅ 专业定制毕设、代码 ✅如需沟通交流,点击《获取方式》 (1)张量协方差分解与虚拟孔径扩展: 针对44均…

作者头像 李华
网站建设 2026/5/11 1:06:44

基于对 goweb3 框架代码的深入分析,我为您提供以下评价

基于对 goweb3 框架代码的深入分析,我为您提供以下评价:一、框架架构概览goweb3 是一个基于 Gin go-micro GORM 构建的企业级 Go 微服务框架,采用领域驱动设计(DDD)和测试驱动开发(TDD)理念。…

作者头像 李华
网站建设 2026/5/11 1:05:47

Cursor AI(AI代码编辑器)

链接:https://pan.quark.cn/s/1084eb1d7d6fCursor AI是领先的Al代码编辑器,通过丰富的功能和简单的操作让用户更快更便捷地构建软件,提供更快的自动代码生成,提供预测功能,基于当前的代码预测并提供更多的建议&#xf…

作者头像 李华
网站建设 2026/5/11 1:05:44

Claude Code真的很神?

很多人习惯了VsCode、Trae这样的图形化AI编程界面,而Anthropic发布的Claude Code是在命令行上写代码,看似简陋,却强大的很。扔掉复杂的软件外壳,只有极简的命令行,直接在终端让AI接管编程,变成终端原生应用…

作者头像 李华