无尘阁日记

无尘阁日记

向量知识库,能把应收账款的一百种说法都认出来吗?
2026-09-09

从"把意思变成坐标",到"表格为什么是噩梦"——一次讲透语义检索的能与不能。

在会计、金融、法律这类行业里,有件特别折磨人的事:同一个意思,有一百种说法。

"应收账款"、"应收款"、"应收"、"AR"、"trade receivable"、"客户欠我们的钱"——说的是同一件事。但你用关键词去搜,"客户欠我们的钱"这七个字,跟"应收账款"一个字都匹配不上。

于是大家把希望寄托在向量数据库上。听说它能"理解语义"。

它确实能。但能到什么程度?边界在哪?尤其当你的文档里有一半是二维表格的时候——那些表格,它到底是怎么切的?

这篇文章讲三件事:原理、能力边界,以及最容易被忽略却最要命的一环——切分。


一、原理:把"意思"变成坐标

1.1 一个只有一条规则的坐标系

假设我们给每个词发一个坐标,规则只有一条:意思越像,坐标越近。

于是会计词汇会自然分成几坨。应收账款、应收款、AR 挤在一起;应付账款、应付款、AP 跑到另一个角落;折旧、摊销、累计折旧在第三个方向。

语义空间示意(真实是 768~3072 维,这里压缩成 2 维才画得出来)

   别人欠我们这一带                      我们欠别人这一带
   ─────────────────                   ─────────────────
     ● 应收账款                            ● 应付账款
     ● 应收款                              ● 应付款
     ● 应收票据                            ● 应付票据
     ● AR                                  ● AP
     ● 应收款项
     ◆ 客户欠我们的钱   ← 你的提问

              成本分摊这一带
              ────────────
                ● 折旧      ● 摊销
                ● 累计折旧  ● 折旧费用

这个坐标不是人定的,是一个叫 embedding 的神经网络,读了海量文本以后自己学出来的。真实的坐标不是 2 个数字,而是 768 个、1536 个甚至 3072 个数字

关键看点:你搜"客户欠我们的钱",这七个字跟"应收账款"一个字都不重合,但它的落点还是掉进了应收那一簇。

这就是向量检索相对关键词搜索最大的本事——它比的是意思,不是字面

1.2 完整流程其实只有五步

【建库 · 一次性】

   文档 ──▶ ① 切块 ──▶ ② 编码 ──▶ ③ 存入向量库并建索引
           按格式切   每段→768个数字    方便快速查最近邻

【查询 · 每次】

   提问 ──▶ ④ 用同一个模型编码 ──▶ ⑤ 比夹角,取最像的 Top-K ──▶ 交给大模型作答
           问题→768个数字

三个要点,缺一不可:

- 问题和文档必须用同一个模型编码,否则是两把尺子,量出来的数没法比。 - 相似度算的是夹角(余弦相似度),不是直线距离。方向一致 = 意思像。 - 它只负责"挑出最像的几段",不负责判断对错——判断是后面大模型的事。


二、它能"完全"识别近义词吗?不能

这是最容易被过度承诺的地方。我给你看相似度分数的真实形状(数值为示意,不同模型会有差异):

与「应收账款」的语义相似度(示意值)

  应收账款          ████████████████████  1.00
  应收款            ██████████████████    0.94
  应收款项          █████████████████     0.92
  AR               ████████████████      0.88
  客户欠款          ███████████████       0.85
  trade receivable ██████████████        0.82   ← 阈值该切在哪儿?
  应收票据          █████████████▌        0.79   ← 其实是另一个科目
  预收账款          █████████████         0.71   ← 是负债,不是资产
  应付账款          ████████████▌         0.68   ← 借贷方向完全相反

2.1 分数是连续的,没有天然的"该在哪切一刀"

看 0.79 和 0.71 那两条。"应收票据"是另一个科目,"预收账款"是负债不是资产,"应付账款"方向完全相反——但它们的分数离"应收款"并不远,向量库照样会把它们一起捞出来,而且排位不低。

