第 6 章 · Worker 的内心戏:ReAct

一句话结论:Supervisor 派活给 Worker 之后,Worker 内部不是「一步出结果」,而是跑一个 ReAct 循环——Reasoning(推理)+ Acting(调用工具)交替进行,直到它自己判断「这步做完了」。这是单 agent 的「最小工作单元」,也是协调链条上的「原子能力」。


1. 为什么 Worker 不能「一步到位」?

假设 Supervisor 派给 Researcher 一步:「查清 OpenAI 和 Google 在视频生成上的最新进展」。

最朴素的 Worker,是一步出结果:

def naive_worker(step):
    return llm(step.description)    # 直接问模型,一次返回

问题:模型不知道「最新进展」,它只能靠训练时学到的旧知识瞎编(幻觉)。要拿到真实信息,Worker 必须自己会调用工具(搜索、抓取),而且往往一次搜索不够——搜到一篇,发现新线索,再搜,再读。这是一个「多轮」的过程。

ReAct 就是为这个「多轮 + 用工具」的过程设计的骨架。


2. 伪代码:ReAct 循环

def react_worker(step, tools, max_rounds=10):
    ctx = [system_prompt, step.description]     # 只有「这一步」的上下文
    for round in range(max_rounds):
        action = llm(ctx)                        # ① 推理:决定下一步干什么
        if action.type == "tool_call":
            result = tools[action.name](action.args)   # ② 行动:调用工具
            ctx.append(f"工具 {action.name} 返回: {result}")  # ③ 观察:把结果塞回上下文
        elif action.type == "finish":
            return action.answer                  # ④ 收敛:模型自己判断「够了」
    return llm(ctx).answer                        # 轮数用尽,强制收尾

拆开看,ReAct 循环就是四个动作的交替:

  1. 推理(Reasoning):模型看当前上下文,决定「下一步该调用哪个工具、传什么参数」,或者「该收工了」。
  2. 行动(Acting):真正调用工具(搜索、抓取、跑代码)。
  3. 观察(Observation):把工具返回的结果写回上下文,成为下一轮推理的输入。
  4. 收敛(Finish):模型判断信息够了,给出最终答案。

为什么是「循环」而不是「一条直线」:因为研究是一个结果驱动、不可预知步数的过程。你事先不知道要搜几次、读几篇,所以不能写死「搜 3 次然后总结」,只能让模型「边干边判断」。


3. 思考的分叉:ReAct 的几个变体,以及为什么 DeerFlow 用它

方向 A:纯推理(不用工具)——就是上面的 naive_worker,会幻觉,研究任务不可用。

方向 B:Plan-and-Execute(先规划再执行)——Worker 先列一个子计划,再一步步执行。

  • 更适合「步骤本身还能再细分」的复杂子任务;但多了一层规划开销。

方向 C:ReAct(边推理边行动)——每一步都即时判断。

  • 优点:简单、通用、自适应,能处理「搜到什么才知道下一步搜什么」这种强反馈的任务。
  • 缺点:可能「打转」(在几个工具间来回调却不收敛)。

为什么 DeerFlow 1.x 的 Worker 用 ReAct:

  • 因为 Worker 干的是「检索/计算」这类强反馈、短平快的子任务,ReAct 的「即时判断」正好匹配。
  • 而且注意:DeerFlow 是「外层 Plan-and-Execute(Planner 出 Plan)+ 内层 ReAct(Worker 干活)」的组合。这很关键——大结构用 Plan 保证收敛,小执行用 ReAct 保证灵活。两种模式不是二选一,而是分层嵌套。

4. ReAct 循环带来的「协调隐患」

ReAct 灵活,但也给协调埋了两个雷,这两个雷在真实 issue 里都爆过:

  1. 不收敛(打转):模型在「搜索→读→再搜索」里绕圈,max_rounds 是最后一道防线,但模型可能编造「我搜到了」来应付。
  2. 上下文膨胀:每轮 ctx.append 都在变长,一个 Worker 跑几十轮,它自己的窗口也可能爆掉。

这直接引出了下一章的主题——上下文管理。协调不只是「把任务分出去」,更是「管好每个 agent 自己的上下文,以及 agent 之间传递的信息量」。

类比:ReAct 是一个工人「接到任务后,自己查资料、自己动手、干到认为完成为止」的循环。但工人干活的桌上不能堆满废纸——既要让他能查资料,又要防止他桌上堆成垃圾山。这就是「上下文带宽」的问题。


思考题

Q:max_rounds 用尽后,代码里是「强制让模型收尾」。但为什么不能更聪明一点——比如轮数用尽时,让模型把「还没查完的部分」上报给 Supervisor,而不是硬着头皮给个可能不完整的答案?

参考答案:

这个想法是对的,而且指向了一个更成熟的协调设计——让 Worker 显式上报「未完成的原因」,而不是伪装完成。

硬收尾的风险在于:模型为了「完成」,可能给出一个看似完整实则缺失的答案,这个缺失会悄悄污染后续的 Reporter。而「上报未完成」把问题显式化了:Supervisor 看到「这一步没做完,原因是 X」,就能决定——重试、换工具、还是降级处理(在报告里标注「此部分信息不足」)。

这里有个深刻的工程原则:协调系统里,最危险的不是「失败」,而是「假成功」。 失败可以被检测、被兜底;但一个伪造的「成功」会顺着协调链路流下去,直到最终报告里出现一处编造的事实。所以好的 Worker 协议,一定要求**「完成」和「未完成」都是显式的一等状态**,而不是让模型自由发挥。这也呼应了第 3 章「结构化契约」的核心:协调靠的是显式状态,不是心照不宣。