昨天深夜调试一个多轮对话Agent,被一个诡异的问题卡住了:用户第三次问“我们刚才讨论的那个方案”,Agent居然一脸茫然地反问“您指的是哪个方案?”。明明前两轮对话里详细讨论过技术选型,Agent却像得了健忘症。问题出在哪?短期记忆缓冲区满了,上下文被无情裁剪。这就是今天要解决的痛点:如何让Agent记住过去的重要信息。

一、为什么需要长期记忆?

传统的对话系统依赖固定长度的上下文窗口,就像只有7秒记忆的金鱼。GPT-4的128K窗口看似很大,但成本高昂且效率低下。更关键的是,每次对话都要重新喂入全部历史,这种“全量回忆”模式在长期交互中根本不可行。

向量数据库的解决思路很巧妙:把对话中的关键信息转换成向量,存进专门的数据集。需要回忆时,用当前问题向量去相似度检索,只召回相关片段。这就像人脑的记忆机制——我们不会记住每顿午饭的细节,但会记得“那家巷子里的面馆味道不错”。

二、动手搭建记忆系统

先看核心代码结构,这里用ChromaDB做示例(别一上来就怼Faiss,小项目杀鸡用牛刀了):

class MemorySystem:
    def __init__(self, persist_dir="./memory_db"):
        # 这里踩过坑:embedding模型要跟LLM匹配
        self.embedding = OpenAIEmbeddings(model="text-embedding-3-small")
        self.vectorstore = Chroma(
            embedding_function=self.embedding,
            persist_directory=persist_dir  # 持久化目录,否则重启就失忆
        )
        self.memory_buffer = []  # 短期缓存,先攒几条再存
        
    def _chunk_text(self, text, chunk_size=500):
        """切分文本,别直接按字数硬切,会切断句子"""
        # 简易版按句子分割,生产环境建议用专门的分割器
        sentences = text.replace('。', '.').split('.')
        chunks = []
        current_chunk = ""
        for sent in sentences:
            if len(current_chunk) + len(sent) > chunk_size:
                chunks.append(current_chunk)
                current_chunk = sent
            else:
                current_chunk += sent
        return chunks

存储记忆不是见啥存啥,得有点策略:

def add_memory(self, text, metadata=None):
    """添加记忆,metadata里一定要打时间戳"""
    if not metadata:
        metadata = {}
    metadata['timestamp'] = datetime.now().isoformat()
    
    # 先放缓冲区,攒够3条或重要信息再存
    self.memory_buffer.append({
        'text': text,
        'metadata': metadata
    })
    
    if len(self.memory_buffer) >= 3 or metadata.get('importance') == 'high':
        # 批量存储效率高,减少向量化调用次数
        texts = [item['text'] for item in self.memory_buffer]
        metadatas = [item['metadata'] for item in self.memory_buffer]
        
        # 这里有个细节:add_texts返回的ID要存下来,方便后续更新
        ids = self.vectorstore.add_texts(texts=texts, metadatas=metadatas)
        self.memory_buffer.clear()  # 清空缓冲区
        return ids

检索环节才是灵魂所在,直接决定记忆召回的质量:

def retrieve_memories(self, query, k=5, score_threshold=0.7):
    """检索相关记忆,阈值过滤很重要"""
    # 先转成向量
    query_vector = self.embedding.embed_query(query)
    
    # 相似度搜索,带上原始距离值
    results = self.vectorstore.similarity_search_with_score(
        query, 
        k=k*2  # 多查点,后面要过滤
    )
    
    # 距离转相似度分数(Chroma用余弦距离,0最相似)
    filtered = []
    for doc, distance in results:
        score = 1 - distance  # 转换一下
        if score >= score_threshold:
            # 把分数塞进metadata,后面排序用
            doc.metadata['relevance_score'] = round(score, 3)
            filtered.append(doc)
    
    # 按时间加权:最近的相关记忆更重要
    sorted_memories = sorted(
        filtered,
        key=lambda x: (
            x.metadata['relevance_score'] * 0.7 + 
            self._recency_weight(x.metadata['timestamp']) * 0.3
        ),
        reverse=True
    )[:k]  # 最终取top-k
    
    return sorted_memories

三、集成到Agent的实战技巧

记忆系统不是独立模块,得跟Agent的工作流深度融合:

class AgentWithMemory:
    def __init__(self):
        self.memory = MemorySystem()
        self.llm = ChatOpenAI(temperature=0.1)  # 温度调低,记忆型Agent要稳定
        
    def chat_cycle(self, user_input):
        # 1. 先检索相关历史
        relevant_memories = self.memory.retrieve_memories(user_input)
        
        # 构建带上下文的prompt
        context = "相关历史对话:\n"
        for mem in relevant_memories:
            context += f"- {mem.page_content}\n"
        
        prompt = f"""
        你是一个有长期记忆的助手。
        {context}
        当前问题:{user_input}
        请基于历史上下文回答,如果历史不相关就忽略。
        """
        
        # 2. 生成回复
        response = self.llm.invoke(prompt)
        
        # 3. 把本轮对话存为记忆(异步操作,别阻塞主流程)
        self._store_conversation_async(user_input, response.content)
        
        return response.content
    
    def _store_conversation_async(self, query, response):
        """存记忆的讲究:只存有价值的交换"""
        # 过滤掉问候语之类的废话
        if len(query) < 10 and "你好" in query:
            return
            
        # 合并问答成一个记忆点
        memory_text = f"用户问:{query}\n助手答:{response[:200]}..."  # 截断避免太长
        
        # 打上重要性标签(简单版,可按长度、关键词判断)
        importance = 'high' if len(response) > 100 else 'medium'
        
        # 丢到后台线程去存
        threading.Thread(
            target=self.memory.add_memory,
            args=(memory_text, {'importance': importance})
        ).start()

四、那些年踩过的坑

坑1:向量化模型不一致
用text-embedding-ada-002存的向量,换text-embedding-3-small检索,相似度全乱套。必须保证存和取用同一个embedding模型。

坑2:metadata设计太随意
早期只存文本,后来想按时间过滤才发现没时间戳。metadata至少包含:timestamp、source(哪个会话)、importance、type(用户输入/助手回复)。

坑3:相似度阈值拍脑袋
阈值设0.9会漏掉相关记忆,设0.3会召回一堆垃圾。建议:先用100组问答测试,画个ROC曲线找最佳阈值。

坑4:忘记清理旧记忆
数据库会一直膨胀,最后检索速度从50ms掉到2s。定期清理策略:按时间删除(保留最近30天)+ 按重要性删除(只留high和medium)。

五、性能优化实战

当记忆条数超过10万时,需要这些优化:

# 1. 分层存储:高频记忆放内存,低频放磁盘
from langchain.vectorstores import FAISS
from langchain.vectorstores import Chroma

class TieredMemory:
    def __init__(self):
        self.hot_memory = FAISS.from_texts([], embedding)  # 内存级,存最近1000条
        self.cold_memory = Chroma(persist_dir="./cold_db")  # 磁盘级
        
    def retrieve(self, query):
        # 先查热记忆,命中率80%以上
        hot_results = self.hot_memory.similarity_search(query, k=2)
        if hot_results and hot_results[0].metadata['score'] > 0.8:
            return hot_results
        
        # 再查冷记忆
        return self.cold_memory.similarity_search(query, k=5)

# 2. 批量操作减少IO
# 别每次对话都存,攒一批再批量写入
# 设置一个记忆缓冲区,满10条或每5分钟刷一次盘

六、给后来者的经验之谈

长期记忆系统不是“有了就行”的装饰品,它彻底改变了Agent的交互模式。三个关键体会:

第一,记忆的“质”比“量”重要。疯狂存储每句对话的结果是检索质量下降。一定要设计过滤规则——存那些有信息量的、可能被问到的内容。我现在的策略是:包含技术术语的、长度超过50字的、用户明确说“记住这个”的。

第二,相似度检索不是银弹。纯向量检索会漏掉关键词匹配的内容。混合搜索(hybrid search)才是正道:向量相似度占70%,关键词匹配占30%。Elasticsearch + 向量数据库的架构值得考虑。

第三,记忆也需要“遗忘”。设计一个记忆衰减机制,三个月没被召回的记忆自动降权,半年没被召回的移入归档库。这符合人类的记忆规律,也控制数据库规模。

最后说个反直觉的发现:有时候故意让Agent“忘记”某些细节反而是好事。用户说“我讨厌上次那个方案”,如果Agent详细回忆起方案内容并反驳,体验会很糟。我的做法是在metadata加个emotion标签,负面情绪的记忆只存概要,不存细节。

记忆系统让Agent有了“历史感”,但也要警惕它变得过于固执——总引用过去的话,缺乏新鲜感。平衡之道在于:用记忆提供上下文,用LLM的创造力生成新内容。记住,我们是在造助手,不是复读机。

Logo

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

更多推荐