前端开发者做多步 Agent:别让 AI 边想边乱跑,用 Plan-Act-Observe 稳住 4 步任务

作者:前端转 AI 深度实践者

【省流助手/核心观点】 :多步 Agent 最怕的不是不会调用工具,而是没有计划地乱调用工具。一个更可靠的 Agent 应该遵循 Plan-Act-Observe:先把任务拆成结构化步骤,再执行当前步骤,观察工具结果,把结果写回计划,并根据观察决定下一步。对前端开发者来说,这很像把复杂交互拆成流程节点:每一步有目标、有状态、有输入输出、有失败处理,而不是把所有逻辑塞进一个巨大的 handleUserInput


第 25 篇我们做了一个最小 Agent Loop。

它已经能完成这样的闭环:

text 复制代码
用户输入
-> 模型判断是否调用工具
-> 程序执行工具
-> 工具结果回到上下文
-> 模型生成最终回答

这对简单问题已经够用。

比如:

text 复制代码
帮我查一下订单 A1001 到哪了。

Agent 调一次 getOrderStatus,再组织答案,就能完成任务。

但真实用户不会总是问这么简单的问题。

他们更可能问:

text 复制代码
帮我查一下订单 A1001 的物流,如果还没送达,再看一下售后政策,告诉我能不能申请延迟补偿。

这个问题突然变成了多步任务:

  1. 查订单状态。
  2. 判断是否送达。
  3. 如果没送达,查延迟补偿政策。
  4. 结合订单和政策给出建议。

这时候,如果 Agent 只是"边想边跑",很容易跑偏。

1. 痛点:没有计划的 Agent,就像没看需求就开写代码

前端开发者应该很熟悉这种场景:

需求还没拆清楚,就开始写组件。

写着写着发现:

  • 状态放错地方了。
  • 接口顺序不对。
  • 错误态没处理。
  • 中间结果没有保存。
  • 最后发现第一步设计就错了。

多步 Agent 也是一样。

如果没有计划,它可能会:

  • 先查政策,再查订单,顺序反了。
  • 查完订单后忘记判断是否送达。
  • 明明订单已签收,还继续查延迟补偿。
  • 工具失败了还继续往下走。
  • 最终回答时说不清依据。

所以多步 Agent 的第一件事不是"多调几个工具",而是先把任务拆清楚。

这就是 Plan。

2. 错误做法:让模型每一步临场发挥

一种常见写法是把所有控制权交给模型:

ts 复制代码
async function runFreeAgent(userInput: string) {
  let context = userInput;

  for (let i = 0; i < 5; i++) {
    const output = await llm.chat(`
你是一个自主 Agent,请根据当前上下文决定下一步。

上下文:
${context}
`);

    const toolResult = await runTool(output.toolCall);
    context += JSON.stringify(toolResult);
  }

  return context;
}

这段代码看起来很"自主",但工程上很难维护:

  1. 不知道任务一开始被拆成了几步。
  2. 不知道当前执行到哪一步。
  3. 不知道某一步失败后该停还是继续。
  4. 不知道哪些步骤应该被跳过。
  5. 最终回答很难追溯依据。

多步 Agent 不是越自由越好。

真正可交付的系统,要让每一步都能被看见、被控制、被复盘。

3. 正确做法:先把任务变成结构化计划

Plan-Act-Observe 可以翻译成:

text 复制代码
Plan:先拆解任务
Act:执行当前步骤
Observe:记录结果,并影响后续步骤

先定义一个计划步骤:

ts 复制代码
type StepStatus =
  | "pending"
  | "running"
  | "done"
  | "skipped"
  | "failed";

type PlanStep = {
  id: string;
  goal: string;
  toolName: string;
  args: Record<string, unknown>;
  status: StepStatus;
  observation?: unknown;
  error?: unknown;
  skipReason?: string;
};

对刚才那个用户问题,一个最小计划可以长这样:

