上一篇文章讲了 Tools,给 Agent 安上了"手"。今天咱们聊聊 Memory,给 Agent 装上"脑子"。核心就是解决 LLM 的"失忆症"问题——短期记忆管上下文,长期记忆管跨会话。记住一件事:别一上来就搞向量库,简单对话用滑动窗口就够了,够用就行。

你有没有发现一个很尴尬的事情?

你跟 LLM 说"我叫张三",它会说"你好张三"。

五分钟后再问"我叫什么?“,它会说"不知道,你还没告诉我”。

这就是 LLM 的"失忆症"——它是无状态的。

LLM 的 Context Window 确实能存储对话历史,但问题在于:

  1. 长度限制:大部分模型的 Context Window 只有 4k-128k tokens,对话一长就爆了
  2. 跨会话丢失:你关掉对话,下次打开,LLM 完全不记得你说过什么
  3. 检索困难:历史对话全都混在一起,想找半年前说的话,大海捞针

这些问题,Memory 都能解决。


Memory 有哪几类?

Memory 这个东西,从不同角度看,分法不一样。

┌─────────────────────────────────────────┐
  系统分析维度(论文元框架) 
          
  形式(Form) / 功能(Function) / 动态(Dynamics) 
  → 回答:"怎么评价一个记忆系统的设计?"      
├─────────────────────────────────────────┤
  认知内容维度(4类记忆)  
            
  工作 / 情景 / 语义 / 程序                 
  → 回答:"Agent 应该记住什么类型的东西?"     
├─────────────────────────────────────────┤
  工程时间维度(2类记忆)  
            
  短期记忆 / 长期记忆                        
  → 回答:"数据存在哪里、存多久?"            
└─────────────────────────────────────────┘

从时间维度看

短期记忆:当前对话的上下文,存在 LLM 的 Context Window 里。

就像人的"工作记忆"——你正在想的事情,暂时存在脑子里,过一会儿就忘了。

优点:响应快,直接用就行。

缺点:有长度限制,跨会话就丢了。

长期记忆:跨会话持久化,存储在外部数据库中。

就像人的"长期记忆"——你记得去年去过的景区、十年前见过的人。

优点:理论上无限,跨会话保留。

缺点:需要额外存储,检索有开销。

从认知内容维度看

工作记忆(Working Memory):当前对话上下文、激活的事实、中间推理步骤。

就像你做数学题时,脑子里正在算的那一步。

情景记忆(Episodic Memory):具体对话事件、用户行为历史、时间线。

就像你记得"上周三和客户开会,客户提了三个需求"。

语义记忆(Semantic Memory):用户画像、领域知识、概念关系。

就像你知道"用户是技术团队负责人"、“行业是电商”。

程序记忆(Procedural Memory):工具调用流程、审批规则、标准操作 SOP。

就像你记得"遇到退款申请,先查订单,再算金额,最后发邮件"。


短期记忆怎么实现?

短期记忆的实现方式有四种:

1. 全量保留

什么都不做,把所有历史对话直接拼接到 Prompt 中。

class NaiveMemory:
    def __init__(self):
        self.messages = []
    
    def add(self, role, content):
        self.messages.append({"role": role, "content": content})
    
    def get(self):
	    # 原封不动返回所有历史
        return self.messages

适用场景:极短对话、测试阶段、Demo 演示。

优点:简单粗暴,什么都不用做。

缺点:对话稍长就会崩,Token 费用高。

2. 滑动窗口

只保留最近 K 轮对话,丢弃更早的。

class SlidingWindowMemory:
    def __init__(self, max_rounds=3):
        self.max_rounds = max_rounds  # 保留的轮数(1轮=1问1答)
        self.history = []

    def add(self, role: str, content: str):
        self.history.append({"role": role, "content": content})
        # 维持窗口大小,超过则切除最旧的消息
        # max_rounds * 2 代表 max_rounds 轮的问答消息数
        if len(self.history) > self.max_rounds * 2:
            self.history = self.history[-self.max_rounds * 2:]

    def get_prompt(self):
        return self.history

适用场景:需要限制 Token 成本、对话时长可控的场景。

优点:实现简单,能控制 Token 用量。

缺点:记忆力"金鱼化",关键信息丢了就找不回。

比如你跟 Agent 说"我下周要出差,帮我安排行程",然后聊了 10 轮别的,最后问"下周去哪出差?"——它忘了,因为你刚才说的内容被滑动窗口切掉了。

3. Token 感知截断

设定一个 Token 上限,每次获取记忆时,如果超限,保留高重要性消息,从最旧的消息开始逐条删除。

怎么评分? 有几种策略:

  • 位置评分:最近的消息得分高
  • 关键词评分:包含用户明确提到的信息(如"我姓王")得分高
  • 情感评分:用户表达强烈意见(如"我讨厌这个")得分高
  • 引用评分:被后续消息引用过的得分高
