OpenClaw Agent

概述

在通过Agent BasicLangGraph的学习,就可以通过看真实的系统来学习生产级的Coding Agent

那么在这篇文章中主要通过阅读真实场景的源码,去理解生产级的Coding Agent的工程架构,在此架构的理论基础上去构建一个属于我自己的Agent

在本文章的核心: 主流的Agent框架,例如LangChain、Dify、CozeDemo阶段很好用,但是上升到了真实的生产环境中,他的行为是不可预测的,问题排查困难,安全面大

对于那些真正运行在生产环境中的Coding AgentClaude code、cursor、pi-momo用的都是自定义的轻量化的架构,它们共同的模式:

  • EventStream驱动的Agent Loop,而不是链式调用,而且用LangGraph也不会用链式调用
  • 可插拨式的Context Engine,不是直接进行简单的消息拦截
  • 并行工具执行+MCP协议,不是串行函数调用
  • 文件级持久化记忆,不是向量数据库对话

那么下面就针对这几个层面进行详细的讲解


为什么要写自己的Agent

明明已经有了现成的Agent框架,例如LangChain、Dify、Coze,而且在Demo阶段也很顺利运行

但是一旦上升到真实的生产环境中就会出问题:

  • 行为不稳定,但debug时全是框架内部的堆栈
  • 想修改一个细节,发现需要翻三层抽象
  • 上了生产遇到的安全漏洞,依赖链太长修不动

这不是因为你使用框架用得不好,而是框架本身就有问题,是框架架构导致的


那么真正跑在生产环境中的Coding Agent用什么呢

目前,Claude code 、Cursor、pi-momo是公认最好的Coding Agent,它们都没有使用LangChain DIfy Coze这些流行的框架

而是根据他们自定义的架构模式来实现:

特征 框架方案 生产级方案
执行模型 链式调用 / RAG EventStream / Agent Loop
上下文管理 直接截断或摘要 可插拨式Context Engine
工具调用 串行 + 同步 并行(promise.all) + MCP
记忆 向量数据库存对话 文件级持久化(MEMORY.md)
语言 python TypeSript(性能 + 类型安全)

pi-momo:开源的生产级的Coding Agent

pi-momo

是用TypeScript实现的,大约3000行核心代码,他的架构直接影响了Claude Code的设计思路

objectivec 复制代码
packages/
  agent/         ← Agent Loop + EventStream 生命周期
  ai/           ← 多 Provider LLM API(OpenAI / Anthropic / Google / Bedrock)
  coding-agent/ ← 交互式编码 Agent CLI
  tui/          ← 终端差分渲染 UI
  web-ui/       ← Web 聊天组件

OpenClaw是在pi-momo的基础上进行完善的平台

pi-momo的基础上加上了企业级特性:

css 复制代码
src/
  memory/          ← MEMORY.md 文件持久化
  context-engine/  ← 可插拔上下文组装/压缩
  sessions/        ← Session 生命周期管理
  agents/          ← Agent 运行时、沙箱、Skills
  tools/           ← 工具执行、安全策略
  mcp/             ← MCP 协议桥接
  channels/        ← 20+ 消息平台集成
  gateway/         ← API 网关
  hooks/           ← 事件钩子系统
  plugins/         ← 插件 SDK
  security/        ← SSRF 策略、命令授权、密钥管理

核心能力差异

能力 pi-mono OpenClaw
Agent Loop ✅(继承)
持久化记忆 MEMORY.md + Context Engine
多渠道接入 ✅ 20+ 平台
沙箱安全 ✅ Docker / SSH / OpenShell
插件系统 ✅ ClawHub Skills Registry

Agent的本质:就是一个循环

不管使用什么框架,用什么框架进行包装,所有的Agent的核心结构都是用同一个循环:

typescript 复制代码
// 伪代码,简化自 pi-mono agent-loop.ts
async function agentLoop(messages: Message[]): AsyncGenerator<Event> {
  while (true) {
    // 1. 调用 LLM
    const response = await llm.chat(messages)
    yield { type: 'message_end', content: response }

    // 2. 如果没有 tool_calls,结束
    if (!response.toolCalls?.length) break

    // 3. 并行执行所有工具
    const results = await Promise.all(
      response.toolCalls.map(call => executeTool(call))
    )
    yield { type: 'tool_execution_end', results }

    // 4. 把工具结果追加到 messages,继续循环
    messages.push(...results.map(toMessage))
  }
}

这就是Agent的本质循环,AgentChatBot的区别就只有一个:当模型返回tools_calls时,Agent会执行工具并继续循环,而Cahtbot就直接结束了


学习路径

如果你只是想快速开发,写一个Demo,不关系底层原理的话就不需要来读源码

如果你想理解Claude code / Cursor这类产品的底层原理,并且开发部署一个真正可控的Agent,就可以学下面给出的资源

lua 复制代码
阅读 pi-mono 源码(TypeScript)
    ↓
理解 Agent Loop / EventStream 模式
    ↓
理解 Context Engine / Memory 架构
    ↓
Fork pi-mono,接入你的 LLM
    ↓
部署为你自己的 [YourName]Claw
资源 类型 内容
OpenClaw-Internals 源码拆解 最深入的中文架构分析(WebSocket 协议、Agent Loop、安全)
build-your-own-openclaw 动手教程 18 步 Python 实现(从 0 到 OpenClaw 克隆)
how-to-build-a-coding-agent 动手教程 Go 语言 6 阶段 Workshop
AI-Coding-Guide-Zh 对比指南 Claude Code + OpenClaw + Codex 三合一(39 篇教程)
openclaw-architecture-analysis 可视化 D3.js 交互式架构演进图(15,000+ commits)
claude-code-vs-openclaw 对比分析 11 维度机制对比

Agent Loop : EventStream驱动的核心循环

pi-momo的核心在pacjages/agent/src/agent-loop.ts,这个文件实现了整个Agent的执行引擎

读懂这个文件就理解了所有生产级Coding Agent的运行原理了


两个入口

typescript 复制代码
// packages/agent/src/agent-loop.ts

//全新对话
export function agentLoop(config : AgentConfig) : EventStream{...}

// 恢复已有对话,从上次中断出继续执行
export function agentLoopContinue(config : AgentConfig, transcript : Event[]):EventStream{...}

agentLoop()启动新对话

agentLoopContinue()从历史的transcript恢复

两者返回同一种类型:EventStream------一个异步时间生成器


EventStream架构

pi-momo不像传统框架那样返回最终结果,而是流式发射生命周期时间:

typescript 复制代码
type Event =
  | { type: 'agent_start' }
  | { type: 'turn_start' }
  | { type: 'message_start', role: 'assistant' }
  | { type: 'message_update', delta: string }
  | { type: 'message_end', content: Message }
  | { type: 'tool_execution_start', toolCall: ToolCall }
  | { type: 'tool_execution_update', delta: string }
  | { type: 'tool_execution_end', result: ToolResult }
  | { type: 'turn_end' }
  | { type: 'agent_end' }

调用方(CLI、Web UI、测试)通过消费这个EventStream来驱动UI渲染、日志记录、进度展示

为什么要这样设计

  • UI解耦:TUI Web、UI、测试harness都会消费同一个EventStreamAgent逻辑不需要知道渲染细节
  • 可观测性:每个时间都是结构化数据,天然就支持日志、trace、metrics
  • 可恢复:事件序列就是transcript,崩溃后从transcript恢复

核心循环的数据流

前面说过Agent的本质就是一个循环

graph TD A[Agent Loop启动] --> B[outer loop:等待用户消息] B --> C[inner Loop : 调用LLM] C --> D{是否有tool_calls} D -->|是| E[并行执行工具] D -->|否| F[emit message_end] E --> G[emit tool_execution_end] G --> H[工具执行结果追加到messages] H --> C F --> I{是否需要follow_up} I -->|否| J[emit agent_loop] I -->|是| B

关键设计:

  • 外层循环 处理多轮对话follow_up messsages
  • 内层循环处理单轮中的多次工具调用
  • 工具执行默认并行 promise.all,可配置为顺序执行

工具执行:并行是默认promise.all

工具的执行默认是并行的,但是你也可以自己去配置成顺序执行

typescript 复制代码
// 简化自 agent-loop.ts
const toolResults = await Promise.all(
  response.toolCalls.map(async (toolCall) => {
    yield { type: 'tool_execution_start', toolCall }

    const result = await executeTool(toolCall, config)

    yield { type: 'tool_execution_end', result }
    return result
  })
)

当模型一次返回多个tool_calls时(比如同时读3个文件),pi-momo会并行执行所有的工具,这是Coding Agent速度快的关键原因之一

对比LangChain的默认串行执行:

模式 3 个工具各 2 秒 总耗时
串行 2 + 2 + 2 6 秒
并行 max(2, 2, 2) 2 秒

并行执行的优势很明显


Hooks:拦截与变换

pi-momo提供了四个hook点,让你在不修改核心循环的情况下注入逻辑:

typescript 复制代码
interface AgentHooks {
  beforeToolCall?: (toolCall: ToolCall) => ToolCall | null  // 拦截/修改工具调用
  afterToolCall?: (result: ToolResult) => ToolResult        // 修改工具结果
  transformContext?: (messages: Message[]) => Message[]      // 发送给 LLM 前变换上下文
  convertToLlm?: (message: Message) => LlmMessage          // 自定义消息格式转换
}

实际用途

  • beforeToolCall:安全过滤,阻止危险命令、权限控制
  • afterToolCall:结果截断,大文件只保留前N
  • transformContext:上下文压缩、注入系统治疗
  • convertToLlm:适配不同的LLM Provider的消息格式

OpenClaw的五阶段执行模型

pi-momoAgent Loop是基础,OpenClaw在此基础上增加了更完整的执行阶段:

yaml 复制代码
Stage 1: RPC Validation
    → 验证请求格式、权限检查、速率限制
Stage 2: Skill Loading
    → 根据用户输入动态加载匹配的 Skill(SKILL.md)
Stage 3: Pi-Agent Runtime
    → 核心 Agent Loop(即 pi-mono 的 agentLoop)
Stage 4: Event Bridging
    → 把 EventStream 事件桥接到具体渠道(Slack/飞书/Web)
Stage 5: Persistence
    → JSONL transcript 持久化 + MEMORY.md 更新

hook介入点

OpenClaw定义了4hook介入点,覆盖执行全流程:

typescript 复制代码
interface OpenClawHooks {
  before_model_resolve: (req: ModelRequest) => ModelRequest
  // 可以动态切换模型(如简单问题用便宜模型)

  before_prompt_build: (context: ContextState) => ContextState
  // 在 assemble() 之前修改上下文状态

  before_tool_call: (call: ToolCall) => ToolCall | null
  // 拦截、修改或阻止工具调用(安全策略的主要入口)

  before_agent_reply: (reply: AgentReply) => AgentReply
  // 在回复发送给用户之前做后处理(脱敏、格式化)
}

并发控制:per-session串行化

typescript 复制代码
// OpenClaw 用文件级写锁保证同一 session 不会并发执行
const lock = await acquireFileLock(`/tmp/openclaw-session-${sessionId}.lock`)
try {
  // 同一 session 的请求排队执行,不会并发
  await runAgentLoop(session)
} finally {
  await lock.release()
}

如果同一个用户同时发送两条消息,两个Agent Loop并发执行会导致:

  • 消息顺序混乱,到底先执行哪条消息
  • 上下文竞态,两个循环同时追加消息
  • 工具冲突,两个循环同时写同一个文件

多层超时

typescript 复制代码
timeouts:
  waitForInput: 30_000      // 30 秒等待用户输入
  maxRuntime: 172_800_000   // 48 小时最大运行时间
  idleWatchdog: 300_000     // 5 分钟无活动自动暂停
  toolExecution: 120_000    // 单个工具最多 2 分钟

Claude Code vs OpenClawAgent Loop

根据claude-code-vs-openclaw11纬度对比,OpenClaw更有优势

维度 Claude Code OpenClaw 胜者
Context Compaction LLM 摘要(无验证) 标识符保留 + 质量检查点 + 重试 OpenClaw
Context Pruning 基于 token 计数 基于 promptAuthority 标志 + 语义重要性 OpenClaw
Memory System CLAUDE.md(单文件) MEMORY.md + Daily Notes + DREAMS.md(三层) OpenClaw
Agent Isolation SubAgent(同进程) 独立 workspace + 文件锁 OpenClaw
Tool Safety 命令黑名单 分层工具调度 + 沙箱 + Ed25519 签名 OpenClaw
Cache Optimization Prompt Caching(Anthropic 专有) N/A Claude Code
Frustration Detection 检测用户沮丧并调整行为 Claude Code

