芥末
发布于 2026-03-26 / 0 阅读
0
0

OpenClaw Agent 运行时架构:沙箱、记忆、技能、会话与配置管理

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:把工具执行放进受控环境

大模型调用工具时,风险不在“模型会说什么”,而在“工具真的会做什么”。如果 execwriteedit 这类工具直接在主机上执行,模型一次错误判断就可能改错文件、删除目录,甚至访问不该访问的系统资源。

OpenClaw 的 Sandbox 是一个 Docker 隔离层,用来承接 Agent 的工具操作。它不是为了让不可信租户共享同一台机器,而是为了缩小单个 Agent 执行失误时的影响范围。

1.1 沙箱负责解决什么问题

Sandbox 的职责可以归纳为三点:

目标说明
限制工具边界execreadwriteedit 等操作不直接落到主机环境
缩小破坏范围模型执行异常命令时,影响被限制在容器或指定工作区内
支持隔离级别配置不同会话、不同 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外部输入任务优先用 nonero
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;      // 引用标注
};

这类结构有两个好处:

  1. Agent 可以只读取相关片段,减少上下文浪费;
  2. 人类可以根据路径和行号回到 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网络参考文档提供不同上下文
3AdGuard DNS补充相关但不重复的信息

这对 Agent 很重要,因为上下文窗口有限,重复片段会挤掉真正有用的信息。

2.6 时间衰减:近期记忆更靠前

每日记忆日志天然带时间属性。OpenClaw 可以对日期型记忆做时间衰减,让近期内容排名更高。

decayedScore = score × e^(-λ × ageInDays)

默认半衰期可以设置为 30 天:

时间分数保留比例
今天100%
7 天约 84%
30 天约 50%
90 天约 12.5%

但并不是所有文件都应该衰减。类似 MEMORY.mdmemory/projects.md 这种长期文件,通常属于常青内容,不应因为时间变旧而降权。

2.7 Embedding 提供商选择

OpenClaw 支持多个 embedding 提供商:

提供商模型示例
OpenAItext-embedding-3-small
Geminigemini-embedding-001
Voyagevoyage-4-large
Mistralmistral-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
);

这里有两个关键点:

  1. chunks 存的是分块文本,避免每次检索整份文件;
  2. embedding_cache 用文本 hash 缓存向量,避免重复计算 embedding。

2.9 索引何时更新

索引更新不是只靠手动命令,而是由多个触发器共同驱动:

触发器场景
会话启动保证 Agent 开始运行前记忆尽量新
搜索前检索前做一次必要同步
定时刷新长时间运行时自动更新
文件监听Markdown 文件变化后触发同步,通常带防抖
会话增量更新会话达到字节数或消息数阈值后异步索引

会话索引一般不会阻塞搜索。达到类似 100KB50 条消息的阈值后,系统异步更新索引即可。

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
来源路径说明
extraconfig.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 兼容性只在指定系统启用
二进制依赖例如需要 ghkubectl
环境变量依赖例如需要某个 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
cronrestcron: 开头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 是会话元数据。它不会直接保存完整对话内容,而是保存会话运行所需的状态。

字段组典型字段用途
身份标识sessionIdsessionFile关联对话历史文件
模型配置modelmodelProvidercontextTokens记录运行时模型和上下文窗口
Token 统计totalTokensinputTokensoutputTokens判断是否需要压缩或刷新记忆
投递路由lastChannellastTolastThreadId确定回复应该发往哪里
会话状态updatedAtsystemSentabortedLastRun判断新鲜度和运行状态
行为配置thinkingLevelverboseLevelsendPolicy保留用户对会话行为的设置
群组元数据chatTypegroupIdspace支持群聊、频道、工作区
线程派生forkedFromParentspawnedBy追踪派生会话来源
压缩状态compactionCountmemoryFlushAt管理上下文压缩和记忆刷新

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;

清理器会:

  1. 检查距离上次清理是否超过最小间隔;
  2. 遍历 Session Store;
  3. 删除过期的 cron run session;
  4. 归档关联的 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}" // 运行时替换
      }
    }
  }
}

配置路径解析优先级如下:

优先级路径
1OPENCLAW_CONFIG_PATH
2OPENCLAW_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 的默认值不应该污染用户配置文件。加载时应用默认值,写入时剥离默认值。

典型默认值包括:

范围默认值
ModelcontextWindow: 200K tokens
ModelmaxTokens: min(8192, contextWindow)
Model costinput/output/cacheRead/cacheWrite 默认为 0
Anthropic API默认 anthropic-messages
AgentmaxConcurrent: 10
SubagentsmaxConcurrent: 5
Sessionmaintenance.mode: "warn"
Sessionmaintenance.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 能否长期、稳定、可控地接入真实业务流程。


评论