import tiktoken

class ProTokenAwareMemory:
    def __init__(self, max_tokens=4000, model="gpt-4o"):
        self.messages = []
        self.max_tokens = max_tokens
        self.encoding = tiktoken.encoding_for_model(model)
        self._seq_counter = 0  # 显式的全局递增序列号,替代 id(m)

    def count(self, text):
        return len(self.encoding.encode(text))

    def add(self, role, content, importance=1):
        self._seq_counter += 1
        msg = {
            "role": role,
            "content": content,
            "importance": importance,  # 1=普通, 2=重要, 3=系统指令
            "tokens": self.count(content) + 4, # +4 是格式开销
            "seq": self._seq_counter   # 明确记录插入的时间顺序
        }
        self.messages.append(msg)
        self._truncate()

    def _truncate(self):
        total = sum(m["tokens"] for m in self.messages)
        
        while total > self.max_tokens and len(self.messages) > 2:
            # 1. 绝不删除第一条和最后一条
            candidates = self.messages[1:-1]
            if not candidates:
                # 如果中间部分为空,说明是最后一条消息太长导致超限
                # 此时只能忍痛截断最后一条消息的内容,或者丢弃(根据业务决定)
                # 这里采用简单丢弃策略,防止死循环
                if total > self.max_tokens and len(self.messages) > 1:
                    removed = self.messages.pop(1) # 丢弃第2条
                    total -= removed["tokens"]
                continue

            # 2. 找到优先级最低(importance最小)且最旧(seq最小)的消息
            to_remove = min(candidates, key=lambda m: (m["importance"], m["seq"]))
            
            # 3. 使用索引删除,比 list.remove() 更高效且安全
            idx = self.messages.index(to_remove)
            self.messages.pop(idx)
            
            # 4. 重新计算总 Token
            total = sum(m["tokens"] for m in self.messages)

    def get(self):
        # 返回标准格式,去掉内部字段
        return [{"role": m["role"], "content": m["content"]} for m in self.messages]



适用场景:生产环境、需要平衡记忆容量和成本的场景。

优点:比滑动窗口更智能,能保留重要信息。

缺点:需要设计评分策略,有一定复杂度。

4. 总结压缩

当对话达到一定长度,调用一次 LLM,把前面的对话总结成一段摘要。下次发送 Prompt 时,用"历史摘要+近期对话"代替全量历史。

import tiktoken

class UltimateSummaryMemory:
    def __init__(self, llm_client, max_raw=6, max_summary_tokens=500, model="gpt-4"):
        self.llm = llm_client
        self.summary = ""                # 压缩后的历史摘要
        self.recent = []                 # 最近未压缩的原始消息
        self.max_raw = max_raw           # 触发压缩的近期消息阈值
        self.max_summary_tokens = max_summary_tokens
        
        # 初始化 Token 计算器
        try:
            self.encoder = tiktoken.encoding_for_model(model)
        except KeyError:
            self.encoder = tiktoken.get_encoding("cl100k_base") # 默认回退

    def _count_tokens(self, text):
        return len(self.encoder.encode(text))

    def _safe_truncate(self, text, max_tokens):
        """安全的 Token 截断:优先保留完整句子"""
        tokens = self.encoder.encode(text)
        if len(tokens) <= max_tokens:
            return text
        
        # 预留 3 个 token 给 "..."
        truncated_tokens = tokens[:max_tokens - 3]
        decoded_text = self.encoder.decode(truncated_tokens)
        
        # 寻找最近的句子结束符,避免语义撕裂
        last_punct = max(
            decoded_text.rfind('。'), 
            decoded_text.rfind('. '), 
            decoded_text.rfind('?'), 
            decoded_text.rfind('? ')
        )
        # 如果在尾部的 30% 范围内找到了标点,就在这里切断
        if last_punct > len(decoded_text) * 0.7:
            decoded_text = decoded_text[:last_punct + 1]
            
        return decoded_text + "..."

    def add(self, role, content):
        self.recent.append({"role": role, "content": content})
        if len(self.recent) > self.max_raw:
            self._compress()

    def _compress(self):
        # 1. 剥离需要被压缩的历史(保留最近 2 轮原貌)
        to_summarize = self.recent[:-2]
        self.recent = self.recent[-2:]

        # 2. 将历史消息格式化为文本
        text = "\n".join([f"{m['role']}: {m['content']}" for m in to_summarize])

        # 3. 防御一:输入端防爆(Prompt 不能无限长)
        # 粗略估算 Prompt 模板本身占 100 Token,预留空间
        max_input_tokens = 3000 - 100  
        
        # 如果原始对话太长,从最旧的开始丢弃(滑动窗口过滤)
        while self._count_tokens(text) > max_input_tokens and len(to_summarize) > 1:
            to_summarize.pop(0) # 移除最旧的一条
            text = "\n".join([f"{m['role']}: {m['content']}" for m in to_summarize])

        # 4. 动态构建 Prompt(核心优化:把压力给到 LLM,而不是工程截断)
        summary_tokens = self._count_tokens(self.summary)
        
        # 动态计算要求 LLM 输出的长度
        # 如果旧摘要已经很长了,就要求 LLM 这次极简输出,强行压缩
        if summary_tokens > self.max_summary_tokens * 0.6:
            target_words = "极简提炼(不超过50字)"
        else:
            target_words = "简明扼要的摘要(不超过200字)"

        prompt = f"""你是一个记忆压缩助手。
已有的历史摘要:{self.summary}
新发生的对话:\n{text}

请结合已有摘要和新对话,生成一份全新的历史摘要。
要求:保留所有关键事实、用户偏好和重要指令,丢弃闲聊和无用细节。
请生成{target_words}。"""

        # 5. 调用大模型
        new_summary = self.llm.generate(prompt)

        # 6. 防御二:输出端兜底(万一 LLM 失控,强行安全截断)
        self.summary = self._safe_truncate(new_summary, self.max_summary_tokens)

    def get(self):
        """返回最终给 LLM 的 Prompt 列表"""
        result = []
        if self.summary:
            result.append({"role": "system", "content": f"[历史摘要] {self.summary}"})
        result.extend([{"role": m["role"], "content": m["content"]} for m in self.recent])
        return result