与传统框架对比

LangChain的链式模型

python 复制代码
# LangChain: 每个步骤是一个 "chain",线性组合
chain = prompt | llm | output_parser
result = chain.invoke({"question": "..."})

如果你需要工具调用循环时,链式模型是不够用的,需要引入AgentExecutor,其内部实现和pi-momoAgent Loop几乎一样

pi-momo的循环模型

typescript 复制代码
// pi-mono: 一个循环就是整个 Agent
while (hasToolCalls) {
  results = await executeTools(toolCalls)
  messages.push(...results)
  response = await llm.chat(messages)
}

没有链、没有DAG、没有中间抽象层、一个while就解决了所有问题


面试高频题:Agent Loop的设计决策

  1. 为什么用EventStream而不是直接返回结果

    生产级的Agent在单次任务的执行可能需要耗费几分钟甚至更长的时间,如果等待所有任务执行完毕后再返回结果,用户体验就会很差,无响应,而用EventStream可以有实时的视觉反馈,可以让UI实时渲染中间状态------正在思考、正在读文件、正在执行命令,每一步都有视觉反馈

  2. 为什么默认并行执行工具

    Coding Agent的典型操作(读文件、grep搜索)是IO密集型的,互相独立的,没有数据依赖。并行执行可以将延迟O(N)降低到O(1)。只有当工具间有显式依赖(写文件->读同一个文件)时才需要串行

  3. AgentCahrBot的本质区别是什么

    在tool_calls的时候是直接返回结果呢还是继续往后循环

    一行代码的区别:if (response.toolCalls?.length) continue

    ChatBot收到LLM回复就会结束执行,Agent检测到tool_calls后继续循环------执行工具、把结果追加到上下文、再次调用LLM,这个循环持续到模型不再强求工具为止


动手跟踪一次完整执行过程

pi-momo克隆到自己本地上,然后在agent-loop.ts的关键位置加console.log

bash 复制代码
git clone https://github.com/badlogic/pi-mono
cd pi-mono

观察一次:读取README.md并总结任务的事件顺序

typescript 复制代码
agent_start
turn_start
message_start (role: assistant)
message_update (delta: "让我读取...")
tool_execution_start (name: "read", args: {path: "README.md"})
tool_execution_end (result: "# pi-mono\n...")
message_start (role: assistant)
message_update (delta: "这个项目是...")
message_end
turn_end
agent_end

把这个事件流画成时序图,你就理解了整个执行模型了


RAG检索增强的工程实现

RAG(Retrieval-Augmented Generation)在Agent系统中有两种用法:

  • 知识库问答:用户提问->检索相关文档->注入上下文->生成回答
  • 代码库导航Agent需要理解大型代码库时,按需检索相关代码片段

这节讲解:从基础实现到生产级优化的完整路径


核心管线

css 复制代码
文档 → 分块 → 编码(Embedding)→ 写入向量库
                                         ↓
用户查询 → 编码 → 向量检索 Top-K → Rerank → 注入 Prompt → LLM 生成

每个环节的选择都会直接影响最终效果


分块策略

按语义边界分块(推荐)

typescript 复制代码
// 代码文件:按函数/类边界切分
function chunkByAST(code: string, language: string): Chunk[] {
  const tree = parser.parse(code, language)
  return tree.rootNode.children
    .filter(node => ['function', 'class', 'method'].includes(node.type))
    .map(node => ({
      content: node.text,
      metadata: { type: node.type, name: node.name, startLine: node.startPosition.row }
    }))
}

按固定窗口分块(简单场景)

typescript 复制代码
function chunkByWindow(text: string, size = 512, overlap = 64): string[] {
  const chunks: string[] = []
  for (let i = 0; i < text.length; i += size - overlap) {
    chunks.push(text.slice(i, i + size))
  }
  return chunks
}

面试考察 为什么需要overlap

因为语意可能跨越切分边界,overlap保证边界处的信息不丢失


Embedding选型

模型 维度 特点
text-embedding-3-small (OpenAI) 1536 通用能力强,需代理
BGE-M3 (BAAI) 1024 中文优秀,开源可部署
GTE-Qwen2 (阿里) 768 代码理解能力强
Cohere embed-v3 1024 支持搜索/分类/聚类多任务

Coding Agent场景推荐使用GTE-Qwen2BGE-M3------代码和中文都表现好,且可本地部署避免网络延迟


向量数据库选择

工具 适用场景 特点
ChromaDB 本地开发、原型验证 Python 嵌入式,零配置
pgvector 已有 PostgreSQL 无需新增服务,事务一致性
Milvus 大规模生产(亿级) 分布式,高吞吐
Qdrant 中等规模生产 Rust 实现,单机性能好

混合检索:稠密 + 稀疏

单纯的向量检索有盲区------对精确关键词匹配(函数名、变量名)效果差,生产系统通常用混合检索:

typescript 复制代码
// 伪代码:混合检索 + RRF 融合
async function hybridSearch(query: string, k: number): Promise<Chunk[]> {
  // 稠密检索:语义相似
  const denseResults = await vectorDB.search(embed(query), k * 2)

  // 稀疏检索:关键词匹配(BM25)
  const sparseResults = await bm25Index.search(query, k * 2)

  // Reciprocal Rank Fusion 融合排序
  return reciprocalRankFusion(denseResults, sparseResults, k)
}

function reciprocalRankFusion(lists: Result[][], k: number): Result[] {
  const scores = new Map<string, number>()
  const RRF_K = 60 // 常数,控制排名衰减速度

  for (const list of lists) {
    list.forEach((item, rank) => {
      const score = 1 / (RRF_K + rank + 1)
      scores.set(item.id, (scores.get(item.id) || 0) + score)
    })
  }

  return [...scores.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, k)
    .map(([id]) => getChunkById(id))
}

GBrain(OpenClaw的生产记忆系统)就使用Postgres + pgvector + BM25 + RRF实现的混合检索,P@5到达49.1%R@5达到97.9%


Reranker精排提升精度

粗召回Top-K后,用Cross-Encoder做精排:

