ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Search-o1:让长推理模型边推理边检索的Reason-in-Documents机制

Search-o1:让长推理模型边推理边检索的Reason-in-Documents机制

长推理模型有个很尴尬的处境:它能把一道数学题拆成十几步推导,但每一步用到的中间事实,全靠参数里那点"记忆"。一旦题目涉及最新事件、冷门论文、具体数值,模型就开始一本正经地编。Search-o1 这个工作就是冲着这个痛点来的——它让模型在推理链的中间环节,自己判断"我这里缺信息了",然后主动发起检索,把搜回来的文档消化成推理能直接用的形式,再接着往下推。这套机制叫 Reason-in-Documents,是整篇论文最值得琢磨的部分。

我把它当成一个"给长推理装外挂"的案例来拆。适合已经跑过基础 RAG、想搞清楚"检索怎么嵌进推理循环"的人,也适合刚接触 agentic search、想知道工程上到底难在哪的人。下面按我自己的理解顺序讲,不按论文目录走。

1. 长推理为什么会"断粮"

1.1 参数记忆的边界在哪里

先想清楚一个基本事实:大模型的参数是训练数据的压缩,不是数据库。压缩意味着有损,有损意味着细节会丢、会串、会过时。你问它"某篇 2024 年 3 月发的论文里那个 benchmark 的准确率是多少",它要么答不出,要么给你一个"看起来很像"的数字。这不是模型不聪明,是信息根本不在它手里。

长推理把这个问题放大了。短问答里模型编一个数字,你一眼能看出来。但长推理是链式的,第 3 步编了一个错的前提,第 4 到第 15 步全在这个错前提上继续推,最后得出一个逻辑自洽但事实错误的结论。这种错误特别隐蔽,因为推理过程看起来严丝合缝。我见过不少 case,模型洋洋洒洒推了 20 步,每一步都"合理",但源头那个事实是幻觉,整条链就废了。

所以长推理对"事实供给"的要求,比短问答高得多。它需要的是:在正确的时机、拿到正确的信息、以正确的形式喂进去。

1.2 传统 RAG 为什么接不住长推理

传统 RAG 的流程是"先检索、后生成":用户提问 → 检索 top-k 文档 → 拼进 prompt → 模型一次性生成答案。这个流程对"单跳问答"够用,对长推理就不行了。

问题出在"一次性"上。长推理的信息需求是动态的:推到第 5 步才发现需要某个定义,推到第 9 步才发现需要某个对比数据。你在开头一次性检索,根本不知道后面会缺什么。检索早了,信息用不上还占上下文;检索晚了,模型已经在编了。

更麻烦的是,传统 RAG 检索回来的是一整段文档,直接塞进 prompt。长推理的上下文本来就紧张,塞几段原文进去,推理链本身的空间就被挤没了。而且原文里大量内容和当前推理步骤无关,模型还得自己过滤,等于把"信息筛选"的负担又丢回给模型。

Search-o1 的思路就是把这套"先检索后生成"改成"边推理边检索",并且专门解决"检索回来的东西怎么用"这个问题。

2. Search-o1 的两段式结构拆解

2.1 主推理模型和检索的协作关系

Search-o1 的架构可以理解成两个角色:一个负责"想",一个负责"查"。主推理模型(reasoning model)负责生成推理链,它有一个特殊能力——在推理过程中输出一个检索触发信号。一旦触发,系统暂停推理,去执行检索,把结果处理后交回主模型,主模型接着往下推。

这个"暂停-检索-恢复"的循环,是它和传统 RAG 最本质的区别。推理不是一次成型的,是被检索切成一节一节的。每一节推理结束后,模型有机会"抬头看看"自己缺不缺东西。

触发信号怎么来?论文里的做法是让模型在推理链中显式地生成一个查询(query),这个 query 就是它当前步骤需要的信息的自然语言描述。比如推到"需要确认这个定理的适用条件"时,模型会输出一个类似"XX 定理的适用条件"的查询。这比固定规则触发灵活得多,因为只有模型自己知道它下一步要什么。

2.2 Reason-in-Documents:检索结果不是拿来就用

这是我觉得整篇论文最核心的贡献。检索回来的文档,Search-o1 不是直接拼进上下文,而是先做一次"精炼"。

具体做法是:把检索到的文档和当前推理上下文一起,交给模型做一次"reason in documents"的处理。模型的任务是从文档里抽出和当前推理步骤真正相关的信息,并且把它组织成推理链能直接续上的形式。输出的不是原文摘录,而是经过理解的、和当前推理语境对齐的中间结论。