适用场景:长会话、复杂任务、需要长期追踪的场景。

优点:既保留了历史信息,又控制了 Token 用量。

缺点:有信息损失风险,需要额外调用 LLM 成本。


长期记忆怎么实现?

长期记忆有三种主要实现方式:

1. 键值存储

从对话中提取结构化属性(如姓名、年龄、饮食禁忌),以 Key-Value 形式存入数据库。

"""
长期记忆 - 键值存储(结构化画像)
用 Key-Value 存储用户结构化信息,支持精确查询和关键词搜索
"""

import json
from pathlib import Path


class KeyValueMemory:
    """
    存储结构: { user_id: { category: { key: value } } }
    例: { "user_001": { "profile": {"name": "张三"}, "prefs": {"lang": "Python"} } }
    """

    def __init__(self, path="kv_memory.json"):
        self.path = Path(path)
        self.store = {}
        if self.path.exists():
            try:
                with open(self.path, "r", encoding="utf-8") as f:
                    self.store = json.load(f)
            except (json.JSONDecodeError, OSError) as e:
                # 文件损坏时:记录警告,从空状态启动(生产环境应报警/备份恢复)
                print(f"  [KV存储] 加载失败({e}),从空状态启动")

    def _save(self):
        # 原子写入:先写临时文件再 rename,避免写入中断导致文件损坏
        tmp = self.path.with_suffix(".tmp")
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(self.store, f, ensure_ascii=False, indent=2)
        tmp.replace(self.path)

    def save(self, user_id, category, key, value):
        """保存一条结构化记忆"""
        self.store.setdefault(user_id, {}).setdefault(category, {})[key] = value
        self._save()

    def get(self, user_id, category, key, default=None):
        """精确查询一条记忆"""
        return self.store.get(user_id, {}).get(category, {}).get(key, default)

    def get_all(self, user_id):
        """获取某用户全部记忆"""
        return self.store.get(user_id, {})

    def delete(self, user_id, category, key):
        """删除一条记忆,空分类和空用户自动清理"""
        if user_id not in self.store:
            return
        if category not in self.store[user_id]:
            return
        self.store[user_id][category].pop(key, None)
        # 清理空分类
        if not self.store[user_id][category]:
            del self.store[user_id][category]
        # 清理空用户(防止长期运行内存泄漏)
        if not self.store[user_id]:
            del self.store[user_id]
        self._save()

    def search(self, user_id, keyword):
        """按关键词搜索(简单字符串匹配,遍历所有键值对)"""
        results = []
        for cat, kvs in self.store.get(user_id, {}).items():
            for k, v in kvs.items():
                if keyword.lower() in str(k).lower() or keyword.lower() in str(v).lower():
                    results.append({"category": cat, "key": k, "value": v})
        return results

    def build_prompt(self, user_id):
        """将用户记忆格式化为 LLM 可用的上下文提示词"""
        data = self.get_all(user_id)
        if not data:
            return ""
        lines = ["[用户记忆画像]"]
        for cat, kvs in data.items():
            lines.append(f"## {cat}")
            for k, v in kvs.items():
                lines.append(f"- {k}: {v}")
        return "\\n".join(lines)


# 使用
m = KeyValueMemory("/tmp/kv_memory.json")
m.save("user_001", "profile", "name", "张三")
m.save("user_001", "prefs", "framework", "FastAPI")
print(m.get("user_001", "prefs", "framework"))    # "FastAPI"
print(m.search("user_001", "FastAPI"))             # [{"category":"prefs",...}]
print(m.build_prompt("user_001"))                   # 格式化上下文

