前端开发者做多步 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. 每一步都要可回放

记录每一步的 goal、toolName、args、status、observation、error。

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

11. 常见误区

误区 1:计划越详细越好

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

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

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

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

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

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

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

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

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

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

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

这正是重点。

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

结语

多步 Agent 不能边想边乱跑。

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

Plan 让任务有结构。

Act 让系统真正执行。

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

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

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

会写 Prompt 只是开始。

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

相关推荐
LinXunFeng3 小时前
Draw Things 换装 LoRA 实战:用 Silo 秒装,同一提示词对比出图
人工智能·开源·aigc
xiaohaiAIgeo3 小时前
【2026年】通风柜自动门安全吗?联锁逻辑与防夹设计
人工智能·安全·科普知识
像风一样自由20203 小时前
52.Docker部署大模型应用从本地开发到服务化运行
运维·人工智能·docker·容器·大模型·智能体
牛战士coding4 小时前
AI Agent 如何接入真实业务系统?订单助手三层架构完整拆解
人工智能·架构
·云扬·4 小时前
Prompt 工程与安全:从 System 设计到注入防御
java·人工智能·安全·ai·prompt
阿明副业观察4 小时前
2026年免费AI视频生成工具功能深度解析:有无水印、中文语音及商业用途考量
大数据·人工智能·ai作画·aigc·音视频·ai写作
收放扳机4 小时前
PCB收放板设备机械手架构选型参考——三轴/四轴/六轴/蜘蛛手对比
人工智能·科技·架构·自动化·制造·pcb工艺
AKAMAI5 小时前
Akamai 宣布与Anthropic达成 116 亿美元多年期协议,以支持其日益增长的需求
人工智能·云计算
甲维斯5 小时前
我把“赶 Deadline”和AI做成了一款游戏
人工智能·游戏开发
Java后端的Ai之路5 小时前
06-React-Props
前端·react.js·前端框架