这是「从零搭建 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。(真是真实任务!) |
这类任务不会只靠「记住事实」就自然完成。它至少包含几层结构:
- 先确认当前仓库状态和 stage4 变更。
- 再阅读前四章文章,找出系列写作风格。
- 再理解第五章应该承接哪个问题。
- 再提炼代码实现细节。
- 最后写出完整文章,并检查文件是否落盘。
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; | |
} |
也就是说:
| 当前状态 | 可见工具 |
|---|---|
| 没有 plan | read tools + create_plan + read_plan |
| plan pending | read 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 已经有了上下文管理、记忆和计划之后,系统怎样发现「我正在原地打转」?怎样在连续失败时介入?什么时候应该要求模型重新审视计划?什么时候应该请求用户确认?