搞RAG落地的人迟早要面对表格数据。文本类的PDF和网页好处理split一下丢进向量库就行但一到CSV、Excel或者数据库里的结构化数据问题就来了直接按行切分语义碎成一地整表塞进去token不够用就算硬塞进去用户问上季度华东区销售额最高的SKU是什么召回的根本不是那行数据。这篇是RAG数据导入与解析全攻略的第三篇专门聊表格与数据库导入从CSV、Excel到LlamaHub连库实战把清洗、分割、语义补全和库表直连整套流程讲透。适合正在搭RAG知识库的开发者尤其是被结构化数据折腾过的人。1. 表格数据在RAG里为什么这么难搞1.1 结构化数据与纯文本的天然鸿沟先想清楚一个问题为什么PDF和网页好处理表格难处理因为RAG的核心逻辑是语义相似度检索而表格本质上是一个二维结构它的语义不在单个单元格里而在行列关系、表头定义、跨行跨列的上下文里。你把一张销售明细表按行切块每一块都是2024-03-12A001华东SKU-882312800这东西丢给embedding模型和一堆乱码区别不大。更麻烦的是embedding模型是用自然语言训练的它对字段名分隔符值这种格式的编码能力很弱。我曾经测过一张50列的宽表按行切成块丢进向量库后召回率只有两成左右排在最前面的反而是一些格式相似的噪声块。表格数据必须经过语义化预处理让它从二维结构变成一段段有上下文、有因果的自然语言文本才符合RAG的检索逻辑。这也牵扯到RAG知识库和结构化知识库的区分。纯文本RAG适合做文档问答、制度查询它擅长的是模糊语义匹配而表格、数据库这类结构化知识本质上是精确查询逻辑计算的场景比如查某订单的金额汇总某个月的销量。把结构化数据硬塞进向量库里做模糊检索方向就错了。正确的做法是两条腿走路高频、问答型的内容走向量检索精确、计算型的内容走数据库查询或者表格转文本后再向量化。1.2 三条导入路线的选型逻辑表格和数据库导入实际落地时无非三条路CSV、Excel、数据库直连。我见过不少团队一上来就想搞最重的方案结果被运维成本和延迟拖死。选哪条路取决于数据的三个特征更新频率、数据规模、查询复杂度。CSV适合一次性导入的静态数据比如历史报表、离线导出的清单清洗成本低最适合做文本化后进向量库。Excel适合手工程度高的数据因为里面常有格式、批注、多Sheet处理起来要细致一些但胜在业务人员最熟悉。数据库直连适合两类场景一是数据量大到导出CSV不现实二是数据更新频繁需要实时同步。数据库直连还有一个隐藏优势——可以直接让LLM生成SQL去查询这条路根本不走向量召回准确率远高于RAG。实操中我建议按静态数据走CSV、半结构化走Excel、动态数据走数据库来分最省力气。很多人喜欢把Excel读出来后转成CSV再走管线看着没问题但Excel里的多Sheet、公式、数据类型会在转换中丢失信息等踩了坑再回头改就慢了。2. CSV文件导入清洗、编码与块分割实战2.1 CSV解析前必须做的三件事CSV看起来最简单实际坑最多。第一个坑就是编码。中文字符集各种历史遗留问题UTF-8、GBK、GB18030混着来。直接从网上下载的CSV我真切遇到过用pandas读出来全是乱码的情况。处理方式很简单读取之前用chardet检测编码不要硬指定。import chardet with open(sales_data.csv, rb) as f: raw f.read(10000) result chardet.detect(raw) encoding result[encoding] print(f检测到编码: {encoding})第二个坑是分隔符。CSV不一定是逗号分隔分号、制表符、竖线都常见。你写死分隔符遇到那种字段值里自带逗号的数据就崩了。推荐直接用pandas的read_csv它会根据前几行自动推断分隔符或者用csv.Sniffer手动检测。第三个坑是脏数据。CSV里的空行、列数不一致、全角空格、日期的各种变体都会直接影响后续分割质量。我的习惯是读取后先做一轮标准化统一列名、删空行、填充关键字段、日期统一格式。import pandas as pd df pd.read_csv(sales_data.csv, sep,, encodingutf-8) df.columns [col.strip().replace( , _) for col in df.columns] df df.dropna(howall) df df.fillna() df[date] pd.to_datetime(df[date]).dt.strftime(%Y-%m-%d)很多人在这一步偷懒觉得能读出来就行。但清洗不干净后面分割和向量化的效果一定打折扣这是我在多个项目里验证过的规律。2.2 表格块分割策略按行分组、按列拆解清洗完的CSV要进RAG接下来就是分割。社区里流传的csv文件分割神器2.0核心思路其实就一句话不要按行切成原子碎片而是按语义块切。具体怎么做我总结为三条。第一如果表是明细数据一行一个实体/一条记录比如订单表、用户表建议按行分组切块每组5-10行。这样每块文本里既有结构又有样本值检索时命中率高。第二如果表列数太多超过20列整行切块会让每个块的内容非常稀疏embedding效果极差。这种建议拆表把相关联的列拆成多个子表或者按列组转成多个文本块。第三如果表有层次结构如表头有两行、有合并单元格先把表头拍平再分割。def segment_csv_by_rows(df, rows_per_block8): blocks [] total len(df) for start in range(0, total, rows_per_block): end min(start rows_per_block, total) sub_df df.iloc[start:end] block_text sub_df.to_csv(indexFalse) blocks.append({ text: block_text, metadata: { start_row: start, end_row: end, block_type: csv_row_block } }) return blocks这个函数看着简单但有个细节很多人注意不到metadata里要放start_row和end_row。因为后面用户命中某个块之后如果要反查原始数据或者做分页展示没有行列范围信息会非常痛苦。如果表格的列数特别多比如那种一百多列的设备参数表我建议做转置切分。把列转成行每一行变成参数名: 参数值的文本块然后合并成段落。表格在embedding模型眼里是一行一个样本转置之后变成一个特征一个样本语义密度大幅提升。2.3 表格转自然语言给数据配旁白分割只是第一步真正提升召回率的是把表格块转换成自然语言描述。这一步的核心思路是让每一块文本形成自包含的语义单元。比如2024-03-12A001华东SKU-882312800直接进向量库检索效果很差但改成2024年3月12日订单号A001华东区域商品SKU-8823销售额12800元这就接近自然语言了。实际操作中我会按表的类型选模板。明细表用逐行描述模板把表头拼进每个值里宽表用整体描述关键列提取模板先写一句表级别的总述再列若干重点行。这块代码因表而异不追求通用但可以总结一个套路def block_to_natural_language(df_block, table_title销售数据表): lines [] lines.append(f{table_title}共{len(df_block)}条记录) for _, row in df_block.iterrows(): parts [] for col in df_block.columns: val row[col] if val ! and val is not None: parts.append(f{col}为{val}) lines.append(.join(parts)) return \n.join(lines)做完这步CSV才算真正可以进向量库了。3. Excel文件导入格式处理与工具链选择3.1 Excel解析的隐藏坑多Sheet、合并单元格与公式缓存Excel比CSV麻烦得多因为它不仅存数据还存格式。我在处理Excel时遇到的第一个问题是多Sheet。一个工作簿里10个Sheet业务含义完全不同——Sheet1是订单明细Sheet2是汇总Sheet3是客户信息如果你直接用pandas的read_excel默认Sheet读取90%的内容直接丢了。第二个问题是合并单元格。pandas的read_excel会把合并单元格解析成NaN除了第一个单元格有值其他全是空。这种表你要是直接丢进分割逻辑出来的文本全是null。解决办法是读取时显式处理合并单元格import openpyxl wb openpyxl.load_workbook(report.xlsx, data_onlyTrue) ws wb[Sheet1] merged_ranges list(ws.merged_cells.ranges) for merged in merged_ranges: value ws.cell(merged.min_row, merged.min_col).value for row in range(merged.min_row, merged.max_row 1): for col in range(merged.min_col, merged.max_col 1): ws.cell(row, col).value value这样合并单元格的所有区域都有值了后续转DataFrame才干净。第三个问题是公式。Excel里的单元格可能是公式比如SUM(C2:C10)。openpyxl的data_onlyTrue只能拿到公式的缓存值如果这个文件是从外部系统生成、没被Excel打开过缓存值可能不存在。稳妥的做法是读两次第一次带data_onlyTrue拿值拿不到的值再用公式字符串兜底至少别让None进RAG。还有一个很多人在Windows上踩过的坑程序处理完Excel文件后任务管理器里的Excel进程还在。这通常是因为用pywin32的COM对象操作Excel用完没release。在服务端跑批处理时我的原则是能用openpyxl绝不用COM避免Excel进程泄漏拖垮服务器。3.2 Excel导入实操从库表到文本块Excel导入的完整流程我建议按这个顺序走。先把工作簿的所有Sheet名读出来逐个Sheet判断是否要导入。判断依据很简单Sheet里有数据且和业务主题相关就导入空白Sheet或者图表Sheet直接跳过。然后对每个Sheet做标准化合并单元格填充、空行删除、列名统一。这里有个经验很多业务Excel模板的表头在第2行甚至第3行上面是标题行XX公司月度报表直接用第1行做列名会全错。所以读取的时候要指定header行且要容忍表头里带换行符的情况。import pandas as pd def load_excel_sheet(filepath, sheet_name, header_row0): df pd.read_excel(filepath, sheet_namesheet_name, headerheader_row) df df.dropna(howall) df df.fillna() return df完成标准化后就可以复用CSV那套分割逻辑了。但Excel有一个CSV没有的额外信息——Sheet名。Sheet名本身就是极好的metadata比如华东区门店销售明细2024年Q1供应商对账单这些名字要加进每个块的metadata里检索时能起到天然的过滤作用。3.3 Excel文件的批量导入与增量管理不少人处理Excel时只写一次性脚本跑完就扔。但真实业务里Excel是持续更新的——每月一张新表格式几乎一样。这时候我建议做一个简单的增量管理用文件路径加Sheet名做唯一键记录每个文本块的哈希值下次扫描时对比哈希变了才重新导入没变就跳过。这样既省token费又能保证知识库跟得上文件的更新节奏。我用过一套很朴素的方案维护一个SQLite表字段是file_path、sheet_name、block_hash、updated_at。每次导入前查询哈希相同就跳过。这套方案扛住了几十个Excel文件的月度更新没出过问题。如果你懒得搭至少也要在metadata里带上文件的修改时间靠它决定是否重新导入。4. LlamaHub连库实战数据库导入的完整方案4.1 数据库直连 vs 导出CSV何时选择直连很多人觉得数据库不就是查出来再走CSV流程吗这个思路在数据量小的时候没问题但数据量大了就扛不住。我做过一个项目业务库里有几百张表最重要的几张表都是千万级行数。把全表导出成CSV再导入向量库光是导出就耗时几个小时而且业务库的生产环境根本不允许你随便全表扫描。数据库直连的意义在于两点。一是按需加载你可以只读取需要的那几列、那几行而不是全表搬走。二是支持实时查询如果用户的问题需要精确数值可以让LLM直接生成SQL查库这属于从RAG检索到库表计算的延伸。我比较推荐的做法是知识问答类的走向量库数值计算类的走SQL生成两条链路并存。4.2 LlamaHub数据库读取器选型与原理LlamaHub是LlamaIndex的官方组件仓库里面有各种数据加载器。做数据库导入最常用的是DatabaseReader。这个读取器不直接返回一堆数据行而是做两件事第一根据你提供的SQL查询语句把结果读出来第二根据表结构信息生成表的Schema描述节点。为什么要生成Schema描述节点这就是LlamaIndex设计上比较聪明的地方。每个表节点是表的DDL结构描述比如表名、列名、列类型、外键关系。LLM拿到这个结构信息后才能根据用户的问题生成合理的SQL。如果只有数据没有结构那LLM连表里有哪些列都不知道自然查不准。数据库读取器还能配合SQLTableNodeMapping和SQLDatabaseNodeCache一起用前者负责把表结构映射成节点后者负责缓存这些节点避免重复读取。这样数据库导入后检索时既能召回表结构信息也能返回具体的数据行两个层面配合效果远好于单独把数据行向量化。4.3 连库实战从SQLite到MySQL的导入全流程下面给出一套我实际用过多次的流程以SQLite为例MySQL只需要换数据库连接串。需要安装依赖pip install llama-index llama-index-readers-database sqlalchemy然后写连接和读取逻辑from sqlalchemy import create_engine from llama_index.core import SQLDatabase from llama_index.readers.database import DatabaseReader from llama_index.core.utilities.sql_wrapper import SQLDatabaseNodeWrapper engine create_engine(sqlite:///business.db) sql_database SQLDatabase(engine, include_tables[orders, customers, products]) reader DatabaseReader( sql_databasesql_database, engineengine, )这里include_tables要重点说一句。不要把所有表都塞进去只挑RAG用得上的。原因有二一是表的数量越多Schema节点越多检索时容易互相干扰二是很多业务库里有一堆日志表、临时表全导进来纯粹是噪音。生产环境里表的白名单一定要和业务方确认清楚。之后构建节点from llama_index.core.node_parser import SimpleNodeParser from llama_index.core.schema import TextNode table_nodes [] for table_name in sql_database.get_table_names(): table_schema sql_database.get_table_columns(table_name) table_context f表名: {table_name}\n列结构:\n for col in table_schema: table_context f - {col[name]}: {col[type]}\n table_context 该表用于记录订单、客户和商品等业务信息可用于查询销售额、客户分布等。 node TextNode(texttable_context, metadata{table_name: table_name, node_type: table_schema}) table_nodes.append(node)这里给每一个Schema节点都手动拼了一段业务语义描述比如该表用于记录订单、客户和商品等业务信息。这个补充特别重要。因为数据库里的表名和列名往往是缩写比如od_cust_idLLM光看结构根本不知道它代表什么。你手动补充的这段业务旁白直接决定后续SQL生成的质量高低。数据行的部分我建议先抽样再导入。比如每张表抽取最近1000行按列转成自然语言描述后向量化。注意这里千万不能全表翻入向量库除非你有专门的硬件预算。正确姿势是全量数据留在数据库向量库里只放有代表性的样本表结构描述真需要精确值时走查库链路。4.4 连接池与超时服务端连库的稳定性保障最后补一个服务端连库的坑。很多人在本地脚本里连数据库没问题部署到服务上就频繁报错。原因通常是连接没有复用每次调用都新建连接数据库端的连接数被打满。我用的方案是让SQLAlchemy的engine维持一个连接池性能够用且稳定。同时加事务隔离级别engine create_engine( mysqlpymysql://user:passwordhost:3306/business_db?charsetutf8mb4, pool_size5, max_overflow10, pool_timeout30, pool_recycle1800, ) with engine.connect() as conn: conn.exec_driver_sql(SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED)连接池的池大小要根据并发量调。我曾经图省事把pool_size设成100把业务库连接数直接打满差点出事故。生产环境里池大小5-10、max_overflow 20以内是保守的选择。5. 结构化数据导入后的质量验证与调优5.1 召回测试表格知识库的专项评测表格数据导入完怎么判断效果到底行不行常规的RAG评测通常是文档问答对但表格数据要单独测。我的做法是构造表格专项问答集覆盖几类典型问题单值查询、多条件筛选、聚合计算求和/计数、排行类问题、模糊描述问题。问题类型示例期望召回单值查询订单A001是什么时候创建的命中对应行多条件筛选华东区3月份的销售额是多少命中聚合相关块排行类销量最高的前5个SKU有哪些命中明细块或汇总块模糊描述哪个客户最近投诉过物流慢命中备注/标签列每个类别准备5个问题一共25个左右跑一遍检索看召回的块是否包含答案。如果单值查询都召回不到大概率是表格转文本环节出了问题如果聚合类查不到多半是缺少汇总表或者块切分太碎。5.2 常见问题速查表乱码、切分乱、召回差做表格导入这几篇下来我遇到的典型问题列个表基本覆盖80%的情况。现象根因解决方案导入后文本乱码编码识别错误读取前用chardet检测统一转UTF-8某列全是空/NaN合并单元格未展开openpyxl先填充合并区域再转DataFrame召回结果全是无关数据表格未转自然语言给每个块配表头描述和表级总述聚合问题回答不了向量库里没有汇总型文本单独生成汇总表文本配一张表颗粒度概览Excel进程占用文件COM未释放换成openpyxl或者显式quit和释放数据库导入极慢全表扫描改成分批取样Schema描述节点多Sheet只导入了第一个未循环Sheet逐个Sheet读取Sheet名写入metadata检索到块但答案边界不清晰块内多行语义重叠降低每块行数或给每行添加序号标志有一个坑很值得单独提矢量化后很多人发现文本块互相重叠严重几行之间内容重复导致检索结果一团糟。这通常发生在按行分组时相邻块的头尾行在语义上没断开。解决方法是设置行与行之间的主键锚点每组都带上一个唯一业务键比如订单号、用户ID这样即使text里有重复metadata里的主键也能帮你做去重和过滤。5.3 持续更新结构化数据的增量同步策略表格和数据库和静态文档不一样它们是会变的。如果知识库不做增量更新今天导入的数据版本和业务真实状态可能已经差了很远。我给团队定的策略是文件级哈希定时扫描CSV和Excel用文件修改时间加哈希判断是否重导数据库表则用更新时间字段或行数变化判断。触发更新后不需要重建整个索引只替换变化的节点即可。实际操作里用LlamaIndex的话可以检索时做元数据过滤按文件名或表名过滤节点版本避免出现新旧数据同时命中的问题。这套做法我们上线后跑了一个季度知识库的召回准确率保持稳定。我个人做表格导入几个项目下来最大的体会是表格数据进RAG成败往往在预处理而不是模型。CSV的编码、Excel的合并单元格、数据库的表结构描述每一步都有它自己的坑你在这个环节多花一小时后面调优就能少花十小时。尤其是数据库导入想清楚哪些表走向量、哪些表走SQL生成比优化embedding参数重要得多。这套流程跑顺之后表格类问答知识库的落地速度会快一大截后续再扩展其他格式也会顺手不少。
阅读完成 · 觉得有帮助?