跳到內容
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 執行本機模型:

Terminal window
# 程式碼中讀取的環境變數(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 # 快取目錄

快取預設永遠啟用,並透過環境變數進行配置:

Terminal window
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 "我偏好 <X>"、"我喜歡 <X>"、"我討厭 <X>" 使用者偏好
DECISION_PATTERNS "我會使用 <X>"、"我決定要 <X>"、"我選擇了 <X>" 使用者決策(情節性)
PATTERN_PATTERNS "我通常 <X>"、"我總是 <X>"、"我從不 <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];

當使用者說:

「我偏好 TypeScript。這個專案我會使用 Postgres。我總是在推送前提交。我不喜歡 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)。請在下列情況考慮這麼做:

  • 訊息量很大,且擷取成本不可忽略
  • 對話大多是暫時性的(聊天、偵錯),沒有長期價值
  • 已透過自訂外掛程式擷取上下文

倒數排名融合 (Reciprocal Rank Fusion, 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+ 更平坦——如果低排名結果出現在多個系統中,甚至也可能占主導地位 召回率比精確率更重要時
Terminal window
# 預設值
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 觸發:

Terminal window
curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"

若要保持停用,只需將 autoSummarize 維持在其預設值 (false)。

  • 先使用 dryRun 預覽 — summarizeMemoriesOlderThan(..., true) 會傳回 候選清單與 token 總數,讓您在刪除原始記憶前確認哪些內容將被合併。
  • 如果您擁有大量記憶資料,請在低流量時段執行摘要 — LLM 呼叫是最耗時的部分
Terminal window
# 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) 設定主要後端與備援後端
  • 將 CRUD/搜尋路由至主要後端,失敗時使用備援鏈
  • 定期對所有後端執行健康檢查

備援行為:

操作 主要後端 備援後端
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 vault。

記憶體後端設定儲存在應用程式設定資料表中,並透過 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);
Terminal window
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)
Terminal window
npm run typecheck:core

預期結果:0 個錯誤。


OmniRoute 原始碼 (a58000c7685f)

HagiCode

HagiCode 是智慧代理程式開發工作台,結合結構化工作流程、多代理程式執行與 Hero Dungeon 介面,將想法化為交付成果。

以更聰明、更快速且更有趣的智慧代理程式工作流程,打造實用的軟體。

HagiCode 淺色主題介面畫面
  • Smart結構化流程將意圖轉化為從構想到交付的可執行步驟。
  • Efficient多代理程式工作流程讓研究、實作與審查並行進行。
  • FunHero Dungeon 讓長時間的程式協作更直覺、更有參與感。
造訪 HagiCode