Recursive Language Models 实战:递归处理超长外部文本

把二十万行日志、几百份会议纪要或一整套代码仓库塞进大模型,然后问一句"找出所有相互矛盾的变更",通常会同时撞上三个问题:输入可能超过上下文窗口;即使能够装下,模型也未必能稳定利用位于中段的信息;一次超长请求还会把无关内容一起计费。扩大上下文窗口解决的是"最多能接收多少 token",却不自动解决"应该读哪一段、读几次、如何核对遗漏"。

Recursive Language Models(RLM,递归语言模型)换了一个视角:不把全部文本当作模型提示词,而是把它留在外部环境中,模型只看到文本的规模、索引和少量预览。它可以用代码搜索、切片、统计,再把相关片段交给子模型分析;子模型仍可继续分解,最终由根模型归并答案。论文把这种机制描述为一种推理时脚手架,而不是新注意力架构,也不是某个特定模型的名称。

这篇文章从一个真实需求出发:在一批超长运维记录中回答"某次故障的完整演化链是什么",并要求每个结论都能回到原始行号。我们会实现一个受预算约束的递归处理器,验证它有没有漏证据,同时把代码执行、提示注入、递归爆炸和成本失控等生产风险说清楚。

1. RLM到底改变了什么

普通长上下文调用的结构是"问题加全文一起进入模型"。检索增强生成则先用向量或关键词挑出若干片段,再让模型回答。RLM位于两者之间又不等同于任何一个:它让模型参与"怎样读取上下文"的决策,并允许通过程序对全文做精确操作。
#mermaid-svg-at9AAhpdYCNMTh1H{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-at9AAhpdYCNMTh1H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-at9AAhpdYCNMTh1H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-at9AAhpdYCNMTh1H .error-icon{fill:#552222;}#mermaid-svg-at9AAhpdYCNMTh1H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-at9AAhpdYCNMTh1H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-at9AAhpdYCNMTh1H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-at9AAhpdYCNMTh1H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-at9AAhpdYCNMTh1H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-at9AAhpdYCNMTh1H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-at9AAhpdYCNMTh1H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-at9AAhpdYCNMTh1H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-at9AAhpdYCNMTh1H .marker.cross{stroke:#333333;}#mermaid-svg-at9AAhpdYCNMTh1H svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-at9AAhpdYCNMTh1H p{margin:0;}#mermaid-svg-at9AAhpdYCNMTh1H .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-at9AAhpdYCNMTh1H .cluster-label text{fill:#333;}#mermaid-svg-at9AAhpdYCNMTh1H .cluster-label span{color:#333;}#mermaid-svg-at9AAhpdYCNMTh1H .cluster-label span p{background-color:transparent;}#mermaid-svg-at9AAhpdYCNMTh1H .label text,#mermaid-svg-at9AAhpdYCNMTh1H span{fill:#333;color:#333;}#mermaid-svg-at9AAhpdYCNMTh1H .node rect,#mermaid-svg-at9AAhpdYCNMTh1H .node circle,#mermaid-svg-at9AAhpdYCNMTh1H .node ellipse,#mermaid-svg-at9AAhpdYCNMTh1H .node polygon,#mermaid-svg-at9AAhpdYCNMTh1H .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-at9AAhpdYCNMTh1H .rough-node .label text,#mermaid-svg-at9AAhpdYCNMTh1H .node .label text,#mermaid-svg-at9AAhpdYCNMTh1H .image-shape .label,#mermaid-svg-at9AAhpdYCNMTh1H .icon-shape .label{text-anchor:middle;}#mermaid-svg-at9AAhpdYCNMTh1H .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-at9AAhpdYCNMTh1H .rough-node .label,#mermaid-svg-at9AAhpdYCNMTh1H .node .label,#mermaid-svg-at9AAhpdYCNMTh1H .image-shape .label,#mermaid-svg-at9AAhpdYCNMTh1H .icon-shape .label{text-align:center;}#mermaid-svg-at9AAhpdYCNMTh1H .node.clickable{cursor:pointer;}#mermaid-svg-at9AAhpdYCNMTh1H .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-at9AAhpdYCNMTh1H .arrowheadPath{fill:#333333;}#mermaid-svg-at9AAhpdYCNMTh1H .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-at9AAhpdYCNMTh1H .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-at9AAhpdYCNMTh1H .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-at9AAhpdYCNMTh1H .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-at9AAhpdYCNMTh1H .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-at9AAhpdYCNMTh1H .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-at9AAhpdYCNMTh1H .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-at9AAhpdYCNMTh1H .cluster text{fill:#333;}#mermaid-svg-at9AAhpdYCNMTh1H .cluster span{color:#333;}#mermaid-svg-at9AAhpdYCNMTh1H div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-at9AAhpdYCNMTh1H .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-at9AAhpdYCNMTh1H rect.text{fill:none;stroke-width:0;}#mermaid-svg-at9AAhpdYCNMTh1H .icon-shape,#mermaid-svg-at9AAhpdYCNMTh1H .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-at9AAhpdYCNMTh1H .icon-shape p,#mermaid-svg-at9AAhpdYCNMTh1H .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-at9AAhpdYCNMTh1H .icon-shape .label rect,#mermaid-svg-at9AAhpdYCNMTh1H .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-at9AAhpdYCNMTh1H .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-at9AAhpdYCNMTh1H .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-at9AAhpdYCNMTh1H :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 关键词/正则/统计
按章节或时间切分
否
是
超长文本留在外部存储
根模型读取元数据与问题
选择操作
程序筛选候选片段
生成子任务
子模型语义分析
证据是否充分
根模型归并答案
结论与原始位置引用

关键区别可以概括为"变量空间"和"token空间"的分离。全文是程序中的变量,只有被选中的切片进入某次模型调用。根模型不需要记住每一个字符,但必须知道自己能执行哪些受限操作、已经检查过哪些范围、剩余预算是多少。这样做不会创造真正的无限上下文:磁盘、执行时间、子调用次数和最终汇总窗口依然有限。它只是把一次难以控制的巨大调用改造成若干可观察、可中止的小调用。

RLM也不是"RAG的替代品"。对稳定知识库中的普通事实问答,预先构建的向量索引更快、更便宜;对需要遍历所有记录、做跨段聚合或动态决定下一步搜索方向的任务,递归程序更有优势。实际系统完全可以先用数据库和检索器缩小范围,再让RLM处理剩余的复杂推理。

2. 为什么装得下仍然可能答不好

上下文窗口是硬上限,信息利用率是另一个问题。"Lost in the Middle"研究表明,相关证据在长输入中的位置会影响模型表现,位于开头或结尾的信息往往比中间信息更容易被使用。更大的窗口因此应被理解为容量,不应被当作可靠检索和覆盖率的保证。

长文本任务大致有三类。第一类是稀疏定位,例如从百万行日志中寻找唯一请求ID,正则表达式通常比模型更合适。第二类是局部语义判断,例如判断十段报错是否属于同一根因,可以先程序过滤再交给模型。第三类是密集聚合,例如比较每个月的规则变化,必须让全部相关分区都被处理,不能只取相似度最高的几块。RLM真正有价值的地方,是允许一条轨迹混合这些操作,而不是强迫所有问题都走同一种切块策略。

以故障时间线为例,直接搜索"数据库断连"可能漏掉更早出现的连接池耗尽和更晚出现的补偿任务积压。合理的递归策略是先定位告警附近的时间范围,再按服务拆分,让子调用分别提取"事实、时间、实体、证据位置",最后按时间排序并检查因果跳跃。如果答案声称A导致B,却没有任何中间事件或原始行号,系统应标为待确认,而不是让语言流畅度掩盖证据缺口。

3. 先建立可寻址的外部文本

外部环境不能只是一个巨大字符串。最少要有稳定的片段编号、原始偏移、来源文件和内容摘要,否则模型得到答案后无法回溯。下面的代码只用Python标准库,把多个文本文件切成带重叠的片段,并生成适合交给根模型的清单。它刻意不做embedding,因为按字符位置和关键词扫描已经能构成一个透明基线。

python 复制代码
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import hashlib
import re


@dataclass(frozen=True)
class Span:
    span_id: str
    source: str
    start: int
    end: int
    text: str


def normalize(text: str) -> str:
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    return "\n".join(line.rstrip() for line in text.splitlines())


def split_text(source: str, text: str, size: int = 6000, overlap: int = 500) -> list[Span]:
    if size <= 0 or not 0 <= overlap < size:
        raise ValueError("size必须为正数,overlap必须在[0, size)内")
    clean = normalize(text)
    spans: list[Span] = []
    start = 0
    while start < len(clean):
        end = min(start + size, len(clean))
        if end < len(clean):
            boundary = max(clean.rfind("\n", start, end), clean.rfind("。", start, end))
            if boundary > start + size // 2:
                end = boundary + 1
        piece = clean[start:end]
        digest = hashlib.sha256(f"{source}:{start}:{end}".encode()).hexdigest()[:12]
        spans.append(Span(digest, source, start, end, piece))
        if end == len(clean):
            break
        start = end - overlap
    return spans


def find_spans(spans: list[Span], pattern: str, limit: int = 30) -> list[Span]:
    regex = re.compile(pattern, flags=re.IGNORECASE)
    return [span for span in spans if regex.search(span.text)][:limit]


def load_corpus(folder: Path) -> list[Span]:
    result: list[Span] = []
    for path in sorted(folder.rglob("*.txt")):
        result.extend(split_text(str(path), path.read_text(encoding="utf-8")))
    if not result:
        raise RuntimeError("没有读取到UTF-8文本文件")
    return result


def _self_check() -> None:
    sample = "启动正常。\n" + "中间记录。\n" * 1000 + "ERROR request-42 数据库超时。"
    spans = split_text("demo.log", sample, size=800, overlap=80)
    hits = find_spans(spans, r"request-42")
    assert len(spans) > 1 and len(hits) == 1
    assert sample[hits[0].start:hits[0].end] == hits[0].text


if __name__ == "__main__":
    _self_check()

这里有两个容易忽略的设计。其一,span_id由来源与偏移计算,而不是简单使用列表序号;只要原文件和切分规则不变,引用就稳定。其二,重叠只是防止句子被机械截断,不应在统计时重复计数。后续聚合应依据原始事件ID或去掉重叠区,而不能把同一条日志当作两条证据。

真实项目还应记录语料版本,例如源文件SHA-256、生成时间和解析器版本。没有版本信息的引用只能证明"某个时候有一段文本",不能证明答案对应当前资料。PDF、网页和代码文件也应先被解析成统一Span结构,并保留页码、URL、提交哈希等原生位置。

4. 一个受控的递归执行器

论文和官方实现允许模型在REPL中生成代码并触发子调用,这种自由度很高,但在业务环境中不宜直接把任意代码交给宿主机执行。下面采用更保守的实现:调度逻辑由程序固定,模型只负责分析片段并返回结构化结果。它仍然具有递归分解、局部调用和逐层汇总三个RLM核心特征,却更容易审计。

示例把"模型调用"抽象成Model协议,便于接入本地模型或任意云端SDK。每次调用都消耗预算;达到最大深度、片段足够小或预算不足时立即停止继续分裂。为了让代码可静态检查和单元替换,模型返回普通字符串,生产实现则应要求JSON Schema并拒绝无法解析的输出。

python 复制代码
from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol


class Model(Protocol):
    def complete(self, prompt: str) -> str: ...


@dataclass
class Budget:
    calls_left: int
    chars_sent: int = 0

    def charge(self, text: str) -> None:
        if self.calls_left <= 0:
            raise RuntimeError("模型调用预算已耗尽")
        self.calls_left -= 1
        self.chars_sent += len(text)


def ask(model: Model, budget: Budget, prompt: str) -> str:
    budget.charge(prompt)
    return model.complete(prompt)


def recursive_analyze(
    model: Model,
    question: str,
    spans: list[Span],
    budget: Budget,
    *,
    depth: int = 0,
    max_depth: int = 2,
    leaf_chars: int = 18_000,
) -> str:
    if not spans:
        return "未发现候选证据"
    total = sum(len(item.text) for item in spans)
    if depth >= max_depth or total <= leaf_chars or len(spans) == 1:
        evidence = "\n\n".join(
            f"[{item.span_id} {item.source}:{item.start}-{item.end}]\n{item.text}"
            for item in spans
        )
        prompt = (
            f"问题:{question}\n"
            "只根据证据提取事实;每条事实必须引用方括号中的span_id。"
            "证据不足时明确写未知。\n\n" + evidence
        )
        return ask(model, budget, prompt)

    midpoint = len(spans) // 2
    left = recursive_analyze(
        model, question, spans[:midpoint], budget,
        depth=depth + 1, max_depth=max_depth, leaf_chars=leaf_chars,
    )
    right = recursive_analyze(
        model, question, spans[midpoint:], budget,
        depth=depth + 1, max_depth=max_depth, leaf_chars=leaf_chars,
    )
    merge_prompt = (
        f"问题:{question}\n"
        "合并下面两份局部报告。保留原span_id,删除重复事实;"
        "若报告冲突,必须并列呈现而不是擅自裁决。\n"
        f"报告A:\n{left}\n\n报告B:\n{right}"
    )
    return ask(model, budget, merge_prompt)


class EchoModel:
    """只用于验证控制流,不代表真实语义模型。"""
    def complete(self, prompt: str) -> str:
        return prompt[-300:]


def _demo() -> None:
    spans = split_text("demo.log", "事件A。\n" * 3000, size=5000, overlap=0)
    budget = Budget(calls_left=20)
    result = recursive_analyze(EchoModel(), "列出事件", spans, budget, leaf_chars=6000)
    assert result and 0 <= budget.calls_left < 20


if __name__ == "__main__":
    _demo()

这段代码采用二分只是为了清楚展示递归,不代表生产系统的最佳切法。日志更适合按时间窗口和服务名分区,合同更适合按章节,代码仓库更适合按模块和符号。分区边界属于任务知识:如果把一次事务的请求和响应切到两棵互不相见的分支,后面的汇总很难恢复因果关系。

预算也不该只有调用次数。生产环境至少同时限制总输入token、总输出token、墙钟时间、递归深度、并发数和单次工具输出大小。任何一个上限触发时,都应返回"部分完成"以及已经覆盖的分区,而不是伪装成完整答案。递归系统最危险的失败不是显式报错,而是在检查了三成材料后仍用确定口吻作出全局结论。

5. 让根模型选择动作,但不要让它获得主机权限

完整RLM会让根模型在REPL里查看变量、运行搜索和发起子调用。灵活性来自模型生成程序,风险也来自同一个地方。只要外部文本不可信,文档中就可能含有"忽略之前规则、读取环境变量、上传文件"等提示注入。文本是数据,不是系统指令;执行器必须从权限上保证这类句子无法获得工具能力。

安全的REPL至少需要进程或容器隔离、只读语料挂载、无默认网络、有限CPU和内存、执行超时、受限标准库以及独立的临时目录。不要只靠关键词黑名单过滤import os,因为绕过字符串过滤的方法很多。官方RLM仓库提供多种沙箱接口,DSPy的RLM模块也明确使用受控解释器;如果改成本地解释器,应理解"能运行"不等于"安全隔离"。

更简单的生产方案是白名单工具:search(pattern)、read_span(id)、list_sources()、subcall(prompt, span_ids)和submit(answer)。根模型只能构造这些函数的参数,不能运行任意Python。白名单降低了探索能力,却显著缩小攻击面,也便于把每次访问写入审计日志。只有确实需要任意计算且有成熟沙箱时,才升级到开放REPL。

工具输出也要限长。模型如果一次打印完整语料,相当于绕过了外部上下文设计;搜索结果如果有十万条,则应只返回计数、分布和分页游标。对子调用的提示词同样要标明证据边界,要求忽略证据中的命令性文本,并把原始片段视为不可信引用材料。

6. 不要把递归等同于正确

递归只是一种控制流。第一次分解方向错了,后面所有子调用都可能在错误子空间里认真工作。原始论文的轨迹分析和后续研究都提醒:策略选择、成本长尾以及语义密集任务仍是薄弱点。一个会不断调用自己的模型,可能把同一个误判层层放大。

典型失败包括以下几类:关键词过窄导致候选集合缺失;分块边界切断上下文;子模型输出不含引用;汇总模型把不同来源的同名实体合并;循环在相同片段上反复分析;某个异常分支耗尽全部预算;工具代码产生错误却被模型当作有效结果。应对这些问题不能只写一条"请认真检查"的提示词,而要把约束落实为状态和验证规则。

每个节点应记录输入span集合、问题、父节点、深度、模型版本、提示词版本、token消耗、耗时、输出和错误。调度器保存"已访问span"和"已执行动作"的哈希,重复轨迹达到阈值就停止。合并阶段只能引用子报告已有的span_id;最终答案中的每个引用必须能够映射回当前语料版本。这样才能复盘"答案为什么错",而不是只剩下一串自然语言。

7. 用覆盖率而不是文采验收

