本节摘要:BERT 级底座有输入长度上限(以官方文档为准的 token 上限),长文档塞不进一次前向——laya 的答案是 predict_long(官方口径提供):把长文切成带重叠的窗口,逐窗对同一道题做决策,再把各窗的概率聚合收口。本节讲三件事:窗口机制本身——窗口与步长两个参数怎么互相牵制(窗口定「每段看多少」,步长定「隔多远起一段」,重叠换召回);聚合策略怎么选——概率平均、逐窗投票、分段结论三种各有其场景;两个示例场景的完整落地——长文审核(判的是全文倾向)与多段证据判断(判的是证据是否存在),两者聚合方式不同,恰好覆盖两类典型。所有参数值为示意值,接口写法以官方仓库 README 为准。
第 3.1 节的 predict 假设 context 是「一段塞得进模型的文本」。真实业务不总是这么客气:一份合同几十页、一篇长文上万字、一次会话的完整记录。BERT 级底座的输入上限以 token 计(具体上限以官方文档为准,量级为常见 BERT 系取值),超限的直接做法是截断——但截断对判断型任务是灾难:违规条款可能在第 37 页,截掉后半等于把证据扔了。
截断 vs 窗口扫描 截断(丢信息) 窗口扫描(保信息,加算力) ┌────────────────┐ ┌────────────────────────────┐ │ ██████████▓▓▓▓ │ │ [窗1: 段1段2 ] → 决策 │ │ 塞得下的部分 │ │ [窗2: 段2段3 ] → 决策 │ │ 截掉的部分 ✗ │ │ [窗3: 段3段4] → 决策 │ │(证据可能在这) │ │ 聚合:平均/投票/分段结论 │ └────────────────┘ └────────────────────────────┘
窗口扫描的本质是拿算力换覆盖:每多一个窗口就是多一次前向。对单次 32.8 毫秒量级(官方 README 口径)的引擎,十个窗口也就是三百毫秒量级(示意值)——这是轻量引擎做长文本的底气:同样的扫描放在生成式 LLM 上,代价是完全不同的量级。
predict_long 的两个核心参数(参数名以官方文档为准,此处讲语义):窗口长——每个窗口覆盖的文本量;步长——相邻窗口起点之间的距离。步长小于窗口长就产生重叠,重叠区是「证据恰好在边界上」的保险。
| 参数选择 | 得到 | 失去 | 适用 |
|---|---|---|---|
| 大窗口、大步长(少重叠) | 前向次数少、延迟低 | 边界证据容易漏 | 倾向判断类任务(看整体) |
| 小窗口、小步长(多重叠) | 边界证据保得住 | 前向次数多、延迟升 | 证据定位类任务(看细节) |
| 窗口对齐语义单元 | 每窗语义完整 | 切分逻辑要自己写 | 段落结构清晰的文档 |
定参的顺序建议:先按底座上限确定窗口长的上限,再按「最短可疑片段的长度」往下调(窗口至少要装得下一条完整的违规句或证据句,量级示意);步长从窗口长的一半起步(即 50% 重叠,示意起点),用一批已知答案的长文实测漏检率,漏检高就加密重叠。参数不是抄来的,是漏检率与延迟在你的数据上折衷出来的——这与第 3.2 节批量基准是同一种「自测拿数字」的纪律。
# long_review.py —— 长文审核的窗口扫描用法(写法示意,接口以官方仓库 README 为准) from laya import Router router = Router() result = router.predict_long( context=long_document, # 超长文本,例如整份用户协议 questions=[{ "id": "risk", "type": "noul", "question": "这份文档是否包含高风险条款?", }], window=512, # 窗口长(示意值:接近底座上限的稳妥取法) stride=256, # 步长(示意值:50% 重叠起步) ) # 返回除常规字段外,还带各窗口的中间结果(字段形态以官方文档为准): # item["probabilities"] —— 聚合后的全文分布 # item["windows"] —— 每个窗口各自的结果(证据回溯的原料)
逐窗决策之后,N 个窗口各自给出一份概率分布,聚合方式由「这道题在问什么」决定:
| 聚合策略 | 做法 | 回答的问题 | 典型场景 |
|---|---|---|---|
| 概率平均 | 各窗分布取平均 | 「全文整体倾向如何」 | 长文情绪、整体风险等级 |
| 逐窗投票 | 各窗 argmax 后多数决 | 「多数段落怎么说」 | 章节观点一致性判断 |
| 分段结论 | 任一窗口触发即成立(或输出触达段落) | 「是否存在某证据」 | 违规检测、证据定位 |
选择的关键判断是「证据的存在性还是倾向的加总」。问「这份协议有没有霸王条款」,一条就够定罪——分段结论(任一窗触发即成立),并且要把触发的窗口号带出来供人工复核;问「这篇长评整体是褒是贬」,每段都该发言——概率平均;投票介于两者之间,适合「段落之间可能冲突、要多数意见」的场合。
一个容易被忽视的细节:noul 的第三值「不知道」(第 1.2 节)在窗口层会大量出现——空白段、目录页、格式性文本的窗口多半答「不知道」。聚合时要把它当合法信号处理:分段结论里「不知道」的窗口应被跳过而不是被摊薄进平均,否则全文概率会被无关窗口稀释,真正的证据窗口说话声音变小。
# window_plan.py —— 窗口规划与「不知道跳过」聚合的桌面推演(示意) def plan_windows(total_units, window, stride): """total_units: 语义单元数;返回每个窗口覆盖的 [起点, 终点) 区间""" if stride > window: raise ValueError("步长大于窗口会产生覆盖空洞") plans, start = [], 0 while start < total_units: end = min(start + window, total_units) plans.append((start, end)) if end == total_units: break start += stride return plans def aggregate_by_existence(win_answers, win_probs, trigger=0.80): """分段结论聚合:「不知道」窗口跳过,任一窗过触发线即成立""" fired = [(i, p) for i, (ans, p) in enumerate(zip(win_answers, win_probs)) if ans != "不知道" and p >= trigger] return { "成立": bool(fired), "触发窗口": [i for i, _ in fired], # 写进处置工单:直达嫌疑段 "跳过窗口数": sum(1 for a in win_answers if a == "不知道"), } # print(plan_windows(40, 8, 4)) # [(0, 8), (4, 12), ..., (36, 40)] 共 10 窗(示意:40 单元、窗 8、步长 4) # report = aggregate_by_existence( # win_answers=["不知道"] * 7 + ["否", "是", "不知道"], # win_probs=[0.10] * 7 + [0.46, 0.83, 0.20], # ) # print(report) # 成立=True,触发窗口=[8] 即第 9 窗(示意)
plan_windows 是纯几何:从起点 0 反复推进一个步长,直到某窗触到末尾;步长大于窗口直接报错——那会留下覆盖空洞,是参数事故不是风格选择。aggregate_by_existence 把本节第三部分的聚合规则落成两行:答「不知道」的窗口不参与,其余窗口任一概率过触发线即全文成立,并把窗口号原样带出——窗口号就是处置工单里的「嫌疑段坐标」。触发线取多少、跳过多少「不知道」算异常(全文都是目录也值得报警吗),这两个参数用你自己的已知答案长文集标定(示意建议:几十篇起步)。
场景一:长文审核。 判全文是否含违规内容,证据可能藏在任意位置。窗口取小、重叠取密(漏检代价高);聚合用分段结论;触发的窗口号写进处置工单,审核员点开直达嫌疑段。延迟预算按「窗口数乘单窗延迟加聚合开销」估算(示意:二十窗的长文在几百毫秒到一秒量级,以自测为准)——仍然远快于把全文喂给生成式模型的方案(量级对比,示意)。
把场景一走一遍完整流程(数值均为示意)。输入:一份 40 个语义单元的用户协议,窗口 8 单元、步长 4 单元,切出 10 个窗口;逐窗结果里第 1~7 窗答「不知道」(目录与格式条款),第 8 窗对高风险条款给出 0.46——未过触发线,第 9 窗 0.83——触发。聚合走分段结论:任一窗触发即全文成立,输出处置工单并带窗口号,审核员点开直达嫌疑段。误判复盘:那条高风险条款恰好横跨第 8、9 窗边界,第 8 窗只看到半句给了 0.46;若第 9 窗也不幸压线,这道题就漏了——处置不是继续加轮数,而是按句号先切语义单元再装窗(本节第五部分坑一的方案),重切后被劈开的证据至少会在邻窗完整出现一次。这个流程同时演示了「不知道」窗口的正确处理:7 个「不知道」窗口被跳过而非摊薄,第 9 窗的 0.83 才没有被稀释成无意义的低概率。
场景二:多段证据判断。 例如客服纠纷工单:用户陈述、客服回复、物流记录、历史工单四段材料,判断「责任是否在我方」。与场景一的差别在于分段是天然的——材料自带段落边界,窗口直接对齐材料单元,不用滑窗硬切;聚合策略更像「证据链」:各段分别问一道 noul(这段材料是否支持我方担责),再看支持段的数量与强度(示意设计)。这个场景同时示范了 predict_long 的一个变通用法:窗口不一定是滑出来的,也可以是业务结构给的——机制不变,分段方式跟着业务走。
坑一:窗口边界切断语义。 固定 token 数硬切,一句话被劈成两半,两个窗口各自只看到半句——语义证据双双失真。缓解:按句号或段落先切语义单元,再往窗口里装,装不下的单元整体挪到下一窗(切分逻辑十行以内能写完,示意判断)。
坑二:把窗口结果当独立决策消费。 predict_long 返回的逐窗结果是聚合的原料,不是可以单独上线的结论——单独看某窗口的概率会被重叠区重复计数、边界效应放大。下游消费只认聚合后的分布;逐窗结果只做两件事:证据回溯与聚合策略调优。
| 症状 | 先查什么 | 处置 |
|---|---|---|
| 窗口数比预期多一倍 | 语义单元是不是切得太碎(句号分句后又按逗号再切) | 回到定参顺序:窗口至少装下一条完整证据句 |
| 延迟超预算 | 窗口数乘单窗延迟的乘数账(本节第一部分) | 先加大步长减窗数再谈聚合开销;证据定位任务慎砍重叠 |
| 全部窗口都答「不知道」 | 题面与内容是否匹配(拿英文题面扫中文文档) | 先修题面与语种;全「不知道」是输入侧事故不是模型事故 |
| 聚合后概率总是偏低 | 「不知道」窗口被摊薄进平均而非跳过 | 检查聚合实现,跳过规则见本节第三部分 |
| 逐窗结果直接进了下游 | 消费方拿窗口概率当结论 | 收口:下游只认聚合分布,逐窗只做回溯与调优 |
输入侧的长度问题解决了,还有输出侧的规模问题:选项从四个涨到一百二十个,一次前向的选项面就装不下了。下一节先讲 budget——给算力画一条预算线。