适用场景:用户偏好设置、个人档案、明确的配置项。

优点:极其精准,查询快,存储高效。

缺点:只能存预先定义好的字段,灵活性差。

比如你定义了字段表:name, age, preferences。突然用户说"我之前跟你说过我是个程序员",你的 KV 存储就尴尬了——你表里没这个字段。

2. 向量存储

把文本通过 Embedding 模型转化为高维向量,存入向量数据库。

检索时,计算问题向量和所有记忆向量的余弦相似度,返回得分最高的 Top-K 条记忆。

"""
长期记忆 - 向量存储(语义相似检索)
使用 OpenAI Embedding API 生成向量 + ChromaDB 存储和检索
"""

import uuid
import chromadb
from openai import OpenAI


class VectorMemory:
    def __init__(self, openai_client, embedding_model="text-embedding-3-small",
                 persist_dir="./chroma_memory"):
        """
        openai_client:    OpenAI 客户端实例
        embedding_model:  Embedding 模型(text-embedding-3-small: 1536维,性价比最高)
        persist_dir:      ChromaDB 数据持久化目录
        """
        self.openai_client = openai_client
        self.embedding_model = embedding_model
        self._embed_cache = {}  # 查询向量缓存,避免同一 query 重复调 API

        # ChromaDB 持久化客户端:数据自动落盘,重启后自动加载
        self.chroma = chromadb.PersistentClient(path=persist_dir)
        self.collection = self.chroma.get_or_create_collection(
            name="agent_memory",
            metadata={"hnsw:space": "cosine"},  # 余弦距离度量
        )

    def _embed(self, text):
        """调用 OpenAI Embedding API 获取文本向量"""
        if text in self._embed_cache:
            return self._embed_cache[text]
        resp = self.openai_client.embeddings.create(
            model=self.embedding_model,
            input=text,
        )
        vector = resp.data[0].embedding
        self._embed_cache[text] = vector
        return vector

    def add(self, content, metadata=None):
        """
        添加一条记忆:生成向量 → 存入 ChromaDB
        ChromaDB 同时存储原文、向量、元数据,检索时可直接返回原文
        """
        entry_id = f"mem_{uuid.uuid4().hex[:8]}"
        embedding = self._embed(content)
        self.collection.add(
            ids=[entry_id],
            documents=[content],
            embeddings=[embedding],
            metadatas=[metadata or {}],
        )
        return entry_id

    def search(self, query, top_k=5, threshold=0.5):
        """
        语义检索:将 query 转为向量 → ChromaDB HNSW 索引检索 → 返回最相似的记忆
        ChromaDB 内部使用 HNSW 算法,比暴力遍历快几个数量级
        """
        if self.collection.count() == 0:
            return []

        q_embedding = self._embed(query)
        n_results = min(top_k, self.collection.count())  # 请求数不超过集合大小
        results = self.collection.query(
            query_embeddings=[q_embedding],
            n_results=n_results,
            include=["documents", "metadatas", "distances"],
        )

        scored = []
        for i, doc in enumerate(results["documents"][0]):
            # ChromaDB 返回 cosine distance = 1 - cosine_similarity
            score = round(1 - results["distances"][0][i], 4)
            if score >= threshold:
                scored.append({
                    "id": results["ids"][0][i],
                    "content": doc,
                    "score": score,
                    "metadata": results["metadatas"][0][i],
                })
        # ChromaDB 已按 distance 升序返回(即 similarity 降序),无需再排序
        return scored

    def delete(self, entry_id):
        """按 ID 删除一条记忆"""
        self.collection.delete(ids=[entry_id])

    def build_prompt(self, query, top_k=3):
        """检索相关记忆并格式化为 LLM 上下文"""
        results = self.search(query, top_k=top_k)
        if not results:
            return ""
        lines = ["[相关记忆]"]
        for i, r in enumerate(results, 1):
            lines.append(f"{i}. {r['content']} (相关度: {r['score']})")
        return "\n".join(lines)


# 使用
from openai import OpenAI
client = OpenAI()

m = VectorMemory(openai_client=client, persist_dir="/tmp/chroma_memory")
m.add("用户常用 FastAPI 搭建 API,数据库用 PostgreSQL", {"source": "对话"})
m.add("部署环境是 Docker + K8s,CI 用 GitHub Actions", {"source": "对话"})
print(m.search("容器化部署方案", top_k=2))
print(m.build_prompt("怎么部署服务?", top_k=2))

适用场景:非结构化的对话记录、经验总结、知识片段。

优点:能理解"语义",不用精准匹配关键词,灵活性强。

缺点:存在"幻觉检索"——语义相近但不相关的结果可能被检索出来。

比如用户问"我叫什么?“,可能检索出"你之前的客户叫张三”,然后 Agent 误以为你叫张三。

3. 混合存储

