概述
在通过Agent Basic和LangGraph的学习,就可以通过看真实的系统来学习生产级的Coding Agent
那么在这篇文章中主要通过阅读真实场景的源码,去理解生产级的Coding Agent的工程架构,在此架构的理论基础上去构建一个属于我自己的Agent
在本文章的核心: 主流的Agent框架,例如LangChain、Dify、Coze在Demo阶段很好用,但是上升到了真实的生产环境中,他的行为是不可预测的,问题排查困难,安全面大
对于那些真正运行在生产环境中的Coding Agent:Claude 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
是用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的本质循环,Agent和ChatBot的区别就只有一个:当模型返回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都会消费同一个EventStream,Agent逻辑不需要知道渲染细节- 可观测性:每个时间都是结构化数据,天然就支持日志、
trace、metrics - 可恢复:事件序列就是
transcript,崩溃后从transcript恢复
核心循环的数据流
前面说过Agent的本质就是一个循环
关键设计:
- 外层循环 处理多轮对话
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-momo的Agent 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定义了4个hook介入点,覆盖执行全流程:
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 OpenClaw的Agent Loop
根据claude-code-vs-openclaw的11纬度对比,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-momo的Agent 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的设计决策
-
为什么用
EventStream而不是直接返回结果生产级的
Agent在单次任务的执行可能需要耗费几分钟甚至更长的时间,如果等待所有任务执行完毕后再返回结果,用户体验就会很差,无响应,而用EventStream可以有实时的视觉反馈,可以让UI实时渲染中间状态------正在思考、正在读文件、正在执行命令,每一步都有视觉反馈 -
为什么默认并行执行工具
Coding Agent的典型操作(读文件、grep搜索)是IO密集型的,互相独立的,没有数据依赖。并行执行可以将延迟O(N)降低到O(1)。只有当工具间有显式依赖(写文件->读同一个文件)时才需要串行
-
Agent和CahrBot的本质区别是什么在tool_calls的时候是直接返回结果呢还是继续往后循环
一行代码的区别:
if (response.toolCalls?.length) continueChatBot收到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-Qwen2或BGE-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)
}
常用Reranker:bge-reranker-v2-m3、cohere-reranker-v3
精排可以将Top-5精度提升10-20%,但增加约200ms延迟
在Agent中集成的方式
在Coding Agent中RAG通常不是独立的检索步骤,而是作为工具被模型按需调用:
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')
}
}
模型自己决定什么时候搜索代码库,而不是每次都强制检索
OpenClaw的RAG特点
OpenClaw没有在核心机构中内置RAG------他把RAG当作可选插件来使用(可插拨式),因为:
Coding Agent的主要操作是读写文件(read工具带offset/limit),大多数情况下精确路径+grep就够了- 只有在处理大规模知识库(文档库、工单库)时才需要向量检索
RAG质量高度一粒分块策略和Embedding选型,不适合做通用默认的方案
但是GBrain(OpenClaw的外部记忆宿主)提供了完整的RAG能力
- Postgres + pgvector 混合检索
- "Compiled Truth + Timeline" 模式------每个知识页有当前理解 + 追加式证据链
- 自动知识图谱:提取实体引用和类型化链接
- "Dream Cycle" 夜间合成:定期整理、聚合、丰富知识
面试题
-
RAG和Fine-tuning什么时候选哪个维度 RAG Fine-tuning 知识更新 实时(改文档即生效) 需要重新训练 幻觉控制 有来源可追溯 无法保证 成本 推理时增加检索开销 训练成本高,推理不增加 适用场景 知识库问答、文档检索 风格/格式/推理模式固化 -
向量检索的
Top-K设多少合适取决于 Reranker 和上下文窗口。经验值:粗召回 K=20,精排后取 Top-5 注入 Prompt。K 太大会引入噪声,太小可能漏掉相关内容。
-
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的结果,例如先Write再read同一个文件验证,即下一步工具的执行是手动上一步工具执行结果的影响的 - 需要严格的副作用顺序,如:先创建目录再写文件
但这种情况在实际中很少,模型通常会在不同轮次分开强求有依赖的工具,所以一般是不会顺序执行的
MCP : Model Context Protocol
MCP是Anthropic在2024年提出的跨进程工具通信标准。核心思想:工具不必在Agent进程内,可以独立服务
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 }
OpenClaw的MCP集成
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的能力包
Skill是OpenClaw特有的概念------比单个工具负责,比独立的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的加载机制:
Agent启动时扫描Skills目录,注册所有可用Skill- 用户输入命中
trigger时,动态加载对应Skill的prompt和工具 Skill执行完毕后卸载,不污染Agent上下文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 开始,逐步授权提升。这比"全部允许或全部拒绝"更精细。
面试题
-
为什么
Coding Agent的工具不能太多工具数量是模型决策空间的维度。8 个工具的选择空间是 8^n(n 为步骤数),20 个工具是 20^n。决策空间指数增长导致模型更容易选错工具或生成无效的参数组合。实验数据:从 20+ 削减到 8 个,任务完成率提升 ~15%
-
MCP相比直接函数调用的优劣优势:语言无关(Go 写的 MCP Server 可被 TypeScript Agent 调用)、进程隔离(工具崩溃不影响 Agent)、可复用(社区共享)。劣势:序列化开销(JSON-RPC)、进程管理复杂度、调试困难(跨进程调用链)。
-
如何设计一个安全的
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原生格式 :md是LLM最擅长读写的格式
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:标识符保留策略
OpenClaw的Compaction不是简单的用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:外部记忆宿主(高级)
GBrain是OpenClaw的生产级外部记忆系统,解决了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 Truth是Agent直接使用的结论
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
GBrain用Postgres原生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 份报告
OpenClaw的SubAgent的实现
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 的工具调用记录污染