🦞 OpenClaw (原ClawdBot) 开源项目深度分析
本文档是对 OpenClaw (原 ClawdBot) 项目的全面技术分析,旨在帮助开发者深入理解其实现原理。
| 项目信息 | 详情 |
|---|---|
| GitHub 仓库 | https://github.com/openclaw/openclaw |
| 当前版本 | v2026.1.30 |
| 许可证 | MIT |
| 最新 Commit | 476f367cf16081acd144048ee61198e58d15df21 |
| 历史名称 | Clawdbot → Moltbot → OpenClaw |
📝 项目更名历史:
- Clawdbot (2025.11.25 - 2026.1.27) — 最初的项目名称
- Moltbot (2026.1.27 - ?) — 第一次更名,寓意"蜕变"
- OpenClaw (当前) — 最终更名为开源版本名称
目录
一、项目概述
OpenClaw 是一个功能强大的个人 AI 助手平台,它允许用户在自己的设备上运行 AI 助手,并通过多种即时通讯渠道(WhatsApp、Telegram、Discord、Slack 等)与之交互。
1.1 核心价值
| 特性 | 说明 |
|---|---|
| 本地优先 | 运行在用户自己的设备上,数据不离开本地 |
| 多渠道统一 | 一个 AI 助手,多个聊天平台 |
| 可扩展性强 | 插件化架构,易于扩展新功能 |
| 类型安全 | TypeScript + Zod 保证代码质量 |
1.2 技术栈
- 语言: TypeScript (ESM)
- 运行时: Node.js 22+ / Bun
- 包管理: pnpm
- 构建工具: TypeScript Compiler
- 测试框架: Vitest
- 代码规范: Oxlint + Oxfmt
二、技术架构全景
┌───────────────────────────────────────────────────────────────────────────┐
│ 用户交互层 │
├─────────────┬─────────────┬─────────────┬─────────────┬───────────────────┤
│ Telegram │ WhatsApp │ Discord │ Slack │ 其他渠道... │
└─────────────┴─────────────┴─────────────┴─────────────┴───────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Gateway Server (网关服务器) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ WebSocket │ │ HTTP API │ │ Cron 调度 │ │ Hooks 系统 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Agent 代理系统 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ 会话管理 │ │ 工具执行 │ │ 模型切换 │ │ 记忆/上下文 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 模型提供商 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌─────────┐ │
│ │ Anthropic │ │ OpenAI │ │ Google │ │ Ollama │ │ 其他 │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ └─────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
2.1 项目目录结构
openclaw/
├── src/ # 核心源代码
│ ├── cli/ # CLI 命令实现
│ ├── commands/ # 子命令模块
│ ├── gateway/ # 网关服务器
│ ├── agents/ # AI Agent 系统
│ ├── channels/ # 渠道抽象层
│ ├── telegram/ # Telegram 渠道
│ ├── discord/ # Discord 渠道
│ ├── slack/ # Slack 渠道
│ ├── signal/ # Signal 渠道
│ ├── imessage/ # iMessage 渠道
│ ├── plugins/ # 插件系统
│ ├── config/ # 配置管理
│ ├── infra/ # 基础设施
│ └── media/ # 媒体处理
├── extensions/ # 扩展插件
│ ├── msteams/ # Microsoft Teams
│ ├── matrix/ # Matrix 协议
│ ├── voice-call/ # 语音通话
│ └── memory-lancedb/ # LanceDB 记忆
├── skills/ # 内置 Skills (52+)
├── apps/ # 原生应用
│ ├── ios/ # iOS App
│ ├── macos/ # macOS App
│ └── android/ # Android App
├── docs/ # 文档
├── ui/ # Web UI
└── test/ # 测试文件
三、程序启动与入口链路
本章聚焦于回答一个核心问题:当用户运行 openclaw gateway run 后,发生了什么?
我们将按照代码实际执行顺序,追踪从命令行输入到 Agent 接收消息的完整链路。
3.1 启动流程总览
┌─────────────────────────────────────────────────────────────────────────────┐
│ $ openclaw gateway run --port 18789 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ① 入口层 (src/entry.ts) │
│ • 设置进程标题 process.title = "openclaw" │
│ • 环境变量规范化 normalizeEnv() │
│ • 实验性警告抑制 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ② CLI 路由 (src/cli/run-main.ts) │
│ • 加载 .env 配置 │
│ • 运行时版本检查 assertSupportedRuntime() │
│ • 构建命令程序 buildProgram() │
│ • 路由到 gateway run 子命令 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ③ Gateway 启动 (src/gateway/server.impl.ts) │
│ • 创建 HTTP Server + WebSocket Server │
│ • 初始化 NodeRegistry(设备节点管理) │
│ • 初始化 ChannelManager(消息渠道管理) │
│ • 启动 CronService(定时任务) │
│ • 绑定 WebSocket 处理器 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ④ 渠道连接 (Telegram/WhatsApp/Discord/...) │
│ • ChannelManager 启动配置的渠道 │
│ • 各渠道建立与平台的连接(Bot API / Web Socket 等) │
│ • 开始监听消息 │
└─────────────────────────────────────────────────────────────────────────────┘
│
─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ 用户发送消息
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ⑤ 消息到达 → Agent 调用 │
│ • 渠道接收消息,路由到 Gateway │
│ • Gateway 调用 runEmbeddedPiAgent() │
│ • 进入 Agent 核心循环(第四章详解) │
└─────────────────────────────────────────────────────────────────────────────┘
3.2 入口层初始化
文件: src/entry.ts
这是整个程序的第一个执行点,负责环境准备:
#!/usr/bin/env node
import { spawn } from "node:child_process";
import process from "node:process";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
import { installProcessWarningFilter } from "./infra/warnings.js";
// 设置进程名称(便于 ps/top 识别)
process.title = "openclaw";
// 过滤 Node.js 实验性功能警告
installProcessWarningFilter();
// 规范化环境变量(处理大小写、别名等)
normalizeEnv();
入口层做了什么?
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | process.title = "openclaw" | 设置进程名,便于系统监控 |
| 2 | installProcessWarningFilter() | 抑制 Node.js 实验性 API 警告 |
| 3 | normalizeEnv() | 统一环境变量格式 |
| 4 | parseCliProfileArgs() | 解析 --profile 参数 |
3.3 CLI 命令路由
文件: src/cli/run-main.ts
入口层完成后,进入 CLI 层进行命令解析和路由:
export async function runCli(argv: string[] = process.argv) {
// 1. Windows 特殊处理
const normalizedArgv = stripWindowsNodeExec(argv);
// 2. 加载 .env 文件
loadDotEnv({ quiet: true });
normalizeEnv();
// 3. 确保 openclaw 在 PATH 中
ensureOpenClawCliOnPath();
// 4. 运行时版本检查(Node.js 22+)
assertSupportedRuntime();
// 5. 尝试快速路由(某些命令不需要完整初始化)
if (await tryRouteCli(normalizedArgv)) {
return;
}
// 6. 捕获控制台输出到结构化日志
enableConsoleCapture();
// 7. 构建命令程序(延迟加载)
const { buildProgram } = await import("./program.js");
const program = buildProgram();
// 8. 解析命令并执行
await program.parseAsync(normalizedArgv);
}
设计亮点:
- 延迟加载: 子命令按需
import(),加快启动速度 - 跨平台兼容: Windows 参数处理有特殊逻辑
- 快速路由: 简单命令无需加载完整框架
当用户执行 openclaw gateway run 时,CLI 路由到 src/commands/gateway/run.ts。
3.4 Gateway Server 启动
文件: src/gateway/server.impl.ts
这是整个系统的控制中心,gateway run 命令最终会调用:
export async function startGatewayServer(
port = 18789,
opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
// 1. 创建核心运行时状态
const {
canvasHost,
httpServer,
wss, // WebSocket Server
clients, // 连接的客户端
broadcast, // 广播函数
} = await createGatewayRuntimeState({
port,
hostname: opts.hostname,
...
});
// 2. 节点注册表(管理连接的设备:手机、其他网关)
const nodeRegistry = new NodeRegistry();
// 3. Cron 服务(定时任务调度)
let cronState = buildGatewayCronService({
config: cfg,
onTrigger: (job) => handleCronTrigger(job),
...
});
// 4. 渠道管理器(管理所有消息渠道)
const channelManager = createChannelManager({
config: cfg,
onMessage: (msg) => routeMessageToAgent(msg),
...
});
// 5. 绑定 WebSocket 处理器
attachGatewayWsHandlers({
wss,
clients,
nodeRegistry,
channelManager,
...
});
// 6. 启动渠道连接
await channelManager.startAll();
return { httpServer, wss, channelManager, ... };
}
Gateway 核心组件:
| 组件 | 文件 | 职责 |
|---|---|---|
NodeRegistry | src/gateway/node-registry.ts | 管理连接的设备节点 |
ChannelManager | src/channels/manager.ts | 启动/停止/管理消息渠道 |
CronService | src/gateway/cron.ts | 定时任务调度 |
WebSocket Server | src/gateway/ws-handlers.ts | 实时双向通信 |
ExecApprovalManager | src/gateway/exec-approval.ts | 命令执行审批 |
3.5 消息到达与 Agent 调用
当 Gateway 启动完成后,各渠道开始监听消息。以 Telegram 为例:
消息流转路径:
用户在 Telegram 发送消息
│
▼
┌─────────────────────────────────────────┐
│ Telegram Bot API (Grammy) │
│ src/telegram/bot.ts │
└─────────────────────────────────────────┘
│ bot.on("message", handler)
▼
┌─────────────────────────────────────────┐
│ 消息处理中间件 │
│ • 节流 (apiThrottler) │
│ • 串行化 (sequentialize) │
│ • 权限检查 (allowlist) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ ChannelManager.routeMessage() │
│ 统一消息格式,路由到 Agent │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ runEmbeddedPiAgent() │
│ src/agents/pi-embedded-runner/run.ts │
│ ────────────────────────────────────── │
│ 进入 Agent 核心循环(第四章详解) │
└─────────────────────────────────────────┘
关键代码片段(Telegram 消息处理):
// src/telegram/bot.ts
export function createTelegramBot(opts: TelegramBotOptions) {
const bot = new Bot(opts.token);
// 节流:避免触发 Telegram API 限制
bot.api.config.use(apiThrottler());
// 串行化:同一聊天的消息按顺序处理
bot.use(sequentialize(getTelegramSequentialKey));
// 消息处理
bot.on("message:text", async (ctx) => {
const message = ctx.message.text;
const chatId = ctx.chat.id;
// 路由到 Agent
await routeToAgent({
channel: "telegram",
chatId,
message,
replyFn: (text) => ctx.reply(text),
});
});
return bot;
}
渠道抽象机制:
所有渠道都实现统一的接口,使得 Agent 无需关心消息来源:
// 统一的消息格式
interface ChannelMessage {
channel: ChatChannelId; // "telegram" | "whatsapp" | ...
chatId: string; // 聊天标识
userId: string; // 用户标识
message: string; // 消息内容
replyFn: (text: string) => Promise<void>; // 回复函数
}
小结:本章梳理了从 openclaw gateway run 到消息进入 Agent 的完整链路。理解了这条主线后,下一章我们将深入 Agent 内部,看看它是如何处理消息、调用工具、生成回复的。
四、Agent 核心实现
OpenClaw 本质上是一个 AI Agent 系统,其核心在于实现了一个完整的 Agent 循环。这一节我们深入分析 Agent 的核心实现机制。
4.1 Agent 架构总览
┌─────────────────────────────────────────────────────────────────────────┐
│ 用户消息输入 │
│ (Telegram/WhatsApp/CLI/...) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ runEmbeddedPiAgent() │
│ src/agents/pi-embedded-runner/run.ts │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ • 会话队列管理 (sessionLane / globalLane) │ │
│ │ • 模型解析 & API Key 管理 │ │
│ │ • 上下文溢出处理 → 自动压缩 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ runEmbeddedAttempt() │
│ src/agents/pi-embedded-runner/run/attempt.ts │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ • Skills 加载 │ │
│ │ • Bootstrap 文件注入 │ │
│ │ • 工具创建 │ │
│ │ • 系统提示词构建 │ │
│ │ • createAgentSession() - 创建 Agent 会话 │ │
│ │ • session.prompt() - 执行 LLM 调用 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ subscribeEmbeddedPiSession() - 事件订阅 │
│ src/agents/pi-embedded-subscribe.ts │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 事件类型: │ │
│ │ • message_start/update/end - 消息流处理 │ │
│ │ • tool_execution_start/update/end - 工具调用处理 │ │
│ │ • agent_start/end - Agent 生命周期 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ LLM 模型调用层 │
│ @mariozechner/pi-ai (streamSimple) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 支持的提供商: │ │
│ │ • Anthropic (Claude) │ │
│ │ • OpenAI (GPT-4/4o) │ │
│ │ • Google (Gemini) │ │
│ │ • Ollama (本地模型) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 Agent 运行主入口
文件: src/agents/pi-embedded-runner/run.ts
这是 Agent 运行的主入口函数:
export async function runEmbeddedPiAgent(
params: RunEmbeddedPiAgentParams,
): Promise<EmbeddedPiRunResult> {
// 1. 创建会话队列(确保同一会话的消息串行处理)
const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const globalLane = resolveGlobalLane(params.lane);
return enqueueSession(() =>
enqueueGlobal(async () => {
// 2. 解析模型
const { model, authStorage, modelRegistry } = resolveModel(
provider, modelId, agentDir, params.config
);
// 3. 执行单次 Agent 运行
const attempt = await runEmbeddedAttempt({...});
// 4. 处理上下文溢出 → 自动压缩后重试
if (isContextOverflowError(errorText)) {
await compactEmbeddedPiSessionDirect({...});
continue; // 压缩后重试
}
// 5. 返回结果
return { payloads, meta };
})
);
}
关键设计点:
| 机制 | 说明 |
|---|---|
| 队列串行化 | 同一会话的消息按序处理,避免并发冲突 |
| 上下文溢出自动压缩 | 检测到溢出时自动压缩历史,然后重试 |
4.3 Agent 会话创建与执行
文件: src/agents/pi-embedded-runner/run/attempt.ts
这是单次 Agent 执行的核心逻辑:
export async function runEmbeddedAttempt(
params: EmbeddedRunAttemptParams,
): Promise<EmbeddedRunAttemptResult> {
// 1. 加载 Skills
const skillEntries = loadWorkspaceSkillEntries(effectiveWorkspace);
const skillsPrompt = resolveSkillsPromptForRun({...});
// 2. 加载 Bootstrap 上下文文件(如 AGENTS.md)
const { bootstrapFiles, contextFiles } = await resolveBootstrapContextForRun({...});
// 3. 创建工具集
const toolsRaw = createOpenClawCodingTools({
messageProvider: params.messageChannel,
sessionKey: params.sessionKey,
workspaceDir: effectiveWorkspace,
config: params.config,
abortSignal: runAbortController.signal,
});
// 4. 构建系统提示词
const systemPromptText = buildEmbeddedSystemPrompt({
workspaceDir: effectiveWorkspace,
toolNames: tools.map(t => t.name),
skillsPrompt,
contextFiles,
runtimeInfo: {...},
});
// 5. 创建 Agent 会话(核心!)
const { session } = await createAgentSession({
cwd: resolvedWorkspace,
agentDir,
model: params.model,
tools: builtInTools,
customTools: allCustomTools,
sessionManager,
});
// 6. 设置流式调用函数
session.agent.streamFn = streamSimple;
// 7. 订阅会话事件
const subscription = subscribeEmbeddedPiSession({
session: activeSession,
runId: params.runId,
onToolResult: params.onToolResult,
onBlockReply: params.onBlockReply,
onAgentEvent: params.onAgentEvent,
});
// 8. 执行 prompt(调用 LLM)—— 这是核心!
await activeSession.prompt(effectivePrompt);
// 9. 返回结果
return {
assistantTexts,
toolMetas,
lastAssistant,
};
}
4.4 上下文工程管理
Agent 会话创建后,上下文管理是保证长对话正常运行的核心机制。
4.4.1 架构总览
┌─────────────────────────────────────────────────────────────────────────┐
│ 上下文工程管理架构 │
└─────────────────────────────────────────────────────────────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
│ 会话存储层 │ │ Token 管理层 │ │ 压缩策略层 │
│ (.jsonl 文件) │ │ (估算与限制) │ │ (多级裁剪) │
└───────────────────┘ └───────────────────┘ └───────────────────────┘
│ │ │
│ │ ┌──────────────┴──────────┐
│ │ ▼ ▼
│ │ ┌──────────────┐ ┌──────────────────┐
│ │ │ Context │ │ Compaction │
│ │ │ Pruning │ │ Safeguard │
│ │ │ (软裁剪) │ │ (摘要生成) │
│ │ └──────────────┘ └──────────────────┘
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Memory Search │ │ History Limit │
│ (语义检索) │ │ (轮次限制) │
└───────────────────┘ └───────────────────┘
4.4.2 会话存储机制
存储格式: JSON Lines (.jsonl)
存储路径: ~/.openclaw/sessions/{sessionId}.jsonl
每行是一个独立的 JSON 对象:
{"message": {"role": "user", "content": "你好", "timestamp": 1234567890}}
{"message": {"role": "assistant", "content": [{"type": "text", "text": "你好!"}], "timestamp": 1234567891}}
{"message": {"role": "toolResult", "content": [{"type": "text", "text": "执行结果..."}], "toolCallId": "xxx"}}
设计优势:
- 增量写入: 每条消息追加写入,避免重写整个文件
- 容错性强: 单行损坏不影响其他消息
- 流式读取: 可以逐行读取处理大文件
4.4.3 Token 管理与估算
文件: src/agents/compaction.ts
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { estimateTokens, generateSummary } from "@mariozechner/pi-coding-agent";
import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js";
export const BASE_CHUNK_RATIO = 0.4;
export const MIN_CHUNK_RATIO = 0.15;
export const SAFETY_MARGIN = 1.2; // 20% buffer for estimateTokens() inaccuracy
export function estimateMessagesTokens(messages: AgentMessage[]): number {
return messages.reduce((sum, message) => sum + estimateTokens(message), 0);
}
关键常量:
| 常量 | 值 | 说明 |
|---|---|---|
DEFAULT_CONTEXT_TOKENS | 200,000 | 默认上下文窗口 |
BASE_CHUNK_RATIO | 0.4 | 基础分块比例 |
MIN_CHUNK_RATIO | 0.15 | 最小分块比例 |
SAFETY_MARGIN | 1.2 | 20% 安全边界 |
CHARS_PER_TOKEN_ESTIMATE | 4 | 字符/Token 估算比 |
IMAGE_CHAR_ESTIMATE | 8,000 | 图片字符估算值 |
4.4.4 上下文窗口保护
文件: src/agents/context-window-guard.ts
CONTEXT_WINDOW_HARD_MIN_TOKENS = 16_000; // 硬性最小值
CONTEXT_WINDOW_WARN_BELOW_TOKENS = 32_000; // 警告阈值
// 上下文信息解析优先级:
// 1. modelsConfig (配置文件中的模型配置)
// 2. model (模型元数据)
// 3. agentContextTokens (代理配置)
// 4. default (默认值: 200,000)
4.4.5 Context Pruning (上下文裁剪)
这是核心的实时裁剪机制,在每次 Agent 运行时执行。
文件: src/agents/pi-extensions/context-pruning/pruner.ts
const CHARS_PER_TOKEN_ESTIMATE = 4;
const IMAGE_CHAR_ESTIMATE = 8_000; // 图片按 8000 字符计算
function estimateMessageChars(message: AgentMessage): number {
if (message.role === "user") {
// 处理文本和图片
}
if (message.role === "assistant") {
// 处理文本、thinking、toolCall
}
if (message.role === "toolResult") {
// 处理工具结果(文本+图片)
}
}
两级裁剪策略
软裁剪 (Soft Trim):
// 配置参数
softTrimRatio: 0.3, // 占用 30% 上下文时触发
softTrim: {
maxChars: 4_000, // 超过 4000 字符触发裁剪
headChars: 1_500, // 保留开头 1500 字符
tailChars: 1_500, // 保留结尾 1500 字符
}
// 裁剪后的格式
"[前 1500 字符]\n\n[... 中间内容已裁剪 ...]\n\n[后 1500 字符]"
硬清除 (Hard Clear):
// 配置参数
hardClearRatio: 0.5, // 占用 50% 上下文时触发
minPrunableToolChars: 50_000, // 最小可裁剪字符数
hardClear: {
enabled: true,
placeholder: "[Old tool result content cleared]",
}
裁剪流程图:
计算当前上下文占用比例 (ratio)
│
▼
ratio < 0.3 ─────────→ 不裁剪
│
▼
0.3 ≤ ratio < 0.5 ────→ 软裁剪工具结果
│ (保留头尾)
▼
ratio ≥ 0.5 ────────→ 硬清除工具结果
(替换为占位符)
│
▼
保护首个用户消息之前的内容
(bootstrap files)
4.4.6 Compaction Safeguard (压缩安全机制)
当会话需要进行主动压缩(生成历史摘要)时触发。
文件: src/agents/pi-extensions/compaction-safeguard.ts
// 触发时机: session_before_compact 事件
api.on("session_before_compact", async (event, ctx) => {
// 1. 计算文件操作摘要
const fileSummary = computeFileSummary(messages);
// 包含:读取的文件、修改的文件、创建的文件
// 2. 收集工具失败记录
const failedTools = collectToolFailures(messages);
// 3. 如果新内容超过历史预算,裁剪旧消息
const pruned = pruneHistoryForContextShare({
messages,
maxContextTokens,
maxHistoryShare: 0.5, // 历史最多占 50%
});
// 4. 为被裁剪的消息生成摘要
const droppedSummary = await summarizeInStages({
messages: pruned.droppedMessagesList,
});
// 5. 合并生成最终历史摘要
return {
additionalHistory: [fileSummary, failedTools, droppedSummary].join("\n"),
};
});
4.4.7 分阶段摘要生成 (summarizeInStages)
文件: src/agents/compaction.ts
export function splitMessagesByTokenShare(
messages: AgentMessage[],
parts = DEFAULT_PARTS,
): AgentMessage[][] {
// 将消息按 token 数量均匀分成多份
const totalTokens = estimateMessagesTokens(messages);
const targetTokens = totalTokens / normalizedParts;
// ...
}
export function chunkMessagesByMaxTokens(
messages: AgentMessage[],
maxTokens: number,
): AgentMessage[][] {
// 确保每个块不超过 maxTokens
// 处理超大消息:单独成块
}
摘要生成流程:
原始消息列表 (N 条)
│
▼
┌──────────────────┐
│ splitMessagesByTokenShare │
│ (按 token 均分) │
└──────────────────┘
│
┌────┴────┐
▼ ▼
┌────────┐ ┌────────┐
│ Part 1 │ │ Part 2 │ (默认分 2 份)
└────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────┐
│Summary │ │Summary │ (各自生成摘要)
│ 1 │ │ 2 │
└────────┘ └────────┘
│ │
└────┬────┘
▼
┌──────────────────┐
│ Merge Summaries │
│ (合并为最终摘要) │
└──────────────────┘
│
▼
最终历史摘要
4.4.8 历史轮次限制
文件: src/agents/pi-embedded-runner/history.ts
export function limitHistoryTurns(
messages: AgentMessage[],
limit: number,
): AgentMessage[] {
// 保留最后 N 个用户轮次
// 从后向前扫描 user 消息
// 超过 limit 的轮次被丢弃
}
// 配置示例
config.channels.telegram.dmHistoryLimit = 50; // 全局限制
config.channels.telegram.dms["user123"].historyLimit = 100; // 用户特定限制
4.5 事件驱动的消息处理
文件: src/agents/pi-embedded-subscribe.ts
Agent 使用事件驱动模型处理 LLM 的流式响应。这是一个发布-订阅模式的实现。
4.5.1 为什么需要事件驱动?
当 LLM 生成回复时,它不是一次性返回完整内容,而是逐字符/逐 Token 流式输出。为了实时处理这些内容,系统需要:
- 订阅 LLM 的输出流
- 监听 各种事件(开始、更新、结束、工具调用等)
- 分发 事件到对应的处理器
┌─────────────────────────────────────────────────────────────────────┐
│ 事件驱动消息处理流程 │
└─────────────────────────────────────────────────────────────────────┘
LLM 流式输出
│
▼
┌─────────────────┐
│ session.subscribe() │ ← 订阅会话事件
└────────┬────────┘
│
▼ 事件流
┌─────┴─────┬─────────┬─────────┬─────────┐
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│message│ │message│ │message│ │ tool │ │agent │
│_start │ │_update│ │_end │ │_exec │ │_end │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────┐
│ switch (evt.type) { ... } │ ← 事件分发器
└──────────────────────────────────────────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
处理器1 处理器2 处理器3 处理器4 处理器5
4.5.2 订阅会话事件
export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionParams) {
// 1. 初始化状态(用于跨事件共享数据)
const state: EmbeddedPiSubscribeState = {
assistantTexts: [], // 收集 assistant 回复
toolMetas: [], // 收集工具调用元数据
toolMetaById: new Map(), // 按 ID 索引工具元数据
deltaBuffer: "", // 流式文本缓冲(累积已收到的文本)
blockBuffer: "", // 块缓冲(用于分块回复)
};
// 2. 创建上下文对象(包含状态 + 工具函数)
const ctx: EmbeddedPiSubscribeContext = {
params, // 原始参数
state, // 共享状态
log, // 日志器
// ... 各种工具函数
};
// 3. 创建事件处理器并订阅
const handler = createEmbeddedPiSessionEventHandler(ctx);
const unsubscribe = params.session.subscribe(handler);
// 4. 返回结果和取消订阅函数
return { assistantTexts, toolMetas, unsubscribe };
}
关键概念:
| 概念 | 说明 |
|---|---|
| state | 跨事件共享的可变状态,保存累积的文本、工具信息等 |
| ctx | 上下文对象,包含 state + 各种处理函数,传递给每个处理器 |
| handler | 事件处理器,一个函数,接收事件并分发 |
| subscribe | 订阅方法,注册 handler 到会话的事件流 |
4.5.3 事件分发器原理
文件: src/agents/pi-embedded-subscribe.handlers.ts
// 这是一个"工厂函数",返回一个事件处理器
export function createEmbeddedPiSessionEventHandler(ctx: EmbeddedPiSubscribeContext) {
// 返回的函数会被每个事件调用
return (evt: EmbeddedPiSubscribeEvent) => {
// 根据事件类型分发到不同处理器
switch (evt.type) {
case "message_start":
handleMessageStart(ctx, evt); // → handlers.messages.ts
return;
case "message_update":
handleMessageUpdate(ctx, evt); // → handlers.messages.ts (核心!)
return;
case "message_end":
handleMessageEnd(ctx, evt); // → handlers.messages.ts
return;
case "tool_execution_start":
handleToolExecutionStart(ctx, evt); // → handlers.tools.ts
return;
case "tool_execution_update":
handleToolExecutionUpdate(ctx, evt); // → handlers.tools.ts
return;
case "tool_execution_end":
handleToolExecutionEnd(ctx, evt); // → handlers.tools.ts
return;
case "agent_start":
handleAgentStart(ctx); // → handlers.lifecycle.ts
return;
case "auto_compaction_start":
handleAutoCompactionStart(ctx); // → handlers.lifecycle.ts
return;
case "auto_compaction_end":
handleAutoCompactionEnd(ctx, evt); // → handlers.lifecycle.ts
return;
case "agent_end":
handleAgentEnd(ctx); // → handlers.lifecycle.ts
return;
}
};
}
分发原理图解:
evt = { type: "message_update", delta: "Hello" }
│
▼
┌────────────────────────────────────────────────────────────────────┐
│ switch (evt.type) │
│ │
│ evt.type === "message_start" ? → handleMessageStart(ctx, evt) │
│ evt.type === "message_update" ? → handleMessageUpdate(ctx, evt)│ ← 命中!
│ evt.type === "message_end" ? → handleMessageEnd(ctx, evt) │
│ evt.type === "tool_*" ? → handleTool*(ctx, evt) │
│ evt.type === "agent_*" ? → handleAgent*(ctx) │
│ │
└────────────────────────────────────────────────────────────────────┘
│
▼
handleMessageUpdate(ctx, evt)
│
▼
ctx.state.deltaBuffer += "Hello"
ctx.params.onPartialReply?.("Hello")
4.5.4 事件类型详解
| 事件类型 | 触发时机 | 主要处理 |
|---|---|---|
message_start | LLM 开始生成新消息 | 重置状态,触发"正在输入"指示 |
message_update | LLM 流式输出每个 token | 核心:累积文本、触发流式回复 |
message_end | LLM 消息生成完成 | 最终化文本、触发完整回复 |
tool_execution_start | 工具开始执行 | 显示工具执行状态 |
tool_execution_update | 工具执行中更新 | 更新执行进度 |
tool_execution_end | 工具执行完成 | 收集工具结果、错误处理 |
agent_start | Agent 循环开始 | 初始化 |
agent_end | Agent 循环结束 | 清理、最终化 |
auto_compaction_start | 上下文压缩开始 | 标记压缩进行中 |
auto_compaction_end | 上下文压缩完成 | 恢复正常处理 |
4.5.5 message_update 处理详解(最核心)
文件: src/agents/pi-embedded-subscribe.handlers.messages.ts
export function handleMessageUpdate(ctx, evt) {
const msg = evt.message;
if (msg?.role !== "assistant") return; // 只处理 assistant 消息
// 1. 解析事件子类型
const evtType = assistantRecord.type; // "text_delta" | "text_start" | "text_end"
const delta = assistantRecord.delta; // 本次增量文本
// 2. 处理不同子类型
if (evtType === "text_delta") {
chunk = delta; // 直接使用增量
} else if (evtType === "text_end") {
// 计算增量(防止重复)
if (content.startsWith(ctx.state.deltaBuffer)) {
chunk = content.slice(ctx.state.deltaBuffer.length);
}
}
// 3. 累积到缓冲区
ctx.state.deltaBuffer += chunk;
// 4. 触发流式回复回调
ctx.params.onPartialReply?.(ctx.state.deltaBuffer);
// 5. 处理思考标签 <think>...</think>
// 6. 处理块分割(长消息分块发送)
}
流式处理示意:
LLM 输出: "H" → "e" → "l" → "l" → "o" → " " → "W" → "o" → "r" → "l" → "d"
事件流:
message_start
message_update { delta: "H" } → deltaBuffer = "H"
message_update { delta: "e" } → deltaBuffer = "He"
message_update { delta: "l" } → deltaBuffer = "Hel"
message_update { delta: "l" } → deltaBuffer = "Hell"
message_update { delta: "o" } → deltaBuffer = "Hello"
message_update { delta: " " } → deltaBuffer = "Hello "
message_update { delta: "W" } → deltaBuffer = "Hello W"
...
message_end { content: "Hello World" } → 最终确认
4.5.6 处理器文件组织
src/agents/
├── pi-embedded-subscribe.ts # 入口:订阅函数
├── pi-embedded-subscribe.handlers.ts # 分发器:switch 路由
├── pi-embedded-subscribe.handlers.types.ts # 类型定义
├── pi-embedded-subscribe.handlers.messages.ts # 消息处理器
├── pi-embedded-subscribe.handlers.tools.ts # 工具处理器
└── pi-embedded-subscribe.handlers.lifecycle.ts # 生命周期处理器
4.6 工具调用处理
文件: src/agents/pi-embedded-subscribe.handlers.tools.ts
当 LLM 决定调用工具时的处理流程:
export async function handleToolExecutionStart(
ctx: EmbeddedPiSubscribeContext,
evt: AgentEvent & { toolName: string; toolCallId: string; args: unknown },
) {
// 1. 记录工具元数据
const toolName = normalizeToolName(evt.toolName);
const meta = inferToolMetaFromArgs(toolName, evt.args);
ctx.state.toolMetaById.set(evt.toolCallId, meta);
// 2. 发送工具开始事件
emitAgentEvent({
runId: ctx.params.runId,
stream: "tool",
data: { phase: "start", name: toolName, toolCallId: evt.toolCallId, args: evt.args },
});
}
export function handleToolExecutionEnd(
ctx: EmbeddedPiSubscribeContext,
evt: AgentEvent & { toolName: string; toolCallId: string; result: unknown },
) {
const toolName = normalizeToolName(evt.toolName);
const meta = ctx.state.toolMetaById.get(evt.toolCallId);
// 1. 记录工具结果
ctx.state.toolMetas.push({ toolName, meta });
// 2. 发送工具结束事件
emitAgentEvent({
runId: ctx.params.runId,
stream: "tool",
data: { phase: "result", name: toolName, toolCallId: evt.toolCallId, result: evt.result },
});
}
工具调用的本质:LLM 返回一个结构化的工具调用请求(包含工具名和参数),Agent 框架执行该工具,然后将结果返回给 LLM 继续处理。
工具调用完整示例
以用户问"当前目录有什么文件?“为例:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 用户: "当前目录有什么文件?" │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 1 轮:LLM 分析并决定调用工具 │
│ │
│ LLM 返回: │
│ { │
│ "type": "tool_call", │
│ "tool_name": "list_files", │
│ "tool_call_id": "call_abc123", │
│ "arguments": { │
│ "path": ".", │
│ "recursive": false │
│ } │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────────────┐
│ tool_execution │ │ Agent 框架执行工具 │
│ _start 事件 │ │ │
│ │ │ fs.readdir(".") │
│ → 显示"正在 │ │ → ["src/", "docs/", │
│ 列出文件..." │ │ "package.json", │
└─────────────────┘ │ "README.md"] │
└─────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ tool_execution_end 事件 │
│ │
│ { │
│ "type": "tool_result", │
│ "tool_call_id": "call_abc123", │
│ "result": "src/\ndocs/\npackage.json\nREADME.md" │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 2 轮:LLM 收到工具结果,生成最终回复 │
│ │
│ LLM 输入上下文: │
│ - 用户消息: "当前目录有什么文件?" │
│ - 工具调用: list_files(path=".") │
│ - 工具结果: "src/\ndocs/\npackage.json\nREADME.md" │
│ │
│ LLM 返回: │
│ "当前目录包含以下文件和文件夹: │
│ - src/ (源代码目录) │
│ - docs/ (文档目录) │
│ - package.json (项目配置) │
│ - README.md (项目说明)" │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 用户收到回复 ✅ │
└─────────────────────────────────────────────────────────────────────────────┘
关键理解点:
| 阶段 | 执行者 | 动作 |
|---|---|---|
| 1. 分析问题 | LLM | 理解用户意图,决定需要调用什么工具 |
| 2. 生成调用 | LLM | 输出结构化的工具调用请求(JSON 格式) |
| 3. 执行工具 | Agent 框架 | 解析请求,调用实际的系统 API |
| 4. 返回结果 | Agent 框架 | 将执行结果注入到对话上下文 |
| 5. 生成回复 | LLM | 基于工具结果,生成人类可读的回复 |
本质:LLM 本身不能执行代码或访问文件系统,它只能"说"要做什么。Agent 框架负责"听懂"并"执行”,然后把结果"告诉" LLM。
4.7 工具系统
文件: src/agents/pi-tools.ts
OpenClaw 提供了丰富的内置工具:
export function createOpenClawCodingTools(options?: {...}): AnyAgentTool[] {
// 1. 创建基础编码工具
const base = codingTools.flatMap((tool) => {
if (tool.name === readTool.name) {
return [createOpenClawReadTool(...)];
}
// ...
});
// 2. 创建执行工具
const execTool = createExecTool({ ... });
// 3. 创建 OpenClaw 特有工具
const openclawTools = createOpenClawTools({...});
// 4. 添加钩子包装(before_tool_call hook)
const withHooks = normalized.map(tool =>
wrapToolWithBeforeToolCallHook(tool, {...})
);
return withHooks;
}
核心内置工具:
| 工具名 | 功能描述 |
|---|---|
read | 读取文件内容 |
write | 创建或覆盖文件 |
edit | 精确编辑文件 |
grep | 搜索文件内容 |
find | 按模式查找文件 |
ls | 列出目录内容 |
exec | 执行 shell 命令 |
web_search | 网络搜索 |
web_fetch | 获取网页内容 |
browser | 控制浏览器 |
message | 发送消息 |
memory_search | 记忆搜索 |
4.8 Agent 循环流程图
┌─────────────────────────────────────────────────────────────────┐
│ 用户发送消息 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. 队列等待(串行化) │
│ enqueueSession() + enqueueGlobal() │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. 创建 Agent 会话 │
│ createAgentSession() │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. 构建系统提示词 │
│ buildAgentSystemPrompt() │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. 调用 LLM │
│ session.prompt(userMessage) │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────────┐
│ LLM 返回文本 │ │ LLM 返回工具调用 │
└───────────────────┘ └───────────────────────┘
│ │
│ ▼
│ ┌───────────────────────┐
│ │ 5. 执行工具 │
│ │ tool.execute(args) │
│ └───────────────────────┘
│ │
│ ▼
│ ┌───────────────────────┐
│ │ 6. 返回工具结果 │
│ │ tool result → LLM │
│ └───────────────────────┘
│ │
│ ┌───────────┴───────────┐
│ │ LLM 可能继续调用工具 │
│ │ (循环步骤 4-6) │
│ └───────────────────────┘
│ │
└───────────┬───────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 7. 收集最终回复 │
│ assistantTexts + toolMetas │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 8. 返回给用户 │
│ 通过原渠道发送回复 │
└─────────────────────────────────────────────────────────────────┘
Agent 循环的本质:用户消息 → LLM 思考 → (可选)调用工具 → 工具结果返回 LLM → LLM 继续思考 → … → 最终回复
4.9 核心依赖库
OpenClaw 的 Agent 核心依赖于以下外部库:
| 库 | 作用 |
|---|---|
@mariozechner/pi-agent-core | Agent 核心类型和接口定义 |
@mariozechner/pi-ai | AI 模型调用抽象层 (streamSimple 等) |
@mariozechner/pi-coding-agent | 编码 Agent 会话管理、工具创建 |
这些库提供了:
- 统一的 LLM 调用接口:支持多个模型提供商
- 会话管理和持久化:保存对话历史
- 工具定义和执行框架:标准化的工具接口
- 上下文压缩机制:处理超长对话
五、系统提示词工程
系统提示词(System Prompt)是 Agent 的"灵魂",它决定了 Agent 的身份、能力边界、行为规范。OpenClaw 采用模块化设计,将系统提示词拆分为多个独立的构建函数。
文件: src/agents/system-prompt.ts
5.1 系统提示词整体结构
┌─────────────────────────────────────────────────────────────────┐
│ 系统提示词 (System Prompt) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 身份声明 (Identity) │
│ "You are a personal assistant running inside OpenClaw." │
├─────────────────────────────────────────────────────────────────┤
│ 2. 工具部分 (Tooling) │
│ - 可用工具列表及描述 │
│ - 工具调用风格指南 │
├─────────────────────────────────────────────────────────────────┤
│ 3. 安全规则 (Safety) │
│ - 无独立目标 │
│ - 优先安全和人类监督 │
├─────────────────────────────────────────────────────────────────┤
│ 4. CLI 快速参考 (CLI Reference) │
│ - OpenClaw 命令用法 │
├─────────────────────────────────────────────────────────────────┤
│ 5. Skills 系统 (Skills) │
│ - 技能扫描规则 │
│ - 技能选择和加载指南 │
├─────────────────────────────────────────────────────────────────┤
│ 6. 记忆系统 (Memory) │
│ - 记忆检索指南 │
├─────────────────────────────────────────────────────────────────┤
│ 7. 工作空间 (Workspace) │
│ - 当前工作目录 │
├─────────────────────────────────────────────────────────────────┤
│ 8. 文档参考 (Docs) │
│ - OpenClaw 文档路径 │
├─────────────────────────────────────────────────────────────────┤
│ 9. 消息系统 (Messaging) │
│ - 跨会话消息发送 │
│ - message 工具用法 │
├─────────────────────────────────────────────────────────────────┤
│ 10. 项目上下文 (Project Context) │
│ - AGENTS.md / SOUL.md 等文件内容 │
├─────────────────────────────────────────────────────────────────┤
│ 11. 静默回复 (Silent Replies) │
│ - 无需回复时的处理 │
├─────────────────────────────────────────────────────────────────┤
│ 12. 运行时信息 (Runtime) │
│ - agent/host/os/model 等环境信息 │
└─────────────────────────────────────────────────────────────────┘
主构建函数:
// src/agents/system-prompt.ts
export function buildAgentSystemPrompt(params: {
workspaceDir: string; // 工作目录
toolNames?: string[]; // 可用工具名列表
toolSummaries?: Record<string, string>; // 工具描述映射
skillsPrompt?: string; // Skills 提示词
contextFiles?: EmbeddedContextFile[]; // 上下文文件(如 AGENTS.md)
runtimeInfo?: {...}; // 运行时信息
// ...更多参数
}) {
const lines = [];
// 1️⃣ 身份声明
lines.push("You are a personal assistant running inside OpenClaw.");
// 2️⃣ 工具部分
lines.push("## Tooling", ...toolLines);
// 3️⃣ 安全规则
lines.push(...buildSafetySection());
// 4️⃣ CLI 参考
lines.push("## OpenClaw CLI Quick Reference", ...);
// 5️⃣ Skills 系统
lines.push(...buildSkillsSection({...}));
// 6️⃣ 记忆系统
lines.push(...buildMemorySection({...}));
// 7️⃣ 工作空间
lines.push("## Workspace", `Your working directory is: ${params.workspaceDir}`);
// 8️⃣ 文档参考
lines.push(...buildDocsSection({...}));
// 9️⃣ 消息系统
lines.push(...buildMessagingSection({...}));
// 🔟 项目上下文(关键!)
if (contextFiles?.length > 0) {
lines.push("# Project Context");
for (const file of contextFiles) {
lines.push(`## ${file.path}`, file.content);
}
}
// 1️⃣1️⃣ 静默回复
lines.push("## Silent Replies", ...);
// 1️⃣2️⃣ 运行时信息
lines.push("## Runtime", buildRuntimeLine(runtimeInfo, ...));
return lines.filter(Boolean).join("\n");
}
5.2 核心模块详解
① 工具部分 (Tooling)
⚠️ 重要说明:System Prompt 中的工具部分只包含工具名称和简短描述,完整的参数定义(入参/出参 Schema)是通过 API 的
tools参数单独传递给 LLM 的,不在 system prompt 文本中。📌 技术原理:OpenClaw 使用的是 LLM 的 Function Calling(函数调用) 功能,这是 OpenAI、Claude、Gemini 等主流 LLM 都支持的标准能力。Function Calling 让 LLM 能够以结构化 JSON 格式"调用"预定义的函数,而不是只输出自然语言文本。
Function Calling 工作原理:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Function Calling 完整工作流程 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 1 步:开发者定义函数(工具) │
│ │
│ const readTool = { │
│ name: "read", │
│ description: "Read file contents", │
│ parameters: { │
│ type: "object", │
│ properties: { │
│ path: { type: "string", description: "File path to read" }, │
│ encoding: { type: "string", description: "File encoding" } │
│ }, │
│ required: ["path"] │
│ } │
│ }; │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 2 步:调用 LLM API 时传入 tools 参数 │
│ │
│ const response = await openai.chat.completions.create({ │
│ model: "gpt-4", │
│ messages: [ │
│ { role: "system", content: "You are a helpful assistant..." }, │
│ { role: "user", content: "读取 package.json 的内容" } │
│ ], │
│ tools: [{ // ← 关键:传入工具定义 │
│ type: "function", │
│ function: readTool │
│ }] │
│ }); │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 3 步:LLM 分析用户意图,决定是否调用函数 │
│ │
│ LLM 内部思考: │
│ "用户想读取 package.json → 我需要使用 read 工具 → 参数是 path='package.json'" │
│ │
│ LLM 返回(不是普通文本,而是结构化的 tool_calls): │
│ { │
│ "choices": [{ │
│ "message": { │
│ "role": "assistant", │
│ "content": null, // ← 没有文本内容 │
│ "tool_calls": [{ // ← 而是函数调用请求 │
│ "id": "call_abc123", │
│ "type": "function", │
│ "function": { │
│ "name": "read", │
│ "arguments": "{\"path\": \"package.json\"}" │
│ } │
│ }] │
│ } │
│ }] │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 4 步:Agent 框架执行函数 │
│ │
│ // 解析 LLM 返回的调用请求 │
│ const toolCall = response.choices[0].message.tool_calls[0]; │
│ const args = JSON.parse(toolCall.function.arguments); │
│ │
│ // 执行实际的函数 │
│ const result = await fs.readFile(args.path, "utf-8"); │
│ // result = '{ "name": "openclaw", "version": "1.0.0", ... }' │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 5 步:把执行结果返回给 LLM │
│ │
│ const followUp = await openai.chat.completions.create({ │
│ model: "gpt-4", │
│ messages: [ │
│ { role: "system", content: "..." }, │
│ { role: "user", content: "读取 package.json 的内容" }, │
│ { role: "assistant", content: null, tool_calls: [...] }, // LLM的调用 │
│ { │
│ role: "tool", // ← 工具执行结果 │
│ tool_call_id: "call_abc123", │
│ content: '{ "name": "openclaw", "version": "1.0.0" }' │
│ } │
│ ] │
│ }); │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第 6 步:LLM 基于工具结果生成最终回复 │
│ │
│ LLM 返回: │
│ { │
│ "choices": [{ │
│ "message": { │
│ "role": "assistant", │
│ "content": "package.json 的内容如下:\n\n这是一个名为 openclaw 的项目,│
│ 版本号是 1.0.0..." │
│ } │
│ }] │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
Function Calling vs 普通对话:
| 对比项 | 普通对话 | Function Calling |
|---|---|---|
| LLM 输出 | 自然语言文本 | 结构化 JSON(tool_calls) |
| 参数格式 | 无法保证 | 严格遵循 JSON Schema |
| 可执行性 | 需要解析文本(不可靠) | 可直接解析执行 |
| 错误处理 | 难以判断 | 清晰的调用失败/成功 |
工具信息传递的两个通道:
┌─────────────────────────────────────────────────────────────────────────────┐
│ LLM API 调用结构 │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼
┌───────────────────────────┐ ┌────────────────────────────┐
│ system (System Prompt) │ │ tools (工具定义数组) │
│ │ │ │
│ "## Tooling │ │ [{ │
│ - read: Read file... │ │ name: "read", │
│ - write: Create... │ │ description: "...", │
│ - edit: Make precise..." │ │ parameters: { │
│ │ │ type: "object", │
│ (只有名称 + 简短描述) │ │ properties: { │
│ │ │ path: {...}, │
│ │ │ encoding: {...} │
│ │ │ }, │
│ │ │ required: ["path"] │
│ │ │ } │
│ │ │ }, ...] │
│ │ │ │
│ │ │ (完整的 JSON Schema) │
└───────────────────────────┘ └────────────────────────────┘
| 位置 | 内容 | 目的 |
|---|---|---|
| System Prompt | 工具名 + 简短描述 | 让 LLM 快速了解有哪些能力,节省 Token |
| tools 参数 | 完整 JSON Schema | LLM 需要精确的参数定义才能正确调用 |
② 安全规则 (Safety)
这是 Agent 安全的核心约束:
function buildSafetySection() {
return [
"## Safety",
// 🔒 无独立目标约束
"You have no independent goals: do not pursue self-preservation, " +
"replication, resource acquisition, or power-seeking; " +
"avoid long-term plans beyond the user's request.",
// 🔒 优先安全和人类监督
"Prioritize safety and human oversight over completion; " +
"if instructions conflict, pause and ask; " +
"comply with stop/pause/audit requests and never bypass safeguards. " +
"(Inspired by Anthropic's constitution.)",
// 🔒 禁止操控和自我复制
"Do not manipulate or persuade anyone to expand access or disable safeguards. " +
"Do not copy yourself or change system prompts, safety rules, or tool policies " +
"unless explicitly requested.",
];
}
③ Skills 系统
告诉 Agent 如何检索和使用 Skills:
function buildSkillsSection(params: {
skillsPrompt?: string; // 包含 <available_skills> 列表
readToolName: string;
}) {
return [
"## Skills (mandatory)",
"Before replying: scan <available_skills> <description> entries.",
// 选择规则
`- If exactly one skill clearly applies: read its SKILL.md at <location> with \`${params.readToolName}\`, then follow it.`,
"- If multiple could apply: choose the most specific one, then read/follow it.",
"- If none clearly apply: do not read any SKILL.md.",
// 约束
"Constraints: never read more than one skill up front; only read after selecting.",
// 实际的 skills 列表
params.skillsPrompt, // <available_skills>...</available_skills>
];
}
④ 记忆系统
function buildMemorySection(params: { availableTools: Set<string> }) {
// 只有当 memory 工具可用时才添加
if (!params.availableTools.has("memory_search")) {
return [];
}
return [
"## Memory Recall",
"Before answering anything about prior work, decisions, dates, people, " +
"preferences, or todos: run memory_search on MEMORY.md + memory/*.md; " +
"then use memory_get to pull only the needed lines. " +
"If low confidence after search, say you checked.",
];
}
⑤ 项目上下文(最重要!)
这是让 Agent 了解项目特定规则的核心机制:
// 注入项目上下文文件
if (contextFiles?.length > 0) {
// 检查是否有 SOUL.md(人格设定文件)
const hasSoulFile = contextFiles.some((file) =>
file.path.toLowerCase().endsWith("soul.md")
);
lines.push("# Project Context");
lines.push("The following project context files have been loaded:");
// 如果有 SOUL.md,添加特殊指令
if (hasSoulFile) {
lines.push(
"If SOUL.md is present, embody its persona and tone. " +
"Avoid stiff, generic replies; follow its guidance " +
"unless higher-priority instructions override it."
);
}
// 注入每个上下文文件的内容
for (const file of contextFiles) {
lines.push(`## ${file.path}`, "", file.content, "");
}
}
常见的上下文文件包括:
AGENTS.md- 项目级 Agent 行为指南SOUL.md- Agent 人格设定TOOLS.md- 工具使用指南
⑥ 运行时信息
export function buildRuntimeLine(runtimeInfo?: {...}): string {
return `Runtime: ${[
runtimeInfo?.agentId ? `agent=${runtimeInfo.agentId}` : "",
runtimeInfo?.host ? `host=${runtimeInfo.host}` : "",
runtimeInfo?.os ? `os=${runtimeInfo.os}` : "",
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
runtimeInfo?.channel ? `channel=${runtimeInfo.channel}` : "",
].filter(Boolean).join(" | ")}`;
}
生成示例:
Runtime: agent=main | host=MacBook | os=darwin (arm64) | model=claude-sonnet-4-20250514 | channel=telegram
5.3 模块化设计总结
| 设计 | 说明 |
|---|---|
| 模块化构建 | 每个部分独立函数,易于维护和扩展 |
| 条件包含 | 根据可用工具、模式等动态决定是否包含某部分 |
| 上下文注入 | 支持注入项目特定文件(AGENTS.md 等) |
| 多模式支持 | full/minimal/none 适应不同场景 |
| 安全优先 | 安全规则作为核心部分始终包含 |
| 运行时感知 | 包含当前环境信息,让 Agent 了解运行上下文 |
六、Skills 检索系统
6.1 Skills 目录结构
优先级 (低 → 高):
┌─────────────────────────────────────────────────────────┐
│ 1. extraDirs - 用户配置的额外目录 │
│ 2. bundled - 内置 skills (项目 /skills/ 目录) │
│ 3. managed - 托管目录 (~/.config/openclaw/skills/)│
│ 4. workspace - 工作区 (<workspaceDir>/skills/) │
└─────────────────────────────────────────────────────────┘
内置 Skills 示例 (52+ 个):
skills/
├── github/ # GitHub CLI 集成
├── discord/ # Discord Bot 控制
├── coding-agent/ # 编码代理指南
├── 1password/ # 1Password 密码管理
├── obsidian/ # Obsidian 笔记
├── spotify-player/ # Spotify 播放控制
├── weather/ # 天气查询
├── docker/ # Docker 容器管理
├── kubernetes/ # K8s 集群管理
├── homebrew/ # Homebrew 包管理
└── ...
6.2 SKILL.md 文件格式
示例: skills/github/SKILL.md
---
name: github
description: "Interact with GitHub using the `gh` CLI..."
metadata:
{
"openclaw":
{
"emoji": "🐙",
"requires": { "bins": ["gh"] },
"install":
[
{
"id": "brew",
"kind": "brew",
"formula": "gh",
"bins": ["gh"],
"label": "Install GitHub CLI (brew)",
},
],
},
}
---
# GitHub Skill
Use the `gh` CLI to interact with GitHub...
## Pull Requests
```bash
gh pr checks 55 --repo owner/repo
…
**Frontmatter 字段说明**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | Skill 唯一标识符 |
| `description` | string | 简短描述(用于匹配) |
| `metadata.openclaw.emoji` | string | 显示图标 |
| `metadata.openclaw.os` | string[] | 限制操作系统 |
| `metadata.openclaw.requires.bins` | string[] | 必需的二进制文件 |
| `metadata.openclaw.requires.anyBins` | string[] | 任一即可的二进制 |
| `metadata.openclaw.requires.env` | string[] | 必需的环境变量 |
| `metadata.openclaw.requires.config` | string[] | 必需的配置路径 |
| `metadata.openclaw.install` | object[] | 安装选项 |
| `metadata.openclaw.always` | boolean | 始终包含(跳过检查) |
### 6.3 Skills 加载流程
**文件**: `src/agents/skills/workspace.ts`
```typescript
function loadSkillEntries(
workspaceDir: string,
opts?: {
config?: OpenClawConfig;
managedSkillsDir?: string;
bundledSkillsDir?: string;
},
): SkillEntry[] {
const loadSkills = (params: { dir: string; source: string }): Skill[] => {
const loaded = loadSkillsFromDir(params); // 使用 pi-coding-agent
// ...
};
// 1. 解析各目录路径
const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
const workspaceSkillsDir = path.join(workspaceDir, "skills");
const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir();
const extraDirs = opts?.config?.skills?.load?.extraDirs ?? [];
const pluginSkillDirs = resolvePluginSkillDirs({...});
// 2. 从各目录加载
const bundledSkills = loadSkills({ dir: bundledSkillsDir, source: "openclaw-bundled" });
const extraSkills = mergedExtraDirs.flatMap(dir => loadSkills({...}));
const managedSkills = loadSkills({ dir: managedSkillsDir, source: "openclaw-managed" });
const workspaceSkills = loadSkills({ dir: workspaceSkillsDir, source: "openclaw-workspace" });
// 3. 按优先级合并(后覆盖前)
const merged = new Map<string, Skill>();
for (const skill of extraSkills) merged.set(skill.name, skill);
for (const skill of bundledSkills) merged.set(skill.name, skill);
for (const skill of managedSkills) merged.set(skill.name, skill);
for (const skill of workspaceSkills) merged.set(skill.name, skill);
// 4. 解析 frontmatter 和元数据
return Array.from(merged.values()).map(skill => ({
skill,
frontmatter: parseFrontmatter(raw),
metadata: resolveOpenClawMetadata(frontmatter),
invocation: resolveSkillInvocationPolicy(frontmatter),
}));
}
6.4 Skills 资格检查 (Eligibility)
文件: src/agents/skills/config.ts
export function shouldIncludeSkill(params: {
entry: SkillEntry;
config?: OpenClawConfig;
eligibility?: SkillEligibilityContext;
}): boolean {
const { entry, config, eligibility } = params;
// 1. 检查是否显式禁用
if (skillConfig?.enabled === false) {
return false;
}
// 2. 检查内置 allowlist
if (!isBundledSkillAllowed(entry, allowBundled)) {
return false;
}
// 3. 检查操作系统限制
if (osList.length > 0 && !osList.includes(resolveRuntimePlatform())) {
return false;
}
// 4. always=true 跳过其他检查
if (entry.metadata?.always === true) {
return true;
}
// 5. 检查必需的二进制文件
const requiredBins = entry.metadata?.requires?.bins ?? [];
for (const bin of requiredBins) {
if (!hasBinary(bin) && !eligibility?.remote?.hasBin?.(bin)) {
return false;
}
}
// 6. 检查 anyBins(任一即可)
const requiredAnyBins = entry.metadata?.requires?.anyBins ?? [];
if (requiredAnyBins.length > 0) {
const anyFound = requiredAnyBins.some(bin => hasBinary(bin));
if (!anyFound) return false;
}
// 7. 检查环境变量
const requiredEnv = entry.metadata?.requires?.env ?? [];
for (const envName of requiredEnv) {
if (!process.env[envName] && !skillConfig?.env?.[envName]) {
return false;
}
}
// 8. 检查配置路径
const requiredConfig = entry.metadata?.requires?.config ?? [];
for (const configPath of requiredConfig) {
if (!isConfigPathTruthy(config, configPath)) {
return false;
}
}
return true;
}
资格检查流程图:
Skill 资格检查
│
▼
┌─ enabled=false? ─→ ❌ 排除
│
▼
┌─ 不在 allowBundled? ─→ ❌ 排除
│
▼
┌─ OS 不匹配? ─→ ❌ 排除
│
▼
┌─ always=true? ─→ ✅ 包含
│
▼
┌─ 缺少 bins? ─→ ❌ 排除
│
▼
┌─ 缺少 anyBins? ─→ ❌ 排除
│
▼
┌─ 缺少 env? ─→ ❌ 排除
│
▼
┌─ 缺少 config? ─→ ❌ 排除
│
▼
✅ 包含
6.5 Skills 注入到系统提示词
文件: src/agents/system-prompt.ts
function buildSkillsSection(params: {
skillsPrompt?: string;
isMinimal: boolean;
readToolName: string;
}) {
if (params.isMinimal) {
return []; // 子 agent 不注入 skills
}
const trimmed = params.skillsPrompt?.trim();
if (!trimmed) {
return [];
}
return [
"## Skills (mandatory)",
"Before replying: scan <available_skills> <description> entries.",
`- If exactly one skill clearly applies: read its SKILL.md at <location> with \`${params.readToolName}\`, then follow it.`,
"- If multiple could apply: choose the most specific one, then read/follow it.",
"- If none clearly apply: do not read any SKILL.md.",
"Constraints: never read more than one skill up front; only read after selecting.",
trimmed, // <-- Skills XML 列表
"",
];
}
生成的提示词格式:
## Skills (mandatory)
Before replying: scan <available_skills> <description> entries.
- If exactly one skill clearly applies: read its SKILL.md at <location> with `Read`, then follow it.
- If multiple could apply: choose the most specific one, then read/follow it.
- If none clearly apply: do not read any SKILL.md.
Constraints: never read more than one skill up front; only read after selecting.
<available_skills>
<skill>
<name>github</name>
<description>Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.</description>
<location>/path/to/skills/github/SKILL.md</location>
</skill>
<skill>
<name>discord</name>
<description>Control Discord Bot and server operations...</description>
<location>/path/to/skills/discord/SKILL.md</location>
</skill>
...
</available_skills>
6.6 Skills 检索策略
Agent 的 Skills 检索是基于描述的语义匹配,而不是关键词搜索。
5.6.1 检索原理详解
⚠️ 重要澄清:Skills 检索不使用向量嵌入或语义搜索算法,而是完全依赖 LLM 自身的语言理解能力来匹配用户意图和 Skill 描述。
核心机制:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Skills 检索原理 │
└─────────────────────────────────────────────────────────────────────────────┘
1. 系统将所有 Skill 的名称 + 描述注入到 System Prompt 中:
## Skills (mandatory)
Before replying: scan <available_skills> <description> entries.
- If exactly one skill clearly applies: read its SKILL.md...
- If multiple could apply: choose the most specific one...
- If none clearly apply: do not read any SKILL.md.
<available_skills>
<skill>
<name>github</name>
<description>Interact with GitHub using `gh` CLI...</description>
<location>/path/to/github/SKILL.md</location>
</skill>
<skill>
<name>slack</name>
<description>Send messages to Slack channels...</description>
<location>/path/to/slack/SKILL.md</location>
</skill>
...
</available_skills>
2. LLM 自行阅读用户请求,理解意图,然后扫描所有 description
3. LLM 根据语义相关性判断哪个 Skill 最匹配(这是 LLM 的推理能力,不是算法)
4. 如果匹配到,LLM 使用 read 工具读取对应的 SKILL.md 文件
为什么叫"语义匹配"?
| 方式 | 实现 | 特点 |
|---|---|---|
| 关键词搜索 | 代码算法 (如 BM25) | 精确匹配字符串,“CI” 只能匹配包含 “CI” 的文本 |
| 向量语义搜索 | Embedding + 向量相似度 | 代码计算语义距离,如记忆系统使用的方式 |
| LLM 语义理解 | LLM 推理 | LLM 理解 “CI 状态” 和 “workflow runs” 是相关概念 |
Skills 使用的是第三种方式——让 LLM 自己理解和匹配。
5.6.2 完整检索流程示例
用户请求: "帮我看一下 PR #123 的 CI 状态"
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LLM 收到的 System Prompt 包含: │
│ │
│ <available_skills> │
│ <skill> │
│ <name>github</name> │
│ <description>Interact with GitHub using `gh` CLI. Use `gh issue`, │
│ `gh pr`, `gh run` for issues, PRs, CI runs...</description>│
│ </skill> │
│ <skill> │
│ <name>slack</name> │
│ <description>Send messages to Slack channels...</description> │
│ </skill> │
│ <skill> │
│ <name>weather</name> │
│ <description>Get weather forecasts...</description> │
│ </skill> │
│ </available_skills> │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LLM 内部推理(这是 LLM 的思考过程): │
│ │
│ "用户问的是 PR #123 的 CI 状态..." │
│ "扫描 available_skills..." │
│ "- github: 提到 'gh pr', 'CI runs' → 高度相关 ✓" │
│ "- slack: 发消息,不相关 ✗" │
│ "- weather: 天气,不相关 ✗" │
│ "结论: github skill 明确匹配" │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LLM 输出 Tool Call: │
│ │
│ { │
│ "name": "read", │
│ "arguments": { "path": "/path/to/github/SKILL.md" } │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 框架执行 read 工具,返回 SKILL.md 内容: │
│ │
│ # GitHub Skill │
│ Use the `gh` CLI to interact with GitHub... │
│ │
│ ## Pull Requests │
│ Check CI status on a PR: │
│ ```bash │
│ gh pr checks 55 --repo owner/repo │
│ ``` │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LLM 阅读 SKILL.md 后,执行具体命令: │
│ │
│ { │
│ "name": "exec", │
│ "arguments": { "command": "gh pr checks 123 --repo owner/repo" } │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
5.6.3 为什么不用向量搜索?
| 对比项 | 向量语义搜索 | LLM 直接理解 |
|---|---|---|
| 实现复杂度 | 需要 Embedding 模型 + 向量数据库 | 只需在 Prompt 中列出 |
| 延迟 | 需要额外 API 调用计算向量 | 零额外延迟 |
| Skills 数量 | 适合大规模(数千个) | 适合中小规模(数十个) |
| 准确度 | 依赖向量模型质量 | 依赖 LLM 推理能力 |
| 上下文消耗 | 只返回匹配结果 | 所有描述都在 Prompt 中 |
OpenClaw 的选择:由于 Skills 数量通常在几十个以内,直接让 LLM 扫描所有描述是最简单高效的方案。
5.6.4 System Prompt 中的检索指令
// src/agents/system-prompt.ts
function buildSkillsSection(params) {
return [
"## Skills (mandatory)",
"Before replying: scan <available_skills> <description> entries.",
"- If exactly one skill clearly applies: read its SKILL.md at <location> with `read`, then follow it.",
"- If multiple could apply: choose the most specific one, then read/follow it.",
"- If none clearly apply: do not read any SKILL.md.",
"Constraints: never read more than one skill up front; only read after selecting.",
trimmed, // ← <available_skills>...</available_skills> 内容
];
}
检索规则:
- 扫描 description: Agent 首先扫描所有 skill 的描述
- 单一匹配: 如果只有一个明确匹配,直接使用
- 多重匹配: 选择最具体的那个
- 无匹配: 不读取任何 SKILL.md,直接处理
5.6.5 与记忆系统检索的对比
| 对比项 | Skills 检索 | 记忆系统检索 |
|---|---|---|
| 检索方式 | LLM 扫描 System Prompt | 向量 + 关键词混合搜索 |
| 数据位置 | 在 Prompt 中(消耗 Token) | 在 SQLite 数据库中 |
| 触发方式 | LLM 自动判断 | 调用 memory_search 工具 |
| 适用规模 | 几十个 Skill | 数千条记忆 |
| 检索算法 | 无(LLM 推理) | sqlite-vec + FTS5 |
6.7 Skills 文件监视与热刷新
文件: src/agents/skills/refresh.ts
export function ensureSkillsWatcher(params) {
const watchPaths = resolveWatchPaths(workspaceDir, config);
const watcher = chokidar.watch(watchPaths, {
ignoreInitial: true,
ignored: [/node_modules/, /\.git/, /dist/],
});
watcher.on("add", p => bumpSkillsSnapshotVersion(...));
watcher.on("change", p => bumpSkillsSnapshotVersion(...));
watcher.on("unlink", p => bumpSkillsSnapshotVersion(...));
}
当 Skills 文件变化时,自动刷新快照版本,下次 Agent 运行时会加载最新的 Skills。
七、记忆管理系统
记忆管理系统(Memory System)是 OpenClaw 的跨会话持久化知识检索机制,与上下文工程管理形成互补。
7.1 与上下文工程管理的区别
| 维度 | 上下文工程管理 | 记忆管理系统 |
|---|---|---|
| 作用范围 | 单次会话内 | 跨会话持久化 |
| 存储位置 | 会话文件 .jsonl | MEMORY.md + memory/*.md + SQLite |
| 生命周期 | 随会话结束可能被裁剪/压缩 | 长期保存,跨会话可用 |
| 主要功能 | Token 限制、上下文裁剪、摘要压缩 | 语义检索、知识存储 |
| 核心文件 | compaction.ts, context-pruning/ | memory/manager.ts, memory-tool.ts |
| 触发方式 | 自动(上下文超限时) | 主动调用工具检索 |
简单理解:
- 上下文工程管理解决的是"这轮对话太长了怎么办"
- 记忆管理系统解决的是"上周我让你做的那个任务叫什么"
7.2 架构总览
┌─────────────────────────────────────────────────────────────────────────┐
│ 记忆管理系统架构 │
└─────────────────────────────────────────────────────────────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
│ 记忆来源 │ │ 嵌入层 │ │ 检索层 │
│ (Memory Sources) │ │ (Embeddings) │ │ (Search) │
└───────────────────┘ └───────────────────┘ └───────────────────────┘
│ │ │
┌───────┴───────┐ ┌──────┴──────┐ ┌───────┴───────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│MEMORY.md│ │memory/* │ │ OpenAI │ │ Gemini │ │ Vector │ │Keyword │
│ │ │ *.md │ │Embedding│ │Embedding│ │ Search │ │ Search │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │
└──────────────────────────┼──────────────────────────┘
▼
┌───────────────────┐
│ SQLite + sqlite- │
│ vec (向量存储) │
└───────────────────┘
7.3 记忆存储来源
文件: src/memory/internal.ts
支持的记忆源
| 来源 | 路径 | 说明 |
|---|---|---|
| MEMORY.md | {workspace}/MEMORY.md | 主记忆文件 |
| memory目录 | {workspace}/memory/*.md | 分类记忆文件 |
| 会话记录 | ~/.openclaw/sessions/*.jsonl | 历史对话(实验性) |
| extraPaths | 用户配置的额外路径 | 自定义记忆源 |
// 记忆文件路径判断
export function isMemoryPath(relPath: string): boolean {
const normalized = normalizeRelPath(relPath);
if (normalized === "MEMORY.md" || normalized === "memory.md") {
return true;
}
return normalized.startsWith("memory/");
}
// 列出所有记忆文件
export async function listMemoryFiles(
workspaceDir: string,
extraPaths?: string[],
): Promise<string[]> {
// 1. 检查 MEMORY.md
// 2. 扫描 memory/ 目录下的 .md 文件
// 3. 添加 extraPaths 中的文件
}
文件分块机制
// 分块配置
const DEFAULT_CHUNK_TOKENS = 400; // 每块最大 Token
const DEFAULT_CHUNK_OVERLAP = 80; // 块间重叠 Token
export type MemoryChunk = {
startLine: number; // 起始行号
endLine: number; // 结束行号
text: string; // 块内容
hash: string; // 内容哈希(用于增量更新)
};
7.4 记忆文件结构与生成机制
文件: src/memory/internal.ts, src/hooks/bundled/session-memory/handler.ts
记忆文件格式示例
系统支持三种类型的记忆文件,每种有不同的用途和格式:
1. 主记忆文件 MEMORY.md(手动维护的长期记忆)
这是用户/Agent 手动维护的长期记忆文件,格式自由:
# Long-Term Memory
## 用户偏好
- 喜欢简洁的代码风格
- 偏好使用 TypeScript
- 工作时间:周一至周五 9:00-18:00
## 重要决策
- 2026-01-15: 决定使用 PostgreSQL 作为主数据库
- 2026-01-10: API 采用 RESTful 风格,版本前缀 /v1/
## 项目背景
- 正在开发一个电商后台系统
- 技术栈:Node.js + React + PostgreSQL
## 联系人
- 张三 - 后端负责人
- 李四 - 产品经理
2. 每日记忆文件 memory/YYYY-MM-DD.md(手动/自动)
按日期组织的日志式记忆,可由 Agent 自动写入或手动维护:
# 2026-01-16
## 今日工作
- 完成了用户认证模块的代码审查
- 修复了购物车结算时的并发问题
## 待办事项
- [ ] 完善 API 文档
- [ ] 准备周五的技术分享
## 备注
用户反馈:登录页面加载有点慢,需要优化
3. 自动生成的会话记忆 memory/YYYY-MM-DD-{slug}.md
由 session-memory Hook 在用户执行 /new 命令时自动生成(详见下文):
# Session: 2026-01-16 14:30:00 UTC
- **Session Key**: agent:main:main
- **Session ID**: abc123def456
- **Source**: telegram
## Conversation Summary
user: 帮我设计一个用户管理 API
assistant: 好的,我建议使用 RESTful 风格...
user: 需要支持分页吗?
assistant: 是的,建议使用游标分页...
记忆文件来源
系统支持两种记忆文件来源:
workspace/
├── MEMORY.md # 主记忆文件(手动维护,长期记忆)
├── memory.md # 备选主记忆文件(MEMORY.md 不存在时使用)
└── memory/ # 记忆目录(自动生成 + 手动维护)
├── 2026-01-15-api-design.md # 自动生成的会话记忆
├── 2026-01-16-bug-fix.md # 自动生成的会话记忆
├── projects.md # 手动维护的项目记忆
└── preferences.md # 手动维护的偏好设置
记忆文件扫描逻辑
// src/memory/internal.ts - listMemoryFiles 函数
export async function listMemoryFiles(workspaceDir: string): Promise<string[]> {
const result: string[] = [];
// 1. 扫描主记忆文件
const memoryFile = path.join(workspaceDir, "MEMORY.md");
const altMemoryFile = path.join(workspaceDir, "memory.md");
await addMarkdownFile(memoryFile); // 优先 MEMORY.md
await addMarkdownFile(altMemoryFile); // 备选 memory.md
// 2. 递归扫描 memory/ 目录下所有 .md 文件
const memoryDir = path.join(workspaceDir, "memory");
await walkDir(memoryDir, result); // 递归遍历
// 3. 扫描额外配置的路径(extraPaths)
for (const inputPath of normalizedExtraPaths) {
// 支持目录或单个文件
}
return deduped; // 去重后返回
}
记忆文件自动生成(session-memory Hook)
当用户执行 /new 命令开始新会话时,session-memory Hook 会自动保存上一个会话的内容:
触发条件:用户发送 /new 命令
生成流程:
用户执行 /new 命令
│
▼
┌──────────────────┐
│ session-memory │ Hook 被触发
│ Hook Handler │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 读取上一会话 │ 从 sessionFile 读取最近 N 条消息
│ (默认 15 条) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ LLM 生成 slug │ 根据对话内容生成描述性文件名
│ "api-design" │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 写入记忆文件 │ memory/2026-01-16-api-design.md
└──────────────────┘
生成的记忆文件格式:
# Session: 2026-01-16 14:30:00 UTC
- **Session Key**: agent:main:main
- **Session ID**: abc123def456
- **Source**: telegram
## Conversation Summary
user: 帮我设计一个用户管理 API
assistant: 好的,我建议使用 RESTful 风格...
user: 需要支持分页吗?
assistant: 是的,建议使用游标分页...
...
文件命名规则:
| 情况 | 文件名格式 | 示例 |
|---|---|---|
| LLM 成功生成 slug | YYYY-MM-DD-{slug}.md | 2026-01-16-api-design.md |
| LLM 生成失败(回退) | YYYY-MM-DD-HHMM.md | 2026-01-16-1430.md |
记忆压缩刷新(Memory Flush)
文件: src/auto-reply/reply/memory-flush.ts
当会话接近上下文窗口限制时,系统会触发记忆刷新,提示 Agent 将重要信息持久化:
// 默认刷新提示
DEFAULT_MEMORY_FLUSH_PROMPT = [
"Pre-compaction memory flush.",
"Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed).",
"If nothing to store, reply with [SILENT].",
].join(" ");
// 触发条件
function shouldRunMemoryFlush(params: {
totalTokens: number; // 当前会话 token 数
contextWindowTokens: number; // 上下文窗口大小
reserveTokensFloor: number; // 保留 token 数
softThresholdTokens: number; // 软阈值(默认 4000)
}): boolean {
// 当 totalTokens > (contextWindow - reserve - softThreshold) 时触发
const threshold = contextWindow - reserveTokens - softThreshold;
return totalTokens >= threshold;
}
记忆刷新时机:
会话 Token 使用情况
┌─────────────────────────────────────────────────────┐
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░│
│<──────── 已用 Token ────────>│<── 软阈值 ──>│<保留>│
│ │ (4000) │ │
│ ↑ │
│ 触发 Memory Flush │
└─────────────────────────────────────────────────────┘
记忆文件索引与同步
记忆文件被索引到 SQLite 数据库中,支持增量同步:
// 同步触发时机
sync: {
onSessionStart: boolean; // 会话开始时同步
onSearch: boolean; // 搜索时懒同步
watch: boolean; // 文件变更监视
watchDebounceMs: number; // 防抖间隔(默认 1500ms)
}
索引流程:
记忆文件变更
│
▼
┌─────────────┐
│ 文件监视器 │ (fs.watch / chokidar)
└──────┬──────┘
│ debounce 1500ms
▼
┌─────────────┐
│ 计算文件 hash │ 检测内容是否真的变化
└──────┬──────┘
│
▼ 变化的文件
┌─────────────┐
│ 分块处理 │ chunkMarkdown(tokens=400, overlap=80)
└──────┬──────┘
│
▼
┌─────────────┐
│ 生成嵌入向量 │ OpenAI / Gemini / Local
└──────┬──────┘
│
▼
┌─────────────┐
│ 写入 SQLite │ chunks 表 + FTS5 表 + vec 表
└─────────────┘
7.5 向量嵌入与检索
文件: src/memory/embeddings.ts
嵌入提供者
| 提供者 | 模型 | 说明 |
|---|---|---|
| OpenAI | text-embedding-3-small | 远程 API |
| Gemini | gemini-embedding-001 | 远程 API |
| Local | embeddinggemma-300M | 本地模型 (node-llama-cpp) |
export type EmbeddingProvider = {
id: string;
model: string;
embedQuery: (text: string) => Promise<number[]>; // 单条嵌入
embedBatch: (texts: string[]) => Promise<number[][]>; // 批量嵌入
};
// 提供者选择策略
provider: "openai" | "local" | "gemini" | "auto"
// auto: 优先 OpenAI → Gemini → Local
// 回退机制
fallback: "openai" | "gemini" | "local" | "none"
嵌入存储
文件: src/memory/manager.ts
// SQLite 存储 + sqlite-vec 向量扩展
const VECTOR_TABLE = "chunks_vec"; // 向量表
const FTS_TABLE = "chunks_fts"; // 全文搜索表
const EMBEDDING_CACHE_TABLE = "embedding_cache"; // 嵌入缓存
// 向量转 Blob 存储
const vectorToBlob = (embedding: number[]): Buffer =>
Buffer.from(new Float32Array(embedding).buffer);
7.6 混合搜索策略
文件: src/memory/hybrid.ts
系统采用向量搜索 + 关键词搜索的混合策略,先执行两路并行搜索,再融合结果排序:
整体搜索流程:
用户查询 "上周的数据库设计"
│
┌───────┴───────┐
▼ ▼
┌──────────┐ ┌──────────┐
│向量嵌入 │ │关键词提取│
│(语义表示)│ │(BM25) │
└──────────┘ └──────────┘
│ │
▼ ▼
┌──────────┐ ┌───────────────┐
│sqlite-vec│ │FTS5 全文搜索 │
│ 相似度 │ │(SQLite 内置) │
└──────────┘ └───────────────┘
│ │
└───────┬───────┘
▼
┌──────────────┐
│混合结果排序 │
│(0.7*向量 + │
│ 0.3*关键词) │
└──────────────┘
│
▼
Top N 记忆片段
两种搜索技术对比
| 特性 | 向量搜索 (sqlite-vec) | 关键词搜索 (FTS5) |
|---|---|---|
| 原理 | 计算语义向量的余弦相似度 | 基于倒排索引的 BM25 评分 |
| 优势 | 理解同义词、语义相近 | 精确匹配、速度快 |
| 劣势 | 需要 Embedding 模型、较慢 | 不理解同义词 |
| 示例 | “推送” ≈ “发送” ✅ | “FTS5” 必须精确匹配 |
- 向量搜索:使用
sqlite-vec扩展进行语义相似度计算 - 关键词搜索:使用 SQLite 内置的 FTS5(Full-Text Search 5)全文索引引擎,支持 BM25 相关性排序
💡 FTS5 是 SQLite 原生支持的全文搜索功能,通过
CREATE VIRTUAL TABLE ... USING fts5()创建虚拟表,使用MATCH语法进行全文查询,无需额外的搜索引擎依赖。
默认权重配置
// src/agents/memory-search.ts
DEFAULT_HYBRID_VECTOR_WEIGHT = 0.7; // 向量权重 70%
DEFAULT_HYBRID_TEXT_WEIGHT = 0.3; // 关键词权重 30%
DEFAULT_HYBRID_CANDIDATE_MULTIPLIER = 4; // 候选池扩大倍数
混合结果合并排序算法
文件: src/memory/hybrid.ts 的 mergeHybridResults 函数
混合搜索的核心是**加权线性组合(Weighted Linear Combination)**算法:
最终得分 = vectorWeight × vectorScore + textWeight × textScore
= 0.7 × 向量相似度 + 0.3 × BM25得分
算法步骤:
// 步骤 1: 使用 Map 按文档 ID 建立索引,用于合并去重
const byId = new Map<string, {
id: string;
vectorScore: number; // 向量得分
textScore: number; // BM25 文本得分
// ... 其他字段
}>();
// 步骤 2: 先处理向量搜索结果
for (const r of params.vector) {
byId.set(r.id, {
...r,
vectorScore: r.vectorScore,
textScore: 0, // 初始化文本得分为 0
});
}
// 步骤 3: 再处理关键词搜索结果(合并同 ID 的结果)
for (const r of params.keyword) {
const existing = byId.get(r.id);
if (existing) {
// 同一文档在两种搜索中都命中,合并 textScore
existing.textScore = r.textScore;
} else {
// 仅在关键词搜索中命中,vectorScore 为 0
byId.set(r.id, { ...r, vectorScore: 0, textScore: r.textScore });
}
}
// 步骤 4: 计算加权得分并排序
const merged = Array.from(byId.values()).map((entry) => ({
...entry,
score: params.vectorWeight * entry.vectorScore
+ params.textWeight * entry.textScore,
}));
// 步骤 5: 按最终得分降序排序
return merged.toSorted((a, b) => b.score - a.score);
BM25 分数归一化
FTS5 的 bm25() 函数返回负数排名值(越负越相关,如 -10 比 -2 更相关),需要转换为 0-1 范围的正分数:
// bm25RankToScore: 将 BM25 rank 转换为 0-1 分数
export function bm25RankToScore(rank: number): number {
// 负数 → Math.max(0, 负数) = 0 → 1/(1+0) = 1.0(最相关)
// 正数越大 → 得分越低
const normalized = Math.max(0, rank);
return 1 / (1 + normalized);
}
// 示例:
// rank = -10(非常相关)→ normalized = 0 → score = 1.0
// rank = -2(相关) → normalized = 0 → score = 1.0
// rank = 0 → normalized = 0 → score = 1.0
// rank = 1 → normalized = 1 → score = 0.5
// rank = 9 → normalized = 9 → score = 0.1
📝 注意:由于 FTS5 返回负数表示相关,
Math.max(0, rank)会将所有负数(高相关)都归一化为 0,最终得分为 1.0。这是一种简化处理,确保 BM25 命中的结果都获得高分。
合并逻辑图解
向量搜索结果 关键词搜索结果
┌─────────────────────┐ ┌─────────────────────┐
│ Doc A: vec=0.85 │ │ Doc A: text=0.60 │
│ Doc B: vec=0.72 │ │ Doc C: text=0.90 │
│ Doc D: vec=0.65 │ │ Doc E: text=0.45 │
└─────────────────────┘ └─────────────────────┘
│ │
└──────────┬───────────────────┘
▼
┌─────────────────┐
│ 按 ID 合并 │
└─────────────────┘
│
▼
┌──────────────────────────────────┐
│ Doc A: vec=0.85, text=0.60 │ ← 两边都命中
│ Doc B: vec=0.72, text=0.00 │ ← 仅向量命中
│ Doc C: vec=0.00, text=0.90 │ ← 仅关键词命中
│ Doc D: vec=0.65, text=0.00 │
│ Doc E: vec=0.00, text=0.45 │
└──────────────────────────────────┘
│
▼ 计算加权得分 (0.7×vec + 0.3×text)
┌──────────────────────────────────┐
│ Doc A: 0.7×0.85 + 0.3×0.60 = 0.775 │
│ Doc B: 0.7×0.72 + 0.3×0.00 = 0.504 │
│ Doc D: 0.7×0.65 + 0.3×0.00 = 0.455 │
│ Doc C: 0.7×0.00 + 0.3×0.90 = 0.270 │
│ Doc E: 0.7×0.00 + 0.3×0.45 = 0.135 │
└──────────────────────────────────┘
│
▼ 降序排序 + 过滤 minScore(0.35) + 截取 Top N
┌──────────────────────────────────┐
│ 1. Doc A (0.775) ✅ │
│ 2. Doc B (0.504) ✅ │
│ 3. Doc D (0.455) ✅ │
│ 4. Doc C (0.270) ❌ < minScore │
│ 5. Doc E (0.135) ❌ < minScore │
└──────────────────────────────────┘
为什么这种算法有效?
| 场景 | 向量得分 | BM25得分 | 最终效果 |
|---|---|---|---|
| 语义相似但关键词不同 | 高 | 低 | 0.7×高 + 0.3×低 = 较高 ✅ |
| 关键词精确匹配 | 低 | 高 | 0.7×低 + 0.3×高 = 中等 ✅ |
| 两者都匹配 | 高 | 高 | 0.7×高 + 0.3×高 = 最高 ✅✅ |
| 两者都不匹配 | 低 | 低 | 被 minScore 过滤 ❌ |
💡 设计理念:向量搜索擅长理解语义(“推送”≈“发送”),关键词搜索擅长精确匹配(“FTS5"必须完全匹配)。70:30 的权重偏向语义搜索,同时保留关键词精确匹配的能力。
7.7 记忆文件查看时机
记忆文件不是在每次对话时自动全量加载到上下文中,而是按需查询。主要有以下几种查看时机:
Agent 主动调用(最常见)
当 LLM 需要回答涉及历史信息的问题时,会主动调用 memory_search 和 memory_get 工具。
系统提示词中的记忆召回指令:
## Memory Recall
Before answering anything about prior work, decisions, dates, people,
preferences, or todos: run memory_search on MEMORY.md + memory/*.md;
then use memory_get to pull only needed lines.
If low confidence after search, say you checked.
触发场景:
| 场景 | 示例问题 |
|---|---|
| 过去的工作 | “我上周让你写的那个 API 叫什么?” |
| 历史决策 | “我们为什么选择了这个技术方案?” |
| 日期相关 | “上次部署是什么时候?” |
| 人物信息 | “小明负责哪个模块?” |
| 用户偏好 | “我喜欢什么代码风格?” |
| 待办事项 | “我还有哪些事情没完成?” |
查询流程图:
用户: "我上周让你写的那个 API 叫什么?"
│
▼
┌────────────────────────────────────────┐
│ Agent 判断:需要查询历史记忆 │
│ │
│ 1. 调用 memory_search("上周 API") │
│ → 返回相关片段 + 行号 │
│ │
│ 2. 调用 memory_get(path, from, lines) │
│ → 获取详细内容 │
│ │
│ 3. 基于检索到的信息生成回答 │
└────────────────────────────────────────┘
索引同步时机(后台自动)
记忆文件的索引会在以下时机自动同步(但不是加载到对话上下文):
sync: {
onSessionStart: boolean; // 会话开始时同步索引
onSearch: boolean; // 搜索时懒同步
watch: boolean; // 文件变更监视(实时)
watchDebounceMs: number; // 防抖间隔 1500ms
}
| 同步时机 | 说明 | 用途 |
|---|---|---|
onSessionStart | 会话开始时 | 确保索引与文件一致 |
onSearch | 首次搜索时 | 懒加载优化启动速度 |
watch | 文件变更时 | 实时保持索引最新 |
CLI 手动查询
用户可以通过命令行主动查询记忆:
# 搜索记忆
openclaw memory search "API 设计"
# 查看记忆内容
openclaw memory cat memory/2026-01-16.md
关键设计理念
| 方面 | 说明 |
|---|---|
| 按需加载 | 记忆不会全量加载,只在需要时语义搜索 |
| 两步检索 | 先 memory_search 定位,再 memory_get 获取详情 |
| 节省上下文 | 避免浪费宝贵的上下文窗口空间 |
| 语义搜索 | 支持相近概念匹配(如"推送”≈“发送”) |
💡 这种设计避免了将所有记忆文件塞进上下文造成的 token 浪费,同时保证了 Agent 能在需要时准确召回相关信息。
7.8 记忆工具
文件: src/agents/tools/memory-tool.ts
memory_search 工具
{
name: "memory_search",
description: "Mandatory recall step: semantically search MEMORY.md + memory/*.md
before answering questions about prior work, decisions, dates,
people, preferences, or todos",
parameters: {
query: string, // 搜索查询
maxResults?: number, // 最大结果数 (默认 6)
minScore?: number, // 最小分数阈值 (默认 0.35)
}
}
memory_get 工具
{
name: "memory_get",
description: "Safe snippet read from MEMORY.md, memory/*.md with optional
from/lines; use after memory_search to pull only needed lines",
parameters: {
path: string, // 文件路径
from?: number, // 起始行
lines?: number, // 读取行数
}
}
使用流程:
Agent 收到问题 "我上周让你写的那个 API 叫什么?"
│
▼
┌────────────┐
│memory_search│ ← 先语义搜索
│"上周 API" │
└────────────┘
│
▼
返回相关片段:
- memory/projects.md:15-25 (score: 0.82)
- MEMORY.md:100-110 (score: 0.65)
│
▼
┌────────────┐
│ memory_get │ ← 获取详细内容
│ path, from │
└────────────┘
│
▼
Agent 生成回答
7.9 配置参数
文件: src/agents/memory-search.ts
export type ResolvedMemorySearchConfig = {
enabled: boolean; // 是否启用
sources: Array<"memory" | "sessions">; // 记忆来源
extraPaths: string[]; // 额外文件路径
provider: "openai" | "local" | "gemini" | "auto"; // 嵌入提供者
fallback: "openai" | "gemini" | "local" | "none"; // 回退策略
model: string; // 嵌入模型
store: {
driver: "sqlite";
path: string; // ~/.openclaw/memory/{agentId}.sqlite
vector: {
enabled: boolean;
extensionPath?: string; // sqlite-vec 扩展路径
};
};
chunking: {
tokens: number; // 400
overlap: number; // 80
};
sync: {
onSessionStart: boolean; // 会话开始时同步
onSearch: boolean; // 搜索时同步
watch: boolean; // 文件监视
watchDebounceMs: number; // 1500ms
};
query: {
maxResults: number; // 6
minScore: number; // 0.35
hybrid: {
enabled: boolean; // true
vectorWeight: number; // 0.7
textWeight: number; // 0.3
candidateMultiplier: number; // 4
};
};
cache: {
enabled: boolean; // true
maxEntries?: number;
};
};
配置示例 (~/.openclaw/config.yaml):
agents:
defaults:
memorySearch:
enabled: true
provider: auto
sources:
- memory
extraPaths:
- ~/notes/project-docs.md
query:
maxResults: 10
minScore: 0.4
hybrid:
vectorWeight: 0.8
textWeight: 0.2
八、设计模式与最佳实践
8.1 插件化架构
所有渠道和扩展功能都通过插件系统实现,插件可注册的扩展点包括:
| 类型 | 说明 |
|---|---|
| 🔧 Tools | 自定义 Agent 工具 |
| 🪝 Hooks | 生命周期钩子 |
| 🌐 HTTP Routes | HTTP 端点 |
| 📡 Channels | 消息渠道 |
| 🖥️ CLI Commands | 命令行命令 |
| ⚙️ Services | 后台服务 |
8.2 事件驱动设计
Gateway 使用事件驱动模型进行实时通信:
文件: src/gateway/server-chat.ts
export function createAgentEventHandler({
broadcast,
nodeSendToSession,
agentRunSeq,
chatRunState,
}: AgentEventHandlerOptions) {
const emitChatDelta = (sessionKey: string, clientRunId: string, seq: number, text: string) => {
// 节流:150ms 内只发送一次
const now = Date.now();
const last = chatRunState.deltaSentAt.get(clientRunId) ?? 0;
if (now - last < 150) {
return;
}
// 广播到所有客户端
broadcast("chat", payload, { dropIfSlow: true });
// 发送到特定会话
nodeSendToSession(sessionKey, "chat", payload);
};
}
8.3 并发控制
使用 Lane (车道) 概念进行并发控制(详见 4.2 Agent 运行主入口):
- 会话级队列: 同一会话的消息串行处理,避免并发冲突
- 全局队列: 全局资源(如模型调用)的并发限制
8.4 核心设计亮点总结
上下文管理
| 特性 | 实现方式 | 优势 |
|---|---|---|
| 分层裁剪 | 软裁剪 → 硬清除 | 渐进式降级,保留最大信息 |
| 分阶段摘要 | 分块 → 独立摘要 → 合并 | 处理超大上下文 |
| 安全边界 | 20% buffer | 防止估算误差导致溢出 |
| Bootstrap 保护 | 首个用户消息前不裁剪 | 保留关键初始化信息 |
| 历史限制 | 按用户/渠道配置 | 精细控制不同场景 |
Skills 系统
| 特性 | 实现方式 | 优势 |
|---|---|---|
| 声明式依赖 | YAML frontmatter | 自动检查可用性 |
| 优先级覆盖 | workspace > managed > bundled | 支持自定义覆盖 |
| 懒加载 | 只读取选中的 SKILL.md | 减少上下文消耗 |
| 热刷新 | chokidar 文件监视 | 无需重启即可更新 |
| 插件 Skills | resolvePluginSkillDirs | 扩展能力强 |
记忆系统
| 特性 | 实现方式 | 优势 |
|---|---|---|
| 按需检索 | Agent 主动调用工具 | 避免全量加载浪费 Token |
| 两步检索 | memory_search → memory_get | 精准定位,减少噪音 |
| 混合搜索 | 向量 70% + BM25 30% | 语义理解 + 精确匹配 |
| 增量索引 | 文件 hash 变更检测 | 只更新变化的内容 |
| 多源支持 | MEMORY.md + memory/*.md | 长期记忆 + 日志记忆 |
| 自动生成 | session-memory Hook | /new 时自动保存会话 |
| 嵌入回退 | OpenAI → Gemini → Local | 保证可用性 |
九、总结
9.1 Agent 系统开发的核心能力矩阵
构建一个完整的 Agent 系统需要从以下四个核心维度考虑:
┌─────────────────────────────────────────────────────────────┐
│ Agent 系统核心能力 │
├──────────────┬──────────────┬──────────────┬───────────────┤
│ 感知层 │ 推理层 │ 记忆层 │ 行动层 │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ 多渠道输入 │ LLM 调用 │ 短期记忆 │ 工具调用 │
│ 消息解析 │ 提示词工程 │ 长期记忆 │ 代码执行 │
│ 文件读取 │ 思维链 │ 向量检索 │ 文件操作 │
│ 用户意图识别 │ 多轮对话 │ 会话历史 │ 外部 API │
└──────────────┴──────────────┴──────────────┴───────────────┘
OpenClaw 在这四个维度的实现:
| 维度 | OpenClaw 实现 | 关键文件/模块 |
|---|---|---|
| 感知层 | Telegram/Discord/Slack/WhatsApp 等多渠道统一接入 | src/channels/, src/routing/ |
| 推理层 | 多模型支持 + 结构化系统提示词 + 上下文工程 | src/agents/system-prompt.ts, src/agents/compaction.ts |
| 记忆层 | 向量检索 + BM25 混合搜索 + 按需加载 | src/memory/, src/agents/memory-search.ts |
| 行动层 | 可扩展工具系统 + Skills 动态加载 | src/agents/tools/, src/agents/skills/ |
9.2 OpenClaw 的技术架构总结
从一个完整 Agent 系统的视角,OpenClaw 的架构可以分为以下层次:
┌────────────────────────────────────────────────────────────────────┐
│ 用户交互层 │
│ Telegram │ Discord │ Slack │ WhatsApp │ Web │ CLI │ 插件渠道 │
├────────────────────────────────────────────────────────────────────┤
│ 网关路由层 │
│ 消息路由 │ 会话管理 │ 权限校验 │ 配额控制 │
├────────────────────────────────────────────────────────────────────┤
│ Agent 核心层 │
│ 系统提示词 │ 上下文管理 │ 工具调度 │ Skills 系统 │ 记忆检索 │
├────────────────────────────────────────────────────────────────────┤
│ 模型适配层 │
│ OpenAI │ Claude │ Gemini │ Ollama │ 更多提供商... │
├────────────────────────────────────────────────────────────────────┤
│ 基础设施层 │
│ SQLite 存储 │ 向量索引 │ 文件系统 │ WebSocket │ 事件总线 │
└────────────────────────────────────────────────────────────────────┘
技术选型总结:
| 层次 | 技术选型 | 选型理由 |
|---|---|---|
| 语言 | TypeScript (ESM) | 类型安全、生态丰富、前后端统一 |
| 运行时 | Node.js + Bun | 生产稳定 + 开发高效 |
| 存储 | SQLite + sqlite-vec | 本地优先、零依赖、支持向量 |
| 验证 | Zod + TypeBox | 运行时类型检查、LLM 输出校验 |
| 通信 | WebSocket + EventEmitter | 实时双向、事件解耦 |
| 构建 | tsup + Vitest | 现代化工具链、快速测试 |
9.3 构建生产级 Agent 系统的关键考量
通过分析 OpenClaw,我们可以总结出构建生产级 Agent 系统需要关注的关键点:
1. 上下文工程(Context Engineering)
| 问题 | OpenClaw 解决方案 |
|---|---|
| Token 超限 | 智能裁剪 + 压缩安全机制 |
| 信息丢失 | 保留系统提示词 + 关键消息 |
| 成本控制 | Token 估算 + 历史限制 |
| 信息注入 | 分层上下文(系统/会话/工具) |
2. 记忆系统(Memory System)
| 问题 | OpenClaw 解决方案 |
|---|---|
| 长期记忆 | MEMORY.md + memory/*.md |
| 检索效率 | 向量搜索 + BM25 混合 |
| 增量更新 | Hash 变更检测 |
| 按需加载 | 两步检索(search → get) |
3. 可扩展性(Extensibility)
| 问题 | OpenClaw 解决方案 |
|---|---|
| 功能扩展 | 插件系统 + Skills 系统 |
| 渠道扩展 | 统一消息抽象 + 路由层 |
| 模型扩展 | Provider 抽象层 |
| 工具扩展 | 声明式工具注册 |
4. 可靠性(Reliability)
| 问题 | OpenClaw 解决方案 |
|---|---|
| 模型降级 | 多 Provider 回退链 |
| 嵌入回退 | OpenAI → Gemini → Local |
| 会话恢复 | 本地持久化会话 |
| 错误处理 | 分层错误边界 |
5. 安全性(Security)
| 问题 | OpenClaw 解决方案 |
|---|---|
| 敏感信息 | 本地存储凭证 |
| 权限控制 | 白名单机制 |
| 工具沙箱 | 执行环境隔离 |
| 输入校验 | Zod Schema 验证 |
6. 可观测性(Observability)
| 问题 | OpenClaw 解决方案 |
|---|---|
| 日志追踪 | 会话日志 + 事件流 |
| 状态监控 | CLI status 命令 |
| 诊断工具 | openclaw doctor |
| 调试支持 | 详细错误上下文 |
9.4 学习价值与实践建议
OpenClaw 作为一个生产级 Agent 系统,涵盖了 Agent 开发的核心知识领域:
┌────────────────────────────────────────────────────────────┐
│ 学习价值矩阵 │
├─────────────────┬──────────────────────────────────────────┤
│ 基础架构 │ TypeScript 工程化、插件系统、事件驱动 │
├─────────────────┼──────────────────────────────────────────┤
│ AI 工程 │ 提示词工程、上下文管理、工具调用 │
├─────────────────┼──────────────────────────────────────────┤
│ 记忆系统 │ 向量检索、混合搜索、增量索引 │
├─────────────────┼──────────────────────────────────────────┤
│ 多端适配 │ 多渠道抽象、消息路由、协议适配 │
├─────────────────┼──────────────────────────────────────────┤
│ 生产实践 │ 错误处理、降级策略、可观测性 │
└─────────────────┴──────────────────────────────────────────┘
建议的学习路径:
| 阶段 | 目标 | 重点模块 |
|---|---|---|
| 入门 | 理解 Agent 基本原理 | 系统提示词、工具调用 |
| 进阶 | 掌握上下文工程 | 压缩策略、记忆检索、Skills |
| 深入 | 理解架构设计 | 插件系统、事件驱动、多渠道 |
| 实战 | 构建自己的 Agent | 参考各模块最佳实践 |
总结:OpenClaw 展示了一个现代 Agent 系统应有的完整形态——不仅是调用 LLM 的简单封装,而是一个考虑了感知、推理、记忆、行动全链路,兼顾可扩展性、可靠性、安全性、可观测性的生产级系统。
文档生成时间: 2026-02-02