结合方式 1 和方式 2。结构化的明确信息用 KV 存,非结构化的经验和知识用向量存。

"""
长期记忆 - 混合存储(键值 + 向量,生产推荐)
SQLite 存结构化数据,ChromaDB 存向量数据,统一检索接口
"""

import tiktoken
from openai import OpenAI

# 复用上文的 KeyValueMemory 和 VectorMemory
# 实际项目: from .kv_memory import KeyValueMemory
#           from .vector_memory import VectorMemory


class HybridMemory:
    def __init__(self, openai_client, llm_model="gpt-4o-mini",
                 embedding_model="text-embedding-3-small",
                 kv_db_path="agent_kv.db",
                 chroma_persist_dir="./chroma_memory",
                 max_context_tokens=4000):
        """
        openai_client:     OpenAI 客户端实例
        llm_model:         用于 auto_save 智能路由的模型
        embedding_model:   用于向量存储的 Embedding 模型
        kv_db_path:        SQLite 数据库路径
        chroma_persist_dir: ChromaDB 持久化目录
        max_context_tokens: build_prompt 输出的最大 token 数
        """
        self.openai_client = openai_client
        self.llm_model = llm_model
        self.kv = KeyValueMemory(kv_db_path)
        self.vec = VectorMemory(openai_client, embedding_model, chroma_persist_dir)
        self.max_context_tokens = max_context_tokens

        try:
            self.encoder = tiktoken.encoding_for_model(llm_model)
        except KeyError:
            self.encoder = tiktoken.get_encoding("cl100k_base")

    def _count_tokens(self, text):
        return len(self.encoder.encode(text))

    # ==================== 显式写入 ====================

    def save_structured(self, user_id, category, key, value):
        """存储结构化记忆(偏好、画像、配置)→ SQLite"""
        self.kv.save(user_id, category, key, value)

    def save_unstructured(self, content, metadata=None):
        """存储非结构化记忆(经验、知识片段)→ ChromaDB"""
        return self.vec.add(content, metadata=metadata)

    # ==================== 智能路由 ====================

    def auto_save(self, user_id, content):
        """
        自动判断存储方式(核心生产逻辑):
        - 调用 LLM 判断内容类型 + 提取结构化字段
        - LLM 失败时降级到规则路由
        """
        try:
            resp = self.openai_client.chat.completions.create(
                model=self.llm_model,
                messages=[{
                    "role": "user",
                    "content": f"""判断以下内容应该存为结构化键值还是非结构化向量。
内容: {content}

如果是明确的偏好/事实/配置(如"我喜欢Python"、"项目用FastAPI"),输出:
STRUCTURED|分类名|键名|值
例: STRUCTURED|preferences|language|Python

如果是经验/知识/对话片段,输出:
UNSTRUCTURED
只输出一行,不要解释。"""
                }],
                temperature=0,
            )
            result = resp.choices[0].message.content.strip()

            if result.startswith("STRUCTURED|"):
                parts = result.split("|")
                if len(parts) >= 4:
                    _, cat, key, val = parts[0], parts[1], parts[2], "|".join(parts[3:])
                    self.kv.save(user_id, cat, key, val)
                    print(f"  [路由→SQLite] {cat}.{key}={val}")
                    return

            # LLM 判断为非结构化,存入 ChromaDB
            self.vec.add(content, metadata={"user_id": user_id})
            print(f"  [路由→ChromaDB] {content[:40]}...")
            return

        except Exception as e:
            # LLM 路由失败,降级到规则路由
            print(f"  [路由] LLM异常({e}),降级到规则路由")

        # 规则路由:短文本 + 偏好关键词 → SQLite,否则 → ChromaDB
        keywords = ["喜欢", "偏好", "我是", "我用", "我的", "设置", "配置", "项目用"]
        if len(content) < 50 and any(kw in content for kw in keywords):
            self.kv.save(user_id, "auto_facts", f"fact_{len(self.kv.get_all(user_id))}", content)
            print(f"  [路由→SQLite] {content}")
        else:
            self.vec.add(content, metadata={"user_id": user_id})
            print(f"  [路由→ChromaDB] {content[:40]}...")

    # ==================== 统一检索 ====================

    def search(self, user_id, query, top_k=5):
        """
        统一检索:同时从 SQLite 和 ChromaDB 中查找
        返回 {"structured": {...}, "unstructured": [...]}
        """
        return {
            "structured": self.kv.get_all(user_id),
            "unstructured": self.vec.search(query, top_k=top_k),
        }

    # ==================== 上下文构建 ====================

    def build_prompt(self, user_id, query, top_k=3):
        """
        构建完整的 LLM 上下文:结构化画像(始终注入)+ 语义检索(按查询动态匹配)
        带 token 预算控制:如果拼接结果超限,优先裁剪向量检索结果
        """
        parts = []
        kv_ctx = self.kv.build_prompt(user_id)
        if kv_ctx:
            parts.append(kv_ctx)
        vec_ctx = self.vec.build_prompt(query, top_k=top_k)
        if vec_ctx:
            parts.append(vec_ctx)

        result = "\n\n".join(parts)
        # Token 预算控制:超出时逐步减少向量检索条数
        while self._count_tokens(result) > self.max_context_tokens and top_k > 0:
            top_k -= 1
            vec_ctx = self.vec.build_prompt(query, top_k=top_k) if top_k > 0 else ""
            parts = []
            if kv_ctx:
                parts.append(kv_ctx)
            if vec_ctx:
                parts.append(vec_ctx)
            result = "\n\n".join(parts)

        # 极端情况:连键值画像都超限,截断
        if self._count_tokens(result) > self.max_context_tokens:
            tokens = self.encoder.encode(result)
            safe = max(self.max_context_tokens - 3, 1)
            result = self.encoder.decode(tokens[:safe]) + "..."

        return result

    def close(self):
        self.kv.close()