ts 复制代码
const plan: PlanStep[] = [
  {
    id: "step_1",
    goal: "查询订单 A1001 的物流状态",
    toolName: "getOrderStatus",
    args: { orderId: "A1001" },
    status: "pending"
  },
  {
    id: "step_2",
    goal: "查询延迟送达补偿政策",
    toolName: "searchPolicy",
    args: { keyword: "延迟补偿" },
    status: "pending"
  }
];

这份计划有几个好处:

  • 每一步目标清楚。
  • 每一步要调用哪个工具清楚。
  • 每一步参数清楚。
  • 当前执行状态清楚。
  • 后面可以记录执行结果。

前端同学可以把它类比成多步骤表单:

text 复制代码
Step 1:填写基础信息
Step 2:选择配送方式
Step 3:确认订单
Step 4:支付

每一步都有状态:未开始、进行中、完成、失败、跳过。

Agent 计划也是一样。

4. Act:一次只执行当前步骤

执行计划时,不要一次把所有步骤全部跑完。

更稳的方式是一次只拿一个 pending 步骤:

ts 复制代码
function getNextStep(plan: PlanStep[]) {
  return plan.find((step) => step.status === "pending") ?? null;
}

然后执行这个步骤:

ts 复制代码
type ToolResult =
  | {
      ok: true;
      data: unknown;
    }
  | {
      ok: false;
      errorType: string;
      message: string;
    };

async function act(step: PlanStep): Promise<ToolResult> {
  return runTool({
    toolName: step.toolName,
    args: step.args
  });
}

这件事看起来简单,但它让系统变得可控。

因为你随时知道:

  • 当前执行到哪一步。
  • 调用了哪个工具。
  • 用了什么参数。
  • 失败时应该标记哪一步。

多步 Agent 最怕"做了很多事,但没人知道它做到哪了"。

5. Observe:工具结果必须写回计划

执行工具之后,要把结果写回计划。

ts 复制代码
function observe(step: PlanStep, toolResult: ToolResult) {
  if (toolResult.ok) {
    step.status = "done";
    step.observation = toolResult.data;
    return;
  }

  step.status = "failed";
  step.error = {
    errorType: toolResult.errorType,
    message: toolResult.message
  };
}

Observe 不是"拿到结果就行"。

Observe 是把结果变成系统状态。只有状态被正确记录,后续步骤才能基于它做判断。

6. 观察结果应该能改变后续计划

计划不是死的。

我们的任务里有一句条件:

text 复制代码
如果还没送达,再看一下售后政策。

如果第一步查到订单已签收,第二步其实应该跳过。

ts 复制代码
type OrderStatus = {
  status: "shipping" | "delivered" | "not_found";
  eta?: string;
};

function updatePlanAfterObservation(plan: PlanStep[]) {
  const orderStep = plan.find((step) => step.id === "step_1");
  if (!orderStep || orderStep.status !== "done") return;

  const order = orderStep.observation as OrderStatus;

  if (order.status === "delivered") {
    for (const step of plan.slice(1)) {
      if (step.status === "pending") {
        step.status = "skipped";
        step.skipReason = "订单已签收,不需要继续查询延迟补偿。";
      }
    }
  }
}

这才是 Observe 的价值。

它不是为了记日志而记日志,而是让工具结果影响下一步。

7. 一个最小 Plan Agent 长这样

下面是一版完整但仍然很小的执行器:

ts 复制代码
type PlanAgentResult = {
  ok: boolean;
  answer: string;
  plan: PlanStep[];
};

async function runPlanAgent(
  userInput: string,
  maxSteps = 4
): Promise<PlanAgentResult> {
  const plan = createPlan(userInput);
  let steps = 0;

  while (steps < maxSteps) {
    steps += 1;
    const step = getNextStep(plan);

    if (!step) {
      return generateFinalAnswer(plan);
    }

    step.status = "running";

    const toolResult = await act(step);
    observe(step, toolResult);

    if (step.status === "failed") {
      return generateFinalAnswer(plan);
    }

    updatePlanAfterObservation(plan);
  }

  return {
    ok: false,
    answer: "超过最大执行步数,Agent 已停止。",
    plan
  };
}

