一次从 PCA 到 ReAct 的智能体重构
最近我把一个通用智能体系统的执行架构,从偏静态的 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-Action | ReAct |
|---|---|---|
| 核心抽象 | 先规划完整步骤,再编译执行图 | 根据上下文选择下一批动作 |
| 适用任务 | 稳定、可模板化、流程明确 | 开放、探索、多轮修订、不确定性高 |
| 错误形态 | 前期计划错误会传导到后续步骤 | 每轮可根据观察修正 |
| 可审计性 | 强,步骤和 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 时,会把重点放在提示词上。从架构师视角看,更大的变化是状态管理。
旧架构里,任务从 planned 到 compiled 到 queued 到 completed,像一个批处理工作流。
新架构里,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 把“不确定性”放回循环里处理:
- 当前信息不足,就先查证据。
- 工具失败,就换工具或降低该证据权重。
- 用户补充,就判断是修订目标、追加约束还是改变交付。
- 证据足够,就进入最终综合。
- 证据不足但无法继续获取,就明确限制和不确定性。
这比开局就规划一个 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 只是在反复调用模型。必须把任务状态、工具输出摘要、失败原因、用户补充、可复用证据组织成下一轮可消费的上下文。
第五,最终交付要从中间过程里分离出来。
智能体内部可以有评估、检索、清洗、校验、图表计划等动作,但用户最终需要的是答案、文档、表格、代码或操作结果。架构层要区分 internal 和 deliverable_content,避免把中间计划当作交付。
取舍总结
从架构师视角看,可以这样概括:
PCA 适合“流程确定、边界清楚、交付稳定”的智能体。它的优势是可控、可审计、可优化,缺点是对动态变化不够轻。
ReAct 适合“目标开放、证据不确定、需要多轮观察”的智能体。它的优势是灵活、抗变化、贴近真实助手行为,缺点是必须认真设计状态、轮次、日志和收敛条件。
这次重构不是从一个“错误架构”切到一个“正确架构”。它只是系统阶段变化后的自然演进。早期需要 PCA,是因为要把智能体拆成可理解、可执行、可调试的工程层。后来切到 ReAct,是因为主要矛盾变成了动态上下文、工具结果不确定和多轮修订。
我现在更认可混合式:
| 层级 | 推荐模式 |
|---|---|
| 通用入口 | ReAct |
| 多轮修订 | ReAct |
| 探索式研究 | ReAct |
| 固定业务流程 | PCA |
| 高合规执行链 | PCA |
| 最终交付整理 | 可独立 Finalizer |
智能体架构的重点,不是选一个模式打天下,而是识别不确定性在哪里。确定的部分编译成流程,不确定的部分留给观察循环。这样系统既不会被静态 DAG 卡死,也不会被自由行动拖散。
参考文献
- OpenAI, Introducing deep research, 2025.
- OpenAI, Computer-Using Agent, 2025.
- Anthropic, Building Effective AI Agents, 2024.
- LangChain, LangGraph overview, LangGraph Docs.
- Shunyu Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, 2022.
- Qingyun Wu et al., AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation, 2023.
- Kimi Team, Kimi K2: Open Agentic Intelligence, 2025.
- Kimi Team, Kimi K2.5: Visual Agentic Intelligence, 2026.
- Minjie Shen and Qikai Yang, From Mind to Machine: The Rise of Manus AI as a Fully Autonomous Digital Agent, 2025.
- 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。