做 RAG 项目做久了,你会发现一个特别有意思的现象:大家聊拆 PDF、聊爬虫抓网页头头是道,一提到表格和数据库,尤其是 CSV、Excel 这种最常见的数据格式,反而容易卡壳。我在本地知识库项目里就踩过不少坑——表格导进去之后检索结果乱七八糟,明明数据都在,答案就是答非所问。今天这篇就专门聊聊 RAG 里表格与数据库导入这件事,把 CSV、Excel 和 LlamaHub 连库的实战过程完整过一遍。无论你是正在搭 OCR 处理流程、折腾 Ollama 简易本地 rag 知识库,还是准备把公司业务库接进向量检索,这篇的内容应该都能直接用上。
1. 为什么表格与数据库是 RAG 中最难啃的一类数据
1.1 表格数据进 RAG 的真正难点
先说结论:表格类数据难,不是因为读取难,而是因为结构语义容易丢。CSV 每行是一个记录,列代表字段,行与行之间还有隐含的排序、分组、统计关系。如果直接把整张表拼成一段纯文本丢进分块器,向量检索看到的就是一大坨“字段:值”的字符串,用户问“哪个部门三季度业绩最高”,它根本不知道哪一列是部门、哪一列是业绩。这就是我常说的平面化损失。
我还发现很多刚开始做 rag 知识库的朋友,习惯用 PDF/文本的思维去处理表格:读取一个单元格,把它当句子;读取一整行,把它当段落。这样出来的向量表征,语义密度很低,因为单元格内容往往只是数字和简短名词,相互之间缺乏上下文衔接。解决思路只有一条——尽量保留行、列、标题的上下文关系,把“一张表”变成一个结构化的 Document,而不是把表压成一堆孤立字符串。
顺便回答一个大家经常问的问题:rag 知识库能不能存图片?能,但那属于多模态 RAG 的范畴,跟表格导入是另一套管线,图片需要单独做视觉嵌入。本文还是聚焦在表格和数据库的纯文本向量化上,先把结构化数据这条线走通。
1.2 连库导入与文件导入的取舍
很多人会问:我到底该用 CSV 文件导入,还是直接连数据库?这事没有标准答案,取决于数据更新频率和加工复杂度。如果你的业务数据每天从业务系统里同步到一张表,那直接连库最省事,每次跑到查询里拉增量,自动生成 Document;如果你只是一次性拿到一份导出的 Excel,那没必要把数据库架起来,清洗完文件直接导入就行。
LlamaHub 在这里承担的角色,是一个统一的数据接入层。它不是一个数据库,也不是一个解析引擎,而是把不同来源的数据源封装成统一的 loader,输出统一的 Document 对象,下游无论是做 OpenAI 向量化还是本地 embedding,根本不需要关心数据是从 CSV 还是 MySQL 来的。这一点在工程上非常关键,因为一旦团队里有人提出“我这边是 PostgreSQL”,你不需要重写整条导入链路,只需换一个 loader。
2. 导入前必备:清洗表格数据的底层逻辑
2.1 三种表格数据形态的差异
CSV、Excel、数据库表,看起来都是表格,解析方式却完全不同。CSV 是纯文本,按分隔符切分,无格式、无多行单元格,读取简单但对编码和引号转义非常敏感。Excel 是二进制容器,除数据外还可能有多个 Sheet、样式、合并单元格、公式,读取必须依赖 openpyxl、xlrd 这类解析引擎。数据库表有强类型和约束,字段名规范,数据质量通常比手工导出的 Excel 高,但读取要过网络连接和 SQL 查询。
这种差异直接决定了清洗和导入策略各不相同。用 pandas 处理 CSV 时,我通常指定 dtype,防止“产品编号 001”被自动转成数字 1;处理 Excel 时会把目标 Sheet 显式列出来,避免遍历所有 Sheet 浪费时间;连数据库时优先用视图或带 WHERE 的查询,而不是 SELECT *,把脏数据过滤放到 SQL 层,效率远高于拉回内存再清洗。
2.2 通用清洗五步走
不管源头是什么,导入前基本走五步:
- 列名归一化:统一命名,去掉空格、特殊符号,确保后续 metadata 稳定。
- 空值处理:CSV 空字段、Excel 合并单元格的空值,先 fillna('') 或者 dropna,看你是否需要保留记录。
- 去重:按主键列去重,避免向量库里出现大量重复片段。
- 类型统一:日期统一成 YYYY-MM-DD,数字去掉千分位逗号。
- 上下文补全:给每一行补上表名和关键列的语义说明,比如把 product_name 列补充到行文本里。
在做清洗时,我体会最深的一点是:清洗的目标不是让数据“好看”,而是让模型“读得懂”。比如一张订单表,只导入 order_id、amount 两个字段,模型根本不知道金额对应的是什么币种。清洗阶段给每个字段补一句注释——“amount 的单位是人民币元”——检索时模型理解就完全不一样。
2.3 为什么清洗直接决定检索效果
RAG 的效果上限,基本被“入库数据质量”提前锁死。倒不是说脏数据一定会导致检索失败,而是脏数据会大幅拉高误检率。比如表格里有大量空行和合并单元格,解析出来就会出现“列名A: 列名B”这种垃圾片段,向量化后占据记忆容量,真正有用的查询反而被挤到相似度排名后面。所以我在导入前会安排一个 sanity check——随机抽几行转换后的 Document,人工看一眼文本还原度,这一步比调 embedding 模型参数还重要。
3. 实操:CSV 导入与向量化全流程
3.1 用 pandas 读取 CSV 的正确姿势
先从最普通的场景说起:你手上有一个 CSV 文件,里面是几千行产品资料,打算导入 rag 知识库。很多教程第一步就是 pd.read_csv('a.csv'),但实际工程里至少要加三个参数:
import pandas as pd df = pd.read_csv( 'products.csv', encoding='utf-8-sig', sep=',', dtype={'product_id': str, 'price': float}, )encoding 用 utf-8-sig 是为了兼容 Excel 导出的带 BOM 的 CSV,不加这个,第一列列名会多出 \ufeff,检索时 metadata 总对不上,排查半天都不知道问题在哪。dtype 强制把 product_id 当字符串,避免前导零丢失。sep 默认确实是逗号,但国内很多系统导出的是制表符或分号,这一步显式指定能少踩很多坑。
读取完之后不要急着遍历行。我会先看 df.columns 和 df.info(),确认列数、空值个数、类型没被误解析。如果 CSV 里有脏行导致报错,可以用 error_bad_lines=False(pandas 1.3 之后是 on_bad_lines='skip'),直接跳过解析失败的行,后续再单独排查。
3.2 按行构建 Document 并注入元数据
pandas 读取完成后,核心动作是“一行变一个 Document”。这里的 Document 指的是向量检索的最小单元。我通常这样写:
from llama_index.core import Document documents = [] for _, row in df.iterrows(): text = "产品ID:{}\n产品名称:{}\n价格:{}\n描述:{}".format( row['product_id'], row['product_name'], row['price'], row['description'] ) documents.append(Document( text=text, metadata={ 'source': 'products.csv', 'row_index': int(row.name), 'product_id': row['product_id'], } ))这段代码的关键不是遍历,而是 metadata 的设计。source 字段让检索结果能追溯来源文件;row_index 保留原始行号;product_id 作为业务主键,后续做知识库更新或删除时,可以直接通过 metadata 过滤掉旧数据,不需要重建整个向量库。很多项目倒在这一步:文档没有主键元数据,后面想增量更新,只能全量重灌,费时费力还容易撞进缓存。
text 字段的拼装也有讲究。我习惯用固定的模板,把每一列的列名注释也拼进去。比如“价格”后面直接跟数字,模型可能不知道单位,但“价格(元):12.5”就清楚多了。这就是把结构语义转换成自然语言的关键技巧。
3.3 向量化与存储
构建完 Document 列表,接下来就是 embedding 和写入向量存储库。这一步我分开说:先说 embedding,再说存储。如果不依赖云端接口,可以用 Ollama 里拉取的本地 embedding 模型,比如 nomic-embed-text 或 bge-m3,成本低、支持中文;如果项目允许放接口,OpenAI 的 text-embedding-3-small 效果更稳定。选哪个模型取决于你的知识库规模:几千行数据的量级,本地模型完全够;几十万行,再考虑性能更优的商用模型。
存储方面,我建议从 Chroma 或 FAISS 起步。Chroma 适合快速原型,因为自带持久化目录,还能按 metadata 过滤;FAISS 更轻量,适合追求检索性能的场景。写入代码这里不展开,只说一个经验:向量化之前先确认 embedding 模型的维度,向量库的集合/索引维度必须和模型维度一致,否则写入直接报错。我就在这上面浪费过半小时,换模型后忘了建新集合,一直在报错。
4. 实操:Excel 多 Sheet 与复杂表格解析
4.1 Excel 解析工具的选择
Excel 是 RAG 项目里最容易被低估的格式。很多表不是单 Sheet 单层表头,而是多个 Sheet、合并单元格、公式、样式混在一起。工具选型上,团队里如果有现成的 pandas 环境,直接 pd.read_excel() 是最省心的;但遇到超大文件或需要保留公式值时,我会考虑 openpyxl 直接操作。
如果用 LlamaHub 现成的 ExcelReader,它内部也是调 openpyxl,胜在输出直接就是 Document,不需要自己写转换。我的建议是:一次性导入用 LlamaHub ExcelReader,需要做复杂清洗和转换时绕开它、手动用 pandas + openpyxl,再喂给 llama_index。
4.2 合并单元格和批量空行的处理经验
Excel 里最坑的就是合并单元格。pandas 读进来之后,合并区域默认只有左上角有值,其他全是 NaN。比如“季度”列跨了两行合并,第二行的季度就是空值。处理方式一般是先对整个 DataFrame 做前向填充:df['季度'] = df['季度'].fillna(method='ffill'),把上一行的值填充下来。这个方法对层级表头也很好使。
批量空行则会在 CSV 或 Excel 的导入中变成全 NaN 行,这类行直接 dropna(how='all') 干掉,否则向量库里会出现大量空 Document,既浪费存储又拉低检索准确率。还有一个细节:Excel 单元格如果存的是数字,pandas 读出来可能是 int 或 float,但如果是公式,openpyxl 默认读公式字符串而不是计算结果。如果表格是公式生成的,需要显式指定 data_only=True 来拿缓存值,否则会导入一串公式字符串,向量化之后完全是无意义文本。
4.3 两层表头如何扁平化
有些业务表格是典型的“两层表头”,第一行是大类,第二行是明细字段,比如“2023年收入”“2023年成本”这种。直接用 pandas 默认行为只会把第一行当列名,细字段全被错位。我的套路是:
df = pd.read_excel('report.xlsx', header=[0, 1]) df.columns = ['_'.join([str(c) for c in col if str(c) != 'nan']) for col in df.columns]header=[0, 1] 会读前两行作为多级列索引,然后用 join 把两级列名拼成一串,比如“2023年收入_华东区”。这样列名既保留了层级关系,又不会因为重名覆盖。拼完列名后,数据一样按行循环转 Document。要不要保留列名层级,取决于你下游检索的 query 方式:如果用户会问“2023年收入华东区是多少”,这种扁平列名就非常友好。
5. 实操:LlamaHub 连库实战
5.1 LlamaHub 里最值得用的三个 Reader
LlamaHub 是 llama_index 生态的 loader 市场,里面几百个数据源 loader,但做表格和数据库导入,最常用的其实是三个:
- CSVReader:轻量,直接把 CSV 变成文本行 Document,适合快速演示。
- ExcelReader:基于 openpyxl,单文件多 Sheet 都能读,适合 Excel 批量导入。
- DatabaseReader:通过 SQLAlchemy 连接主流关系型数据库,用 SQL 查询结果生成 Document,适合线上数据实时导入。
这三个 Reader 的输出结构完全一致,都是 Document 列表。工程上的好处是什么?你可以先在本地用 CSVReader 把链路跑通,再切到 DatabaseReader 换真实数据源,下游的 embedding、store、query 完全不用改。这也是我推荐团队统一走 LlamaHub 的原因,它逼你把数据接入和检索逻辑解耦。
5.2 用 DatabaseReader 连接 MySQL / PostgreSQL
连库实战其实没有想象中复杂。先安装依赖:
pip install llama-index-readers-database然后写一个简单的连接和读取:
from llama_index.readers.database import DatabaseReader reader = DatabaseReader( scheme="mysql+pymysql", host="localhost", port="3306", user="root", password="your_password", dbname="sales", ) documents = reader.load_data( query="SELECT order_id, customer_name, amount, order_date FROM orders WHERE order_date >= '2024-01-01'" )这里有几个要点需要特别提醒。第一,scheme 是 SQLAlchemy 的连接串前缀,MySQL 是 mysql+pymysql,PostgreSQL 是 postgresql+psycopg2,不要写成 mysql 或 postgresql 简写,否则底层驱动会找不到。第二,query 参数是核心,不要用 SELECT *,最好显式列出需要的列。因为 DatabaseReader 会把你查询返回每一行变成一个 Document,列多了文本就乱,且容易拖慢查询。第三,生产环境不要在代码里硬编码密码,用环境变量或密钥管理服务读取。
DatabaseReader 本身不限制 SQL 类型,你甚至可以写 JOIN 和聚合查询,把多表数据预处理好再生成文档。这种方式非常适合数据仓库表比较多、业务口径复杂的场景。
5.3 查询边界、增量更新与连接管理
连库导入最需要考虑的不是怎么读,而是怎么管理查询边界和更新频率。全量 SELECT 第一次没问题,第二次跑的时候知识库里已经有一批旧数据了,怎么办?我常用的做法有三个:
- 按时间戳增量:查询的条件用 created_at > 上次导入时间,每次只捞新增数据。
- 在 SQL 里过滤,而不是拉回内存再过滤:数据库有索引,过滤开销极小,全量拉回来后 pandas 过滤反而浪费网络和内存。
- 结合 metadata 里存主键,导入后做去重或清理:通过 metadata 过滤器把旧版本文档删掉,再插入新数据。
连接管理方面,DatabaseReader 底层走 SQLAlchemy,每次 load_data 默认会新建连接。短连接在低频导入时无所谓,但如果你的任务每天跑很多次,更稳妥的方式是使用 SQLAlchemy 的 pool 配置,或者在外面做好连接复用。真遇到连接超时,优先检查数据库的 wait_timeout 参数,其次检查 SQL 是不是全表扫描没走索引。这个坑在导入大数据量表时非常典型,排序一下索引就出来了。
6. 常见问题与排查技巧实录
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| CSV 中文乱码 | 文件编码不是 UTF-8,可能是 GBK 或带 BOM | 读取时指定 encoding='utf-8-sig',乱码时试 gbk |
| 第一列列名多出 \ufeff | Excel 导出 BOM 未处理 | 用 utf-8-sig 读取,或者代码里 strip '\ufeff' |
| Excel 某列大量 NaN | 合并单元格或空行没处理 | 用 ffill 前向填充,dropna(how='all') 删除空行 |
| 导入内容全是公式字符串 | openpyxl 默认读公式不读缓存值 | 用 data_only=True 读取 |
| DatabaseReader 连不上 | scheme 写错或驱动未安装 | 检查 mysql+pymysql / postgresql+psycopg2 是否安装 |
| 向量库写入报维度错误 | embedding 模型与集合维度不一致 | 删除旧的 collection,按模型维度重建 |
| ollama 本地 embedding 响应慢 | 模型未预热或服务并发低 | 首次查询会慢,先跑几个 query 预热 |
这个表里的问题我基本都踩过一遍。尤其要说说 CSV 的 BOM 坑,如果你用 utf-8 读取带 BOM 的文件,第一列列名会多一个不可见字符,检索时用户问题带列名反而匹配不上,debug 时很难发现,因为打印出来长得一模一样。我的习惯是统一用 utf-8-sig 处理所有 Excel 导出的 CSV。
再补充一个 Excel 文件本身的花样:有朋友反馈过“Excel 加载项被禁用”和“ctrl+v 失效”这类问题。如果你遇到 Excel 自身功能异常,优先检查是否开了多个插件互相抢占快捷键,这在数据准备阶段会浪费大量时间,我的建议是手动清洗时尽量用 pandas / openpyxl,不依赖 Excel 客户端,稳定性高得多。这一点是我在批量整理表格数据时摸索出来的,后期处理报表我基本不开 Excel 界面,全部脚本化。
最后说一个数据库导入的内存问题。如果表特别大,比如几百万行,一次 load_data 全查回来内存很容易崩。我的做法是分页查询,每页 5 万行,逐页构建 Document 并写入向量库,全部处理完后释放连接。虽然慢一点,但稳定性好很多。你甚至可以用 SQL 的 LIMIT 和 OFFSET 来做分批,或者按日期字段分片。这个思路对 MySQL、PostgreSQL 都适用。
关于 LlamaHub 依赖安装的问题,很多人卡在 pip install 阶段。llama_index 生态是模块化的,不要一股脑装全家桶,只需要按 loader 名装对应的包。比如 DatabaseReader 就是 llama-index-readers-database,ExcelReader 是 llama-index-readers-file。装错包或者装了版本不一致会导致 Reader 属性找不到,先看导入报错信息,再对症装依赖。
写到这里,本次表格与数据库导入的核心链路已经完整了。我个人实际操作下来,最大的体会是:CSV、Excel、数据库连库看着是三个话题,本质上是一件事——如何把结构化数据无损地翻译成模型能理解的语义文本,再配上可靠的元数据。不要一上来就追求牛掰的 embedding 模型或者 RAG 框架,先把数据形态理清楚,清洗干净,导入链路稳定了,检索质量自然不会差。后续我还会继续更新这个系列,下一期大概率会聊表格类数据的分块策略和混合检索,大家要是有更好的表格处理经验,可以一起交流,评论区见。