合同做 RAG,为什么不能直接按 Token 切?
合同做 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 SLA99.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 最怕的不是没搜到而是只搜到了半条规则。