为什么这一步重要?因为检索回来的文档是"外部视角"写的,它的组织逻辑和当前推理链的逻辑不一样。直接拼接,模型得自己做一次"翻译",这个翻译过程容易出错,也浪费上下文。Reason-in-Documents 相当于把这个翻译工作显式化、专门化,让模型在一个受控的步骤里完成,而不是混在长推理里顺手做。

我打个比方:传统 RAG 像是把一本参考书直接扔给你,让你边写论文边翻。Reason-in-Documents 像是先让一个助手把参考书里相关的那两页读一遍,用你自己的论文逻辑给你讲一遍,你再接着写。显然后者更省心,也更不容易跑偏。

2.3 两个阶段的分工边界

整个流程可以拆成两个阶段:

  • 推理阶段:主模型生成推理链,遇到信息缺口时输出查询,暂停。
  • 检索与精炼阶段:执行检索,对每篇文档做 Reason-in-Documents 处理,把精炼结果合并后交回主模型。

这两个阶段的边界很关键。推理阶段只负责"想",不负责"查";检索阶段只负责"查和整理",不负责"下结论"。职责分离的好处是每一步都可控、可调试。如果最终答案错了,你能定位是推理错了还是检索错了还是精炼错了,而不是一锅粥。

3. Reason-in-Documents 到底在做什么

3.1 从"文档检索"到"信息蒸馏"

Reason-in-Documents 的本质是一次信息蒸馏。输入是若干篇检索文档加当前推理上下文,输出是一段精炼后的、可直接续接推理的信息。

蒸馏的目标有三个:去噪、对齐、压缩。去噪是把文档里和当前步骤无关的内容扔掉;对齐是把文档的表达方式转成当前推理链的语言风格和逻辑结构;压缩是把长文档压成推理能消化的短片段。

这三个目标里,对齐最容易被忽略,但最重要。举个例子,文档里写"该方法在 F1 上达到 0.87,相比 baseline 提升 3 个点",而当前推理链在讨论"这个方法是否适合处理类别不平衡数据"。直接拼接,模型得自己意识到"F1 提升"和"类别不平衡"之间的关系。Reason-in-Documents 会直接输出"该方法在类别不平衡场景下 F1 达 0.87,说明它对不平衡数据有一定鲁棒性"——把文档事实和推理需求接上了。

3.2 精炼过程中的上下文管理

长推理的上下文是稀缺资源。Search-o1 在精炼时有个隐含的取舍:精炼结果要足够短,短到不挤占推理空间;又要足够全,全到推理不用再回头查。

这个平衡怎么把握?论文里的做法是让精炼步骤也由模型完成,模型自己判断"哪些信息是当前推理必需的"。这其实是在赌模型的判断力。实测中这个赌注基本成立,因为模型在"当前推理需要什么"这件事上有天然的优势——它自己就是推理的执行者。

但这里有个坑:如果精炼步骤本身也幻觉了,错误会被直接注入推理链,而且因为经过了"精炼"这道工序,看起来更可信。所以精炼步骤的可靠性,直接决定整个系统的可靠性。工程上通常会给精炼步骤加约束,比如要求它引用原文片段、要求它标注信息来源,降低幻觉风险。

3.3 多文档精炼结果的合并策略

一次检索可能返回多篇文档,每篇都做一次 Reason-in-Documents,得到多个精炼结果。这些结果怎么合并?

简单拼接是最直接的做法,但会有冗余和冲突。两篇文档可能说了同一件事,也可能说了矛盾的事。Search-o1 的处理是让模型在精炼时就把"这篇文档提供了什么"说清楚,合并时按信息点去重,冲突的地方保留双方并标注。

我自己的经验是,合并策略对最终效果影响很大。如果只是拼接,模型在后续推理里可能被冗余信息干扰;如果过度合并,又可能丢掉细节。一个实用的折中是:按"信息点"而不是"文档"来组织精炼结果,每个信息点标注来源,冲突信息点并列保留。

4. 把 Search-o1 跑起来的关键环节

4.1 检索触发时机的判断

触发太频繁,推理被切得太碎,上下文里全是检索结果,推理链本身反而被淹没。触发太少,模型该查的时候不查,继续编。

论文里靠模型自己生成查询来触发,这依赖模型的"自知之明"。实际用下来,触发质量跟模型能力关系很大。能力强的模型能比较准地判断"我这里确实不知道",能力弱的模型要么不触发,要么乱触发。

