51 KiB
| id | title | slug | status | content_type | channels | language | source_urls | assets | cover_image | template | owner | created_at | updated_at | published_at | review_status | review_passed_at | review_issues | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-03-11-openclaw-agent-loop | 从无限循环到可控执行:OpenClaw的Agent Loop是怎么做到的 | openclaw-agent-loop | polish | article |
|
zh-CN | article | content-forge | 2026-03-11T00:00:00+08:00 | 2026-03-11T00:00:00+08:00 | passed | 2026-03-11T01:13:56+08:00 |
|
从无限循环到可控执行:OpenClaw的Agent Loop是怎么做到的
去年写了篇《做了两年AI Agent,我发现99%的AI Agent项目都死在了Message Flow设计上》,聊了消息结构、Prompt构造、上下文管理这些"静态"设计。
当时有读者问:消息流设计好了,那Agent拿到消息之后到底怎么执行?
说实话,那篇文章故意跳过了这个话题——因为执行循环(Execution Loop)是另一块硬骨头,值得单独写。
这块看起来简单:不就是LLM调用 → 拿结果 → 返回嘛?
实际上坑很多。工具调用多少轮才够?中间状态怎么管?出错了怎么办?
OpenClaw火了之后,我把它的Agent Loop源码翻了一遍,发现它的设计确实有些巧思——不是那种"天才构想",而是工程上的"刚刚好"。
今天就延续去年的话题,聊聊Agent执行循环这个"动态"层面的问题。
一、Agent生命周期与执行流程
先看一张完整的流程图,这是OpenClaw Agent执行每一个任务的完整生命周期:
<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,而是加载上下文。
// 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自己规划要调用哪些工具、调用顺序是什么。
看一个实际的例子:
<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是怎么实现这个循环的:
// 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用了一个很聪明的设计:主会话和子会话加载不同的上下文。
<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)。
代码实现
// 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用的是流式响应:用户发消息 → 立刻开始输出 → 一个字一个字地显示。
<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开始工作了,不用猜测,不用焦虑。
流式响应的代码实现
// 处理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相关代码组织得非常清晰,每个文件职责单一:
<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>
这个架构的亮点:
- 模块职责单一:每个文件只做一件事,改一个不影响其他的
- 依赖方向清晰:Core → Tools/Config/Context,单向依赖
- 可测试性强:每个模块可以独立测试
去年那篇文章里我说过"商业级长期迭代的项目一定要定制化智能体架构",OpenClaw的这个设计就是很好的参考——不是最复杂的,但足够清晰。
六、错误处理与恢复:优雅降级
Agent执行过程中,最怕的就是工具调用失败。一个工具报错,整个任务就崩了?
OpenClaw的处理方式是:把错误也当作一种结果,让Agent自己决定怎么办。
三种错误类型
| 错误类型 | 触发条件 | 处理方式 |
|---|---|---|
| 超时 | 命令执行超过30秒 | 返回超时错误,Agent可重试 |
| 输出过大 | 输出超过1MB | 自动截断,Agent收到截断后的内容 |
| 执行异常 | 权限不足、网络错误等 | 返回异常详情,Agent可换方案 |
关键点:错误不是终结,而是信息。
Agent收到错误后,可以:
- 重试(比如网络抖动)
- 换方案(比如权限不够换sudo)
- 放弃并告诉用户(比如文件不存在)
代码实现
// 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走的是另一条路:简单到极致。
它的设计哲学可以概括为:
- 能用数据结构解决的问题,不用代码解决(上下文隔离)
- 能用配置解决的问题,不写代码解决(热更新)
- 能消灭的特殊情况,不要用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。
山远路险,鞋里有沙。
往期推荐: