构建生产级 Agent Memory 系统架构

很多人在做 Agent 时,第一版 Memory 的实现大概长这样:

memory = []
memory.append({"role": "user", "content": "你好"})
memory.append({"role": "assistant", "content": "你好!有什么可以帮你的?"})
import java.util.*;

List<Map<String, String>> memory = new ArrayList<>();
memory.add(Map.of("role", "user", "content", "你好"));
memory.add(Map.of("role", "assistant", "content", "你好!有什么可以帮你的?"));

跑几天后发现问题了:

  • 对话一长,Token 消耗爆炸
  • 用户上次说过的偏好,下次完全不记得
  • 多个 Session 之间的信息没法共享
  • 想查某个历史事实,得翻完整条对话
  • 模型在长上下文里注意力稀释,反而答得更差

于是开始往里面塞向量数据库、塞总结、塞 Graph,结果又发现:Memory 系统不是越复杂越好,而是越适合你 Agent 的行为模式越好。

这篇文章不讲泛泛的概念,而是从工程角度拆解:一个生产级的 Agent Memory 系统到底该考虑哪些维度,每种方案解决什么问题、带来什么新问题,以及如何组合它们。


目录


一、Agent Memory 到底是什么

很多人把 Memory 等同于"聊天记录",这是第一个坑。

在 Agent 的语境下,Memory 应该覆盖三类信息,每一类的存储和检索方式完全不同:

类型定义例子特点
情节记忆 (Episodic)Agent 经历过的具体交互过程用户上一次问了什么、我上次怎么回答的时序敏感、总量大、按时间衰减
语义记忆 (Semantic)从经历中提炼出的事实和知识用户叫张三、他偏好 PostgreSQL、项目 deadline 是下周五需要提取和泛化,跨 session 共享
程序记忆 (Procedural)Agent 学会的做事方式和策略用户习惯先概要再细节、工具调用失败后的回退策略相对稳定,可跨 session 复用

大多数 Memory 系统只实现了"情节记忆"这一层——而且实现方式是"全量塞进上下文"。这种做法在 5 轮对话内还好,到 50 轮就基本不可用了。

所以生产级 Memory 的第一个设计决策是:把三类记忆分离开,用不同的策略管理它们。


二、整体架构:三层分离

先把架构轮廓画出来,再逐一拆开讲每层的设计。

flowchart TD
    A["Agent 运行时"] --> B["Memory Orchestrator"]
    
    B --> C["Episodic Store (情节)"]
    B --> D["Semantic Store (语义)"]
    B --> E["Procedural Store (程序)"]
    
    C --> C1["Time-series DB / Log"]
    C --> C2["Sliding Window Reader"]
    
    D --> D1["Vector Store (嵌入)"]
    D --> D2["Key-Value Store (结构化事实)"]
    D --> D3["Graph Store (关系)"]
    D --> D4["Summarizer + Extractor"]
    
    E --> E1["Preference KV"]
    E --> E2["Tool Usage Patterns"]
    
    B --> F["Context Builder"]
    F --> G["LLM Context Window"]
    
    B --> H["Memory Maintenance"]
    H --> H1["TTL Expiry"]
    H --> H2["Consolidation"]
    H --> H3["Importance Scoring"]

核心思路是:不在同一个存储系统里解决所有问题。 让每种记忆用最合适的工具管理,由 Orchestrator 统一协调"当前上下文里应该放什么"。


三、Episodic Memory(情节记忆):最基础也最容易搞砸的部分

问题本质

Episodic Memory(情节记忆)是一个时序日志。它的核心约束是:

  • 写多读少(每次交互都写,只在构建上下文时读)
  • 越旧的信息价值越低(但不一定为零)
  • 总量线性增长,必须在某个点截断或压缩

常见实现方式对比

方案优点缺点适合场景
全量塞入 Context零实现成本Token 爆炸,稀释注意力短对话(<10 轮)
Sliding Window(截断)简单可控丢失早期关键信息通用兜底
定期 Summarization压缩效果好有信息损失,增加 LLM 调用成本长对话,对信息完整性要求适中的场景
分层 Summarization保留细节和概要实现复杂,管理多级缓存极长会话(企业级场景)
Rolling Window + RAG兼顾近期细节和远期事实需要嵌入和检索设施大多数生产场景

推荐方案:分层管理

当前轮次(完整保留) → 最近 N 轮(滑动窗口) → 历史总结(压缩存储) → 必要时检索

具体来说:

  1. 当前轮次:完整保留本轮用户输入和 Agent 输出,用于本轮后续调用。
  2. 滑动窗口:保留最近 K 轮完整对话(K 取决于模型上下文长度和预算,一般 10-20 轮)。
  3. 历史总结:超出滑动窗口的内容,定期由 LLM 总结后压缩存储。
  4. 检索补充:当需要历史细节时,从原始日志中通过向量检索找回。

这个分层的好处是:日常推理只在滑动窗口和当前轮次上进行,不增加延迟。 只有确实需要历史信息时,才触发检索路径。

工程实现要点

以下是分层记忆管理的完整实现,Python 和 Java 各一版。两者都包含:滑动窗口、历史总结、向量检索补充、TTL 过期。

from collections import deque
from datetime import datetime, timedelta
from typing import Optional
import json

