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

51 KiB
Raw Blame History

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
wechat
x
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
id category severity description status
S1 structure medium OpenClaw缺少背景介绍 open
id category severity description status
F1 factual medium 95%统计无数据来源 open
id category severity description status
F2 factual medium 对比表评价过于绝对化 open
id category severity description status
FM1 format high 错误处理图SVG HTML属性语法错误 open

从无限循环到可控执行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>

这个架构的亮点:

  1. 模块职责单一:每个文件只做一件事,改一个不影响其他的
  2. 依赖方向清晰Core → Tools/Config/Context单向依赖
  3. 可测试性强:每个模块可以独立测试

去年那篇文章里我说过"商业级长期迭代的项目一定要定制化智能体架构"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走的是另一条路简单到极致

它的设计哲学可以概括为:

  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感兴趣欢迎来试试AtomStormstudio.atomstorm.ai),目前开放内测,私信我或者加群获取。


我是栗子KK还在路上的AI产品Founder。

山远路险,鞋里有沙。


往期推荐: