面向开发者/插件作者的技术分析文档
本文档深入分析 OpenClaw 每次对话前加载的上下文机制,帮助理解其 Token 开销构成和系统架构。
OpenClaw 是一个多平台 AI 对话框架,允许通过统一的接口与多个消息平台集成:
- 支持平台:QQ、WeCom(企业微信)、DingTalk(钉钉)、Feishu(飞书)
- 核心能力:
- 对话会话管理
- 技能(Skill)系统
- 工具调用与自动化
- 多模态支持(文本、图像、语音)
在 LLM 应用中,上下文(Context) 是指每次 API 请求时发送给模型的所有信息,包括:
- 系统提示词(System Prompt):定义 AI 的身份、能力和行为准则
- 对话历史(History):之前的用户消息和 AI 回复
- 动态上下文(Dynamic Context):用户信息、群组信息、时间戳等运行时数据
- Bootstrap 文件:工作区中的配置和说明文件
Token 成本影响:上下文越长,每次 API 调用的成本越高。OpenClaw 的设计需要在"提供足够上下文"和"控制 Token 成本"之间取得平衡。
┌─────────────────────────────────────────────────────────────────────────────┐
│ OpenClaw 对话数据流 │
└─────────────────────────────────────────────────────────────────────────────┘
用户消息 上下文组装 LLM 请求
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────────┐ ┌─────────────┐
│ 平台 │─────────────▶│ Bootstrap │─────────────▶│ LLM Provider│
│ Gateway │ │ + History │ │ (MiniMax, │
│ (QQ/ │ │ + Context │ │ Qwen, etc) │
│ WeCom) │ │ + Skills │ │ │
└─────────┘ └──────────────┘ └─────────────┘
│
▼
┌──────────────┐
│ 响应处理 │
│ 格式化/发送 │
└──────────────┘
上下文组成结构:
┌─────────────────────────────────────────────────────────────┐
│ 最终 LLM 请求体 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 系统提示词层 │ │
│ │ - SOUL.md, IDENTITY.md, TOOLS.md 等 │ │
│ │ - 技能 SKILL.md 文件 │ │
│ │ - 动态上下文信息(用户、群组、时间等) │ │
│ └─────────────────────────────────────────────────────┘ │
│ + │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 会话历史层 │ │
│ │ - 之前的对话记录(来自 .jsonl 转录文件) │ │
│ │ - 工具调用和执行结果 │ │
│ └─────────────────────────────────────────────────────┘ │
│ + │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 当前用户消息 │ │
│ │ - 文本内容 │ │
│ │ - 附件(图片、语音等) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
OpenClaw 的上下文管理系统由以下核心模块组成:
┌─────────────────────────────────────────────────────────────────────────┐
│ OpenClaw 核心架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Gateway │────▶│ Runtime │────▶│ Agent │ │
│ │ (消息接收) │ │ (运行时) │ │ (对话处理) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Platform │ │ Bootstrap │ │ Session │ │
│ │ Adapters │ │ Loader │ │ Manager │ │
│ │ (QQ/WeCom) │ │ (文件加载) │ │ (会话存储) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Transcript │ │
│ │ System │ │
│ │ (对话转录) │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
模块职责:
| 模块 | 位置 | 职责 |
|---|---|---|
| Gateway | extensions/*/src/gateway.ts |
接收平台消息,解析事件 |
| Runtime | dist/plugin-sdk/runtime*.js |
提供运行时 API 和插件管理 |
| Agent | dist/subagent-registry*.js |
处理对话,调用 LLM |
| Bootstrap Loader | dist/plugin-sdk/agents/bootstrap-*.js |
加载工作区文件 |
| Session Manager | dist/sessions-*.js |
管理会话状态 |
| Transcript System | dist/transcript-events-*.js |
记录对话历史 |
会话数据存储在 ~/.openclaw/agents/{agentId}/sessions/sessions.json:
interface SessionEntry {
// 会话标识
sessionId: string; // 唯一会话 ID(UUID)
sessionKey: string; // 格式: "agent:main:main"
// 时间戳
updatedAt: number; // 最后更新时间(Unix ms)
// 会话状态
systemSent: boolean; // 系统消息是否已发送
abortedLastRun: boolean; // 上次运行是否中止
chatType: "direct" | "group"; // 对话类型
// 投递上下文
deliveryContext: {
channel: string; // 平台标识 (qqbot, wecom, feishu)
to: string; // 目标地址
accountId: string; // 账户 ID
};
// 轨迹信息
lastChannel: string;
lastTo: string;
lastAccountId: string;
origin: SessionOrigin; // 会话来源信息
// 文件路径
sessionFile: string; // 转录文件路径(.jsonl)
compactionCount: number; // 压缩次数
// 技能快照
skillsSnapshot: SkillsSnapshot; // 可用技能列表
}// 关键函数位置:dist/sessions-*.js
function resolveSessionKey(ctx: Context): string {
// 从上下文中提取会话标识符
// 格式: "agent:{agentId}:{accountId}"
const agentId = ctx.agent?.id ?? "main";
const accountId = ctx.account?.id ?? "main";
return `agent:${agentId}:${accountId}`;
}OpenClaw 使用内存缓存减少文件 I/O:
// 位置:dist/sessions-*.js
const SESSION_STORE_CACHE = new Map<string, {
store: SessionStore; // 会话存储数据
loadedAt: number; // 加载时间戳
storePath: string; // 文件路径
mtimeMs: number; // 文件修改时间
}>();
function isSessionStoreCacheEnabled(): boolean {
return config?.session?.cache !== false;
}
function isSessionStoreCacheValid(cached: CachedStore): boolean {
const now = Date.now();
const age = now - cached.loadedAt;
return age < CACHE_TTL_MS; // 默认 TTL
}Bootstrap 文件是 OpenClaw 的核心上下文来源,定义了 AI 的身份、能力和环境配置。
~/.openclaw/workspace/
├── SOUL.md # AI 核心身份和行为准则
├── IDENTITY.md # 用户信息和 AI 人设
├── TOOLS.md # 环境特定配置(摄像头、SSH 等)
├── AGENTS.md # 子代理配置
├── HEARTBEAT.md # 心跳任务(定期检查项)
├── USER.md # 用户偏好设置
├── BOOTSTRAP.md # 引导文件
└── skills/ # 技能目录
├── [skill-name]/
│ └── SKILL.md # 技能说明文件
└── ...
// 关键函数:dist/plugin-sdk/agents/bootstrap-files.js
async function resolveBootstrapFilesForRun(params: {
workspaceDir: string;
config?: OpenClawConfig;
sessionKey?: string;
sessionId?: string;
agentId?: string;
warn?: (message: string) => void;
}): Promise<WorkspaceBootstrapFile[]> {
// 1. 扫描工作区目录
// 2. 查找 SOUL.md, IDENTITY.md 等文件
// 3. 应用字符限制
// 4. 返回可注入的文件列表
}
async function resolveBootstrapContextForRun(params: {
// 同上参数
}): Promise<{
bootstrapFiles: WorkspaceBootstrapFile[];
contextFiles: EmbeddedContextFile[];
}> {
// 加载 bootstrap 文件 + 技能文件
}| 配置项 | 默认值 | 说明 |
|---|---|---|
bootstrapMaxChars |
20,000 | 单个文件最大字符数 |
bootstrapTotalMaxChars |
150,000 | 所有文件总计最大字符数 |
// 位置:dist/pi-embedded-helpers-*.js
function resolveBootstrapMaxChars(cfg?: OpenClawConfig): number {
const raw = cfg?.agents?.defaults?.bootstrapMaxChars;
return raw ?? 20000;
}
function resolveBootstrapTotalMaxChars(cfg?: OpenClawConfig): number {
const raw = cfg?.agents?.defaults?.bootstrapTotalMaxChars;
return raw ?? 150000;
}文件注入逻辑:
- 读取文件内容(如果存在)
- 检查是否超过
bootstrapMaxChars,超过则截断 - 计算总字符数,超过
bootstrapTotalMaxChars则丢弃部分文件 - 将剩余内容注入到系统提示词中
技能是可加载的功能模块,每个技能有一个 SKILL.md 文件描述其用途:
interface ResolvedSkill {
name: string; // 技能名称
description: string; // 技能描述
filePath: string; // SKILL.md 文件路径
baseDir: string; // 技能根目录
source: "openclaw-extra" | "workspace" | "plugin";
disableModelInvocation?: boolean;
requiredEnv?: string[]; // 必需的环境变量
}技能加载位置:
- OpenClaw 内置技能:
~/.local/share/pnpm/global/*/node_modules/openclaw/skills/ - 扩展技能:
~/.openclaw/extensions/{extension}/skills/ - 工作区技能:
~/.openclaw/workspace/skills/
转录系统记录对话的完整历史,支持会话恢复和上下文延续。
转录文件使用 JSON Lines 格式,每行一个事件:
~/.openclaw/agents/main/sessions/{sessionId}.jsonl
事件类型:
// 1. 会话开始
{"type":"session","version":3,"id":"...","timestamp":"..."}
// 2. 模型变更
{"type":"model_change","id":"...","parentId":"...","provider":"minimax","modelId":"MiniMax-M2.5"}
// 3. 用户消息
{"type":"message","id":"...","parentId":"...","message":{"role":"user","content":[...]}}
// 4. AI 回复
{"type":"message","id":"...","parentId":"...","message":{"role":"assistant","content":[...]}}
// 5. 思考过程(Thinking)
{"type":"thinking_level_change","id":"...","thinkingLevel":"off"}// 位置:dist/transcript-events-*.js
// 监听转录更新
function onSessionTranscriptUpdate(
transcriptPath: string,
callback: (update: TranscriptUpdate) => void
): () => void;
// 触发更新事件
function emitSessionTranscriptUpdate(transcriptPath: string): void;
// 追加消息到转录
function appendAssistantMessageToSessionTranscript(params: {
agentId: string;
sessionId: string;
message: AssistantMessage;
}): void;以 QQ Bot 为例,消息处理流程如下:
// 位置:extensions/qqbot/src/gateway.ts
// 1. WebSocket 连接接收消息
ws.on('message', (data) => {
const payload = JSON.parse(data.toString()) as WSPayload;
handleEvent(payload);
});
// 2. 事件分发
async function handleEvent(payload: WSPayload) {
switch (payload.t) {
case "C2C_MESSAGE_CREATE":
await handleC2CMessage(payload as C2CMessageEvent);
break;
case "GROUP_MESSAGE_CREATE":
await handleGroupMessage(payload as GroupMessageEvent);
break;
case "GUILD_MESSAGE_CREATE":
await handleGuildMessage(payload as GuildMessageEvent);
break;
}
}// 位置:extensions/qqbot/src/gateway.ts (第 618-664 行)
// 1. 解析用户输入(包括附件)
const parsedContent = parseFaceTags(event.content);
const userContent = parsedContent + attachmentInfo;
// 2. 构建动态上下文信息
const contextInfo = `你正在通过 QQ 与用户对话。
【本次会话上下文】
- 用户: ${event.senderName || "未知"} (${event.senderId})
- 场景: ${isGroupChat ? "群聊" : "私聊"}${isGroupChat ? ` (群组: ${event.groupOpenid})` : ""}
- 消息ID: ${event.messageId}
- 投递目标: ${targetAddress}
【发送图片方法】
你可以发送本地图片!使用 <qqimg>图片路径</qqimg> 标签...
【当前毫秒时间戳】${nowMs}
举例:3分钟后 atMs = ${nowMs} + 180000 = ${nowMs + 180000}...
【定时提醒 — 必读】
设置提醒时,cron 工具的 payload 必须用 agentTurn...
`;
// 3. 合并账户级别系统提示(如果有)
const systemPrompts: string[] = [];
if (account.systemPrompt) {
systemPrompts.push(account.systemPrompt);
}
// 4. 最终发送给 Agent 的消息体
const agentBody = systemPrompts.length > 0
? `${contextInfo}\n\n${systemPrompts.join("\n")}\n\n${userContent}`
: `${contextInfo}\n\n${userContent}`;// 位置:extensions/qqbot/src/gateway.ts (第 792-819 行)
const dispatchPromise = pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: messagesConfig.responsePrefix,
deliver: async (payload: { text?: string; mediaUrls?: string[] }, info: { kind: string }) => {
hasResponse = true;
// 处理 AI 的回复
// 支持文本和媒体发送
}
}
});OpenClaw 使用启发式方法估算 Token 数量:
// 位置:dist/auto-reply/reply/commands-context-report.ts
function estimateTokensFromChars(chars: number): number {
return Math.ceil(chars / 4); // 假设平均 4 字符 = 1 token
}不同消息类型的 Token 计算(来自 pi-coding-agent):
| 消息类型 | 计算方法 |
|---|---|
| 用户消息 | 文本内容长度 / 4 |
| 助手消息 | (文本 + thinking + tool_call) / 4 |
| 工具结果 | 文本内容 / 4 |
| 图像 | 固定 4800 字符 (~1200 tokens,基于 1200px) |
// 默认配置
const DEFAULT_IMAGE_SIZE = 1200; // 像素
// Token 估算(基于 Claude 的图像计费规则)
// 1200px × 1200px 图像 ≈ 1200 tokens(使用 baseline 模式)配置项:agents.defaults.imageMaxDimensionPx(默认 1200)
当对话历史过长时,OpenClaw 会自动触发压缩:
// 位置:openclaw.json
{
"agents": {
"defaults": {
"compaction": {
"mode": "safeguard" // safeguard | aggressive | off
}
}
}
}压缩模式:
safeguard:仅在接近上下文限制时压缩aggressive:更频繁地压缩,保持较短历史off:禁用自动压缩
压缩命令:用户可使用 /compact 手动触发
OpenClaw 支持 Anthropic 的 Prompt Caching 功能:
// 模型配置示例(来自 openclaw.json)
{
"models": [
{
"id": "MiniMax-M2.5",
"cost": {
"input": 15, // 输入 token 价格(每百万)
"output": 60, // 输出 token 价格
"cacheRead": 2, // 缓存读取价格
"cacheWrite": 10 // 缓存写入价格
}
}
]
}心跳机制:定期发送心跳请求保持缓存温暖:
const HEARTBEAT_PROMPT = "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.";interface WorkspaceBootstrapFile {
fileName: string; // 文件名
filePath: string; // 完整路径
content: string; // 文件内容
rawChars: number; // 原始字符数
truncatedChars?: number; // 截断后的字符数
}interface TranscriptMessage {
type: "message";
id: string;
parentId: string | null;
timestamp: string; // ISO 8601
message: {
role: "user" | "assistant" | "system";
content: Array<{
type: "text" | "image" | "toolCall" | "thinking";
text?: string;
source?: { type: "url"; data: string };
name?: string;
id?: string;
arguments?: any;
}>;
timestamp?: number;
usage?: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
};
};
}| 配置路径 | 默认值 | 说明 |
|---|---|---|
agents.defaults.bootstrapMaxChars |
20,000 | 单个 Bootstrap 文件最大字符数 |
agents.defaults.bootstrapTotalMaxChars |
150,000 | 所有 Bootstrap 文件总字符数上限 |
agents.defaults.imageMaxDimensionPx |
1200 | 图像最大尺寸(像素) |
agents.defaults.compaction.mode |
"safeguard" | 会话压缩模式 |
agents.defaults.maxConcurrent |
4 | 最大并发处理数 |
heartbeat.enabled |
true | 心跳启用状态 |
用户发送 QQ 消息
│
▼
WebSocket 接收 (gateway.ts)
│
▼
handleC2CMessage() / handleGroupMessage()
│
├──► parseFaceTags() - 解析表情标签
├──► downloadFile() - 下载图片附件
├──► convertSilkToWav() - 转换语音
│
▼
构建 messageBody 和 contextInfo
│
▼
formatInboundEnvelope() - 格式化消息信封
│
▼
finalizeInboundContext() - 最终化上下文
│
▼
dispatchReplyWithBufferedBlockDispatcher()
│
▼
Runtime → Agent → LLM Provider
Agent 开始处理
│
▼
loadSessionStore() - 加载会话存储
│
├──► 检查缓存 (SESSION_STORE_CACHE)
└──► 读取 sessions.json
│
▼
resolveBootstrapFilesForRun() - 解析 Bootstrap 文件
│
├──► 扫描 workspace/*.md
├──► 扫描 skills/*/SKILL.md
├──► 应用字符限制
└──► 返回可注入文件列表
│
▼
loadTranscript() - 加载对话历史
│
└──► 读取 {sessionId}.jsonl
│
▼
组装最终 Prompt
│
├──► 系统提示词 (Bootstrap 文件)
├──► 技能列表 (skillsSnapshot)
├──► 对话历史 (Transcript)
├──► 动态上下文 (contextInfo)
└──► 当前用户消息
│
▼
发送到 LLM Provider
| 功能模块 | 文件位置 | 关键函数/类型 |
|---|---|---|
| 会话存储 | dist/sessions-*.js |
loadSessionStore(), resolveStoreSessionEntry() |
| Bootstrap 加载 | dist/plugin-sdk/agents/bootstrap-files.js |
resolveBootstrapFilesForRun() |
| 转录事件 | dist/transcript-events-*.js |
onSessionTranscriptUpdate() |
| Token 估算 | dist/pi-embedded-helpers-*.js |
estimateTokensFromChars() |
| QQ Bot 网关 | extensions/qqbot/src/gateway.ts |
handleC2CMessage(), handleGroupMessage() |
| WeCom 网关 | extensions/wecom/src/agent/api-client.ts |
消息接收处理 |
| 配置类型 | dist/plugin-sdk/config/types.agent-defaults.d.ts |
AgentDefaults 接口 |
~/.openclaw/
├── agents/
│ └── main/ # 主代理
│ ├── agent/
│ │ └── models.json # 模型配置
│ └── sessions/
│ ├── sessions.json # 会话元数据存储
│ ├── {session-id-1}.jsonl # 转录文件 1
│ ├── {session-id-2}.jsonl # 转录文件 2
│ └── *.deleted.*.jsonl # 已删除的会话
│
├── extensions/ # 插件扩展
│ ├── qqbot/
│ │ └── skills/ # QQ Bot 技能
│ │ ├── qqbot-cron/
│ │ │ └── SKILL.md
│ │ └── qqbot-media/
│ │ └── SKILL.md
│ └── wecom/
│ └── skills/ # WeCom 技能
│
├── workspace/ # 工作区(上下文来源)
│ ├── SOUL.md # AI 身份核心
│ ├── IDENTITY.md # 用户/人设信息
│ ├── TOOLS.md # 环境配置
│ ├── AGENTS.md # 子代理配置
│ ├── HEARTBEAT.md # 心跳任务
│ ├── USER.md # 用户偏好
│ ├── BOOTSTRAP.md # 引导文件
│ └── skills/ # 工作区技能
│ └── [skill-name]/
│ └── SKILL.md
│
├── openclaw.json # 主配置文件
└── qqbot/ # QQ Bot 数据
└── images/ # 图片缓存
{
"agent:main:main": {
"sessionId": "abc12345-def6-7890-abcd-ef1234567890",
"updatedAt": 1772687015665,
"systemSent": true,
"abortedLastRun": false,
"chatType": "direct",
"deliveryContext": {
"channel": "feishu",
"to": "user:user_def456ghi789",
"accountId": "default"
},
"lastChannel": "feishu",
"lastTo": "user:user_def456ghi789",
"lastAccountId": "default",
"origin": {
"label": "user_def456ghi789",
"provider": "feishu",
"surface": "feishu",
"chatType": "direct",
"from": "feishu:user_def456ghi789",
"to": "user:user_def456ghi789",
"accountId": "default"
},
"sessionFile": "/root/.openclaw/agents/main/sessions/abc12345-def6-7890-abcd-ef1234567890.jsonl",
"compactionCount": 0,
"skillsSnapshot": {
"prompt": "...", // 技能列表提示词
"skills": [
{ "name": "qqbot-cron" },
{ "name": "qqbot-media" },
...
],
"resolvedSkills": [
{
"name": "qqbot-cron",
"description": "QQ Bot 智能提醒技能...",
"filePath": "/root/.openclaw/extensions/qqbot/skills/qqbot-cron/SKILL.md",
"baseDir": "/root/.openclaw/extensions/qqbot/skills/qqbot-cron",
"source": "openclaw-extra"
},
...
]
}
}
}{"type":"session","version":3,"id":"abc12345-def6-7890-abcd-ef1234567890","timestamp":"2026-03-04T22:56:19.694Z","cwd":"/root/.openclaw/workspace"}
{"type":"model_change","id":"xyz789","parentId":null,"timestamp":"2026-03-04T22:56:19.696Z","provider":"minimax","modelId":"MiniMax-M2.5"}
{"type":"thinking_level_change","id":"def456","parentId":"xyz789","timestamp":"2026-03-04T22:56:19.696Z","thinkingLevel":"off"}
{"type":"message","id":"msg001","parentId":"ctx002","timestamp":"2026-03-04T22:56:19.703Z","message":{"role":"user","content":[...],"timestamp":1772664979701}}
{"type":"message","id":"msg002","parentId":"msg001","timestamp":"2026-03-04T22:56:22.877Z","message":{"role":"assistant","content":[...],"usage":{"input":40,"output":73,"cacheRead":0,"cacheWrite":15888,"totalTokens":16001}}}OpenClaw 的上下文管理系统是一个复杂但设计良好的多组件架构:
- 分层存储:会话元数据 (JSON) + 完整对话历史 (JSONL) 分离存储
- Bootstrap 系统:通过工作区 MD 文件定义 AI 身份和能力,支持字符限制
- 技能系统:模块化的技能加载机制,支持按需激活
- 缓存优化:会话存储内存缓存减少文件 I/O
- 事件驱动:转录事件系统实现实时更新通知
- Token 管理:估算、限制、压缩、缓存等多维度控制
Token 开销构成(按典型场景估算):
| 组件 | 预估 Token |
|---|---|
| SOUL.md | ~500 |
| IDENTITY.md | ~200 |
| TOOLS.md | ~400 |
| 技能提示词 | ~1000-2000 |
| 动态上下文 | ~300-500 |
| 对话历史(每轮) | ~500-2000 |
| 图像(每张) | ~1200 |
理解这些机制后,可以更有针对性地优化上下文加载策略,或开发自定义的上下文管理插件。
文档版本:1.0 生成日期:2026-03-05 分析基准:OpenClaw 2026.2.9