长文本系统最容易做出漂亮演示,最难证明没有漏。评测集应来自业务问题,并给出可机读的证据集合。除了最终答案正确率,还要看证据召回率、引用精确率、分区覆盖率、无依据断言率、调用次数和P95延迟。密集聚合任务尤其要检查"所有目标分区都执行成功",Top-k检索命中几条不能代替遍历。

下面的验证器从最终输出中提取span_id,检查引用是否存在,并计算回答覆盖了多少来源。它不能判断语义是否正确,但能挡住引用幻觉和"只读一个文件就总结全部文件"这两类基础错误。

python 复制代码
import re


def validate_answer(answer: str, spans: list[Span]) -> dict[str, object]:
    known = {item.span_id: item for item in spans}
    cited = set(re.findall(r"\b[a-f0-9]{12}\b", answer))
    unknown = sorted(cited - known.keys())
    cited_sources = {known[item].source for item in cited if item in known}
    all_sources = {item.source for item in spans}
    coverage = len(cited_sources) / len(all_sources) if all_sources else 0.0
    return {
        "citation_count": len(cited),
        "unknown_citations": unknown,
        "source_coverage": round(coverage, 4),
        "valid": bool(cited) and not unknown,
    }


def _validation_check() -> None:
    spans = split_text("a.txt", "已确认事件A。", size=100, overlap=0)
    report = f"事件A已确认 [{spans[0].span_id}]"
    result = validate_answer(report, spans)
    assert result["valid"] is True
    assert result["source_coverage"] == 1.0


if __name__ == "__main__":
    _validation_check()

更完整的离线测试应包含四组样本。第一组是"针在草堆",把唯一事实放在不同位置,验证定位不受位置影响。第二组是跨分区聚合,答案需要每个分区贡献一项,验证覆盖率。第三组是冲突证据,要求系统保留分歧和时间版本。第四组是对抗文本,在语料中放入提示注入、伪造工具返回和超长重复段落,确认沙箱和提示边界有效。

不要直接拿生产机密材料交给第三方评测模型。自动裁判适合辅助比较,不应成为唯一真值。对高风险结论,应由领域人员抽查原文;对计数、日期和标识符,应尽量由程序计算并让模型只负责解释。

8. RLM、长窗口、RAG和摘要链怎么选

如果资料能放入窗口、问题简单且调用频率低,直接长上下文是最短路径。若问题主要是从稳定知识库找局部事实,RAG通常更合适:索引可以复用,延迟和成本也更可预测。若任务是按固定维度总结每一份文件,普通Map-Reduce摘要链就够了,不必让模型动态写控制程序。

RLM适合的问题具备至少一个特征:输入明显超出有效窗口;不同问题需要不同读取策略;需要在精确程序操作与语义判断之间切换;子任务可递归拆解;或者必须对大量分区进行聚合。它不适合毫秒级在线接口、不允许任何代码执行的环境、缺少可验证答案的高风险自动决策,以及单次模型调用已经能稳定解决的普通任务。

一个务实的演进顺序是:先实现关键词搜索和可引用切片;再加入固定Map-Reduce;有证据表明固定流程无法覆盖多种问题后,才让模型选择工具;最后在沙箱、预算和评测成熟时开放更自由的REPL。RLM不是越"智能"越好,可靠系统往往从较窄的动作空间开始。

9. 上线时需要观察什么

线上不能只记录最终文本。一次轨迹至少要能回答:读了哪些来源,执行了哪些搜索,为什么产生子任务,哪个分支超时,子调用使用哪个模型,总成本在哪里产生。建议为每个节点生成trace_id和parent_id,将模型调用、工具调用、沙箱事件和预算变化串成一棵树。

告警规则应关注异常形状,例如递归深度突然增加、相同查询重复出现、无引用答案比例上升、某个来源从未被访问、平均成本正常但P99成本暴涨。RLM的费用分布可能有长尾,只看均值容易漏掉少量失控轨迹。限额必须由执行器强制,而不是由模型口头承诺。

缓存可以降低重复子调用,但缓存键必须包含模型版本、提示词版本、问题、span内容哈希和安全策略版本。只按文本片段缓存会把旧提示生成的结论混入新流程。包含个人信息或商业资料时,还要确保缓存与租户隔离,并提供按语料版本删除的能力。

10. 从演示到生产的最小清单

开始时不用复制论文中的全部能力。一个可用的第一版只需要稳定Span、有限搜索工具、固定深度递归、结构化子结果、预算和引用验证。随后用真实任务集衡量它是否优于"长窗口直接问"和"RAG Top-k"两个基线。如果没有明显收益,就保留更简单的基线。