class EpisodicMemory:
    """分层情节记忆:当前轮次 → 滑动窗口 → 历史总结 → 向量检索"""

    def __init__(self,
                 sliding_window_size: int = 20,
                 summary_llm=None,
                 vector_store=None,
                 ttl_days: int = 30):
        self.window = deque(maxlen=sliding_window_size)
        self.summary: Optional[str] = None
        self.raw_store: list = []          # 完整日志,带时间戳
        self.summary_llm = summary_llm     # 用于总结的 LLM
        self.vector_store = vector_store   # 向量检索后端
        self.ttl = timedelta(days=ttl_days)

    def add(self, turn: dict):
        """写入一轮对话,附带时间戳"""
        turn["timestamp"] = datetime.utcnow().isoformat()
        self.raw_store.append(turn)
        self.window.append(turn)

    def build_context(self,
                      need_detail: bool = False,
                      query: Optional[str] = None) -> list:
        """组装最终送入 LLM 的上下文"""
        context = []

        # 1. 注入历史总结
        if self.summary:
            context.append({
                "role": "system",
                "content": f"[历史摘要] {self.summary}"
            })

        # 2. 滑动窗口(最近 K 轮)
        context.extend(list(self.window))

        # 3. 检索补充:按需从向量库召回
        if need_detail and query and self.vector_store:
            retrieved = self.vector_store.similarity_search(query, k=3)
            for doc in retrieved:
                context.append({
                    "role": "system",
                    "content": f"[历史记录] {doc}"
                })

        return context

    def summarize(self):
        """将窗口中较旧的内容压缩为摘要"""
        if not self.summary_llm:
            return
        # 保留最近 5 轮不压缩,压缩之前的
        turns_to_summarize = list(self.window)[:-5]
        if not turns_to_summarize:
            return

        prompt = (
            f"以下是对话历史,请压缩为一段摘要(保留关键决定、"
            f"用户偏好、任务进展):\n{json.dumps(turns_to_summarize, ensure_ascii=False)}"
        )
        new_summary = self.summary_llm(prompt)

        # 增量合并摘要
        if self.summary:
            self.summary = (
                f"{self.summary}\n[增量] {new_summary}"
            )
        else:
            self.summary = new_summary

    def cleanup_expired(self):
        """删除超出 TTL 的原始日志"""
        cutoff = datetime.utcnow() - self.ttl
        self.raw_store = [
            t for t in self.raw_store
            if t["timestamp"] >= cutoff.isoformat()
        ]
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.function.Function;

/**
 * 分层情节记忆 —— 滑动窗口 + 历史总结 + 向量检索 + TTL 过期
 */
public class EpisodicMemory {
    private final int slidingWindowSize;
    private final Deque<Map<String, Object>> window;
    private final List<Map<String, Object>> rawStore;
    private String summary;
    private final Function<String, String> summaryLlm;
    private final Function<String, List<String>> vectorStore;
    private final Duration ttl;

    public EpisodicMemory(int slidingWindowSize,
                          Function<String, String> summaryLlm,
                          Function<String, List<String>> vectorStore,
                          Duration ttl) {
        this.slidingWindowSize = slidingWindowSize;
        this.window = new ArrayDeque<>(slidingWindowSize);
        this.rawStore = new ArrayList<>();
        this.summary = null;
        this.summaryLlm = summaryLlm;
        this.vectorStore = vectorStore;
        this.ttl = ttl;
    }

    /** 写入一轮对话,附带时间戳 */
    public void add(Map<String, Object> turn) {
        turn.put("timestamp", Instant.now().toString());
        rawStore.add(new HashMap<>(turn));
        if (window.size() >= slidingWindowSize) {
            window.pollFirst();
        }
        window.addLast(turn);
    }

    /** 组装最终送入 LLM 的上下文 */
    public List<Map<String, Object>> buildContext(
            boolean needDetail, String query) {
        List<Map<String, Object>> context = new ArrayList<>();

        // 1. 注入历史摘要
        if (summary != null && !summary.isEmpty()) {
            context.add(Map.of("role", "system",
                    "content", "[历史摘要] " + summary));
        }

        // 2. 滑动窗口
        context.addAll(window);

        // 3. 向量检索补充
        if (needDetail && query != null && vectorStore != null) {
            List<String> retrieved = vectorStore.apply(query);
            for (String doc : retrieved) {
                context.add(Map.of("role", "system",
                        "content", "[历史记录] " + doc));
            }
        }

        return context;
    }

    /** 将窗口中较旧的内容压缩为摘要 */
    public void summarize() {
        if (summaryLlm == null) return;

        List<Map<String, Object>> toSummarize = new ArrayList<>(window);
        // 保留最近 5 轮不压缩
        if (toSummarize.size() <= 5) return;
        toSummarize = toSummarize.subList(0, toSummarize.size() - 5);

        String prompt = "压缩以下对话历史为一段摘要(保留关键决定、"
                + "用户偏好、任务进展):" + toSummarize;
        String newSummary = summaryLlm.apply(prompt);

        if (summary != null) {
            summary = summary + "\n[增量] " + newSummary;
        } else {
            summary = newSummary;
        }
    }

    /** 清理超出 TTL 的原始日志 */
    public void cleanupExpired() {
        Instant cutoff = Instant.now().minus(ttl);
        rawStore.removeIf(turn -> {
            String ts = (String) turn.get("timestamp");
            return ts != null && Instant.parse(ts).isBefore(cutoff);
        });
    }
}

需要注意的是:总结也有信息损失。 是否需要检索能力来补充损失,取决于你的 Agent 在历史信息上的准确度要求。如果是客服场景,用户说"上次你们赔了我 500 块",你总结里没有这个数字,那就麻烦了。


四、Semantic Memory(语义记忆):从经历中提炼事实

这是最有价值、也最难做好的部分。

核心思路

不求"记住所有对话",而是从对话中提取有长期价值的事实

典型流程:

flowchart LR
    A["对话完成"] --> B["Extractor: 提取事实对"]
    B --> C["Deduplicator: 去重/合并"]
    C --> D[("Vector Store")]
    C --> E[("KV Store")]
    D --> F["Retriever: 语义检索"]
    E --> G["Retriever: 精确匹配"]

提取什么

经验来看,以下几类信息值得提取:

  1. 用户画像:姓名、偏好、技术栈、行业
  2. 正在进行的任务:项目名、目标、关键限制
  3. 重要约定:用户明确说过的偏好或规则
  4. 已排除的方案:避免重复推荐

存储选型

信息类型存储方案检索方式例子
事实性偏好KV Store (Redis / SQL)精确匹配user.preferred_language = "Java"
语义化知识Vector StoreTop-K 语义检索“用户不喜欢用 MongoDB”
实体关系Graph DB关系遍历“张三 → 负责 → 项目A”

提取策略:被动 vs 主动

被动提取:每轮对话完成后,由 LLM 从对话中提取事实。优点是简单,缺点是有延迟。

主动提取:Agent 在对话过程中发现新信息时主动写入。优点是实时,缺点是增加 Agent 决策复杂度。

生产建议是两者结合:Agent 在对话中主动写入明确的事实(如"我叫张三"),后台定期跑批处理做被动提取兜底。

关键难点:事实的更新与冲突

用户说"我喜欢 PostgreSQL",下个月说"我换用 MySQL 了"。

这里的处理策略:

  1. 时间戳标记:每个事实记录写入时间,检索时优先返回最新的
  2. 版本化存储:保留历史版本,可追溯
  3. 置信度评分:明确表达 > 间接推断 > 默认值
{
  "fact": "user.preferred_database",
  "value": "MySQL",
  "confidence": 0.95,
  "source": "explicit_statement",
  "updated_at": "2026-04-20T10:30:00Z",
  "history": [
    {"value": "PostgreSQL", "updated_at": "2026-03-15T08:00:00Z"}
  ]
}

五、Procedural Memory(程序记忆):Agent 怎么学会"做事情的方式"

这可能是三类记忆里最容易被忽视的。

Procedural Memory(程序记忆)解决的场景是:同一个用户,每一次交互 Agent 都要重新"试探"用户的偏好。比如:

  • 用户喜欢先看结论再看细节
  • 用户在做代码审查时希望逐文件过,不要一次性给所有 diff
  • 用户习惯用中文提问,但代码注释要用英文

这些信息既不是"情节"也不是"事实",而是做事的方式

实现方式

最简单的做法:在 KV Store 里存一组配置。

{
  "user_id": "zhangsan",
  "preferences": {
    "response_style": "concise_with_details",
    "code_review_mode": "per_file",
    "language": "zh",
    "code_comments": "en"
  }
}

进阶做法是让 Agent 自己学习和沉淀:

  1. 每次交互后,让 Agent 判断是否学到了用户的新偏好
  2. 如果识别到稳定的行为模式,写入 Procedural Memory(程序记忆)
  3. 下次交互时,将相关偏好注入 system prompt

何时写入

这是 Procedural Memory(程序记忆)最微妙的地方:写入太积极,用户会觉得你固执;写入太保守,又记不住。

一个工程上的折中是"三次确认"规则:

同一行为模式出现三次以上,才写入 Procedural Memory(程序记忆)。出现一次反例,移除或降级该模式。


六、Context Builder:决定往 Prompt 里塞什么

上面三类 Memory 是数据层,Orchestrator 需要把它们组装成最终送入 LLM 的上下文。

这是整个系统中对延迟和效果影响最大的环节。

组装策略

以下实现展示 Orchestrator 如何协调三层记忆,并按 Token 预算组装上下文。

from dataclasses import dataclass
from typing import Optional

@dataclass
class TokenBudget:
    """每类内容的 Token 预算软限制"""
    system_prompt: int = 1024
    procedural: int = 512
    semantic: int = 1024
    episodic: int = 4096
    current_turn: int = 1536

class ContextBuilder:
    """将三层记忆按策略组装为 LLM 上下文"""

    def __init__(self,
                 procedural_store,
                 semantic_store,
                 episodic_store,
                 budget: Optional[TokenBudget] = None):
        self.procedural_store = procedural_store
        self.semantic_store = semantic_store
        self.episodic_store = episodic_store
        self.budget = budget or TokenBudget()

    def build(self, request: str, user_id: str,
              scene: str = "general") -> list:
        """根据场景决定检索策略"""
        system_parts = []

        # 1. 程序记忆 —— 固定注入,体积小
        procedural = self.procedural_store.get(user_id)
        if procedural:
            system_parts.append({
                "role": "system",
                "content": f"[用户偏好] {self._truncate(procedural, self.budget.procedural)}"
            })

        # 2. 语义记忆 —— 按场景决定是否检索
        if self._should_retrieve(scene):
            semantic = self.semantic_store.query(
                user_id=user_id,
                query=request,
                top_k=5,
                min_score=0.7
            )
            if semantic:
                system_parts.append({
                    "role": "system",
                    "content": f"[相关事实] {self._truncate(semantic, self.budget.semantic)}"
                })

        # 3. 情节记忆 —— 分层加载
        need_detail = self._need_historical_detail(scene, request)
        episodic = self.episodic_store.build_context(
            need_detail=need_detail,
            query=request if need_detail else None
        )

        return system_parts + episodic

    def _should_retrieve(self, scene: str) -> bool:
        """按场景决定是否触发语义检索"""
        retrieval_scenes = {"task_continuation", "qa", "tool_call"}
        return scene in retrieval_scenes

    def _need_historical_detail(self, scene: str, request: str) -> bool:
        """判断是否需要检索历史细节"""
        if scene in {"task_continuation", "tool_call"}:
            return True
        if any(kw in request for kw in ["刚才", "之前", "上次", "还记得"]):
            return True
        return False

    @staticmethod
    def _truncate(text: str, max_tokens: int) -> str:
        """按 Token 预算截断(简易版,生产应使用 tokenizer)"""
        words = text.split()
        if len(words) * 1.3 > max_tokens:  # 粗略估算
            limit = int(max_tokens / 1.3)
            return " ".join(words[:limit]) + "..."
        return text
import java.util.*;

/**
 * 将三层记忆按策略组装为 LLM 上下文。
 * 支持 Token 预算控制和场景驱动的检索策略。
 */
public class ContextBuilder {
    private final ProceduralStore proceduralStore;
    private final SemanticStore semanticStore;
    private final EpisodicMemory episodicStore;
    private final TokenBudget budget;

    public ContextBuilder(ProceduralStore proceduralStore,
                          SemanticStore semanticStore,
                          EpisodicMemory episodicStore,
                          TokenBudget budget) {
        this.proceduralStore = proceduralStore;
        this.semanticStore = semanticStore;
        this.episodicStore = episodicStore;
        this.budget = budget;
    }

    /** 按场景组装上下文 */
    public List<Map<String, Object>> build(String request, String userId, String scene) {
        List<Map<String, Object>> parts = new ArrayList<>();

        // 1. 程序记忆 —— 固定注入
        String procedural = proceduralStore.get(userId);
        if (procedural != null && !procedural.isEmpty()) {
            parts.add(Map.of("role", "system",
                    "content", "[用户偏好] " + truncate(procedural, budget.procedural)));
        }

        // 2. 语义记忆 —— 按场景检索
        if (shouldRetrieve(scene)) {
            List<SemanticResult> semantic = semanticStore.query(userId, request, 5, 0.7);
            if (!semantic.isEmpty()) {
                parts.add(Map.of("role", "system",
                        "content", "[相关事实] " + truncate(semantic.toString(), budget.semantic)));
            }
        }

        // 3. 情节记忆 —— 分层加载
        boolean needDetail = needHistoricalDetail(scene, request);
        List<Map<String, Object>> episodic = episodicStore.buildContext(needDetail, request);
        parts.addAll(episodic);

        return parts;
    }

    private boolean shouldRetrieve(String scene) {
        return Set.of("task_continuation", "qa", "tool_call").contains(scene);
    }

    private boolean needHistoricalDetail(String scene, String request) {
        if (Set.of("task_continuation", "tool_call").contains(scene)) return true;
        return request.contains("刚才") || request.contains("之前")
                || request.contains("上次") || request.contains("还记得");
    }

    private String truncate(String text, int maxTokens) {
        String[] words = text.split("\\s+");
        if (words.length * 1.3 > maxTokens) {
            int limit = (int) (maxTokens / 1.3);
            return String.join(" ", Arrays.copyOf(words, limit)) + "...";
        }
        return text;
    }

    /** Token 预算配置 */
    public record TokenBudget(
            int systemPrompt,
            int procedural,
            int semantic,
            int episodic,
            int currentTurn
    ) {
        public TokenBudget() {
            this(1024, 512, 1024, 4096, 1536);
        }
    }

    // 以下为依赖接口定义
    public interface ProceduralStore {
        String get(String userId);
    }
    public interface SemanticStore {
        List<SemanticResult> query(String userId, String query, int topK, double minScore);
    }
    public record SemanticResult(String fact, double score, String updatedAt) {}
}

Token 预算分配

生产实践中一个重要的经验:提前为不同类型的内容分配 Token 预算。 而不是"能塞多少塞多少"。

例如一个 8K 上下文的 Agent:

内容类型预算说明
System Prompt1K角色、规则、输出格式
Procedural Memory(程序记忆)0.5K用户偏好
Semantic Memory(语义记忆)1K检索到的相关事实
Episodic Window(情节窗口)4K最近 N 轮对话
当前轮次1.5K本次输入 + 中间结果

这个预算是个软限制——当某一项超过预算时,不是直接截断,而是压缩或降级。例如 Semantic Memory(语义记忆)有 10 条匹配结果但只放得下 5 条,就取置信度最高的 5 条。

检索触发策略

不是每一轮都需要检索。根据场景决定是否需要查 Semantic Memory(语义记忆):

  • 闲聊场景:不检索,只走 Episodic(情节记忆)
  • 任务延续场景:检索任务相关的 Semantic(语义)信息
  • 知识问答场景:优先检索 Semantic(语义),再查 Episodic(情节)
  • 工具调用场景:检索类似的工具使用案例

七、数据生命周期与维护

Memory 不是"写进去就不管了"。生产级系统必须考虑:

1. TTL 与过期

不同类型的记忆有不同的生命周期:

类型建议 TTL原因
Episodic(情节·原始日志)7-30 天用于调试和审计,过久无意义
Episodic(情节·总结)永久(可压缩)摘要本身占空间小
Semantic(语义·用户事实)90 天或用户明确更新长期偏好可能变化
Semantic(语义·会话事实)随会话结束例如"正在处理订单 #12345"
Procedural(程序)永久,但需要验证稳定的行为模式

2. Consolidation(合并)

定期将多条相关事实合并为更精简的表达。例如:

[1] 用户说喜欢 Java
[2] 用户说用 Spring Boot
[3] 用户说主要在写微服务
→ 合并:用户是 Java / Spring Boot 后端开发者,专注微服务架构

合并减少 Token 占用,也提高信息的密度。但需要注意:合并不可逆,必须保留原始数据用于回溯。

3. Importance Scoring(重要性评分)

不是所有信息都有长期保留价值。可以在写入时给每条记忆打分:

import re
from typing import Set, Optional

class ImportanceScorer:
    """记忆重要性评分 —— 多信号加权评估"""

    def __init__(self, current_goal: Optional[str] = None):
        self.current_goal = current_goal

    def score(self, content: str) -> float:
        signals = []

        # 用户明确强调
        if self._has_emphasis(content):
            signals.append((0.9, "user_emphasis"))

        # 包含个人身份信息
        if self._contains_pii(content):
            signals.append((0.85, "pii"))

        # 涉及当前任务目标
        if self._relates_to_goal(content):
            signals.append((0.75, "goal_relevant"))

        # 包含具体数值或时间
        if self._contains_specifics(content):
            signals.append((0.7, "has_specifics"))

        # 情感强烈
        if self._has_strong_sentiment(content):
            signals.append((0.6, "strong_sentiment"))

        # 默认识别
        signals.append((0.3, "default"))

        best_score, best_reason = max(signals, key=lambda x: x[0])
        return best_score

    # ---- 信号检测方法 ----
    _EMPHASIS_KEYWORDS: Set[str] = {"记住", "重要的是", "一定要注意", "千万别", "务必"}

    def _has_emphasis(self, text: str) -> bool:
        return any(kw in text for kw in self._EMPHASIS_KEYWORDS)

    def _contains_pii(self, text: str) -> bool:
        # 简单 PII 模式匹配
        patterns = [
            r'\b\d{3,4}-?\d{4}-?\d{4}\b',  # 手机号/银行卡
            r'[我](?:叫|是|的(?:名字|邮箱|电话)(?:是|为))[一-鿿]+',
        ]
        return any(re.search(p, text) for p in patterns)

    def _relates_to_goal(self, text: str) -> bool:
        if not self.current_goal:
            return False
        goal_keywords = set(self.current_goal.split())
        text_keywords = set(text.split())
        return len(goal_keywords & text_keywords) > 0

    _SPECIFICS_PATTERNS = [
        r'\d+',                             # 数字
        r'\d{4}-\d{2}-\d{2}',              # 日期
        r'https?://\S+',                    # 链接
    ]

    def _contains_specifics(self, text: str) -> bool:
        return any(re.search(p, text) for p in self._SPECIFICS_PATTERNS)

    def _has_strong_sentiment(self, text: str) -> bool:
        strong_words = {"糟糕", "太好了", "气死了", "完美", "绝对不行", "非常满意"}
        return any(w in text for w in strong_words)
import java.util.*;
import java.util.regex.Pattern;

/**
 * 记忆重要性评分 —— 多信号加权评估。
 * 用于判断哪些记忆值得长期保留。
 */
public class ImportanceScorer {

    private final String currentGoal;

    public ImportanceScorer(String currentGoal) {
        this.currentGoal = currentGoal;
    }

    /** 对一段记忆内容进行重要性评分 [0, 1] */
    public double score(String content) {
        List<ScoredSignal> signals = new ArrayList<>();

        if (hasEmphasis(content))
            signals.add(new ScoredSignal(0.9, "user_emphasis"));
        if (containsPii(content))
            signals.add(new ScoredSignal(0.85, "pii"));
        if (relatesToGoal(content))
            signals.add(new ScoredSignal(0.75, "goal_relevant"));
        if (containsSpecifics(content))
            signals.add(new ScoredSignal(0.70, "has_specifics"));
        if (hasStrongSentiment(content))
            signals.add(new ScoredSignal(0.60, "strong_sentiment"));

        signals.add(new ScoredSignal(0.30, "default"));

        return signals.stream()
                .mapToDouble(ScoredSignal::score)
                .max()
                .orElse(0.3);
    }

    // ---- 信号检测 ----

    private static final Set<String> EMPHASIS_KEYWORDS = Set.of(
            "记住", "重要的是", "一定要注意", "千万别", "务必");

    boolean hasEmphasis(String text) {
        return EMPHASIS_KEYWORDS.stream().anyMatch(text::contains);
    }

    private static final Pattern PII_PATTERN = Pattern.compile(
            "\\b\\d{3,4}-?\\d{4}-?\\d{4}\\b");

    boolean containsPii(String text) {
        return PII_PATTERN.matcher(text).find();
    }

    boolean relatesToGoal(String text) {
        if (currentGoal == null || currentGoal.isEmpty()) return false;
        // 简易关键词重叠判断
        Set<String> goalWords = new HashSet<>(Arrays.asList(currentGoal.split("\\s+")));
        Set<String> textWords = new HashSet<>(Arrays.asList(text.split("\\s+")));
        goalWords.retainAll(textWords);
        return !goalWords.isEmpty();
    }

    private static final Pattern SPECIFICS_PATTERN = Pattern.compile(
            "\\d+|\\d{4}-\\d{2}-\\d{2}|https?://\\S+");

    boolean containsSpecifics(String text) {
        return SPECIFICS_PATTERN.matcher(text).find();
    }

    private static final Set<String> STRONG_SENTIMENT_WORDS = Set.of(
            "糟糕", "太好了", "气死了", "完美", "绝对不行", "非常满意");

    boolean hasStrongSentiment(String text) {
        return STRONG_SENTIMENT_WORDS.stream().anyMatch(text::contains);
    }

    private record ScoredSignal(double score, String reason) {}
}

低分记忆在 Consolidation 阶段优先被合并或丢弃。


八、从 Claude Code 中看到的优秀设计

我在分析 Claude Code 源码 时,注意到它的 Memory 系统有一些值得借鉴的设计决策:

1. 文件即数据库

没有用向量数据库或 Redis,而是直接用文件系统(memory/ 目录)。每个记忆是一个独立的 .md 文件,通过 MEMORY.md 作为倒排索引管理。

这个设计的好处:

  • 零基础设施依赖:不需要额外启动 DB 服务
  • 可读可审计:任何时刻都能打开文件查看记忆内容
  • 版本控制友好:记忆变化可以通过 git diff 追踪

缺点也很明显:不适合大规模场景。但这个设计思路的价值在于:先选最简单的存储,等确实不够用了再升级。

2. 类型化记忆

Claude Code 把记忆分为 userfeedbackprojectreference 四种类型。每种类型有不同的写入策略、读取优先级的更新逻辑。

类型化的意义:不是所有的记忆都被同等对待。

  • user 记忆在每次对话开始时加载
  • feedback 记忆在需要判断行为模式时检索
  • project 记忆有有效期,过期自动忽略

3. 检索时的验证机制

它对每个从记忆中读取的"事实"都做验证:

如果记忆提到函数 X 存在 → 调用前检查文件是否存在
如果记忆提到标志位 Y 存在 → grep 确认

这个机制解决了 Memory 系统中一个核心问题:记忆是过去某个时刻的快照,不是当前状态。 直接信任记忆而不验证,会导致 Agent 引用已经不存在的东西。

4. 不混淆"记忆"和"任务状态"

Claude Code 明确把当前对话的工作状态(plan、tasks)和跨对话的记忆分开。Plan 和 Task 是临时工作产物,Memory 是长期沉淀。这两类信息有不同的生命周期管理策略,放在一起会导致混乱。


九、主流框架的 Memory 实现案例

理论讲完,来看几个主流框架实际怎么做 Memory。它们的取舍能帮你理解"理论到工程"的映射。

1. LangChain:模块化 Memory 体系

LangChain 把 Memory 设计为独立的 Chain 间的状态传递层,提供多种 Memory 类型:

Memory 类型底层原理适用场景
ConversationBufferMemory全量缓存对话历史到列表短对话、调试
ConversationSummaryMemory每次交互后 LLM 总结,存摘要长对话压缩
ConversationBufferWindowMemory固定大小滑动窗口通用兜底
VectorStoreRetrieverMemory对话存入向量库,按相似度检索需要历史回忆的场景
PostgresChatMessageHistoryPostgreSQL 持久化生产环境、多实例

关键设计思路:LangChain 把 Memory 抽象为两个操作 —— load_memory_variables(读)和 save_context(写),你可以自由组合不同的存储后端与不同的 Memory 策略。

from langchain.memory import ConversationSummaryBufferMemory
from langchain_community.chat_message_histories import SQLChatMessageHistory

memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=2000,
    chat_memory=SQLChatMessageHistory(
        session_id="user_123",
        connection_string="sqlite:///memory.db"
    ),
    return_messages=True
)

优缺点:模块化好、可组合性强;但 Memory 和 Agent 逻辑耦合较紧,大量对话时 performance 是瓶颈。

2. CrewAI:基于角色的记忆分工

CrewAI 的 Memory 系统设计更贴近"多 Agent 协作"场景。每个 Agent 可以有独立的记忆,支持三类:

  • ShortTermMemory:基于 ConversationBufferWindowMemory,保存最近交互
  • LongTermMemory:基于 SQLite,记录任务执行的任务和结果
  • EntityMemory:基于向量存储,提取和记住任务中提到的实体信息
from crewai import Agent, Task, Crew, Process
from crewai.memory import LongTermMemory

agent = Agent(
    role="数据分析师",
    goal="分析用户数据并提供洞察",
    memory=True,                     # 开启记忆
    long_term_memory=LongTermMemory() # 使用长期记忆
)

优缺点:开箱即用、多 Agent 场景表现出色;但定制灵活性不如 LangChain,Memory 类型扩展受限。

3. Mem0:专注"记忆层"的独立框架

Mem0 是一个专注于 LLM Memory 层的独立框架(可以看作"Memory 即服务")。它的设计思路与本文最接近:

  • 三层记忆:User Memory、Session Memory、Agent Memory
  • 自动提取:从对话中自动提取事实和偏好
  • 历史版本:记录事实的变更历史,支持回滚
  • 多种存储:支持向量库(Qdrant、Pinecone)、图数据库、KV 存储
from mem0 import Memory

m = Memory(
    vector_store="qdrant",          # 向量存储
    graph_store="neo4j",            # 图存储(可选)
    llm="gpt-4",                    # 用于事实提取
)

# 写入记忆
m.add("用户说:我喜欢 PostgreSQL,但最近换成了 MySQL", user_id="zhangsan")

# 检索记忆
results = m.search("用户用什么数据库", user_id="zhangsan")
print(results)
# → "用户偏好数据库从 PostgreSQL 切换到了 MySQL(更新于 2026-04-20)"

优缺点:独立于任何 Agent 框架,API 干净,三层的设计理念和本文一致;但较新,生态不够成熟,大规模场景的性能需验证。

4. Spring AI:Java 生态的 Memory 实现

Spring AI 是为 Java / Spring 生态设计的 AI 框架,其 Memory 通过 ChatMemory 接口抽象,并提供开箱即用的实现:

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory;

// 内存实现 —— 开发和测试
ChatMemory memory = new InMemoryChatMemory();

// JDBC 实现 —— 生产环境,自动建表
ChatMemory jdbcMemory = new JdbcChatMemory(dataSource);

// 使用:在 ConversationChain 中注入
ConversationChain chain = new ConversationChain(
    chatClient,
    "user-session-001",
    jdbcMemory
);

优缺点:Java / Spring 生态的无缝集成,生产级 JDBC 持久化;但目前只有"全量记录"和"滑动窗口"两种策略(通过 PromptMessageUtils 截断),缺少语义记忆和向量检索支持。需要自行对接 Spring AI 的 VectorStore 来补充。

5. AgentScope4J:observe/call 分离与 MsgHub 记忆共享

AgentScope4J 是一个 Java 生态的多 Agent 框架,它的 Memory 设计有些独特的工程决策。

核心设计:observe 与 call 分离

这是 AgentScope4J 最值得注意的设计——把"写入记忆"和"触发推理"拆成两个独立操作:

ReActAgent agent = ReActAgent.builder()
        .name("analyst")
        .sysPrompt("你是研究员,先澄清问题,再调用工具")
        .memory(windowMemory)    // 短期记忆
        .maxTurns(8)
        .build();

agent.observe(Msg.user("请分析 A 公司利润变化")).block();  // 只写记忆,不触发模型
Msg answer = agent.call().block();                         // 读取记忆,执行推理

为什么这样设计?

  • 防止"收到消息就自动回复"的无限连锁反应
  • 让调度层精准控制"何时生成",而不是让模型决定
  • 回放和调试更容易:输入与触发时机是可复现的

记忆的写入与读取路径

AgentScope4J 的 Memory 是一个接口,WindowMemory 是默认实现:

// 写入路径
protected Mono<Void> doObserve(Msg msg) {
    memory.addMessage(msg);   // 追加到滑动窗口
    return Mono.empty();
}

// 读取路径 — call() 时构建快照
public Mono<Msg> call() {
    return plannerLoop(memory.snapshot());  // 快照不可变,避免并发问题
}

snapshot() 返回当前记忆的不可变副本,后续工具执行结果通过 observe(Msg.tool(result)) 写回,形成"读时快照、写时追加"的天然隔离。

MsgHub:多 Agent 记忆共享

AgentScope4J 通过 MsgHub 实现跨 Agent 记忆共享:

try (MsgHub hub = MsgHub.builder()
        .name("analysis_hub")
        .participants(researcher, riskReviewer, writer)
        .announcement(Msg.system("请围绕同一主题协作"))
        .enableAutoBroadcast(true)
        .build()) {

    hub.enter().block();
    researcher.call().block();   // call 完成后,结果自动 broadcast
    riskReviewer.call().block(); // 上一轮结果已通过 observe 写入记忆
    writer.call().block();
}

enableAutoBroadcast(true) 的含义:某个 Agent call() 完成后,其输出通过 broadcast 推送给其他参与者并触发 observe(),从而实现"共享上下文但不自动触发推理"——避免了 Agent 间无限连锁调用。

ReAct 循环中的记忆流转

在 AgentScope4J 的 ReAct 实现中,每轮推理都从 memory.snapshot() 构建上下文,工具结果写回后进入下一轮:

for (int i = 0; i < maxTurns; i++) {
    ModelOutput out = llm.generate(memory.snapshot());  // 读快照
    if (out.hasToolCall()) {
        ToolResult tr = toolGateway.invoke(out.toolCall());
        memory.addMessage(Msg.tool(tr));                // 写回记忆
        continue;
    }
    return Msg.assistant(out.finalAnswer());
}

这套机制映射到本文的三层架构来看:

本文概念AgentScope4J 对应
Episodic Memory(情节)WindowMemory + observe() 追加
Semantic Memory(语义)需自行实现,框架提供 observe 扩展点
Procedural Memory(程序)通过 system prompt 动态注入
Context Buildermemory.snapshot() 构建不可变上下文

优缺点:observe/call 分离是工程上非常务实的设计,MsgbHub 的多 Agent 记忆共享机制简洁干净;但 Semantic Memory 和 Procedural Memory 需要自行扩展,框架本身没有提供事实提取和检索的基础设施。

6. Hermes Agent(Nous Research):四层记忆与可插拔 Provider

Hermes Agent 是 Nous Research 开发的自主 AI Agent 框架,它的 Memory 系统是目前开源社区中最完整的设计之一,分为 四层内建记忆 + 可插拔外部 Provider

架构总览

flowchart TD
    A["System Prompt"] --> B["Layer 1: Prompt Memory"]
    B --> B1["MEMORY.md(~800 tokens)"]
    B --> B2["USER.md(~500 tokens)"]
    
    A --> C["Layer 2: Session Archive"]
    C --> C1["SQLite + FTS5 全文检索"]
    
    A --> D["Layer 3: Skills"]
    D --> D1["Markdown 技能文件"]
    
    A --> E["Layer 4: External Provider"]
    E --> E1["Hindsight / Mem0 / OpenViking…"]

Layer 1:Prompt Memory(热记忆)

Hermes 最核心的设计:每次会话启动时,将 MEMORY.mdUSER.md 的内容作为固定快照注入 system prompt。

# MEMORY.md 存放的内容
- 持久事实(用户的长期偏好、项目约定)
- 环境信息(开发环境、API Key 位置)
- 已确定的决策记录

