向量知识库,能把应收账款的一百种说法都认出来吗?
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 字重叠",然后怪模型不行。
发表评论: