一次从 PCA 到 ReAct 的智能体重构

智能体架构ReActPlanningDAG软件架构

最近我把一个通用智能体系统的执行架构,从偏静态的 Planning -> Compiling -> Action,改成了以 ReAct 为主的行动模式。表面上是少了一层 Planner 和 Compiler,实际改的是系统处理“不确定性”的方式。

如果是定义清楚的专用流程,Planning-Compiling-Action 很自然:先把用户目标拆成步骤,再把步骤编译成 DAG,最后交给执行器跑。它像传统工作流引擎,结构清楚,可审计,也容易验证。

通用智能体的问题不是“能不能生成一个漂亮计划”。用户目标经常不完整,工具结果经常不稳定,网页、文件、数据库、模型输出都可能改变下一步判断。静态计划生成得越早,执行中失效的概率越高。

这篇文章记录这次重构里的设计取舍。示例代码来自真实项目的核心架构,但已经抽象成通用智能体代码,不包含业务实现。

两种模式怎么分工

我把旧架构称为 Planning-Compiling-Action,简称 PCA。

flowchart LR
  U[User Goal] --> P[Planning]
  P --> C[Compiling]
  C --> D[DAG]
  D --> A[Action Runtime]
  A --> R[Final Delivery]

PCA 的假设是:系统可以在执行前生成一个相对完整、稳定、可编译的计划。

ReAct 的假设相反:计划不应该一次性固定,智能体要在“思考、行动、观察”之间循环,根据观察结果调整下一步。

flowchart LR
  U[User Goal] --> T[Thought]
  T --> A[Action]
  A --> O[Observation]
  O --> T
  O --> F[Final Answer]

在工程实现里,两者都可能落到 DAG 或任务队列上执行。差别不在执行器,而在 DAG 是什么时候、基于什么信息生成的。

维度Planning-Compiling-ActionReAct
核心抽象先规划完整步骤,再编译执行图根据上下文选择下一批动作
适用任务稳定、可模板化、流程明确开放、探索、多轮修订、不确定性高
错误形态前期计划错误会传导到后续步骤每轮可根据观察修正
可审计性强,步骤和 DAG 边界清楚需要额外记录 thought/action/observation
工程复杂度Planner、Compiler、Validator 分层复杂单轮决策更直接,但状态管理更重要
用户体验像提交工作流后等待完成像一个助手边看结果边推进

我现在更愿意这样分工:PCA 像智能工作流编排,ReAct 像智能体运行时里的调度循环。前者适合可预测流程,后者适合通用任务。

旧架构:先规划,再编译

旧架构的第一层是 Planner。它的任务不是执行工具,而是把用户目标拆成语义步骤。

抽象后的 Planner 输出大致是这样:

export type PlannerStep = {
  title: string;
  action?: string;
  contentSpec?: {
    totalWords: number;
    sections?: Array<{ name: string; words: number }>;
  };
};

export async function planSteps(input: string, contextText: string) {
  const response = await chatComplete({
    temperature: 0.2,
    messages: [
      { role: "system", content: buildPlannerSystemPrompt() },
      { role: "user", content: `上下文:${contextText}\n\n用户需求:${input}` },
    ],
  });

  const parsed = plannerSchema.safeParse(extractJsonObject(response.text));
  return parsed.success ? parsed.data.steps : fallbackSteps(input);
}

这个设计的好处是语义层清楚。Planner 只关心“应该做什么”,不用关心每一步落到哪个工具、传哪些参数、怎么连依赖。

第二层是 Compiler。它把 PlannerStep 映射成可执行节点。

export type DagNode = {
  stepId: string;
  title: string;
  cellId: string;
  args: Record<string, unknown>;
  dependsOn: string[];
};

export async function compileStepsToDag(opts: {
  input: string;
  steps: PlannerStep[];
  catalog: CellCatalog;
  providedInputs?: Record<string, unknown>;
}) {
  const allowedCellIds = new Set(opts.catalog.cells.map((cell) => cell.cellId));

  const response = await chatComplete({
    temperature: 0.2,
    messages: [
      { role: "system", content: buildCompilerSystemPrompt(opts.catalog) },
      {
        role: "user",
        content:
          `用户需求:${opts.input}\n\n` +
          `planner steps:\n${formatSteps(opts.steps)}\n\n` +
          "请为每个步骤选择 cellId、args 和 dependsOn。",
      },
    ],
  });

  const parsed = dagSchema.safeParse(extractJsonObject(response.text));
  if (!parsed.success) return needInputOrFallback();

  return parsed.data.nodes
    .filter((node) => allowedCellIds.has(node.cellId))
    .map((node) => ({
      ...node,
      args: validateAndFixArgs(node.cellId, node.args, opts.catalog),
    }));
}