typescript 复制代码
async function rerankResults(query: string, chunks: Chunk[], topN: number): Promise<Chunk[]> {
  const scored = await Promise.all(
    chunks.map(async chunk => ({
      chunk,
      score: await crossEncoder.score(query, chunk.content)
    }))
  )
  return scored.sort((a, b) => b.score - a.score).slice(0, topN)
}

常用Rerankerbge-reranker-v2-m3cohere-reranker-v3

精排可以将Top-5精度提升10-20%,但增加约200ms延迟


Agent中集成的方式

Coding AgentRAG通常不是独立的检索步骤,而是作为工具被模型按需调用:

typescript 复制代码
const searchCodeTool = {
  name: 'search_codebase',
  description: '在代码库中搜索与查询语义相关的代码片段',
  parameters: {
    query: { type: 'string', description: '搜索查询' },
    k: { type: 'number', description: '返回结果数量', default: 5 }
  },
  execute: async ({ query, k }) => {
    const results = await hybridSearch(query, k)
    return results.map(r => `${r.metadata.path}:${r.metadata.startLine}\n${r.content}`).join('\n---\n')
  }
}

模型自己决定什么时候搜索代码库,而不是每次都强制检索


OpenClawRAG特点

OpenClaw没有在核心机构中内置RAG------他把RAG当作可选插件来使用(可插拨式),因为:

  • Coding Agent的主要操作是读写文件(read工具带offset/limit),大多数情况下精确路径+ grep就够了
  • 只有在处理大规模知识库(文档库、工单库)时才需要向量检索
  • RAG质量高度一粒分块策略和Embedding选型,不适合做通用默认的方案

但是GBrain(OpenClaw的外部记忆宿主)提供了完整的RAG能力

  • Postgres + pgvector 混合检索
  • "Compiled Truth + Timeline" 模式------每个知识页有当前理解 + 追加式证据链
  • 自动知识图谱:提取实体引用和类型化链接
  • "Dream Cycle" 夜间合成:定期整理、聚合、丰富知识

面试题

  1. RAGFine-tuning什么时候选哪个

    维度 RAG Fine-tuning
    知识更新 实时(改文档即生效) 需要重新训练
    幻觉控制 有来源可追溯 无法保证
    成本 推理时增加检索开销 训练成本高,推理不增加
    适用场景 知识库问答、文档检索 风格/格式/推理模式固化
  2. 向量检索的Top-K设多少合适

    取决于 Reranker 和上下文窗口。经验值:粗召回 K=20,精排后取 Top-5 注入 Prompt。K 太大会引入噪声,太小可能漏掉相关内容。

  3. Embedding模型和生成模型用同一个可以吗

    不推荐。Embedding 模型是专门训练的双塔/对比学习模型,生成模型的隐状态不适合做相似度检索。用专用 Embedding 模型效果显著更好。


工具系统:MCP协议和并行协议

Agent的能力边界是由工具决定的:pi-momo / OpenClaw的工具系统有三个层次:

  • 内置工具:本地函数,进程内执行
  • MCP Server:跨进程通信,标准协议
  • SKills:结构化能力,可组合

内置工具:少即是多

pi-momo的内置工具的数量刻意控制得很少:

工具 功能 关键设计
Read 读文件 支持 offset/limit(只读需要的行)
Write 写文件 整文件覆写
Edit 精确替换 old_string → new_string,失败时报错
Bash 执行 shell 超时控制 + 输出截断
Grep 正则搜索 返回匹配行 + 上下文
Find 文件查找 Glob 模式匹配
WebFetch HTTP 请求 SSRF 防护
Agent 子 Agent 上下文隔离的子任务

工具越多模型在做选择的时候消耗的推理能力越多,有职责重叠的工具会导致模型在选的时候犹豫或误选,bash本身就是万能工具,很多操作都能在shell完成

原则:能用Bash解决的,就不要单独做工具


工具定义:TypeScript Schema

pi-momo中每个工具的定义格式:

typescript 复制代码
// packages/agent/src/tools/read.ts
export const readTool: ToolDefinition = {
  name: 'Read',
  description: '读取文件内容。支持 offset 和 limit 参数读取大文件的部分内容。',
  parameters: {
    type: 'object',
    properties: {
      file_path: { type: 'string', description: '文件的绝对路径' },
      offset: { type: 'integer', description: '起始行号(可选)' },
      limit: { type: 'integer', description: '读取行数(可选)' }
    },
    required: ['file_path']
  },
  execute: async (args: { file_path: string; offset?: number; limit?: number }) => {
    const content = await fs.readFile(args.file_path, 'utf-8')
    const lines = content.split('\n')
    const start = args.offset || 0
    const end = args.limit ? start + args.limit : lines.length
    return lines.slice(start, end).map((l, i) => `${start + i + 1}\t${l}`).join('\n')
  }
}

核心点:

  • description:是给LLM看到,写得越清晰模型越能正确使用
  • parameters:用JSON Schema格式,LLM输出结构化参数
  • execute:是实际执行函数,返回字符串结果

并行执行:默认行为

当模型一次强求多个工具时,pi-momo默认并行执行:

typescript 复制代码
// agent-loop.ts 中的工具执行逻辑
async function executeToolCalls(toolCalls: ToolCall[]): Promise<ToolResult[]> {
  if (config.sequentialTools) {
    // 顺序模式:有依赖关系时使用
    const results: ToolResult[] = []
    for (const call of toolCalls) {
      results.push(await executeSingle(call))
    }
    return results
  }

  // 默认:并行执行
  return Promise.all(toolCalls.map(call => executeSingle(call)))
}

什么时候需要顺序执行呢

  • 工具A依赖工具B的结果,例如先Writeread同一个文件验证,即下一步工具的执行是手动上一步工具执行结果的影响的
  • 需要严格的副作用顺序,如:先创建目录再写文件

但这种情况在实际中很少,模型通常会在不同轮次分开强求有依赖的工具,所以一般是不会顺序执行的


MCP : Model Context Protocol

MCPAnthropic在2024年提出的跨进程工具通信标准。核心思想:工具不必在Agent进程内,可以独立服务

graph TD A[Agent进程] -->|JSON-RPC over stdio| B[MCP Server A github 操作] A -->|JSON-RPC over HTTP| C[MCP Server B数据库查询] A -->|JSON-RPC over stdio| D[MCP Server C 文件系统]

MCP的通信协议

typescript 复制代码
// Agent → MCP Server: 请求工具列表
{ "jsonrpc": "2.0", "method": "tools/list", "id": 1 }

// MCP Server → Agent: 返回工具定义
{ "jsonrpc": "2.0", "result": { "tools": [...] }, "id": 1 }

// Agent → MCP Server: 调用工具
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "query", "arguments": {...} }, "id": 2 }

// MCP Server → Agent: 返回结果
{ "jsonrpc": "2.0", "result": { "content": [...] }, "id": 2 }

OpenClawMCP集成

typescript 复制代码
// src/mcp/channel-bridge.ts
// OpenClaw 把每个 MCP Server 当作一个 "channel",统一管理生命周期
export class McpChannelBridge {
  private servers: Map<string, McpServerProcess> = new Map()

  async connectServer(config: McpServerConfig): Promise<void> {
    const process = spawn(config.command, config.args)
    const transport = new StdioTransport(process.stdin, process.stdout)
    this.servers.set(config.name, { process, transport })
  }

  async listTools(): Promise<ToolDefinition[]> {
    const allTools: ToolDefinition[] = []
    for (const [name, server] of this.servers) {
      const { tools } = await server.transport.request('tools/list')
      allTools.push(...tools.map(t => ({ ...t, server: name })))
    }
    return allTools
  }
}

什么时候使用MCP,什么时候用内置工具

场景 选择 原因
读写本地文件 内置工具 无需跨进程开销
查询数据库 MCP Server 独立部署,可复用
GitHub API 操作 MCP Server 社区已有成熟实现
简单文本处理 Bash 工具 一行命令搞定
复杂多步流程 Skill 内部有子流程

Skills : OpenClaw的能力包

SkillOpenClaw特有的概念------比单个工具负责,比独立的Agent轻量:

typescript 复制代码
src/agents/skills/
  code-review/
    manifest.json    ← 技能描述、触发条件
    prompt.md        ← 技能专用的系统指令
    tools.ts         ← 技能专属工具(可选)
  security-audit/
    manifest.json
    prompt.md
typescript 复制代码
// manifest.json
{
  "name": "code-review",
  "description": "审查代码变更,检查安全问题和最佳实践",
  "triggers": ["review", "审查", "看看这段代码"],
  "requiredTools": ["Read", "Grep", "Bash"]
}

Skill.md标准格式

pi-momo生态(pi-skills, 1.6k stars)定义了跨平台Skii格式------一个SKILL.md文件兼容ClaudeCode、OpenClaw、COdex CLI、Amp、Droid等多个Agent

typescript 复制代码
<!-- SKILL.md -->
---
name: security-review
description: 审查代码变更中的安全漏洞
triggers:
  - "review security"
  - "安全审查"
  - "check vulnerabilities"
tools_required:
  - Read
  - Grep
  - Bash
---

# Security Review Skill

你是安全审计专家。审查用户指定的代码文件,检查以下类别的问题:
1. 注入攻击(SQL、命令、XSS)
2. 认证/授权缺陷
3. 敏感信息泄露
4. 不安全的依赖

输出格式:
- 严重程度(Critical/High/Medium/Low)
- 位置(文件:行号)
- 问题描述
- 修复建议

skill的加载机制:

  1. Agent启动时扫描Skills目录,注册所有可用Skill
  2. 用户输入命中trigger时,动态加载对应Skillprompt和工具
  3. Skill执行完毕后卸载,不污染 Agent上下文
  4. SKILL.md格式跨Agent通用------写一次,多处运行

这就是 Claude Code 中 /review/init 等斜杠命令的实现原理。

Skill生态

OpenClaw 的 ClawHub 注册了 1,800+ 社区 Skill,覆盖:

  • 代码质量(lint、review、refactor)
  • DevOps(deploy、monitor、rollback)
  • 数据分析(SQL 生成、可视化)
  • 文档(API 文档生成、翻译)

安全策略

OpenClaw对工具执行有多层安全防护

typescript 复制代码
// src/security/command-auth.ts
export class CommandAuthorizer {
  private allowList: RegExp[] = []
  private denyList: RegExp[] = [
    /rm\s+-rf\s+\//,         // 禁止 rm -rf /
    /sudo/,                   // 禁止 sudo
    /curl.*\|.*sh/,          // 禁止管道执行远程脚本
    /chmod\s+777/,           // 禁止全权限
  ]

  authorize(command: string): AuthResult {
    for (const pattern of this.denyList) {
      if (pattern.test(command)) {
        return { allowed: false, reason: `Blocked by security policy: ${pattern}` }
      }
    }
    return { allowed: true }
  }
typescript 复制代码
// src/security/ssrf-policy.ts
// 防止工具访问内网地址
export function validateUrl(url: string): boolean {
  const parsed = new URL(url)
  const ip = await dns.resolve(parsed.hostname)
  return !isPrivateIP(ip) // 拒绝 10.x / 172.16.x / 192.168.x
}

生成环境还会加沙箱Docker / SSH,把工具执行隔离在容器内

分局安全模型(来自安全审计)

OpenClaw 的安全不是单一黑名单,而是四层防御

yaml 复制代码
Layer 1: 命令级过滤(正则黑名单)
Layer 2: Per-channel Ed25519 身份验证(每个渠道独立密钥)
Layer 3: 分层工具调度(不同权限级别可用不同工具子集)
Layer 4: 硬化容器(cap_drop ALL, read-only rootfs, 64MB tmpfs)




// 工具权限分级
toolPermissions:
  level_0:  ['Read', 'Grep', 'Find']           // 只读,无风险
  level_1:  ['Read', 'Grep', 'Find', 'Edit', 'Write']  // 可修改文件
  level_2:  ['Read', 'Grep', 'Find', 'Edit', 'Write', 'Bash']  // 可执行命令
  level_3:  ['*']               

用户首次使用时从 level_0 开始,逐步授权提升。这比"全部允许或全部拒绝"更精细。


面试题

  1. 为什么Coding Agent的工具不能太多

    工具数量是模型决策空间的维度。8 个工具的选择空间是 8^n(n 为步骤数),20 个工具是 20^n。决策空间指数增长导致模型更容易选错工具或生成无效的参数组合。实验数据:从 20+ 削减到 8 个,任务完成率提升 ~15%

  2. MCP相比直接函数调用的优劣