工程上可以加一些辅助信号:比如推理链里出现具体数值、具体日期、具体人名时,提高触发倾向;出现"根据常识""一般来说"这类模糊表述时,也提示可能需要检索。这些规则不是替代模型判断,是给模型判断加一层兜底。

4.2 查询构造的质量控制

模型生成的查询,质量参差不齐。有的查询很精准,直接命中目标文档;有的查询太宽泛,检索回来一堆无关内容;有的查询太具体,检索不到东西。

查询构造的质量,直接决定检索质量,进而决定整个系统的上限。我见过的一个实用技巧是:让模型生成查询时,同时生成"这个查询想找什么类型的信息"的说明。检索时可以用这个说明做过滤或重排,提高命中率。

另一个技巧是查询改写。模型第一次生成的查询可能不理想,可以让它对查询做一次自我改写,或者生成多个候选查询并行检索,再合并结果。代价是检索次数增加,延迟上升。

4.3 精炼结果注入推理链的方式

精炼结果怎么交回主模型,也有讲究。直接追加到推理链末尾是最简单的,但可能打断推理的连贯性。更好的做法是把它作为"参考信息"标注出来,让模型知道"这是外部信息,不是我自己推出来的",避免模型把外部信息和自己的推理混为一谈。

这个标注很重要。如果外部信息和推理链混在一起,后续推理可能把外部信息当成自己已经确认的前提,一旦外部信息有误,错误就被"洗白"了。显式标注能让模型在后续推理里对这部分信息保持一定的"警惕"。

5. 实测中容易踩的几个坑

5.1 检索延迟拖垮推理体验

长推理本来就慢,每一步都要等模型生成。如果中间还插入检索,延迟会成倍增加。一次完整推理如果有 5 次检索触发,每次检索加精炼要几秒,整体延迟就很可观了。

缓解办法有几个:并行检索(多个查询同时发)、缓存(相同查询直接命中)、异步预取(根据推理进度预测下一步可能需要的查询,提前检索)。这些都需要工程上的额外投入,不是模型层面能解决的。

5.2 精炼步骤的幻觉放大

前面提过,精炼步骤幻觉是致命的。我实测中遇到过精炼结果"添油加醋"的情况:原文只说"在某个数据集上表现良好",精炼后变成"在所有数据集上都达到 SOTA"。这种放大在后续推理里几乎无法察觉。

对策是给精炼步骤加"引用约束":要求精炼结果必须能对应到原文的具体片段,不能引入原文没有的结论。实现上可以让精炼步骤输出时带上原文片段的位置或原文句子,便于核查。

5.3 推理链被检索结果"带偏"

检索结果有时候会把推理带向一个原本没打算走的方向。比如推理本来在讨论方法 A 的局限性,检索回来一篇讲方法 B 的文档,模型可能就被带过去讨论 B 了。

这个问题的根源是精炼结果和当前推理步骤的对齐不够紧。如果精炼时严格以"当前推理步骤需要什么"为准,就不容易被带偏。所以精炼步骤的 prompt 设计很关键,要反复强调"只提取和当前步骤相关的信息"。

6. 这套思路能迁移到哪里

Search-o1 的价值不只是一个具体系统,更是一套可迁移的模式:把"检索"从生成的前置步骤,变成生成过程中的一个可调用能力。

这个模式可以迁移到很多场景。比如代码生成,模型写到一半发现需要查某个 API 的用法,主动触发检索;比如数据分析,模型推到某一步发现需要某个统计口径的定义,主动查;比如多轮对话,模型发现用户提到一个自己不确定的实体,主动查证。

迁移的关键是两件事:一是让模型有能力判断"我什么时候需要外部信息",二是让外部信息能以"推理友好"的形式注入。前者靠模型能力,后者靠 Reason-in-Documents 这类精炼机制。这两件事做好了,检索就不再是外挂,而是推理的一部分。

我自己在做的几个小项目里,把类似机制用在了文档问答和代码辅助上,效果比传统 RAG 稳不少,尤其是在需要多步推理的场景。代价是系统复杂度上去了,延迟也上去了,得看具体场景值不值得。

最后分享一个实操体会:这套东西调起来,最花时间的不是模型本身,是精炼步骤的 prompt。精炼 prompt 写得好,整个系统就顺;写得糙,检索回来的信息要么用不上,要么用错。我一般会准备一批"推理中途缺信息"的测试 case,专门盯精炼步骤的输出质量,反复迭代 prompt,直到精炼结果能稳定地"接上"推理链。这个迭代过程没什么捷径,就是磨。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进