这套架构放在专用智能体里很好用。固定的数据处理、报告生产、审批辅助、文档流水线,都可以先规划,再编译,再执行。每一层职责也清楚:

职责
Planner将目标拆解为人能理解的步骤
Compiler将步骤映射为工具、参数和依赖
Validator检查参数、依赖和数据流
Runtime按 DAG 执行并收集结果
Finalizer汇总结果并交付

问题也来自这种分层。用户一旦在执行中补充新需求,或者某个工具返回了意外结果,系统就要重新规划、重新编译、取消旧任务、迁移上下文、处理版本号。架构开始变重。

PCA 的典型复杂度

PCA 模式最容易膨胀的地方是 Compiler。

一开始,Compiler 只是做步骤到工具的映射。后来它会被迫承担越来越多兜底逻辑:

需求Compiler 里的补丁
Planner 忘了字数或章节自动补充 contentSpec
模型选错工具修正明显的 cellId mismatch
参数名不一致query/prompt/task/description 互相映射
附件任务自动插入附件读取节点
长文档生成拆分章节节点
图表和正文顺序错后处理 DAG 依赖
数据流不完整运行前校验依赖

这些逻辑单独看都合理,堆在一起后,Compiler 就不再只是“编译器”。它会变成半个 Planner、半个 Policy Engine、半个 Error Recovery。

旧模式的启动链路也比较长:

const outcome = await specifyOutcome({ input, contextText });

const planned = await planSteps({
  input,
  contextText,
  traceTag: "agent.planner",
});

const compiled = await compileStepsToDag({
  input,
  contextText,
  steps: planned.steps,
  catalog,
  providedInputs,
  targetOutcome: outcome,
  traceTag: "agent.compiler",
});

await rebuildTasksForDag({
  runId,
  dagVersion: 1,
  nodes: compiled.nodes,
});

这条链路不是不能工作,而是把太多决策前置了。系统还没观察到真实工具结果,就试图把目标形态、执行步骤、工具选择、参数、依赖、交付结构一次性定下来。

对可预测任务,这很高效。对通用智能体,太早了。

新架构用 ReAct 生成行动 DAG

重构后的变化是:不再把“规划”和“编译”拆成两个大阶段,而是让模型在当前上下文下直接输出一组可执行 action。

抽象后的 schema 类似这样:

const actionSchema = z.object({
  thoughtSummary: z.string().min(1),
  actions: z.array(
    z.object({
      title: z.string().min(1),
      tool: z.string().min(1),
      args: z.record(z.any()).default({}),
      dependsOn: z.array(z.string()).default([]),
    }),
  ).min(1).max(8),
  finalInstruction: z.string().optional(),
});

对应的构建函数不再返回 PlannerStep,而是直接返回可执行节点和 ReAct 事件。

export async function buildReactDag(opts: {
  input: string;
  contextText?: string;
  catalog: CellCatalog;
  incremental?: boolean;
}) {
  const allowed = new Set(opts.catalog.cells.map((cell) => cell.cellId));

  let actions = buildHeuristicActions(opts.input, opts.catalog);
  let thoughtSummary = "先选择必要工具获取证据,再根据结果交付答案。";

  const response = await chatComplete({
    temperature: 0.1,
    messages: [
      { role: "system", content: buildReactPrompt(opts.catalog, opts.incremental) },
      {
        role: "user",
        content:
          `${opts.contextText ? `上下文:\n${opts.contextText}\n\n` : ""}` +
          `用户需求:\n${opts.input}`,
      },
    ],
  });

  const parsed = actionSchema.safeParse(extractJsonObject(response.text));
  if (parsed.success) {
    const validActions = parsed.data.actions.filter((action) =>
      allowed.has(action.tool),
    );
    if (validActions.length > 0) {
      thoughtSummary = parsed.data.thoughtSummary;
      actions = validActions;
    }
  }

  actions = ensureFinalSynthesisAction(actions, opts.input, opts.catalog);
  actions = normalizeActionDependencies(actions);

  return {
    nodes: actions.map(toDagNode),
    traceEvents: buildReactTraceEvents(thoughtSummary, actions),
  };
}

