☰
RAG表格数据导入实战:CSV、Excel与LlamaHub连库解析
2026/10/31 16:06:57 网站建设 项目流程

表格数据在RAG项目里,是最容易在导入阶段翻车的。我做了几个RAG落地项目之后有个很深的感受:纯文本型文档清洗再麻烦,好歹是线性结构,按段落切就行;一碰到CSV、Excel,还有需要连库查询的数据库表,问题就全冒出来了——编码、分隔符、多Sheet、合并单元格、日期格式、数据量爆炸,随便一个都能卡住大半天。这个系列的第三篇,我就专门把这部分讲透:CSV、Excel 以及基于 LlamaHub 的数据库导入实战,顺带把我在真实项目里踩过的那些坑一并交代清楚。

为什么表格类数据在 RAG 链路里这么特殊?因为它天然是二维结构,而大模型和向量检索的处理单元是文本块,行与列的信息在“转文本”的过程中非常容易错乱。更现实的问题是,很多业务数据的更新频率很高,你不可能每周手动导一次 CSV,这时候就需要通过 LlamaHub 这类加载器直接连数据库取数,让 RAG 知识库的数据保持新鲜。

这篇攻略适合正在搭建知识库、卡在“表格数据怎么喂给 RAG”这一环的同学,也适合那些已经把基础文档导入跑通、想进一步接入业务数据库的开发者。下面我按“思路 → CSV → Excel → LlamaHub 连库 → 问题排查”的顺序展开。

1. 表格数据导入的整体设计思路

1.1 为什么表格数据这么难处理

先聊一个概念层面的问题:CSV 和 Excel 本质上是“二维表”,每一行是一条完整记录,每一列是一个字段。而 RAG 流水线里的文本切分器、embedding 模型,处理的是“一维的”文本序列。如果不做任何处理直接把 CSV 文件喂进去,常见的现象是:

  • 检索“某客户最近三个月的消费金额”时,向量检索返回的可能是整个表头,或者表里某一列的所有值,而不是对应的那一行记录;
  • 多列数据被拼接后,模型分不清哪些内容属于同一个字段;
  • 数字类型的字段(金额、日期、ID)被当成纯文本,语义信息丢失。

我用一个生活化的类比解释一下:表格就像一张通讯录,每一行是“张三、139xxxx、北京”这样的完整条目。如果你把整张通讯录一页一页撕下来扔进碎纸机,再让助理从碎纸里找“张三的电话”,她看到的全是零碎的字词,根本不知道哪些碎片属于同一个人。RAG 处理未加工的表格数据,就是这个效果。

1.2 两条路线:文档化转换与连库查询

所以表格数据接入 RAG,我一般会先判断它的“生命周期”:

  • 静态快照型:历史报表、年度统计、导出的订单明细,这类数据一次导入后基本不变,适合“文档化转换”——把表格逐行转成自然语言描述,再切块嵌入向量库。
  • 动态业务型:订单库、用户库、库存表,这类数据每天都在变,如果还走“导入向量库”的路线,你很快会面临知识库过期的问题。更合理的做法是走“连库查询”路线,使用 LlamaHub 提供的数据库加载器,按需从源库取出结构化数据,再交给 LLM 或并入检索结果。

选型还需要考虑表的数量和关联复杂程度。几十张以内的静态表,CSV/Excel 方案完全够用;上百张表、还有跨表 JOIN 需求,老老实实走数据库方案。注意我这里的“数据库方案”指的是让 RAG 系统能按需查询数据库,而不是把整库导成文本塞进向量库——后者既笨重又低效,属于新手常犯的错误。

还有一个容易被忽视的点:图片和文件路径。很多人问“RAG 知识库能存储图片吗”,我一般回答:向量模型处理不了原始图片(除非接多模态模型),但表格里完全可以在某个字段存图片路径或文件 URL,导入时把路径这个字段保留成元数据,检索到对应行之后再由业务系统读取图片。这样既避开了向量库的局限,又不破坏表格的结构完整性。

路线定下来之后,剩下就是具体的导入实操了。

2. CSV 导入实战:轻量但处处是坑

2.1 读取 CSV 前必须先解决编码与分隔符

CSV 看上去是“最简单”的格式,做数据处理的人都知道它其实最阴险。我在一个项目里处理 football-data.co.uk 这类体育数据网站的 CSV 时就遇到典型场景:文件可能是 UTF-8 编码,但用 Excel 打开后另存为一次,就变成了 ANSI(GBK);有些欧洲来源的 CSV,分隔符不是逗号而是分号;还有些字段里本身就带逗号或换行符,用split(',')处理会直接裂开。

我的建议是:永远不要用文本分割函数去解析 CSV,一律用标准库csv模块或pandas.read_csv。读取之前先看文件的编码,常见处理方式:

import csv # 先确认编码,先用二进制打开看前几行 with open("data.csv", "rb") as f: raw = f.read(200) print(raw) # 观察是否有 BOM:\xef\xbb\xbf 表示 UTF-8 with BOM # 推荐显式指定编码,避免 Windows 下默认 GBK 导致乱码 with open("data.csv", "r", encoding="utf-8-sig", errors="replace") as f: reader = csv.DictReader(f) rows = [row for row in reader]

utf-8-sig会自动去掉 BOM 头,errors="replace"保证遇到个别坏字符时不至于整个崩溃。如果你用的是 pandas,加engine="python"可以处理更复杂的分隔符和引号情况:

