docs: add agent-loop paradigms note and OpenCode plan-mode analysis
agent-loop-notes.md: common agent-loop paradigms (ReAct, Plan-and-Execute, Reflection, Tool-use Loop, multi-agent, ToT/LATS) plus framework comparison of OpenClaw / OpenCode / Claude Code, and an Extended Thinking section corrected to adaptive thinking. opencode-plan-mode.md: source-read of how OpenCode implements Plan Mode as a named permission ruleset (agent), enforced via evaluate/disabled/ask. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# Agent Loop 范式笔记
|
||||
|
||||
## 一、常见 Agent Loop 范式
|
||||
|
||||
### 1. ReAct(Reason + Act)
|
||||
最经典的范式,每一步:**思考 → 行动 → 观察**,循环直到完成。
|
||||
|
||||
```
|
||||
Thought: 我需要查一下天气
|
||||
Action: search("北京天气")
|
||||
Observation: 晴,25°C
|
||||
Thought: 已有结果,可以回答了
|
||||
Final Answer: ...
|
||||
```
|
||||
|
||||
### 2. Plan-and-Execute
|
||||
先整体规划,再逐步执行。规划与执行分离,适合长任务。
|
||||
|
||||
### 3. Reflection / Self-critique
|
||||
执行后自我评估,不满意就修正再执行。代表:Reflexion 论文。
|
||||
|
||||
### 4. Tool-use Loop
|
||||
模型输出 tool call,执行工具,结果追加到上下文,继续推理。Claude Code 本身就是这个范式。
|
||||
|
||||
### 5. Multi-agent(Orchestrator + Subagents)
|
||||
一个 orchestrator 分解任务,派发给专门的 subagent 并行执行,汇总结果。
|
||||
|
||||
### 6. Tree of Thoughts(ToT)
|
||||
在每个决策点展开多个分支,用评分函数剪枝,找最优路径。
|
||||
|
||||
### 7. LATS
|
||||
ToT + Monte Carlo Tree Search,引入 UCB 策略做节点选择。
|
||||
|
||||
### 选型参考
|
||||
|
||||
| 场景 | 推荐范式 |
|
||||
|------|---------|
|
||||
| 通用任务、工具调用 | ReAct / Tool-use Loop |
|
||||
| 长流程、复杂项目 | Plan-and-Execute |
|
||||
| 代码生成/需要验证 | Reflection |
|
||||
| 并行子任务 | Multi-agent |
|
||||
| 数学/逻辑推理 | Tree of Thoughts |
|
||||
|
||||
---
|
||||
|
||||
## 二、ReAct 详解
|
||||
|
||||
ReAct 是 2022 年 Google 提出的论文(**Re**asoning + **Act**ing),核心思想是让 LLM **交替生成推理链和动作**。
|
||||
|
||||
每一轮由三部分组成:
|
||||
```
|
||||
Thought: [模型的内部推理,分析当前情况]
|
||||
Action: [调用工具或执行操作]
|
||||
Observation: [工具返回的结果,注入回上下文]
|
||||
```
|
||||
|
||||
**为什么有效:**
|
||||
- Thought 让模型显式推理,避免盲目行动
|
||||
- Observation 把真实世界的反馈注入推理链,纠正幻觉
|
||||
- 两者交织,形成"落地"的推理
|
||||
|
||||
| | Chain-of-Thought | ReAct |
|
||||
|--|--|--|
|
||||
| 推理 | 有 | 有 |
|
||||
| 工具调用 | 无 | 有 |
|
||||
| 外部信息 | 无 | 实时获取 |
|
||||
| 适合场景 | 数学/逻辑 | 信息检索、操作任务 |
|
||||
|
||||
---
|
||||
|
||||
## 三、典型框架分析
|
||||
|
||||
### OpenClaw
|
||||
开源通用 agent 平台。核心范式是**以 ReAct 为骨架的 tool-use loop**:
|
||||
|
||||
- **主循环**:ReAct / tool-use loop — 见 `agent-loop.md` 里的双重循环(外层 steering/follow-up 续命 + 内层"请求→执行工具→再请求"),这是已对照源码核实的实际形态。
|
||||
- **可选叠加**:subagent 委派(Agent/Task 工具)属于按需扩展,不是默认的"多 agent 编排";prompt 层的 todo/计划相当于轻量 plan 层。
|
||||
|
||||
> 不要把它描述成"Gateway 作 orchestrator 调度多个专职 subagent"——Gateway 是单一控制平面,agent 主体仍是单循环。下面是高层组件示意(非严格架构图):
|
||||
|
||||
```
|
||||
Channel → Gateway(控制平面)
|
||||
↓
|
||||
Agent Loop(ReAct 主循环)
|
||||
↙ ↘
|
||||
Memory & Knowledge Plugins & Skills
|
||||
↓
|
||||
LLM Provider
|
||||
```
|
||||
|
||||
### OpenCode
|
||||
SST 团队开源的终端原生编程 agent,bring-your-own-provider(号称 75+ 提供商)。**核心 agent 逻辑用 TypeScript 写,TUI 用 Go(Bubble Tea 风格),TUI 作为本地 agent server 的客户端**——不是"纯 Go 编写"。
|
||||
|
||||
- **核心**:Tool-use Loop
|
||||
- **特色**:Plan/Build 双模式(Plan-and-Execute 的落地实现)
|
||||
|
||||
| 模式 | 权限 | 职责 |
|
||||
|------|------|------|
|
||||
| Plan Mode | 受限(只读为主) | 分析需求、制定方案 |
|
||||
| Build Mode | 完整权限 | 执行:改代码、重构、跑测试 |
|
||||
|
||||
### Claude Code
|
||||
Anthropic 官方的终端编程 agent,只接 Claude 模型。也是**以 ReAct 为骨架的 tool-use loop**,但把多种范式的扩展点做得最完整——值得作为"骨架 + 按需叠加"工程化形态的参照:
|
||||
|
||||
- **主循环(ReAct / tool-use loop)**:模型流式输出 → 检测 tool call → 执行 → 结果回写 transcript → 继续,直到 `end_turn`。
|
||||
- **专用工具而非裸 bash**:`Read`/`Edit`/`Bash`/`Grep`/`Glob` 等独立工具,让 harness 能逐工具做**权限闸门、staleness 校验(编辑前文件改过就拒)、自定义渲染、并行调度**——这是"为什么要把动作提升成专用工具"的典型落地。
|
||||
- **Plan 层**:Plan Mode(`EnterPlanMode`/`ExitPlanMode`)先只读分析、产出计划交人确认再执行,是 Plan-and-Execute 的轻量门控;TodoList 工具提供更细的任务跟踪。
|
||||
- **Multi-agent(orchestrator-worker)**:`Task`/`Agent` 工具派发 subagent(如 Explore、general-purpose、Plan),可给 subagent 配**更便宜的模型**(Explore 用 Haiku)以省 token,并隔离主循环的上下文与 prompt cache。
|
||||
- **上下文管理**:compaction(接近上限时服务端摘要)+ context editing(裁剪陈旧 tool 结果/thinking),配合 prompt cache 的确定性排序。
|
||||
- **其它扩展**:Hooks(事件钩子)、Skills(按需渐进披露的领域指令)、MCP(外部工具/数据源)、文件式 Memory(`CLAUDE.md` + memory 目录跨会话持久化)、Adaptive thinking。
|
||||
|
||||
> 一句话:Claude Code ≈ **ReAct 主循环 + 专用工具闸门 + Plan 门控 + subagent 委派 + 上下文压缩** 的合集,正好把第一节那张"范式叠加"表落到了一个产品里。
|
||||
|
||||
### 对比
|
||||
|
||||
| | OpenCode | OpenClaw | Claude Code |
|
||||
|--|--|--|--|
|
||||
| 定位 | 编程专用 coding agent | 通用 agent 平台 | 官方终端编程 agent |
|
||||
| 模型 | provider 无关(75+) | 多 provider | 仅 Claude |
|
||||
| 核心范式 | Tool-use Loop + Plan/Build 双模式 | 以 ReAct 为骨架的 tool-use loop(subagent 按需叠加) | ReAct 骨架 + Plan 门控 + subagent 委派 |
|
||||
| 特色 | provider 无关、LSP 集成、终端原生 | 插件生态、持久记忆、本地运行 | 专用工具闸门、Skills/Hooks/MCP、上下文压缩 |
|
||||
|
||||
---
|
||||
|
||||
## 四、Tool-use Loop vs ReAct
|
||||
|
||||
**ReAct** 是一种**提示范式**,强调把推理(Thought)显式写出来,和行动交织在一起。
|
||||
|
||||
**Tool-use Loop** 是一种**执行机制**,描述"调用工具 → 拿到结果 → 继续"这个循环本身,不强调推理是否显式。
|
||||
|
||||
```
|
||||
Tool-use Loop(大概念)
|
||||
├── ReAct 实现(Thought 显式输出)
|
||||
└── 隐式推理实现(模型内部思考,直接输出 tool call)
|
||||
```
|
||||
|
||||
**ReAct 是 Tool-use Loop 的一种实现方式**,区别在于推理是否暴露出来。
|
||||
|
||||
现代 LLM 通过 Function Calling API 实现的循环,推理通常在模型内部,对外只暴露 tool call,更接近隐式的 Tool-use Loop。
|
||||
|
||||
---
|
||||
|
||||
## 五、`<thinking>` 的实现
|
||||
|
||||
### 两种不同的东西
|
||||
|
||||
**1. 扩展思考(Extended Thinking)— 模型层面**
|
||||
|
||||
Anthropic 在训练时赋予模型的原生能力,在给出最终回答前先生成一段草稿推理:
|
||||
- 通过 API 的 `thinking` 参数开启
|
||||
- thinking block 会出现在 `response.content` 里(与 `text` 块并列);在多轮 tool use 场景中必须把它回传给模型,并非"不进上下文"
|
||||
- 本质是让模型有更多 token 预算来"想清楚再说"
|
||||
|
||||
最新 Opus 4.8 / 4.7 只支持 **adaptive thinking**(由模型自行决定何时、想多深),固定 `budget_tokens` 已被移除——发 `{type:"enabled", budget_tokens:N}` 会返回 400。思考深度改用 `effort` 控制:
|
||||
|
||||
```python
|
||||
response = client.messages.create(
|
||||
model="claude-opus-4-8",
|
||||
thinking={"type": "adaptive"},
|
||||
output_config={"effort": "high"}, # low|medium|high|xhigh|max
|
||||
messages=[...]
|
||||
)
|
||||
# response.content 里会有 type="thinking" 和 type="text" 两块
|
||||
# 注意:4.8/4.7 默认 display="omitted"(thinking 文本为空),
|
||||
# 需 thinking={"type":"adaptive","display":"summarized"} 才能看到推理摘要
|
||||
```
|
||||
|
||||
> 老模型(如 Sonnet 4.5)仍用旧写法 `thinking={"type":"enabled","budget_tokens":N}`(须 < `max_tokens`)。固定 token 预算这一概念整体上已被 adaptive thinking 取代。
|
||||
|
||||
**2. ReAct 的 Thought — 提示词层面**
|
||||
|
||||
通过 few-shot 示例让模型学会先写推理再写行动,是输出格式的约定,不是模型原生能力。
|
||||
|
||||
### 对比
|
||||
|
||||
| | Extended Thinking | ReAct Thought |
|
||||
|--|--|--|
|
||||
| 实现层 | 模型训练 + API 参数 | 提示词格式约定 |
|
||||
| 推理深度 | 更深,可自我纠错 | 受限于单次输出 |
|
||||
| 对上下文可见性 | 可选暴露给调用方 | 完全可见 |
|
||||
| 出现时间 | Claude 3.7+ / o1 | 2022 年论文 |
|
||||
|
||||
**本质**:两者都是给模型"打草稿"的空间。Extended Thinking 是把这个能力内化到模型权重里,更自然、更强;ReAct Thought 是用提示词"骗"模型模拟这个行为,是工程 hack。
|
||||
@@ -0,0 +1,154 @@
|
||||
# OpenCode Plan Mode 是怎么实现的
|
||||
|
||||
> 基于 sibling 仓库 `../opencode` 源码实读。关键文件:
|
||||
> `packages/opencode/src/agent/agent.ts`、`.../permission/index.ts`、
|
||||
> `.../session/llm/request.ts`、`.../tool/plan.ts`、`.../tool/edit.ts`。
|
||||
|
||||
## 一句话
|
||||
|
||||
OpenCode **没有专门的「plan mode」状态机**。Plan Mode 就是一个叫 `plan` 的 **agent**,
|
||||
本质是一套 **permission 规则集(Ruleset)**;它复用了 OpenCode 给每次工具调用做权限
|
||||
闸门的同一套机制。「进/出 plan」= 切换这一回合归属的 agent。
|
||||
|
||||
---
|
||||
|
||||
## 1. Mode 即 Agent:plan 是预置 agent 之一
|
||||
|
||||
`agent.ts:138` 起定义了四个 native agent,每个就是一个 permission Ruleset:
|
||||
|
||||
| agent | mode | 作用 |
|
||||
|-------|------|------|
|
||||
| `build` | primary | 默认 agent,按配置权限执行(可编辑) |
|
||||
| `plan` | primary | Plan mode,禁所有编辑工具 |
|
||||
| `general` | subagent | 通用并行子任务 |
|
||||
| `explore` | subagent | 只读探索代码库 |
|
||||
|
||||
`plan` 的定义(`agent.ts:154`):
|
||||
|
||||
```ts
|
||||
plan: {
|
||||
description: "Plan mode. Disallows all edit tools.",
|
||||
permission: Permission.merge(
|
||||
defaults, // "*": allow;plan_enter/plan_exit/question 默认 deny
|
||||
Permission.fromConfig({
|
||||
question: "allow",
|
||||
plan_exit: "allow", // 只有 plan agent 能退出
|
||||
task: { general: "deny" }, // 禁止派 general subagent(否则可绕道写文件)
|
||||
edit: {
|
||||
"*": "deny", // ★ 核心:禁所有编辑
|
||||
[".opencode/plans/*.md"]: "allow", // 但允许写计划 md
|
||||
[globalPlansDir + "/*.md"]: "allow",
|
||||
},
|
||||
}),
|
||||
user, // 用户 config 最后合并,可覆盖
|
||||
),
|
||||
mode: "primary", native: true,
|
||||
}
|
||||
```
|
||||
|
||||
对照 `build`(`agent.ts:139`):`plan_enter: "allow"` + `question: "allow"`,继承
|
||||
`"*": "allow"`,所以能编辑、也能进入 plan。
|
||||
|
||||
`defaults`(`agent.ts:117`)是所有 agent 的底:`"*": "allow"`、`doom_loop: "ask"`、
|
||||
`question: "deny"`、`plan_enter/plan_exit: "deny"`、`read` 对 `*.env` 类 `ask`、
|
||||
外部目录 `ask`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则集怎么被执行:last-match-wins + 两层闸门
|
||||
|
||||
### 规则求值
|
||||
|
||||
`evaluate()`(`permission/index.ts:39`)在合并后的 ruleset 里 `findLast` 命中
|
||||
`{permission, pattern}` 的规则——**后写覆盖先写**(所以 `user` 放在 merge 最后能
|
||||
覆盖默认),无命中则默认 `ask`。
|
||||
|
||||
### 第一层 · 整工具移除(发请求前)
|
||||
|
||||
`resolveTools()`(`session/llm/request.ts:198`)调 `Permission.disabled()`
|
||||
(`index.ts:215`):
|
||||
|
||||
```ts
|
||||
export function disabled(tools, ruleset) {
|
||||
const edits = ["edit", "write", "apply_patch"]
|
||||
return new Set(tools.filter((tool) => {
|
||||
const permission = edits.includes(tool) ? "edit" : tool
|
||||
const rule = ruleset.findLast((r) => Wildcard.match(permission, r.permission))
|
||||
return rule?.pattern === "*" && rule.action === "deny" // 仅当 "*" 全禁才删
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
一个工具若其**最后命中**规则是 `pattern === "*" && action === "deny"`,就整个从
|
||||
模型可见工具表里删掉(`edit/write/apply_patch` 都映射到 `edit` 这个 key)。
|
||||
|
||||
### 第二层 · 调用时逐次 ask(运行时)
|
||||
|
||||
工具执行时自己调 `ctx.ask(...)`。例如 `tool/edit.ts:102,145`:
|
||||
|
||||
```ts
|
||||
yield* ctx.ask({ permission: "edit", patterns: [文件路径], ... })
|
||||
```
|
||||
|
||||
`Permission.ask()`(`index.ts:78`)对每个 pattern 跑 `evaluate`:
|
||||
|
||||
- `deny` → 抛 `DeniedError`,该 tool call 被拒,错误回写给模型;
|
||||
- `allow` → 静默放行;
|
||||
- `ask` → 发 `Asked` 事件、阻塞在 Deferred 上,等用户回 once / always / reject。
|
||||
|
||||
### 为什么 plan 能「只读但能写计划文件」
|
||||
|
||||
关键细节:plan 给 `.opencode/plans/*.md` 留了 `allow`,所以 `disabled()` 的
|
||||
`findLast("edit")` 命中的是那条 **path 级 allow**(`pattern` 不是 `"*"`)——
|
||||
于是 **edit 工具不会被整个删掉**,而是保留下来、在运行时按路径逐个判:
|
||||
|
||||
- 写普通文件 → 命中 `edit "*": deny` → `DeniedError` 挡掉;
|
||||
- 写 `.opencode/plans/foo.md` → 命中 path allow → 放行。
|
||||
|
||||
这就是「只读分析、但能把计划落地写进 md」的实现方式。
|
||||
|
||||
---
|
||||
|
||||
## 3. 进/出 Plan:`plan_enter` / `plan_exit` 是受权限管的伪工具
|
||||
|
||||
切换不是 UI flag,而是两个权限动作:
|
||||
|
||||
- `build`:`plan_enter: allow`、`plan_exit: deny`(继承)→ 只能进
|
||||
- `plan`:`plan_exit: allow`→ 只能出
|
||||
|
||||
退出由 `plan_exit` 工具实现(`tool/plan.ts:15`):
|
||||
|
||||
1. `question.ask(...)` 弹确认:「计划已完成,要切到 build agent 开始实现吗?」
|
||||
(`plan.ts:30`)
|
||||
2. 选 **No** → 抛 `Question.RejectedError`,留在 plan(`plan.ts:46`)。
|
||||
3. 选 **Yes** → 注入一条 **synthetic user 消息**,把 `agent: "build"`
|
||||
(`plan.ts:53-61`)+ 合成文本「计划已批准,你现在可以编辑文件,执行计划」
|
||||
(`plan.ts:67`)。这一回合的归属 agent 翻成 `build`,其 ruleset 放开编辑——
|
||||
权限随之解锁。
|
||||
|
||||
```
|
||||
[plan agent] edit "*": deny ──plan_exit(用户确认 Yes)──▶ 注入 agent="build" 的 user 消息
|
||||
└─ [build agent] "*": allow,可编辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计要点
|
||||
|
||||
OpenCode 把 **plan-mode、只读 explore、各种 subagent、用户自定义 agent 全部统一成
|
||||
「命名的 permission ruleset」**。同一套:
|
||||
|
||||
- `evaluate`(last-match-wins)
|
||||
- `disabled`(整工具删除)
|
||||
- `ask`(运行时 deny / allow / 询问)
|
||||
|
||||
既做单次工具闸门,也实现了 Plan Mode。「换模式」= 换这一回合归属的 agent,
|
||||
经 `plan_enter` / `plan_exit` 且需用户确认。
|
||||
|
||||
> 对比 Claude Code:Claude Code 的 Plan Mode 是独立的 `EnterPlanMode`/`ExitPlanMode`
|
||||
> 门控;OpenCode 则把它压进通用 permission 体系里,代价更小、可被用户 config 复用同
|
||||
> 一套 allow/ask/deny 规则。
|
||||
|
||||
> 注:`../opencode/packages/core/src/plugin/agent.ts:117` 有一份 V1 core 等价镜像
|
||||
> 定义(`{action:"plan_enter", effect:"deny"}` 等),结构一致;上文以
|
||||
> `packages/opencode/src` 下现行实现为准。
|
||||
Reference in New Issue
Block a user