你只能自己拍一个阈值(比如 0.75)。可拍在哪儿,都会错一批。

2.2 差一个字就是两个意思的,它基本分不出

会计金融里这种对子特别多,而且个个致命:

容易混淆实际差别
银行承兑汇票 / 商业承兑汇票承兑人不同,信用风险天差地别
应收 / 应付资产 vs 负债,借贷方向相反
总额法 / 净额法收入确认金额完全不同
资本化 / 费用化进资产还是进当期损益
合并报表 / 个别报表编制主体不同
适用 / 不适用否定词几乎不改变向量

2.3 需要精确匹配的东西,它天然不擅长

科目代码 1122、准则编号 CAS 14、第几条第几款、2023 年 vs 2024 年、5% 还是 10%——这些在模型眼里都是"差不多的一串字符",压成向量后细节就糊了。

还有缩写歧义:AR 是应收账款还是 augmented reality?AP 是应付账款还是亚太区?

2.4 它是"整段压缩"

一段 500 字压成一个向量,段落里的限定条件——"除……之外"、"仅适用于上市公司"——会被稀释掉。


三、切分:最脏、最要命、最少被讲清楚的一环

前面都还算好理解的。真正让人翻车的是接下来这件事。

3.1 分块器不理解语义

先把幻想打破:分块器本身不理解语义。它看的是格式符号,不是意思。

主流的"递归字符切分"逻辑是这样的——拿一把刀,按优先级从粗到细找切口:

第 1 刀:找 \n\n(空行/段落)  → 找不到就降级
第 2 刀:找 \n(换行)          → 找不到就降级
第 3 刀:找 。!?(句末)      → 找不到就降级
第 4 刀:找 ,、                → 还找不到
第 5 刀:按字数硬切

所以它切的是排版边界,只是碰巧"段落边界"经常和"语义边界"重合。一旦不重合,它就切错,而且它自己不知道。

3.2 语义单元被腰斩

看一个真实例子。会计准则里"应收账款"的语义流是这样的:

语义流:
   [A 定义][A 确认条件] ┊ [B 坏账计提] ┊ [C 列报][C 披露]
                       ↑ 边界1         ↑ 边界2

按字数硬切(每 300 字一刀):
   chunk 1   [A 定义][A 确认条件][B 坏账计
   chunk 2                       提方法…][C 列报][C 披露]
                                 ↑ B 被腰斩,两个 chunk 的向量都被"平均"了

结果:chunk 1 里混进半个 B,chunk 2 里躺着另外半个 B。两个 chunk 的向量都被"平均"了——哪个主题都不突出,这就是所谓的向量稀释

你问"坏账怎么计提",两个 chunk 的分数都只有 0.6 左右,可能都进不了 Top 5。

而且语义边界本质上是连续的,不是离散的。"确认条件"和"坏账计提"之间到底算一个话题还是两个,人都不一定能说清,别指望一个计数器。

3.3 二维表格为什么是噩梦

因为 embedding 模型吃的是一维序列,而表格是二维结构。你必须先把表"拍扁"成一行字符串,行列关系在这个过程中就丢了一半。

更糟的是切分:

原表(二维结构)
                  2023      2024
   应收账款       1,200     1,450
   应收票据         320       280

拍扁成一维(Markdown)
   | 科目     | 2023  | 2024  |
   | 应收账款 | 1,200 | 1,450 |
   | 应收票据 |   320 |   280 |

若表太大被迫切开,且不重复表头:
   chunk A  | 科目 | 2023 | 2024 |          ← 光杆表头,零信息量
   chunk B  | 应收账款 | 1,200 | 1,450 |    ← 只剩数字,谁看得懂

chunk B 的向量会跟任何一张财务表的任何一行都很像——数字在 embedding 里几乎没有语义,"1,200"和"1,450"的向量距离近得离谱。这就是为什么表格检索是 RAG 的重灾区。

再加上跨页表格、合并单元格、表头在第二行、单位写在表外("单位:万元")……PDF 解析阶段就已经丢了一轮信息。

3.4 一个常被低估的环节:PDF → Markdown

同一张表格,序列化成 | a | b | 的 embedding 质量,远高于 <td><tr> 标签堆。

所以文档解析这一步——MinerU、Marker、各类 OCR 后处理——选得对不对,往往比选哪个向量库影响更大。很多团队花几周对比向量数据库,却在解析环节随手一选,本末倒置。


四、那怎么办:不换掉向量库,而是在它外面加几层

4.1 表格的五招

1. 小表整体入,不切。 表标题 + 表头 + 单位 + 脚注一起打包成一个 chunk。这是最有效的——90% 的表格其实都在 50 行以内。 2. 大表按行切,但每行都重复带表头。 体积翻倍没关系,重要的是每一行单独看都自解释。 3. 表格转原子事实(效果最好,成本最高)。不走切分,而是让 LLM 把每行转成一句话:

> "应收账款 2024 年余额 1,450 万元,较 2023 年 1,200 万元增长 20.8%。"

这种句子的向量质量远高于表格行,而且自带了对比和趋势。

4. 表格走结构化通道。 把表真正写进数据库,问题来了走 NL2SQL,别硬塞进向量库。财务数据天然适合这条路。 5. 解析环节输出 Markdown 而非 HTML。 理由见 3.4。

4.2 文本的三招——别追求"切对",靠冗余覆盖

- 重叠窗口:相邻 chunk 留 10~20% 重叠,边界处的内容在两个 chunk 里都存在,被切断的概率大幅下降。 - 父子块(small-to-big,目前最主流):用小 chunk(200 字)建索引保证精度,命中后返回它所属的父 chunk(1000 字)保证上下文完整。检索用小块,喂给大模型用大块。 - 多向量:同一块文本生成多个向量。比如让模型给它提 3 个假设性问题,问题也一起建索引,从多个角度指向同一段内容,提高被命中的概率。

4.3 检索链路的四层补丁

1. 术语同义词表(ROI 最高):查询进来先做一次改写。维护一张显式映射:应收账款 = 应收款 = 应收 = AR = trade receivable = 客户欠款。这层是可控、可审计的——金融场景里这点很重要,你能解释"为什么这条被召回"。 2. 混合检索:关键词(BM25)负责精确命中科目代码、准则号;向量负责语义泛化。两边各召回一批,用 RRF 融合。主流向量库现在都原生支持。 3. Rerank 精排:先粗召回 50 条,再用 cross-encoder 逐条精算,重排到 5 条。上面那些"字面像意思不对"的,大部分在这一步被刷掉。 4. 元数据硬过滤:年份、报表类型、准则版本、行业、主体性质——先用结构化字段筛一遍,再进语义检索。别什么都丢给向量。

另外还有一条:准则、制度、科目说明这类文本,按条款切,一条款一个 chunk 并带上上级标题,别按字数硬切。


五、一句话心法

切分的目标,从来不是"切出干净的语义单元"——这个做不到

真正的目标是:

保证任何一个语义单元,都至少有一个 chunk 完整包含它。

所以判断切得好不好的标准,不是"切得准不准",而是"覆盖冗余够不够"。宁可多切、重叠、父子双份,也别追求一刀两断的干净。

回到开头那个问题:向量数据库能识别大部分近义说法——这是它的强项;但没法"完全"做到,恰恰在会计金融这种"差一个字就是两个科目"的领域最容易翻车。

把它当召回的第一层,别当唯一一层。

向量库负责"大概像",术语表负责"必须准",rerank 负责"排对序"。


最后给一句实操建议:如果你正准备动手,先拿 20 份典型文档手工跑一遍,看看你的表长什么样、段落有多长、术语怎么分布,再定切分策略。

绝大多数团队的错误,是一上来就套默认的"512 字 + 50 字重叠",然后怪模型不行。