import pandas as pd df = pd.read_csv( "data.csv", encoding="utf-8-sig", sep=",", dtype={"客户ID": str}, # 关键:防止 ID 变成科学计数法 parse_dates=["下单日期"], na_values=["", "N/A", "null"], ) print(df.head(5)) print(df.dtypes)

2.2 从 CSV 到向量知识库:标准流水线实操

CSV 本质上是表格,我推荐在导入前先做一次“行转文本”的预处理。以 LlamaIndex 为例,最省事的方案其实是直接用它的SimpleDirectoryReader,它会自动处理 .csv 文件并逐行读取:

from llama_index.core import SimpleDirectoryReader documents = SimpleDirectoryReader( input_files=["orders.csv"], filename_as_id=True, file_metadata=lambda filename: {"source": "orders.csv"}, ).load_data() print(len(documents)) print(documents[0].text[:200])

SimpleDirectoryReader输出的每个 Document 就是 CSV 中的一行,但这种方式的问题在于:它把整行所有列直接拼成一段文本,字段名和值混在一起。如果 CSV 有几十列,文本会非常冗长,检索效果很差。

更可控的做法是先用 pandas 清洗,再把每行格式化成“字段名:值”的结构化文本,最后转成 Document:

from llama_index.core import Document import pandas as pd df = pd.read_csv("orders.csv", encoding="utf-8-sig", dtype={"订单ID": str}) docs = [] for _, row in df.iterrows(): # 只保留非空字段,避免生成大量 “字段:NaN” parts = [f"{col}: {val}" for col, val in row.items() if pd.notna(val)] text = ";".join(parts) docs.append( Document( text=text, metadata={"row_id": row["订单ID"], "source": "orders.csv"}, ) ) # 后续再用 chunk_size 控制切块,比如 800 tokens

这里有两个关键点。

  • ID 字段务必转成字符串。Excel 里超过 15 位的数字(比如订单号、身份证号)会自动显示成科学计数法,导入 pandas 后如果不指定 dtype,也会被当成 float,末尾几位直接丢精度,这就是为什么很多人导入后发现“ID 全变了”。
  • 日期字段要显式解析。不解析的话,2024-01-05只是一个字符串,检索“一月份的订单”时没法被语义化理解,最好统一格式化成2024年1月5日这样的自然语言描述。

2.3 用 LangChain 的 CSVLoader 做对照

除了 LlamaIndex,LangChain 系的CSVLoader也很好用,它会把每行变成带 source 元数据的文档,配合loader_kwargs可以自定义分隔符和编码:

from langchain_community.document_loaders import CSVLoader loader = CSVLoader( file_path="data.csv", encoding="utf-8-sig", csv_args={"delimiter": ",", "fieldnames": ["id", "name", "amount"]}, ) documents = loader.load()

实测下来,LangChain 的 CSVLoader 适合数据量小、不需要做复杂字段清洗的场景;一旦涉及日期解析、空值处理、多类型字段,pandas + Document 自定义方案更灵活。按我的经验,二三十列以内的小表用 LangChain 够用,上了数据仓库的规模就别在这种阶段省事了。

3. Excel 导入实战:多 Sheet 与单元格级的细节

3.1 Excel 与 CSV 的本质差异

Excel 和 CSV 最大的区别,是 Excel 有“工作簿”结构——一个 xlsx 文件里可以包含多个 Sheet,每个 Sheet 还有表头样式、合并单元格、公式、条件格式等。CSV 只有一张扁平的二维表,Excel 则是一个完整的小型数据容器。

在 RAG 导入时,以下几个 Excel 特有的问题必须处理:

  • 多 Sheet:默认读取只会取第一个 Sheet,其他 Sheet 会被丢掉。如果你的 Excel 是“每月一个 Sheet”或“总表 + 明细表”的结构,一定要遍历读取。
  • 合并单元格:常见的报表会把第一列“部门名称”合并好几行,pandas 读出来之后只有第一行有值,其他行是 NaN,直接导入会丢信息。
  • 公式与值:openpyxl 默认读的是公式字符串(比如=SUM(A1:A10)),不是计算结果。如果拿公式字符串去喂 RAG,检索到的全是函数表达式,没有实际意义。
  • 隐藏行/列:有些报表为了打印美观会隐藏列,导入时如果不加判断,会把隐藏列里的无关数据也带进来。

处理这些问题的核心工具是 pandas + openpyxl。实际项目中我通常这样写:

import pandas as pd # data_only=True 表示读取缓存的计算结果而非公式 xl = pd.ExcelFile("月度报表.xlsx", engine="openpyxl") print(xl.sheet_names) all_docs = [] for sheet_name in xl.sheet_names: df = pd.read_excel( xl, sheet_name=sheet_name, header=0, dtype=str, # 先全部按字符串读,后面按需转换 ) # 处理合并单元格:向前填充 df = df.ffill() # 删除完全空的列 df = df.dropna(axis=1, how="all") # 行转文本 for _, row in df.iterrows(): text = ";".join([f"{col}: {val}" for col, val in row.items() if pd.notna(val)]) all_docs.append(f"【{sheet_name}】 {text}")

注意dtype=str这个细节。我的习惯是第一遍读取时全部转成字符串,避免 pandas 自动把“0012”这种编号解析成整数 12。等到字段分析清楚了,再单独把日期、数值列做转换。这种“先保守读入,再精炼字段”的策略能避免很多因为类型推断造成的数据损失。

3.2 多级表头、汇总行与非表格区域的清理

真实世界里的 Excel 很少是“第一行表头 + 干净数据”的理想形态。我遇到最多的三类杂讯是:

  • 顶端有几行大标题,比如“2024 年度销售统计表”,真正的列名在第三行;
  • 底部有汇总行,比如“合计:123456”,这种行不应该进入知识库;
  • 右侧有备注列,比如注释性的说明文字,容易干扰检索。

处理技巧很直接:用df = pd.read_excel(xl, sheet_name=..., header=2)跳过前两行,用df = df[~df.iloc[:, 0].astype(str).str.contains("合计")]过滤汇总行,用df = df.drop(columns=[备注列])清理备注列。这些都属于“导入前数据消毒”,别偷懒,否则后面 RAG 检索质量要用十倍的代价来补。

合并单元格的坑我再强调一次。pandas 读取合并单元格后,左上角单元格有值,其他位置是 NaN,用ffill()只能填充“值向下延续”的方向,如果是列方向的合并,需要用ffill(axis=1)或者先 transpose 再填充。多级表头(两层表头)建议直接把第二层转成列名,丢弃第一层分类名,因为向量检索没有层级概念,冗余的父级分类名会稀释文本语义。

3.3 Excel 导入时的常见日常问题

在整理 Excel 导入流程时,经常会同时遇到一些 Excel 使用层面的问题,虽然不是 RAG 核心,但会打断开发节奏,这里顺带收录:

  • Excel 加载项被禁用:如果你装了一些增强插件,打开 xlsx 时提示“已禁用的加载项”,一般是在“文件 → 选项 → 加载项 → 管理 COM 加载项”里重新启用。公司电脑上常因为权限丢失导致这种情况,但注意不要随便启用来路不明的加载项。
  • Ctrl + V 复制粘贴失效:大概率是运行了某些监控软件或剪贴板冲突,重启 Excel 或清空剪贴板历史即可。
  • 批量出图要插入 Excel 表格:ArcGIS 这类 GIS 软件里也有这个场景,本质是把表格按坐标/ID 关联到要素,导出的 CSV 建议统一用 UTF-8 编码,避免中文属性乱码。

3.4 给表格加一个稳定 ID 列

做 RAG 导入时,我发现很多人忽略一个关键设计:给每条导入记录加一个稳定的 ID 字段。Excel 里用公式生成 UUID 并不复杂,但直接在 pandas 里生成更可控:

import uuid df["record_id"] = [str(uuid.uuid4()) for _ in range(len(df))]

这个 ID 的作用有两层。第一层是去重,相同的业务数据在重复导入时不会产生重复文档;第二层是关联,检索命中某条记录后,可以通过 record_id 反查源 Excel 行号,做溯源展示。我之前一次项目里,业务方要求“AI 引用每一句话都能定位到是第几行第几列”,没有稳定 ID 的时候只能靠文本相似度反查,效率和准确率都很低。加上 ID 之后,体验完全不一样。

至此,CSV 和 Excel 两条静态导入路线基本讲完了。接下来是最有含金量的部分:怎么用 LlamaHub 直接连数据库。

4. LlamaHub 连库实战:把数据库直接接进 RAG

4.1 LlamaHub 里能连库的加载器有哪些

LlamaHub 是 LlamaIndex 的官方加载器市场,里面有大量数据加载器,其中跟数据库相关的核心包是llama-index-readers-database,它提供了DatabaseReader,基于 SQLAlchemy 设计,支持 PostgreSQL、MySQL、SQLite、Oracle 等主流数据库。

安装命令:

pip install llama-index-readers-database sqlalchemy psycopg2-binary

如果是 MySQL,装pymysql或mysqlclient;如果是 SQLite,Python 自带sqlite3,连包都不用装。

在我的项目中,用得最多的是这个模式:

from sqlalchemy import create_engine from llama_index.readers.database import DatabaseReader # 以 PostgreSQL 为例 engine = create_engine("postgresql://user:password@localhost:5432/mydb") reader = DatabaseReader(sql_engine=engine) # 读取某张表的全量数据,每行生成一个 Document documents = reader.load_data(query="SELECT * FROM customers WHERE created_at >= '2024-01-01'")

关键接口是load_data(query=...),它会执行 SQL 并将查询结果逐行转成 Document。也就是说,你不需要把数据库“整库导入”,而是把“查询结果”作为输入,这给了我们极大的灵活性。

4.2 实操:MySQL 接入与按需查询

MySQL 的接入方式与 PostgreSQL 几乎一致,只是把连接串换成 mysql 协议:

from sqlalchemy import create_engine engine = create_engine( "mysql+pymysql://user:password@localhost:3306/mydb?charset=utf8mb4" )

为什么强调charset=utf8mb4?因为 MySQL 默认的字符集可能是 latin1,读取中文内容时容易乱码,RAG 系统里一旦乱码,检索质量直接归零。

接下来是一个实战场景:业务库里有张orders订单表,每天新增上千条记录,我不想每天手动导出 CSV,希望 RAG 应用每次查询时直接从库里拿最近 30 天的订单。这时候我不建议把全表塞进向量库,而是设计一个“查询时加载”的流程:

# 查询最近30天的有效订单,并按金额降序 query = """ SELECT order_id, customer_name, product_name, amount, order_date FROM orders WHERE order_status = 'completed' AND order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) ORDER BY amount DESC LIMIT 5000 """ documents = reader.load_data(query=query)

注意中间加了LIMIT 5000,这非常重要。数据库里如果有一两百万行历史数据,一条不带条件的 SQL 会把内存直接撑爆,我见过不止一次因为SELECT * FROM 大表导致进程被 OOM Kill 的情况。RAG 知识库做的是“精准召回”,不是“全量复制”,所以查询范围务必加上时间窗口、状态过滤和行数上限。

4.3 更进一步:让 LLM 辅助生成 SQL 的注意事项

LlamaIndex 的 SQL 能力远不止把查询结果转文本,它还有一套 Text-to-SQL 的机制,用户问“上个月销量最高的前十个商品是什么”,它会自动生成 SQL 并执行查询。这确实是 RAG 连库的高级玩法,我在项目里也用过,但有几个教训必须提醒:

  • 不要让用户输入直接拼接进 SQL。这一点怎么强调都不为过,我见过有人把用户问句直接拼到查询字符串里,这在内部系统也许能跑,一旦暴露到公网就是灾难性的。
  • 给 LLM 限定表名和字段白名单。在 LlamaIndex 配置SQLTableNodeMapping和SQLDatabase时,只暴露允许查询的表,不要让它自由发挥访问所有表。
  • 用只读账号连接数据库。我强烈建议专门为 RAG 系统创建一个只有SELECT权限的数据库账号,连查询操作都限制在业务需要的视图或表内。即便 SQL 生成出错,也只影响读取,不会破坏数据。
  • 数据库连接建议配连接池。RAG 系统频繁查询时,如果每次新建连接开销很大,最好用 SQLAlchemy 的pool_size和max_overflow控制并发连接数:
engine = create_engine( "postgresql://user:password@localhost:5432/mydb", pool_size=5, max_overflow=10, pool_pre_ping=True, )

4.4 数据库日常运维的顺手工具

在做连库导入前,你大概率需要先检查一下库里的数据和表结构。这里推荐几个我用着顺手的工具:

  • SQLite:如果你用的是 SQLite,官方推荐 DB Browser for SQLite,免费轻量;DBX 也是不错的选择,支持多种数据库统一管理。
  • MySQL:Navicat 和 DBeaver 都很好用,修改表结构时(ALTER TABLE)建议先看下SHOW PROCESSLIST,确认没有长事务在跑。
  • 数据库同步:如果源库是生产库、RAG 系统连的是分析库,建议用现成的数据库同步工具(比如 DataX、Flink CDC)定时同步,而不是让 RAG 直连生产库,避免拖垮业务系统。

另外补充一点:数据库表结构变动(比如新增列、修改字段名)会导致 RAG 查询的元数据过期。我建议每次变更表结构后,跑一遍下面的检查脚本,确认查询结果里没有“字段不存在”的错误:

SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'orders';

这些虽然不是 LlamaHub 的直接功能,但属于连库实战中绕不开的运维背景,提前安排妥当能省掉大量排障时间。

5. 导入质量检查与问题排查实录

5.1 导入前和导入后的核对清单

到这里,三类数据源(CSV、Excel、数据库)的导入方案都讲完了。但导入完成不等于工作完成,我每次都会留一个“质量验证”环节,这是我在项目中吃过亏之后养成的习惯。核对清单大概长这样:

检查项预期标准对应工具 / 方法
原始文件行数与导入后 Document 数量一致wc -l、len(df)
关键字段空值率不超过 5%df.isna().mean()
ID 列唯一性无重复且无科学计数法df["id"].nunique()
中文编码无乱码字符抽样查看 Document 前 100 字
日期解析格式统一df["date"].dt.strftime
检索命中率测试的 10 个问题至少命中 8 个手动测试向量检索
数据库查询耗时单次查询不超过 2 秒EXPLAIN ANALYZE

重点检查“Document 数量与源数据行数是否一致”。我遇到过的典型问题是:CSV 里明明有 5000 行,导入后只有 4900 条 Document,原因是某一行里包含换行符,被解析器当成了下一行的一部分。这种问题不核对数量级根本发现不了。

5.2 检索效果测试:导入后必做的三步验证

导入完成后,不要直接开始问问题,先用最原始的方式做三轮检索验证。

第一轮:关键词命中测试。拿一条源数据里的完整字符串,比如“张伟 2024-05-01 12800”,在向量库里检索,看返回结果是否能定位到对应的那条记录。如果连精确字符串都检索不到,说明切块粒度或嵌入方式有问题。

第二轮:字段关联测试。问一些需要跨字段理解的问题,比如“5 月份金额大于 10000 的订单有哪些”,看系统是否正确关联了“月份”和“金额”两个维度。表格数据最容易在跨字段关联上翻车。

第三轮:最大值/最小值/求和测试。比如“所有订单的总金额是多少”,这类聚合问题如果走的是纯向量检索,大概率答不上来。正确的做法是配置 SQL 查询工具(比如 LlamaIndex 的 QueryEngine 接数据库),让聚合运算交给数据库,而不是指望向量检索里“检索”出来。这一步能帮你验证连库模块是否真正生效。

5.3 常见问题速查表

把我在“表格与数据库导入”这个主题下遇到的最高频问题整理成一张速查表,直接对照解决:

现象可能原因解决方案
CSV 中文乱码文件是 GBK,代码按 UTF-8 读encoding="gbk"或"utf-8-sig"
Excel 读出来全是 NaN合并单元格未填充df.ffill()
Excel 读出来是公式不是值data_only=False改为data_only=True
订单号变成科学计数法数字位数过长被转 floatdtype={"ID": str}
导入后 Document 数量比源数据少多行文本字段被错误截断用csv模块或 pandas,不用 split
数据库连接超时连接池不够或网络不通配pool_pre_ping=True,检查网络
大表 SELECT 内存爆炸没有加 LIMIT 或时间窗口限定查询范围和行数上限
检索不到某个字段的值字段在转文本时被丢弃检查 Document 文本是否包含该字段
Excel 显示“加载项已禁用”加载项权限问题在选项 → 加载项中重新启用
Ctrl+V 失效剪贴板冲突或插件干扰重启 Excel 或清空剪贴板历史
数据库表结构修改后查询失败列名过期查 information_schema 核对字段
SQLite 工具选择困难生态较杂推荐 DB Browser for SQLite 或 DBX

5.4 两条独家经验

最后分享两个我在实际项目里反复用到的经验。

第一个是“小批量试错原则”。不管是 CSV、Excel 还是连库查询,第一次跑通一定是先用前 50 到 100 行做样本,把编码、字段类型、切块效果全部验证完,再放开全量导入。很多人图省事直接把百万行数据全量灌进去,结果发现字段类型判断错了,返回去重跑一遍不仅浪费时间,清洗逻辑还可能因为数据量太大而内存不足。用小样本来试错,成本低得多。

第二个是“保留原始文件与转换脚本的版本”。我曾经因为覆盖了清洗脚本,导致一个月后重新导入数据时完全想不起当时的转换规则,结果只能靠 diff 对比重新反推。现在我的项目目录固定保留raw/(原始文件)、processed/(清洗后结果)、scripts/(每个清洗脚本带日期后缀)三层结构,每次导入都能回溯。这个习惯在团队协作时尤其重要,因为别人不一定能读懂你写了一半的转换逻辑。

表格与数据库导入这块,做到“能查、能溯、能更新”三个阶段就算过关了。CSV 和 Excel 解决了静态数据的入库问题,LlamaHub 连库解决了动态数据的实时性问题,配合一套严谨的核对清单和检索验证流程,RAG 知识库里的表格数据才能真正经得起业务提问的检验。我个人在实际操作中的体会是:表格导入的难点从来不在“读文件”本身,而在数据清洗、结构保留和更新机制的提前设计。如果你没有在一开始就规划好这三件事,后面每一个问题都会让你怀疑人生。先把今天这套流程跑通一遍,再用自己的数据做一次彻底检验,你会发现表格类数据交给 RAG 其实并没有那么恐怖。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询