RAG结构化数据导入实战:CSV/Excel与LlamaHub连库全攻略

发布时间:2026/10/31 8:47:21
RAG结构化数据导入实战:CSV/Excel与LlamaHub连库全攻略

RAG 这种东西,做 demo 的时候都靠 PDF 撑面子,真正放到公司内部用,你就会发现一半以上的数据其实都在表格和数据库里。CSV 导出、Excel 报表、业务库里的订单表,这些才是知识库要接的主战场。这篇是《RAG 数据导入与解析全攻略》的第三篇,专门聊表格与数据库导入:从 CSV、Excel 这两种最常见的文件格式说起,再带你用 LlamaIndex 生态里的 LlamaHub 读取器,直接连上 MySQL、PostgreSQL、SQLite 这类正经数据库。你可以把它当成一份可以照着抄的实操笔记,适合 RAG 系统已经跑通、想把结构化和半结构化数据源也接进来的朋友。

1. 表格数据在RAG里的真实定位:为什么"喂给LLM"和"查数据库"不是一回事

1.1 表格数据与文档数据的本质区别

前两篇我们一直在处理 PDF 和网页,这类数据的特点是信息连续、叙述性强,切成平均长度差不多的分片就能塞进向量库,检索的时候按语义相似度捞回几段就是合理的答案来源。表格和数据库则完全不是这个脾气,它们是离散的、带业务语义的、严格按行列组织的结构。一张订单表如果也按 500 个字符去随机切,很容易把"订单号、客户、金额"这三列切成两个残缺的块,检索时拿到的可能是半截话,LLM 靠这点上下文根本答不出有意义的结论。

所以表格导入 RAG 的核心决策不在用哪个 loader,而在于你想让 LLM 用哪种方式访问这些表格。是把它转成自然语言描述后靠向量检索回忆,还是把表格本身作为"可查询的库"让 LLM 生成 SQL 去查?这两条路的后续配置完全不同。我的经验是大多数业务场景不是二选一,而是两条路都要走,但入口和索引结构会不一样。理解不了这一点,后面所有的导入都是白做。

1.2 三种数据导入路径的选择逻辑

表格数据进入 RAG,我把它分成三条路径。

第一条是纯文件导入路径。数据是一次性的、更新不频繁、量级不大的 CSV 或 Excel,比如某个调研报告附表、业务分析用的数据快照。这种最简单,用 LlamaHub 的 CSVReader、PandasCSVReader、ExcelReader 读进来直接构建索引就行。

第二条是连库导入路径。数据活在业务系统里,比如订单库、用户表、库存表,每天都会更新,你不能每天早上手动导一份 CSV,导完还得告诉知识库"文件换了"。这种就应该用 LlamaHub 的 DatabaseReader 之类的组件,直接连接数据库执行查询,把结果按批次读出来进索引。

第三条是交互查询路径。数据量太大、不能也不该全量塞进向量库,或者用户问的是精确汇总问题("上个月华东区销售额是多少"),此时应该把数据库当作一个外部工具,RAG 系统负责从自然语言问题里识别出查询意图,生成 SQL 到库里执行,再把结果回填给 LLM 做最终回答。严格说这条路不是"导入",但它跟连库实战强相关,我放到后面一起讲。

2. CSV 导入实战:CSVReader、PandasCSVReader 与三个最容易翻车的细节

2.1 文件编码、分隔符与 BOM

CSV 看起来是全世界最老实的格式,但在中文项目里它是最容易出乱码的。最常见的情况是运维同学从某管理后台导出的文件是 GBK/GB18030 编码,你用 UTF-8 去读,一读就是满屏的乱码。更隐蔽的是 UTF-8 带 BOM 的文件,BOM 是文件开头那三个不可见字节,读取器如果不处理,第一个列名就会被拼成'\ufeff订单编号'。

我处理 CSV 的第一道工序永远是先做文件体检,而不是直接把文件丢给读取器。体检三步走:

  1. 用file命令或者文本编辑器查看文件真实编码;
  2. 确认分隔符是不是逗号,有些系统导出的是分号或制表符(尤其是欧洲本地化软件特别喜欢用分号);
  3. 看第一行有没有 BOM,有的话要么转码去掉,要么在读取时处理。

LlamaHub 里的 PandasCSVReader 支持传pandas_kwargs,可以在调用pd.read_csv时指定encoding、sep这些参数,这是应对编码和分隔符最直接的入口。但我的建议是更保险的做法:先用一个小脚本统一转成 UTF-8 无 BOM 的标准文件,再进导入流程。转完之后 CSV 就不再是风险源,后续处理才能安心。

2.2 CSVReader 还是 PandasCSVReader:两种读取器的取舍

LlamaHub 上 CSV 相关读取器有好几个,最常用的是CSVReader和PandasCSVReader,很多人分不清该用哪个。这两者的差异我用一整段说清楚。

CSVReader的逻辑很简单:把整个 CSV 文件读进来,然后按行拆成 Document。它适合列不多、每行是一个完整业务实体的文件,比如商品目录、人员名册、事件列表。每个 Document 的文本内容是这一行的字段拼接,LLM 拿到一行就知道一个实体的完整信息。

PandasCSVReader更灵活,它内部调用 pandas 的read_csv,意味着你可以做列选择、类型转换、空值填充,还能通过参数控制输出方式。如果 DataFrame 有多列几十列,全部拼成文本会又长又杂,PandasCSVReader 可以让你先选列再导入。对于数据清洗派的人来说,它明显更顺手。

我自己的选择标准是:

判断维度CSVReaderPandasCSVReader
数据规模中小型文件更合适支持大文件,内存可控
列数列少、行语义完整列多、需要筛选清洗
自定义程度低,开箱即用高,可传 pandas 参数
典型场景简单名录、配置表报表、业务数据快照

2.3 大文件、超长数字与空值怎么处理

CSV 导入最容易翻车的不是解析,而是数据本身的两个特性:超长数字和空值。

超长数字是经典陷阱。订单号、身份证、银行卡这类数据在 CSV 里本是纯文本,但如果你用 pandas 默认读法,超过一定长度的数字会被自动变成科学计数法,比如123456789012345678变成1.234568e+17,精度直接丢失。解决方案是在read_csv时给这些列指定dtype=str,让它们保持字符串原样。这个坑在 CSV 文件来自 Excel 导出时尤其常见,因为 Excel 自己对长数字就有 15 位精度的截断问题,源头文件可能已经损坏了,你得跟业务方确认。

空值也很麻烦。CSV 里留空、写NA、写NULL的都有,Pandas 默认会把空值转成NaN,再转成文本时就成了字符串"nan",LLM 有可能会一本正经地把nan当成真实内容。我在导入前一般会做一次全局处理,把空值统一替换成空字符串或者业务上更明确的占位词,比如"未填写"。

大文件的处理另说。几十万行的 CSV 一次读入内存会产生几千个 Document,构建索引时既慢又吃资源。比较实用的做法是按时间范围或主键范围分批读取,每批 1 万到 2 万行,分多次调用 loader 追加到同一套索引里。LlamaIndex 对增量写入的支持已经比较成熟,只要给每个 Document 设置稳定的 doc_id,重复导入不会造成大量重复。

3. Excel 导入实战:公式缓存、多工作表和"先清洗再导入"

3.1 ExcelReader 读取的是公式还是结果

Excel 比 CSV 强的地方是能存格式、能写公式,但这也是它进 RAG 时最大的坑。一个单元格里写的是=A1+B1,这个单元格的"值"到底是多少,取决于你用什么工具去读。Excel 文件本身保存的公式和上次计算后的缓存值都存在文件里,不同读取库对这两者的处理方式不一样。

我一贯的提醒是:不要默认读取器会返回计算后的结果,先用一个小片段验证。LlamaIndex 的ExcelReader底层依赖处理 Excel 的库来做解析,不同版本对公式单元格的处理行为有差异,有些场景下你拿到的可能是公式字符串本身,而不是数值结果。这会导致 LLM 看到满屏=SUM(...)、=VLOOKUP(...)之类的符号,回答得驴唇不对马嘴。

一个稳妥的生产方案是:在导入之前,用 Excel 或 LibreOffice 把公式列重算并另存为值,或者直接用 pandas 把 Excel 读成 DataFrame,确认列的值类型符合预期后再传给 RAG 读取器。你可以在 pandas 读取后把 DataFrame 转成临时 CSV 或直接喂给下游。实际业务中,"Excel 里的公式"往往承载着重要的计算逻辑,但这些逻辑 LLM 并不会真的执行,所以导入 RAG 之前,必须把它变成"计算后的结果值"。

3.2 多工作表、合并单元格和日期序列的应对

多工作表是第二个常见问题。一个报表工作簿里经常有"汇总表"、"明细表"、"附录说明"三个 sheet,内容风格完全不同,如果全部一股脑读进来再切块,检索时容易串味。ExcelReader一般支持通过参数指定工作表,但我更习惯在业务层面拆分:每个 sheet 单独处理,甚至单独建索引或单独打标签。比如"明细表"适合走数据库查询路径,"附录说明"适合走普通文本路径,把它们混在一个索引里对检索质量没什么好处。

合并单元格导致的解析异常也很值得讲。中国人做 Excel 报表喜欢合并单元格,尤其是表头区。"华东区"三个字合并了八行,数据里只有第一行有值,其余七行是空的。读取器如果没做前向填充,你拿到的数据就是一个个空洞,转成文本后 LLM 看到一堆孤立的数字不知道属于哪个区。解决手法是在 pandas 里用ffill做前向填充,把合并单元格的值填到下面所有行,然后再导入。

日期序列是 Excel 的老毛病。Excel 内部把日期存成数字,1900 年 1 月 1 日是 1,之后每一个自然日加 1。如果读取器没有正确识别列格式,你可能拿到一串45292之类的数字,而不是"2024-01-01"。pandas 读取时通常能自动识别日期,但如果源头文件某些单元格是文本格式某些是日期格式,混在一起就会被拆成两列或者变成字符串。我的做法是在清洗阶段统一把日期列用pd.to_datetime规范成 ISO 格式字符串,再进 RAG。

3.3 推荐的 Excel 导入姿势

直接调ExcelReader能跑通 demo,但生产环境我不推荐这么做。Excel 的格式自由度太高,任何看似合法的文件都可能夹带私货:有批注的单元格、隐藏列、条件格式、数据验证下拉列表、甚至嵌入图表。这些元素对向量检索没有帮助,只会把文本弄脏。

我的标准流程是三层加工:

第一层,pandas 读入原始 Excel,做列名规范化、空值填充、日期格式化、公式重算检查。第二层,根据业务字段把表格转成适合 LLM 阅读的文本结构,比如逐行转成 JSON 或者键值对。第三层,再交给 LlamaHub 读取器或直接用于构建节点。这个过程听起来绕了一圈,但实际返工率最低。Excel 本身从来不是问题所在,问题在于"未经清洗的 Excel"直接进入 RAG,检索时各种脏数据会像幽灵一样冒出来。

4. LlamaHub 连库实战:从 MySQL/PostgreSQL 到 SQLite 的读取器配置

4.1 为什么我推荐直接连库而不是导成 Excel

每天凌晨用脚本从业务库导出 CSV,再定时导入知识库,这种方案不是不能用,但会引入两代同步延迟,而且文件路径、权限、命名规则任何一个环节出错,知识库就静默地停留在昨天的数据。我做过几次之后果断放弃了,改让 RAG 系统直接连业务库。直接连库有三个明显的好处:实时性有保障、不需要管中间文件、可以用 SQL 做更精确的过滤。

但同时也要想清楚代价。连库意味着你要把数据库连接串、账号凭据交给知识库进程,这是安全上的一个面。所以我的建议是:给 RAG 系统建一个专门的最小权限账号,只读,只允许访问指定的表或视图,最好连库端口也只对内网开放。知识库不是什么都要,它只需要读取它需要的字段。

4.2 创建只读账号和最小权限库

以 PostgreSQL 为例,我会先建一个专用账号:

CREATE USER rag_reader WITH PASSWORD 'change_this_to_a_strong_password'; GRANT CONNECT ON DATABASE sales_db TO rag_reader; GRANT USAGE ON SCHEMA public TO rag_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO rag_reader; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO rag_reader;

