这是「从零搭建 Agent」系列的第五篇。前四章我们已经完成了 Agent Loop、基础工具箱、Context Engine 和 Memory System。到这里,Agent 已经能行动、能整理上下文、也能保存和召回状态。本章会实现 Planner,让 Agent 在复杂任务中先计划、再执行、按证据推进,并在计划未完成前不能草率收尾。

同步项目地址:https://github.com/Tritium0041/Singularity,本章对应变更:https://github.com/Tritium0041/Singularity/commit/236defbce0086e0091a16975e091c3438bc62637

# Planner 负责哪些工作?

第四章结尾时,我们已经有了两块 Harness 能力。

第一块是 Context Engine。它解决的是「模型这一轮应该看到什么」。

tool result truncation
  -> token estimate
  -> budget decision
  -> handoff summary
  -> dynamic compression
  -> request view

第二块是 Memory System。它解决的是「哪些事实值得留下来,以及需要时怎么找回来」。

workspace notes
  -> markdown memory store
  -> memory tools
  -> session persistence

这两块能力让 Agent 不再只是沿着完整聊天历史往前滚。它可以压缩旧历史,可以保存阶段性结论,也可以从长期记忆里召回项目约定。

但如果把 Agent 放进更真实的任务里,比如:

阅读这个仓库最近一轮变更,对照前四章文章,写出完整第五章,并保存成 Markdown。(真是真实任务!)

这类任务不会只靠「记住事实」就自然完成。它至少包含几层结构:

  1. 先确认当前仓库状态和 stage4 变更。
  2. 再阅读前四章文章,找出系列写作风格。
  3. 再理解第五章应该承接哪个问题。
  4. 再提炼代码实现细节。
  5. 最后写出完整文章,并检查文件是否落盘。

Memory 可以保存「我读到了什么」,但它不表达「我现在正在做哪一步」。Context Engine 可以决定「哪些内容进入 request」,但它不判断「任务是否已经完成」。

于是就会出现三个很典型的失败模式:

第一是任务漂移。Agent 执行几轮后,注意力被最近的工具结果牵着走,开始只处理眼前的信息,而忘记最初目标。比如本来要写完整文章,读着读着变成只总结代码变更。

第二是假完成。模型生成了一个看起来很完整的 final answer,但其实漏掉了关键环节。比如还没有核对 demo 命令,还没有提到 session 持久化,就说「第五章完成了」。

第三是重复劳动。已经读过的文件、跑过的命令、确认过的结论没有被映射成结构化进度,于是模型过几轮又重新检查一遍。它不是笨,只是没有一份可靠的「任务账本」。

Planner 要解决的就是长任务中的不稳定性。

User goal
  -> create explicit plan
  -> review / approve plan
  -> execute current step
  -> observe result
  -> update step with evidence
  -> replan when needed
  -> stop only when completion criteria are satisfied

这也就是本章标题里的 **Planning and Solve:** 它要让 Agent 把自己的每一步操作固定为状态对象,由 harness 层统一管理。

# Plan-and-Solve 不是一次性 prompt trick

很多时候我们说 Plan-and-Solve,会想到一种 prompt 技巧:先让模型输出计划(think step by step, or CoT),再让模型按计划回答。

这种方式对单次问答的 chatbot 有用,但对 Agent runtime 来说还不够。原因很简单:Agent 的计划不是写在纸面上给人看的,而是要在工具调用、错误、用户反馈、上下文压缩、session 切换之间持续活下去。如果计划只是一段 assistant message,它会遇到几个问题:

  • 它可能被 Context Engine 压缩成摘要。
  • 它没有机器可检查的步骤状态。
  • 它不能阻止模型提前 final answer。
  • 它不能天然和工具权限、用户审批、session 持久化联动。
  • 它也无法表达「这个步骤完成的证据是什么」。

我们最终采用的架构是:

Plan:
  把用户目标拆成结构化步骤,给每一步定义状态和完成条件。
Review:
  计划创建后先暂停,让用户审批或修改,避免 Agent 直接进入破坏性执行。
Solve:
  每一轮只推进当前最重要的一步,基于工具 observation 更新计划状态。
Replan:
  当发现原计划不成立、步骤阻塞或用户改变目标时,显式修改计划。

这里最关键的转变是:Planning 不再是模型输出的一段文字,而是 Harness 里的一个任务状态机。

