本文基于 OpenClaw 源码分析,探讨一个消息从进入系统到生成回复的完整上下文生命周期。
一、整体架构:上下文处理的五个阶段
OpenClaw 的上下文处理并非一个单一的函数,而是贯穿整个消息生命周期的协作流水线。整体可分为五个阶段:
消息入口 → 预检过滤 → 上下文组装 → 模型调用 → 上下文维护每个阶段职责清晰、边界明确,通过 ctxPayload 这个核心数据结构串联。
二、阶段一:消息接收与预检过滤
2.1 入口:preflightDiscordMessage
以 Discord 频道为例(message-handler-DbRk4Lwu.js),消息通过 WebSocket 事件进入后,首先经过 preflightDiscordMessage 函数进行预检。这不是一个简单的"有没有消息"的判断,而是一套完整的安全与路由决策引擎。
// 源码位置: message-handler-DbRk4Lwu.js
async function preflightDiscordMessage(params) {
// 1. 认证与去重
if (isProcessAborted(params.abortSignal)) return null;
if (dedupeKey && recentInboundMessages.check(dedupeKey)) return null;
// 2. 机器人过滤
if (author.bot && allowBotsMode = "off" && !sender.isPluralKit) return null;
// 3. 消息类型过滤(系统事件、斜杠命令等)
if (isGuildMessage && (
message.type = MessageType.ChatInputCommand ||
message.type = MessageType.ContextMenuCommand
)) return null;
// 4. DM/频道权限策略
if (dmPolicy = "disabled") return null;
const dmAccess = await resolveDiscordDmCommandAccess({ ... });
// 5. 提及检测(@机器人)
const wasMentioned = matchesMentionWithExplicit({ text, mentionRegexes, ... });
// 6. 成员白名单
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({ ... });
if (isGuildMessage && hasAccessRestrictions && !memberAllowed) return null;
// 7. 其他频道消息忽略
if (ignoreOtherMentions && hasUserOrRoleMention && !wasMentioned) return null;
// 8. 系统事件注入
const systemText = resolveDiscordSystemEvent(message, systemLocation);
if (systemText) {
enqueueSystemEvent(systemText, { sessionKey: effectiveRoute.sessionKey, ... });
return null;
}
}
2.2 频道历史管理
预检通过后,消息会进入频道历史记录(reply-history-CYr7j6cE.js):
// 构建历史上下文标记
const HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
const CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
function buildHistoryContext(params) {
const { historyText, currentMessage } = params;
if (!historyText.trim()) return currentMessage;
return [
HISTORY_CONTEXT_MARKER,
historyText,
"",
CURRENT_MESSAGE_MARKER,
currentMessage
].join(lineBreak);
}
// 历史记录追加(带 LRU 驱逐)
const MAX_HISTORY_KEYS = 1e3;
function appendHistoryEntry(params) {
const history = historyMap.get(historyKey) ?? [];
history.push(entry);
while (history.length > params.limit) history.shift(); // 超过限制时移除最旧记录
historyMap.set(historyKey, history);
evictOldHistoryKeys(historyMap);
}
关键设计点:频道历史以 Map 结构存储在内存中,而非每次都从磁盘读取。通过 MAX_HISTORY_KEYS = 1000 防止内存无限增长,通过 historyLimit(默认50条)控制每个频道的历史深度。
三、阶段二:上下文组装
3.1 核心结构 ctxPayload
通过 finalizeInboundContext(templating-BpbUbFSs.js)构建最终传递给模型的上下文结构:
const ctxPayload = finalizeInboundContext({
Body: combinedBody, // 格式化后的消息内容
BodyForAgent: baseText ?? text, // 供 Agent 使用的原始文本
InboundHistory: inboundHistory, // 频道历史(已格式化)
RawBody: baseText, // 未处理的原始文本
CommandBody: baseText, // 命令体(用于 /command 解析)
// 路由信息
From: effectiveFrom, // 消息来源标识
To: effectiveTo, // 目标会话
SessionKey: boundSessionKey ?? threadKeys.sessionKey,
AccountId: route.accountId,
// 发送者信息
SenderName: senderName,
SenderId: sender.id,
SenderUsername: senderUsername,
SenderTag: senderTag,
// 群组/频道信息
GroupSubject: groupSubject, // 群组主题
GroupChannel: groupChannel, // 频道标签
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : void 0,
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) : void 0,
// 安全上下文
UntrustedContext: untrustedContext,
OwnerAllowFrom: ownerAllowFrom,
// 回复上下文
ReplyToId: replyContext?.id,
ReplyToBody: replyContext?.body,
ReplyToSender: replyContext?.sender,
// 线程信息
MessageThreadId: threadChannel?.id,
ThreadStarterBody: threadStarterBody,
ThreadLabel: threadLabel,
// 媒体信息
...mediaPayload,
// 元数据
Provider: "discord",
Surface: "discord",
WasMentioned: effectiveWasMentioned,
Timestamp: resolveTimestampMs(message.timestamp),
MessageSid: message.id,
});
3.2 安全防护:输入净化
这是 OpenClaw 上下文处理中最体现安全意识的部分(templating-BpbUbFSs.js):
const BRACKETED_SYSTEM_TAG_RE = /\[\s*(System\s*Message|System|Assistant|Internal)\s*\]/gi;
const LINE_SYSTEM_PREFIX_RE = /^(\s*)System:(?=\s|$)/gim;
function sanitizeInboundSystemTags(input) {
return input
.replace(BRACKETED_SYSTEM_TAG_RE, (_match, tag) => `(${tag})`)
.replace(LINE_SYSTEM_PREFIX_RE, "$1System (untrusted):");
}
防护逻辑:用户可能尝试在消息中注入 [System Message] 或 System: 前缀来冒充系统指令。sanitizeInboundSystemTags 会将这些标记替换为不可执行的格式,防止 Prompt Injection 攻击。
3.3 会话键(SessionKey)的路由逻辑
会话键是 OpenClaw 多租户/多会话隔离的核心机制(sessions-uRDRs4f-.js):
function deriveSessionKey(scope, ctx) {
if (scope === "global") return "global";
const resolvedGroup = resolveGroupSessionKey(ctx);
if (resolvedGroup) return resolvedGroup.key; // 群组会话: "discord:group:123456"
return (ctx.From ? normalizeE164(ctx.From) : "") || "unknown"; // 私聊 → main
}
// 群组会话键示例:
// discord:group:123456
// telegram:channel:-1002381931352
// whatsapp:group:123456789@g.us
关键设计:私聊消息全部归一化到 main 会话,群组/频道消息则保持隔离。这意味着 AI 在私聊中拥有完整的多轮上下文,在群组中则按频道隔离。
四、阶段三:上下文向 Prompt 的转化
4.1 上下文引擎注册机制
OpenClaw 实现了可插拔的上下文引擎架构(pi-embedded-BaSvmUpW.js):
// 注册表设计
function registerContextEngineForOwner(id, factory, owner, opts) {
// id: "legacy" | "custom-plugin-slot"
// owner: "core" | "public-sdk"
// 支持第三方通过 api.registerContextEngine() 插入自定义引擎
}
// 内置 Legacy 引擎(默认)
var LegacyContextEngine = class {
async ingest(_params) { return { ingested: false }; }
async assemble(params) { return { messages: params.messages, estimatedTokens: 0 }; }
async compact(params) { return await delegateCompactionToRuntime(params); }
};
4.2 引导文件与工作区上下文
OpenClaw 在启动时会加载工作区中的引导文件,组成初始系统上下文(pi-embedded-BaSvmUpW.js):
// 加载 bootstrap 文件
const bootstrapFiles = filterBootstrapFilesForSession({
sessionKey, workspaceDir, bootstrapContextMode
});
// 典型文件: SOUL.md, AGENTS.md, USER.md, HEARTBEAT.md
// 构建初始上下文
async function loadWorkspaceBootstrapFiles(sessionKey, workspaceDir) {
// 1. 读取 workspaceDir 下的 .md 文件
// 2. 按文件名排序拼接
// 3. 作为 system prompt 前缀注入
}
这正是 AGENTS.md 中描述的行为:每次会话启动时,读取 SOUL.md → USER.md → memory/ 当日文件 → MEMORY.md,这些文件共同构成了 AI 的"人格"和"记忆"。
4.3 Skills 系统:动态工具上下文注入
Skills 是 OpenClaw 的可扩展工具集,通过 SKILL.md 文件描述(skills-Xrdxpo0d.js):
// Skill 解析流程
function loadSkillsFromDir(skillDir) {
const entries = [];
const files = listMarkdownFilesRecursive(skillDir); // 递归查找 .md 文件
for (const filePath of files) {
const frontmatter = parseFrontmatterBlock(raw);
const promptTemplate = stripFrontmatter(raw);
// frontmatter.name → 技能名称
// frontmatter.description → 技能描述(用于 agent 理解何时调用)
// promptTemplate → 技能的完整指令文本
}
}
// Skill 过滤与选择
function filterSkillEntries(entries, config, skillFilter, eligibility) {
let filtered = entries.filter(entry => shouldIncludeSkill({ entry, config, eligibility }));
if (skillFilter !== void 0) {
const normalized = normalizeSkillFilter(skillFilter) ?? [];
filtered = normalized.length > 0
? filtered.filter(entry => normalized.includes(entry.skill.name))
: [];
}
return filtered;
}
关键设计:Skill 的路径在注入 Prompt 前会被压缩为 ~/... 格式(compactSkillPaths),节省大量 token:
// 原始: /Users/alice/.bun/node/lib/skills/github/SKILL.md
// 压缩后: ~/.bun/node/lib/skills/github/SKILL.md
function compactSkillPaths(skills) {
const home = os.homedir();
return skills.map(s => ({
...s,
filePath: s.filePath.startsWith(prefix) ? "~/" + s.filePath.slice(prefix.length) : s.filePath
}));
}
五、阶段四:会话存储与历史持久化
5.1 双层存储架构
OpenClaw 实现了内存缓存 + 磁盘持久化的双层会话存储(sessions-uRDRs4f-.js):
// 内存层: SessionManager (来自 pi-coding-agent 包)
// 磁盘层: session store (JSON 文件)
const CURRENT_SESSION_VERSION = "...";
async function loadSessionStore(storePath) {
const raw = await readFile(storePath, "utf-8");
return JSON.parse(raw);
}
async function saveSessionStore(storePath, store) {
await writeJsonAtomic(storePath, store);
}
5.2 会话元数据管理
function mergeSessionEntry(existing, patch) {
return mergeSessionEntryWithPolicy(existing, patch);
}
function mergeSessionEntryWithPolicy(existing, patch, options) {
const sessionId = patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID();
const updatedAt = resolveMergedUpdatedAt(existing, patch, options);
if (!existing) {
return normalizeSessionRuntimeModelFields({ ...patch, sessionId, updatedAt });
}
return normalizeSessionRuntimeModelFields({
...existing,
...patch,
sessionId,
updatedAt
});
}
5.3 磁盘预算控制
会话文件会不断增长,OpenClaw 实现了磁盘预算控制:
// 预算配置
const maintenance = {
maxDiskBytes: 100 * 1024 * 1024, // 100MB 上限
highWaterBytes: 80 * 1024 * 1024, // 80MB 时开始清理
keepArchivedTranscripts: true,
removeEmptySessionFiles: true,
};
// 按修改时间排序,删除最旧的会话文件直到低于预算
async function enforceSessionDiskBudget(params) {
const files = await readSessionsDirFiles(sessionsDir);
files.sort((a, b) => a.mtimeMs - b.mtimeMs); // 最旧的先删
while (total > highWaterBytes && files.length > 0) {
const removed = await removeFileForBudget({ filePath: files.shift().path });
total -= removed;
}
}
六、阶段五:上下文压缩与溢出处理
6.1 溢出检测
当模型上下文窗口接近上限时,OpenClaw 会触发压缩(agent-runner.runtime-i_gf132J.js):
async function runAgentTurnWithFallback(params) {
while (true) {
try {
const result = await runWithModelFallback({
...resolveModelFallbackOptions(params.followupRun.run),
runId,
run: (provider, model, runOptions) => {
return runEmbeddedPiAgent({
...embeddedContext,
// 模型调用
onCompactionStart: async () => {
// 通知用户正在压缩
await params.opts.onCompactionStart?.();
},
onCompactionEnd: async () => {
// 压缩完成,重新计数
attemptCompactionCount += 1;
await params.opts.onCompactionEnd?.();
}
});
}
});
} catch (error) {
if (isContextOverflowError(error)) {
// 触发上下文压缩后重试
await compactEmbeddedPiSessionDirect({ sessionFile, tokenBudget, force: true });
continue; // 重试
}
throw error;
}
}
}
6.2 委托式压缩
自定义上下文引擎可以将压缩委托给内置运行时:
async function delegateCompactionToRuntime(params) {
const { compactEmbeddedPiSessionDirect } = await import("./compact.runtime-C_quO5Zm.js");
const result = await compactEmbeddedPiSessionDirect({
sessionId: params.sessionId,
sessionFile: params.sessionFile,
tokenBudget: params.tokenBudget,
force: params.force,
customInstructions: params.customInstructions,
workspaceDir: params.workspaceDir
});
return { compacted: result.compacted, ... };
}
七、完整的上下文流图
Discord/Telegram/Signal 等消息源 │ ▼ ┌──────────────────┐ │ preflightMessage │ ← 预检过滤(权限/白名单/去重/系统事件) └────────┬─────────┘ │ ctxPayload ▼ ┌──────────────────────────┐ │ finalizeInboundContext │ ← 净化用户输入、构建标准上下文结构 └────────┬─────────────────┘ │ ▼ ┌──────────────────────────┐ │ recordInboundSession │ ← 写入会话存储、更新元数据 └────────┬─────────────────┘ │ ▼ ┌──────────────────────────┐ │ buildPendingHistoryContext│ ← 追加频道历史 └────────┬─────────────────┘ │ ▼ ┌──────────────────────────┐ │ dispatchInboundMessage │ ← 触发 Agent 执行 └────────┬─────────────────┘ │ ▼ ┌──────────────────────────────────────────────┐ │ runEmbeddedPiAgent │ │ ├─ 加载引导文件 (SOUL.md / AGENTS.md / USER.md) │ │ ├─ 注入 Skills (SKILL.md 系统) │ │ ├─ 注入会话历史 (SessionManager) │ │ └─ 构建最终 messages[] 数组 │ └────────┬─────────────────────────────────────┘ │ ▼ 模型 API 调用 │ ┌─────┴─────┐ │ 成功 │ 溢出错误 │ │ ▼ ▼ 回复投递 触发压缩 (compact) │ ▼ 重试调用模型八、关键设计哲学总结
| 安全净化 | sanitizeInboundSystemTags[System] 等注入 | |
| 会话隔离 | main | |
| 历史热存储 | ||
| 上下文压缩 | ||
| Skills 动态加载 | ||
| 磁盘预算 | ||
| 多渠道统一抽象 | ctxPayload |
源码版本: OpenClaw (Node.js), 2026-03-26 基于 dist/ 目录分析
夜雨聆风