標籤: AI Agent 長期記憶

  • 為 AI Agent 建長期記憶:Rust 實戰

    為 AI Agent 建長期記憶:Rust 實戰

    📌 本文重點

    • 單純丟向量庫不足以支撐可運維的長期記憶
    • 將 Events / Sessions / Facts 結構化並分層檢索
    • 以 Rust 記憶服務提供 vendor-agnostic 的統一記憶層

    AI Agent 開到一定規模後,「把聊天記錄丟進向量資料庫」很快就不夠用:

    • 記憶膨脹導致成本失控、查詢變慢
    • 多個 Agent、不同 LLM 共用資料時互相污染
    • 想換模型或供應商時,歷史記憶幾乎不能重用

    這篇從記憶層設計問題拆解起,用 Rust 開源專案 akitaonrails/ai-memory 當主線,示範如何把「長期記憶」升級成可運維的基礎設施,而不是一堆臨時向量。


    重點說明:把「記憶」變成明確的基礎設施

    💡 關鍵: 把記憶從「單一向量庫」拆成 Events / Sessions / Facts,可以同時兼顧語義檢索與結構化查詢,讓長期記憶真正可維護、可重用。

    1. 資料結構:從 chat log 到 events / sessions / facts

    粗糙作法:

    • 每輪對話做 embedding → 丟進向量庫 → 用 semantic search 回撈

    問題:

    • 事件順序感消失:只剩相似度,沒有「先後」與「上下文」
    • 無法表達持久事實:像使用者偏好、系統狀態,只是散落在多個對話片段

    較好的設計是拆成三類:

    • Events:原始互動紀錄
    • 例:user_message, tool_call, agent_decision
    • 保留時間戳與來源,適合做 audit 和重播
    • Sessions:一次任務或一段對話的邊界
    • 例:session_id、agent_id、status=completed
    • 讓你知道「這次訂房流程」完整發生了什麼
    • Facts:可重用、可更新的持久知識
    • 例:使用者偏好、系統配置、業務規則
    • 需要版本與狀態(active, deprecated)

    這樣一來,semantic search 用在 Events/Facts 的內容檢索,structured query 用在 Sessions/Facts 的條件篩選,組出可靠的上下文再餵給 LLM,而不是全靠相似度。


    2. 檢索策略與 vendor-agnostic 介面

    實際上你會需要兩層 API:

    • 語義檢索層:
    • 例如:search_events(query, top_k)、search_facts(query, filters)
    • 背後可接不同 embedding provider(OpenAI, local model 等),但對上層 Agent 暴露的是穩定的介面

    • 結構化查詢層:

    • 例如:get_session(session_id)、list_facts(owner_id, kind="preference")
    • 通常走資料庫索引(Postgres, SQLite),不牽涉向量搜尋

    akitaonrails/ai-memory 正是要提供這種「供應商無關的記憶層」,讓你可以:

    • 今天用 OpenAI,明天換到本地模型
    • 多個 Agent framework(LangChain, LlamaIndex, 自家的 SDK)共用同一套記憶服務

    3. Rust 記憶服務 + RAG / 工具調用整合

    長期記憶一旦變成基礎設施,就有三個技術要求:

    • 效能:高頻讀寫、多 Agent 並行
    • 安全:記憶裡必然有 PII 和敏感業務資訊
    • 跨語言可接:Python、Node、Go、甚至 CLI agent 都要用

    Rust 在這裡的角色:

    • 提供一個高效、型別安全的記憶核心(ai-memory)
    • 對外以 FFI / HTTP / IPC 三種方式暴露 API
    • Agent 框架只需要呼叫類似 store_event / query_memory 的介面,就能把記憶接進 RAG pipeline 或工具調用流程

    實作範例:用 ai-memory 建一個共用長期記憶服務

    以下以虛構的 ai-memory 介面示意,重點放在設計思路而不是精確函式名稱。

    1)定義記憶 schema,接上現有 Agent

    先在 Rust 端定義核心結構:

    // memory_schema.rs
    
    #[derive(Debug, Clone)]
    pub enum EventKind {
        UserMessage,
        AgentReply,
        ToolCall,
        ToolResult,
    }
    
    #[derive(Debug, Clone)]
    pub struct Event {
        pub id: String,
        pub session_id: String,
        pub agent_id: String,
        pub kind: EventKind,
        pub content: String,
        pub metadata: serde_json::Value,
        pub created_at: chrono::DateTime<chrono::Utc>,
    }
    
    #[derive(Debug, Clone)]
    pub struct Fact {
        pub id: String,
        pub owner_id: String,    // user_id 或 system
        pub kind: String,        // preference, policy, profile
        pub content: String,
        pub embedding: Option<Vec<f32>>,  // for semantic search
        pub version: i32,
        pub status: String,      // active, deprecated
    }
    

    對 Python Agent 來說,只需要一個薄封裝,把每輪互動寫入記憶:

    # agent_memory.py
    
    class MemoryClient:
        def __init__(self, base_url: str):
            self.base_url = base_url
    
        def store_event(self, session_id, agent_id, kind, content, metadata=None):
            payload = {
                "session_id": session_id,
                "agent_id": agent_id,
                "kind": kind,
                "content": content,
                "metadata": metadata or {},
            }
            requests.post(f"{self.base_url}/events", json=payload)
    
        def search_facts(self, query, owner_id=None, kind=None, top_k=5):
            params = {
                "query": query,
                "owner_id": owner_id,
                "kind": kind,
                "top_k": top_k,
            }
            resp = requests.get(f"{self.base_url}/facts/search", params=params)
            return resp.json()["results"]
    

    Agent loop 中的使用方式:

    memory = MemoryClient(base_url="http://localhost:8080")
    
    # 每輪對話寫入 event
    memory.store_event(
        session_id=session_id,
        agent_id="support-bot-v2",
        kind="UserMessage",
        content=user_input,
    )
    
    # 在回應前查詢長期偏好
    facts = memory.search_facts(
        query="使用者的語言偏好與通知設定",
        owner_id=user_id,
        kind="preference",
        top_k=3,
    )
    
    context = format_facts_for_prompt(facts)
    response = llm.chat(prompt=build_prompt(user_input, context))
    

    這樣你的 Agent 邏輯完全不需要知道底層用的是哪家向量資料庫,也不綁在單一 LLM provider。


    2)Rust 記憶服務:FFI / HTTP / IPC 三種接法

    假設 ai-memory 提供核心 crate ai_memory_core,我們可以用三種方式包出去。

    HTTP 服務(最通用)

    優點:任何語言都能接;缺點:有網路 overhead。

    // http_server.rs
    
    use ai_memory_core::{MemoryStore, SearchQuery};
    use axum::{routing::get, routing::post, Json, Router};
    
    async fn create_event(Json(payload): Json<CreateEventRequest>) -> Json<EventResponse> {
        let mut store = MemoryStore::global();
        let event = store.store_event(payload.into())?;
        Json(EventResponse::from(event))
    }
    
    async fn search_facts(Json(query): Json<SearchFactsRequest>) -> Json<SearchFactsResponse> {
        let store = MemoryStore::global();
        let results = store.search_facts(SearchQuery::from(query))?;
        Json(SearchFactsResponse { results })
    }
    
    pub fn app() -> Router {
        Router::new()
            .route("/events", post(create_event))
            .route("/facts/search", get(search_facts))
    }
    

    命令列 Agent 或後端服務只要跑一個共用 memory server,就可以用 HTTP 存取。

    FFI(嵌入到 Python / Node 進程)

    優點:效能好、延遲低;缺點:需維護 binding。適用高頻工具調用型 Agent。

    // lib.rs (Rust)
    
    #[no_mangle]
    pub extern "C" fn store_event_ffi(json_payload: *const c_char) -> *const c_char {
        // 解析 JSON,呼叫 MemoryStore,回傳 JSON 字串
    }
    

    Python 側使用 ctypes 或 pyo3 包一層,暴露同樣的 store_event / search_facts 介面,對應前面的 MemoryClient。

    IPC(同機不同進程,高安全場景)

    可以用 Unix socket + protobuf 或 Cap’n Proto:

    • 優點:比 HTTP 更輕量,適合同機多服務共用記憶
    • 缺點:部署稍複雜,需額外 tooling

    設計上只要確保三種接法都共用同一套核心 API(MemoryStore),就能在不同專案中任意選擇實作方式,而不改動 Agent 邏輯。


    3)版本升級、資料遷移、多模型共用記憶庫

    長期記憶真正困難在於 「持續演化」。

    Schema 版本管理

    在 Fact 結構上掛版本欄位:

    pub struct Fact {
        pub id: String,
        pub owner_id: String,
        pub kind: String,
        pub content: String,
        pub version: i32,       // schema/version
        pub status: String,
        pub embedding: Option<Vec<f32>>,  
    }
    

    當你需要新增欄位或改變結構時:

    • 新寫入用 version = 2
    • 舊資料由 migration job 緩慢升級
    • 查詢 API 接受 min_version / max_version 作為過渡策略

    多模型共用同一記憶庫

    不同 LLM 對同一條記錄的解讀可能不同,所以要在 metadata 明確標注來源模型:

    pub struct Event {
        pub model_name: Option<String>,   // gpt-4o, llama-3-70b 等
        pub agent_id: String,
        // ...
    }
    

    策略上:

    • Facts 儘量由工具或人類決策產生,減少「模型幻覺」寫入持久記憶
    • 檢索時可加 filter:model_name in ["gpt-4o", "internal-rule-engine"],避免用某些品質較差模型產生的事件來推論

    建議與注意事項:把坑提前填好

    💡 關鍵: 控制 embedding 範圍、處理 PII、標記 embedding 模型,是讓長期記憶在成本、隱私與準確度間取得平衡的三個關鍵。

    1. 記憶膨脹與成本控制

    問題:

    • 所有對話都 embedding → 向量庫數量爆炸,成本跟查詢延遲一起上升

    建議:

    • 只對重要 Events / Facts 做 embedding,例如:完成一個任務時產生 summary fact
    • 針對長期 session,定期做 conversation summarization,保留摘要而不是 raw log
    • 對向量庫設計 TTL 或冷/熱層級:冷資料只保留摘要向量

    2. 隱私與合規(PII / 敏感資料)

    問題:

    • 長期記憶通常含姓名、電話、訂單資訊等 PII

    建議:

    • 設計 PII-aware schema:把 PII 拆出獨立欄位,便於加密與 masking
    • 在寫入前跑簡單的 PII 檢測 rule:
    • 例:電話號碼、email pattern 直接用工具抽出 → 存入安全欄位
    • 對查詢 API 加上角色權限(例如 owner_id+role=internal_support)

    3. 語義檢索漂移與模型更新

    問題:

    • 換 embedding 模型後,舊向量的語義分佈不同,semantic search 精度變差

    簡單防線:

    • 在向量旁存 embedding_model 欄位
    • 新模型上線後:
    • 新增資料用新 embedding
    • 舊資料分批 re-embed,或只重算活躍 Facts
    • 用 offline evaluation:定義一組標準 query + expected hits,監控 semantic search 成效

    4. 不同模型對同一紀錄的解讀不一致

    問題:

    • Model A 把某次對話解讀為「使用者喜歡簡訊通知」,Model B 覺得是「偏好 email」

    建議:

    • 把「推論」與「事實」分開存:
    • Fact:明確的、可驗證的偏好(使用者自行設定)
    • Inference:模型的猜測,需經多次交叉驗證或人工確認才升級為 Fact
    • 在 prompt 中區分:
    • 「已確認偏好」 vs. 「推測偏好」

    結語:把長期記憶升級成基礎設施

    對成熟的 AI 專案來說,長期記憶不再是「多塞幾個向量」的 hack,而是需要設計、版本與治理的系統。利用像 akitaonrails/ai-memory 這種以 Rust 實作的開源方案,你可以:

    • 為所有 Agent 建立一個 供應商無關、可演化的記憶層
    • 清楚區分 Events / Sessions / Facts,同時用語義與結構化查詢做精準檢索
    • 以 HTTP / FFI / IPC 等方式,在不同語言與框架中重用同一記憶庫

    這些設計一開始多花一點功夫,換到的是更穩定的成本、更易維護的架構,以及在多 Agent、多模型環境裡,真正能「記住」使用者與業務脈絡的系統。

    🚀 你現在可以做的事

    • 到 GitHub 搜尋並閱讀 akitaonrails/ai-memory 專案原始碼與文件
    • 在現有 Agent 專案中先導入 Events / Sessions / Facts 基本 schema,重構記憶寫入流程
    • 實作一個簡單的 HTTP memory server,讓至少兩個不同語言或框架的 Agent 共用同一記憶層