这个函数里有几个需要放在代码里的约束。

第一,LLM 输出 action,不输出抽象步骤。tool 必须来自 catalog,args 必须能直接给执行器,dependsOn 必须引用已知节点。Planner 和 Compiler 的边界被合并成“行动选择”。

第二,系统保留启发式兜底。模型失败、JSON 解析失败、工具不在 catalog 里时,不直接崩溃,而是回退到保守动作。

第三,系统强制补一个最终综合动作。通用智能体很容易只完成检索或中间分析,忘记给用户一个明确交付。这个约束应该放在架构层,不要完全交给提示词。

function ensureFinalSynthesisAction(actions: Action[], input: string, catalog: CellCatalog) {
  if (actions.some((action) => isFinalAnswerAction(action))) {
    return actions;
  }

  return [
    ...actions,
    {
      title: "综合观察结果并交付最终答案",
      tool: preferredAiTool(catalog),
      args: {
        task: [
          "综合上游所有工具观察结果,直接输出对用户问题的最终答案。",
          "只输出面向用户的最终交付内容,不输出规划说明或步骤编号。",
          `用户原始需求:${input}`,
        ].join("\n"),
      },
      dependsOn: actions.map((_, index) => `react_${index + 1}`),
    },
  ];
}

第四,ReAct 模式需要把 thought/action/observation 显式记录下来。否则它会比 PCA 更难审计。

const traceEvents = [
  {
    type: "react_thought",
    payload: { iteration: 1, summary: thoughtSummary },
  },
  ...nodes.flatMap((node, index) => [
    {
      type: "react_action",
      payload: {
        iteration: index + 1,
        tool: node.cellId,
        title: node.title,
        argsPreview: compactArgs(node.args),
      },
    },
    {
      type: "react_observation",
      payload: {
        iteration: index + 1,
        summary: `等待 ${node.cellId} 的结果,再决定下一步。`,
      },
    },
  ]),
];

这不是为了展示“模型思考过程”,而是为了调试系统:为什么选择这个工具、传了哪些参数、下一轮为什么追加或取消动作。

真正变化在状态机

很多人理解 ReAct 时,会把重点放在提示词上。从架构师视角看,更大的变化是状态管理。

旧架构里,任务从 plannedcompiledqueuedcompleted,像一个批处理工作流。

新架构里,run 是一个可多轮演进的状态机:

const contextText = [
  buildConversationContext(messages),
  buildObservationContext(completedTasks),
  trigger === "auto"
    ? "根据已经完成的观察结果判断下一步。"
    : "根据用户最新补充判断是否修订计划。",
].join("\n");

const react = await buildReactDag({
  input: latestUserText,
  contextText,
  catalog,
  incremental: true,
});

const nextDagVersion = currentDagVersion + 1;

await cancelPendingTasks({ runId, dagVersion: currentDagVersion });
await rebuildTasksForDag({
  runId,
  dagVersion: nextDagVersion,
  nodes: selectReactRoundNodes(react.nodes),
});

这里不是“再调用一次 LLM”这么简单。每一轮都要带上观察上下文:哪些任务完成了、哪些失败了、用户补充了什么、哪些结果仍可复用、哪些证据不足。

PCA 的重规划是“把旧计划推倒重来”。ReAct 的重规划是“基于观察追加下一步”。

这会直接改变用户体验。用户不必等一个大计划全部跑完;系统可以在中间结果出现后继续判断,也可以在用户插话后重排后续动作。

放到主流智能体设计里看

重新看 OpenAI Deep Research、OpenAI Computer-Using Agent、Anthropic 的 workflow/agent 区分、LangGraph、Kimi K2/K2.5、Manus 这几类系统后,我会把判断改得更窄一点:分界不在“有没有规划”,而在“规划是一次性前置,还是在观察循环中持续更新”。

这里的引用分两类使用。OpenAI、Anthropic、LangGraph 文档主要用来观察主流产品和工程框架如何组织 agent;ReAct、AutoGen、Kimi、Manus 相关论文主要用来支撑 reasoning/action、multi-agent、agentic capability 和自主执行风险这些概念。它们不是为了证明某一种架构永远正确,而是帮助判断哪些设计已经稳定下来,哪些还只是产品形态上的探索。

OpenAI Deep Research 是一个很好的例子。它面向复杂研究任务,官方描述里强调多步研究、浏览、分析、综合,以及根据新信息 pivot [1]。这个系统并不是没有 planning,而是把 planning 放进了长时间运行的研究循环里:搜索、读源、发现缺口、再搜索、再综合。它更像 ReAct + 研究型 finalizer,不像一次性静态 DAG。

