我们做 RAG(Retrieval-Augmented Generation,检索增强生成)项目时,最头疼的往往不是模型选型,而是数据准备。PDF、Word 这类非结构化文档还能靠解析器硬啃,但表格和数据库这种天生带结构的数据,进了 RAG 就各种水土不服:要么解析出来是一团乱麻,要么检索时上下文被截断,要么数值被当成普通文本唠嗑。这篇是这个系列第三篇,专门讲表格类数据的导入与解析,重点围绕 CSV、Excel 和 LlamaHub 连库实战展开。我会把我在实际项目里踩过的坑、验证过的方案、以及调参细节一并交代清楚,给正在做 RAG 知识库、特别是想接入结构化数据的朋友一份可以直接照做的参考。
CSV 和 Excel 这类文件,看着简单,但真要塞进 RAG 流程里,涉及的问题一点都不少:分隔符识别、编码处理、表头规范化、数据清洗、切块策略、以及最关键的——如何让检索器理解“这是一个表格”而不是一串干巴巴的文本。至于 LlamaHub 连数据库,则是把 RAG 的触角伸向真正实时更新的结构化数据源,让知识库不再是“导入一次就固化”的静态文档集。这篇文章会从设计思路讲起,再一步步拆解实操细节,最后给出常见问题的排查实录,全程没有晦涩的理论堆砌,都是能直接用起来的经验。
1. 内容整体设计与思路拆解
1.1 表格数据在 RAG 里的核心痛点
先说一个经常被忽视的事实:表格数据是“半结构化”的,它既有文本的自由性,又有模式(Schema)的约束性。你在 CSV 里看到的一行记录,其实是一个由多个字段组成的逻辑实体;你在 Excel 里的一个 Sheet,可能同时包含标题、表头、合并单元格、公式、日期格式等多重信息。普通文本切块(Chunking)方式对表格极不友好,因为切块是按字数就近分割的,表头和一行数据被切到两个 chunk 里是常有的事,检索时拿到的上下文自然支离破碎。
另一个痛点是检索质量的严重下滑。RAG 的检索阶段依赖向量相似度或关键词匹配,但表格里的数值型字段(价格、日期、编号)在向量化之后,语义几乎完全丢失。比如“2024Q3 营收 12.8 亿”这个字段,在向量空间里跟“2024Q3 营收 13.1 亿”的距离远到让人怀疑,但这两条记录在业务上其实是相邻的历史版本。如果只做纯文本嵌入,查询“去年三季度营收”时,模型很可能返回一条语义接近但数值完全错误的行,这就是表格类 RAG 最常见的幻觉来源。
所以我做这类项目时,第一原则是:在导入前先想清楚这个表格将来会被怎么查询。如果查询集中在“某某字段等于什么”“某个时间段的汇总”这类精确匹配,那应该走 LlamaHub 连数据库 + SQL 生成的路子;如果查询是“这段描述对应哪条业务记录”这种模糊匹配,那 CSV/Excel 解析成带结构的文本更合适。两条路线解决的是不同的问题,千万不要混用。
1.2 为什么选择 CSV、Excel 与 LlamaHub 这个组合
CSV 和 Excel 是数据交换的“最低公约数”,几乎所有系统都能导出这两个格式,而且它们代表了两种不同的建模思想:CSV 是纯数据、无格式,适合批量处理和程序化解析;Excel 是带格式的工作表,适合人工维护和可视化查看。对 RAG 项目来说,CSV 通常来自系统导出、日志收集、API 拉取,Excel 则多用于业务人员手工维护的配置表、报价单、物料清单。前者的特点是量大、字段规整,后者的特点是结构复杂、带有格式信息(颜色、合并单元格、注释)。两种都要支持,否则知识库的覆盖面就缺了一大块。
LlamaHub 在这个体系里的定位是“数据连接器中枢”。它的价值不在于本身多复杂,而在于把“连接数据库”这件事从手写 SQL + 封装 ORM 的重复劳动,变成声明式配置。你不需要关心数据库连接池怎么管、分页怎么拉、字段类型怎么映射,LlamaHub 把这些都封装好了。结合数据库本身支持 SQL 查询的特性,RAG 就能从“拿文档相似度匹配”升级到“理解用户意图后自动生成 SQL 做精确查询”,这是表格数据导入的终极形态。
1.3 方案选型的取舍分析
我对比过几套常见方案,这里直接给出结论,有特殊需求再另说。
- 纯文本解析 + 向量检索:适合字段少、记录多、查询偏语义的行式表格。优点是实现快,缺点是对精确匹配支持差,日期区间和数值比较尤其容易出错。
- 表格结构保留 + 向量检索:在切块时保留表头信息、用分隔符标记字段,检索时把整行作为一条记录处理。适合中等规模的业务表,比纯文本解析好不少,但依然没有解决“数值比较”这个硬伤。
- LlamaHub 连数据库 + 语义 SQL:适合数据实时更新、查询模式清晰的场景,比如 CRM 系统里的客户记录、库存表、订单表。优点是精确、实时,缺点是依赖数据库本身可用性,且生成 SQL 需要额外校验,LLM 有时会写出错误语法。
- 混合方案:大规模数据全量走向量检索用于语义召回,关键字段用数据库做精确过滤。这个方案效果最好,但工程复杂度也最高,适合对检索质量要求极高的生产环境。
我的建议是:项目初期先选第一种或第二种跑通流程,等确认了核心查询场景,再把高频查询切换到 LlamaHub 连库的路线。一上来就建全套混合架构,大概率会陷入过度设计的泥潭。
2. CSV 导入的完整流程与核心细节
2.1 数据准备:清洗永远比解析更重要
CSV 导入的第一步不是写代码,而是先处理数据本身。很多人直接把源文件丢给解析器,结果字段错位、乱码、空行一股脑涌进来,后续所有环节都在带病运行。我的经验是先做一次轻量级清洗,一般在 Python 里用 Pandas 或 csv 模块搞定。
最常见的坑是编码和 BOM(Byte Order Mark)。Windows 下 Excel 导出的 CSV 通常是 GBK 或 GB2312 编码,而且文件开头会带一个 \ufeff 字符,如果不处理,第一列的列名就会变成“\ufeff名称”,后续所有下游逻辑都会遭殃。我的固定套路是先读二进制内容,检测编码(可以用 chardet 或 cchardet),再统一转为 UTF-8 并去掉 BOM:
import chardet with open("raw_data.csv", "rb") as f: raw = f.read() encoding = chardet.detect(raw[:1000])["encoding"] # 统一转为 UTF-8 且不带 BOM import codecs text = raw.decode(encoding) text = text.lstrip("\ufeff") with open("cleaned.csv", "w", encoding="utf-8") as f: f.write(text)另一个高频问题是分隔符不一致。有些系统导出的 CSV 并不是严格逗号分隔,可能是分号,也可能是制表符(TSV)。用 Pandas 读取时可以用sep=None+engine="python"让引擎自动嗅探分隔符,但更可靠的做法是自己先读第一行判断:
import csv with open("cleaned.csv", "r", encoding="utf-8") as f: sample = f.readline() delimiter = csv.Sniffer().sniff(sample).delimiter这里有个细节:Sniffer 会尝试多种分隔符组合,但如果表头里本身包含逗号或分号,它可能判断错误。我的兜底方案是准备好下拉列表式枚举,常见分隔符依次尝试,然后检查列数是否一致,不一致就换下一个。
2.2 字段类型判断与日期规范化
CSV 里所有字段最初读进来都是字符串,但后续解析时需要知道哪些列是文本、哪些是数字、哪些是日期。用 Pandas 读取时,dtype参数可以显式指定,但更省心的做法是让 Pandas 先推断,再针对日期列单独处理。
日期格式化是这里最容易出 bug 的地方。CSV 里的日期格式五花八门:2024/1/1、2024-01-01、01/01/2024、20240101,甚至带时间戳的完整时间。如果直接按字符串存进向量库,检索时“今年 1 月的数据”和“去年 1 月的数据”在文本上完全无法区分;如果统一成 ISO 8601 格式(2024-01-01),则能保证字符串排序等于时间排序。我的做法是:
import pandas as pd df = pd.read_csv("cleaned.csv", encoding="utf-8") # 指定可能的日期列做统一格式化 date_cols = ["publish_date", "update_time"] # 按实际情况调整 for col in date_cols: df[col] = pd.to_datetime(df[col], errors="coerce").dt.strftime("%Y-%m-%d")这里errors="coerce"会把无法解析的日期变成 NaT,后续统一过滤掉,避免脏数据混入知识库。数值列也一样,先转成 float 或 int,再把格式化后的字符串写回文本,保证向量化时数值表达一致。比如金额列统一保留两位小数,而不是有的行是 12.8、有的行是 12.80。
2.3 行级切块:让检索器看到完整的“记录体”
CSV 导入过程中最核心的设计决策是切块策略。我强烈建议按“行”作为基本切块单位,而不是按字数。因为一行 CSV 本质上是一条完整记录,拆开就废了。
具体做法是把每一行拼成一个结构化的文本块,表头信息作为 metadata 保留。举个例子,一个包含“城市、人口、GDP、年份”四列的 CSV,一行数据就可以拼成:
城市:上海 | 人口:2487 万 | GDP:4.72 万亿 | 年份:2023这个拼接格式有两个好处:一是语义完整,检索时能一次性拿到这条记录的所有关键字段;二是字段之间有显式分隔符,嵌入模型能更好地学习字段与值之间的对应关系。分隔符我建议用“|”或换行符,不要用逗号或分号,因为字段内容里本身就可能包含逗号,会产生歧义。
同时,把列名(表头)作为 metadata 保存,而不是拼进文本内容。这样一来,查询“上海的 GDP 是多少”时,即使文本内容里没有出现“GDP”这个词,metadata 里的列名也能帮检索器定位到正确的 chunk。
from llama_index.core import Document records = [] for _, row in df.iterrows(): content = " | ".join([f"{col}:{row[col]}" for col in df.columns]) doc = Document( text=content, metadata={"source": "city_stats.csv", "row_index": row.name} ) records.append(doc)这里要注意一个性能问题:df.iterrows()在数据量大时很慢,几万行就要几秒。如果 CSV 超过 5 万行,建议改用df.itertuples(index=True),速度能快 5~10 倍,或者直接to_dict("records")转成列表再循环。
2.4 大文件分段处理与内存优化
CSV 动不动就几个 GB 的情况很常见,一次性读进 Pandas DataFrame 会直接把内存打爆。我的经验是分块读取,每次处理 10 万行左右,处理完就释放内存。
chunk_size = 100000 for chunk in pd.read_csv("large.csv", chunksize=chunk_size): # 对 chunk 做清洗、格式化、构建 Document process_chunk(chunk)分块的好处不只是省内存,还能做断点续传——如果第 3 块处理时程序崩了,下次可以从第 3 块接着跑,不用从头再来。配合 tqdm 输出进度条,导入 1 个 G 的 CSV 全程肉眼可见,安全感会强很多。
我还会额外做一次去重。很多业务系统导出的 CSV 里会混着重复行,如果不去重,知识库里的条目会重复,检索时同名记录返回多条,干扰模型判断。去重逻辑很简单:
df = df.drop_duplicates(subset=["primary_key_col"], keep="last")如果没有唯一键,可以选取所有字段做全字段去重,但不建议对包含文本的列做全字段去重,因为文本列里可能有细微差别(比如多了个空格),全字段去重容易误伤。
3. Excel 导入的进阶处理与实操要点
3.1 多 Sheet 与格式信息的处理策略
Excel 和 CSV 最大的区别在于它可以包含多个 Sheet,每个 Sheet 有独立的表头和数据区,有时候还有合并单元格、标题行、备注行。我之前遇到过一份 8 个 Sheet 的报价单,每个 Sheet 的表头结构都不一样,甚至同一个 Sheet 里分了上下两个表格区域,中间隔着几行标题。
针对这种数据,我建议不要试图“智能识别”所有结构,而是建立一个配置化的映射规则。每个 Sheet 手动指定:起始行、表头行、数据结束行、需要跳过的列。用 openpyxl 或 pandas 读取时精确控制范围:
import pandas as pd df = pd.read_excel( "quotation.xlsx", sheet_name="Sheet1", header=2, # 第 3 行是表头 skiprows=[0, 1], # 前两行是标题和副标题 usecols="A:F" # 只取 A 到 F 列 )这个方案虽然需要人工干预,但非常可靠。我见过有人试图用大模型自动推断 Excel 结构,但在复杂格式面前成功率并不高,而且每次运行成本很高。配置化规则是一次投入、长期复用,业务部门改一次格式,你只需要改配置文件里的一行参数。
合并单元格是一个特别容易踩的坑。Pandas 读 Excel 时,合并单元格只有左上角的第一个单元格有值,其余都是 NaN。比如一个“地区:华东”的单元格跨了 5 行,那 5 行里只有第一行有“华东”,其余 4 行是 NaN。我的处理方案是向前填充(ffill):
df["region"] = df["region"].ffill()这样可以保证每条记录都有完整的上下文,不会出现“这条属于哪个地区都不知道”的情况。注意这里要按列填充,而不是全表填充,否则会把无关列的缺失值也填上错误内容。
3.2 公式、条件格式与数据验证的处理预案
Excel 里的公式是另一个麻烦。有些单元格的值不是静态的,而是由公式计算出来的,比如=VLOOKUP(A2, 价格表!A:B, 2, 0)或=SUM(D2:F2)。如果直接用 openpyxl 读,你会拿到公式字符串,而不是计算结果。RAG 知识库要的是最终值,不是公式。我的方案是先用 Excel 应用本身把公式计算结果固化,或者在脚本里用data_only=True读取缓存值:
import openpyxl wb = openpyxl.load_workbook("with_formula.xlsx", data_only=True) ws = wb["Sheet1"]注意,data_only=True读取的是上次 Excel 打开文件时计算并缓存的结果。如果这个文件自生成后从未被 Excel 打开过,缓存可能是空的,返回 None。这种情况我的应对是先用 LibreOffice 的命令行做一次批量转换,强制重算并另存:
soffice --headless --convert-to xlsx --outdir converted/ with_formula.xlsx转换后公式会变成值,再加载就没有 None 的问题了。条件格式对 RAG 基本没意义,直接忽略即可;数据验证(下拉列表)如果只是输入约束,也不影响数据读取,可以跳过。
3.3 Excel 读取的性能优化与类型保真
Excel 文件一大,读取速度瞬间拉胯。一个 50 MB 的 xlsx,Pandas 默认引擎 openpyxl 可能要读 30 秒以上。如果只是读取数据而不关心公式,可以尝试engine="calamine"(如果环境里有)或者先转成 CSV 再处理。我的经验是,纯数据场景下,先用 LibreOffice 把 xlsx 转成 csv,再用 csv 模块按 2.1 节的流程走,速度能快一个数量级。
另外要特别留意数字精度问题。Excel 里的浮点数在二进制存储时本身就是不精确的,如果你把 12.999999 当成 13 来处理,排序还好,但做等值匹配就麻烦了。我的做法是读取后统一做舍入:
import pandas as pd numeric_cols = df.select_dtypes(include="number").columns df[numeric_cols] = df[numeric_cols].round(4)这里取 4 位小数是因为大多数业务数据不需要比 4 位更高的精度,太长的尾数反而可能干扰检索。对于金额列,建议保留 2 位小数,和业务口径保持一致。
4. LlamaHub 连库实战:让 RAG 直连实时数据
4.1 LlamaHub 生态与数据库连接器概览
LlamaHub 是 LlamaIndex 官方的数据连接器仓库,里面集成了几百个数据源的加载器。连数据库时,我们一般用DatabaseReader或SQLDatabase封装类,它们会把数据库表映射成 Document 或可查询的结构化源。核心思路是:不把整张表灌进向量库,而是保留 SQL 查询能力,让 LLM 在需要时生成 SQL 去数据库里查,查到的结果再交给生成模型做回答。
这样做的好处很明显:数据实时、查询精确、不占向量库空间。缺点是每次查询都要走一次数据库连接和 SQL 执行,延迟比纯向量检索高;而且 LLM 生成 SQL 有失败概率,需要加校验和兜底。
我常用的连接器组合是SQLDatabase+NLSQLTableQueryEngine,一个负责连库、一个负责把自然语言翻译成 SQL。还有一个老牌工具SQLTableRetrieverQueryEngine,适合关联多表查询的场景,返回的上下文更丰富,但检索链路也更复杂。如果项目刚起步,建议从NLSQLTableQueryEngine开始,收敛面小,出错好排查。
4.2 数据库连接配置与表结构映射
先说配置。无论用什么连接器,第一步都是建立数据库连接。以 PostgreSQL 为例:
from sqlalchemy import create_engine engine = create_engine( "postgresql+psycopg2://user:password@localhost:5432/dbname" )这里有几个细节要注意:一是连接字符串里的密码如果包含特殊字符,必须做 URL 编码,否则解析会出错;二是 生产环境千万不要把密码硬编码在脚本里,建议用环境变量或密钥管理服务读取。从 LlamaHub 这一侧没有额外限制,但要确保数据库账号有读权限,并且网络连通性没问题。
表结构映射方面,连接器需要知道你有哪些表、每张表有哪些列。LlamaIndex 的SQLDatabase会自动从数据库 catalog 里读取表信息,生成一份 schema 描述。但自动读取的 schema 不一定直观,比如列名是c1、c2的隐藏命名,模型根本看不懂。我建议先手动补一层列注释:
COMMENT ON COLUMN orders.order_id IS '订单号,唯一标识每笔订单'; COMMENT ON COLUMN orders.amount IS '订单金额,单位:元';有了注释之后,LLM 生成 SQL 的准确性会显著提升,因为它能明白amount指的是金额而不是数量。这类注释对普通用户不可见,但对 LLM 理解 schema 是刚需。
4.3 从自然语言到 SQL 的生成与校验
LlamaHub 连数据库的核心价值是“自然语言 → SQL”,这个过程如果是黑盒,风险很大。我强烈建议在生成 SQL 之后加一道独立的校验层,至少要做三件事:语法校验、表名列名白名单校验、空结果对应逻辑。
语法校验最简单的方式是把生成的 SQL 丢给数据库做 EXPLAIN,不真正执行,也能发现大部分语法错误:
try: with engine.connect() as conn: result = conn.execute(text(f"EXPLAIN {sql}")) # 语法没问题 except Exception as e: # 语法有问题,记录日志并返回兜底提示 fallback_response = "我暂时无法从数据库中找到相关信息,请尝试换个问法。"白名单校验更重要。LLM 可能生成SELECT * FROM users; DROP TABLE users;这种恶意 SQL(虽然大多数情况下只是幻觉写出不存在的列名),必须在执行前拦截。我的做法是先用正则抽取所有表名和列名,再和数据库 schema 做比对:
import re tables_used = re.findall(r"from\s+([a-z_0-9]+)", sql, re.IGNORECASE) cols_used = re.findall(r"select\s+(.*?)\s+from", sql, re.IGNORECASE) allowed_tables = {"orders", "customers", "products"} if any(t not in allowed_tables for t in tables_used): raise ValueError("检测到非授权表名,已终止执行")最后是空结果处理。自然语言问“上个月卖了多少台”但生成的 SQL 查出来是空,这时不能直接告诉用户“没数据”,而应该回退到更宽松的查询条件再试一次,比如去掉时间过滤条件、模糊匹配商品名称等。这个回退逻辑放在连接器外层做,不要放在连接器内部,方便单独调试。
4.4 多表关联查询与上下文增强
单表查询只是入门,业务系统里真正有价值的是多表关联。比如“近 30 天华东区客户投诉最多的商品排名”,涉及订单表、区域表、投诉表三张表。SQLTableRetrieverQueryEngine在这类场景里表现更好,因为它会先做一次“表检索”,判断哪些表与当前问题相关,再生成跨表 SQL。
不过我实测下来,三张表以上的关联查询,LLM 生成正确 SQL 的概率会明显下降,尤其遇到同名字段(比如多个表都有created_at)时容易搞错关联条件。我的对策是给表名和字段名加前缀别名,比如orders.created_at,让模型不容易混淆。另一个对策是在数据库里建好视图(View),把高频关联查询固化成一个虚拟表,LLM 只需查询视图,少一层 JOIN,成功率直线上升。
上下文增强方面,查询结果返回后直接丢给 LLM 也行,但我习惯再补一层业务口径说明。比如把“金额单位是元”这种信息拼接进 prompt,避免 LLM 把 47200 解释成 4.72 万。这一步不复杂,但对回答质量的影响非常明显。
5. 实操环境准备与完整项目流程整合
5.1 环境依赖与版本选择
我建议直接用 Python 3.10 以上版本,搭配 venv 或 conda 环境隔离依赖。核心依赖清单如下:
pip install llama-index pandas openpyxl sqlalchemy psycopg2-binary如果有 Google Sheets 或其他在线表格需求,再加装google-auth、gspread等。LlamaIndex 版本迭代很快,API 变动频繁,我建议锁定主版本号安装,比如 llama-index==0.10.x,避免升一个大版本后代码大面积报错。
嵌入模型和 LLM 的配置同样关键。我习惯把 key 放到环境变量里,避免硬编码。如果是要私有化部署,也可以选择本地模型比如 embedding 用 bge-large-zh,生成用 ChatGLM 或 Qwen,这样数据不出内网,安全性更高。
5.2 从 CSV/Excel 到向量库的完整流水线
把 CSV 和 Excel 导入向量库,完整流水线应该是这样:读取 → 清洗 → 字段类型规范化 → 构建 Document → 切块 → 嵌入 → 写入向量库。切块参数用我前面 2.3 节的行级方案,但嵌入和写入向量库时,有几个参数需要特别注意。
chunk_size建议设置得比普通文本大一些,因为一行 CSV 拼接后的字符串往往接近 300~500 字符,设太小的 chunk_size 会把一条记录截成两半。overlap在表格场景里意义不大,甚至可以设为 0,因为表格记录之间没有文章那种上下文衔接。写入向量库时建议用批量模式而不是逐条插入,比如一次性 insert 1000 条,耗时能减少 60% 以上。
from llama_index.core import VectorStoreIndex, Settings from llama_index.core.node_parser import SentenceSplitter Settings.embed_model = embed_model splitter = SentenceSplitter( chunk_size=600, chunk_overlap=50, paragraph_separator="\n" ) nodes = splitter.get_nodes_from_documents(records) index = VectorStoreIndex(nodes)这里我再多提一句:SentenceSplitter是按文本块切分的,它不会感知 CSV 的行边界。所以如果你把多行 CSV 拼成一个长文本再交给 splitter,它依然可能切坏。我的保险做法是自己先把每一行构建成独立 Document,每个 Document 的长度控制在 chunk_size 以内,再让 splitter 直接返回原样,不要二次切分。具体来说,给 splitter 设置一个大 chunk_size(比如 4096),让它变成“不切块”的透传模式:
splitter = SentenceSplitter(chunk_size=4096, chunk_overlap=0) nodes = [Document(text=doc.text, metadata=doc.metadata) for doc in records]这样每条记录作为一个节点进入向量库,检索时一条记录就是一个整体,不会再被拦腰切断。
5.3 LlamaHub 连库的端到端调试步骤
连库调试我分成三步,每一步都有明确的验证信号。
第一步:验证数据库连接。执行一句SELECT 1,能拿到返回就说明网络、账号、驱动都正常。这一步挂了就什么都别做,先排查网络连通和数据库白名单。
第二步:验证 schema 读取。让SQLDatabase对象打印出所有表和列的摘要,人工确认关键表都在,没有缺列。如果发现缺失,看看是不是账号权限不足,或者数据库里有多个 schema,默认只读了 public。
第三步:自然语言转 SQL 测试。用三五条典型查询样本过一遍流程,比如“列出所有客户名”“最近一周的订单总量”。不仅要看 SQL 生成是否正确,还要看查询结果能否正确格式化给 LLM。我通常会把中间产物打印出来,包括生成的 SQL、查询耗时、返回记录数。这一步是排查“回答离谱”的第一现场。
5.4 日常运行与增量更新的维护节奏
表格数据不会永远是那个表,尤其是数据库,每天都在变。我的维护策略是分级刷新:CSV/Excel 这类静态文件,用定时任务或事件触发重新导入即可,频率取决于业务更新周期;数据库则不需要固定刷新,因为它走的是实时 SQL 查询。唯一要注意的是 LlamaIndex 里缓存的 schema 描述可能过期,如果数据库表结构变更了,一定要刷新 schema 缓存,否则 LLM 还在拿旧结构生成 SQL,会频繁报错。
为了保证知识库可用性,我会给每个数据源打标签(source_tag),向量库里每条记录都能回溯到原始的 CSV 行号或数据库主键。这样一旦发现某条数据有误,可以直接定位并删除,不用全量重建。
6. 常见问题与排查技巧实录
6.1 表格切块错乱与检索丢失上下文
症状:检索结果看起来像“断章取义”,或者回答里出现了一半字段。原因多半是切块时把行掐断了。我在 5.2 节给出的透传方式是最简单可靠的解法,但如果你必须用小 chunk_size,务必将 chunk_overlap 设置成至少一条记录的 1/3 长度,让分割器有缓冲。
另外要检查嵌入模型的最大输入 token 长度。有的模型只支持 512 token,但你的行拼接文本超过 800 token,嵌入时会被截断,相当于后半段字段全丢了。这种情况要么换大窗口模型,要么把字段裁剪到只保留核心列。
6.2 Excel 读取乱码与空数据
已经处理过 BOM 了还乱码,大概率是编码判断错了。chardet 在小样本上经常误判,尤其是中文 GBK 和 Big5 混排的场景。我的经验是多采样几段再判断,或者在判断结果不确定时回退到 GB18030,它是 GBK 的超集,兼容面更大。
空数据问题则往往来自data_only=True拿不到公式缓存值。前面提过用 LibreOffice 重算。如果公司环境不允许装 LibreOffice,另一个办法是用 Excel COM 组件(Windows)打开文件并保存一次,强制刷新缓存,但这就依赖桌面环境了,不适合服务器跑批。
6.3 LlamaHub 连库时 SQL 生成错误
SQL 生成错误我统计过,80% 是表名或列名拼错了,跟大小写和命名规则息息相关。解决方法是把 schema 描述里的表名列名做成全小写规范化,并在提示词里强调“严格使用给定的表名和字段名,不要随意改名”。剩下 20% 是多表 JOIN 条件写错,建议用视图来规避。
还有一个隐藏问题:有些数据库的连接超时时间很短,长查询执行到一半连接断了,异常信息又不够直观。我建议把所有数据库操作统一包一层超时控制:
from sqlalchemy import text from contextlib import contextmanager @contextmanager def db_query(engine, sql, timeout=10): with engine.connect() as conn: conn.execute(text(f"SET statement_timeout = {timeout * 1000}")) yield conn这个对 PostgreSQL 有效,MySQL 可以用MAX_EXECUTION_TIME,只是语法略有差异。设置超时既保护数据库,也避免 RAG 接口长时间无响应。
6.4 常见问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| CSV 首列列名带“\ufeff” | 文件带 BOM | 用text.lstrip("\ufeff")去除 |
| 日期列被识别为字符串 | 未指定 dtype | 用pd.to_datetime统一格式 |
| 检索结果全是片段 | chunk_size 过小切断了行 | 按行构建 Document,不二次切分 |
| Excel 某列全为空 | 合并单元格 | 使用ffill()向前填充 |
| Excel 读取返回 None | 公式缓存为空 | 用 LibreOffice 重算转格式 |
| LLM 生成 SQL 表名错误 | schema 描述不清晰 | 补列注释 + 白名单校验 |
| 数据库查询超时 | 执行计划不佳 | 加数据量限制 + 超时控制 |
| 多表关联出现假数据 | JOIN 条件写错 | 建视图简化查询 |
6.5 我踩过的一些坑与经验总结
做表格导入最怕的是“表面成功、实际有毒”:CSV 导入后没有报错,但向量检索出的内容张冠李戴;Excel 读取没有任何异常,但公式缓存让数字全部变成 None;LlamaHub 连库没有报错,但 SQL 查出来是同名不同表的数据。每一步都要有验证信号,不能只依赖代码跑通。我的经验是:导入后先做一轮抽样人工检查,比如随机挑 10 条记录打印出来看看格式对不对;连库后先用 3~5 个真实问题做集成测试,确认回答质量再上线。
另外,日志真的很重要。每次导入、每个查询,我都把 CSV 文件名、行号、构建的文本内容、数据库连接标识、SQL 语句全部记录下来。这样一旦线上出了问题,定位到具体批次数据,处理起来会快得多。
基于我自己的项目经验,表格数据导入 RAG 没有银弹,核心是两种手段的取舍:CSV/Excel 走向量检索,适合语义匹配、内容探索;LlamaHub 连数据库走 SQL 生成,适合精确查询、实时数据。最稳的架构是两者结合:向量索引负责召回候选,数据库负责精排过滤。你先按这个思路跑一版最小可用系统,等对自家数据的查询模式摸清了,再决定要不要往混合架构压重注。