
做 RAG 知识库做到第三个星期我发现最让人头大的不是 PDF也不是网页爬虫而是那些规规矩矩躺在 CSV 和 Excel 里的表格数据。PDF 好歹是文本流切片后检索还能看表格数据一旦切碎行列关系全乱词也搜不准向量也嵌不准。后来我把 LlamaHub 上的表格加载器和数据库读取器挨个实测了一遍才把这条链路真正打通。这篇就聊聊 CSV、Excel 和数据库直连导入的实战经验适合已经在做 RAG 项目、想把业务表格和线上数据库接进知识库的团队也适合刚摸到 LlamaIndex 边、想少走弯路的新手。1. 表格数据进 RAG 前先想清楚这三件事1.1 表格不是“文本的另一种形式”很多人在导入阶段犯的第一个错是拿 PDF 的那套思路处理表格读成字符串、按固定 chunk_size 切片、丢给 embedding 模型。结果就是检索出来的片段经常是半行数据加一截别的内容LLM 看到的是“8849.5 华东大区 2025-03-01”完全不知道这个数字是销售额还是成本。表格数据的语义藏在二维结构里列名定义了维度行数据是具体事实。把表格当成纯文本切片等于把一张发票从中间撕开金额和抬头各在一半。这也是为什么表格类 RAG 项目失败率特别高——问题往往不在检索算法而在导入时就把结构丢了。所以在动手写代码之前先要明确表格类数据导入 RAG核心任务不是“读取文件”而是“在保留行列语义的前提下把表格转换成可检索的文档单元”。转换得好不好直接决定后面每一步的效果。这个认知不建立起来后面调什么参数都白搭。1.2 决定检索效果的两个粒度问题表格数据进 RAG 时切片粒度通常有三种选择各有适用场景整表一个 Document适合几十行以内的小表比如国家列表、产品分类表。优点是上下文完整缺点是超过几百行后文本太长embedding 模型根本装不下检索精度也急剧下降。每行一个 Document适合宽表、流水表、订单表这类“一行就是一个完整事实”的数据。这是我在大多数项目里默认采用的方式。单元格级或者行列交叉切片几乎不推荐。除非你把表头信息一起带进去否则单个单元格没有任何语义。另外还要考虑列的方向。有些表是“宽表”上百列但行数少有些是“长表”列少但行数多。宽表更适合按行转成结构化描述文本长表则可以按“列分组”做成多个维度的文档单元。这个选型没有标准答案建议先取 50 行做一个手工抽查用原生的关键问题去测试召回效果再决定要不要全量导入。1.3 什么时候别硬塞表格进 RAGRAG 对表格数据并不是万能的。如果你要回答“上季度华东区销售额环比增长多少”这种聚合统计类问题或者“A 表和 B 表 join 之后的最大订单是哪条”这种关系查询靠向量检索把表内容召回再用 LLM 生成的方案效果会非常差。原因很简单向量检索擅长召回“相似文本”不擅长精确计算和跨行关联。这类场景我一般会明确告诉业务方表格要么转成 Text-to-SQL 的管道让模型负责写查询要么进入知识图谱让关系成为可遍历的结构。判断标准也很朴素——问的是“某一行/某几行的具体信息”可以走 RAG问的是“全表的规律、汇总、跨表关系”就别硬塞。把表格塞进向量库解决不了的问题换一个再好的 embedding 模型也解决不了。2. LlamaHub 上两种表格加载器CSV 与 Excel 实战2.1 CSVReader小表最省事大表别指望LlamaHub 的llama-index-readers-file里提供了一个很直接的 CSVReader安装和使用都非常简单pip install llama-index-readers-filefrom pathlib import Path from llama_index.readers.file import CSVReader loader CSVReader() documents loader.load_data(filePath(sales.csv)) for doc in documents[:2]: print(doc.text) print(doc.metadata)默认行为是把整个 CSV 文件当作一个 Document 读进去text 字段就是 CSV 的原文内容metadata 里带上文件名等信息。注意不同版本里load_data参数略有差异老版本传字符串路径新版本传Path遇到报错先看文档。这个加载器适合什么场景小表、配置表、固定字典表比如车型对照表、仓库编码表。一个文件一条文档检索时整个上下文都是完整的没有切片破坏问题。但如果你的 CSV 动辄几百 MB有几十万行那 CSVReader 默认行为几乎是灾难——一个 Document 塞了全表内容embedding 截断得厉害检索也根本定位不到具体行。这种情况下不要硬用 CSVReader我会直接用下一节讲的 Pandas 手工方案。2.2 ExcelReader多 Sheet、公式缓存与老格式Excel 文件比 CSV 麻烦的地方在于一个文件可能有多个 Sheet单元格里可能有公式还有合并单元格、日期格式、编码问题。LlamaHub 里的 ExcelReader 同样在llama-index-readers-file包里但底层依赖openpyxl需要单独安装pip install openpyxlfrom pathlib import Path from llama_index.readers.file import ExcelReader loader ExcelReader() documents loader.load_data(filePath(inventory.xlsx))不同版本的 ExcelReader 对多个 Sheet 的处理不大一样有的会把每个 Sheet 都读出来有的允许你通过sheet_name参数指定只读某个 Sheet。我的建议是导入前先花一分钟打印documents[0].text看看实际读进来的是什么格式再决定要不要过滤。实践经验里最容易踩的坑有三个公式单元格默认读的是缓存值如果这个 Excel 是程序批量生成的公式从未被 Excel 计算过读出来就是None。合并单元格只有左上角第一个值其余位置是 NaN直接导入会丢数据。老版本的.xls文件需要额外装xlrd而且新版本xlrd只支持.xls不支持.xlsx真遇到老文件先转成.xlsx再处理更省事。2.3 PandasReader 和自己动手把每一行做成标准文本LlamaHub 里还有个 PandasReader底层就是 pandas 读取后按行转文本。但它的问题在于不同版本参数差异较大调试成本高而且默认输出的文本格式不一定是 LLM 友好的结构。我的习惯是直接用 pandas 读文件手动构造 Document一行一个文档import pandas as pd from llama_index.core import Document df pd.read_csv(sales.csv, dtypestr) documents [] for idx, row in df.iterrows(): parts [f{col}{val} for col, val in row.items() if pd.notna(val)] text ; .join(parts) doc Document( texttext, metadata{ source: sales.csv, row_index: idx, order_id: row.get(order_id, ), }, ) documents.append(doc)这里有两个非常关键的决定。第一dtypestr强制把每一列都读成字符串避免几十位的订单号被转成科学计数法也避免 NaN 以浮点数的形式输出。第二把order_id塞进 metadata为后面问题排查、检索回溯留了后路。你永远不知道线上用户会问出什么问题但你知道每个文档是从哪一行来的这就够了。3. LlamaHub 连库实战DatabaseReader 把 SQL 结果召回3.1 数据库直连前要装什么很多表格数据不在文件里而在线上数据库里。LlamaHub 的llama-index-readers-database提供了 DatabaseReader专门解决从数据库直接导入知识库的问题。安装命令pip install llama-index-readers-database sqlalchemy psycopg2-binarysqlalchemy负责统一各种数据库的连接方式psycopg2-binary是 PostgreSQL 的驱动。如果你用的是 MySQL把最后一项换成pymysql。连接串的格式是标准的 SQLAlchemy URLfrom llama_index.readers.database import DatabaseReader reader DatabaseReader( sql_databasepostgresql://user:passwordlocalhost:5432/warehouse )我第一次用时还想着是不是要装一个独立的数据库中间件实际不需要。DatabaseReader 就是拿着连接串执行你给的 SQL把结果集转成 Document。连接方式走的是成熟生态只要你本地能连上数据库这套链路就能跑通。3.2 用一条查询生成知识库文档连上之后核心方法就是一个load_data(query...)documents reader.load_data( query SELECT order_id, product_name, quantity, amount FROM orders WHERE updated_at 2025-01-01 ) for doc in documents[:3]: print(doc.text) print(doc.metadata)查询结果的每一行会转成一条 Documenttext 大致是order_id123; product_name某种商品; quantity2; amount5999.0这种 key-value 结构metadata 里也会带上一部分列值。这个设计很合理每个文档就是一个行级事实检索命中的时候模型拿到的是一整行完整数据而不是表格里的半截文本。实操上要注意两点。第一尽量不要SELECT *尤其表里有大文本字段、JSON 字段或日志字段时向量化全是噪音。先搞清楚业务问题会落在哪些列再决定查询哪些列。第二一次查询的行数要控制几万行一次性塞进内存建索引机器会很吃力后面会讲分批处理。3.3 增量导入与定时同步别把整库塞进向量库文件和数据库最大的区别是数据库会变。删一条数据、改一个字段、新增一行如果每次都是全量重导几百 GB 的库根本扛不住。增量导入的核心思路是“记录上次导到哪了”。如果表里有自增主键比如order_id可以记录上次最大的order_id下次只导order_id 上次值的数据。如果表里有updated_at时间戳逻辑类似。如果表结构比较干净这两种方式基本够用。代码层面大概是last_id 100000 # 从导入记录表里读出来 new_docs reader.load_data( queryfSELECT ... FROM orders WHERE order_id {last_id} ) # 建立索引索引然后更新 last_id new_id更稳妥一点可以在源库里建一张导入记录表专门记每个表的last_id和last_updated_at。注意如果删数据也要同步到知识库光做增量还不够需要额外记录删除事件或者在业务低峰期做一次全量对账。这个成本不低但如果做的是面向生产环境的 RAG这一步省不掉。3.4 直接连库还是先导出 CSV 再导这不是一个非此即彼的问题。我的决策逻辑很简单数据量不大、更新频率低、字段相对静态——先导出 CSV 再走文件导入链路。好处是链路简单出问题好排查还能手动编辑清洗。数据量大、更新频繁、字段多——用 DatabaseReader 直接连库。好处是实时性高不用维护中间文件同步流程。两种方案的对比我用多了之后心里基本有数直接连库的优势是“不落文件、按需取数”劣势是权限管理和查询压力要照顾到生产库别让知识库的建索引任务把线上数据库拖垮。我的建议是给 RAG 建一个只读账号只授予需要的表权限并且尽量在从库或副本上跑查询。4. 表格 RAG 的收益优化切片、模板、召回增强4.1 行级切片后的上下文重建如果你只是简单地把每行转成order_id123; amount5999这种文本LLM 能读懂但理解深度有限。更好的做法是在每个文档的 metadata 里挂一个表级摘要检索命中后让 LLM 知道这行的“坐标系”是什么table_summary 表sales_order_2025 含义2025年销售订单明细 列说明order_id订单号customer客户名amount金额元status订单状态 doc.metadata[table_summary] table_summary这样一来检索到某个订单行时模型不光看到孤立的数值还知道“金额单位是元”“状态字段有哪几种取值”。如果你的检索链路支持 metadata 拼接回 prompt强烈建议这么做。一次添加摘要的成本很低但对下游生成质量的提升非常明显。4.2 语义锚点为代码型字段补上自然语言表格数据里到处都是编码product_codeP001、region_id7、status3。用户不会用编码问问题他们问的是“红色跑鞋卖了多少”“华南区退货率多少”。如果导入时不做任何处理embedding 模型对P001这种符号完全不敏感召回失败几乎是必然的。我在实际项目里做过一个很土但是很有效的事导入前先把编码字段 join 一下映射表把P001变成P001 红色跑鞋把region_id7变成region_id7 华南大区。这个步骤我叫做“语义锚点注入”。它解决的是表格数据“符号化”的问题跟 embedding 模型选型无关属于数据预处理层面最值得投入的改进点。4.3 混合检索配合表格文档表格数据里的列名、编码、缩写很多都是符号型 token向量模型对这些并不友好。这也是为什么在做表格 RAG 时我强烈建议不要只用纯向量检索而是把 BM25 关键词召回或全文检索一起用起来。用户输入“order_id 100001 状态 3”关键词检索能直接命中order_id100001的文档向量检索反而可能因为文本太短而效果不佳。LlamaIndex 里可以直接把多种检索器组合成混合检索。就算不用复杂框架至少在建索引时保留一份可全文搜索的数据副本用来做召回兜底。表格数据不像长文本文档那样有丰富的语义密度混合检索的价值比在 PDF 场景还要大。4.4 表格到底该进向量索引还是知识图谱热词里总有人搜“rag 瓶颈”“知识库和结构知识库的区分”。我的判断标准是问题能通过“找一行、比对几行”完成就上向量 RAG问题需要“跨多行聚合、关系推演、多轮 join”向量 RAG 就明显不合适应该考虑知识图谱或 Text-to-SQL。表格数据的本质是结构化事实但并不是所有结构化事实都适合变成向量。从架构上看把表转成知识图谱适合那些实体之间关系密集、并且经常被问到关系类问题的场景比如“A 客户买过哪些品类”“这些品类来自哪些供应商”。这个决策最好在导入前做不要等向量检索效果不好再反悔因为两套管线的数据清洗逻辑完全不一样。5. 常见问题与排错清单5.1 CSV 数字列变成科学计数法或长 ID 丢精度CSV 里 18 位的订单号用 pandas 默认参数读进来会直接变成浮点数123456789012345678变成1.234568e17导入知识库就废了。这个坑我踩过不止一次。解决办法就是前面说的dtypestr或者在读取后对长整数字段单独处理。如果某些列确实需要参与数值计算也不要让 pandas 自动推断类型手动指定 dtype 才是安全的。5.2 Excel 公式读不出值 / 日期变成序列号 / 合并单元格公式问题前面提过openpyxl 默认读缓存值。日期列有可能读出45000这种序列号需要确认读取参数并按YYYY-MM-DD格式标准化。合并单元格在 pandas 里表现为只有左上角有值其余是 NaN可以用ffill()把值向下填充。但注意填充前要按业务逻辑排好序乱序填充会让数据串行。5.3 数据库时区、Decimal、JSON 字段报错数据库里常见的Decimal、datetime、JSONB字段直接拼字符串经常会出格式问题。我的习惯是在导出的 SQL 里就把类型转换做掉比如amount::float、updated_at::varchar、jsonb字段::text避免 Python 侧再去处理一堆复杂对象。如果不想改 SQL也可以在代码里写一个 to_text 函数统一把非字符串类型转成可读文本。这个问题不大但每次都在建索引时突然爆出来非常烦人。5.4 一行 Document 文本过长 / 一次性内存爆炸如果某张表的某一行有几十个字段且每个字段的值都很长行级 Document 会变得很大embedding 效率低检索精度也受影响。建议在导入前先看行宽字段个数超过 10 个就要考虑是否拆分“核心字段”和“扩展字段”。内存方面大批量导入务必分批处理比如用LIMIT 1000 OFFSET N或者上一节讲的增量 id 方式循环。一次性全量导入几百万行不仅慢还会让索引文件大得离谱。5.5 召回质量不佳时的排查顺序很多人在 RAG 效果不好时第一时间怀疑 embedding 模型我却建议先看导入产物。写一个小脚本加载三五个 Document把doc.text和doc.metadata完整打印出来肉眼看看有没有列名有没有语义锚点时间格式对不对有时候问题出得非常低端比如列名因为编码问题变成了乱码或者公式缓存值是 None。这个自检脚本我每做一个新的数据源都会跑一遍能省下大量无意义的调参时间。最后再分享一个个人习惯无论用哪个 Loader导入完成后我都会先问自己一个问题——“如果我是用户我会用什么词来搜这一行数据”如果答案和导入文本之间的语义距离很远就说明预处理还没做够先别急着调检索参数。表格和数据库导入这件事难的不是把文件读进来而是把表格里的“二维语义”在导入前想清楚这一段做扎实后面的 RAG 链路会顺非常多。