OpenAI Computer-Using Agent 也说明了同一件事。它的基本循环是 perception、reasoning、action:看屏幕,判断下一步,用鼠标键盘执行,再看新的屏幕状态 [2]。这里如果使用完整 PCA,计划很容易被网页弹窗、登录态、DOM 变化、验证码、表单错误打断。GUI 智能体天然需要观察驱动,而不是只依赖前置计划。

Anthropic 的《Building Effective Agents》给了一个很实用的分类:固定子任务适合 prompt chaining、routing、parallelization 这类 workflow;不可预知子任务更适合 orchestrator-workers 或 agent [3]。这和混合架构的判断一致:可预测部分流程化,不可预测部分交给动态编排。

LangGraph 则从工程运行时角度强调长运行、有状态、持久化、human-in-the-loop、调试和可观测 [4]。这提醒我,ReAct 不是一个 prompt 技巧,而是一个运行时问题:状态如何持久化,失败后如何恢复,用户如何中途介入,trace 如何用于调试。这也是我在重构里把 DAG 版本、observation context、trace events 放进主链路的原因。

ReAct 论文是本文“思考、行动、观察”循环的直接概念来源 [5]。AutoGen 则说明 multi-agent conversation 可以作为更高层的协作编排方式 [6]。这两篇文献放在一起看,可以把单智能体 ReAct 和多智能体协作区分开:前者解决单个 agent 如何基于工具观察继续行动,后者解决多个 agent 如何分工、对话和汇总。

Kimi K2 和 K2.5 的技术报告代表了另一条路线:模型本身越来越强调 agentic capability,K2.5 还提出 Agent Swarm,把复杂任务动态拆成异构子问题并发执行 [7][8]。这不是否定 ReAct,而是在 ReAct 之上引入多智能体并发。架构上要接受一件事:单轮 action 不一定是一条线性链路,也可能是一组并行 worker 和一个汇总器。

Manus 相关研究更像产品层案例。它被描述为能自主规划、浏览、写代码、处理文件、生成结构化结果,也有“透明执行窗口”和较强的端到端任务感 [9]。但 AI agent 软件的用户研究也指出,当前 agent 产品常见问题包括目标误解、不可控循环、结果验证困难和用户信任落差 [10]。自主性越强,系统越需要观察日志、用户中断、权限边界、结果验证和失败分类。

我会把智能体架构拆成四层:

层级主流系统里的体现对本文架构的启发
模型能力层Kimi K2/K2.5、OpenAI o 系列、Claude [7][8]模型要会工具使用、长程推理和环境交互
行动循环层ReAct、CUA、Deep Research [1][2][5]复杂任务需要 thought/action/observation 循环
编排运行时层LangGraph、Manus 执行环境 [4][9][10]状态、持久化、审批、trace、恢复比 prompt 更关键
专用工作流层Anthropic prompt chaining/routing/parallelization [3]稳定流程仍应工作流化,避免通用 agent 过度自由发挥

更准确地说,PCA 不适合作为通用智能体的唯一顶层控制流,但它仍然应该存在于 ReAct 系统内部,作为某些稳定子任务的局部 workflow。成熟的智能体系统不需要在 ReAct 和 PCA 之间二选一。ReAct 负责动态决策,PCA 负责稳定执行片段。

通用智能体为什么更适合 ReAct

通用智能体面对的是开放任务。用户可能说:

用户输入形态架构挑战
“帮我查一下这个问题”不知道需要搜索、计算、读文件还是直接回答
“基于刚才结果再补充一下”需要识别历史结果和新增约束
“不要用刚才那个来源”需要降权或替换数据源
“先做一个简版”需要动态收缩交付
“结果不对,换个角度”需要保留可复用证据,同时调整分析框架

这些场景都有一个共同点:正确动作依赖上下文和观察结果,不只依赖初始输入。

ReAct 把“不确定性”放回循环里处理:

  1. 当前信息不足,就先查证据。
  2. 工具失败,就换工具或降低该证据权重。
  3. 用户补充,就判断是修订目标、追加约束还是改变交付。
  4. 证据足够,就进入最终综合。
  5. 证据不足但无法继续获取,就明确限制和不确定性。

这比开局就规划一个 10 步 DAG 更接近通用智能体的真实运行方式。

专用智能体仍然需要 PCA

