有谷大脑
返回博客

Excel 表格进知识库,为什么不能按行硬切

按行读 Excel 只能拿到值,结构、公式和边界全丢了。有谷大脑用六段确定性流水线——空行切块、反差认表头、可引用渲染——让模型能说「据 Q4!B6」,而不是「据我看到的表格」。

把一张销售报表 .xlsx 丢进知识库,然后问:「摄像头 10 月卖了多少?」

如果入库时只是把每个单元格的值拼成文本,或者按固定 token 数硬切,常见的结果是这样的:检索命中了 8800091000 两个数字,模型不知道哪个是摄像头、哪个是 10 月——因为 chunk 里既没有列名,也没有行号,更没有 Excel 坐标。

Excel 和 PDF、Word 不一样。PDF 的问题是「图里藏着字」;Excel 的问题是信息一大半不在值里,而在值与值的排布关系里——空行划边界、合并单元格标分组、公式串写因果、冻结窗格声明表头。按行读只取走了值,把关系全丢了。

有谷大脑对 Excel 的处理,不走「LibreOffice 转 PDF 再 OCR」这条路,而是走专用的表格语义解析流水线——底层基于开源项目 excel-parser,全程不调模型、不做猜测,同一份文件重跑永远得到同一批 chunk。

Excel 表格 Chunking 架构:读取网格信号 → 空行切块并修补 → 反差认表头 → 可引用渲染 → 公式依赖 → 封装 chunk。

按行读,丢的是什么

打开一张普通的 Q4 销售报表:第 1 行是标题,第 2 行是制表人和日期,第 3 行空着,第 4 行才是真正的列名,往下是数据行和合计行。再往下空两行,藏着第二张小表「假设参数」。

用最常见的 openpyxl 按行读:

for row in sheet.iter_rows(values_only=True):
    print(row)

你会得到 15 个元组,但没有任何一处告诉你「这里其实是两张表」、「第 4 行才是列名」、=SUM(B5:D5) 算出来的是季度合计。公式读出来是 None——业务逻辑跟着一起消失了。

这四类丢失,正好对应解析器要做的四件事:

丢失后果解析器怎么补
结构分不清哪行是列名认表头 + 切块
公式=SUM 变空保留公式串 + 建依赖图
出处拿到 138000 说不出在哪个格渲染时带行号、列字母
边界两张表被当成一片空行切块 + 修补

六段流水线,各管一件事

一份 .xlsx 从字节到检索单元,中间经过六段处理:

  1. 读取 — 把网格搬进内存,同时抓住值、公式、加粗、合并区域、隐藏行、冻结窗格
  2. 切块 — 一张 sheet 上有几张独立的表,各自占哪个矩形
  3. 认表头 — 哪一行才是真正的列名
  4. 渲染 — 写成模型读得懂、能引用的文本
  5. 算依赖 — 从公式串里扫出「谁算出了谁」
  6. 封装 — 变成带 metadata 的 chunk,大表超预算时分片

第 2 段和第 3 段是全部难度所在。其余是水到渠成。

同一张 Q4 表跑完,产出 4 个 chunk:说明区、销售表、假设参数表、汇总表。销售表渲染出来长这样:

[Q4!A4:E8] (table) cols: A=产品线, B=10月, C=11月, D=12月, E=季度合计
| row | 产品线  | 10月    | 11月    | 12月    | 季度合计        |
|-----|------|--------|--------|--------|-------------|
| 5   | 智能门锁 | 120000 | 138000 | 151000 | =SUM(B5:D5) |
| 6   | 摄像头  | 88000  | 91000  | 105000 | =SUM(B6:D6) |

对比开头那 15 个元组:表头认对了、公式留住了、边界切准了、行号和列字母都在。用户问「摄像头 10 月卖了多少」,模型能从 cols: 查到「10月」是 B 列,从行号栏查到「摄像头」在第 6 行,拼出 Q4!B6 = 88000

切块:先粗暴地切,再有条件地粘回去

一张 sheet 上可能有一张表,也可能有五张表加说明文字。切块要回答:有几个独立区域,各自占哪个矩形。

第一刀:空行空列

最可靠的边界信号是空白。人在 Excel 里排版,两张表之间总会留空行。

算法很直白:找出所有有内容的行,行号之间隔了空行就断开;每个行组内部再按列间隙切。默认阈值是 1——隔一个空行/空列就断。极密的表(密度 > 0.9,典型数据导出)放宽到隔 2 行才断,但列间隙从不放宽——空列几乎总是真实边界。

第一刀切完,常见两处「受伤」:

  • 标题块:只有标题和制表信息,没有数据,单独作为检索单元毫无意义
  • 孤儿续表:表中间有空行时,后半段有数据但没有表头——检索命中它,模型看到一列数字,不知道是哪个月

所以第一刀之后必须修补。

修补一:并标题

把孤零零的标题块折进下面的数据块。但不能无脑合并——两个各三行的独立小表会被互相粘死。

设了严格的准入条件:标题块不超过 3 行、没有公式、不是 Excel 正式表、必须紧贴在数据块上方、下面那块必须更高、列范围有重叠。只把小的折进大的。

修补二:缝表身

从一个带表头的「锚块」出发,往下扫。遇到的多列、列范围重叠、自己没有表头的块——吞掉,锚块范围往下延伸。遇到单列的小行(比如「华北区」这样的分组标签)——先挂起,后面真有续表才一起并入;如果后面停了,就留作独立块。

判断一个东西是什么,有时候要看它后面跟着什么。 这个思路在 Excel 解析里反复出现。

认表头:加粗不可靠,第一行更不可靠

两个直觉都是错的:

  • 「表头是加粗的那行」——合计行也加粗,很多表头反而不加粗
  • 「表头是第一行」——上面往往压着标题、制表信息、单位说明

真正可靠的信号是反差:逐列比较「这一行这个格子是不是文字」和「它下面整列是不是数据」。两者同时成立,这一列算一次反差;累计 2 列以上,这行就是表头。

对 Q4 表第 4 行:10月 下面是 120000/88000/46000,11月12月季度合计 同理——五列全中。这个判断跟加粗完全无关。

怎么跳过标题行?

  • 第 1 行横跨全表的合并单元格 → 横幅,不是表头
  • 第 2 行「制表人: 张伟 制表日期: 2025-01-08」→ 键值对说明行,跳过

找到锚点后还要向下收编多层表头,但不能把数据行吃进去。刹车机制叫「体征签名」:给每行算类型指纹(文字/数字/日期/空),签名跟下方数据行一致的行——即使加粗——也是数据,band 在此停住。

还有一个「作者优先」规则:如果作者冻结了窗格,band 越过冻结线时会被裁回去。冻结窗格是作者在说「这几行要一直钉在上面」,优先于任何启发式。

find_header_span() 这个函数被三处共用:切块时判断有没有表头、渲染时决定 Markdown 表头行、大表分片时决定每片重复哪一行。三处共用同一个判断,才能保证一张表切成 5 片后,每片的表头完全一致。

渲染:写给模型看,不是写给人看

渲染文本有四个关键部件:

  1. 坐标括号行[Q4!A4:E8] (table),标明范围和块类型
  2. 列字母对照cols: A=产品线, B=10月, ...,建立列名与 Excel 列字母的映射
  3. 行号栏 — 每行钉上真实 Excel 行号,隐藏行标 [hidden]
  4. 原始值 — 写 1272 而不是 1,272.00,因为用户在检索框里打的是 1272

列字母不放在表头行里——表头行留给真实列名。配上行号栏,模型能反推出任意数字的完整地址,回答里写「据 Q4!B6」,而不是「据我看到的表格」。

隐藏行不删,打标记。审计的人需要知道「这份表藏了三行」。整个项目的立场是:尽量不丢信息,把判断权交给下游。

公式依赖:=SUM 里藏的业务逻辑

=SUM(B5:D5) 里面藏的信息是:E5 这个数字是由 B5、C5、D5 加出来的。把整个工作簿的这类关系连起来,就得到一张有向图——这份表的业务逻辑。

解析器对公式做的唯一一件事,是扫出里面所有的引用,不求值——直接读 Excel 缓存进文件里的计算结果。自己实现几百个 Excel 函数是另一个量级的工程,而且没必要。

每个 chunk 会附一份依赖摘要。检索命中销售表时,模型顺带知道:这块的数字被汇总表的两个格子引用着。这是纯值读取给不了的上下文。

大表分片:不能把表头切丢

默认预算是每片 2000 token,小表原样保留。只有超预算的表才切。

切的时候,每片重复表头,并声明自己是第几片:

[PART 2/5 of table 明细!A1:H301 — rows 72–141 of 2–301; header repeated below]

三个细节:

  • 每片重复的表头,是同一个 find_header_span() 判出来的,5 片列名保证一致
  • 片的坐标只覆盖数据行,重复的表头算「渲染时加的糖」,不算进坐标
  • 标题只在第 1 片保留,避免每片都顶着同一句报表标题

每片装多少行,不能把表头开销按平均行数摊薄——每片都要重复付一次表头、cols: 映射、横幅的开销,固定开销要先扣掉再算能装几行。

chunk_id 由文件哈希 + 表名 + 块类型 + 坐标确定性算出,不含时间戳、不含随机数。同一份文件解析一千次,得到的 id 完全相同——去重、版本比对、复现调试都免费到手。

和通用 chunk 的实际差异

同一份 300 行 8 列的订单明细表:

  • 通用 512-token 切割:约 40–60 个 chunks。大量 chunk 从表中间切断,第 2 片没有表头,检索命中时模型看到 199.5 只能猜是单价还是金额
  • 表格语义切割:按 sheet 上的独立区域切,销售表、假设参数表、说明区各成一块;超预算的大表按行分片,每片带完整表头。每个 chunk 是可引用的矩形区域,向量表示聚焦

还有一个常见路径的对比:LibreOffice 转 PDF 再 OCR。表格变成图片,列对齐、公式、隐藏行全部丢失,OCR 还容易把数字读错。专用 Excel 解析保留结构语义,这是表格类文档 RAG 准确率的基础。

什么时候 Excel RAG 最容易出问题

以下几种情况是重灾区:

问具体格子的值:「华东区 Q4 摄像头 11 月销售额」——需要列名 + 行号 + 坐标,通用文本 chunk 给不了。

问公式算出来的数:「季度合计怎么来的」——需要公式串和依赖关系,=SUMNone 就答不完整。

一张 sheet 多张表:主表和假设参数表混在一起切,问毛利率时检索到销售数据。

大表中间段:300 行明细被硬切,中间某片没有表头,数字失去了列语义。

跨表引用:汇总表引用销售表的合计格,chunk 之间没有依赖信息,模型不知道两个表的关系。

典型使用场景

财务和业务分析:把季度报表、预算模型、经营仪表盘传进知识库,直接问「哪个产品线 Q4 增长最快」「假设参数里的毛利率是多少」,不用每次打开 Excel 找对应 sheet。

采购和供应链:价格表、BOM、供应商报价单通常是多 sheet、多表混排。表格语义切块让每张价目表成为独立检索单元。

审计和合规:隐藏行打标记而不是删除,公式依赖保留,回答能追溯到具体单元格——「据 Q4!E8」比「据表格底部那个合计」可验证得多。

Excel 是企业知识库里最高频的结构化数据源之一,却长期被当作「转 PDF 处理」的二等公民。把表格当成表格来切——而不是当成一段普通文本——是让它在 RAG 里真正好用的前提。