# USER.md 存放的内容
- 用户基本信息
- 交互风格偏好
- 高频需求模式

关键机制 —— Frozen Snapshot(冻结快照):

  • 会话启动时,读取文件内容,冻结为 system prompt 的一部分
  • 会话中写入的修改立即持久化到磁盘,但不会出现在当前会话的 prompt 中
  • 修改在下一次会话启动时才生效

这样做的目的是保持 LLM Prefix Cache 的稳定性——如果 prompt 在会话中变化,缓存命中率会大幅下降。这是工程效率和 Agent 智能之间的一个务实取舍。

Layer 2:Session Archive(冷记忆)

所有 CLI 交互和消息历史存储在 SQLite 数据库中(~/.hermes/state.db),使用 FTS5 全文检索引擎。

# Hermes 通过 session_search 工具按需检索历史
result = agent.search_session("我们之前讨论过什么数据库方案?")
# → FTS5 全文匹配,返回相关历史片段

Agent 可以在对话中自主决定何时调用 session_search 工具来回忆历史——不需要每轮都加载。

Layer 3:Skills(程序记忆)

Hermes 将"做事的方式"沉淀为 Skill 文件,存储在 ~/.hermes/skills/ 目录下:

  • 每个 Skill 是一个 Markdown 文件,描述完成某类任务的步骤
  • 任务完成后由 Agent 自动创建(反应式学习)
  • 后续遇到类似任务时,Agent 自动检索并复用相关 Skill
  • 通过复用和修正实现自我进化

这是与本文的 Procedural Memory 最直接对应的实现——Agent 自己学会怎么做事情。

Layer 4:External Provider(可插拔外部记忆)

Hermes 近期推出了统一的 Pluggable Memory Provider 系统,运行 hermes memory setup 选择:

Provider存储核心特性LongMemEval
HindsightPostgreSQL(本地/云)知识图谱 + 结构化事实 + 反射合成91.4%
Holographic本地 SQLiteHRR 代数 + 信任评分,零依赖
OpenViking自托管L0/L1/L2 分层加载,节省 80-90% Token
Mem0云端最快部署(30 秒),服务端 LLM 提取67.6%
Honcho云端辩证用户建模(学习你的思维方式)
ByteRover本地/云可读 Markdown 知识树 + 预压缩提取
RetainDB云端混合搜索(向量 + BM25 + 重排序)

Hermes Memory Provider 不是 RAG。RAG 解决的是"模型知识不足,检索外部文档增强回答"——读多写少,写入后基本不变。Memory Provider 解决的是"Agent 需要跨会话记住用户、事实和偏好"——持续读写,需要处理事实更新、冲突解决、版本追溯。两者的核心区别在于写入频率和更新语义。Provider 中的 RetainDB 和 OpenViking 确实用到了向量检索技术,但那是实现手段,不是设计目的。

重要设计原则:外部 Provider 是叠加在内建记忆之上的,不替代内建记忆。MEMORY.md / USER.md 始终活跃。

与本文三层架构的映射

本文概念Hermes 对应实现
Episodic Memory(情节)FTS5 Session Archive + 全量会话历史
Semantic Memory(语义)MEMORY.md(结构化事实)+ 外部 Provider(知识图谱)
Procedural Memory(程序)Skills 目录 + 自动创建/复用机制
Context BuilderFrozen Snapshot 注入 system prompt
Memory MaintenanceTTL 由 Provider 管理 + Agent 自主 curation

优缺点:四层记忆设计是目前开源社区最完整的方案之一,Frozen Snapshot 机制在工程效率上非常聪明,7 种可插拔 Provider 覆盖了从本地到云端的各种场景;但对于简单场景来说配置偏重,核心的 FTS5 检索依赖关键词匹配(语义检索需要外部 Provider),Agent 自主 curation 在短会话中可能导致 MEMORY.md 为空。

7. OpenClaw:文件即记忆的"梦境"系统

OpenClaw 是一个以 文件优先、本地优先 为设计哲学的 AI Agent 框架。它的记忆系统非常独特:以 Markdown 文件为数据源,通过一个名为 Dreaming(梦境) 的异步机制完成记忆的沉淀和固化。

记忆文件体系

OpenClaw 的记忆 = 一组有明确职责划分的 Markdown 文件:

文件职责读写时机
MEMORY.md长期记忆(决策、事实、经验教训)Agent 自主写入,Dreaming 晋升
memory/YYYY-MM-DD.md每日对话日志 / 会话笔记每次会话自动追加
AGENTS.md工作区规则与安全边界启动时加载
USER.md用户画像(姓名、偏好等)Agent 写入,跨会话持久
SOUL.mdAgent 人格与价值观定义几乎不变
DREAMS.mdDreaming 系统输出(仅人工审查)Dreaming 写入

这个设计与 Claude Code 的文件即记忆思路同源,但多了 SOUL.md 和 DREAMS.md 两个概念。

内建记忆组件

OpenClaw 提供两个开箱即用的记忆后端:

# 默认:memory-core —— 本地 Markdown + 混合检索
from openclaw.memory import MemoryCore
memory = MemoryCore()  # BM25 + 向量混合搜索

# 可选:memory-lancedb —— 本地向量数据库
from openclaw.memory import LanceDBMemory
memory = LanceDBMemory()  # 自动召回 + 自动捕获

关键限制:两者都缺乏多用户隔离,所有用户的数据混在同一存储空间中,会导致"记忆污染"。

Dreaming System(梦境系统):异步记忆固化

这是 OpenClaw 最具创新性的设计 —— 一个基于 Cron 调度的后台记忆处理管线,模拟人类睡眠的记忆固化过程:

Light Sleep(浅睡)→ REM Sleep(快速眼动)→ Deep Sleep(深睡)

Light Sleep(浅睡):摄入对话日志,做 Jaccard 去重

REM Sleep(快速眼动):模式分析,候选事实选择,置信度评分

Deep Sleep(深睡):六维评分筛选候选事实,达到阈值后晋升到 MEMORY.md

评分维度含义
Frequency(频率)该信息出现的次数
Relevance(相关性)与当前任务的相关程度
Diversity(多样性)覆盖不同场景的程度
Recency(时效性)最近是否还被提及
Consolidation(整合度)与其他事实的关联程度
Conceptual Richness(概念丰富度)信息的抽象层次

默认晋升条件:综合评分 ≥ 0.80,且至少被 3 个独立信号触发。

企业级方案

OpenClaw 社区和云厂商提供了多个企业级记忆扩展:

