2871 行的 research pipeline:阶段流怎么走

2026-08-10

deeptutor/agents/research/pipeline.py 是 DeepTutor 的 agents 目录里最大的一个文件,2871 行。同目录下第二大的 question/pipeline.py 是 2161 行,第三是 chat/agentic_pipeline.py 的 1563 行。research 这个子包整体 11 个文件、5830 行,是八个子包里行数最多的一个。

行数大不代表逻辑复杂到读不动。这个文件真正需要搞清楚的只有一件事:它的”四阶段”里,第三阶段不是一趟走完的直线。下面按行号把阶段流拆开,所有数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,常量名和标签名是稳的。

一、阶段名在两个地方各写了一遍

第一处是 research/pipeline.py:1-31 的文件 docstring,把阶段写成 Rephrase → Decompose → Research blocks → Reporting 四段。

第二处是 research/capability.py:37DeepResearchCapability,它的 manifest 里 stages 字段是 ["rephrasing","decomposing","researching","reporting"],同时声明 tools_used=["rag","web_search","paper_search","code_execution"],CLI 别名是 researchresearch/capability.py:38-45)。

两处的四个阶段名是对得上的:docstring 在 research/pipeline.py:1-31,manifest 在 research/capability.py:38-45。这两处分别由谁读取、各自流向哪里,我们没有核,不做推断。

类本身在 research/pipeline.py:285class ResearchPipeline

二、八个标签词,分成两种用法

这个 pipeline 的调度不是靠函数调用顺序,而是靠一套标签协议。标签词表一共八个(research/pipeline.py:117-125):

THINK / TOOL / APPEND / FINISH / OUTLINE / INTRO / SECTION / CONCLUSION

八个词分成两种用法,混在一起看会绕:

用法标签出现在哪一段
循环体内的轮次标签THINK / TOOL / FINISH第一阶段与第三阶段的循环
块循环专有的中间标签APPEND只出现在第三阶段的块循环,由 _BlockLoopHost.on_intermediate 处理
一次性 labeled stepOUTLINE / INTRO / SECTION / CONCLUSION第二阶段与第四阶段

两点需要单独说明。一是 APPEND 不要跟着 THINK/TOOL/FINISH 一起理解成通用轮次标签,它绑定在第三阶段块循环的 _BlockLoopHost.on_intermediate 上,第一阶段的改写循环没有它。二是 FINISH 不只在循环里出现——第四节要讲的 Note Agent 就是一个只带 FINISH 一个标签的单步,同一个标签词在这个 pipeline 里既做循环的收尾轮,也做单步的产出标记。

至于一次性 labeled step 那一行:OUTLINE 用在第二阶段(把改写后的题目拆成子主题),INTRO / SECTION / CONCLUSION 按标签名与 docstring 的语义读,对应的是第四阶段报告的引言、各节与结论——这一层归属是按名字读出来的,不是我们从调用点核到的。可以确定的是:这四个都是单步产出、不带循环,真正的循环只发生在第一阶段和第三阶段。

对应的 LoopHost 也只有两个:_RephraseLoopHostresearch/pipeline.py:2642)与 _BlockLoopHostresearch/pipeline.py:2278)。你在这个 2871 行的文件里找入口,认这两个类名比按阶段顺序往下翻要快。

三、反直觉的那一处:第三阶段会自己改写待办队列

APPEND 是这八个标签里唯一一个中间标签research/pipeline.py:17-23 的 docstring 写得很直白:APPEND_BlockLoopHost.on_intermediate 改写动态队列,外层调度器串行或并行地把队列抽干。也就是说这一轮不是往报告里添内容,而是往待办里添块,后追加的块自然会被后续批次捡走。

为什么说它反直觉:读到”四阶段”这个说法时,大多数人脑子里画的是一条从左到右的流水线,第三阶段是”把第二阶段拆出来的 N 个子主题挨个做完”。实际不是。第三阶段的待办清单在它自己运行的过程中是会变长的,第二阶段给出的子主题只是队列的初始内容,不是全集。

这条队列的实现是 class DynamicTopicQueue,它的 docstring 自称是”系统的核心记忆与调度中心”(research/data_structures.py:240-242)。块状态是一个四值枚举 TopicStatus:pending / researching / completed / failed(research/data_structures.py:44-51)。

约束这个队列的三个默认常量都在 research/pipeline.py:224-238

常量默认值
DEFAULT_INITIAL_SUBTOPICS5
DEFAULT_QUEUE_MAX_LENGTH8
DEFAULT_MAX_PARALLEL_TOPICS3

把三个数放在一起读才有意义:初始子主题是 5,而队列长度上限是 8——这两个数不相等,正说明队列被设计成运行中可以增长;上限 8 这个值就是给 APPEND 的追加划的硬上限。并行度 3 是主题块这一层的并发上限。

另外两个循环预算也在同一段:DEFAULT_REPHRASE_MAX_ITERATIONS = 8(第一阶段的循环上限)、DEFAULT_REPHRASE_MAX_ROUNDS = 3DEFAULT_BLOCK_MAX_ITERATIONS = 5(单个主题块的循环上限)。

这里要跟另一个”8”分清楚:单轮里的并行工具调用上限 MAX_PARALLEL_TOOL_CALLS = 8 定义在 deeptutor/core/agentic/tool_dispatch.py:47,chat / question / research 三个 pipeline 共用它。它管的是一轮之内同时发几个工具调用,和主题块级的并行度 3 是两个层级的东西,别混着算。

需要说明的是:以上都是源码里的默认配置,不是运行结果的保证。这些值该调成多少取决于你的题目和用法,仓库没有给通用建议值。

四、工具结果不会直接进报告,中间隔了一个压缩步骤

第三阶段的每次工具调用,结果都不是原样留到报告里的。这条链上有两道明确的削减:

第一道在数据结构上。 ToolTrace 会限制 raw_answer 的大小,超过 DEFAULT_RAW_ANSWER_MAX_SIZE = 50 * 1024 就截断,并把 raw_answer_truncated 置真(research/data_structures.py:63-89)。也就是说超过 50KB 的原始返回本身就已经不完整了。

第二道是 Note Agent。 它是一个只有 FINISH 一个标签的单步,把原始工具结果压成短摘要,供引用侧车使用。两个常量:DEFAULT_NOTE_MAX_TOKENS = 1500 是摘要的 token 预算,NOTE_RAW_INPUT_TRUNCATE_CHARS = 8000 是喂进摘要器之前对原始输出的截断长度(research/pipeline.py:194-221)。

把两道放一起看,可核查的结论是:进入引用体系的是摘要,不是原文,而且摘要器看到的输入本身也被截到 8000 字符。写这类 pipeline 的人经常想当然地以为”报告里引用的那段就是工具原样返回的那段”,在这里不成立。至于摘要之后引用编号与锚点怎么注入报告,逻辑在 research/utils/citation_manager.py(866 行),我们只核了它被谁引用,内部算法没读,不下结论。

顺带一个代价上的事实:这个摘要步骤本身是一次额外的 LLM 调用。question pipeline 那边有一个同类机制,源码里明写代价是”每个工具结果多一次主模型 LLM 调用”;research 这一侧的 Note Agent 是独立的单标签步骤,两者不是同一段代码,具体开销我们没有核实。

五、工具面被收窄了两次

第三阶段可用的工具不是”用户开了什么就有什么”。组合这一步由 compose_enabled_tools() 负责(在 deeptutor/agents/_shared/tool_composition.py),research 会把它的组合结果再用一个白名单收窄:RESEARCH_BLOCK_TOOL_ALLOWLIST,内容是 rag、web_search、paper_search、code_execution,加三个只读的 obsidian 工具(research/pipeline.py:97-112)。

同一个文件里还有一个 CITABLE_TOOLSresearch/pipeline.py:206-215),也就是”结果会被摘要并记进引用管理器”的那批工具。它和上面的白名单是同一个集合。这个等价关系本身是可核查的,含义也直白:能在块里用的工具,就是能产出引用的工具。

这里有一处清单不一致,按纪律只陈述、不推断:research/capability.py:38-45 的 manifest 里 tools_used 列了四个(rag / web_search / paper_search / code_execution);research/pipeline.py:97-112RESEARCH_BLOCK_TOOL_ALLOWLIST 在这四个之外还包含三个只读 obsidian 工具。两处的条目数不同。以我们实读的仓库状态为准,说完就停。

要提醒一句的是 code_execution:它在白名单里,也在可引用工具集合里。这条链上存在会执行代码、执行外部程序的工具——除了 code_execution,同一仓库的工具层还有一个 execdeeptutor/tools/exec_tool.py:57)。这些工具各自 execute() 的具体实现我们没有读,它们的执行环境与隔离方式我们也没有核,因此不做任何判断——但”这条流水线里挂着能执行代码的工具”这一点是可核查的,开不开取决于你的部署形态,不该被略过。

六、docstring 里提到的两个 research agent 类不存在

写 research 这一层时会撞上一处可核实的不一致。deeptutor/agents/__init__.py:5 的包 docstring 写的是 “research: Deep research agents (DecomposeAgent, ResearchAgent, etc.)”;而全仓 grep -rn "class DecomposeAgent\|class ResearchAgent" deeptutor/ 没有任何结果,deeptutor/agents/research/ 下也没有 agents/ 子目录。

这一点对读代码的人有直接影响:如果你按 docstring 的说法去找”负责拆解的那个 agent 类”,是找不到的。这一层的阶段逻辑落在 pipeline 的 labeled step 和两个 LoopHost 上,不是一组 agent 类。按纪律,我们只标明两处位置、陈述差异,不推断原因,也不据此评价项目。

七、四种模式与一句必须说清的话

research 支持四种模式,由 ModeStrategy 描述:notes / report / comparison / learning_path(research/mode_strategy.py:1-18)。

其中 learning_path 这个模式名,以及这个项目里其他与学习相关的机制,都是该项目自己的实现选择。它们写在代码里,是可以核查的事实;但”这样组织产出对学习是否有效”,不是本文能回答的问题,也不是这些常量能证明的事情。本文只讲这些参数写在哪一行、按名字语义如何运作,不评价其学习科学上的有效性,也不承诺任何学习效果。

八、你可以照着核的五步

  1. 打开 research/pipeline.py:1-31,读文件 docstring 的四阶段说明,重点看 Phase 3 那段对 APPEND 的描述。
  2. 打开 research/pipeline.py:117-125,把八个标签常量抄下来,对照第二节的表分清哪些在循环里、哪些是一次性 step。
  3. 打开 research/pipeline.py:224-238,核对 DEFAULT_INITIAL_SUBTOPICSDEFAULT_QUEUE_MAX_LENGTHDEFAULT_MAX_PARALLEL_TOPICS 三个值,确认初始值和上限值不相等。
  4. 打开 research/pipeline.py:97-112:206-215,比对白名单与 CITABLE_TOOLS 是不是同一个集合,再回 research/capability.py:38-45 比 manifest 的 tools_used
  5. 在仓库里搜 class DecomposeAgentclass ResearchAgent,确认搜不到,再回 deeptutor/agents/__init__.py:5 看那行 docstring。

最后交代边界:这 2871 行我们只精读了头部常量区、类定义位置与两个 LoopHost 的段落,中间的提示词组装、报告拼装、引用锚注入逻辑没有逐行核;deeptutor/core/agentic/run_agentic_looprun_labeled_step 的内部实现也没有读,凡是涉及”跑起来之后循环具体怎么退出”的部分,本文一律不下结论。文中所有阈值与默认值都是源码里的默认配置。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。