    优势:语言无关(Go 写的 MCP Server 可被 TypeScript Agent 调用)、进程隔离(工具崩溃不影响 Agent)、可复用(社区共享)。劣势:序列化开销(JSON-RPC)、进程管理复杂度、调试困难(跨进程调用链)。

  3. 如何设计一个安全的Bash工具

    三层防护:1) 命令黑名单(正则匹配危险模式);2) 超时控制(防止无限循环);3) 输出截断(防止大输出撑爆上下文)。生产环境额外加 Docker 沙箱隔离文件系统。


Context Engine :Openclaw记忆架构

Agent的记忆不是一个简单的消息列表,OpenClaw的记忆系统分为三层:

复制代码
持久化记忆(MEMORY.md)  ← 跨 Session 存活,人类可编辑
Context Engine           ← 每轮动态组装上下文,token 预算内最大化信息量
Session 管理             ← 会话生命周期、transcript 持久化

三个层次各自解决不同的问题,组合起来就是一个完整的生产级的记忆系统


第一层:MEMORY.md------文件级持久化

OpenClaw使用md文件作为跨session记忆的载体:

typescript 复制代码
// src/memory/root-memory-files.ts
export const MEMORY_FILE_NAMES = ['MEMORY.md', 'memory.md'] // canonical + legacy

export function findMemoryFile(workspacePath: string): string | null {
  for (const name of MEMORY_FILE_NAMES) {
    const fullPath = path.join(workspacePath, name)
    if (fs.existsSync(fullPath)) return fullPath
  }
  return null
}

MEMORY.md的设计哲学

为什么是用md文件而不是用数据库

  • 可读可编辑:用户可以直接打开文件查看、修改、删除记忆
  • GIT友好:可以版本控制,diff、回滚
  • 零依赖 :不需要额外的服务Redis 、Postgres,文件系统就够了,不会依赖外部服务
  • LLM原生格式mdLLM最擅长读写的格式

memory.md的结构

scss 复制代码
- [用户偏好](user_preferences.md) --- 喜欢简洁回复,代码注释用英文
- [项目架构](project_arch.md) --- monorepo,pnpm workspace,TypeScript
- [禁止操作](forbidden.md) --- 不允许 git push,不删除 .env

每条记忆是一个独立的 .md 文件,MEMORY.md 是索引。这样:

  • 单条记忆可以独立更新/删除
  • 索引文件保持简短,不超出上下文预算
  • Agent 按需读取具体记忆文件

记忆的写入时机

arduino 复制代码
// Agent 在以下时机写入记忆:
// 1. 用户显式要求 "记住这个"
// 2. 用户纠正了 Agent 的行为(feedback 类型)
// 3. 发现了非显而易见的项目规则
// 4. 用户角色/偏好信息

// 记忆不应该存储的:
// - 代码结构(读文件就能得到)
// - Git 历史(git log 就能查到)
// - 临时任务状态(用 task list 跟踪)

第二层:Context Engine ------可插拨的上下文管理

这是OpenClaw的记忆系统的核心,Context Engine负责在每次LLM调用前,在Token预算内组装最优的上下文

接口定义

typescript 复制代码
// src/context-engine/index.ts
export interface ContextEngine {
  // 初始化:加载 MEMORY.md、系统指令等
  bootstrap(config: EngineConfig): Promise<void>

  // 摄入新内容(用户消息、工具结果)
  ingest(event: ContextEvent): Promise<void>

  // 核心方法:组装发送给 LLM 的完整 messages
  assemble(budget: TokenBudget): Promise<Message[]>

  // 压缩:当上下文接近预算上限时触发
  compact(): Promise<void>

  // 每轮结束后的维护(更新摘要、清理过期内容)
  maintain(): Promise<void>

  // 生命周期钩子
  afterTurn(turnResult: TurnResult): Promise<void>
}

assemble()上下文组装的核心

assemble()是整个记忆系统中最关键的方法,它要在有限的token预算内,决定那些信息进入上下文,选出最优的方案:

typescript 复制代码
async assemble(budget: TokenBudget): Promise<Message[]> {
  const messages: Message[] = []
  let tokensUsed = 0

  // 1. 系统指令(最高优先级,必须包含)
  const systemPrompt = this.buildSystemPrompt()
  messages.push({ role: 'system', content: systemPrompt })
  tokensUsed += countTokens(systemPrompt)

  // 2. MEMORY.md 索引(持久化记忆)
  const memoryContent = await this.loadMemoryIndex()
  if (memoryContent) {
    messages[0].content += `\n\n# Memory\n${memoryContent}`
    tokensUsed += countTokens(memoryContent)
  }

  // 3. 早期对话的压缩摘要(如果有)
  if (this.compressedSummary) {
    messages.push({ role: 'assistant', content: `[Earlier context]\n${this.compressedSummary}` })
    tokensUsed += countTokens(this.compressedSummary)
  }

  // 4. 近期对话(从最新往前填充,直到预算用完)
  const recentMessages = this.transcript.slice().reverse()
  const fittingMessages: Message[] = []

  for (const msg of recentMessages) {
    const msgTokens = countTokens(msg.content)
    if (tokensUsed + msgTokens > budget.maxTokens) break
    fittingMessages.unshift(msg)
    tokensUsed += msgTokens
  }

  messages.push(...fittingMessages)
  return messages
}

优先从高到底:系统指令 > 持久化记忆 > 压缩摘要 > 近期对话。预算不够时,从低优先级开始裁剪


compact()上下文压缩

当对话历史接近token预算时触发压缩:

typescript 复制代码
async compact(): Promise<void> {
  // 取出需要压缩的早期消息
  const cutoff = this.transcript.length - this.keepRecentCount
  const toCompress = this.transcript.slice(0, cutoff)

  // 用 LLM 生成摘要
  const summary = await this.llm.chat([
    { role: 'system', content: 'Summarize the key decisions, findings, and context from this conversation. Preserve actionable information.' },
    ...toCompress
  ])

  // 替换为摘要
  this.compressedSummary = summary.content
  this.transcript = this.transcript.slice(cutoff)
}

压缩 和 截断

方案 做法 问题
截断 直接丢弃最早的消息 可能丢失关键决策和约束
压缩 用 LLM 生成摘要替代原始消息 保留语义,代价是一次 LLM 调用
OpenClaw 方案 压缩 + 持久化关键信息到 MEMORY.md 重要信息永不丢失