chat history:    发生过什么
memory:          哪些事实值得保留
context engine:  本轮该看什么
planner:         目标是什么,当前做到哪一步,凭什么说完成了

这四者放在一起,Agent 才开始像一个能完成长任务的系统,而不是一个会用工具的聊天窗口。

# 本章新增了什么?

stage4 的核心变更集中在四组文件。

第一组是 Planning 模块本体:

src/planning/
  index.ts
  types.ts
  planner.ts
  planning-tools.ts
  instructions.ts

第二组是 Agent Loop 接入:

src/agent/agent-loop.ts
src/types.ts
src/tools/registry.ts

第三组是 session 和 demo:

src/session/session-store.ts
examples/run-agent.ts
README.md

第四组是测试:

tests/planning.test.ts
tests/agent-loop.test.ts
tests/session-store.test.ts
tests/provider-tools.test.ts

本章完成后,Singularity 新增了这些能力:

能力作用
Planner  状态层维护 objective、steps、current step、revision、review status
Planning tools允许模型创建、读取、修改、审批和清空计划
静态规划指令告诉模型复杂任务应先计划、后执行、按证据更新
动态计划快照每轮把当前 plan snapshot 注入 request view
计划审批闸门计划创建后暂停,等待用户批准再执行写入 / 命令类工具
Final answer guard计划仍有 open step 时,阻止模型提前结束
Session 持久化plan state 跟随会话保存和恢复
Demo 命令/plan/plan run/plan approve/plan json/plan clear

# Planner 任务状态机

Planner  本体在  src/planning/planner.ts 。它不依赖 LLM,也不执行工具,只负责纯状态管理。

公开接口大致如下:

export class Planner {
  constructor(initial?: PlanState);
  get state(): PlanState | undefined;
  create(input: CreatePlanInput): PlanState;
  replace(input: ReplacePlanInput): PlanState;
  approve(input?: ApprovePlanInput): PlanState;
  addStep(input: AddPlanStepInput): PlanStep;
  updateStep(input: UpdatePlanStepInput): PlanStep;
  setCurrentStep(id: string): PlanStep;
  completeStep(input: CompletePlanStepInput): PlanStep;
  blockStep(input: BlockPlanStepInput): PlanStep;
  clear(): void;
}

这层实现遵守几个原则。

第一个原则是 state getter 不暴露内部引用。

get state(): PlanState | undefined {
  return this.plan === undefined ? undefined : cloneState(this.plan);
}

外部读取到的是 clone,不能绕过  Planner  的校验直接改内部状态。这个设计和第四章的  WorkspaceMemory  很像:状态可以被观察,但修改必须走受控接口。

第二个原则是每次修改都递增  revision

function touch(plan: PlanState): void {
  plan.revision += 1;
  plan.updatedAt = new Date().toISOString();
}

revision  的作用是让 UI、调试和上下文快照都能清楚知道 plan 是否发生过变化。长任务里这类小字段可以让 Agent 更明确了解当前目标是否是明确的。

第三个原则是不能跳过依赖。

function ensureDependenciesCompleted(plan: PlanState, step: PlanStep): void {
  for (const dependencyId of step.dependsOn ?? []) {
    const dependency = findStep(plan, dependencyId);
    if (dependency.status !== "completed") {
      throw new Error(`Plan step ${step.id} depends on incomplete step ${dependencyId}.`);
    }
  }
}

Planner 的状态中包含了  dependsOn 字段,并且在设置 current step 或进入  in_progress  时会检查依赖是否完成。

第四个原则是完成必须有证据。

function ensureCompletionEvidence(evidence: string[] | undefined): void {
  if (!evidence || evidence.length === 0) {
    throw new Error("Completed plan steps must include evidence.");
  }
}

Agent 最容易出现的坏习惯之一,就是把「我觉得做完了」当成「任务真的做完了」。要求 completed step 必须带 evidence,会强迫模型把完成判断落到可追溯的 observation 上。但是这样设计的坏处是,写证据和审批证据的角色都是 Agent 自己。他有可能自己给自己 hack 过了证据检查。

第五个原则是结构性修改会让审批状态回到 pending。

function shouldInvalidatePlanReview(input: UpdatePlanStepInput): boolean {
  if (
    input.title !== undefined ||
    input.description !== undefined ||
    input.dependsOn !== undefined ||
    input.completionCriteria !== undefined
  ) {
    return true;
  }
  return false;
}

