合同做 RAG,为什么不能直接按 Token 切?
用 RAG 查合同,有一种错误特别麻烦。
用户问:
「我们的赔偿责任上限是多少?」
系统很快回答:
最高不超过过去 12 个月支付的服务费。
单看这句话没问题。
但回到原合同继续往后看,后面可能还紧跟着重大过失、知识产权侵权、违反保密义务等例外。前面的“上限”找到了,但决定这个上限什么时候不适用的内容没有一起回来。
模型未必在胡编。
它只是拿到了一个不完整的 chunk。
这也是我们在做企业级 AI 知识库「有谷大脑」时,为什么会把合同类文档从普通文档里单独拆出来处理。
合同和一般文章不同。很多条款的含义并不只在当前这一段里,还依赖父条款、Definitions、例外条款、交叉引用,甚至附件。
所以在有谷大脑里,合同不会直接走“抽文本 → 固定 Token 切割”这条路径,而是先做文档识别和结构解析,恢复条款层级之后,再生成候选 chunk。
一、固定 Token 切割,为什么容易把合同切坏?
很多 RAG 项目一开始都会用固定长度切割,比如每个 chunk 控制在几百 tokens,再保留一点 overlap。
实现简单,也很通用。
但合同里最麻烦的是:法律语义经常刚好跨过切割点。
比如:
12.4 Limitation of Liability 甲方对乙方承担的最大赔偿责任,不超过乙方在过去 12 个月内实际支付的服务费,但以下情况除外: (a) 因甲方故意或重大过失导致的损失; (b) 第三方知识产权侵权; (c) 违反保密义务造成的损失。如果切割点落在“但以下情况除外”前后,就可能变成:
Chunk A: 最大赔偿责任不超过过去 12 个月内实际支付的服务费。以及:
Chunk B: 但以下情况除外: (a) 故意或重大过失…… (b) 知识产权侵权……用户搜“赔偿责任上限”,Chunk A 往往更容易被召回。
模型回答“12 个月服务费”也很自然。
问题是,它看到的本来就只有半条规则。
Overlap 可以缓解一部分情况,但解决不了这个根本问题,因为切割依据仍然是长度,而不是条款完整性。
合同里还有几类类似的问题。
Definitions 和正文可能断开。
比如合同开头定义:
“服务”是指甲方根据本协议及附件 A 向乙方提供的软件服务、 技术支持服务及相关升级服务。 “用户数据”是指乙方或其授权用户通过平台上传、存储、 生成或处理的数据。后面的正文会反复使用“服务”“用户数据”“保密信息”这些词。
在合同里,这些不是普通词,而是已经被定义过的术语。
如果用户问:
服务终止后,用户数据怎么处理?
检索可能把终止条款找出来了,却没有把“用户数据”的定义带回来。
这时候正文虽然没切坏,上下文依然不完整。
交叉引用也可能悬空。
合同里大量存在:
如 Section 9.3(a) 所述……或者:
具体服务等级见附件 B。如果当前 chunk 只有“见附件 B”,但系统不知道附件 B 跟它有什么关系,那么这个引用其实是悬空的。
子条款也可能失去父级语义。
比如:
1.1 未经甲方书面同意,乙方不得: (a) 将前述权利转让给任何第三方; (b) 对服务进行转售;如果只检索到:
(a) 将前述权利转让给任何第三方。“前述权利”是什么已经丢了。
所以合同 Chunking 真正要保护的,不只是某一段文字,而是条款之间原本存在的结构关系。
二、合同本身,其实已经告诉我们应该怎么切
合同和普通长文本有一个很大的区别:它通常有非常明确的层级结构。
英文合同常见:
Article I Section 1.01 (a) (i) (ii)中文合同常见:
第一条 第一款 (一) (二)或者:
1. 1.1 1.1.1 1.1.2这些编号不是普通排版,它们本身就是合同提供的结构信号。
所以处理合同的时候,与其先问:
这一段有多少 Token?
不如先回答:
这一段到底属于哪一条?
在有谷大脑里,合同进入知识库后,会先尝试识别Article、Section、1.1、1.1(a)、第 N 条这类编号结构,恢复条款层级,再决定哪些节点适合作为候选 chunk。
三、有谷大脑怎么做合同条款级 Chunking?
前面这些问题,本质上都指向同一件事:合同不能只按文本长度切,还得把原来的条款结构和引用关系保留下来。
在有谷大脑的合同解析链路里,大致会经历:
合同识别 → 条款结构解析 → 生成候选 clause chunk → 补充 Definitions / 父级 / 引用关系 → 超预算条款继续细分
也就是说,Token 预算还在,但它不负责决定“第一刀从哪里切”。
它只是限制一个 chunk 最终能有多大。
图 1:有谷大脑合同条款级 Chunking 架构——先恢复合同层级,再分别处理定义索引、叶子条款和交叉引用,检索阶段根据命中结果按需展开上下文。
比如:
Section 12. Limitation of Liability │ ├─ 12.1 General Cap │ ├─ 12.2 Exceptions │ ├─ 12.2(a) Gross negligence │ ├─ 12.2(b) IP infringement │ └─ 12.2(c) Confidentiality breach │ └─ 12.3 Mutual cap这里Section 12是父节点,12.2下面还有子条款。
真正适合作为候选 chunk 的,通常是:
12.1 12.2(a) 12.2(b) 12.2(c) 12.3而不是从某个 Token 位置切一刀,把12.1的后半段和12.2的前半段塞进同一个 chunk。
如果某个条款本身特别长,再优先沿子条款、编号列表、自然段继续拆。
只有前面这些语义边界都不够用时,才退到长度边界。
所以这里并不是不用 Token,而是把 Token 从“切割依据”变成了“长度约束”。
四、切成条款还不够,关系也要一起保存
只把合同按照 Section 和 Clause 切开,还没有结束。
有些条款单独看依然不完整,所以还需要把它和父级、定义、引用对象之间的关系一起保存下来。
祖先路径
比如检索命中了:
12.2(a) Gross negligence如果只拿这一句,信息还是不够。
所以条款 chunk 会带上自己的结构路径,比如:
{"path":"Section 12 > 12.2 > 12.2(a)","level":3}这样系统知道它属于:
Section 12 → 12.2 Exceptions → 12.2(a)一方面模型能拿到必要的父级语义,另一方面最终回答也更容易回到具体条款,而不是只说“根据知识库内容”。
对合同场景来说,来源路径本身就是很重要的信息。
Definitions
定义块识别出来以后,可以转成结构化记录:
{"term":"Customer Data","definition":"..."}然后单独建立定义索引。
当查询里出现合同已经定义过的术语时,系统可以把对应定义一起补进 Context。
比如:
Customer Data 在合同终止后多久删除?
实际可能需要的上下文是:
Customer Data 的定义 + 合同终止条款 + 数据删除 / 返还条款而不是只期待向量检索刚好把 Definitions 那一页搜回来。
交叉引用
比如:
{"path":"Section 12.1","text":"……不超过过去12个月内乙方已实际支付的服务费(如第9.3(a)条所定义)……","cross_references":["Section 9.3(a)"]}这样Section 12.1和Section 9.3(a)之间就有明确关系。
但检索到12.1时,并不一定每次都要把9.3(a)也塞进 Context。
用户只是问:
责任上限是多少?
当前条款可能已经够用。
如果继续问:
这里的“过去 12 个月服务费”具体怎么算?
再沿着cross_references把9.3(a)拉进来。
这种按需展开,比把整个引用链一次性全塞进去更稳。合同的交叉引用有时很多,上下文越多不代表回答越好。
附件
企业合同里,真正重要的数字经常不在正文。
比如主协议只写:
具体服务等级见附件 B。附件 B 才写:
Availability SLA:99.9% P1 故障响应时间:30 分钟如果附件被当成一份完全无关的文档,用户问:
这份合同的 SLA 是多少?
主合同很可能只返回一句“见附件 B”。
所以附件除了自己要切对,还要保留和主协议的关联。
价格、SLA、API 限额、服务范围、数据处理要求,这些内容都很常见。
五、怎么判断一份文档该不该走合同解析?
企业知识库里不会只有合同,所以入库时还要先决定走哪条解析路径。
有谷目前会结合几类信号来判断。
比如文件名:
合同、协议、NDA、SLA、服务条款、License Agreement
再看结构上有没有高密度的:
1.1、1.1(a)、Section、Article、第一条、第一款
以及正文里:
甲方、乙方、违约、赔偿、本协议
这类词的分布。
单看一个信号都不够稳,组合起来会更可靠。
对于置信度低的文档,保留回退到通用文档解析的路径,而不是强行按合同切。
因为普通报告如果被误切成大量细碎的“条款”,一样会影响检索效果。
六、条款级 Chunking 和固定 Token,到底差在哪?
放在一起看会更直观:
| 对比项 | 固定 Token Chunking | 合同条款级 Chunking |
|---|---|---|
| 切割依据 | Token 长度 | 条款结构和语义边界 |
| 条款完整性 | 可能从中间切开 | 优先保留完整条款 |
| 父子关系 | 容易丢失 | 保留祖先路径 |
| Definitions | 当普通文本检索 | 单独提取和索引 |
| 交叉引用 | 容易悬空 | 保存引用关系 |
| 附件 | 可能和正文割裂 | 保留主协议关联 |
| 超长条款 | 到长度继续切 | 优先沿子条款、段落拆 |
| 来源定位 | 文本片段 | Section / Clause 路径 |
所以这里并不是说合同不能使用 Token。
更准确地说:
Token 应该是长度约束,而不应该成为唯一的切割依据。
固定 Token 关心的是“这一段有多长”。
条款级 Chunking 更关心的是“这一段到底属于哪一条,以及和哪些内容有关”。
七、合同 RAG 做得对不对,可以直接这样测
比起“帮我总结这份合同”,下面几类问题更容易暴露底层 Chunking 有没有真的做好。
责任上限
赔偿责任上限是多少?有哪些例外?
看责任上限和 Exceptions 有没有被拆开。
定义术语
合同里的“保密信息”具体指什么?
看 Definitions 能不能正确关联回来。
跨条款问题
延迟付款超过 30 天,会触发哪些后果?
这类问题通常不只依赖一个条款,可能同时涉及付款、违约、暂停服务甚至终止。
附件问题
SLA 是多少?P1 故障多久响应?
看附件和主协议有没有断开。
来源追溯
这个结论具体来自合同哪一条?
如果最后只能回答“根据知识库内容”,对合同场景来说还不太够。
至少应该尽可能回到具体的 Section / Clause,让用户能打开原合同核对。
这几类问题,比“帮我总结一下合同”更容易看出底层 Chunking 到底有没有做好。
八、最后:合同 RAG 最怕的不是没搜到,而是只搜到半条规则
做合同 RAG,模型能力当然重要,但不少错误其实在模型回答之前就已经埋下了。
如果入库时把责任上限和例外切开,把 Definitions 和正文断开,把Section 9.3(a)变成一个无法展开的引用,那么后面换 Embedding、调召回参数,甚至换更大的模型,都很难把这些结构关系完整补回来。
所以在有谷大脑里,合同类文档会单独走结构化解析链路:
先恢复合同层级,再做条款级 Chunking,同时保留 Definitions、父子路径、交叉引用和附件关系。
我们更关注的是:检索回来的上下文是不是完整,以及答案能不能回到原合同核对。
除了合同,有谷大脑也在针对 Excel、Word、PDF、PPT 等不同类型的企业文档做对应的解析和 Chunking。
前面已经拆过 Excel,后面还会继续写:
- PDF 里的复杂表格怎么进知识库?
- 几百页的 Word 制度文件怎么保留章节层级?
- 为什么 RAG 明明检索到了,模型还是会答错?
- 多版本文档并存时,怎么避免引用旧版本?
如果你也在做企业知识库、RAG、合同智能问答或企业 AI 落地,可以关注后续更新。
如果想进一步了解这些能力在产品里的实际应用,可以体验:
有谷大脑:https://brain.yogu.pro
合同 RAG 最怕的不是没搜到,而是只搜到了半条规则。