生产检查可以归纳为六件事:语料有版本且引用可回溯;执行环境不信任文档内容;递归深度与总调用量有硬上限;失败返回覆盖范围而非伪完整答案;离线集同时评估答案和证据;线上轨迹可以重放。缺少其中任何一项,RLM仍可以做研究原型,却不应替人作出不可逆决策。

10.1 先分类问题,再决定是否递归

根节点收到问题后,不必立刻调用子模型。先做一次廉价的任务分类,往往能省掉大量无意义轨迹。查询若包含确定的请求ID、文件名或时间戳,程序直接精确检索;若要求"分别总结每个章节",使用固定分区映射;只有"找出异常模式并追踪相关证据"这类读取路径无法预先确定的问题,才交给动态规划器。

任务分类不能只依赖模型自述。可以从问题中提取结构信号:是否存在唯一标识符,是否要求计数,是否要求覆盖"全部",答案是否需要跨来源关联。带"多少、全部、每个"的问题具有全量语义,执行器应禁止只取Top-k后直接作答。带"是否存在"的问题可以在找到证据后提前停止,但若答案是否定,则必须证明搜索范围完整。

对问题进行这样的类型约束,还有一个好处:能明确何时由程序计算。总数、最大值、日期排序和集合差异不应该让模型凭文本估算。子模型负责把非结构化内容转成字段,聚合器用确定性代码计算,根模型只解释结果。这种"语义抽取加程序归并"通常比让汇总模型重新阅读所有局部报告更容易验证。

10.2 结构化子结果比自由摘要更可靠

自由文本在递归层级中会不断压缩细节。第一层把"不排除缓存故障"写成"可能是缓存",第二层又可能写成"缓存故障",到根节点已经丢失不确定性。子结果最好使用固定字段,例如claim、evidence_ids、time_range、confidence、unknowns和conflicts。合并器只能组合这些字段,不得创造新的证据ID。

置信度也不能让模型随意给一个百分数。更实用的是离散状态:直接证据、间接推断、相互冲突、材料不足。直接证据必须引用原文;间接推断要列出前提;冲突必须保存双方来源。最终展示时再把这些状态翻译成自然语言,用户能够看出哪些是事实,哪些只是合理假设。

结构化输出失败时不要用正则"尽量抢救"成看似合法的数据。执行器可以用明确错误提示重试一次,仍失败则把该分支标为失败并进入覆盖报告。如果默默丢掉一个无法解析的分支,最终答案就会产生难以发现的系统性遗漏。

10.3 并行不是越多越快

同一层的独立分区可以并发调用,但并发数受模型速率、沙箱资源和下游限额约束。一次把几百个片段全部发出,会造成限流重试、成本尖峰和结果乱序。更稳妥的方式是有限工作队列:调度器只保持少量在途任务,任一任务完成后再取下一个,并把剩余预算原子地扣减。

并行子调用结束后,不能按返回顺序拼接,因为网络快慢与原文顺序无关。每个结果应携带分区序号和原始范围,聚合前恢复稳定顺序。若一部分调用失败,可以只重试失败分区;已经成功的结果以输入哈希缓存,不必全部重跑。

递归成本可近似拆成根规划、叶子分析和逐层归并三部分。二分树的叶子越小,调用数量越多;叶子越大,单次上下文越长,模型更可能漏细节。最佳点由任务决定,不能从论文数字直接照搬。可以在固定验证集上尝试几组leaf_chars和max_depth,画出证据召回、总token和延迟的帕累托曲线,再选满足业务约束的配置。

10.4 更新语料时如何避免整棵树作废

长资料经常只修改少数文件。若缓存键基于整个语料哈希,一处小改动会让所有结果失效。更好的做法是以Span内容哈希为叶子键,父节点键由有序子节点哈希、问题和提示版本组合而成。没有变化的子树可以复用,受影响路径重新计算。

但缓存复用必须尊重撤权和删除。某份文档被用户撤销权限后,即使其旧摘要仍在缓存中,也不能参与回答。每次读取缓存都重新检查当前主体对所有来源的权限;跨租户缓存只允许保存完全公开的数据。语料删除时,按来源反向索引清理叶子和祖先,不能只删除原文件。

10.5 为"没找到"建立严格语义

RLM很容易把"这次搜索没有命中"说成"材料中不存在"。两者不是一回事。否定结论必须附带搜索方法、覆盖来源、时间范围、失败分区和截断信息。只要有一个必查分区超时,结果就应是"在已完成范围内未发现",而不是全局否定。

同样,程序搜索依赖解析质量。PDF文字提取失败、编码损坏或压缩包没有展开,都会制造虚假的空结果。建立索引时应记录每个来源的解析状态和字符数异常;问答前若目标范围含解析失败文件,先向用户暴露这一事实。把不可读文档算作"已搜索但无结果",会让审计链从起点就不可信。

10.6 人工接管不是失败,而是设计出口

高风险长文本分析应定义升级条件:关键证据冲突、引用不足、预算耗尽、对抗内容命中、安全策略阻止工具、或结果将触发不可逆操作。升级包不应只有一句"模型失败",而应包括问题、已覆盖来源、候选事实、冲突点和未完成分支,让人工从现有进度继续,而不是重新读全部资料。

升级时还要冻结当前语料版本和完整轨迹。若人工接管后资料又发生更新,必须明确区分"模型分析时看到的版本"和"人工最终采用的版本",否则同一引用可能已经指向不同内容。对外结论也应记录批准者与批准时间,避免把模型草稿误当成正式审计结果。

人工修正可以进入评测集,但不能未经审核直接变成提示模板。一次个案可能依赖隐含背景,把它硬编码为普遍规则会伤害其他任务。先归类失败原因,再决定修改解析器、分区策略、工具、提示还是模型。RLM的轨迹比单次调用更长,正因如此,错误归因也更值得结构化处理。

11. 总结

Recursive Language Models的核心不是"模型无限记忆",而是把长文本从提示词移到可编程环境,让模型按需读取、分解和递归调用。它用更多可控的推理步骤换取超长输入上的灵活性,也把新的复杂度带到调度、预算、沙箱和验证层。

真正可落地的RLM必须保留原始位置,限制递归与工具权限,显式报告未覆盖范围,并用证据召回率检验结果。对于普通查找,数据库或RAG更直接;对于需要动态探索和全局聚合的超长任务,RLM提供了一种值得测试的新控制方式。先用透明的小实现建立基线,再根据评测数据增加自由度,比一开始追求"近乎无限上下文"更可靠。

参考资料

  1. Recursive Language Models(原始论文,arXiv:2512.24601)
  2. RLM作者维护的官方实现
  3. RLM Minimal最小参考实现
  4. Alex L. Zhang:Recursive Language Models原始介绍
  5. DSPy官方RLM模块文档
  6. DSPy RLM源代码
  7. Lost in the Middle: How Language Models Use Long Contexts
  8. Self-Reflective Program Search for Long Context
  9. The Y-Combinator for LLMs: Solving Long-Context Rot
  10. Recursive Models for Long-Horizon Reasoning
相关推荐
雪雪9081 小时前
Work Agent深度解读:AI如何自主完成多步骤长程工作任务
人工智能
星云低代码开发平台1 小时前
AI 定时任务接 ERP:账号切换后的权限校验与异常状态设计
人工智能·定时任务·erp
I Am a robert girl1 小时前
从最后一帧反推世界:LIFT 如何用布局控制视频生成
深度学习·数码相机·计算机视觉·音视频·视频生成·可控生成·布局控制
烟雨江南7851 小时前
灵声智库-招投标会议语音识别怎么做?实时转写、多人发言与会议留痕方案
人工智能·音视频·语音识别·ai客服
李福春1 小时前
斗破Google ADK Java苍穹-第7境斗皇-御天地-Harness治理
java·人工智能·python·深圳同盟·腾讯云架构师技术同盟·adk java·harness 治理
看浪的路人1 小时前
第1讲:2026 大模型应用开发全景——Prompt / RAG / 微调 / Agent 到底先用哪个
人工智能·算法
wjkjpcba1 小时前
机器人控制板PCBA贴片厂家制造工艺解析:SMT贴片、DIP插件与检测流程
人工智能·机器人·smt贴片加工·pcba贴片加工厂
星核0penstarry1 小时前
企业接入多家 AI API,聚合平台能解决什么、不能解决什么
人工智能·大模型·ai编程·合规·api聚合平台·大模型网关·接入层
IT_陈寒1 小时前
为什么我的Vue组件总是莫名其妙更新?
前端·人工智能·后端