13 AI记忆框架原理与实现(搜索、读取、回退、注入)
这篇回答"记忆系统怎么做才不会拖垮主对话"。
核心源码入口
src/memory/backend-config.tssrc/memory/search-manager.tssrc/memory/manager.tssrc/memory/manager-embedding-ops.tssrc/memory/qmd-manager.tssrc/agents/tools/memory-tool.tssrc/gateway/server-startup-memory.ts
总体结构
OpenClaw 记忆系统 = 配置解析 + 管理器选择 + 工具暴露 + 启动预热 + (可选)Hook注入
模块一 配置解析状态机
resolveMemoryBackendConfig(...) 先决定 backend:
memory.backend != qmd:直接 builtinmemory.backend = qmd:解析 qmd 配置
关键解析项:
searchModecollections/pathsupdate(interval、debounce、commandTimeout、updateTimeout)limits(尤其maxInjectedChars)citations
qmd limits 完整类型:
type QmdLimits = {
maxResults: number; // 最多返回条数
maxSnippetChars: number; // 单条结果最大字符
maxInjectedChars: number; // 注入上下文的总字符上限(默认 4000)
timeoutMs: number; // 搜索超时
};maxInjectedChars 默认 4000,防记忆挤爆上下文。
模块二 管理器选择与回退
getMemorySearchManager 流程
backend=qmd?
├─ 查 QMD_MANAGER_CACHE(key = buildQmdCacheKey(agentId, qmd))
│ 命中 → 直接返回缓存实例
│ 未命中 → QmdMemoryManager.create(...)
│ 成功 → 写缓存 → 包一层 FallbackMemoryManager
│ 失败 → warn → 动态 import MemoryIndexManager(builtin)
└─ backend!=qmd
└─ MemoryIndexManager.get(...)builtin 也失败时:return { manager: null, error },不 throw,主流程继续。
FallbackMemoryManager 完整状态
class FallbackMemoryManager {
primaryFailed: boolean = false; // qmd 是否已失效
fallback: MemorySearchManager | null = null; // 懒加载的 builtin
lastError?: string; // 最后一次 primary 失败原因
cacheEvicted: boolean = false; // 缓存是否已被驱逐
}状态转换:
- 默认
primaryFailed=false,所有请求走 qmd。 - qmd
search抛错 →primaryFailed=true→ 关闭 qmd → 记录lastError。 ensureFallback()懒加载 builtin manager(不是启动时就加载)。- 后续所有请求走 fallback。
close()时:关闭 primary + fallback → 调evictCacheEntry()。
evictCacheEntry(缓存主动失效)
// FallbackMemoryManager 内部
private evictCacheEntry() {
QMD_MANAGER_CACHE.delete(this.cacheKey); // 移除缓存
this.cacheEvicted = true;
}qmd 失败后主动从缓存删除,下次 getMemorySearchManager 调用时会重新尝试创建 qmd, 而不是永远返回已失效的缓存实例。这是"在线自愈"的关键设计。
缓存键:buildQmdCacheKey(agentId, qmd) — 绑定 agentId + qmd 配置内容。
模块三 MemorySearchManager 接口
interface MemorySearchManager {
search: (query: string, opts?: SearchOpts) => Promise<MemoryResult[]>;
readFile: (opts: { relPath: string; from?: number; lines?: number }) => Promise<string>;
status: () => MemoryStatus;
// 可选方法(并非所有 backend 都实现)
sync?: () => Promise<void>;
probeEmbeddingAvailability: () => Promise<boolean>;
probeVectorAvailability: () => Promise<boolean>;
close?: () => Promise<void>;
}probeVectorAvailability 用于检测向量搜索后端是否可用(启动预热和健康检查用)。
模块四 工具层实现细节
memory_search(源码 description)
"Mandatory recall step: semantically search MEMORY.md + memory/*.md...""Mandatory recall step" 是刻意的措辞,强迫模型在回答前必须先做记忆召回。
createMemorySearchTool(...) 流程:
getMemorySearchManager(...)- scope guard:
QmdMemoryManager.search()内部先检查isScopeAllowed(sessionKey), 不在允许范围内的 session 直接返回[] - 等待待处理更新:
waitForPendingUpdateBeforeSearch()— 确保最新写入的记忆被索引后再搜 manager.search(query, {maxResults, minScore, sessionKey})shouldIncludeCitations(...)决定是否加引用decorateCitations(...)格式化来源路径和行号- qmd 模式执行
clampResultsByInjectedChars(..., limits.maxInjectedChars)
返回值结构:
{
results: MemoryResult[]; // 搜索结果列表
provider: string; // 使用的 provider(qmd 时为 "qmd")
model: string; // 使用的 model
fallback: boolean; // 是否使用了 fallback backend
citations: boolean; // 本次是否附带引用
}memory_get(源码 description)
"Safe snippet read from MEMORY.md or memory/*.md with optional from/lines;
use after memory_search to pull only the needed lines and keep context small."createMemoryGetTool(...) 流程:
manager.readFile({ relPath, from, lines })- 只返回指定片段,避免把整文件灌入上下文
工具名称设计暗示了使用顺序:memory_search 先定位,memory_get 再按需读取。
模块五 引用策略(citations)
shouldIncludeCitations(...) 行为:
on:总是显示off:总是不显示auto:direct chat 显示,group/channel 默认不显示
这样能在私聊保证可追溯,在群聊避免噪音。
模块六 启动预热
startGatewayMemoryBackend(...) 在网关启动时执行:
- 非 qmd 直接跳过
- qmd 时提前调用
getMemorySearchManager(...)+probeVectorAvailability() - 成功打 info,失败打 warn
目的:把故障前置到启动阶段,不等用户首问才炸。
模块七 向量索引构建与更新(embedding ops)
manager-embedding-ops.ts 负责"把文本变成可检索向量"的工程细节:
- 分块:
chunkMarkdown(...)把文档切成 chunk - 批处理:
buildEmbeddingBatches(...)按 token/字节预算分批 - 缓存:
embedding_cache命中则跳过重复 embedding - 回写:
chunks/chunks_vec/chunks_fts三类表同步更新 - 重试与超时:远程/本地模型用不同超时阈值,失败走有限次重试
这层决定"索引更新是否快、成本是否可控、失败能否自愈"。
模块八 与智能体链路结合
记忆进入主回答有两条路:
- 工具路:模型主动调用
memory_search/memory_get - 注入路:插件在
before_agent_start生成prependContext
建议优先做"工具路",更稳定、可控、可调试。
最小复刻骨架
async function resolveMemoryManager(cfg: Config, agentId: string) {
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
if (resolved.backend === "qmd") {
const cacheKey = buildQmdCacheKey(agentId, resolved.qmd);
const cached = QMD_MANAGER_CACHE.get(cacheKey);
if (cached) return cached;
try {
const primary = await QmdMemoryManager.create(resolved);
const fallback = new FallbackMemoryManager(primary, cacheKey, () => createBuiltinManager(cfg, agentId));
QMD_MANAGER_CACHE.set(cacheKey, fallback);
return fallback;
} catch {
return await createBuiltinManager(cfg, agentId);
}
}
return await createBuiltinManager(cfg, agentId);
}
async function memorySearch(query: string, cfg: Config, agentId: string, sessionKey?: string) {
const manager = await resolveMemoryManager(cfg, agentId);
// scope guard:由 manager.search 内部处理
const raw = await manager.search(query, { maxResults: 5, sessionKey });
const withCitations = decorateCitations(raw, shouldIncludeCitations(cfg));
return clampResultsByInjectedChars(withCitations, 4000);
}自检清单
- qmd 挂掉后是否能自动切 builtin(
primaryFailed=true→ensureFallback())。 evictCacheEntry是否在 primary 失败时调用,下次能重试创建 qmd。- manager 不可用时是否返回 disabled,而不是炸主流程。
maxInjectedChars是否生效(clampResultsByInjectedChars)。- 引用模式在 direct/group 是否符合预期。
waitForPendingUpdateBeforeSearch是否在 search 前等待最新写入。isScopeAllowedscope guard 是否过滤掉不在范围的 session。- 启动阶段是否能提前暴露 qmd 配置错误(
probeVectorAvailability)。