改成 ReAct,不代表 PCA 过时。

如果任务是高度稳定的专用流程,PCA 仍然更合适。比如:

专用任务特征为什么 PCA 更好
输入字段固定可以直接校验 schema
工具链固定DAG 可以预定义或半编译
合规要求强每个步骤都需要明确审计
成本要可控执行前可以估算步骤数和资源
失败恢复明确可以按节点重试,不必重新推理

专用智能体最怕“自由发挥”。它需要确定性、可复核、可回放。PCA 的静态结构反而是优势。

所以更合理的做法是分层使用:

flowchart TD
  U[User Goal] --> R[General ReAct Agent]
  R -->|开放问题| A[Dynamic Actions]
  R -->|识别为稳定专用任务| P[Specialized PCA Workflow]
  P --> C[Compiled DAG]
  A --> E[Runtime]
  C --> E
  E --> F[Final Delivery]

通用智能体负责理解用户目标、选择策略、处理多轮上下文;专用智能体负责执行稳定流程。通用层用 ReAct,专用层可以继续用 PCA。

这次重构的几个设计原则

第一,行动比计划更接近执行真相。

旧架构里,Planner 输出的步骤经常很漂亮,但 Compiler 才知道工具是否存在、参数是否满足、依赖是否合理。重构后,模型直接在 catalog 约束下选择 action,减少了语义步骤到工具节点之间的信息损耗。

第二,强约束要放在代码里,不只放在提示词里。

例如工具必须来自 catalog、最终必须有交付节点、依赖只能引用已存在节点、参数要归一化。这些都不应该完全依赖模型自觉。

第三,ReAct 不是让系统无限循环。

工程上必须有轮次上限、单轮动作上限、动态模式开关、失败动作去重、DAG 版本管理。否则 ReAct 会变成不可控的自动追加任务。

第四,观察上下文是新架构里最值钱的资产。

如果没有结构化 observation,ReAct 只是在反复调用模型。必须把任务状态、工具输出摘要、失败原因、用户补充、可复用证据组织成下一轮可消费的上下文。

第五,最终交付要从中间过程里分离出来。

智能体内部可以有评估、检索、清洗、校验、图表计划等动作,但用户最终需要的是答案、文档、表格、代码或操作结果。架构层要区分 internaldeliverable_content,避免把中间计划当作交付。

取舍总结

从架构师视角看,可以这样概括:

PCA 适合“流程确定、边界清楚、交付稳定”的智能体。它的优势是可控、可审计、可优化,缺点是对动态变化不够轻。

ReAct 适合“目标开放、证据不确定、需要多轮观察”的智能体。它的优势是灵活、抗变化、贴近真实助手行为,缺点是必须认真设计状态、轮次、日志和收敛条件。

这次重构不是从一个“错误架构”切到一个“正确架构”。它只是系统阶段变化后的自然演进。早期需要 PCA,是因为要把智能体拆成可理解、可执行、可调试的工程层。后来切到 ReAct,是因为主要矛盾变成了动态上下文、工具结果不确定和多轮修订。

我现在更认可混合式:

层级推荐模式
通用入口ReAct
多轮修订ReAct
探索式研究ReAct
固定业务流程PCA
高合规执行链PCA
最终交付整理可独立 Finalizer

智能体架构的重点,不是选一个模式打天下,而是识别不确定性在哪里。确定的部分编译成流程,不确定的部分留给观察循环。这样系统既不会被静态 DAG 卡死,也不会被自由行动拖散。

参考文献

  1. OpenAI, Introducing deep research, 2025.
  2. OpenAI, Computer-Using Agent, 2025.
  3. Anthropic, Building Effective AI Agents, 2024.
  4. LangChain, LangGraph overview, LangGraph Docs.
  5. Shunyu Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, 2022.
  6. Qingyun Wu et al., AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation, 2023.
  7. Kimi Team, Kimi K2: Open Agentic Intelligence, 2025.
  8. Kimi Team, Kimi K2.5: Visual Agentic Intelligence, 2026.
  9. Minjie Shen and Qikai Yang, From Mind to Machine: The Rise of Manus AI as a Fully Autonomous Digital Agent, 2025.
  10. Pradyumna Shome, Sashreek Krishnan, and Sauvik Das, Why Johnny Can’t Use Agents: Industry Aspirations vs. User Realities with AI Agent Software, 2025.

评论

评论由 GitHub Issues 提供支持,需要登录 GitHub。