可插拨式架构

typescript 复制代码
// src/context-engine/registry.ts
// 进程全局单例注册表,支持切换不同的 Context Engine 实现
class ContextEngineRegistry {
  private engines: Map<string, ContextEngineFactory> = new Map()
  private activeEngine: string = 'default'

  register(name: string, factory: ContextEngineFactory): void {
    this.engines.set(name, factory)
  }

  getActive(): ContextEngine {
    return this.engines.get(this.activeEngine)!.create()
  }
}

// 配置文件中选择策略
// config.plugins.slots.contextEngine = "legacy" | "semantic" | "custom"

做成可插拨式可以满足不同的业务场景所需要的上下文策略:

  • 短对话不需要压缩,直接全量传入
  • 长任务场景,激进压缩,只保留最近几轮+任务计划
  • RAG场景:每轮动态注入检索结果,压缩早期检索内容

Dreaming系统:记忆的自动整理

OpenClaw独有的Dreaming机制------受人类睡眠中记忆巩固的启发,通过定时任务自动整理和丰富长期记忆

三阶段合成

阶段 对应睡眠 做什么
Light Sleep 浅睡眠 扫描当日对话,提取候选记忆片段
Deep Sleep 深睡眠 合并相似主题、修复引用、消除矛盾
REM 快速眼动 跨主题关联、生成新的 Compiled Truth

记忆评分公式

每条候选记忆通过加权评分决定是否被提升为长期记忆:

typescript 复制代码
Score = frequency(0.24) + relevance(0.30) + query_diversity(0.15)
      + recency(0.15) + consolidation(0.10) + conceptual_richness(0.06)
维度 权重 含义
relevance 0.30 与用户核心工作的相关度
frequency 0.24 被提及/使用的频率
query_diversity 0.15 在不同类型查询中出现
recency 0.15 最近的信息权重更高
consolidation 0.10 已经被其他记忆引用的次数
conceptual_richness 0.06 包含的概念复杂度

只有有据可查的记忆片段grounded snippets才有资格被提升------防止幻觉记忆进入长期存储

运行机制

typescript 复制代码
// 默认 cron 配置:每天凌晨 3 点运行
dreaming:
  schedule: "0 3 * * *"
  phases: [light, deep, rem]
  maxDuration: 1800  // 最多运行 30 分钟

输出写入DREAMS.md------Agent的工偶日记:

markdown 复制代码
<!-- DREAMS.md -->
## 2026-05-09 Consolidation

### Promoted to long-term
- 用户偏好 TypeScript monorepo 结构 (score: 0.87)
- 项目禁止 git push 到 remote (score: 0.82)

### Merged
- "API 架构偏好" + "微服务选型" → 合并为 "后端架构偏好"

### Retired
- "调试 ESLint 配置" --- 问题已解决,不再相关

面试价值

Dreaming 系统体现了一个关键认知:记忆不是只写不删的日志,而是需要主动维护的知识库。面试中提到这个机制,能展示你对"记忆质量 > 记忆数量"这个生产级认知的理解。


Compaction:标识符保留策略

OpenClawCompaction不是简单的用LLM总结------他是有一套精确的规则确保压缩后的上下文还能用:

typescript 复制代码
// 压缩时的标识符保留策略
compaction:
  identifierPreservation: "strict"  // 默认严格模式
  // strict: 文件路径、函数名、变量名、行号必须原样保留
  // relaxed: 只保留文件路径和函数名
  // none: 不做特殊保留(不推荐)

为什么需要标识符保留

typescript 复制代码
// 不保留标识符的压缩结果:
"之前分析了认证模块,发现了几个安全问题。"
→ Agent 无法继续工作(哪个文件?哪个函数?哪一行?)

// 保留标识符的压缩结果:
"分析了 src/auth/login.ts:42-78 的 validateToken() 函数,
发现 JWT 验证缺少 exp 字段检查(第 56 行)。"
→ Agent 可以直接定位并继续操作

Compaction还支持双模式

  • 自动模式token使用率超过阈值时会自动触发
  • 手动模式 :用户显式请求(如Claude Code的/compact命令)

压缩前会执行auto-flush------把关键信息写入MEMORY.md,确保压缩不会导致知识丢失


记忆后端:可插拨存储

OpenClaw支持多种记忆后端

后端 适用场景 特点
SQLite(默认) 个人使用、本地部署 零配置、单文件、够用
LanceDB 需要向量检索 嵌入式向量库,无服务
Honcho 多用户、大规模 专为 AI Agent 设计的记忆服务
GBrain 生产级、企业级 Postgres + pgvector,功能最全

第三层:Session管理

typescript 复制代码
// src/sessions/index.ts
export interface Session {
  id: string                    // 唯一标识
  createdAt: Date
  lastActiveAt: Date
  transcript: TranscriptEvent[] // 完整事件记录
  config: SessionConfig         // 模型、级别等覆盖配置
}

Session管理解决的问题:

  • 多用户隔离:不同用户的会话互不干扰
  • 断点续传agentLoopContinue()transcript恢复
  • 审计追踪:所有交互都有记录

GBrain:外部记忆宿主(高级)

GBrainOpenClaw的生产级外部记忆系统,解决了MOMORY.md无法处理的大规模记忆场景

架构

markdown 复制代码
Postgres + pgvector
    ├── 混合检索(向量 + BM25 + RRF 融合)
    ├── Compiled Truth 页(当前理解,可被更新)
    ├── Timeline 条目(追加式证据链,不可变)
    └── 知识图谱(实体引用 + 类型化链接)

Compiled Truth + Timeline模式

传统做法:每次新信息来了就追加一条记忆,新信息越多,记忆就越多,就会导致检索越来越困难

GBrain的做法:

yaml 复制代码
Page: "用户的技术栈偏好"
├── Compiled Truth: "偏好 TypeScript + pnpm,讨厌 Python 类型系统"
└── Timeline:
    ├── 2025-03-01: 用户说 "我主要写 TypeScript"
    ├── 2025-03-15: 用户说 "Python 的类型注解太弱了"
    └── 2025-04-02: 用户在项目中使用了 pnpm workspace

Compiled TruthAgent直接使用的结论

Timeline是支撑这个结论的原始证据,当新信息到来时,更新Compiled Truth


