OpenClaw 可以理解为一套 AI Agent 运行时框架。它不只是把大模型接到几个工具上,而是围绕“Agent 如何安全执行工具、如何保存长期记忆、如何扩展能力、如何管理多渠道会话、如何读取配置”做了一整套工程化设计。
从运行时视角看,OpenClaw 的核心链路大致是这样的:
flowchart LR
U[用户 / 外部通道] --> G[Gateway]
G --> S[Session 管理]
S --> A[Agent Runtime]
A --> C[Config 配置系统]
A --> M[Memory 记忆系统]
A --> K[Skills 技能系统]
A --> T[Tools 工具调用]
T --> P[工具策略过滤]
P --> B[Sandbox 沙箱]
B --> W[Workspace 工作区]
B --> D[(Docker 容器)]
M --> F[Markdown 记忆文件]
M --> I[(SQLite + 向量索引)]
这套结构背后的目标很明确:Agent 可以拥有执行能力,但执行能力必须被限制;Agent 可以拥有记忆,但记忆不能只存在模型上下文里;Agent 可以不断扩展能力,但扩展过程要能被配置、过滤和审计。
1. Sandbox:把工具执行放进受控环境
大模型调用工具时,风险不在“模型会说什么”,而在“工具真的会做什么”。如果 exec、write、edit 这类工具直接在主机上执行,模型一次错误判断就可能改错文件、删除目录,甚至访问不该访问的系统资源。
OpenClaw 的 Sandbox 是一个 Docker 隔离层,用来承接 Agent 的工具操作。它不是为了让不可信租户共享同一台机器,而是为了缩小单个 Agent 执行失误时的影响范围。
1.1 沙箱负责解决什么问题
Sandbox 的职责可以归纳为三点:
| 目标 | 说明 |
|---|---|
| 限制工具边界 | exec、read、write、edit 等操作不直接落到主机环境 |
| 缩小破坏范围 | 模型执行异常命令时,影响被限制在容器或指定工作区内 |
| 支持隔离级别配置 | 不同会话、不同 Agent 可以选择不同隔离策略 |
相关模块集中在 src/agents/sandbox/:
src/agents/sandbox/
├── types.ts # 沙箱核心类型
├── config.ts # 配置合并逻辑
├── context.ts # 解析沙箱上下文
├── docker.ts # Docker 容器管理
├── browser.ts # 隔离浏览器容器
├── tool-policy.ts # 工具允许 / 拒绝策略
├── validate-sandbox-security.ts # 安全配置验证
├── fs-bridge.ts # 文件系统桥接
└── prune.ts # 容器自动清理
1.2 三类沙箱模式
OpenClaw 的沙箱不是一个固定开关,而是有模式控制。
| 模式 | 行为 |
|---|---|
off | 不启用隔离,工具直接在主机运行 |
non-main | 只隔离非主会话,通常作为默认策略 |
all | 所有会话都放进沙箱 |
non-main 适合一种常见场景:主会话需要较高权限处理本地工作区,子 Agent 或外部触发的会话则进入容器,避免扩散风险。
1.3 容器作用域
Sandbox 还要决定“容器复用粒度”。
| 作用域 | 容器数量 | 特点 |
|---|---|---|
session | 每个会话一个容器 | 隔离性较好,默认选择 |
agent | 每个 Agent 一个容器 | 同一 Agent 的会话共享环境 |
shared | 所有会话共享容器 | 资源占用较低,但隔离性最弱 |
如果任务之间不能互相污染,优先选择 session。如果容器启动成本较高,而且任务来自同一可信 Agent,可以考虑 agent。
1.4 工作区访问权限
容器内是否能访问 Agent 的工作区,也需要单独配置。
| 权限 | 挂载行为 |
|---|---|
none | 使用完全隔离目录,例如 ~/.openclaw/sandboxes |
ro | 只读挂载 Agent 工作区到 /agent |
rw | 读写挂载工作区到 /workspace |
这三个选项体现了不同安全等级:
none:适合运行不可信输入、临时任务、外部触发任务。ro:适合读取项目上下文,但不允许修改。rw:适合需要真实修改工作区的自动化任务,但风险最高。
1.5 沙箱安全限制
OpenClaw 会拒绝明显危险的 Docker 配置,尤其是那些会绕过隔离边界的挂载和网络模式。
禁止绑定挂载的路径包括:
/etc
/proc
/sys
/dev
/root
/boot
/run
/var/run/docker.sock
/
其中 /var/run/docker.sock 特别危险。容器一旦拿到 Docker socket,基本就能控制宿主机上的 Docker,沙箱隔离会失去意义。
禁止的网络模式包括:
| 网络模式 | 风险 |
|---|---|
host | 容器直接使用宿主机网络,绕过网络隔离 |
container:<id> | 加入其他容器命名空间,破坏隔离边界 |
默认安全配置通常应该偏保守:
{
"readOnlyRoot": true,
"network": "none",
"capDrop": ["ALL"]
}
含义分别是:
- 根文件系统只读;
- 默认无网络;
- 丢弃 Linux capabilities(能力),避免容器获得额外系统权限。
1.6 工具策略是逐层收紧的
Sandbox 不是唯一的权限控制点。OpenClaw 在工具调用前会经过多层策略过滤:
flowchart TD
A[Agent 请求调用工具] --> B[全局工具策略]
B --> C[Agent 专属策略]
C --> D[Sandbox 工具策略]
D --> E[子 Agent 策略]
E --> F{是否允许}
F -- 是 --> G[执行工具]
F -- 否 --> H[拒绝调用]
关键点在于:Sandbox 工具策略只能进一步限制,不能放宽前面已经禁止的工具。
默认允许的工具通常是偏文件和命令执行类的工具,例如:
exec
read
write
edit
apply_patch
image
默认禁止的工具包括:
browser
canvas
nodes
cron
gateway
消息通道类工具
这种默认策略避免容器里的 Agent 绕过隔离去操作 Gateway、消息通道或计划任务。
1.7 沙箱配置示例
{
"agents": {
"defaults": {
"sandbox": {
"mode": "non-main",
"scope": "session",
"workspaceAccess": "none",
"docker": {
"image": "openclaw-sandbox:bookworm-slim",
"network": "none",
"memory": "512m",
"cpus": 1
},
"prune": {
"idleHours": 24,
"maxAgeDays": 7
}
}
}
}
}
几个配置项需要重点关注:
| 配置项 | 建议 |
|---|---|
mode | 默认使用 non-main,高风险场景使用 all |
workspaceAccess | 外部输入任务优先用 none 或 ro |
network | 没有明确联网需求时保持 none |
memory / cpus | 给容器资源上限,避免异常任务拖垮主机 |
prune | 自动清理闲置容器,控制磁盘占用 |
常用命令:
openclaw sandbox list # 列出沙箱容器
openclaw sandbox recreate # 强制重建容器
openclaw sandbox explain # 查看当前沙箱配置如何生效
2. Memory:让文件成为真相,让索引成为加速器
Agent 的记忆不能只依赖上下文窗口。上下文会被压缩、截断,也会随着会话结束而丢失。OpenClaw 的记忆系统采用“文件优先,索引辅助”的设计:
| 层次 | 作用 |
|---|---|
| Markdown 文件 | 人类可读、可编辑、可审查,是记忆的真实来源 |
| SQLite 索引 | 机器检索加速 |
| 向量嵌入 | 支持语义搜索 |
| BM25 / FTS | 支持关键词精确匹配 |
2.1 记忆文件布局
默认工作区中的记忆文件大致这样组织:
~/.openclaw/workspace/
├── MEMORY.md
└── memory/
└── YYYY-MM-DD.md
两类文件承担不同角色:
| 文件 | 用途 |
|---|---|
MEMORY.md | 长期记忆,保存稳定决策、用户偏好、重要事实 |
memory/YYYY-MM-DD.md | 每日记忆日志,保存短期上下文、临时笔记、当天事件 |
一个实用规则是:
- “以后长期都要遵守”的内容写进
MEMORY.md; - “当前任务阶段有用”的内容写进当天日志;
- 用户明确说“记住这个”时,应立即持久化。
2.2 搜索结果的数据结构
记忆检索返回的不是整份文件,而是带路径、行号和分数的片段。
type MemorySource = "memory" | "sessions";
type MemorySearchResult = {
path: string; // 文件路径
startLine: number; // 起始行号
endLine: number; // 结束行号
score: number; // 相关性得分
snippet: string; // 文本片段
source: MemorySource; // memory 或 sessions
citation?: string; // 引用标注
};
这类结构有两个好处:
- Agent 可以只读取相关片段,减少上下文浪费;
- 人类可以根据路径和行号回到 Markdown 文件里核对来源。
2.3 MemoryIndexManager 的职责
记忆索引管理器负责把 Markdown 文件变成可搜索的索引。它主要处理四件事:
| 职责 | 说明 |
|---|---|
| SQLite 索引管理 | 保存文件、分块、全文索引、向量索引 |
| 混合检索 | 同时执行向量搜索和关键词搜索 |
| 文件同步 | 文件变化后自动更新索引 |
| Embedding 提供商管理 | OpenAI、Gemini、本地模型等 |
索引不是记忆本身。Markdown 文件才是事实来源,索引只是为了让检索更快、更准。
2.4 混合搜索:向量搜索 + BM25
单纯依赖向量搜索会漏掉一些精确 token,例如错误码、提交哈希、函数名、配置键。单纯依赖关键词搜索又不擅长处理同义表达。OpenClaw 因此采用混合搜索。
flowchart TD
Q[用户查询] --> A[关键词提取]
Q --> B[生成查询向量]
A --> C[BM25 / FTS 搜索]
B --> D[向量相似度搜索]
C --> E[分数归一化]
D --> E
E --> F[加权融合]
F --> G[时间衰减]
G --> H[MMR 去重]
H --> R[Top-K 记忆片段]
两种搜索方式的特点如下:
| 搜索方式 | 擅长 | 不擅长 |
|---|---|---|
| 向量搜索 | 语义相似、同义词、模糊表达 | 精确 ID、代码符号、错误字符串 |
| BM25 | 精确 token、函数名、配置键、哈希 | 语义理解、改写表达 |
融合分数可以简化理解为:
finalScore = vectorWeight × vectorScore + textWeight × textScore
默认权重通常偏向语义搜索:
vectorWeight = 0.7
textWeight = 0.3
如果语料里有大量代码、日志、错误码,可以适当提高文本匹配权重。
2.5 MMR 去重:避免返回一堆重复片段
MMR(Maximal Marginal Relevance,最大边际相关性)用于平衡“相关性”和“多样性”。
公式可以写成:
score = λ × relevance - (1 - λ) × max_similarity_to_selected
λ 控制取舍:
| λ | 含义 |
|---|---|
1.0 | 只看相关性,不考虑重复 |
0.0 | 只追求多样性,可能牺牲相关性 |
0.7 | 常见默认值,兼顾相关和去重 |
例如查询“家庭网络设置”时,没做 MMR 可能返回三段都在讲“路由器 + VLAN”。开启 MMR 后,结果可能变成:
| 排名 | 片段主题 | 价值 |
|---|---|---|
| 1 | 路由器 + VLAN | 最高相关 |
| 2 | 网络参考文档 | 提供不同上下文 |
| 3 | AdGuard DNS | 补充相关但不重复的信息 |
这对 Agent 很重要,因为上下文窗口有限,重复片段会挤掉真正有用的信息。
2.6 时间衰减:近期记忆更靠前
每日记忆日志天然带时间属性。OpenClaw 可以对日期型记忆做时间衰减,让近期内容排名更高。
decayedScore = score × e^(-λ × ageInDays)
默认半衰期可以设置为 30 天:
| 时间 | 分数保留比例 |
|---|---|
| 今天 | 100% |
| 7 天 | 约 84% |
| 30 天 | 约 50% |
| 90 天 | 约 12.5% |
但并不是所有文件都应该衰减。类似 MEMORY.md 或 memory/projects.md 这种长期文件,通常属于常青内容,不应因为时间变旧而降权。
2.7 Embedding 提供商选择
OpenClaw 支持多个 embedding 提供商:
| 提供商 | 模型示例 |
|---|---|
| OpenAI | text-embedding-3-small |
| Gemini | gemini-embedding-001 |
| Voyage | voyage-4-large |
| Mistral | mistral-embed |
| Local | 本地 embedding 模型 |
自动选择逻辑可以理解成:
if (local.modelPath exists) return "local";
if (OPENAI_API_KEY exists) return "openai";
if (GEMINI_API_KEY exists) return "gemini";
if (VOYAGE_API_KEY exists) return "voyage";
if (MISTRAL_API_KEY exists) return "mistral";
return "disabled";
对于大型语料,批量 embedding 很关键:
| 能力 | 作用 |
|---|---|
| OpenAI Batch API | 异步生成 embedding,降低成本 |
| Gemini Batch | 使用批处理端点生成向量 |
| 并发控制 | 默认限制并发批处理任务,避免 API 或本机资源打满 |
2.8 SQLite 索引结构
记忆索引数据库可以拆成几张核心表:
CREATE TABLE files (
path TEXT PRIMARY KEY,
source TEXT,
mtime INTEGER,
hash TEXT
);
CREATE TABLE chunks (
id TEXT PRIMARY KEY,
path TEXT,
startLine INTEGER,
endLine INTEGER,
text TEXT,
embedding BLOB,
source TEXT
);
CREATE VIRTUAL TABLE chunks_vec USING vec0(...);
CREATE VIRTUAL TABLE chunks_fts USING fts5(...);
CREATE TABLE embedding_cache (
hash TEXT PRIMARY KEY,
embedding BLOB
);
这里有两个关键点:
chunks存的是分块文本,避免每次检索整份文件;embedding_cache用文本 hash 缓存向量,避免重复计算 embedding。
2.9 索引何时更新
索引更新不是只靠手动命令,而是由多个触发器共同驱动:
| 触发器 | 场景 |
|---|---|
| 会话启动 | 保证 Agent 开始运行前记忆尽量新 |
| 搜索前 | 检索前做一次必要同步 |
| 定时刷新 | 长时间运行时自动更新 |
| 文件监听 | Markdown 文件变化后触发同步,通常带防抖 |
| 会话增量更新 | 会话达到字节数或消息数阈值后异步索引 |
会话索引一般不会阻塞搜索。达到类似 100KB 或 50 条消息的阈值后,系统异步更新索引即可。
2.10 自动记忆刷新
上下文接近压缩时,Agent 容易丢失临时但重要的信息。OpenClaw 支持在压缩前触发一次静默提示,引导模型把持久信息写入记忆。
触发条件可以理解为:
tokenEstimate > contextWindow - reserveTokensFloor - softThresholdTokens
配置示例:
{
"agents": {
"defaults": {
"compaction": {
"reserveTokensFloor": 20000,
"memoryFlush": {
"enabled": true,
"softThresholdTokens": 4000,
"systemPrompt": "Session nearing compaction...",
"prompt": "Write any lasting notes to memory/YYYY-MM-DD.md..."
}
}
}
}
}
这个机制的关键不是让模型多说一句话,而是在上下文被压缩前,把应该长期保留的信息落到文件里。
2.11 记忆系统配置建议
小型语料库可以关闭混合搜索,降低复杂度:
{
"provider": "openai",
"query": {
"hybrid": {
"enabled": false
}
}
}
大型语料库或每日笔记很多时,建议开启混合搜索、MMR 和时间衰减:
{
"provider": "openai",
"remote": {
"batch": {
"enabled": true
}
},
"query": {
"hybrid": {
"enabled": true,
"mmr": {
"enabled": true,
"lambda": 0.7
},
"temporalDecay": {
"enabled": true,
"halfLifeDays": 30
}
}
}
}
完全本地运行可以使用本地 embedding 模型:
{
"provider": "local",
"fallback": "none",
"local": {
"modelPath": "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF"
}
}
常用命令:
openclaw memory status
openclaw memory sync --force
openclaw config get agents.defaults.memorySearch
3. Skills:用 Markdown 扩展 Agent 能力
Skills 是 OpenClaw 的能力扩展系统。一个 Skill 本质上是一份 SKILL.md 文件,里面包含元数据和使用说明。Agent 通过加载这些说明,知道自己在特定任务中应该调用什么工具、遵循什么步骤、依赖什么环境。
这和传统插件不同。Skill 不一定直接注入代码,它更像“可安装、可过滤、可提示注入的能力说明书”。
3.1 Skill 文件结构
一个典型的 SKILL.md 包含 YAML frontmatter 和正文指南:
---
name: github
description: "GitHub operations via `gh` CLI..."
metadata:
openclaw:
emoji: "🐙"
requires:
bins: ["gh"]
install:
- id: brew
kind: brew
formula: gh
bins: ["gh"]
---
# GitHub Skill
Use `gh` CLI for repository, issue, pull request and workflow operations.
frontmatter 描述这个技能的名称、依赖、安装方式和运行条件;正文则告诉 Agent 如何使用它。
3.2 Skills 模块结构
src/agents/skills/
├── types.ts # 类型定义
├── config.ts # 配置解析与过滤
├── workspace.ts # 核心加载逻辑
├── frontmatter.ts # SKILL.md 解析
├── filter.ts # 技能过滤器
├── bundled-dir.ts # 内置技能目录解析
├── bundled-context.ts # 内置技能缓存
├── plugin-skills.ts # 插件技能集成
├── refresh.ts # 文件监听与版本刷新
├── env-overrides.ts # 环境变量注入
├── serialize.ts # 并发控制锁
├── tools-dir.ts # 工具目录路径
└── skills-install.ts # 技能安装器
3.3 加载优先级
OpenClaw 会从多个位置加载 Skill,并按优先级覆盖。
优先级从低到高如下:
extra → bundled → managed → agents-skills-personal → agents-skills-project → workspace
| 来源 | 路径 | 说明 |
|---|---|---|
extra | config.skills.load.extraDirs | 用户额外配置目录 |
bundled | 包内 skills/ | OpenClaw 内置技能 |
managed | ~/.openclaw/skills | 全局管理技能 |
agents-skills-personal | ~/.agents/skills | 个人 Agent 技能 |
agents-skills-project | 项目 .agents/skills | 项目 Agent 技能 |
workspace | 工作区 skills/ | 项目技能,优先级最高 |
这种设计允许内置技能提供默认能力,项目工作区再按需要覆盖或增强。
3.4 Skill 类型模型
可以简化理解为三层:
type SkillEntry = {
skill: Skill;
frontmatter: ParsedSkillFrontmatter;
metadata?: OpenClawSkillMetadata;
invocation?: SkillInvocationPolicy;
};
type OpenClawSkillMetadata = {
always?: boolean;
skillKey?: string;
primaryEnv?: string;
emoji?: string;
os?: string[];
requires?: {
bins?: string[];
env?: string[];
config?: string[];
};
install?: SkillInstallSpec[];
};
type SkillSnapshot = {
prompt: string;
skills: Array<{
name: string;
primaryEnv?: string;
requiredEnv?: string[];
}>;
skillFilter?: string[];
resolvedSkills?: Skill[];
version?: number;
};
其中 SkillSnapshot 很关键。它表示当前运行时最终注入给 Agent 的技能快照,里面包含 prompt、技能列表、过滤结果和版本号。
3.5 技能过滤逻辑
不是目录里存在 SKILL.md 就一定会生效。OpenClaw 会做一系列资格检查:
flowchart TD
A[发现 SKILL.md] --> B[解析 frontmatter]
B --> C{配置是否禁用}
C -- 是 --> X[排除]
C -- 否 --> D{是否在 allowlist}
D -- 否 --> X
D -- 是 --> E{OS 是否兼容}
E -- 否 --> X
E -- 是 --> F{二进制依赖是否满足}
F -- 否 --> X
F -- 是 --> G{环境变量 / 配置是否满足}
G -- 否 --> X
G -- 是 --> H[加入 Skill Snapshot]
检查项包括:
| 检查 | 示例 |
|---|---|
| 配置开关 | config.skills.entries[skillKey].enabled !== false |
| 内置技能白名单 | 只加载允许的 bundled skills |
| OS 兼容性 | 只在指定系统启用 |
| 二进制依赖 | 例如需要 gh、kubectl |
| 环境变量依赖 | 例如需要某个 API key |
| 配置路径依赖 | 某些配置存在时才启用 |
3.6 Skill 安装类型
OpenClaw 支持多种安装来源:
type SkillInstallSpec = {
kind: "brew" | "node" | "go" | "uv" | "download";
formula?: string;
package?: string;
module?: string;
url?: string;
};
这让 Skill 可以声明自己需要的外部工具。例如 GitHub Skill 可以声明需要 gh CLI,系统再根据安装规范进行安装或提示。
3.7 安全机制
Skills 会影响 Agent 的行为,因此加载和安装过程必须做安全控制:
| 安全点 | 说明 |
|---|---|
| 路径安全 | 解析沙箱路径时防止路径穿越 |
| 环境变量安全 | 注入环境变量前过滤危险变量 |
| 安装前扫描 | 检查目录中是否存在明显危险模式 |
| 文件监听版本号 | SKILL.md 变更后递增快照版本,避免旧 prompt 常驻 |
关键 API 包括:
loadWorkspaceSkillEntries(); // 加载技能条目
buildWorkspaceSkillSnapshot(); // 构建技能快照
buildWorkspaceSkillsPrompt(); // 生成 Agent prompt
filterWorkspaceSkillEntries(); // 过滤技能
buildWorkspaceSkillCommandSpecs(); // 生成命令规格
syncSkillsToWorkspace(); // 同步到沙箱
applySkillEnvOverrides(); // 注入环境变量
4. Session:让多渠道、多 Agent 对话可持久化
OpenClaw 的 Session 系统负责管理会话身份、状态、历史文件和投递路由。它要解决的问题不是“保存聊天记录”这么简单,而是要让不同 Agent、不同通道、不同线程、不同任务运行都能映射到稳定的会话。
整体结构可以理解为:
flowchart TD
A[Session Key 身份标识] --> B[Session Entry 会话元数据]
B --> C[Session Store 持久化存储]
C --> D[Transcript File 对话历史文件]
4.1 Session Key 设计
Session Key 的基础格式是:
agent:<agentId>:<rest>
示例:
agent:main:main
agent:ops:work
agent:main:telegram:direct:user123
agent:main:discord:group:guild789
agent:main:cron:daily-backup:run:uuid
agent:main:subagent:child-session
解析过程会先做规范化:
function parseAgentSessionKey(sessionKey: string | undefined | null) {
const raw = (sessionKey ?? "").trim().toLowerCase();
const parts = raw.split(":").filter(Boolean);
if (parts.length < 3 || parts[0] !== "agent") {
return null;
}
return {
agentId: parts[1],
rest: parts.slice(2).join(":")
};
}
所有 key 统一小写可以减少大小写差异造成的重复会话。
4.2 会话类型判断
Session Key 的 rest 部分包含通道和场景信息,系统可以据此判断会话类型。
| 类型 | 识别模式 | 示例 |
|---|---|---|
direct | 包含 :direct: 或 :dm: | agent:main:telegram:direct:user123 |
group | 包含 :group: | agent:main:discord:group:guild789 |
channel | 包含 :channel: | agent:main:slack:channel:C123 |
cron | rest 以 cron: 开头 | agent:main:cron:backup |
subagent | 包含 :subagent: | agent:main:subagent:child |
thread | 包含 :thread: 或 :topic: | agent:main:discord:group:123:thread:456 |
4.3 Session Entry 保存什么
SessionEntry 是会话元数据。它不会直接保存完整对话内容,而是保存会话运行所需的状态。
| 字段组 | 典型字段 | 用途 |
|---|---|---|
| 身份标识 | sessionId、sessionFile | 关联对话历史文件 |
| 模型配置 | model、modelProvider、contextTokens | 记录运行时模型和上下文窗口 |
| Token 统计 | totalTokens、inputTokens、outputTokens | 判断是否需要压缩或刷新记忆 |
| 投递路由 | lastChannel、lastTo、lastThreadId | 确定回复应该发往哪里 |
| 会话状态 | updatedAt、systemSent、abortedLastRun | 判断新鲜度和运行状态 |
| 行为配置 | thinkingLevel、verboseLevel、sendPolicy | 保留用户对会话行为的设置 |
| 群组元数据 | chatType、groupId、space | 支持群聊、频道、工作区 |
| 线程派生 | forkedFromParent、spawnedBy | 追踪派生会话来源 |
| 压缩状态 | compactionCount、memoryFlushAt | 管理上下文压缩和记忆刷新 |
4.4 会话初始化流程
一次入站消息进入 OpenClaw 后,会话初始化大致经过这些步骤:
flowchart TD
A[收到消息] --> B[解析 Agent ID]
B --> C[加载 Session Store]
C --> D{是否触发 /new 或 /reset}
D -- 是 --> E[创建新会话 / 归档旧会话]
D -- 否 --> F[评估会话新鲜度]
F --> G{是否需要线程派生}
G -- 是 --> H[从父会话创建分支]
G -- 否 --> I[复用或创建会话文件]
H --> I
E --> I
I --> J[持久化 Session Entry]
J --> K[触发插件会话钩子]
重置触发器默认包括:
const DEFAULT_RESET_TRIGGERS = ["/new", "/reset"];
系统同时支持带参数的重置:
/new 请帮我重新规划这个任务
这种情况下,/new 触发新会话,后面的文本作为新会话第一条用户消息。
4.5 会话新鲜度策略
不同类型会话的过期时间不同:
type ResetPolicy = {
direct: number;
group: number;
thread: number;
};
const DEFAULT_RESET_HOURS = {
direct: 24,
group: 4,
thread: 24
};
群组会话默认更短,是因为群聊上下文变化更快。一个四小时前的话题,在群里可能已经不适合作为当前上下文。
4.6 线程派生机制
线程会话可能需要继承父会话上下文,但不能无限继承。如果父会话 token 已经过多,继续派生会导致上下文溢出。
const DEFAULT_PARENT_FORK_MAX_TOKENS = 100_000;
派生时会记录父会话文件:
const header = {
type: "session",
version: CURRENT_SESSION_VERSION,
id: sessionId,
parentSession: parentSessionFile
};
这样既能追溯来源,又避免所有线程共享同一份庞大的历史。
4.7 Session Store 的存储结构
OpenClaw 支持每个 Agent 独立存储会话:
~/.openclaw/
├── agents/
│ ├── main/
│ │ └── sessions.json
│ ├── ops/
│ │ └── sessions.json
│ └── ...
└── sessions.json
~/.openclaw/sessions.json 主要用于旧版兼容。多 Agent 场景中,Gateway 会加载多个 Agent 的存储文件,并合并成统一视图。
4.8 遗留 key 清理
历史系统可能存在大小写不一致的 key。OpenClaw 会把 canonical key 作为标准,并清理其他大小写变体。
function pruneLegacyStoreKeys(params: {
store: Record<string, unknown>;
canonicalKey: string;
candidates: Iterable<string>;
}) {
const keysToDelete = new Set<string>();
for (const candidate of params.candidates) {
if (candidate !== params.canonicalKey) {
keysToDelete.add(candidate);
}
for (const legacyKey of findStoreKeysIgnoreCase(params.store, candidate)) {
if (legacyKey !== params.canonicalKey) {
keysToDelete.add(legacyKey);
}
}
}
for (const key of keysToDelete) {
delete params.store[key];
}
}
4.9 Cron 会话清理
Cron 任务会产生大量运行会话。如果不清理,磁盘和会话列表都会膨胀。
默认策略类似:
const DEFAULT_RETENTION_MS = 24 * 3_600_000;
const MIN_SWEEP_INTERVAL_MS = 5 * 60_000;
清理器会:
- 检查距离上次清理是否超过最小间隔;
- 遍历 Session Store;
- 删除过期的 cron run session;
- 归档关联的 transcript 文件。
4.10 投递路由
多渠道接入时,Agent 回复不能只依赖当前消息来源。有些内部消息来自 webchat 或系统通道,但真正应该回复的目标可能是 Telegram、Slack 或 Discord。
投递路由会保存这些信息:
deliveryContext: {
channel: update.channel,
to: update.to,
accountId: update.accountId,
threadId: update.threadId
}
当消息来自内部通道时,系统会优先使用已持久化的外部通道;如果没有,再尝试从 Session Key 中解析通道提示。
5. 自进化:通过文件系统改变 Agent 行为
OpenClaw 的自进化不是让模型直接修改自身权重,而是让 Agent 修改工作区里的行为文件、记忆文件和技能文件。下次运行时,系统提示会重新构建,新的文件内容自然进入运行时。
可被 Agent 修改的核心文件包括:
| 文件 | 作用 |
|---|---|
AGENTS.md | 操作指南、工作规则、行为约束 |
SOUL.md | 身份、人设、语气、边界 |
MEMORY.md | 长期记忆、经验、重要决定 |
memory/YYYY-MM-DD.md | 短期上下文、每日记录 |
skills/ | 项目技能扩展 |
运行时提示由多种信息拼接:
flowchart LR
A[AGENTS.md] --> P[动态系统提示]
B[SOUL.md] --> P
C[MEMORY.md] --> P
D[Skills Snapshot] --> P
E[工具列表] --> P
F[运行时环境] --> P
P --> R[Agent 行为]
自进化循环可以表示为:
flowchart LR
U[用户反馈 / 任务结果] --> A[Agent 修改工作区文件]
A --> B[文件持久化]
B --> C[下次运行重新加载]
C --> D[系统提示变化]
D --> E[Agent 行为变化]
E --> U
这种方式的好处是透明。人类可以打开 Markdown 文件检查 Agent 到底记住了什么、规则怎么变了、技能是否被添加。相比把所有状态藏进数据库或模型内部,文件化设计更容易审计和回滚。
需要注意的是,自我修改能力必须配合沙箱、工具策略和文件系统限制使用。否则 Agent 修改规则的能力也可能变成绕过约束的入口。
6. 工作区与 Agent 路由
工作区是 Agent 的文件系统级运行环境,里面放置引导文件、记忆、身份配置和技能目录。默认路径通常是:
~/.openclaw/workspace
多 Agent 或多环境部署时,可以通过 profile 或状态目录切换不同工作区。一个常见模型是:
一个用户 / 一台主机 / 一个 Gateway / 多个 Agent
工作区和路由之间的关系可以理解成:
flowchart TD
G[Gateway] --> K[Session Key]
K --> A{解析 agentId}
A --> M[main Agent]
A --> O[ops Agent]
A --> H[hr Agent]
M --> WM[main 工作区]
O --> WO[ops 工作区]
H --> WH[hr 工作区]
WM --> SM[main sessions.json]
WO --> SO[ops sessions.json]
WH --> SH[hr sessions.json]
这种隔离不是强安全隔离,而是配置和状态层面的隔离。真正的执行隔离仍然要依赖 Sandbox、工具策略和操作系统权限。
7. 安全模型:可信主机 + Gateway 认证 + 沙箱收敛风险
OpenClaw 的安全边界需要明确:它假设本机操作者是可信的,不提供完整的多租户隔离。也就是说,不能把它当成“多个互不信任用户共享执行环境”的安全平台。
更合理的安全模型是:
| 边界 | 作用 |
|---|---|
| 主机权限 | 依赖操作系统用户、文件权限、Docker 权限控制 |
| Gateway 认证 | 控制谁能远程调用 Agent |
| Sandbox | 限制工具执行的影响范围 |
| 工具策略 | 控制 Agent 能调用哪些工具 |
| 工作区限制 | 控制文件读写范围 |
| 配置验证 | 防止危险配置绕过边界 |
7.1 Web 接口不要直接暴露公网
推荐只绑定回环地址:
openclaw gateway run --bind loopback
配置可以写成:
gateway.bind = "loopback"
远程访问建议使用:
| 方案 | 说明 |
|---|---|
| SSH 隧道 | 简单、可控、适合个人和小团队 |
| Tailscale | 适合私有网络访问 |
| 反向代理 + 强认证 | 需要严格配置认证、TLS、访问控制 |
不建议把 Gateway 裸露到公网。
7.2 运行时要求
推荐运行环境:
| 项目 | 要求 |
|---|---|
| Node.js | >= 22.12.0 |
| Docker | 非 root 用户运行 |
| 文件系统 | 最小权限、必要目录可写 |
| 容器 | 只读根文件系统、丢弃能力、限制网络 |
7.3 工具文件系统加固
可以把写入限制在工作区内:
tools.exec.applyPatch.workspaceOnly = true
如果希望更严格,可以限制所有文件工具只能访问工作区:
tools.fs.workspaceOnly = true
这类配置可以降低误操作风险,但也会减少 Agent 能处理的本机任务范围。
7.4 安全问题如何判断
在这种安全模型下,真正有价值的安全问题通常是“边界绕过”,例如:
| 类型 | 示例 |
|---|---|
| 沙箱逃逸 | 容器配置绕过,访问宿主敏感路径 |
| Gateway 认证绕过 | 未授权用户调用远程接口 |
| 工具策略绕过 | 被禁止工具仍可间接调用 |
| 路径穿越 | 读取或写入工作区外文件 |
| 敏感值泄露 | 配置中的 token、password 被日志输出 |
如果一个场景已经假设本机可信用户拥有配置写权限,那么“他可以配置危险工具”本身不等于边界漏洞。关键要看是否突破了系统宣称的限制。
8. 配置管理:分层加载、验证、默认值和审计
OpenClaw 的配置系统集中在 src/config/,它负责从文件、环境变量、include 模块、插件 schema 中组装最终运行时配置。
模块结构如下:
src/config/
├── io.ts # 配置 I/O 主流程
├── paths.ts # 配置路径解析
├── validation.ts # 多层验证
├── zod-schema.ts # Zod Schema
├── defaults.ts # 运行时默认值
├── includes.ts # $include 模块化配置
├── env-substitution.ts # 环境变量替换
└── types.ts # TypeScript 类型聚合
8.1 配置文件格式与路径优先级
配置文件使用 JSON5,支持注释和尾随逗号:
{
"models": {
"providers": {
"anthropic": {
"apiKey": "${ANTHROPIC_API_KEY}" // 运行时替换
}
}
}
}
配置路径解析优先级如下:
| 优先级 | 路径 |
|---|---|
| 1 | OPENCLAW_CONFIG_PATH |
| 2 | OPENCLAW_STATE_DIR/openclaw.json |
| 3 | ~/.openclaw/openclaw.json |
| 4 | ~/.clawdbot/clawdbot.json |
8.2 配置生命周期
配置加载流程可以表示为:
flowchart LR
A[读取 JSON5] --> B[解析 include]
B --> C[环境变量替换]
C --> D[Schema 验证]
D --> E[插件配置验证]
E --> F[应用运行时默认值]
F --> G[路径规范化]
G --> H[缓存配置]
简化后的加载逻辑:
function loadConfig(): OpenClawConfig {
const raw = fs.readFileSync(configPath, "utf-8");
const parsed = json5.parse(raw);
const resolvedIncludes =
resolveConfigIncludesForRead(parsed, configPath, deps);
const { resolvedConfigRaw } =
resolveConfigForRead(resolvedIncludes, deps.env);
const validated =
validateConfigObjectWithPlugins(resolvedConfigRaw);
const cfg = applyModelDefaults(
applyAgentDefaults(validated.config)
);
normalizeConfigPaths(cfg);
return cfg;
}
8.3 $include:拆分大型配置
大型部署里,把所有模型、通道、插件、Agent 都写在一个文件里会很难维护。$include 用于拆分配置:
{
"$include": [
"./models/anthropic.json5",
"./channels/telegram.json5"
],
"gateway": {
"mode": "remote"
}
}
include 解析必须防止路径逃逸:
if (!isPathInside(this.rootDir, normalized)) {
throw new ConfigIncludeError(
`Include path escapes config directory: ${includePath}`,
includePath
);
}
const real = fs.realpathSync(normalized);
if (!isPathInside(this.rootRealDir, real)) {
throw new ConfigIncludeError(
"Include path resolves outside config directory (symlink)",
includePath
);
}
还要限制嵌套深度,例如最多 10 层,避免循环 include 或恶意深嵌套。
8.4 深度合并策略
include 后的配置需要合并。OpenClaw 的合并规则可以简化成:
| 类型 | 合并方式 |
|---|---|
| 数组 | 拼接 |
| 对象 | 递归合并 |
| 基础值 | 后者覆盖前者 |
示例实现:
function deepMerge(target: unknown, source: unknown): unknown {
if (Array.isArray(target) && Array.isArray(source)) {
return [...target, ...source];
}
if (isPlainObject(target) && isPlainObject(source)) {
const result = { ...target };
for (const key of Object.keys(source)) {
result[key] =
key in result
? deepMerge(result[key], source[key])
: source[key];
}
return result;
}
return source;
}
8.5 多层验证框架
配置验证不是只跑一遍 schema,而是分层检查:
validateConfigObjectWithPlugins(raw)
→ validateConfigObjectRaw(raw)
→ findLegacyConfigIssues(raw)
→ findDuplicateAgentDirs(config)
→ validateIdentityAvatar(config)
→ validatePluginSchemas(config)
各层职责如下:
| 验证层 | 作用 |
|---|---|
| 基础 Zod 验证 | 类型、枚举值、结构 |
| 历史配置检查 | 找出废弃字段并给迁移提示 |
| Agent 目录冲突检测 | 避免多个 Agent 指向同一危险路径 |
| Avatar 路径安全 | 避免身份资源路径逃逸 |
| 插件 Schema 验证 | 动态校验插件配置 |
Zod Schema 中还可以做跨字段验证。例如 broadcast 中引用的 agentId 必须存在于 agents.list:
const agentIds = new Set(
cfg.agents?.list?.map((a) => a.id) ?? []
);
for (const [peerId, ids] of Object.entries(cfg.broadcast ?? {})) {
for (const agentId of ids) {
if (!agentIds.has(agentId)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["broadcast", peerId],
message: `Unknown agent id "${agentId}"`
});
}
}
}
8.6 环境变量双阶段处理
配置读取时会把 ${VAR} 替换成环境变量值:
function resolveConfigForRead(resolvedIncludes, env) {
applyConfigEnvVars(resolvedIncludes, env);
return {
resolvedConfigRaw: resolveConfigEnvVars(resolvedIncludes, env),
envSnapshotForRestore: { ...env }
};
}
写回配置时则尽量恢复 ${VAR} 引用,而不是把真实密钥写进配置文件:
cfgToWrite = restoreEnvVarRefs(
cfgToWrite,
parsedRes.parsed,
envForRestore
);
这个设计能避免 API key、token 等敏感值被意外持久化。
8.7 默认值只在运行时生效
OpenClaw 的默认值不应该污染用户配置文件。加载时应用默认值,写入时剥离默认值。
典型默认值包括:
| 范围 | 默认值 |
|---|---|
| Model | contextWindow: 200K tokens |
| Model | maxTokens: min(8192, contextWindow) |
| Model cost | input/output/cacheRead/cacheWrite 默认为 0 |
| Anthropic API | 默认 anthropic-messages |
| Agent | maxConcurrent: 10 |
| Subagents | maxConcurrent: 5 |
| Session | maintenance.mode: "warn" |
| Session | maintenance.pruneAfter: "30d" |
8.8 配置缓存与审计
配置加载有短期缓存,默认 TTL 约 200ms:
const DEFAULT_CONFIG_CACHE_MS = 200;
缓存可以减少频繁读取和解析配置的成本。诊断场景可以禁用:
OPENCLAW_DISABLE_CONFIG_CACHE=1
配置写入时会记录审计日志,包含:
| 字段 | 作用 |
|---|---|
previousHash / nextHash | 判断配置内容是否变化 |
previousBytes / nextBytes | 检测异常大小变化 |
changedPathCount | 变化路径数量 |
suspicious | 标记可疑行为 |
pid / ppid / cwd / argv | 追踪写入来源 |
可疑行为示例:
size-drop
missing-meta-before-write
gateway-mode-removed
8.9 配置安全细节
配置系统还包含一些底层安全措施。
原型污染防护
const BLOCKED_OBJECT_KEYS = new Set([
"__proto__",
"constructor",
"prototype"
]);
function isBlockedObjectKey(key: string): boolean {
return BLOCKED_OBJECT_KEYS.has(key);
}
敏感值脱敏
Schema 可以标记敏感字段,例如 token、password、apiKey。输出日志或诊断信息时,这些值会被替换成:
[REDACTED]
文件权限
配置目录和文件采用仅所有者可访问的权限:
await fs.promises.mkdir(dir, {
recursive: true,
mode: 0o700
});
await fs.promises.writeFile(tmp, json, {
encoding: "utf-8",
mode: 0o600
});
9. 企业级 Agent 的架构演进方向
OpenClaw 的很多设计都指向企业级 Agent 的核心问题:数据要留在自己手里,执行要可控,能力要能扩展,操作要能审计。
9.1 控制平面与执行节点解耦
企业环境里,控制和执行往往不应该放在同一个位置。
flowchart LR
C[中央控制平面] --> P[权限审核]
C --> R[任务路由]
C --> CFG[配置同步]
R --> N1[员工终端执行节点]
R --> N2[内部服务器执行节点]
R --> N3[专用硬件执行节点]
N1 --> D1[本地数据]
N2 --> D2[内网系统]
N3 --> D3[设备能力]
中央控制平面负责策略、权限、配置和路由;执行节点靠近数据和设备,减少敏感数据外流。
9.2 多 Agent 协作网络
企业里很难有一个 Agent 处理所有业务。更现实的结构是多个专业 Agent 协作:
| Agent | 典型职责 |
|---|---|
| HR Agent | 员工政策、假期、入职流程 |
| 财务 Agent | 报销、预算、发票校验 |
| IT 运维 Agent | 账号、权限、故障排查 |
| 法务 Agent | 合同条款、合规检查 |
| 数据分析 Agent | 报表、指标解释、数据查询 |
它们需要通过标准化协议传递任务,而不是互相复制上下文。
9.3 更细粒度的权限与硬件集成
企业 Agent 可能接入会议室设备、PDA、扫码枪、摄像头、门禁或生产设备。这时必须引入更细的 TCC(Transparency、Consent、Control,透明度、同意和控制)策略。
敏感操作应至少包含:
| 控制点 | 示例 |
|---|---|
| 透明度 | 告知将调用哪个设备或系统 |
| 同意 | 用户或管理员确认 |
| 控制 | 可撤销、可限制、可审计 |
| 审计 | 记录调用者、时间、参数和结果 |
9.4 全渠道业务集成
Agent 不应该只停留在单独聊天窗口里。企业场景更需要把 Agent 嵌入 Slack、Discord、飞书、钉钉、邮件、工单、CRM(客户关系管理)和 ERP(企业资源计划)系统中。
目标不是“能聊天”,而是让对话直接触发业务动作:
sequenceDiagram
participant User as 员工
participant Chat as 协作工具
participant Agent as 企业 Agent
participant Biz as 业务系统
User->>Chat: 申请审批 / 查询订单
Chat->>Agent: 投递消息
Agent->>Biz: 调用审批或订单接口
Biz-->>Agent: 返回结果
Agent-->>Chat: 回复处理状态
9.5 基于沙箱的生产环境执行
当 Agent 处理外部客户输入、不可信文件或动态脚本时,隔离环境会成为基础设施。
合适的策略是:
| 输入类型 | 执行环境 |
|---|---|
| 内部可信指令 | 可使用受限主机工具 |
| 外部客户输入 | 临时沙箱 |
| 未知脚本 | 无网络、只读根文件系统沙箱 |
| 需要访问业务系统的任务 | 带最小权限 token 的受控环境 |
9.6 企业私有 Skill Hub
企业可以建立私有技能中心,由 IT 或安全团队审核 Skill,再允许员工或 Agent 安装。
flowchart LR
D[开发者提交 Skill] --> R[安全审核]
R --> H[企业 Skill Hub]
H --> A[Agent 按需安装]
A --> W[工作区加载 SKILL.md]
W --> P[运行时提示注入]
这种方式可以把能力扩展从“个人随便安装脚本”变成“企业统一审核、按需分发、可追踪版本”的流程。
10. OpenClaw 架构的核心取舍
OpenClaw 的设计可以归纳为几条清晰的工程取舍:
| 设计 | 取舍 |
|---|---|
| 文件化记忆 | 人类可读、可审计,但需要索引系统提升检索效率 |
| Docker 沙箱 | 降低工具误操作风险,但不能替代完整多租户隔离 |
| Skills Markdown 化 | 扩展简单透明,但必须做好依赖检查和安全扫描 |
| Session Key 规范化 | 多渠道路由稳定,但需要清理历史 key |
| 配置分层加载 | 适合复杂部署,但验证和错误提示必须足够严格 |
| Local-first | 数据所有权更清晰,但本机权限和 Gateway 暴露需要谨慎管理 |
这套架构的关键不是单个模块多复杂,而是每个模块都围绕 Agent 运行时的真实问题建立边界:工具执行要隔离,记忆要持久,能力要可扩展,会话要可路由,配置要可验证,安全假设要说清楚。对于企业级智能体来说,这些边界比单次模型回答质量更重要,因为它们决定了 Agent 能否长期、稳定、可控地接入真实业务流程。