跳转到内容
OmniRoute source

Memory System (中文 (简体))

OmniRoute 的记忆引擎支持四种嵌入来源(src/lib/memory/embedding/)。每种来源在延迟、成本、模型质量和设置复杂度方面各有取舍。

提供者 来源 延迟 成本 质量 设置
transformers 本地 ONNX 模型 (Xenova/all-MiniLM-L6-v2) ~50-150ms (CPU) 免费 良好 只需 npm install
static 预计算向量(已缓存) <1ms 免费 不适用(取决于是否命中缓存) 无
remote OpenAI / Cohere / Voyage API ~100-300ms $0.02-0.10/1M tokens 极佳 API 密钥
auto 在运行时选择最佳可用来源 与所选来源相同 免费 与所选来源相同 无
(cache) 位于任意来源之上的内存 LRU 层 <1ms(命中),完整延迟(未命中) 免费 与底层来源相同 始终启用(不可选为来源)
你的部署环境是什么?
│
┌───────────┼───────────┬──────────────┐
│ │ │ │
开发/测试 小型生产环境 大型生产环境 边缘/离线
│ │ │ │
▼ ▼ ▼ ▼
transformers transformers remote (Qdrant) transformers
(免费,无需 API) (质量最佳) (无需互联网)
│ │ │ │
└────────┬──┴───────────┴──────────────┘
│
▼
始终在顶层添加 `cache` 层
(LruCache 会包装任意提供者)

记忆嵌入选项通过设置 API/UI 配置,而不是通过环境变量配置。设置中的相关数据库键(src/lib/memory/settings.ts 中的 normalizeMemorySettings)如下:

  • memoryEmbeddingSource:"transformers"(本地)、"remote"(基于 API,例如 OpenAI)、"static"(外部存储)或 "auto"
  • memoryEmbeddingProviderModel:远程/静态来源的模型标识符(例如 "text-embedding-3-small")
  • memoryTransformersEnabled:true | false
  • memoryStaticEnabled:true | false
  • memoryVectorStore:"sqlite-vec"、"qdrant" 或 "auto"

在内部使用 transformers.js 运行本地模型:

终端窗口
# 在代码中读取的环境变量 (src/lib/memory/embedding/index.ts):
MEMORY_TRANSFORMERS_MODEL=Xenova/all-MiniLM-L6-v2 # HF 模型仓库
MEMORY_STATIC_MODEL=minishlab/potion-base-8M # HF 静态 potion 模型
MEMORY_STATIC_CACHE_DIR=<DATA_DIR>/embeddings # 缓存目录

默认情况下缓存始终启用,并通过环境变量配置:

终端窗口
MEMORY_EMBEDDING_CACHE_MAX=1000 # 最大缓存条目数
MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # TTL(5 分钟)

在典型的 4 核 x86 服务器上进行的基准测试(每段文本约 100 个 token):

提供者 p50 p95 p99 每 100 万次嵌入的成本
transformers (CPU) 80ms 180ms 350ms 免费
remote (OpenAI) 120ms 220ms 400ms 约 $0.02 (ada-002) / $0.13 (3-large)
static (Qdrant) 15ms 30ms 60ms 取决于 Qdrant 托管服务
cache(命中) <1ms <1ms 2ms 免费

extraction.ts 模块 (src/lib/memory/extraction.ts) 使用正则表达式模式匹配从对话消息中提取结构化事实。了解这些模式有助于你针对自己的使用场景优化提取质量。

类别 示例模式 捕获内容
PREFERENCE_PATTERNS "I prefer <X>"、"I like <X>"、"I hate <X>" 用户偏好
DECISION_PATTERNS "I'll use <X>"、"I decided to <X>"、"I went with <X>" 用户决策(情景记忆)
PATTERN_PATTERNS "I usually <X>"、"I always <X>"、"I never <X>" 持久的行为模式
// 来自 src/lib/memory/extraction.ts
const PREFERENCE_PATTERNS = [
/\bI\s+(?:really\s+)?prefer\s+([^.,\n]+)/gi,
/\bI\s+(?:really\s+)?like\s+([^.,\n]+)/gi,
/\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi,
];
const DECISION_PATTERNS = [
/\bI'?(?:ll|will)\s+use\s+([^.,\n]+)/gi,
/\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi,
];
const PATTERN_PATTERNS = [/\bI\s+usually\s+([^.,\n]+)/gi, /\bI\s+always\s+([^.,\n]+)/gi];

当用户说:

“I prefer TypeScript. I’ll use Postgres for this project. I always commit before pushing. I don’t like Python.” 提取过程会生成 4 条记忆:

键 类别 类型 内容
preference:typescript preference factual “TypeScript”
decision:postgres_for_this_project decision episodic “Postgres for this project”
pattern:commit_before_pushing pattern factual “commit before pushing”
preference:python preference factual “Python”

为防止提取失控,适用以下限制:

| 最小内容长度 | 3 个字符 | | 最大内容长度 | 500 个字符 |

只要启用了记忆功能,提取就会自动运行;没有单独的 仅提取开关。要将其关闭,请完全禁用记忆功能(通过 PUT /api/settings/memory 设置 enabled: false)。在以下情况下可考虑这样做:

  • 消息量很大,且提取成本不可忽略
  • 对话大多是临时性的(聊天、调试),没有长期价值
  • 已经通过自定义插件捕获上下文

倒数排名融合 (RRF) 算法会合并 FTS5(关键词)和向量(语义)结果。k 参数控制赋予排名靠后结果的权重。

对于每个候选记忆,RRF 分数为:

RRF(d) = Σ 1 / (k + rank_i(d))

其中:

  • k 是常数(默认为 60)
  • rank_i(d) 是文档 d 在第 i 个检索系统(FTS、向量)中的排名
  • 对所有检索系统的结果求和
k 值 效果 最适合的场景
k=0 纯排名融合(无平滑) 理论基线
k=10-30 大幅提高靠前结果的权重,低排名结果几乎没有贡献 排名前 3 的结果通常正确时
k=60(默认值) 均衡——排名前 10 的结果都能做出有意义的贡献 通用检索
k=100+ 更平坦——如果低排名结果出现在多个系统中,它们甚至可能占主导地位 召回率比精确率更重要时
终端窗口
# 默认值
MEMORY_RRF_K=60
# 激进的精确率设置(小型记忆库、文档较少)
MEMORY_RRF_K=20
# 最大召回率(大型记忆库、查询多样)
MEMORY_RRF_K=120

k=20 时的示例:

  • FTS 排名第 1 → 贡献 1/21 = 0.048
  • FTS 排名第 10 → 贡献 1/30 = 0.033
  • 向量排名第 1 → 贡献 0.048
  • 合并后的最大值:0.096

k=60 时的示例:

  • FTS 排名第 1 → 贡献 1/61 = 0.016
  • FTS 排名第 10 → 贡献 1/70 = 0.014
  • 向量排名第 1 → 贡献 0.016
  • 合并后的最大值:0.033

k 越高,排名第 1 与排名第 10 之间的相对差异就越小,因此该算法更依赖各检索系统之间的共识,而不是最高排名的置信度。

症状 可尝试的方法
排名第一的结果总是获胜,但它是错误的 降低 k(例如 20)——更看重靠前排名的置信度
正确答案位于前 5 名,但不是第 1 名 提高 k(例如 100)——更平坦的评分方式会奖励共识
召回率高,但精确率低 降低 k——增强排名区分度
召回率低(遗漏相关文档) 提高 k——给排名靠后的文档一个机会

倒数排名融合对语义向量排名和全文搜索排名使用相同的权重:

RRF(d) = 1/(k + rank_vector) + 1/(k + rank_fts)

没有可用于调整各自权重的环境变量(MEMORY_RRF_VECTOR_WEIGHT/MEMORY_RRF_FTS_WEIGHT 不存在)。


summarization.ts 模块 (src/lib/memory/summarization.ts) 会压缩较旧的记忆,以在保留召回能力的同时缩小活跃记忆集。

触发方式 阈值(默认)
通过 API 手动触发 不适用

summarization.ts 导出了两个入口点:

  • summarizeMemories(apiKeyId, sessionId?, maxTokens = 4000) — 将某个会话的 记忆压缩为一段摘要文本,并将其限制在指定的 token 预算内。
  • summarizeMemoriesOlderThan(apiKeyId, days, dryRun) — API 使用的基于时间的 压缩方法:它会选择所有早于 days 的记忆,根据它们创建一条压缩后的摘要记忆, 并且(当 dryRun 为 false 时)删除原始记忆。传入 dryRun: true 可预览候选集 和 token 总数,而不会修改任何内容。

不存在标签/键聚类过程,也不会对每条记忆进行“核心内容与可摘要内容”的评分 — 选择完全基于时间截止点,而摘要文本则是每个候选项经过压缩并带有类型前缀的一行内容。

摘要是手动启用 / 选择性启用的 — autoSummarize 设置默认为 false, 因此不会自动压缩任何内容。可通过 API 触发:

终端窗口
curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"

若要保持关闭状态,只需将 autoSummarize 保持为默认值 (false)。

  • 先使用 dryRun 预览 — summarizeMemoriesOlderThan(..., true) 会返回 候选项列表和 token 总数,以便你在删除原始记忆之前确认将要合并的内容。
  • 如果记忆语料库较大,请在低流量时段运行摘要任务 — LLM 调用是最耗时的部分
终端窗口
# Cron 风格:每天凌晨 3 点生成摘要
0 3 * * * curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"

权威来源: src/lib/memory/backend.ts, src/lib/memory/genericBackend.ts, src/lib/memory/manager.ts 测试: src/lib/memory/__tests__/generic-backend.test.ts

MemoryBackend 提供程序模式在现有记忆引擎之上引入了一个可插拔的后端抽象层。记忆系统不再绑定到单一存储实现,而是支持多个后端(SQLite、Obsidian、Notion、自定义 HTTP 后端),并且可以配置主后端/回退后端路由。

┌──────────────────────────────────────────────────────────┐
│ API 路由 │
│ (src/app/api/memory/route.ts) │
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────────────▼───────────────────────────────────┐
│ MemoryManager │
│ 单例协调器 (manager.ts) │
│ │
│ 主后端 ──► 后端 A (例如 SQLite) │
│ 回退 ──► 后端 B (例如 Obsidian) │
│ 后端 C (例如通过 GenericBackend 使用 Notion)│
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────────────┐
│ SQLite │ │ Obsidian │ │ GenericMemory │
│ 后端 │ │ 后端 │ │ 后端 (HTTP) │
└────────────┘ └────────────┘ └──────────────────┘

每个后端都必须实现 MemoryBackend 接口:

interface MemoryBackend {
readonly id: string;
readonly displayName: string;
// 增删改查
create(input: CreateMemoryInput): Promise<Memory>;
get(id: string): Promise<Memory | null>;
update(id: string, updates: Partial<...>): Promise&lt;boolean&gt;;
delete(id: string): Promise&lt;boolean&gt;;
list(filter: MemoryFilter): Promise<{ data: Memory[]; total: number; byType: Record<string, number> }>;
// 搜索
search(config: SearchConfig): Promise<Memory[]>;
// 健康状态
health(): Promise<HealthCheckResult>;
// 生命周期(可选)
initialize?(): Promise&lt;void&gt;;
shutdown?(): Promise&lt;void&gt;;
}

这是一个单例协调器,负责:

  • 通过 register(backend) 注册后端 — 在启动时从 index.ts 调用
  • 通过 configure(primary, fallbacks) 配置主后端和回退后端
  • 将增删改查/搜索请求路由到主后端,并在失败时使用回退链
  • 定期对所有后端执行健康检查

回退行为:

操作 主后端 回退后端
create ✅ 仅主后端 ❌
get ✅ 先尝试主后端 ✅ 返回 null 时回退
update ✅ 仅主后端 ✅ 即发即弃式同步
delete ✅ 仅主后端 ✅ 即发即弃式同步
list ✅ 仅主后端 ❌
search ✅ 优先使用主后端 ✅ 出错时回退

这是一个通用 HTTP 连接器,可将任意 REST API 适配为 MemoryBackend。适用于:

  • Notion — 通过 Notion API 连接
  • Obsidian — 通过 Obsidian Local REST API 连接
  • 自定义后端 — 任何公开 RESTful 记忆 API 的服务

配置:

interface GenericBackendConfig {
baseUrl: string; // 后端 API 的基础 URL
apiKey?: string; // 用于身份验证的 Bearer 令牌
headers?: Record<string, string>; // 自定义 HTTP 标头
timeout?: number; // 请求超时时间(默认:30000ms)
backendType?: string; // 用于日志记录
// 端点覆盖(默认使用 REST 约定)
endpoints?: {
search?: string; // 默认:"/memories/search"
create?: string; // 默认:"/memories"
list?: string; // 默认:"/memories"
get?: string; // 默认:"/memories/{id}"
update?: string; // 默认:"/memories/{id}"
delete?: string; // 默认:"/memories/{id}"
health?: string; // 默认:"/health"
};
// 查询参数名称映射
queryParams?: {
query?/apiKeyId?/limit?/offset?/strategy?/maxTokens?/type?/sessionId?/orderBy?/orderDir?/options?
};
// 路径参数名称映射
pathParams?: {
id?/memoryId?
};
}

已知后端已在 KNOWN_BACKENDS 中预配置:

createKnownBackend("obsidian"); // → 指向 localhost:27123 的 GenericMemoryBackend
createKnownBackend("notion"); // → 指向 api.notion.com/v1 的 GenericMemoryBackend

默认主后端。使用 src/lib/memory/store.ts 封装现有的 SQLite 内存存储。启动时自动注册。

import { sqliteBackend } from "./sqliteBackend";
memoryManager.register(sqliteBackend);

封装现有的 Obsidian 集成(src/lib/memory/obsidianBackend.ts)。通过 Obsidian Local REST API 连接到 Obsidian 仓库。

内存后端设置存储在应用设置表中,并通过 src/lib/memory/settings.ts 管理:

设置 环境/配置键 默认值 描述
主后端 memoryPrimaryBackend "sqlite" 主后端的 ID
回退后端 memoryFallbackBackends [] 按顺序排列的回退后端 ID
后端配置 memoryBackendConfigs {} 每个后端的配置覆盖

设置通过 normalizeMemorySettings() 进行规范化,并缓存在 getMemorySettings() 中。

应用启动
→ index.ts 导入(副作用):注册 SQLiteBackend
→ 从应用生命周期中调用 initMemoryBackends():
1. 加载设置(getMemorySettings)
2. 配置主后端和回退后端
3. 初始化所有后端(健康检查)
4. 准备接收请求
  1. 在 src/lib/memory/&lt;name&gt;Backend.ts 中实现 MemoryBackend 接口
  2. 从 src/lib/memory/index.ts 导出
  3. 启动时使用 memoryManager.register(yourBackend) 注册
  4. 通过设置进行配置:将 memoryPrimaryBackend 设为你的后端 ID
  5. 以 src/lib/memory/__tests__/generic-backend.test.ts 为参考进行测试
import { createGenericMemoryBackend } from "./genericBackend";
const brainBackend = createGenericMemoryBackend("brain", "BK-Brain", {
baseUrl: process.env.BRAIN_API_URL || "http://localhost:9099",
apiKey: process.env.BRAIN_API_KEY,
endpoints: {
search: "/api/memory/search",
create: "/api/memory",
health: "/api/health",
},
});
memoryManager.register(brainBackend);
终端窗口
npx vitest run src/lib/memory/__tests__/generic-backend.test.ts --reporter=verbose

预期输出:35 个测试,全部通过,涵盖:

  • 构造函数(2)
  • 健康检查(4)— 成功、500 失败、网络错误、延迟
  • 初始化(2)— 成功、失败
  • 创建(2)— 默认端点、自定义端点
  • 获取(4)— 成功、404 → null、非 404 抛出异常、自定义路径参数
  • 更新(2)— 成功、404 → false
  • 删除(2)— 成功、404 → false
  • 列表(2)— 查询参数、自定义参数名称
  • 搜索(3)— 查询参数、自定义端点、选项序列化
  • 身份验证标头(2)— Bearer 令牌、自定义标头
  • 工厂函数(1)
终端窗口
npm run typecheck:core

预期:0 个错误。


OmniRoute 源码 (a58000c7685f)

HagiCode

HagiCode 是一套智能体编码工作台:结构化工作流、多 Agent 并行执行与 Hero Dungeon 视图,把想法变成真正交付的软件。

让想法更快变成好用的软件,让智能编码更聪明、更高效,也更有趣。

HagiCode 浅色主题主界面截图
  • Smart结构化工作流将意图转化为从想法到交付的可执行路径。
  • Efficient多 Agent 工作流让调研、实现与审阅并行推进。
  • FunHero Dungeon 让长时间编码协作更直观、更有参与感。
访问 HagiCode