# 使用
from openai import OpenAI
client = OpenAI()

m = HybridMemory(
    openai_client=client,
    llm_model="gpt-4o-mini",
    kv_db_path="/tmp/agent_kv.db",
    chroma_persist_dir="/tmp/chroma_memory",
)
m.save_structured("user_001", "profile", "name", "张三")
m.save_structured("user_001", "prefs", "framework", "FastAPI")
m.save_unstructured("PostgreSQL JSONB 比 JSON 更高效,支持索引查询")

# 自动路由
m.auto_save("user_001", "我喜欢用 Python")       # → SQLite
m.auto_save("user_001", "Docker 多阶段构建可大幅减小镜像体积,推荐 slim 基础镜像")  # → ChromaDB

# 统一检索 + 上下文
print(m.search("user_001", "数据库优化"))
print(m.build_prompt("user_001", "怎么优化查询性能?"))
m.close()

适用场景:生产环境、需要兼顾精度和灵活性的场景。

优点:两全其美,结构化信息精准,非结构化信息灵活。

缺点:实现复杂,需要维护两套存储。


选型建议

维度全量保留滑动窗口Token 截断总结压缩键值存储向量存储混合存储
复杂度极低
Token 效率极差最好N/AN/AN/A
信息保留完整丢早期丢早期保留要点完整完整完整
查询方式----精确语义精确+语义
存储引擎内存内存内存内存SQLite/RedisChromaDB/Milvus两者组合
生产可用勉强

实践建议

  1. 短期记忆:生产环境推荐 Token 截断 + 总结压缩 组合。先按 token 裁剪保证不超限,再对裁剪掉的内容做摘要保留要点。

  2. 长期记忆:生产环境推荐 混合存储。结构化走 SQLite/Redis(快而准),非结构化走 ChromaDB/Milvus(广而灵)。

  3. 短期 + 长期联动

┌──────────────────────────────────────────────────┐
│                  完整记忆架构                      │
│                                                   │
│  对话开始 → 从长期记忆检索相关上下文               │
│      │           (SQLite 精确查询                  │
│      │          + ChromaDB 语义检索)               │
│      ▼                                             │
│  注入 System Prompt → 开始对话                     │
│      │                                             │
│      ▼                                             │
│  对话进行中 → 短期记忆管理                         │
│      │           (Token截断 + 总结压缩)            │
│      ▼                                             │
│  对话结束 → 提取关键信息                           │
│      │           (LLM 提取事实/偏好/经验)          │
│      ▼                                             │
│  写入长期记忆 → SQLite(结构化)or ChromaDB(向量)│
└──────────────────────────────────────────────────┘

四类记忆各司其职

在完整的 Agent 系统中,四类记忆各司其职:

Working Memory(工作记忆):是中枢,所有决策在这里做出。

LLM 每次生成回答,都是基于 Working Memory 中的上下文。它是推理的"工作台"。

Episodic Memory(情景记忆):负责"回忆"。

当用户说"上次"“之前”"刚才提到"时,Episodic Memory 被激活,检索相关的历史对话。

Semantic Memory(语义记忆):负责"理解"。

提供背景知识,让回复更专业。比如知道用户是技术负责人,回答时用技术术语;知道用户是产品经理,回答时关注业务价值。

Procedural Memory(程序记忆):负责"执行"。

为什么需要分层记忆?

想象你是一个客服 Agent,正在处理用户咨询:

用户说:“我上周投诉的 COD 超标问题,你们查得怎么样了?”

要回答这个问题,你需要:

  1. 现在对话的上下文(用户刚才说了什么)→ Working Memory
  2. 上周的投诉记录(具体事件)→ Episodic Memory
  3. COD 是什么、超标标准是什么(领域知识)→ Semantic Memory
  4. 处理投诉的标准流程(先查上游→生成报告→人工审核)→ Procedural Memory

没有分层,所有信息混在一起,Agent 会"失忆"或"错乱"。

协作模式

