十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RAG 系统中 Excel/表格数据的正确处理方式

RAG 系统中 Excel/表格数据的正确处理方式 RAG 系统中 Excel/表格数据的正确处理方式从向量检索到 Text-to-SQL 的架构演进文章目录RAG 系统中 Excel/表格数据的正确处理方式从向量检索到 Text-to-SQL 的架构演进前言一、问题复现Excel 走 RAG 链路的灾难1.1 当前链路1.2 一个典型的失败案例1.3 根因分析二、方案对比临时表 vs 永久表2.1 方案 A临时表 Text-to-SQL2.2 方案 B永久数据库表✅ 推荐2.3 对比总结三、架构设计与 RAG 知识库隔离3.1 为什么必须隔离3.2 整体架构图3.3 模块划分四、核心实现设计4.1 数据源注册表4.2 动态建表的安全约束4.3 Schema 推断4.4 SQL Tool给 Agent 用4.5 Agent 工厂改造多 Tool 支持五、进阶PDF/DOCX 中的表格如何处理5.1 PDF 中的表格 vs Excel 文件的本质区别5.2 三种处理策略策略 A作为 Chunk 的一部分向量化默认策略 B提取建表和 Excel 一样策略 C智能分层处理✅ 最终推荐5.3 表格分类规则5.4 数据密集型表格的处理流程六、Text-to-SQL 查询流程详解6.1 完整流程6.2 多 Tool 协同示例七、总结7.1 核心原则7.2 最终架构一览7.3 避坑清单前言在构建 RAG检索增强生成系统时很多人会踩一个坑把 Excel 表格和 PDF/DOCX 文档一视同仁全部丢给 MinerU 解析成 Markdown再切片向量化存入 Milvus。这样做会导致一系列严重问题❌ 无法做聚合操作SUM、AVG、COUNT❌ 无法排序、比较、筛选❌ 浪费 LLM Token 去理解纯数字文本❌ 查询精度极低产品A 3月销售额这种精确问题答不上来本文将完整梳理这个问题的根因分析、方案对比、架构设计以及最终的落地方案。一、问题复现Excel 走 RAG 链路的灾难1.1 当前链路Excel 文件 → MinerU 解析 → MarkdownHTML table→ 切片 → Embedding → Milvus 向量存储1.2 一个典型的失败案例用户上传了一份2024年Q1销售报表.xlsx产品1月2月3月合计产品A12,50015,80018,20046,500产品B8,3009,10011,40028,800产品C22,00019,50025,30066,800用户提问“Q1 各产品总销售额排名”RAG 系统的回答语义检索返回了可能相关的文本片段LLM 试图从模糊的 chunk 中拼凑答案——结果要么答错要么回答知识库中未找到相关内容。1.3 根因分析本质原因Excel 是结构化数据应该走 SQL 路径而不是 RAG 语义检索路径。问题类型需要的操作向量检索能力“Q1 各产品总销售额排名”SUM GROUP BY ORDER BY❌ 无法聚合“产品A 3月的销售额”精确查找WHERE 产品A AND 月份3月❌ 语义模糊匹配“销售额前10的产品”ORDER BY LIMIT❌ 无法排序“Q1 和 Q2 销售额对比”跨表/跨行计算❌ 无法计算“公司退货政策是什么”语义理解✅ 向量检索擅长结论不是所有数据都适合向量化。结构化数据需要结构化的查询方式。二、方案对比临时表 vs 永久表2.1 方案 A临时表 Text-to-SQL上传 Excel → 存储原始文件 → 提取文件名/表头 ↓ 用户提问 → LLM 生成 SQL → 创建临时表 → 导入数据 → 执行 SQL → 返回结果优点不污染数据库数据随用随弃致命缺点缺点说明每次查询都要重新导入10MB 的 Excel 每次CREATE TEMP TABLE COPY延迟 2-5 秒无法跨查询复用用户连续问 3 个问题数据要导入 3 次无索引临时表没有索引大表查询慢并发问题多用户同时查询同一文件要创建多份临时表生命周期管理复杂临时表何时清理会话结束超时2.2 方案 B永久数据库表✅ 推荐上传 Excel → 解析 Schema → CREATE TABLE永久→ 批量 INSERT → 建立索引 ↓ 用户提问 → LLM Text-to-SQL → 直接查询永久表 → 返回结果优点优点说明零延迟查询数据已在数据库中直接 SQL毫秒级支持索引对高频查询列建索引大表也快数据可复用一次导入N 次查询支持复杂查询JOIN、子查询、窗口函数、聚合全部可用生命周期清晰跟随data_source的del_flag删除文档时 DROP TABLE与现有架构一致已有 PostgreSQL不需要额外组件需要注意的问题问题解决方案动态建表的安全风险表名用excel_前缀隔离列名做白名单校验Schema 推断准确性用 pandas 读取前 N 行推断类型支持人工修正多 Sheet 处理每个 Sheet 一张表表名excel_{doc_id}_{sheet_name}存储空间Excel 数据通常不大 100MBPostgreSQL 完全能承载2.3 对比总结维度临时表方案永久表方案推荐查询延迟高每次导入 2-5s低直接查询 ms 级索引支持❌ 无✅ 可建索引并发支持❌ 多份临时表✅ 共享同一张表生命周期复杂超时/会话简单跟随文档删除存储开销低临时中永久但可控实现复杂度中低三、架构设计与 RAG 知识库隔离3.1 为什么必须隔离维度RAG 知识库Excel/结构化数据数据类型非结构化文本PDF/DOCX/图片结构化表格数据存储Milvus 向量库 rag_chunk 表PostgreSQL 业务表检索方式语义相似度Embedding精确 SQL 查询适用问题“公司退货政策是什么”“Q1 各产品销售额排名”处理链路MinerU → 切片 → Embedding → Milvusopenpyxl/pandas → CREATE TABLE → INSERTTool 语义knowledge_search(query)execute_sql(query)核心原则RAG 管语义SQL 管数据Agent 负责路由判断。3.2 整体架构图┌──────────────────────────────────────────────────────────────┐ │ 用户上传文件 │ └──────────────────┬───────────────────────────────────────────┘ │ ┌──────┴──────┐ │ 文件类型判断 │ └──────┬──────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ PDF/DOCX/PPTX/图片 XLSX/XLS/CSV │ │ ▼ ▼ ┌──────────┐ ┌──────────────┐ │ module_rag│ │ module_data │ │ │ │ │ │ MinerU │ │ 解析 Schema │ │ ↓ │ │ ↓ │ │ 切片 │ │ CREATE TABLE │ │ ↓ │ │ ↓ │ │ Embedding│ │ 批量 INSERT │ │ ↓ │ │ ↓ │ │ Milvus │ │ 存储元数据 │ └──────────┘ └──────────────┘ │ │ ▼ ▼ ┌──────────────────────────────────────────┐ │ Agent (FunctionAgent) │ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ knowledge_search│ │ data_query │ │ │ │ Tool │ │ Tool │ │ │ └───────┬────────┘ └───────┬────────┘ │ │ │ │ │ │ 语义类问题 数据类问题 │ │ 退货政策是什么 销售额前10排名 │ └──────────────────────────────────────────┘3.3 模块划分module_data/ ← 新建结构化数据管理模块 ├── controller/ │ └── data_source_controller.py ← Excel 上传/管理 API ├── service/ │ ├── data_source_service.py ← 业务逻辑解析、建表、导入 │ └── text_to_sql_service.py ← Text-to-SQL 核心 ├── dao/ │ └── data_source_dao.py ← 数据源 CRUD ├── entity/ │ ├── do/ │ │ └── data_source_do.py ← 数据源表 ORM 模型 │ └── vo/ │ └── data_source_vo.py ← 请求/响应模型 └── tools/ └── sql_tool.py ← 给 Agent 用的 SQL Tool module_agent/ ← 现有Agent 模块改造 ├── tools/ │ ├── rag_tool.py ← 现有知识检索 Tool │ └── (sql_tool.py 从 module_data 导入) ├── service/ │ ├── agent_factory.py ← 改造支持注入多个 Tool │ └── agent_service.py ← 改造多 Tool 协同四、核心实现设计4.1 数据源注册表CREATETABLEdata_source(idVARCHARPRIMARYKEY,-- UUIDfile_nameVARCHARNOTNULL,-- 原始文件名file_pathVARCHARNOTNULL,-- 文件存储路径table_nameVARCHARNOTNULL,-- 实际 PG 表名 (excel_{uuid})sheet_nameVARCHAR,-- 原始 Sheet 名column_info JSONBNOTNULL,-- 列信息table_descriptionTEXT,-- LLM 生成的表描述row_countINTEGER,-- 数据行数source_typeVARCHARNOTNULL,-- excel | csv | pdf_tablesource_doc_idVARCHAR,-- 来源文档 IDPDF 提取的表格create_byVARCHAR,create_timeTIMESTAMPDEFAULTNOW(),del_flagCHAR(1)DEFAULT0);column_info示例[{name:产品,pg_type:TEXT,nullable:false,sample:产品A,description:产品名称},{name:1月,pg_type:NUMERIC,nullable:false,sample:12500,description:1月销售额元},{name:2月,pg_type:NUMERIC,nullable:false,sample:15800,description:2月销售额元}]4.2 动态建表的安全约束importreimportuuid TABLE_PREFIXexcel_def_sanitize_table_name(doc_id:str)-str:生成安全的表名固定前缀 UUID杜绝 SQL 注入safe_idre.sub(r[^a-zA-Z0-9],,doc_id)returnf{TABLE_PREFIX}{safe_id}def_sanitize_column_name(name:str)-str:列名清洗保留中文、字母、数字、下划线cleanedre.sub(r[^\w\u4e00-\u9fff],_,str(name).strip())returncleanedorunnamed_column# SQL 安全校验ALLOWED_SQL_KEYWORDS{SELECT,FROM,WHERE,GROUP BY,ORDER BY,HAVING,LIMIT,OFFSET,JOIN,ON,AS,AND,OR,NOT,IN,BETWEEN,LIKE,IS,NULL,ASC,DESC,COUNT,SUM,AVG,MAX,MIN}defvalidate_sql(sql:str)-bool:只允许 SELECT 查询禁止任何写操作upper_sqlsql.upper().strip()ifnotupper_sql.startswith(SELECT):raiseValueError(只允许 SELECT 查询)forbidden{INSERT,UPDATE,DELETE,DROP,ALTER,CREATE,TRUNCATE,EXEC,EXECUTE,GRANT,REVOKE}forkeywordinforbidden:ifre.search(rf\b{keyword}\b,upper_sql):raiseValueError(f禁止的 SQL 关键字:{keyword})# 强制追加 LIMIT防止大结果集ifLIMITnotinupper_sql:sqlsql.rstrip(;) LIMIT 1000returnTrue4.3 Schema 推断importpandasaspddefinfer_schema(file_path:str,sheet_name:str)-list[dict]:读取 Excel 前 100 行推断每列的数据类型dfpd.read_excel(file_path,sheet_namesheet_name,nrows100)columns[]forcolindf.columns:pg_type_pandas_dtype_to_pg_type(df[col].dtype)samplestr(df[col].dropna().iloc[0])ifnotdf[col].dropna().emptyelseNonecolumns.append({name:_sanitize_column_name(col),original_name:str(col),pg_type:pg_type,nullable:bool(df[col].isnull().any()),sample:sample,description:,# 后续由 LLM 补充描述})returncolumnsdef_pandas_dtype_to_pg_type(dtype)-str:pandas 数据类型 → PostgreSQL 类型映射importnumpyasnpifnp.issubdtype(dtype,np.integer):returnBIGINTelifnp.issubdtype(dtype,np.floating):returnNUMERICelifnp.issubdtype(dtype,np.datetime64):returnTIMESTAMPelse:returnTEXT4.4 SQL Tool给 Agent 用fromllama_index.core.toolsimportFunctionTooldefcreate_sql_tool(data_source_ids:list[str]|NoneNone)-FunctionTool: 创建 SQL 查询工具供 Agent 查询结构化数据。 Agent 用自然语言描述问题系统自动 1. 获取相关数据源的 Schema 2. 构建 Text-to-SQL Prompt 3. LLM 生成 SQL 4. 安全校验 执行 5. 返回格式化结果 asyncdefexecute_data_query(query:str)-str:对用户上传的 Excel/CSV 数据执行查询。 当用户的问题涉及数据统计、排名、对比、聚合如求和、平均、最大最小 或需要精确数值时必须调用此工具。 Args: query: 自然语言查询问题 # 1. 获取数据源 Schemaschemasawaitget_schemas_by_ids(data_source_ids)# 2. 构建 Text-to-SQL Promptprompt_build_text_to_sql_prompt(query,schemas)# 3. LLM 生成 SQLsqlawaitllm_generate_sql(prompt)# 4. 安全校验validate_sql(sql)# 5. 执行查询resultawaitexecute_readonly_query(sql)# 6. 格式化返回returnformat_query_result(result)returnFunctionTool.from_defaults(async_fnexecute_data_query,namedata_query,description(查询用户上传的 Excel/CSV 数据。当问题涉及数据统计、排名、对比、聚合或精确数值时使用此工具。参数: query自然语言查询问题),)4.5 Agent 工厂改造多 Tool 支持classAgentFactory:classmethoddefcreate_agent(cls,kb_id:str,collection_name:str,data_source_ids:list[str]|NoneNone,# 新增参数system_prompt:str|NoneNone,)-FunctionAgent:tools[]# 1. RAG 工具始终可用tools.append(create_rag_tool(kb_id,collection_name))# 2. SQL 工具有数据源时才注入ifdata_source_ids:tools.append(create_sql_tool(data_source_ids))# 系统提示也要调整final_promptsystem_promptor_build_system_prompt(has_sql_toolbool(data_source_ids))returnFunctionAgent(nameknowledge_assistant,description基于知识库和数据的智能问答助手,system_promptfinal_prompt,toolstools,llmget_llm(),streamingTrue,)五、进阶PDF/DOCX 中的表格如何处理5.1 PDF 中的表格 vs Excel 文件的本质区别维度PDF/DOCX 中的表格独立 Excel 文件定位文档的一部分有上下文包裹独立的数据源目的通常是摘要/展示给读者看通常是原始数据给分析用完整性往往是汇总后的子集5-10行完整的数据集几百到几万行例子合同里的费用明细表“2024年全年销售明细.xlsx”5.2 三种处理策略策略 A作为 Chunk 的一部分向量化默认MinerU 已经把表格转成了 Markdown/HTML 格式切片时表格自然包含在 chunk 里## 3.2 费用明细 | 项目 | 单价(元) | 数量 | 合计(元) | |------|---------|------|---------| | 设计费 | 50,000 | 1 | 50,000 | | 施工费 | 30,000 | 3 | 90,000 | | 监理费 | 15,000 | 2 | 30,000 | 如上表所示本项目总费用为 17 万元...整块一起向量化用户问设计费是多少时语义检索能命中这个 chunkLLM 直接从表格文本中读取答案。适用场景表格较小 50 行、表格是描述性/摘要性的、用户问的是是什么而不是算一下。策略 B提取建表和 Excel 一样把 PDF 中的表格从文档中抠出来单独建一张 PostgreSQL 表。适用场景表格很大100 行、表格是结构化的数据集、用户经常需要对表格数据做聚合查询。缺点表格脱离了文档上下文丢失语义管理复杂。策略 C智能分层处理✅ 最终推荐核心思路默认走策略 A向量化遇到数据密集型表格时自动升级到策略 B。MinerU 输出 Markdown │ 表格检测 │ ┌────┴────┐ │ 有表格 │ └────┬────┘ │ ┌────┴────────────────────┐ ▼ ▼ 描述性表格 数据密集型表格 20行 50行 有上下文包裹 纯数据无叙述 │ │ ▼ ▼ 保留在 Chunk 中 提取建表到 PostgreSQL → 向量化到 Milvus → 原文替换为摘要 → 通过 sql_tool 查询5.3 表格分类规则defclassify_table(headers:list[str],row_count:int,surrounding_text:str)-bool: 判断表格是描述性还是数据密集型 返回 True 表示数据密集型需要建表 判断依据 1. 行数阈值 50 行 2. 列头含数值关键词金额、数量、合计、%... 3. 周围文本是否缺少叙述性描述 # 规则 1: 行数阈值ifrow_count50:returnTrue# 规则 2: 列头含数值关键词占比超过 50%numeric_keywords[金额,数量,合计,总计,单价,比例,%,元,万]numeric_countsum(1forhinheadersifany(kwinhforkwinnumeric_keywords))ifnumeric_countlen(headers)*0.5:returnTrue# 规则 3: 周围文本很短 50 字符说明表格是独立的数据罗列iflen(surrounding_text.strip())50:returnTruereturnFalse5.4 数据密集型表格的处理流程asyncdefprocess_data_heavy_table(table_info,document):处理数据密集型表格提取建表 原文替换为摘要# 1. 提取数据动态建 PostgreSQL 表table_namefdoc_{document.id}_table_{uuid4().hex[:8]}awaitcreate_table_from_html(table_name,table_info.table_html,table_info.headers)awaitinsert_data_to_table(table_name,table_info.table_html)# 2. 注册到 data_source 表和 Excel 共用同一套体系awaitsave_data_source(doc_iddocument.id,table_nametable_name,column_infotable_info.headers,source_typepdf_table,# 标记来源是 PDF 中的表格original_filedocument.file_name,)# 3. 在原文中替换为摘要保留上下文summary(f\n 此处有一张数据表{table_info.row_count}行f列{, .join(table_info.headers)}。f可通过数据查询工具精确查询。\n)returnsummary# 替换原文中的表格 HTML六、Text-to-SQL 查询流程详解6.1 完整流程用户: Q1 各产品总销售额排名取前10 │ ▼ Agent 判断: 这是数据查询问题调用 data_query Tool │ ▼ data_query Tool 内部: │ ├── 1. 从 data_source 表获取 column_info table_description │ ├── 2. 构建 Prompt: │ 你是一个 SQL 专家。以下是可用的数据表 │ 表名: excel_a1b2c3d4 (2024年Q1销售报表) │ 列: 产品(TEXT), 1月(NUMERIC), 2月(NUMERIC), 3月(NUMERIC) │ │ 用户问题: Q1 各产品总销售额排名取前10 │ 请生成 SELECT 语句。 │ ├── 3. LLM 生成 SQL: │ SELECT 产品, (1月2月3月) AS q1_total │ FROM excel_a1b2c3d4 │ ORDER BY q1_total DESC LIMIT 10 │ ├── 4. 安全校验: 只含 SELECT ✅表名在白名单内 ✅ │ └── 5. 执行 SQL返回结果 │ ▼ Agent 拿到结果组织自然语言回答: Q1 销售额前10的产品如下 1. 产品A: 46,500 元 2. 产品B: 28,800 元 ...6.2 多 Tool 协同示例用户: 合同里的费用明细是多少另外和预算表对比一下 │ ▼ Agent 判断: 这个问题涉及两部分 │ ├── 1. 合同里的费用明细 │ → knowledge_search语义检索合同 PDF 中的表格 chunk │ └── 2. 和预算表对比 → data_querySQL 查询预算表 Excel 的数据 │ ▼ Agent 综合两个 Tool 的结果生成最终回答七、总结7.1 核心原则结构化数据走 SQL非结构化数据走向量— 不要把所有东西都塞进 MilvusRAG 管语义SQL 管数据Agent 管路由— 各司其职Excel 永久建表— 一次导入N 次查询毫秒级响应PDF 中的表格智能分层— 小表格向量化大表格建表统一查询入口— 无论数据来源是 Excel 还是 PDF 提取都通过同一个sql_tool查询7.2 最终架构一览数据类型处理方式存储查询 Tool适用问题PDF/DOCX 文本MinerU → 切片 → EmbeddingMilvusknowledge_search“退货政策是什么”PDF 中的小表格保留在 chunk 中向量化Milvusknowledge_search“设计费是多少”PDF 中的大表格提取建表 原文摘要PostgreSQLdata_query“费用明细合计”Excel/CSV 文件直接建表PostgreSQLdata_query“Q1 销售额排名”7.3 避坑清单❌ 错误做法✅ 正确做法Excel 每行转文本 → 向量化Excel → 建表 → Text-to-SQLPDF 中所有表格都建表智能分类小表格向量化大表格建表临时表方案每次查询重新导入永久表方案一次导入多次查询RAG 和结构化数据混在一起隔离为独立模块通过 Agent Tool 统一入口直接传 LLM 生成的 SQL 执行安全校验只允许 SELECT 白名单表名 LIMIT 保护作者RAG 不是万能的。结构化数据天然就有更好的查询方式SQL强行走向量化是在用错误工具解决正确问题。好的架构是让每种数据走最适合它的路径。
返回列表