这段代码没有炫技,但结构非常清楚:

  • 先有计划。
  • 找到下一步。
  • 执行当前动作。
  • 观察结果。
  • 根据结果更新计划。
  • 没有下一步就回答。

如果以后接入真实模型,这个结构仍然成立。

只是 createPlan 可以由模型生成,generateFinalAnswer 也可以由模型根据计划结果生成。

8. 补上 createPlan 和 final answer 的最小实现

学习阶段不一定要一上来就让模型生成计划。

你可以先用规则把流程跑通。

ts 复制代码
function createPlan(userInput: string): PlanStep[] {
  if (!userInput.includes("A1001")) {
    return [
      {
        id: "step_1",
        goal: "告知用户当前示例只支持订单 A1001",
        toolName: "none",
        args: {},
        status: "skipped",
        skipReason: "当前示例只处理订单 A1001"
      }
    ];
  }

  return [
    {
      id: "step_1",
      goal: "查询订单 A1001 的物流状态",
      toolName: "getOrderStatus",
      args: { orderId: "A1001" },
      status: "pending"
    },
    {
      id: "step_2",
      goal: "查询延迟送达补偿政策",
      toolName: "searchPolicy",
      args: { keyword: "延迟补偿" },
      status: "pending"
    }
  ];
}

最终回答也可以先用规则生成:

ts 复制代码
function generateFinalAnswer(plan: PlanStep[]): PlanAgentResult {
  const failed = plan.find((step) => step.status === "failed");
  if (failed) {
    return {
      ok: false,
      answer: `任务在「${failed.goal}」失败:${JSON.stringify(
        failed.error
      )}`,
      plan
    };
  }

  const orderStep = plan.find((step) => step.id === "step_1");
  const policyStep = plan.find((step) => step.id === "step_2");
  const runnableSteps = plan.filter((step) => step.status !== "skipped");

  if (runnableSteps.length === 0) {
    return {
      ok: false,
      answer:
        plan
          .map((step) => step.skipReason)
          .filter(Boolean)
          .join("\n") || "当前任务没有可执行步骤。",
      plan
    };
  }

  const lines = [
    orderStep?.observation
      ? `订单查询结果:${JSON.stringify(orderStep.observation)}`
      : "没有订单查询结果。"
  ];

  if (policyStep?.status === "skipped") {
    lines.push(`政策查询已跳过:${policyStep.skipReason}`);
  } else if (policyStep?.observation) {
    lines.push(`政策查询结果:${JSON.stringify(policyStep.observation)}`);
  }

  return {
    ok: true,
    answer: lines.join("\n"),
    plan
  };
}

这不是最终产品文案,但它能帮你先验证流程。

等 Plan-Act-Observe 跑稳后,再让模型接管计划生成和最终表达,会更容易排查问题。

9. 前端页面怎么展示计划?

多步 Agent 如果只展示最终回答,用户不知道系统做了什么,开发者也很难排查。

可以把计划展示成步骤列表:

tsx 复制代码
function AgentPlanView({ plan }: { plan: PlanStep[] }) {
  return (
    <ol>
      {plan.map((step) => (
        <li key={step.id}>
          <strong>{step.goal}</strong>
          <span>{step.status}</span>
          {step.skipReason && <p>{step.skipReason}</p>}
          {step.error && <pre>{JSON.stringify(step.error, null, 2)}</pre>}
        </li>
      ))}
    </ol>
  );
}

这类 UI 在开发环境、运营后台、企业内部工具里很有价值。

因为它能回答几个关键问题:

  • Agent 原计划做什么?
  • 当前执行到哪一步?
  • 哪一步失败了?
  • 哪一步被跳过了?
  • 最终答案基于哪些观察结果?

10. 生产环境避坑指南

1. 不要让计划无限长

初期计划控制在 2 到 4 步更稳。

计划越长,错误传播越严重,成本和延迟也越高。

2. 关键步骤失败后不要继续编

如果查订单失败,就不要继续基于空数据查补偿政策。

关键步骤失败时,应该停止并说明失败原因。

3. 跳过步骤要写明原因

不要只把状态改成 skipped

要写清 skipReason,否则排查时不知道是业务条件触发,还是系统漏执行。

4. 高风险步骤必须二次确认

如果计划里包含取消订单、发起退款、发送邮件、删除数据,一定要在执行前确认。

Plan 可以建议高风险步骤,但不能自动越过权限和确认。

5. 每一步都要可回放

记录每一步的 goaltoolNameargsstatusobservationerror

否则多步 Agent 一旦出错,就会变成"它好像自己做了很多事,但没人知道具体发生了什么"。

11. 常见误区

误区 1:计划越详细越好

不一定。初期计划 2 到 4 步最好。太长的计划会增加错误传播和维护成本。

误区 2:生成计划后就不能改

计划应该能根据观察结果调整。否则 Observe 就只是记录日志,没有真正参与决策。

误区 3:工具失败后继续执行后续步骤

如果关键步骤失败,应该停下来说明失败原因,而不是继续编一个完整答案。

误区 4:所有计划都必须由模型生成

不需要。学习阶段可以先用规则生成计划。真实项目里,也可以把固定业务流程写死,只让模型处理自然语言理解和答案表达。

12. 给前端开发者的落地清单

如果你要在团队里做多步 Agent,可以从这份清单开始:

  1. 定义任务类型。
  2. 为每种任务设计最短计划。
  3. 每一步都要有 idgoalstatus
  4. 每一步只调用一个清晰工具。
  5. 工具结果写入 observation
  6. 失败写入 error
  7. 可跳过步骤写入 skippedskipReason
  8. 最终答案必须基于 plan 里的观察结果。
  9. 记录完整执行日志。
  10. 用测试用例覆盖完成、跳过、失败三种路径。

这份清单看起来像工程流程,而不是 AI 魔法。

这正是重点。

Agent 工程越往后走,越不是让模型随便发挥,而是给模型一个清晰、可观察、可回放的工作台。

结语

多步 Agent 不能边想边乱跑。

它需要先计划,再行动,再观察。

Plan 让任务有结构。

Act 让系统真正执行。

Observe 让结果回到状态,并影响下一步。

这就是 Agent 从"能调工具"走向"能完成任务"的关键一步。

对前端开发者来说,这不是陌生领域。你早就熟悉流程、状态、副作用和错误处理。现在只是把这些工程能力,用在 AI Agent 上。

会写 Prompt 只是开始。

会设计可控的多步执行流程,才是 AI 工程真正的成长信号。

相关推荐
Maiko Star5 小时前
Spring AI ChatClient 完全指南:从基础配置到流式调用
java·人工智能·spring
Aaron15885 小时前
RFSOC+VU13P+GPU 在6G互联网中的技术应用
大数据·人工智能·算法·fpga开发·硬件工程·信息与通信·信号处理
Raink老师5 小时前
【AI面试临阵磨枪-31】Agent 反思(Reflection)机制如何实现?作用是什么?
人工智能·ai 面试
安卓程序员_谢伟光5 小时前
如何使用ai开发
人工智能
这张生成的图像能检测吗5 小时前
(论文速读)让机器人像人一样走路:注意力机制如何让腿足机器人征服复杂地形
人工智能·深度学习·计算机视觉·机器人控制
一切皆是因缘际会5 小时前
预制式制衡智能:大模型瓶颈下的 AI 迭代新思路
人工智能·安全·ai·架构
一锤捌拾5 小时前
V8引擎精品漫游指南--Ignition篇(下 一) 动态执行前的事情
前端·javascript
动恰客流管家5 小时前
动恰3DV3丨2026年实体商业数字化转型:客流数据是第一生产力——全场景智慧客流解决方案
大数据·人工智能·3d·性能优化
遇见~未来5 小时前
第六篇_CSS进阶_深入浏览器与工程化
前端·css·rust