场景:用户投诉 COD 超标

用户:我上周投诉的 COD 超标问题,你们查得怎么样了?
import time
from datetime import datetime, timedelta

# ========== 初始化四层记忆 ==========
working_mem = WorkingMemory(max_tokens=4000)
episodic_mem = EpisodicMemory(embedding_model=mock_embed)
semantic_mem = SemanticMemory(embedding_model=mock_embed)
procedural_mem = ProceduralMemory()

# 预置一些知识(Semantic Memory)
semantic_mem.set_user_profile("user_001", {
    "name": "张三",
    "region": "湘江流域",
    "role": "环保监测员",
    "preferred_channel": "email"
})
semantic_mem.add_knowledge(
    "COD(化学需氧量)标准值:地表水 IV 类 ≤ 30mg/L",
    source="GB 3838-2002"
)

# 预置流程(Procedural Memory)
procedural_mem.register_workflow("complaint_followup", {
    "trigger": "user asks about previous complaint",
    "steps": [
        {"name": "查询历史", "action": "query_episodic", "params": {"keyword": "{{complaint_type}}"}},
        {"name": "检查状态", "action": "check_status", "params": {"ticket_id": "{{ticket_id}}"}},
        {"name": "生成回复", "action": "generate_response", "params": {"template": "followup"}}
    ]
})

# ========== 模拟:上周的投诉记录(Episodic Memory) ==========
last_week = time.time() - 7 * 86400
episodic_mem.record_event(
    session_id="sess_lastweek_001",
    event_type="user_complaint",
    content="用户张三投诉湘江流域 COD 检测值 45mg/L,超过标准值 30mg/L,要求查明原因",
    metadata={"severity": "high", "region": "湘江流域", "pollutant": "COD"}
)

# ========== 开始新会话 ==========
session_id = "sess_today_001"

# Step 1: 用户输入进入 Working Memory
user_input = "我上周投诉的 COD 超标问题,你们查得怎么样了?"
working_mem.add_message("user", user_input, tokens=25)

# Step 2: Agent 理解意图(在 Working Memory 中推理)
working_mem.add_thought("用户询问上周投诉的跟进情况")
working_mem.add_thought("关键词:上周、COD 超标、投诉")
working_mem.add_thought("需要查询 Episodic Memory 获取历史事件")

# Step 3: 查询 Episodic Memory(时间 + 语义)
print("=== [Episodic Memory] 查询历史投诉 ===")
# 3.1 按时间范围查
start = time.time() - 14 * 86400  # 两周内
events = episodic_mem.recall_by_time(start, time.time())
print(f"找到 {len(events)} 个事件")

# 3.2 按语义查(更精确)
similar = episodic_mem.recall_by_similarity("COD 超标投诉", top_k=3)
print(f"语义相似事件: {[e['content'][:30] for e in similar]}")

# Step 4: 查询 Semantic Memory(用户画像 + 知识)
print("\n=== [Semantic Memory] 获取用户信息和领域知识 ===")
profile = semantic_mem.get_user_profile("user_001")
print(f"用户: {profile['name']}, 区域: {profile['region']}")

# RAG 检索相关知识
knowledge = semantic_mem.query_knowledge("COD 标准值")
print(f"相关知识: {knowledge[0]['text'] if knowledge else '无'}")

# Step 5: 加载 Procedural Memory(处理流程)
print("\n=== [Procedural Memory] 执行跟进流程 ===")
# 实际执行工作流
result = procedural_mem.execute_workflow(
    "complaint_followup",
    context={
        "user_id": "user_001",
        "complaint_type": "COD超标",
        "ticket_id": "TICKET_001"
    },
    tool_registry={
        "query_episodic": lambda **kw: {"found": True, "events": similar},
        "check_status": lambda **kw: {"status": "处理中", "progress": "已查上游,待生成报告"},
        "generate_response": lambda **kw: {
            "response": f"张先生,您上周投诉的湘江流域 COD 超标问题(检测值 45mg/L,超过 IV 类标准 30mg/L)目前正在处理中。我们已完成上游排查,正在生成详细报告,预计今日内提交人工审核。审核通过后将第一时间邮件通知您。"
        }
    }
)

# Step 6: 将结果和回复存入 Working Memory
response = result['variables']['result_生成回复']['response']
working_mem.add_message("assistant", response, tokens=80)

# Step 7: 记录本次交互到 Episodic Memory(为下次准备)
episodic_mem.record_event(
    session_id=session_id,
    event_type="followup_inquiry",
    content=f"用户询问上周 COD 投诉进展,Agent回复:{response[:50]}...",
    metadata={"resolved": False, "related_event": "evt_xxx"}
)

print("\n=== [Working Memory] 当前状态 ===")
print(working_mem.get_context())

分析维度