也就是说,如果只是更新 evidence 或 status,不需要重新审批;但如果改了步骤标题、描述、依赖或完成条件,那就说明计划本身变了,需要重新 review。

# Planning Tools:让模型操作计划状态

和 Memory System 一样,我们把 Planner 作为一个 toolset 暴露给 Agent。

src/planning/planning-tools.ts  里新增了七个工具:

工具作用
create_plan创建或替换当前用户目标的结构化计划
read_plan读取当前计划和步骤状态
update_plan_step更新步骤状态、描述、完成条件、证据或 current 标记
add_plan_step在执行中追加新步骤
set_current_step显式切换当前正在推进的步骤
approve_plan用户审批后批准当前计划
clear_plan当前目标结束或换题时清空计划

工具返回值是一段模型可读的 observation:

function planToolResult(message: string, details: unknown) {
  return {
    content: `${message}\n${JSON.stringify(details, null, 2)}`,
    details
  };
}

比如  create_plan  成功后,下一轮模型看到的是:

Plan created.
{
  "objective": "Write file",
  "reviewStatus": "pending",
  "steps": [
    {
      "id": "write",
      "title": "Write file",
      "status": "pending",
      ...
    }
  ],
  "currentStepId": "write",
  "revision": 1,
  ...
}

如果模型调用了不存在的 step,或者把没有 evidence 的 step 标记为 completed, ToolExecutor  会把错误包装成 tool result:

Completed plan steps must include evidence.

然后模型下一轮可以修正自己的调用。

# 静态 Planning Instructions

有了工具还不够。模型需要知道什么时候应该用它们、怎么用、哪些事情不能做。

所以本章新增了  buildPlanningInstructions()

export function buildPlanningInstructions(options: { requirePlanBeforeMutation?: boolean } = {}): PromptFragment {
  const lines = [
    "Planning tools are available for multi-step work.",
    "- For complex tasks, create a structured plan before executing the work.",
    "- If the user request contains an explicit planning directive, you must enter plan mode and call create_plan before any execution.",
    "- After create_plan succeeds, send the plan details to the user and ask them to review/approve it before execution.",
    "- Do not call approve_plan yourself unless the user has approved or revised the pending plan.",
    "- While the current plan reviewStatus is pending, do not execute write/execute tools or begin real task execution.",
    "- Use read_plan to inspect current progress instead of relying only on chat history.",
    "- After completing a real subtask, update the related plan step with evidence.",
    "- If the plan is wrong or blocked, update the plan before continuing.",
    "- When a plan has any pending or in_progress steps, do not produce a final answer or end the conversation.",
    "- Only produce the final answer after every plan step has left pending and in_progress status.",
    "- Before the final answer, confirm required steps are completed or clearly report blockers."
  ];
  ...
}

# 动态 Plan Snapshot:让模型每轮都看见当前计划

只提供  read_plan  工具还不够。因为如果模型忘记先调用  read_plan ,就可能继续沿着聊天历史推进。

所以本章增加了第二层:每轮请求都会把当前 plan snapshot 注入 system prompt 末尾。这里还有优化空间,可以寻找最缓存友好的设计位置。

格式由  formatPlanSnapshot()  生成:

export function formatPlanSnapshot(state: PlanState | undefined, options: { maxSteps?: number } = {}): string | undefined {
  if (!state) {
    return undefined;
  }
  const maxSteps = clampMaxSteps(options.maxSteps ?? 20);
  const visibleSteps = state.steps.slice(0, maxSteps);
  const lines = [
    `<current_plan revision="${escapeXml(String(state.revision))}" review_status="${escapeXml(state.reviewStatus)}">`,
    `  <objective>${escapeXml(state.objective)}</objective>`
  ];
  ...
  lines.push("</current_plan>");
  return lines.join("\n");
}

实际看起来类似这样:

<current_plan revision="3" review_status="approved">
  <objective>Update docs to match the runtime.</objective>
  <reviewed_at>2026-06-18T13:00:00.000Z</reviewed_at>
  <current_step id="write" />
  <step id="inspect" status="completed">Inspect current runtime files.</step>
  <step id="write" status="in_progress" current="true">Patch README and chapter docs.</step>
  <step id="test" status="pending">Run tests and build.</step>
