开源 AI 工具链:Agent 记忆系统的分层存储与检索架构设计

cover

一、Agent 失忆症:长对话场景下的上下文丢失与检索困境

在构建轻量化 Agent 产品时,一个经常被忽视却致命的问题是——Agent 的"记忆"管理。当对话轮次超过数十轮,或 Agent 需要跨会话保持用户偏好时,简单地将所有历史消息拼接到 Prompt 中,不仅会迅速触及 Token 上限,还会导致模型注意力分散、响应质量下降。更棘手的是,在多用户并发场景下,记忆的存储与检索延迟直接影响 Agent 的响应速度。

核心痛点可以归纳为三点:第一,全量上下文注入导致 Token 消耗线性增长,成本不可控;第二,无差别的记忆检索引入大量噪声,降低生成质量;第三,跨会话的长期记忆缺乏有效的过期与合并机制,存储膨胀严重。这些问题在开源 Agent 框架中普遍存在,因为大多数框架只提供了简单的消息列表管理,缺少系统化的记忆架构。

二、分层记忆模型:从工作记忆到长期归档的架构设计

借鉴认知科学中的记忆分层理论,Agent 的记忆系统可以划分为三层:工作记忆(Working Memory)、情景记忆(Episodic Memory)和语义记忆(Semantic Memory)。每一层有不同的存储介质、检索策略和生命周期。

flowchart TB
    subgraph 输入层
        U[用户输入] --> P[预处理与意图识别]
    end

    subgraph 工作记忆
        P --> WM[当前会话上下文窗口]
        WM --> |滑动窗口淘汰| EB[过期缓冲区]
    end

    subgraph 情景记忆
        EB --> |向量化嵌入| VS[向量存储]
        VS --> |语义检索| RC[相关片段召回]
    end

    subgraph 语义记忆
        VS --> |周期性合并| KB[知识摘要库]
        KB --> |关键词检索| SK[结构化知识]
    end

    RC --> AGG[记忆聚合器]
    SK --> AGG
    WM --> AGG
    AGG --> LLM[大语言模型]

    style WM fill:#e1f5fe
    style VS fill:#fff3e0
    style KB fill:#e8f5e9

工作记忆维护当前会话的最近 N 轮对话,直接注入 Prompt,保证即时上下文的完整性。情景记忆将历史对话片段向量化存储,通过语义相似度检索与当前问题相关的历史片段。语义记忆则通过周期性的摘要合并,将高频出现的知识提炼为结构化条目,支持精确的关键词匹配。

这种分层设计的核心优势在于:每一层都可以独立选择最优的存储引擎和检索算法。工作记忆用内存中的环形缓冲区,情景记忆用向量数据库,语义记忆用关系型数据库或文档数据库。各层之间通过异步的晋升(Promotion)机制连接,避免记忆写入阻塞主流程。

三、分层记忆引擎的生产级实现

# memory_engine.py — Agent 分层记忆引擎核心实现
import time
import json
import hashlib
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from collections import deque
from typing import Optional

import numpy as np


@dataclass
class MemoryItem:
    """记忆条目的统一数据模型"""
    content: str
    role: str                    # user / assistant / system
    session_id: str
    timestamp: float = field(default_factory=time.time)
    metadata: dict = field(default_factory=dict)
    embedding: Optional[np.ndarray] = None
    importance_score: float = 0.0  # 重要性评分,用于晋升决策

    @property
    def item_id(self) -> str:
        raw = f"{self.session_id}:{self.timestamp}:{self.content[:64]}"
        return hashlib.md5(raw.encode()).hexdigest()[:16]


class WorkingMemory:
    """工作记忆:滑动窗口 + 重要性保留策略"""

    def __init__(self, max_tokens: int = 4096, reserve_ratio: float = 0.3):
        self.max_tokens = max_tokens
        # 预留比例:为情景记忆召回结果保留的 Token 空间
        self.reserve_ratio = reserve_ratio
        self._buffer: deque[MemoryItem] = deque()
        self._token_count = 0

    def _estimate_tokens(self, text: str) -> int:
        # 粗略估算:中文约 1.5 Token/字,英文约 1 Token/4字符
        cn_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
        en_chars = len(text) - cn_chars
        return int(cn_chars * 1.5 + en_chars * 0.25)

    def add(self, item: MemoryItem) -> list[MemoryItem]:
        """添加记忆条目,返回被淘汰的条目列表"""
        item_tokens = self._estimate_tokens(item.content)
        evicted = []

        # 计算可用空间(扣除预留空间)
        available = self.max_tokens * (1 - self.reserve_ratio)

        while self._buffer and self._token_count + item_tokens > available:
            # 优先淘汰重要性最低且最早的条目
            oldest = self._buffer.popleft()
            self._token_count -= self._estimate_tokens(oldest.content)
            evicted.append(oldest)

        self._buffer.append(item)
        self._token_count += item_tokens
        return evicted

    def get_context(self) -> list[dict]:
        """获取当前工作记忆的上下文,格式化为 LLM 输入"""
        return [
            {"role": item.role, "content": item.content}
            for item in self._buffer
        ]

    @property
    def remaining_tokens(self) -> int:
        return int(self.max_tokens * self.reserve_ratio)


class EpisodicMemory:
    """情景记忆:基于向量相似度的片段检索"""

    def __init__(self, embed_dim: int = 768, top_k: int = 5,
                 similarity_threshold: float = 0.65):
        self.embed_dim = embed_dim
        self.top_k = top_k
        self.similarity_threshold = similarity_threshold
        # 生产环境应替换为 Milvus / Qdrant / Pinecone
        self._vectors: list[np.ndarray] = []
        self._items: list[MemoryItem] = []

    def store(self, item: MemoryItem) -> None:
        """存储记忆条目及其向量表示"""
        if item.embedding is None:
            return  # 无嵌入向量的条目不存入情景记忆
        self._vectors.append(item.embedding)
        self._items.append(item)

    def recall(self, query_embedding: np.ndarray,
               max_tokens: int = 1024) -> list[MemoryItem]:
        """根据查询向量召回最相关的记忆片段"""
        if not self._vectors:
            return []

        # 批量计算余弦相似度
        query_norm = query_embedding / (np.linalg.norm(query_embedding) + 1e-8)
        matrix = np.array(self._vectors)
        matrix_norm = matrix / (np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-8)
        similarities = matrix_norm @ query_norm

        # 按相似度排序,过滤低分结果
        ranked_indices = np.argsort(similarities)[::-1]
        results = []
        token_budget = max_tokens

        for idx in ranked_indices:
            if similarities[idx] < self.similarity_threshold:
                break
            item = self._items[idx]
            item_tokens = len(item.content)  # 简化估算
            if token_budget - item_tokens < 0:
                break
            results.append(item)
            token_budget -= item_tokens
            if len(results) >= self.top_k:
                break

        return results


class SemanticMemory:
    """语义记忆:结构化知识的摘要与合并"""

    def __init__(self):
        # 生产环境应替换为 PostgreSQL / MongoDB
        self._knowledge: dict[str, dict] = {}

    def merge(self, items: list[MemoryItem], summary_fn) -> None:
        """将一组记忆条目合并为结构化知识摘要"""
        if not items:
            return

        # 按主题聚类后分别生成摘要
        cluster_key = items[0].metadata.get("topic", "general")
        contents = [item.content for item in items]
        combined = "\n".join(contents)

        # 调用外部摘要函数(可以是 LLM 调用)
        summary = summary_fn(combined)

        self._knowledge[cluster_key] = {
            "summary": summary,
            "source_count": len(items),
            "updated_at": time.time(),
        }

    def query(self, keywords: list[str]) -> list[dict]:
        """关键词检索结构化知识"""
        results = []
        for key, value in self._knowledge.items():
            # 简单的关键词匹配,生产环境可用全文检索
            if any(kw in key or kw in value["summary"] for kw in keywords):
                results.append({"topic": key, **value})
        return results


