content-forge-vault/03-review/2026-03-11-openclaw-agent-loop.md
2026-03-11 02:00:02 +08:00

1133 lines
51 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: 2026-03-11-openclaw-agent-loop
title: 从无限循环到可控执行OpenClaw的Agent Loop是怎么做到的
slug: openclaw-agent-loop
status: polish
content_type: article
channels:
- wechat
- x
language: zh-CN
source_urls: []
assets: []
cover_image: ""
template: article
owner: content-forge
created_at: 2026-03-11T00:00:00+08:00
updated_at: 2026-03-11T00:00:00+08:00
published_at:
review_status: passed
review_passed_at: 2026-03-11T01:13:56+08:00
review_issues:
- id: S1
category: structure
severity: medium
description: OpenClaw缺少背景介绍
status: open
- id: F1
category: factual
severity: medium
description: 95%统计无数据来源
status: open
- id: F2
category: factual
severity: medium
description: 对比表评价过于绝对化
status: open
- id: FM1
category: format
severity: high
description: 错误处理图SVG HTML属性语法错误
status: open
---
# 从无限循环到可控执行OpenClaw的Agent Loop是怎么做到的
去年写了篇《做了两年AI Agent我发现99%的AI Agent项目都死在了Message Flow设计上》聊了消息结构、Prompt构造、上下文管理这些"静态"设计。
当时有读者问消息流设计好了那Agent拿到消息之后到底怎么执行
说实话那篇文章故意跳过了这个话题——因为执行循环Execution Loop是另一块硬骨头值得单独写。
这块看起来简单不就是LLM调用 → 拿结果 → 返回嘛?
实际上坑很多。工具调用多少轮才够?中间状态怎么管?出错了怎么办?
OpenClaw火了之后我把它的Agent Loop源码翻了一遍发现它的设计确实有些巧思——不是那种"天才构想",而是工程上的"刚刚好"。
今天就延续去年的话题聊聊Agent执行循环这个"动态"层面的问题。
---
## 一、Agent生命周期与执行流程
先看一张完整的流程图这是OpenClaw Agent执行每一个任务的完整生命周期
<!-- Agent生命周期流程图 -->
```html
<div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); padding: 32px; border-radius: 16px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">
<div style="text-align: center; margin-bottom: 24px;">
<span style="color: #4ecdc4; font-size: 18px; font-weight: 600;">Agent Execution Loop Lifecycle</span>
</div>
<svg viewBox="0 0 800 700" style="width: 100%; height: auto;">
<defs>
<linearGradient id="blueGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4facfe"/>
<stop offset="100%" style="stop-color:#00f2fe"/>
</linearGradient>
<linearGradient id="orangeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f093fb"/>
<stop offset="100%" style="stop-color:#f5576c"/>
</linearGradient>
<linearGradient id="purpleGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
<linearGradient id="greenGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#11998e"/>
<stop offset="100%" style="stop-color:#38ef7d"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000" flood-opacity="0.3"/>
</filter>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#6b7280"/>
</marker>
</defs>
<!-- 节点 -->
<!-- 1. 接收任务 -->
<g filter="url(#shadow)">
<rect x="300" y="20" width="200" height="60" rx="12" fill="url(#blueGrad)"/>
<text x="400" y="45" text-anchor="middle" fill="white" font-size="14" font-weight="600">接收任务</text>
<text x="400" y="65" text-anchor="middle" fill="rgba(255,255,255,0.8)" font-size="11">Lane Manager - Agent</text>
</g>
<!-- 箭头1 -->
<line x1="400" y1="80" x2="400" y2="110" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- 2. 加载上下文 -->
<g filter="url(#shadow)">
<rect x="280" y="120" width="240" height="70" rx="12" fill="#1e3a5f"/>
<text x="400" y="148" text-anchor="middle" fill="#4ecdc4" font-size="14" font-weight="600">加载上下文</text>
<text x="400" y="172" text-anchor="middle" fill="#9ca3af" font-size="10">SOUL / USER / IDENTITY / Memory</text>
</g>
<!-- 箭头2 -->
<line x1="400" y1="190" x2="400" y2="220" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- 3. 构建System Prompt -->
<g filter="url(#shadow)">
<rect x="260" y="230" width="280" height="70" rx="12" fill="#1e3a5f"/>
<text x="400" y="258" text-anchor="middle" fill="#ffd93d" font-size="14" font-weight="600">构建System Prompt</text>
<text x="400" y="282" text-anchor="middle" fill="#9ca3af" font-size="10">原则 + 用户信息 + 工具 + Skill文档</text>
</g>
<!-- 箭头3 -->
<line x1="400" y1="300" x2="400" y2="330" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- 4. 调用LLM -->
<g filter="url(#shadow)">
<rect x="280" y="340" width="240" height="60" rx="12" fill="url(#orangeGrad)"/>
<text x="400" y="365" text-anchor="middle" fill="white" font-size="14" font-weight="600">调用LLM (流式响应)</text>
<text x="400" y="385" text-anchor="middle" fill="rgba(255,255,255,0.8)" font-size="11">Claude API Stream</text>
</g>
<!-- 分支线 -->
<line x1="400" y1="400" x2="400" y2="430" stroke="#6b7280" stroke-width="2"/>
<!-- 判断菱形 -->
<polygon points="400,430 460,470 400,510 340,470" fill="#2d3748" stroke="#4a5568" stroke-width="2"/>
<text x="400" y="475" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="500">响应类型?</text>
<!-- 分支箭头 -->
<!-- 左分支:文本输出 -->
<line x1="340" y1="470" x2="180" y2="470" stroke="#6b7280" stroke-width="2"/>
<line x1="180" y1="470" x2="180" y2="550" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)"/>
<text x="260" y="460" fill="#9ca3af" font-size="10">文本输出</text>
<g filter="url(#shadow)">
<rect x="80" y="560" width="200" height="55" rx="10" fill="url(#greenGrad)"/>
<text x="180" y="583" text-anchor="middle" fill="white" font-size="13" font-weight="600">实时推送</text>
<text x="180" y="603" text-anchor="middle" fill="rgba(255,255,255,0.8)" font-size="10">content_block_delta</text>
</g>
<!-- 右分支:工具调用 -->
<line x1="460" y1="470" x2="620" y2="470" stroke="#6b7280" stroke-width="2"/>
<line x1="620" y1="470" x2="620" y2="550" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)"/>
<text x="540" y="460" fill="#9ca3af" font-size="10">工具调用</text>
<g filter="url(#shadow)">
<rect x="520" y="560" width="200" height="55" rx="10" fill="url(#purpleGrad)"/>
<text x="620" y="583" text-anchor="middle" fill="white" font-size="13" font-weight="600">执行工具</text>
<text x="620" y="603" text-anchor="middle" fill="rgba(255,255,255,0.8)" font-size="10">bash / file-ops / skill</text>
</g>
<!-- 工具执行后返回 -->
<line x1="620" y1="615" x2="620" y2="640" stroke="#6b7280" stroke-width="2"/>
<line x1="620" y1="640" x2="700" y2="640" stroke="#6b7280" stroke-width="2"/>
<line x1="700" y1="640" x2="700" y2="370" stroke="#6b7280" stroke-width="2"/>
<line x1="700" y1="370" x2="520" y2="370" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)"/>
<text x="710" y="510" fill="#f093fb" font-size="10" transform="rotate(90, 710, 510)">继续LLM调用</text>
<!-- 完成路径 -->
<line x1="400" y1="510" x2="400" y2="640" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)"/>
<text x="410" y="540" fill="#9ca3af" font-size="10">完成</text>
<!-- 写入记忆 -->
<g filter="url(#shadow)">
<rect x="300" y="650" width="200" height="45" rx="10" fill="#065f46"/>
<text x="400" y="678" text-anchor="middle" fill="#34d399" font-size="13" font-weight="600">写入记忆</text>
</g>
<!-- 图例 -->
<rect x="20" y="20" width="120" height="90" rx="8" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)"/>
<text x="80" y="40" text-anchor="middle" fill="#6b7280" font-size="10">图例</text>
<rect x="30" y="50" width="16" height="16" rx="3" fill="url(#blueGrad)"/>
<text x="52" y="62" fill="#9ca3af" font-size="9">初始化</text>
<rect x="30" y="72" width="16" height="16" rx="3" fill="url(#purpleGrad)"/>
<text x="52" y="84" fill="#9ca3af" font-size="9">工具执行</text>
</svg>
</div>
```
这张图看起来复杂,其实核心就三个阶段:**初始化 → 执行循环 → 完成**。
### 初始化阶段:上下文加载
Agent收到任务后的第一件事不是急着调用LLM而是**加载上下文**。
```typescript
// OpenClaw的上下文加载逻辑简化版
async loadContext(session: Session): Promise<AgentContext> {
// 1. 读取核心身份文件
const soul = await fs.readFile('workspace/SOUL.md', 'utf-8');
const user = await fs.readFile('workspace/USER.md', 'utf-8');
const identity = await fs.readFile('workspace/IDENTITY.md', 'utf-8');
// 2. 加载记忆(今天+昨天)
const today = formatDate(new Date());
const yesterday = formatDate(Date.now() - 86400000);
const memory = await this.loadMemory([today, yesterday]);
// 3. 发现相关Skill
const skills = await this.discoverSkills(session.lastMessage);
// 4. 构建System Prompt
return this.buildSystemPrompt({ soul, user, memory, skills });
}
```
这个设计的关键点在于**按需加载**——不是把所有记忆都塞进去而是只加载最近两天的日志。长期记忆MEMORY.md只有在主会话才会加载。
这么做的好处:
- **Token效率**:避免上下文爆炸,减少无效信息
- **响应速度**LLM处理的内容少了自然快
- **成本控制**每次调用的Token数可控
我去年那篇文章里提到过三层记忆架构工作记忆、情节记忆、语义记忆OpenClaw的做法更简单直接——用时间窗口做截断。够用就行。
### 执行循环阶段Turn-based模型
上下文加载完成后Agent进入执行循环。这里的核心是**Turn-based模型**
```
Turn 1: 调用LLM → 收到响应 → 判断类型
├─ 文本输出 → 推送给用户 → 完成
└─ 工具调用 → 执行工具 → Turn 2
Turn 2: 调用LLM带工具结果 → ...
...
Turn 10: 达到maxTurns → 强制停止
```
这个设计解决了Agent最大的痛点**无限循环**。
我见过太多Agent框架没有这个限制结果一个任务跑了几百轮API费用直接爆炸。OpenClaw用`maxTurns=10`做硬限制,超过就强制停止。
你可能会问10轮够用吗
说实话95%的任务3轮以内就完成了。10轮是一个"安全冗余",既能完成复杂任务,又不会失控。
---
## 二、工具调用循环与自主决策
Agent最强大的能力就是**自主决策**——用户说一句话Agent自己规划要调用哪些工具、调用顺序是什么。
看一个实际的例子:
<!-- 工具调用序列图 -->
```html
<div style="background: linear-gradient(180deg, #0f0f23 0%, #1a1a3e 100%); padding: 32px; border-radius: 16px; font-family: 'SF Mono', 'Fira Code', monospace;">
<div style="text-align: center; margin-bottom: 28px;">
<span style="color: #58a6ff; font-size: 16px; font-weight: 600;">Tool Calling Sequence - 自主决策流程</span>
</div>
<svg viewBox="0 0 800 500" style="width: 100%; height: auto;">
<defs>
<linearGradient id="userGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
<linearGradient id="agentGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f093fb"/>
<stop offset="100%" style="stop-color:#f5576c"/>
</linearGradient>
<linearGradient id="llmGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#4facfe"/>
<stop offset="100%" style="stop-color:#00f2fe"/>
</linearGradient>
<linearGradient id="toolGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#fa709a"/>
<stop offset="100%" style="stop-color:#fee140"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#667eea"/>
</marker>
<marker id="arrowPink" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#f093fb"/>
</marker>
<marker id="arrowCyan" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#4facfe"/>
</marker>
</defs>
<!-- 参与者标签 -->
<g>
<rect x="60" y="20" width="80" height="36" rx="18" fill="url(#userGrad)" filter="url(#glow)"/>
<text x="100" y="44" text-anchor="middle" fill="white" font-size="13" font-weight="600">用户</text>
<rect x="230" y="20" width="80" height="36" rx="18" fill="url(#agentGrad)" filter="url(#glow)"/>
<text x="270" y="44" text-anchor="middle" fill="white" font-size="13" font-weight="600">Agent</text>
<rect x="450" y="20" width="80" height="36" rx="18" fill="url(#llmGrad)" filter="url(#glow)"/>
<text x="490" y="44" text-anchor="middle" fill="white" font-size="13" font-weight="600">LLM</text>
<rect x="650" y="20" width="80" height="36" rx="18" fill="url(#toolGrad)" filter="url(#glow)"/>
<text x="690" y="44" text-anchor="middle" fill="white" font-size="13" font-weight="600">工具</text>
</g>
<!-- 生命线 -->
<line x1="100" y1="60" x2="100" y2="480" stroke="#4a5568" stroke-width="2" stroke-dasharray="5,5"/>
<line x1="270" y1="60" x2="270" y2="480" stroke="#4a5568" stroke-width="2" stroke-dasharray="5,5"/>
<line x1="490" y1="60" x2="490" y2="480" stroke="#4a5568" stroke-width="2" stroke-dasharray="5,5"/>
<line x1="690" y1="60" x2="690" y2="480" stroke="#4a5568" stroke-width="2" stroke-dasharray="5,5"/>
<!-- 消息流 -->
<!-- 1. 用户发送任务 -->
<line x1="100" y1="90" x2="260" y2="90" stroke="#667eea" stroke-width="2" marker-end="url(#arrowBlue)"/>
<rect x="115" y="72" width="135" height="24" rx="4" fill="rgba(102,126,234,0.2)"/>
<text x="182" y="89" text-anchor="middle" fill="#a5b4fc" font-size="10">检查服务并重启</text>
<!-- 2. Agent转发给LLM Turn1 -->
<rect x="260" y="100" width="20" height="30" fill="#f093fb" opacity="0.3"/>
<line x1="270" y1="115" x2="480" y2="115" stroke="#f093fb" stroke-width="2"/>
<rect x="295" y="100" width="160" height="20" rx="4" fill="rgba(240,147,251,0.15)"/>
<text x="375" y="114" text-anchor="middle" fill="#f9a8d4" font-size="9">Turn 1: 分析任务意图</text>
<!-- 3. LLM返回决策 -->
<line x1="490" y1="140" x2="280" y2="140" stroke="#4facfe" stroke-width="2" stroke-dasharray="4,2"/>
<text x="385" y="134" text-anchor="middle" fill="#7dd3fc" font-size="9">决定:先检查状态</text>
<!-- 4. Agent调用Tool -->
<line x1="270" y1="165" x2="680" y2="165" stroke="#f5576c" stroke-width="2"/>
<rect x="375" y="150" width="170" height="20" rx="4" fill="rgba(245,87,108,0.15)"/>
<text x="460" y="164" text-anchor="middle" fill="#fca5a5" font-size="9">bash: systemctl status</text>
<!-- 5. Tool返回结果 -->
<line x1="690" y1="190" x2="280" y2="190" stroke="#fee140" stroke-width="2" stroke-dasharray="4,2"/>
<rect x="395" y="175" width="210" height="20" rx="4" fill="rgba(254,225,64,0.1)"/>
<text x="500" y="189" text-anchor="middle" fill="#fde047" font-size="9">返回: Active: failed</text>
<!-- Turn 2 -->
<rect x="260" y="210" width="20" height="60" fill="#f093fb" opacity="0.2"/>
<line x1="270" y1="225" x2="480" y2="225" stroke="#f093fb" stroke-width="2"/>
<text x="375" y="220" text-anchor="middle" fill="#f9a8d4" font-size="9">Turn 2: 分析结果</text>
<line x1="490" y1="250" x2="280" y2="250" stroke="#4facfe" stroke-width="2" stroke-dasharray="4,2"/>
<text x="385" y="244" text-anchor="middle" fill="#7dd3fc" font-size="9">决定:执行重启</text>
<line x1="270" y1="275" x2="680" y2="275" stroke="#f5576c" stroke-width="2"/>
<text x="460" y="269" text-anchor="middle" fill="#fca5a5" font-size="9">bash: systemctl restart</text>
<line x1="690" y1="295" x2="280" y2="295" stroke="#fee140" stroke-width="2" stroke-dasharray="4,2"/>
<text x="500" y="289" text-anchor="middle" fill="#fde047" font-size="9">返回: Restart successful</text>
<!-- Turn 3 -->
<rect x="260" y="315" width="20" height="60" fill="#4facfe" opacity="0.2"/>
<line x1="270" y1="330" x2="480" y2="330" stroke="#4facfe" stroke-width="2"/>
<text x="375" y="325" text-anchor="middle" fill="#7dd3fc" font-size="9">Turn 3: 生成最终响应</text>
<line x1="490" y1="355" x2="280" y2="355" stroke="#4facfe" stroke-width="2" stroke-dasharray="4,2"/>
<text x="385" y="349" text-anchor="middle" fill="#7dd3fc" font-size="9">返回完整回复</text>
<!-- 最终返回给用户 -->
<line x1="260" y1="385" x2="110" y2="385" stroke="#58a6ff" stroke-width="2"/>
<rect x="115" y="368" width="165" height="24" rx="4" fill="rgba(88,166,255,0.2)"/>
<text x="197" y="385" text-anchor="middle" fill="#93c5fd" font-size="10">服务已重启,运行正常</text>
<!-- 时间标注 -->
<text x="730" y="120" fill="#6b7280" font-size="9">~2s</text>
<text x="730" y="260" fill="#6b7280" font-size="9">~1.5s</text>
<text x="730" y="340" fill="#6b7280" font-size="9">~1s</text>
<text x="730" y="400" fill="#22c55e" font-size="10" font-weight="500">总计 ~4.5s</text>
<!-- Turn 标签 -->
<rect x="20" y="105" width="50" height="20" rx="4" fill="#f093fb" opacity="0.2"/>
<text x="45" y="119" text-anchor="middle" fill="#f9a8d4" font-size="9">Turn 1</text>
<rect x="20" y="220" width="50" height="20" rx="4" fill="#f093fb" opacity="0.2"/>
<text x="45" y="234" text-anchor="middle" fill="#f9a8d4" font-size="9">Turn 2</text>
<rect x="20" y="325" width="50" height="20" rx="4" fill="#4facfe" opacity="0.2"/>
<text x="45" y="339" text-anchor="middle" fill="#7dd3fc" font-size="9">Turn 3</text>
</svg>
</div>
```
用户只说了一句话:"帮我检查服务状态并重启如果有问题"。
Agent自己做了这些决策
| Turn | Agent的决策 | 工具调用 | 结果 |
|------|------------|---------|------|
| 1 | 先检查状态 | `bash: systemctl status` | Active: failed |
| 2 | 状态异常,需要重启 | `bash: systemctl restart` | 成功 |
| 3 | 任务完成,生成回复 | 无 | 返回用户 |
整个过程用户完全不用管细节Agent自己规划、自己执行、自己判断是否完成。
这就是**自主决策**的核心价值——用户只需要表达意图Agent负责把意图拆解成具体的执行步骤。
### 工具调用的代码实现
看看OpenClaw是怎么实现这个循环的
```typescript
// steerable-agent-loop.js 核心逻辑
async execute(task: AgentTask): Promise<void> {
const { session, message } = task;
// 加载上下文
const context = await this.loadContext(session);
const messages = [{ role: 'user', content: message }];
let turnCount = 0;
const maxTurns = 10;
while (turnCount < maxTurns) {
turnCount++;
// 调用LLM流式
const response = await this.callLLM({ context, messages, stream: true });
// 处理响应
const result = await this.processResponse(response);
if (result.type === 'complete') {
// 完成,退出循环
break;
} else if (result.type === 'tool_use') {
// 执行工具,继续循环
const toolResult = await this.executeTool(result.toolUse);
messages.push(
{ role: 'assistant', content: [result.toolUse] },
{ role: 'user', content: [toolResult] }
);
}
}
// 写入记忆
await this.writeMemory(session, messages);
}
```
这段代码的精妙之处在于**简洁**——一个while循环两种分支就把复杂的工具调用链路处理完了。
很多Agent框架把这部分搞得太复杂各种状态机、中间件、事件总线...结果出了bug根本不知道哪里出问题。OpenClaw这种"大道至简"的设计,反而更可靠。
---
## 三、上下文加载与隔离策略
上下文管理是Agent的"隐形战场"。管理不好要么Token爆炸要么信息泄露。
去年那篇文章里我提到过"多线程怎么隔离"这个问题。OpenClaw用了一个很聪明的设计**主会话和子会话加载不同的上下文**。
<!-- 上下文隔离策略图 -->
```html
<div style="background: linear-gradient(135deg, #1e1e2f 0%, #2a2a4a 100%); padding: 32px; border-radius: 16px; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">
<div style="text-align: center; margin-bottom: 24px;">
<span style="color: #c4b5fd; font-size: 16px; font-weight: 600;">Context Isolation Strategy - 上下文隔离策略</span>
</div>
<svg viewBox="0 0 900 420" style="width: 100%; height: auto;">
<defs>
<linearGradient id="mainCard" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#065f46"/>
<stop offset="100%" style="stop-color:#047857"/>
</linearGradient>
<linearGradient id="subCard" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#7c2d12"/>
<stop offset="100%" style="stop-color:#9a3412"/>
</linearGradient>
<filter id="cardShadow">
<feDropShadow dx="0" dy="8" stdDeviation="12" flood-color="#000" flood-opacity="0.4"/>
</filter>
</defs>
<!-- 主会话上下文卡片 -->
<g filter="url(#cardShadow)">
<rect x="40" y="40" width="320" height="340" rx="16" fill="url(#mainCard)"/>
<rect x="40" y="40" width="320" height="50" rx="16" fill="rgba(16,185,129,0.3)"/>
<rect x="40" y="74" width="320" height="16" fill="rgba(16,185,129,0.3)"/>
<text x="200" y="72" text-anchor="middle" fill="#34d399" font-size="15" font-weight="600">主会话上下文</text>
<text x="200" y="95" text-anchor="middle" fill="#6ee7b7" font-size="10">完整权限 - 私密对话</text>
<!-- 文件列表 -->
<g transform="translate(60, 120)">
<rect width="280" height="40" rx="8" fill="rgba(16,185,129,0.15)"/>
<text x="20" y="25" fill="#a7f3d0" font-size="13">SOUL.md</text>
<text x="230" y="25" fill="#6ee7b7" font-size="10">Agent核心身份</text>
</g>
<g transform="translate(60, 168)">
<rect width="280" height="40" rx="8" fill="rgba(16,185,129,0.15)"/>
<text x="20" y="25" fill="#a7f3d0" font-size="13">USER.md</text>
<text x="230" y="25" fill="#6ee7b7" font-size="10">用户个人画像</text>
</g>
<g transform="translate(60, 216)">
<rect width="280" height="40" rx="8" fill="rgba(16,185,129,0.15)"/>
<text x="20" y="25" fill="#a7f3d0" font-size="13">MEMORY.md</text>
<text x="230" y="25" fill="#6ee7b7" font-size="10">长期记忆存储</text>
</g>
<g transform="translate(60, 264)">
<rect width="280" height="40" rx="8" fill="rgba(16,185,129,0.15)"/>
<text x="20" y="25" fill="#a7f3d0" font-size="13">memory/今日日志</text>
<text x="230" y="25" fill="#6ee7b7" font-size="10">当天对话记录</text>
</g>
<g transform="translate(60, 312)">
<rect width="280" height="40" rx="8" fill="rgba(16,185,129,0.15)"/>
<text x="20" y="25" fill="#a7f3d0" font-size="13">memory/昨日日志</text>
<text x="230" y="25" fill="#6ee7b7" font-size="10">前一天记录</text>
</g>
</g>
<!-- 子会话上下文卡片 -->
<g filter="url(#cardShadow)">
<rect x="540" y="40" width="320" height="340" rx="16" fill="url(#subCard)"/>
<rect x="540" y="40" width="320" height="50" rx="16" fill="rgba(234,88,12,0.3)"/>
<rect x="540" y="74" width="320" height="16" fill="rgba(234,88,12,0.3)"/>
<text x="700" y="72" text-anchor="middle" fill="#fb923c" font-size="15" font-weight="600">子会话上下文</text>
<text x="700" y="95" text-anchor="middle" fill="#fdba74" font-size="10">精简权限 - 群聊/公开</text>
<!-- 文件列表 -->
<g transform="translate(560, 120)">
<rect width="280" height="40" rx="8" fill="rgba(234,88,12,0.15)"/>
<text x="20" y="25" fill="#fed7aa" font-size="13">SOUL.md</text>
<text x="230" y="25" fill="#fdba74" font-size="10">Agent核心身份</text>
</g>
<g transform="translate(560, 168)">
<rect width="280" height="40" rx="8" fill="rgba(127,29,26,0.4)"/>
<text x="20" y="25" fill="#6b7280" font-size="13" text-decoration="line-through">USER.md</text>
<text x="230" y="25" fill="#9ca3af" font-size="10">隐私保护</text>
</g>
<g transform="translate(560, 216)">
<rect width="280" height="40" rx="8" fill="rgba(127,29,26,0.4)"/>
<text x="20" y="25" fill="#6b7280" font-size="13" text-decoration="line-through">MEMORY.md</text>
<text x="230" y="25" fill="#9ca3af" font-size="10">隐私保护</text>
</g>
<g transform="translate(560, 264)">
<rect width="280" height="40" rx="8" fill="rgba(234,88,12,0.15)"/>
<text x="20" y="25" fill="#fed7aa" font-size="13">memory/今日日志</text>
<text x="230" y="25" fill="#fdba74" font-size="10">当前会话</text>
</g>
<g transform="translate(560, 312)">
<rect width="280" height="40" rx="8" fill="rgba(127,29,26,0.4)"/>
<text x="20" y="25" fill="#6b7280" font-size="13" text-decoration="line-through">memory/昨日日志</text>
<text x="230" y="25" fill="#9ca3af" font-size="10">隐私保护</text>
</g>
</g>
<!-- 中间箭头 -->
<g transform="translate(400, 200)">
<rect x="-30" y="-25" width="60" height="50" rx="8" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
<text x="0" y="5" text-anchor="middle" fill="#9ca3af" font-size="11">隔离</text>
</g>
<!-- 底部说明 -->
<rect x="200" y="395" width="500" height="30" rx="6" fill="rgba(255,255,255,0.03)"/>
<text x="450" y="415" text-anchor="middle" fill="#6b7280" font-size="11">主会话:个人助手场景 - 子会话:群聊协作场景</text>
</svg>
</div>
```
这个设计解决了一个很实际的问题:**隐私保护**。
想象一下你在私聊里告诉Agent你的工作、爱好、家庭情况。然后你在群里@这个Agent结果它把你的私聊内容全都抖出来了...
这就是**上下文隔离**要解决的问题。
### 主会话 vs 子会话的差异
| 上下文类型 | 主会话(私聊) | 子会话(群聊) | 隔离原因 |
|-----------|--------------|--------------|---------|
| SOUL.md | 加载 | 加载 | Agent身份公开 |
| USER.md | 加载 | 不加载 | 用户隐私 |
| MEMORY.md | 加载 | 不加载 | 长期记忆私密 |
| 今日日志 | 加载 | 加载 | 当前对话上下文 |
| 昨日日志 | 加载 | 不加载 | 历史隐私 |
这个设计的精妙之处在于:**不是简单的全有或全无,而是根据场景精细裁剪**。
Agent在群聊里依然知道自己是谁SOUL.md知道当前在聊什么今日日志但不知道你的个人信息USER.md、MEMORY.md
### 代码实现
```typescript
// agent-scope.js 上下文加载
async loadMemory(session: Session, workspace: string): Promise<string> {
const today = formatDate(new Date());
const yesterday = formatDate(Date.now() - 86400000);
let memory = '';
// 子会话只加载今日日志
const datesToLoad = session.isMainSession()
? [today, yesterday] // 主会话:今天+昨天
: [today]; // 子会话:只有今天
for (const date of datesToLoad) {
const log = await fs.readFile(workspace + '/memory/' + date + '.md', 'utf-8');
memory += '\n## ' + date + '\n' + log + '\n';
}
// 长期记忆仅主会话加载
if (session.isMainSession()) {
const longTerm = await fs.readFile(workspace + '/MEMORY.md', 'utf-8');
memory += '\n## Long-term\n' + longTerm + '\n';
}
return memory;
}
```
`session.isMainSession()` 这一个判断就决定了Agent能"看到"多少信息。简单,但有效。
---
## 四、流式响应:让等待变成期待
Agent执行任务的时候用户最怕的是什么**不知道它在干嘛**。
传统模式是:用户发消息 → 等30秒 → 突然出现一大段回复。这30秒里用户完全不知道发生了什么只能干等。
OpenClaw用的是**流式响应**:用户发消息 → 立刻开始输出 → 一个字一个字地显示。
<!-- 流式响应对比图 -->
```html
<div style="background: #0d1117; padding: 28px; border-radius: 16px; font-family: -apple-system, sans-serif;">
<svg viewBox="0 0 860 320" style="width: 100%; height: auto;">
<defs>
<linearGradient id="waitingGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#fbbf24"/>
<stop offset="50%" style="stop-color:#f97316"/>
<stop offset="100%" style="stop-color:#ef4444"/>
</linearGradient>
<linearGradient id="streamGrad2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#22c55e"/>
<stop offset="50%" style="stop-color:#10b981"/>
<stop offset="100%" style="stop-color:#06b6d4"/>
</linearGradient>
</defs>
<!-- 标题 -->
<text x="215" y="30" text-anchor="middle" fill="#9ca3af" font-size="14" font-weight="500">传统模式</text>
<text x="645" y="30" text-anchor="middle" fill="#9ca3af" font-size="14" font-weight="500">流式模式</text>
<!-- 左侧:传统模式 -->
<g transform="translate(30, 50)">
<rect width="370" height="250" rx="12" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<!-- 用户消息 -->
<rect x="15" y="15" width="200" height="32" rx="16" fill="#1f6feb"/>
<text x="115" y="36" text-anchor="middle" fill="white" font-size="11">帮我检查服务状态</text>
<!-- 等待动画 -->
<g transform="translate(15, 60)">
<rect width="340" height="140" rx="8" fill="#21262d"/>
<text x="170" y="50" text-anchor="middle" fill="#6e7681" font-size="12">LLM 处理中...</text>
<!-- 加载条 -->
<rect x="70" y="70" width="200" height="8" rx="4" fill="#30363d"/>
<rect x="70" y="70" width="0" height="8" rx="4" fill="url(#waitingGrad)">
<animate attributeName="width" values="0;200;0" dur="2s" repeatCount="indefinite"/>
</rect>
<text x="170" y="110" text-anchor="middle" fill="#8b949e" font-size="28" font-weight="600">
<tspan>30</tspan>
<tspan fill="#6e7681" font-size="14"> 秒等待</tspan>
</text>
<text x="170" y="135" text-anchor="middle" fill="#6e7681" font-size="10">用户不知道发生了什么</text>
</g>
<!-- 最终响应 -->
<rect x="15" y="210" width="340" height="28" rx="8" fill="#238636"/>
<text x="185" y="229" text-anchor="middle" fill="white" font-size="11">服务运行正常已运行2天</text>
</g>
<!-- 中间分隔线 -->
<line x1="430" y1="70" x2="430" y2="280" stroke="#30363d" stroke-width="1" stroke-dasharray="4,4"/>
<text x="430" y="165" text-anchor="middle" fill="#6e7681" font-size="10">VS</text>
<!-- 右侧:流式模式 -->
<g transform="translate(460, 50)">
<rect width="370" height="250" rx="12" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<!-- 用户消息 -->
<rect x="15" y="15" width="200" height="32" rx="16" fill="#1f6feb"/>
<text x="115" y="36" text-anchor="middle" fill="white" font-size="11">帮我检查服务状态</text>
<!-- 流式输出 -->
<g transform="translate(15, 60)">
<rect width="340" height="140" rx="8" fill="#0d1117"/>
<!-- 逐字显示效果 -->
<text x="15" y="25" fill="#58a6ff" font-size="11"></text>
<text x="30" y="25" fill="#58a6ff" font-size="11"></text>
<text x="45" y="25" fill="#58a6ff" font-size="11"></text>
<text x="60" y="25" fill="#58a6ff" font-size="11"></text>
<text x="75" y="25" fill="#8b949e" font-size="11">...</text>
<rect x="15" y="35" width="4" height="14" fill="#58a6ff">
<animate attributeName="opacity" values="1;0;1" dur="0.8s" repeatCount="indefinite"/>
</rect>
<text x="15" y="70" fill="#7ee787" font-size="11">服务状态:运行中</text>
<text x="15" y="90" fill="#7ee787" font-size="11">运行时间2天3小时</text>
<text x="15" y="110" fill="#7ee787" font-size="11">内存占用245MB</text>
<text x="15" y="130" fill="#8b949e" font-size="11">正在生成总结</text>
<rect x="110" y="120" width="4" height="14" fill="#8b949e">
<animate attributeName="opacity" values="1;0;1" dur="0.8s" repeatCount="indefinite"/>
</rect>
</g>
<!-- 完成状态 -->
<rect x="15" y="210" width="340" height="28" rx="8" fill="#238636"/>
<text x="185" y="229" text-anchor="middle" fill="white" font-size="11">实时反馈 - 用户全程可见</text>
</g>
<!-- 底部对比 -->
<rect x="50" y="308" width="180" height="24" rx="4" fill="rgba(239,68,68,0.15)"/>
<text x="140" y="324" text-anchor="middle" fill="#f87171" font-size="10">用户焦虑 - 体验差 - 易流失</text>
<rect x="630" y="308" width="180" height="24" rx="4" fill="rgba(34,197,94,0.15)"/>
<text x="720" y="324" text-anchor="middle" fill="#4ade80" font-size="10">用户安心 - 体验好 - 留存高</text>
</svg>
</div>
```
这不是简单的UX优化而是改变了用户和Agent的**交互心智模型**。
### 传统模式的问题
```
用户发消息 → ──────────── 等待30秒 ──────────── → 收到回复
用户焦虑区
"它还在运行吗?"
"是不是卡住了?"
"要不要刷新?"
```
30秒的沉默对用户来说是巨大的心理压力。我见过太多用户在等待过程中直接关闭页面。
### 流式模式的优势
```
用户发消息 → "正在" → "检查" → "服务" → "状态" → "..." → 完成
↑ ↑ ↑ ↑
0.5秒 1秒 1.5秒 2秒
└─────────────────────────────┘
用户全程可见进度
```
用户立刻知道Agent开始工作了不用猜测不用焦虑。
### 流式响应的代码实现
```typescript
// 处理LLM的流式响应
async processResponse(
response: AsyncIterator<MessageChunk>,
session: Session
): Promise<ProcessResult> {
for await (const chunk of response) {
if (chunk.type === 'content_block_delta') {
if (chunk.delta.type === 'text_delta') {
// 实时推送每个字给用户
await session.send({
type: 'text_delta',
text: chunk.delta.text
});
}
}
}
}
```
核心就是对LLM返回的流做**实时转发**——收到什么就推什么,不做缓冲。
这个设计的代价是:实现复杂度更高,需要处理好断连、重连、错误恢复。但用户体验的提升是值得的。
---
## 五、核心文件架构:模块化设计
OpenClaw的Agent相关代码组织得非常清晰每个文件职责单一
<!-- 核心文件架构图 -->
```html
<div style="background: linear-gradient(180deg, #0c0c14 0%, #13131f 100%); padding: 32px; border-radius: 16px; font-family: 'SF Mono', 'Fira Code', monospace;">
<div style="text-align: center; margin-bottom: 24px;">
<span style="color: #818cf8; font-size: 16px; font-weight: 600;">Core File Architecture - 核心文件架构</span>
</div>
<svg viewBox="0 0 900 480" style="width: 100%; height: auto;">
<defs>
<linearGradient id="coreGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#4f46e5"/>
</linearGradient>
<linearGradient id="toolGrad2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f472b6"/>
<stop offset="100%" style="stop-color:#ec4899"/>
</linearGradient>
<linearGradient id="configGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#fbbf24"/>
<stop offset="100%" style="stop-color:#f59e0b"/>
</linearGradient>
<linearGradient id="contextGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#34d399"/>
<stop offset="100%" style="stop-color:#10b981"/>
</linearGradient>
<filter id="nodeShadow">
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000" flood-opacity="0.4"/>
</filter>
<marker id="flowArrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#6b7280"/>
</marker>
</defs>
<!-- Agent核心模块 -->
<g transform="translate(50, 40)">
<rect width="200" height="200" rx="12" fill="rgba(99,102,241,0.1)" stroke="#6366f1" stroke-width="2"/>
<rect width="200" height="36" rx="12" fill="url(#coreGrad)" filter="url(#nodeShadow)"/>
<text x="100" y="24" text-anchor="middle" fill="white" font-size="13" font-weight="600">Agent Core</text>
<g transform="translate(15, 50)">
<rect width="170" height="32" rx="6" fill="rgba(99,102,241,0.2)"/>
<text x="85" y="21" text-anchor="middle" fill="#a5b4fc" font-size="10">steerable-agent-loop.js</text>
</g>
<text x="100" y="95" text-anchor="middle" fill="#6366f1" font-size="9">主循环控制 - Turn限制</text>
<g transform="translate(15, 108)">
<rect width="170" height="32" rx="6" fill="rgba(99,102,241,0.2)"/>
<text x="85" y="21" text-anchor="middle" fill="#a5b4fc" font-size="10">agent-scope.js</text>
</g>
<text x="100" y="153" text-anchor="middle" fill="#6366f1" font-size="9">上下文管理 - 记忆加载</text>
<g transform="translate(15, 166)">
<rect width="170" height="24" rx="6" fill="rgba(99,102,241,0.15)"/>
<text x="85" y="17" text-anchor="middle" fill="#818cf8" font-size="9">agent-tools.js - agent-prompt.js</text>
</g>
</g>
<!-- 工具实现模块 -->
<g transform="translate(280, 40)">
<rect width="180" height="200" rx="12" fill="rgba(244,114,182,0.1)" stroke="#f472b6" stroke-width="2"/>
<rect width="180" height="36" rx="12" fill="url(#toolGrad2)" filter="url(#nodeShadow)"/>
<text x="90" y="24" text-anchor="middle" fill="white" font-size="13" font-weight="600">Tools</text>
<g transform="translate(15, 50)">
<rect width="150" height="26" rx="5" fill="rgba(244,114,182,0.2)"/>
<text x="75" y="18" text-anchor="middle" fill="#f9a8d4" font-size="10">bash.js</text>
</g>
<text x="90" y="88" text-anchor="middle" fill="#f472b6" font-size="8">命令执行</text>
<g transform="translate(15, 98)">
<rect width="150" height="26" rx="5" fill="rgba(244,114,182,0.2)"/>
<text x="75" y="18" text-anchor="middle" fill="#f9a8d4" font-size="10">file-ops.js</text>
</g>
<text x="90" y="136" text-anchor="middle" fill="#f472b6" font-size="8">文件操作</text>
<g transform="translate(15, 146)">
<rect width="150" height="26" rx="5" fill="rgba(244,114,182,0.2)"/>
<text x="75" y="18" text-anchor="middle" fill="#f9a8d4" font-size="10">skill-invoke.js</text>
</g>
<text x="90" y="184" text-anchor="middle" fill="#f472b6" font-size="8">Skill调用</text>
</g>
<!-- 配置模块 -->
<g transform="translate(490, 40)">
<rect width="160" height="200" rx="12" fill="rgba(251,191,36,0.1)" stroke="#fbbf24" stroke-width="2"/>
<rect width="160" height="36" rx="12" fill="url(#configGrad)" filter="url(#nodeShadow)"/>
<text x="80" y="24" text-anchor="middle" fill="white" font-size="13" font-weight="600">Config</text>
<g transform="translate(15, 50)">
<rect width="130" height="26" rx="5" fill="rgba(251,191,36,0.2)"/>
<text x="65" y="18" text-anchor="middle" fill="#fde68a" font-size="10">auth-profiles.json</text>
</g>
<g transform="translate(15, 86)">
<rect width="130" height="26" rx="5" fill="rgba(251,191,36,0.2)"/>
<text x="65" y="18" text-anchor="middle" fill="#fde68a" font-size="10">models.json</text>
</g>
<g transform="translate(15, 122)">
<rect width="130" height="26" rx="5" fill="rgba(251,191,36,0.2)"/>
<text x="65" y="18" text-anchor="middle" fill="#fde68a" font-size="10">tools.json</text>
</g>
<text x="80" y="170" text-anchor="middle" fill="#fbbf24" font-size="9">配置热更新</text>
<text x="80" y="185" text-anchor="middle" fill="#fbbf24" font-size="9">运行时可切换</text>
</g>
<!-- 上下文数据 -->
<g transform="translate(680, 40)">
<rect width="170" height="200" rx="12" fill="rgba(52,211,153,0.1)" stroke="#34d399" stroke-width="2"/>
<rect width="170" height="36" rx="12" fill="url(#contextGrad)" filter="url(#nodeShadow)"/>
<text x="85" y="24" text-anchor="middle" fill="white" font-size="13" font-weight="600">Context</text>
<g transform="translate(15, 50)">
<rect width="140" height="24" rx="5" fill="rgba(52,211,153,0.2)"/>
<text x="70" y="17" text-anchor="middle" fill="#6ee7b7" font-size="10">SOUL.md</text>
</g>
<g transform="translate(15, 82)">
<rect width="140" height="24" rx="5" fill="rgba(52,211,153,0.2)"/>
<text x="70" y="17" text-anchor="middle" fill="#6ee7b7" font-size="10">USER.md</text>
</g>
<g transform="translate(15, 114)">
<rect width="140" height="24" rx="5" fill="rgba(52,211,153,0.2)"/>
<text x="70" y="17" text-anchor="middle" fill="#6ee7b7" font-size="10">MEMORY.md</text>
</g>
<g transform="translate(15, 146)">
<rect width="140" height="24" rx="5" fill="rgba(52,211,153,0.2)"/>
<text x="70" y="17" text-anchor="middle" fill="#6ee7b7" font-size="10">memory/*.md</text>
</g>
<text x="85" y="190" text-anchor="middle" fill="#34d399" font-size="9">人类可读 - Git友好</text>
</g>
<!-- 数据流箭头 -->
<path d="M 250 140 Q 265 140 280 140" stroke="#6b7280" stroke-width="1.5" fill="none" marker-end="url(#flowArrow)"/>
<path d="M 250 100 Q 380 50 490 100" stroke="#6b7280" stroke-width="1.5" fill="none" marker-end="url(#flowArrow)"/>
<path d="M 250 180 Q 450 250 680 180" stroke="#6b7280" stroke-width="1.5" fill="none" marker-end="url(#flowArrow)"/>
<!-- 底部调用流程 -->
<rect x="50" y="280" width="800" height="180" rx="12" fill="rgba(255,255,255,0.02)" stroke="rgba(255,255,255,0.05)"/>
<text x="450" y="305" text-anchor="middle" fill="#6b7280" font-size="12">Agent Loop 执行流程</text>
<g transform="translate(80, 330)">
<rect width="120" height="50" rx="8" fill="#4f46e5"/>
<text x="60" y="30" text-anchor="middle" fill="white" font-size="10">1. loadContext()</text>
<text x="60" y="44" text-anchor="middle" fill="#a5b4fc" font-size="8">加载SOUL/USER</text>
</g>
<line x1="200" y1="355" x2="230" y2="355" stroke="#6b7280" stroke-width="1.5" marker-end="url(#flowArrow)"/>
<g transform="translate(240, 330)">
<rect width="120" height="50" rx="8" fill="#4f46e5"/>
<text x="60" y="30" text-anchor="middle" fill="white" font-size="10">2. callLLM()</text>
<text x="60" y="44" text-anchor="middle" fill="#a5b4fc" font-size="8">流式调用API</text>
</g>
<line x1="360" y1="355" x2="390" y2="355" stroke="#6b7280" stroke-width="1.5" marker-end="url(#flowArrow)"/>
<g transform="translate(400, 330)">
<rect width="120" height="50" rx="8" fill="#4f46e5"/>
<text x="60" y="30" text-anchor="middle" fill="white" font-size="10">3. processResponse()</text>
<text x="60" y="44" text-anchor="middle" fill="#a5b4fc" font-size="8">处理返回内容</text>
</g>
<line x1="520" y1="355" x2="550" y2="355" stroke="#6b7280" stroke-width="1.5" marker-end="url(#flowArrow)"/>
<g transform="translate(560, 330)">
<rect width="120" height="50" rx="8" fill="#4f46e5"/>
<text x="60" y="30" text-anchor="middle" fill="white" font-size="10">4. executeTool()</text>
<text x="60" y="44" text-anchor="middle" fill="#a5b4fc" font-size="8">如果有工具调用</text>
</g>
<line x1="680" y1="355" x2="710" y2="355" stroke="#6b7280" stroke-width="1.5" marker-end="url(#flowArrow)"/>
<g transform="translate(720, 330)">
<rect width="100" height="50" rx="8" fill="#10b981"/>
<text x="50" y="30" text-anchor="middle" fill="white" font-size="10">5. writeMemory()</text>
<text x="50" y="44" text-anchor="middle" fill="#6ee7b7" font-size="8">写入记忆</text>
</g>
<path d="M 660 380 Q 660 420 450 420 Q 240 420 240 380" stroke="#f472b6" stroke-width="1.5" fill="none" stroke-dasharray="4,2" marker-end="url(#flowArrow)"/>
<text x="450" y="440" text-anchor="middle" fill="#f472b6" font-size="9">工具调用后继续循环 (最多10轮)</text>
</svg>
</div>
```
这个架构的亮点:
1. **模块职责单一**:每个文件只做一件事,改一个不影响其他的
2. **依赖方向清晰**Core → Tools/Config/Context单向依赖
3. **可测试性强**:每个模块可以独立测试
去年那篇文章里我说过"商业级长期迭代的项目一定要定制化智能体架构"OpenClaw的这个设计就是很好的参考——不是最复杂的但足够清晰。
---
## 六、错误处理与恢复:优雅降级
Agent执行过程中最怕的就是工具调用失败。一个工具报错整个任务就崩了
OpenClaw的处理方式是**把错误也当作一种结果让Agent自己决定怎么办**。
### 三种错误类型
| 错误类型 | 触发条件 | 处理方式 |
|---------|---------|---------|
| 超时 | 命令执行超过30秒 | 返回超时错误Agent可重试 |
| 输出过大 | 输出超过1MB | 自动截断Agent收到截断后的内容 |
| 执行异常 | 权限不足、网络错误等 | 返回异常详情Agent可换方案 |
关键点:**错误不是终结,而是信息**。
Agent收到错误后可以
- 重试(比如网络抖动)
- 换方案比如权限不够换sudo
- 放弃并告诉用户(比如文件不存在)
### 代码实现
```typescript
// agent-tools.js 工具执行
async executeTool(toolUse: ToolUse): Promise<ToolResult> {
const tool = this.tools.get(toolUse.name);
if (!tool) {
return {
type: 'tool_result',
tool_use_id: toolUse.id,
content: 'Error: Tool not found: ' + toolUse.name,
is_error: true
};
}
try {
const result = await tool.run(toolUse.input);
return {
type: 'tool_result',
tool_use_id: toolUse.id,
content: JSON.stringify(result),
is_error: false
};
} catch (error) {
// 错误也正常返回让Agent决定怎么办
return {
type: 'tool_result',
tool_use_id: toolUse.id,
content: 'Error: ' + error.message,
is_error: true
};
}
}
```
这个设计的精髓在于:**从不throw error到上层**而是把所有结果包括错误都封装成tool_result让LLM自己判断下一步怎么办。
这就是"可控执行循环"的核心——**Agent永远不会因为一个工具报错而崩溃**。
---
## 七、对比与启示
研究完OpenClaw的Agent Loop我对比了一下几个主流框架的设计
| 特性 | LangChain | AutoGPT | OpenClaw |
|-----|-----------|---------|----------|
| 循环控制 | 手动管理 | 容易失控 | maxTurns硬限制 |
| 上下文管理 | Vector DB | 全量加载 | 按需+隔离 |
| 流式响应 | 支持 | 不支持 | 默认开启 |
| 错误处理 | 抛异常 | 容易崩溃 | 优雅降级 |
| 工具调用 | 链式定义 | 递归嵌套 | Turn-based循环 |
| 配置热更新 | 需重启 | 需重启 | 支持 |
OpenClaw不是最强大的但它是最**可预测**的。
### 设计哲学的对比
去年那篇文章里我提到过几个框架的设计哲学:
- **AutoGPT系**:单智能体,大而全,适合演示但生产环境容易失控
- **LangChain系**:模块化设计,生态丰富,但学习曲线陡峭
- **LangGraph系**:图结构设计很优雅,多智能体协作机制不错
OpenClaw走的是另一条路**简单到极致**。
它的设计哲学可以概括为:
1. **能用数据结构解决的问题,不用代码解决**(上下文隔离)
2. **能用配置解决的问题,不写代码解决**(热更新)
3. **能消灭的特殊情况不要用if/else处理**Turn-based模型
这让我想起Linus Torvalds说过的一句话"好的设计通过数据结构消除特殊情况而非通过if/else补丁。"
---
## 写在最后
Agent执行循环表面上看是技术问题实际上是**设计哲学**问题。
你可以让它很复杂——各种中间件、事件系统、状态机。但OpenClaw告诉我们**简单的设计反而更可靠**。
- 一个while循环解决工具调用链
- 一个maxTurns解决无限循环
- 一个isMainSession解决上下文隔离
- 一个try-catch解决错误崩溃
大道至简。
这也是我在做AtomStorm时越来越认同的理念**好的架构不是堆砌功能,而是用最少的代码解决最多的问题**。
去年聊Message Flow今年聊Execution Loop。下一篇准备聊聊**记忆系统**——Agent的"长期记忆"到底该怎么设计,才能既记住重要的事,又不会越来越慢。
如果你对Agent架构、Context Engineering感兴趣欢迎来试试AtomStorm**studio.atomstorm.ai**),目前开放内测,私信我或者加群获取。
***
**我是栗子KK还在路上的AI产品Founder。**
山远路险,鞋里有沙。
---
**往期推荐:**
- [我研究了OpenClaw的8个反常识设计终于明白这个Agent为什么能火爆全球](#)
- [做了两年AI Agent我发现99%的AI Agent项目都死在了Message Flow设计上](#)
- [全球首个Skills Vibe AgentsAtomStorm技术揭秘我是怎么用Context Engineering让Agent不"变傻"的](#)
- [ClaudeCode工程师亲述为什么你的AI Agent总是"智障"?问题可能出在工具设计上](#)
- [精准爆破拆解Claude Skills完整技术架构](#)