</current_plan>

接入点在  Agent.prepareRequest()

const systemPrompt = this.withRuntimePlanningMode(
  this.withDynamicPlanningSnapshot(request.systemPrompt),
  planningMode
);

withDynamicPlanningSnapshot()  的逻辑很简单:

private withDynamicPlanningSnapshot(systemPrompt: string | undefined): string | undefined {
  if (!this.planning?.includeSnapshot) {
    return systemPrompt;
  }
  const snapshot = formatPlanSnapshot(this.planning.planner.state, {
    maxSteps: this.planning.maxSteps
  });
  if (!snapshot) {
    return systemPrompt;
  }
  if (!systemPrompt || systemPrompt.trim() === "") {
    return snapshot;
  }
  return `${systemPrompt}\n\n${snapshot}`;
}

这不是最完美的缓存设计,因为 plan snapshot 每次变化都会影响它之后的 prompt 内容。但第一版选择了最小侵入的方式:先把动态计划状态注入系统提示词末尾,保证模型每轮都能看见。

后续如果要更严格优化 prompt cache,可以把动态 request fragments 从 system prompt 中拆出去,交给 Context Engine 统一调度。现在先保持实现简单。

# 计划审批闸门:计划好之前不能动手

和 Codex 一样,Plan 需要被人类用户审批后再实际执行。如果 Planner 只负责记录状态,那模型仍然可能这样做:

create_plan
write_file
execute_command
final answer

这等于计划还没被用户看过,Agent 就已经开始执行了。对于读文件、搜索这种低风险动作还好;对于写文件、跑 shell、写长期 memory,就不太合适。

所以 stage4 在工具系统里给工具增加了  access

export type ToolAccess = "read" | "write" | "execute" | "planner";
export type AgentTool = {
  name: string;
  description: string;
  parameters: JsonSchema;
  access?: ToolAccess;
  executionMode?: ToolExecutionMode;
  execute(args: unknown, context: ToolExecutionContext): Promise<ToolResult> | ToolResult;
};

核心工具会标明自己的访问类型:

calculator       -> read
read_file        -> read
web_search       -> read
fetch_url        -> read
write_file       -> write
append_file      -> write
execute_command  -> execute
write_note       -> write
store_memory     -> write
planning tools   -> planner

然后 Agent 在每轮构造 runtime tool registry 时,根据计划状态过滤可见工具:

private shouldGatePrePlanTools(): boolean {
  if (!this.planning?.requirePlanBeforeMutation) {
    return false;
  }
  const plan = this.planning.planner.state;
  return !plan || plan.reviewStatus !== "approved";
}

过滤逻辑是:

function isAllowedBeforePlan(tool: AgentTool, plan: PlanState | undefined): boolean {
  if (tool.access === "read") {
    return true;
  }
  if (tool.access !== "planner") {
    return false;
  }
  if (!plan) {
    return tool.name === CREATE_PLAN_TOOL_NAME || tool.name === READ_PLAN_TOOL_NAME;
  }
  return true;
}

也就是说:

当前状态可见工具
没有 planread tools +  create_plan  +  read_plan
plan pendingread tools + planning tools
plan approved全部工具

这个设计有几个好处。

第一,Agent 在创建计划前仍然可以做必要的信息收集。比如读文件、列目录、搜索网页。这避免了「完全不了解环境就瞎计划」。

第二,写入和执行类工具会被挡住。模型即使想调用  write_file ,请求里也看不到这个工具(不缓存友好);如果它硬要调用,ToolExecutor 也只会返回  Tool not found

第三,审批状态不是只靠模型自觉,而是 runtime 真的改变了工具可使用性。这里开始有一点 Harness 治理层的味道了。

# Plan Review:创建计划后暂停

工具闸门解决了「审批前不能执行高风险动作」,但还需要一个机制告诉用户:计划已经准备好了,请确认。

这个机制被设计在  Agent.runLoop()  里。

模型调用  create_plan  后,Planner 的  reviewStatus  会是  pending 。这一轮工具结果回填后,Agent 会检查是否需要暂停:

const reviewResult = this.buildPlanReviewResult();
if (reviewResult) {
  this.messages.push(reviewResult.message);
  await this.emit({ type: "message", message: reviewResult.message }, streamEventSink);
  const result = this.buildResult(reviewResult.message.content, turn, "plan_review");
  await this.emit({ type: "agent_end", result }, streamEventSink);
  return result;
}

