1133 lines
51 KiB
Markdown
1133 lines
51 KiB
Markdown
---
|
||
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 Agents,AtomStorm技术揭秘:我是怎么用Context Engineering让Agent不"变傻"的](#)
|
||
- [ClaudeCode工程师亲述:为什么你的AI Agent总是"智障"?问题可能出在工具设计上](#)
|
||
- [精准爆破,拆解Claude Skills完整技术架构](#)
|