Dream Cycle夜间合成

GBrain 运行定时任务进行知识整理:

  • 合并相似主题的 Pages
  • 检测过时信息并标记
  • 从 Timeline 中提取新的 Compiled Truth

GBrain生产数据

GBrain在实际生产环境中的规模(Garry Tan的个人使用):

diff 复制代码
- 17,888 个知识页面
- 4,383 个人物实体
- 723 个公司实体
- P@5: 49.1%(前 5 结果中有正确答案的概率)
- R@5: 97.9%(正确答案出现在前 5 的概率)

Minions Job Queue

GBrainPostgres原生Job Queue(Minions替代了不稳定的sub-agent方式做后台任务:

php 复制代码
// 传统方式:spawn sub-agent 做异步任务
// 问题:进程崩溃丢失状态、无法重试、无法观测

// GBrain 方式:Postgres 持久化 Job Queue
await minions.enqueue({
  type: 'consolidate_memory',
  payload: { pageId: 'tech-preferences', newEvidence: '...' },
  retries: 3,
  timeout: 60_000
})

好处crash-safe、可重试、可观测、有事务保证。

设计原则:确定性操作优先于 LLM 判断

GBrain 在能用确定性逻辑的地方绝不用 LLM

  • 实体抽取:正则 + 规则,不用 NER 模型
  • 关系链接:模式匹配(works_at、invested_in),不用 LLM 推理
  • 知识图谱连边:基于共现和明确语法结构,不用向量相似度

LLM 只在必须推理时使用(如 Compiled Truth 的更新、Deep Sleep 阶段的冲突消解)。


对比:各家Agent的记忆方案

Agent 短期记忆 长期记忆 跨 Session
ChatGPT 消息列表 Memory 功能(自然语言)
Claude Code 消息列表 + compact CLAUDE.md + MEMORY.md
pi-mono 消息列表
OpenClaw Context Engine MEMORY.md + GBrain
LangChain ConversationBufferMemory 向量数据库 需自建

面试高频题

Q:Agent 的"记忆"和"上下文"有什么区别?

上下文是单次 LLM 调用时传入的 messages------有 token 上限,会话结束就消失。记忆是跨会话持久化的信息------用户偏好、项目规则、关键决策。Context Engine 的工作就是把合适的记忆加载进当前上下文。

Q:为什么 OpenClaw 用文件(MEMORY.md)而不是数据库存记忆?

三个原因:1) 人类可直接查看和编辑(透明性);2) Git 版本控制(可追溯);3) 零依赖(不需要额外服务)。对大规模记忆场景(数千条),再引入 GBrain(Postgres)。

Q:Context Engine 的 assemble() 方法为什么重要?

它解决了 Agent 的核心难题:token 预算有限,但需要最大化上下文信息量。assemble() 的优先级策略决定了 Agent "记住什么、忘记什么"------这直接影响任务完成质量。好的 assemble 策略 = 好的 Agent。

Q:压缩摘要的弊端是什么?怎么缓解?

弊端:摘要会丢失细节(具体代码行号、精确数字)。缓解方案:1) 把关键信息持久化到 MEMORY.md(不依赖摘要);2) 压缩时用 LLM 判断哪些细节必须保留;3) 保留最近 N 轮完整消息不压缩(近期信息最重要)。


Multi-Agent:子进程隔离与多渠道路由

OpenClaw的多Agent架构有两个纬度:

  • SubAgent:通过主Agent派生子Agent处理独立子任务(上下文隔离)
  • Multi-Channel:同一个Agent通过20+消息平台接入(统一路由)

SbuAgent:上下文隔离的正确姿势

为什么需要SubAgent

Agent去处理复杂任务的时候,上下文会被中间过程污染

复制代码
任务:审查 3 个模块的安全问题

单 Agent 做法:
  读 auth.py(500 行进入上下文)
  → 分析 auth.py(LLM 输出 200 token)
  → 读 api.py(800 行进入上下文)
  → 分析 api.py(上下文已经很满了)
  → 读 db.py(上下文溢出,早期分析被压缩/截断)
  → 最终报告质量很差(前面的分析已经丢了)

SubAgent做法:每个模块由独立的子Agent处理,各自维护独立上下文

复制代码
主 Agent:拆任务 + 收结果
  ├── SubAgent 1:审查 auth.py(独立上下文)→ 返回报告
  ├── SubAgent 2:审查 api.py(独立上下文)→ 返回报告
  └── SubAgent 3:审查 db.py(独立上下文)→ 返回报告
主 Agent:合并 3 份报告

OpenClawSubAgent的实现

typescript 复制代码
// src/context-engine/index.ts
export interface ContextEngine {
  // SubAgent 生命周期
  prepareSubagentSpawn(task: SubagentTask): SpawnConfig
  onSubagentEnded(result: SubagentResult): void
}
typescript 复制代码
// 主 Agent 中派生子 Agent
const subagentResult = await spawnSubagent({
  task: '审查 src/auth.py 的安全问题,返回发现的漏洞列表',
  tools: ['Read', 'Grep', 'Bash'],  // 子 Agent 可用的工具子集
  budget: { maxTokens: 32000 },       // 独立的 token 预算
  systemPrompt: '你是安全审计专家...'  // 可以有专用指令
})

// subagentResult 只包含最终输出,不包含中间过程
// 主 Agent 的上下文不会被子 Agent 的工具调用记录污染
相关推荐
AskHarries2 小时前
为什么大多数人创业第一步就错了
人工智能·后端
tyung2 小时前
Go 手写二叉堆优先队列:避开 container/heap 的性能陷阱
数据结构·后端·go
Nirvana在掘金2 小时前
MySQL 事务隔离级别 锁 高并发场景优化经验
后端·mysql
李小狼lee2 小时前
《spring如此简单》第二节--IOC思想的实现,容器是什么
后端·面试
GetcharZp2 小时前
深入浅出 etcd:从 K8s 灵魂到 Golang 实战,分布式系统的“定海神针”!
后端
hikktn3 小时前
企业级Spring Boot应用管理:从零打造生产级启动脚本
java·spring boot·后端
SimonKing3 小时前
别再死磕 Elasticsearch 了,这个轻量级搜索引擎更香
java·后端·程序员
Gopher_HBo3 小时前
阻塞队列之ArrayBlockingQueue
后端