buildPlanReviewResult()  会生成一条 assistant message:

private buildPlanReviewResult(): { message: AssistantMessage } | undefined {
  const plan = this.planning?.planner.state;
  if (!plan || plan.reviewStatus !== "pending") {
    return undefined;
  }
  const message: AssistantMessage = {
    role: "assistant",
    content: formatPlanReviewRequest(plan, { maxSteps: this.planning?.maxSteps })
  };
  return { message };
}

输出大概是:

Plan ready for review.
Objective: Write file
Steps:
1. [pending current] write: Write file
Please review this plan. Approve it to continue execution, or reply with changes.

同时  AgentRunResult.stoppedBy  新增了一个状态:

export type AgentRunResult = {
  output: string;
  messages: AgentMessage[];
  turns: number;
  stoppedBy: "final" | "max_turns" | "plan_review";
  plan?: PlanState;
};

这让 UI 可以明确区分:这次 run 不是失败,也不是完成,而是进入了等待用户审阅的暂停点。

Demo 里会打印:

[plan] waiting for user review. Use /plan approve to approve and continue, or reply with changes.

用户审批后,demo 不是直接改内部状态,而是通过一个 synthetic tool call 继续 Agent Loop:

const result = await agent.continueWithToolCall(
  {
    id: `call_plan_approve_${Date.now().toString(36)}`,
    name: APPROVE_PLAN_TOOL_NAME,
    arguments: note ? { note } : {}
  },
  maxTurns === undefined ? {} : { maxTurns }
);

这个设计很干净。审批本身也作为 tool result 进入 history,模型下一轮能看到:

Plan approved.
{ ... reviewStatus: "approved" ... }

于是执行继续,写入和 shell 工具也重新变为可见。

# Final Answer Guard:计划没完成之前不能停下

有了计划创建和审批,还剩另一个常见问题:模型太早说「完成了」。

比如 plan 已经 approved,里面还有一个  pending  step,但模型直接输出:

done (too early!)

如果 Agent Loop 只按「没有 tool call 就结束」的规则运行,它就会把这个回答当作 final answer。

stage4 为解决这种问题加了一层 plan guard。逻辑仍然放在  runLoop()  里:

if (toolCalls.length === 0) {
  const planGuardResult = this.buildPlanGuardResult();
  if (planGuardResult) {
    const guardedAssistant = this.withSyntheticToolCall(assistant, planGuardResult.toolCall);
    ...
    continue;
  }
  ... final answer ...
}

buildPlanGuardResult()  的判断条件是:

const plan = this.planning?.planner.state;
if (!plan) {
  return undefined;
}
if (plan.reviewStatus !== "approved" || !hasOpenPlanSteps(plan)) {
  return undefined;
}

hasOpenPlanSteps()  会检查是否仍有  pending  或  in_progress  step。

如果计划已审批,且仍有 open step,Agent 会合成一个  read_plan  工具调用,把模型拉回循环里:

Plan mode guard: the conversation cannot end while plan steps are pending or in_progress.
Continue the task, or update the plan by completing, blocking, cancelling, or revising the open steps before the final answer.
<current_plan revision="2" review_status="approved">
  ...
</current_plan>

这是一种很实用的 Harness 介入方式:

  • 不要求模型永远自觉。
  • 不打断整个 Agent Loop。
  • 不把错误抛给用户。
  • 只是把一个结构化 observation 塞回模型,让它继续完成或更新计划。

这也让 Planner 开始从「记录工具」变成「行为约束」。它不再只是告诉模型现在做到哪了,而是参与决定什么时候可以结束。

# 接入 AgentConfig 和 Agent Loop

规划能力通过  AgentConfig.planning  开启:

planning?:
  | false
  | {
      planner?: Planner | PlanState;
      includeInstructions?: boolean;
      includeSnapshot?: boolean;
      maxSteps?: number;
      requirePlanBeforeMutation?: boolean;
    };

默认行为下,plan mode 是默认启用的,会启用 planning instructions、plan snapshot,以及 mutation gate。

内部解析成:

type ResolvedAgentPlanning = {
  planner: Planner;
  includeInstructions: boolean;
  includeSnapshot: boolean;
  maxSteps: number;
  requirePlanBeforeMutation: boolean;
};

接入点主要有四处。

第一处是构造系统提示词时追加静态 planning instructions:

if (this.planning?.includeInstructions) {
  fragments.push(buildPlanningInstructions({
    requirePlanBeforeMutation: this.planning.requirePlanBeforeMutation
  }));
}

第二处是构造 runtime tools 时追加 planning tools:

if (this.planning) {
  tools.push(...createPlanningTools(this.planning.planner));
}

第三处是每轮 request 准备阶段追加动态 plan snapshot:

const systemPrompt = this.withRuntimePlanningMode(
  this.withDynamicPlanningSnapshot(request.systemPrompt),
  planningMode
);

第四处是 run result 暴露当前 plan:

private buildResult(output: string, turns: number, stoppedBy: AgentRunResult["stoppedBy"]): AgentRunResult {
  return {
    output,
    messages: [...this.messages],
    turns,
    stoppedBy,
    plan: this.planning?.planner.state
  };
}

Planner 只是加入了新的 runtime tools、新的 prompt fragments、新的动态状态片段,以及两个围绕计划状态的 guard。Loop 本身没有变成一个复杂的 workflow engine。

# 和 Memory 的协作规则

Planner 加入后,一个问题会变得很实际:哪些东西该放 plan,哪些东西该放 memory?

我的默认规则是:Planner 保存骨架,Memory 保存血肉。

适合放进 Planner 的内容:

  • 当前任务目标。
  • 步骤拆解。
  • 当前正在推进的 step。
  • 每一步的状态。
  • 步骤依赖。
  • 完成条件。
  • 完成证据。
  • 阻塞原因。

适合放进 Workspace Memory 的内容:

  • 文件阅读结论。
  • 设计决策。
  • 错误排查记录。
  • 重要但较长的命令输出摘要。
  • 后续可能需要召回的上下文。
  • 和某个步骤相关但不适合塞进 evidence 的细节。

比如一个 coding task 里,plan 可以写:

step: Update README usage section
status: completed
evidence:
  - wrote README.md plan mode section
  - npm test passed

Workspace note 可以写:

The plan mode implementation gates write/execute tools until reviewStatus is approved.
Before approval, read tools remain visible so the model can inspect files and gather context.

长期 Memory 则更克制。只有当这个经验跨任务可复用时,才应该写入  .agent-memory/MEMORY.md 。如果把所有 plan step 都写进长期 memory,memory store 很快会被任务噪声污染。Planner 的存在反而能保护 Memory:短期任务进度留在 plan,长期经验才进入 memory。

# 这一章完成后,Agent 发生了什么变化?

  • 第二章完成后,Agent 能循环调用工具。
  • 第 2.5 章完成后,Agent 能接触文件、命令和网页。
  • 第三章完成后,Agent 能管理自己每轮看到的上下文。
  • 第四章完成后,Agent 能保存和召回重要状态。
  • 第五章完成后,Agent 开始拥有一份显式行动结构:
user goal
  -> plan steps
  -> review status
  -> current step
  -> dependencies
  -> evidence
  -> pending / in_progress / completed / blocked / cancelled
  -> final answer guard

这个变化会让长任务稳定很多。当模型想提前结束时,plan guard 会把它拉回当前任务。当模型想在计划审批前写文件或跑命令时,tool gate 会挡住高风险工具。当用户切换 session 再回来时,plan state 还在。当某个步骤完成时,它必须带 evidence。这些东西单看都不复杂,但组合起来,Agent 的行为就从「会聊天和用工具」往「能稳定推进长任务」迈了一步。

# 下一章做什么?

Planner 让任务有了结构,但它还不能自动判断自己是不是陷入了坏循环。

比如:

  • 同一个命令连续失败三次。
  • 模型反复修改同一个文件但测试一直不过。
  • 当前 step 长时间没有推进。
  • 工具错误被模型忽略。
  • plan 虽然存在,但 evidence 质量很差。

这些问题不是 Planner 本身应该解决的。Planner 只知道状态,不负责诊断失败模式。

所以下一章会进入 Reflector:反思与纠错模块。它要解决的问题是:当 Agent 已经有了上下文管理、记忆和计划之后,系统怎样发现「我正在原地打转」?怎样在连续失败时介入?什么时候应该要求模型重新审视计划?什么时候应该请求用户确认?