维度含义工程对应例子
Form(形式)记忆以什么数据结构存在文本 / 向量 / 键值 / 图结构用户画像是 JSON(形式),对话历史是文本数组(形式)
Function(功能)记忆用来解决什么问题检索 / 推理 / 决策支持查用户偏好是为了个性化回答(功能)
Dynamics(动态)记忆怎么变化、更新、遗忘写入 / 压缩 / 过期清理 / 强化7天前的记忆自动降权(动态)

Form(形式/结构维度)

是什么

Form 指的是记忆在系统中以什么数据结构存在,即记忆的物理/逻辑表示形式。

具体内容

类型说明示例
参数化记忆存储在模型权重中,通过微调/训练获得大模型预训练知识、LoRA 适配层
非参数化记忆外挂存储,模型通过检索访问向量数据库、知识图谱、文档库
隐式记忆模型推理时的内部状态Transformer 的 KV Cache、隐藏层状态
显式记忆结构化/半结构化的外部记录符号记忆、工作记忆缓冲区、日志

作用

  • 解耦知识与推理:让模型参数专注推理能力,知识通过外挂记忆动态更新
  • 可解释性:显式记忆可被人类审计、编辑、删除
  • 持久化:对话结束后记忆不丢失,支持长期累积

Function(功能维度)

是什么

Function 指的是记忆在 Agent 运行中承担什么认知功能,即记忆被用来做什么。

具体内容

功能类型作用典型场景
工作记忆 (Working Memory)临时保存当前任务上下文,支持多步推理维护对话历史、待办事项列表
情景记忆 (Episodic Memory)记录具体事件/经历,支持经验复用“上次用户说喜欢简约风格”
语义记忆 (Semantic Memory)存储事实性知识和概念关系产品知识库、领域术语
程序记忆 (Procedural Memory)存储技能和操作步骤调用 API 的流程、工具使用规范
前瞻性记忆 (Prospective Memory)未来需要执行的计划/提醒“明天上午提醒用户开会”

作用

  • 突破上下文长度限制:通过功能分区,只把最相关的记忆加载到上下文
  • 支持复杂任务规划:程序记忆让 Agent 能执行多步骤工作流
  • 个性化:情景记忆让 Agent 记住用户偏好,提供定制化服务
  • 持续学习:新经验通过情景记忆积累,提炼为语义记忆

Dynamics(动态性维度)

是什么

Dynamics 指的是记忆如何随时间变化——如何写入、更新、遗忘、巩固,以及记忆之间的相互作用。

具体内容

动态过程说明机制
编码 (Encoding)原始信息转换为记忆表示文本→向量、对话→摘要、经验→知识图谱
巩固 (Consolidation)短期记忆转化为长期记忆睡眠机制(离线摘要)、知识蒸馏
检索 (Retrieval)根据当前需求提取记忆相似度搜索、注意力机制、图遍历
更新 (Update)修正或扩展已有记忆增量学习、冲突检测与合并
遗忘 (Forgetting)清理过时/无关记忆TTL 过期、重要性衰减、主动遗忘策略
泛化 (Generalization)从具体经验抽象出规律模式提取、规则归纳

作用

  • 防止记忆膨胀:无限增长的记忆会导致检索噪音和成本激增
  • 保持记忆准确性:旧记忆可能被新信息覆盖或修正
  • 模拟人类认知节律:工作记忆→情景记忆→语义记忆的层级流转
  • 自适应学习:Agent 越用越"懂"用户,但不会过时

三个维度的关系图

┌─────────────────────────────────────────────┐
│              Dynamics(动态性)               │
│   编码 → 巩固 → 检索 → 更新 → 遗忘 → 泛化     │
│         ↑ 驱动 Form 的流转 ↓                  │
├─────────────────────────────────────────────┤
│              Function(功能)                 │
│   工作记忆 → 情景记忆 → 语义记忆 → 程序记忆     │
│         ↑ 决定 Form 的组织方式 ↓               │
├─────────────────────────────────────────────┤
│              Form(形式)                     │
│   向量 / 图谱 / 键值对 / 模型参数 / 符号       │
└─────────────────────────────────────────────┘

核心逻辑

  • Form 是载体:决定记忆"长什么样"
  • Function 是目的:决定记忆"用来干什么"
  • Dynamics 是过程:决定记忆"如何演化"

说点实在的

Memory 这个东西,搞复杂了容易陷入"过度设计"。

简单对话,用滑动窗口就够了,别上向量库。

需要跨会话记忆的,先用 KV 存储,够用了别折腾向量。

真需要灵活检索的,再用向量存储。

混合存储是生产环境的首选,但前期开发成本高。

写代码的时候,记得多做测试。Memory 系统最容易出问题的是"检索准确率"——你明明存了"用户叫王五",但检索时返回"用户叫王六",这种 Bug 调起来非常痛苦。

下一篇文章,我会详细说说 Action。Action 是把"想法"变成"现实"的关键,包括 Action 的类型、执行流程、可靠性控制,每个环节都有不少细节。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