一文看懂OpenClaw Agent架构

3 阅读5分钟

本文基于 OpenClaw 开源项目的源码,深入剖析 AI Agent 的完整工作流程。

前言

当你在对话框输入“帮我搜索 OpenClaw 的架构文档并总结”,这行文字经历了怎样的过程?

本文将从源码角度,完整剖析一个 AI Agent 如何接收消息、调度工具、调用大模型,最终返回结果。


一、整体架构总览

先来看一张全局图:

┌─────────────────────────────────────────────────────────────────┐
│                        用户界面层                                │
│                    (Feishu / Web / 终端)                        │
└────────────────────────────┬────────────────────────────────────┘
                             │ 消息入口
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Gateway 网关层                              │
│           dispatchInboundMessage → chat.send handler            │
└────────────────────────────┬────────────────────────────────────┘
                             │ Session 加载/保存
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Agent 核心层                                │
│     runEmbeddedPiAgent → runEmbeddedAttempt → session.prompt     │
└────────────────────────────┬────────────────────────────────────┘
                             │ LLM 调用 + 工具执行
          ┌──────────────────┼──────────────────┐
          ▼                  ▼                  ▼
┌─────────────────┐  ┌──────────────┐  ┌─────────────────┐
│   工具系统      │  │  LLM 处理    │  │  流式响应       │
│   pi-tools      │  │  思考循环    │  │  subscribe      │
└─────────────────┘  └──────────────┘  └─────────────────┘

二、消息入口:Gateway 层

2.1 消息接收流程

所有消息都经过同一个入口:

// src/gateway/server-methods/chat.ts
export const chatHandlers: GatewayRequestHandlers = {
  "chat.send": async ({ params, respond, context }) => {
    // 1. 验证参数
    // 2. 加载或创建 Session
    // 3. 保存用户消息
    // 4. 调用 runEmbeddedPiAgent() 触发 Agent
    // 5. 流式返回响应
  }
}

2.2 关键代码片段

// 加载 Session
const { sessionKey, sessionFile } = await loadSessionEntry(params, context);

// 追加用户消息到历史
await appendMessage({ sessionKey, sessionFile, message: params.message });

// 触发 Agent 运行
const result = await runEmbeddedPiAgent({
  sessionKey,
  sessionFile,
  prompt: params.message.content.text,
  // ... 其他参数
});

// 流式返回
for await (const chunk of result.stream) {
  await respond({ data: chunk });
}

三、Agent 核心:消息如何变成思考

3.1 入口函数

// src/agents/pi-embedded-runner/run.ts
export async function runEmbeddedPiAgent(params: RunEmbeddedPiAgentParams) {
  // 1. 解析 workspace 和 agent 配置
  // 2. 构建系统提示词 (System Prompt)
  // 3. 创建 Agent Session
  // 4. 调用 runEmbeddedAttempt() 执行 LLM 调用
  // 5. 返回流式响应
}

3.2 系统提示词工程

System Prompt 是 Agent 的"灵魂",它决定了 Agent 的行为模式:

// src/agents/system-prompt.ts
const lines = [
  "You are a personal assistant running inside OpenClaw.",
  "",
  "## Tooling",
  "Tool availability (filtered by policy):",
  "- web_search: Search the web",
  "- web_fetch: Fetch and extract content from URL",
  "- read: Read file contents",
  "- write: Create or overwrite files",
  // ... 更多工具
  "",
  "## Tool Call Style",
  "Default: do not narrate routine, low-risk tool calls (just call the tool).",
  "Narrate only when it helps: multi-step work, complex problems...",
  "",
  "## Safety",
  "Prioritize safety and human oversight over completion...",
];

3.3 思考标签机制

Agent 使用特殊的 XML 标签来区分"内部思考"和"用户可见输出":

<think>
用户问的是架构文档,我需要先搜索相关内容。

根据搜索结果,OpenClaw 的架构主要包括...

让我组织一下回答的结构。
</think>

<final>
根据搜索结果,OpenClaw 的 Agent 架构主要包含以下几个核心模块...
</final>

代码层面是这样处理的:

// src/agents/pi-embedded-subscribe.ts
const THINKING_TAG_SCAN_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought)\s*>/gi;
const FINAL_TAG_SCAN_RE = /<\s*(\/?)\s*final\s*>/gi;

const stripBlockTags = (text) => {
  // 1. 剥离 <think> 块,保留内部内容用于思考流展示
  // 2. 识别 <final> 块,只返回最终输出给用户
  // 3. 代码块内的标签不会被误处理
};

四、工具系统:Agent 的能力边界

4.1 工具注册

// src/agents/pi-tools.ts
export function createOpenClawCodingTools() {
  return [
    ...baseTools,      // read, write, edit
    ...execTools,      // exec, process
    ...openClawTools,  // message, cron
    ...channelTools,   // 渠道特定工具
    ...pluginTools,    // 插件工具
  ];
}

4.2 工具调用生命周期

当 LLM 决定调用工具时,整个过程如下:

阶段事件说明
开始tool_execution_start记录开始时间,发送摘要
执行工具逻辑运行如调用搜索 API
结束tool_execution_end清理结果,返回给 LLM
// src/agents/pi-embedded-subscribe.handlers.tools.ts
export async function handleToolExecutionStart(ctx, evt) {
  const { toolName, toolCallId, args } = evt;
  
  // 记录工具调用
  ctx.state.toolMetaById.set(toolCallId, {
    name: toolName,
    args,
    startTime: Date.now()
  });
  
  // 发送工具事件
  emitAgentEvent({ stream: "tool", data: { phase: "start", ... }});
}