class MemoryEngine:
    """记忆引擎:协调三层记忆的读写与晋升"""

    def __init__(self, working: WorkingMemory, episodic: EpisodicMemory,
                 semantic: SemanticMemory, embed_fn=None, summarize_fn=None):
        self.working = working
        self.episodic = episodic
        self.semantic = semantic
        self._embed_fn = embed_fn      # 文本向量化函数
        self._summarize_fn = summarize_fn  # 摘要生成函数

    def add_memory(self, content: str, role: str, session_id: str,
                   metadata: dict = None) -> None:
        """写入新记忆,自动处理晋升流程"""
        item = MemoryItem(
            content=content,
            role=role,
            session_id=session_id,
            metadata=metadata or {},
        )

        # 重要性评分:基于内容长度和元数据标记
        item.importance_score = self._calc_importance(item)

        # 写入工作记忆,获取被淘汰的条目
        evicted = self.working.add(item)

        # 被淘汰的条目晋升到情景记忆
        for evicted_item in evicted:
            if self._embed_fn:
                evicted_item.embedding = self._embed_fn(evicted_item.content)
            self.episodic.store(evicted_item)

    def recall(self, query: str, session_id: str = None) -> list[dict]:
        """多路召回:工作记忆 + 情景记忆 + 语义记忆"""
        context = self.working.get_context()
        remaining = self.working.remaining_tokens

        # 情景记忆召回
        if self._embed_fn and remaining > 0:
            query_emb = self._embed_fn(query)
            episodic_results = self.episodic.recall(query_emb, remaining)
            for item in episodic_results:
                context.append({
                    "role": "system",
                    "content": f"[历史相关] {item.content}"
                })

        # 语义记忆召回
        keywords = query.split()[:5]  # 简化关键词提取
        semantic_results = self.semantic.query(keywords)
        for entry in semantic_results:
            context.append({
                "role": "system",
                "content": f"[知识摘要] {entry['summary']}"
            })

        return context

    def _calc_importance(self, item: MemoryItem) -> float:
        """计算记忆条目的重要性评分"""
        score = 0.0
        # 包含决策或结论的内容更重要
        decision_keywords = ["决定", "选择", "确认", "取消", "important", "must"]
        if any(kw in item.content.lower() for kw in decision_keywords):
            score += 0.3
        # 用户偏好类内容更重要
        if item.metadata.get("type") == "preference":
            score += 0.4
        # 较长的内容可能包含更多信息
        score += min(len(item.content) / 500, 0.3)
        return min(score, 1.0)

    def consolidate(self, topic: str = None) -> int:
        """执行记忆合并:将情景记忆中的条目摘要后存入语义记忆"""
        # 简化实现:按主题分组后合并
        items_to_merge = [
            item for item in self.episodic._items
            if topic is None or item.metadata.get("topic") == topic
        ]
        if items_to_merge and self._summarize_fn:
            self.semantic.merge(items_to_merge, self._summarize_fn)
        return len(items_to_merge)

上述实现中,MemoryEngine 作为门面类协调三层记忆的读写。工作记忆使用滑动窗口策略,被淘汰的条目自动晋升到情景记忆。情景记忆基于向量相似度检索,语义记忆通过周期性的合并操作将碎片化的记忆提炼为结构化知识。

四、分层存储的权衡:延迟、一致性与存储成本的三角博弈

分层记忆架构并非银弹,它在解决上下文丢失问题的同时,引入了新的工程权衡。

延迟问题:情景记忆的向量检索在数据量增大时延迟会显著上升。当向量库达到百万级时,即使使用 HNSW 索引,单次检索延迟也可能从毫秒级上升到百毫秒级。缓解方案是在情景记忆层引入缓存——对高频查询的 Top-K 结果进行 LRU 缓存,将热路径的检索延迟压回个位数毫秒。

一致性问题:三层记忆之间的晋升是异步的,存在短暂的不一致窗口。例如,一条记忆刚从工作记忆淘汰但尚未完成向量化存入情景记忆,此时如果发生检索,就会丢失该条记忆。对于大多数 Agent 场景,这种最终一致性是可以接受的;但对于金融交易等强一致性场景,需要引入同步写入模式,在写入情景记忆完成后再返回响应。

存储成本:向量存储的膨胀速度远超预期。一条 100 Token 的对话,其 768 维向量需要约 3KB 存储空间,加上元数据索引,实际占用约 5KB。日活 10 万用户的 Agent,每天新增向量数据可达数 GB。必须设计合理的过期与归档策略——超过 30 天未被召回的情景记忆应降级为冷存储,超过 90 天的应执行摘要合并后删除原始向量。

适用边界:分层记忆架构适用于对话轮次多、需要跨会话保持上下文的 Agent 产品。对于简单的单轮问答工具,引入三层记忆反而增加了不必要的复杂度,直接使用固定窗口的上下文管理即可。

五、总结

Agent 记忆系统的分层设计,本质上是将认知科学中的记忆模型映射到工程架构中。工作记忆保证即时响应,情景记忆提供语义关联,语义记忆沉淀结构化知识。三层之间通过晋升与合并机制实现数据的自动流转。在落地时,需要根据业务场景的延迟要求、一致性等级和存储预算,灵活调整每层的存储引擎和检索策略。对于轻量化 Agent 产品,建议从工作记忆 + 情景记忆两层起步,待数据量增长后再引入语义记忆层。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