方案提供方核心能力
memory-agentcoreAWSBedrock AgentCore Memory,4 种提取策略,层级命名空间,多用户隔离
AgentLoop MemoryStore阿里云多维提取(偏好/事实/摘要/情节),异步非阻塞管线,L3 Agentic 检索
PolarDB Mem0阿里云云原生长期记忆,事实自动演化与冲突解决,多设备同步
@mem0/openclaw-mem0Mem0系统层强制捕获/召回,每轮自动 capture + 自动 recall

与本文三层架构的映射

本文概念OpenClaw 对应实现
Episodic Memory(情节)memory/YYYY-MM-DD.md 每日日志
Semantic Memory(语义)MEMORY.md + Dreaming 系统 + 外部 Provider
Procedural Memory(程序)AGENTS.md(规则)+ Agent 配置文件
Context Builder文件读取 → 注入 system prompt
Memory Maintenance(维护)Dreaming 管线(Light → REM → Deep Sleep)

优缺点:Dreaming 系统是唯一一个模拟生物记忆固化机制的工程实现,文件即记忆的设计非常透明可审计,企业级方案丰富;但内建组件缺乏多用户隔离,Dreaming 的 Jaccard 去重无法处理语义级别的重复,统计评分机制(信号计数)而非语义重要性判断可能导致关键信息被遗漏。


8. AutoGen(Microsoft):对话驱动的记忆共享

Microsoft AutoGen 通过 Agent 间的对话驱动模式管理记忆。每个 Agent 有自己的上下文,通过 ConversableAgentchat_history 管理和检索历史:

from autogen import ConversableAgent, AssistantAgent

# Agent 1:用户代理
user = ConversableAgent("user", 
    max_consecutive_auto_reply=0,
    human_input_mode="ALWAYS")

# Agent 2:带记忆的助手
assistant = AssistantAgent("assistant",
    llm_config={"config_list": [{"model": "gpt-4"}]},
    system_message="你是有记忆的助手")
    
# 多轮对话 —— 记忆在 chat_history 中累积
user.initiate_chat(assistant, message="我上周说的项目方案还记得吗?")

# 查看历史
print(len(assistant.chat_history))  # 累计消息数

优缺点:多 Agent 通信和记忆共享机制灵活;但本身不提供长期记忆持久化或语义检索,生产部署需要用 RedisMongoDB 做外部存储扩展。


框架对比小结:Hermes vs OpenClaw

在所有框架中,Hermes Agent 和 OpenClaw 代表了两种差异鲜明的设计哲学。放在一起对比,能更清楚地看到 Memory 系统设计中的核心取舍:

对比维度Hermes AgentOpenClaw
设计哲学多层架构 + 可插拔 Provider文件即记忆 + 生物模拟
热记忆MEMORY.md + USER.md,Frozen Snapshot 注入 system prompt,会话中不更新MEMORY.md + USER.md + SOUL.md(人格)+ AGENTS.md(规则),直接读取注入
冷记忆SQLite + FTS5 全文检索,按需 session_searchmemory/YYYY-MM-DD.md 每日日志,按文件时间访问
程序记忆Skills 目录,Agent 任务完成后自动创建/复用AGENTS.md 中定义的规则,偏静态配置
最大亮点Frozen Snapshot —— 保护 LLM Prefix Cache,工程效率极高Dreaming System —— Light/REM/Deep Sleep 三级管线,模拟生物记忆固化
记忆固化方式Agent 自主 curation + Provider 后台提取异步 Cron 调度,六维评分晋升到 MEMORY.md
外部扩展7 种 Provider(Hindsight/Mem0/OpenViking 等),统一 Pluggable 接口5 种企业方案(AWS Bedrock/阿里云 AgentLoop/PolarDB/Mem0)
多用户隔离由 Provider 各自实现内建组件不支持,企业级方案才支持
语义检索依赖外部 Provider(内建 FTS5 仅关键词)内建 memory-core 支持 BM25 + 向量混合搜索
Agent 自主性Agent 决定写什么到 MEMORY.md,短会话可能为空Dreaming 异步处理,不依赖 Agent 实时决策
运维复杂度中等,Provider 配置需要学习成本低(内建)/ 高(企业方案),Dreaming 需要 Cron
最适合场景需要长期连续对话 + 跨会话记忆的项目单用户深度使用 + 对记忆透明度要求高的场景

一句话区分:Hermes 的强项是工程效率(Frozen Snapshot + Pluggable Provider),OpenClaw 的强项是记忆固化的仿生机制(Dreaming System)。选择哪个,取决于你对"记忆"的核心诉求是"低成本的跨会话持久化"还是"高保真的信息沉淀"。


十、生产级 Checklist

如果你正在设计一个 Agent Memory 系统,以下是需要回答的问题清单:

存储层

  • 三类记忆有没有明确的分离?
  • 每种记忆的存储选型是否匹配其访问模式?
  • 写入路径是否考虑了去重和更新?
  • 是否有 TTL 和过期策略?

检索层

  • 检索是不是每次请求都做,还是按需触发?
  • 检索结果的排序和过滤策略是什么?
  • 跨 Session 的记忆共享规则是什么?
  • 事实更新后,旧版本的缓存是否及时失效?

上下文组装层

  • 不同类型的记忆按什么优先级注入 Prompt?
  • Token 预算有没有按类型分配?
  • 长上下文的压缩/截断策略是什么?

运维层

  • 记忆系统的读写延迟是否可观测?
  • 是否有审计日志追踪"Agent 读了哪些记忆"?
  • 当记忆数据损坏或错误时,能否手动修正?

十一、总结

Agent Memory 不是一个"用向量数据库存起来"就能解决的问题。它是一个从数据采集 → 提取 → 存储 → 检索 → 组装 → 维护的完整链路。每个环节都有自己的权衡:

  • 存得越多,检索越慢,上下文越臃肿
  • 提取越积极,信息损失越少,但噪音也越多
  • 跨 Session 共享越开放,Agent 越智能,隐私风险越大

好的 Memory 系统不是把所有信息都记住,而是在正确的时候,把正确的信息,以正确的形式,放到正确的位置。

生产级的设计思路可以总结为:

  1. 分类型管理,不用一种方案解决所有问题
  2. 分层存储,热数据放窗口、冷数据放检索
  3. 按需检索,不每轮都查,减少无效延迟
  4. 验证而非信任,记忆是过去状态的快照,使用前需要验证
  5. 从简单开始,先用文件 + KV,不够再加向量和 Graph