export async function handleToolExecutionEnd(ctx, evt) {
  const { toolName, toolCallId, result } = evt;
  
  // 清理结果
  const sanitized = sanitizeToolResult(result);
  
  // 写回 Session
  ctx.state.toolMetas.push({ toolName, result: sanitized });
}

4.3 Web Search 执行示例

sequenceDiagram
    participant LLM
    participant Tool
    participant Runtime
    participant API

    LLM->>Tool: toolUse {web_search, args: {query: "..."}}
    Tool->>Runtime: resolveWebSearchDefinition()
    Runtime->>API: POST /search
    API-->>Runtime: JSON 结果
    Runtime-->>Tool: 搜索结果
    Tool-->>LLM: tool_result

五、Session 管理:记忆的持久化

5.1 为什么用文件系统而非数据库?

考量维度文件系统方案数据库方案
简单性无需额外依赖需要安装配置
追加友好JSONL 天生适合需要特殊配置
可调试直接查看文件需客户端查询
可移植复制文件即可需要导出导入

核心洞察:消息系统的特点是写多读少、顺序追加,JSONL 的追加模式完美契合这个场景。

5.2 存储结构

~/.openclaw/
└── agents/
    └── main/
        ├── sessions/
        │   ├── sessions.json     # Session 索引
        │   ├── sess-abc123.jsonl # 具体消息历史
        │   └── sess-def456.jsonl
        └── sessions.json         # 主索引文件

5.3 消息文件内容示例

{"type":"session","version":3,"id":"sess-abc123","timestamp":"2025-03-23T10:00:00.000Z"}
{"type":"message","id":"msg-001","role":"user","content":[{"type":"text","text":"帮我搜索架构文档"}]}
{"type":"message","id":"msg-002","role":"assistant","content":[{"type":"toolUse","name":"web_search","input":{"query":"OpenClaw architecture"}}]}
{"type":"message","id":"msg-003","role":"assistant","content":[{"type":"text","text":"根据搜索结果..."}]}

5.4 数据量限制

配置项默认值
单文件大小10 MB(自动轮转)
Session 数量500 个(自动清理)
保留期限30 天未使用
磁盘预算可配置上限

六、完整请求流程

用时序图来看整个过程:

sequenceDiagram
    participant User as 👤 用户
    participant Gateway as 网关
    participant Session as Session
    participant Agent as Agent
    participant LLM as 大模型
    participant Tool as 工具
    participant UI as 界面

    User->>Gateway: "帮我搜索架构文档"
    Gateway->>Session: 加载历史
    Session-->>Gateway: 历史消息
    Gateway->>Agent: 触发运行
    Agent->>Agent: 构建 System Prompt
    Agent->>LLM: 发送请求
    
    rect rgb(200, 230, 200)
        Note over LLM: 第一轮:决策
        LLM->>Tool: 调用 web_search
        Tool->>Tool: 执行搜索
        Tool-->>LLM: 返回结果
    end
    
    rect rgb(200, 220, 250)
        Note over LLM: 第二轮:总结
        LLM-->>Agent: 最终回复
    end
    
    Agent->>Session: 保存历史
    Agent-->>UI: 流式输出
    UI-->>User: 实时显示

七、设计哲学与思考本质

7.1 Session 优先:长期记忆的代价

选择持久化而非每次清零:

  • 优势:记得三个月前的对话,多轮交互连贯
  • 代价:需要上下文压缩(Compaction)防止 token 溢出

7.2 工具即能力:安全的边界

每个功能封装为工具,LLM 像搭积木一样组合使用:

搜索 → 读取文档 → 编辑文件 → 发送消息

比直接操作外部世界更安全、更可控。

7.3 流式即体验:透明建立信任

用户实时看到思考过程,而非等待漫长加载——这不仅是体验,更是信任。

7.4 思考的本质

阶段做了什么
路由消息从指尖到网关
加载恢复历史上下文
构建精心设计的提示词
推理模型决定调用工具
执行搜索 API 调用
整合基于结果生成回答
输出流式响应呈现

八、深入探索

如果你想进一步研究源码:

模块路径
Agent 核心src/agents/pi-embedded-runner/
流式处理src/agents/pi-embedded-subscribe.ts
Session 管理src/config/sessions/
消息入口src/gateway/server-methods/chat.ts

结语

AI Agent 的架构,本质上是一套精密的消息处理管道:输入是用户的文字,输出是智能的响应,中间穿插着工具调用、模型推理、记忆管理。

希望这篇文章能帮助你更好地理解 AI Agent 的工作原理。如果你有任何问题,欢迎在评论区交流。


本文涉及的源码文件:

  • src/agents/pi-embedded-runner/run.ts - Agent 主入口
  • src/agents/pi-embedded-runner/run/attempt.ts - 核心运行循环
  • src/agents/pi-embedded-subscribe.ts - 流式响应处理
  • src/agents/system-prompt.ts - 系统提示词构建
  • src/agents/pi-tools.ts - 工具系统
  • src/config/sessions/ - Session 持久化