MySQL 对应的是:

CREATE USER 'rag_reader'@'%' IDENTIFIED BY 'change_this_to_a_strong_password'; GRANT SELECT ON sales_db.* TO 'rag_reader'@'%';

这段操作看起来很基础,但很多人生产上直接拿着业务账号的连接串让 RAG 跑,只因为"懒得再配一个账号"。我强烈不建议这样。RAG 进程要处理的是大量喂给 LLM 的数据,一旦内部有数据泄露或日志打全量数据,影响面是不可控的。最小权限不只保护数据库,也保护你自己。

4.3 DatabaseReader 的配置细节

LlamaHub 里的DatabaseReader就是一个封装好的连接器,它会在内部构建 SQLAlchemy 引擎,执行你传入的 SQL 查询,把每一行结果转成 Document。以 LlamaIndex 0.10.x 的模块化风格为例,你会先安装对应扩展包,再实例化读取器:

from llama_index.readers.database import DatabaseReader reader = DatabaseReader( scheme="postgresql", # 也支持 mysql、sqlite 等 host="127.0.0.1", port="5432", dbname="sales_db", user="rag_reader", password="change_this_to_a_strong_password", ) docs = reader.load_data( query="SELECT order_id, customer_id, amount, created_at FROM orders WHERE created_at >= '2024-01-01' ORDER BY created_at LIMIT 10000" )

SQLite 更简单,因为它是单个文件库,直接指向文件路径就行。这里的核心思路是:加载器只负责执行查询、把结果转成节点,至于查询怎么写、过滤哪些行、用什么排序,都是你来决定。所以连库导入的最终质量,其实由 SQL 写得好不好决定。

我一般会把查询封装成带参数的模板,方便按批次拉取。比如用created_at做游标,每次取一段区间,避免一次把几百万行打进内存。

4.4 全量导入与增量更新的平衡

连库导入最需要设计的是增量更新策略。一种简单可靠的方案是:使用一个时间戳字段作为增量标记,比如updated_at,每次同步只拉取最近 24 小时有变化的数据,导入时用业务主键作为 Document 的 doc_id,让索引层面的 upsert 机制去覆盖旧内容。

我之前在团队里实践下来最顺手的方案是两层同步:

  • 每天凌晨跑一次增量同步,处理当天变更的数据;
  • 每周跑一次全量重同步,用业务主键做幂等覆盖,把那些没更新但之前漏掉的脏数据修回来。

这么做的原因是增量同步偶尔会因为你忘了某条记录、或字段里时间戳为 NULL 而漏数据,全量重跑相当于定期兜底。每次同步都记录自己的完成时间和同步行数,一旦数据对不上就能快速定位是哪一批没执行。

5. 表格数据进入索引后的检索调优

5.1 表格文本化的几种表达方式

表格本身是结构化数据,但向量索引本质上是文本索引,所以我们必须决定:表格的每一行到底要转成什么文本形式让 embedding 模型去编码。不同的转法,检索效果天差地别。

我尝试过三种:

第一种是 Markdown 表格。整张表转成一个 Markdown 表格,保留表头。适合表不大、行数不多、需要整体呈现的场景。缺点是行数一多,分块很容易切断表格结构。

第二种是逐行 JSON。每一行变成{"订单号": "A001", "客户": "张三", "金额": 12345}这样的 JSON 对象。embedding 模型对键值对形式的理解通常不错,而且按行拆分天然解决分块切断的问题。缺点是行数多的时候节点数量涨得很快。

第三种是自然语言描述。把每一行改写成一句话:"订单 A001 的客户是张三,金额为 12345 元"。这看似多此一举,实际上对 embedding 检索最友好,因为用户的提问习惯是自然语言,而描述句的语义空间和用户问题最接近。缺点是改写有成本,大表不现实。

我现在对核心业务表的做法是:清洗完成后转成逐行 JSON,同时额外生成一列"语义摘要",把关键的客户名、产品名、事件用自然语言拼一句放在前面,检索时这个摘要句往往命中率最高。

5.2 元数据的设计与混合过滤

向量检索不是纯靠语义打天下,元数据过滤能让结果质量上升一个层级。我在导入每个表格节点时,一定会挂上几类元数据:来源表名、业务时间戳、主键值、数据级别(比如"明细"还是"汇总")。有了这些元数据,用户提问里如果带时间或范围条件,就可以在检索阶段先做结构化过滤,再进向量相似度匹配。

举个实际例子,用户问"今年一季度华东区的退货原因主要有哪些",如果你只在向量库里做纯相似度搜索,出来的很可能是一些语义相近但不完全符合时间条件的记录。但如果索引里每个节点都带quarter=2024Q1和region=华东这两个元数据,就能先用过滤把候选集缩到几十条,再接向量排序。 LlamaIndex 的 retrievers 支持这类过滤条件,不用额外写复杂逻辑。

这套做法我建议从导入第一天就设计好,而不是等项目跑起来再补。一开始就把元数据字段规划成一张表,后面所有数据源都按这个规范对齐,检索调优会省很多力气。

5.3 向量检索与文本到 SQL 的互补

必须诚实地说,向量检索不是万能的。对于"上个月退货率是多少"这类需要精确计算的聚合问题,你就算把几十万条数据全部塞进向量库,LLM 也没法通过语义相似度算出精确的百分比。这种情况正确的解法是走文本到 SQL 的路线:让 LLM 理解用户意图,生成 SQL 去数据库查询,拿到精确结果后再组织成自然语言回答。

所以在连库场景里,我现在做的是双通道设计。一个通道是离线索引,把表格的主要字段转成向量入库,用于回答"某类情况有没有发生过、大致有哪些"这类开放问题。另一个通道是实时查询,让 LLM 根据问题生成聚合 SQL 直接打库,用于回答精确数值问题。两个通道都有各自的召回逻辑,系统根据问题类型做路由。没有一种单一方案能同时搞定开放语义和精确计算,越早接受这个现实,你的 RAG 越不容易在真实用户面前翻车。

6. 我踩过的坑和目前最顺手的流程

6.1 几个实实在在的坑

第一个坑是 CSV 的 BOM 导致首列列名多出一个不可见字符,当时线上知识库检索"订单号"总是查不到东西,排查很久才发现是\ufeff混进了列名。这之后我把"先统一转成 UTF-8 无 BOM"写成了数据导入流水线的铁律。

第二个坑是 Excel 公式。同事给我的销售明细表里有一列是"毛利",单元格全是公式,RAG 读进来之后 LLM 回答"毛利率"相关问题时,检索到的内容全是=IF(...)公式本身,看起来特别外行。后来我们约定:所有进 RAG 的 Excel 必须先另存为值或用 pandas 重算过。

第三个坑是数据库表中同名字段在不同时间段的含义不一致。某张业务表早期"status"是 0/1,后来系统升级变成 wait/done/closed,向量库里两种值并存,用户问"活跃用户"时系统经常把旧代码当成新值。这种数据语义变更,不靠代码解决,必须靠定期人工审查导入日志和抽样。

第四个坑是权限。有次 RAG 服务真的误执行了 DELETE,虽然最后恢复了备份,但从此所有接库操作我都强制用只读账号。这个教训太贵了。

6.2 我现在的完整操作流程

写了这么多,把目前最顺手的一套流程收个尾。

数据到达后先分流:如果来源是 CSV 或 Excel,先进 pandas 清洗模块,做编码转换、列名规范化、日期格式化、空值填充,再转成 JSON 行文本。如果来源是数据库,先确认拉数 SQL 的范围和分页方案,再用 DatabaseReader 拉取,同样转成带元数据的节点。

所有节点统一挂上来源、表名、更新时间、主键。然后走 LlamaIndex 构建索引,对每一批导入用业务主键做 doc_id,保证重复导入时是覆盖而非堆叠。最后配置双层检索:元数据过滤加向量相似度,再叠加针对聚合类问题的文本到 SQL 通道。

这套流程我现在在多个项目里复用,稳定性和可维护性都明显好于早期"拿到数据就直接丢给读取器"的做法。数据接入这件事,前期多花 10 分钟做清洗设计,后面能省下几十个小时的调试时间。希望这篇表格与数据库导入的实战笔记,能把你在同类问题上要走的路提前铺平。