
在实际部署 AI 智能体时一个最常见的抱怨是它就像金鱼一样只有七秒记忆。Hermes 智能体可以处理复杂的多轮对话但一旦会话结束它就不会记住用户之前的偏好、兴趣和结论。要让 AI 越用越聪明不能只靠更大更强的模型还必须给它一套可持续积累的“记忆外挂”。本文以 Hermes 智能体为例介绍如何在不改动核心模型的情况下通过向量检索和会话存储为智能体增加长期记忆能力。读完本文后你可以独立实现一个最小可用的记忆外挂并知道如何验证、排查和部署。这里所说的“记忆外挂”本质上是把对话历史上一次性的信息转化为可长期存储、可检索、可注入的结构化记忆。它并不改变模型权重而是改变每次请求到达模型前携带的上文内容。这样的设计保留了模型本身的通用能力又让智能体在多次使用中逐渐积累对特定用户的了解。下面先解释为什么要这样做再给出完整实现步骤。1. 为什么 Hermes 需要记忆外挂1.1 没有记忆的 AI只能“一次性聊天”默认情况下大多数智能体框架都是无状态的。每次请求框架会把当前会话的上下文组织成提示词发送给大模型然后返回结果。这种模式有明确的优点上下文可控、token 开销可预测、无状态服务容易水平扩展。缺点也很明显用户上次说过的话、确认过的偏好、讨论过的结论在下一个会话里全部丢失。例如用户昨天告诉 Hermes “我在做 Java 后端的性能优化重点关注 JVM 调优”。今天再打开智能体问“接着昨天的思路讲一下”Hermes 因为没有昨天的记忆只能泛泛而谈。长期使用中用户需要反复提供背景信息体验非常割裂。1.2 记忆外挂的本质把对话变成可检索的知识记忆外挂的思路是把对话内容从“一次性输入”升级为“可积累的知识库”。具体来说它会完成三件事从对话中抽取值得记住的信息例如用户偏好、项目背景、决定事项。将信息向量化后存入向量数据库同时保留原始文本和元数据。在后续对话开始时根据当前问题检索相关记忆并拼接到提示词中。这样模型本身没有变但每次请求携带的“上下文”不再是空白而是从记忆中召回的、与当前问题高度相关的内容。用户会感觉 AI “记住了”自己长期使用后回答越来越贴合个人场景这就是“越用越聪明”的直接来源。1.3 不适合做记忆的信息有哪些记忆外挂不是把所有对话都存下来。如果什么内容都往向量库里塞会导致检索结果混杂、token 浪费甚至把无关历史注入到当前对话里干扰模型回答。以下几类信息通常不适合进入长期记忆临时性寒暄例如“你好”“在吗”。一次性操作指令例如“帮我查一下天气”这种查询结果本身没有长期价值。敏感信息例如密码、密钥、身份证号除非明确设计为安全可控的记忆模块。已经过时且不会再使用的中间讨论过程。因此在实际实现时需要先对对话内容做筛选或摘要再决定是否写入记忆。下文会用一个简单的规则来实现这一点。2. 记忆外挂的整体架构与技术选型2.1 一个最小可用的记忆外挂由哪几部分组成一个最小可用的记忆外挂至少包含四个模块记忆写入模块接收对话记录判断是否值得保存生成文本摘要或直接使用原始文本调用 Embedding 接口得到向量。向量存储模块保存文本、向量和元数据时间、用户 ID、会话 ID、类型等。记忆检索模块将当前用户输入转成向量在向量库中做相似度检索返回 topK 条最相关记忆。记忆注入模块把检索到的记忆整理成固定的提示词片段插入到 Hermes 的上下文开头。这四个模块可以独立成文件也可以封装成一个类。在设计时要把“存储格式”和“底层向量库”解耦这样后续更换存储引擎时不需要改动上层逻辑。2.2 向量数据库选型Chroma、Qdrant、Milvus 还是 FAISS向量数据库是整个记忆外挂的核心。选型要考虑部署难度、数据规模、是否需要持久化、是否支持元数据过滤等因素。下表对比了几种常见方案。方案适合场景部署方式持久化元数据过滤备注Chroma本地开发、中小数据量嵌入式或容器支持落盘支持参数简单上手最快Qdrant生产级、需要高并发Docker Compose支持支持强过滤性能好支持 REST/gRPCMilvus超大规模、分布式集群部署支持支持运维成本较高FAISS离线批量检索、内存数据集库嵌入需要自行设计落盘较弱更适合实验和算法验证对于大多数个人项目、中小团队和第一次尝试推荐先用 Chroma。它不需要单独启动服务可以直接在 Python 进程内运行数据落盘到本地目录非常便于调试。当数据量增长到百万级向量或需要多实例共享时再迁移到 Qdrant 或 Milvus。2.3 Embedding 模型的选择本地模型还是 API 模型将文本变成向量需要 Embedding 模型。选择时有两条路线本地模型例如BAAI/bge-small-zh-v1.5、moka-ai/m3e-small通过sentence-transformers加载。好处是数据不出内网运行成本低适合隐私敏感场景。API 模型例如 OpenAI 的 text-embedding-3-small或其他云厂商的向量化接口。好处是分词质量高、维护简单但每调用一次有费用且依赖外部网络。下面是两种方式的简单对比。对比维度本地 Embedding 模型API Embedding 模型数据隐私数据不离开本机安全性高依赖外部服务需要网络传输部署复杂度需要本地安装模型占用内存只需要 API Key集成简单成本一次性硬件成本无单次调用费按 token 计费长期使用有成本中文效果取决于选型小模型可能一般通常由服务商优化效果较稳定离线可用完全离线不可离线本文示例使用本地模型BAAI/bge-small-zh-v1.5它支持中文和英文体积小适合演示。如果你的 Hermes 部署环境没有 GPU也可以使用纯 CPU 运行只是第一次加载模型会慢一些。3. 环境准备先把 Hermes 和向量存储跑起来3.1 用 Docker 部署 Hermes 智能体不同社区的 Hermes 项目可能有不同的启动方式这里给出一种常见的 Docker Compose 部署思路。假设 Hermes 已经打包成镜像hermes-agent:latest可以通过以下docker-compose.yml启动。version: 3.8 services: hermes: image: hermes-agent:latest container_name: hermes ports: - 8080:8080 environment: HERMES_MODEL_PROVIDER: openai HERMES_MODEL_NAME: gpt-4o-mini HERMES_MODEL_API_KEY: sk-xxxxxxxxxxxxxx HERMES_MODEL_BASE_URL: https://api.openai.com/v1 HERMES_LOG_LEVEL: INFO volumes: - ./hermes-data:/app/data - ./hermes-config:/app/config restart: unless-stopped关键点说明HERMES_MODEL_PROVIDER和HERMES_MODEL_NAME用于指定大模型供应商和模型名实际项目要根据你使用的 Hermes 版本调整。volumes把配置和数据目录挂载到宿主机便于升级容器后数据不丢失。如果 Hermes 镜像使用其他端口需要同步修改ports映射。不要在生产环境把 API Key 直接写在docker-compose.yml里推荐使用环境变量文件或密钥管理服务。启动后可以通过docker compose ps查看状态并通过curl http://localhost:8080/health之类的健康检查接口确认服务是否就绪。3.2 安装并启动 Chroma 向量数据库Chroma 支持两种使用方式嵌入式模式和客户端模式。嵌入式模式最简单直接在 Python 代码里引入chromadb数据落盘到本地目录。客户端模式则需要启动 Chroma 服务适合多进程共享。先创建一个 Python 虚拟环境并安装依赖。mkdir hermes-memory cd hermes-memory python3 -m venv venv source venv/bin/activate pip install chromadb sentence-transformers在代码中使用嵌入式模式时只需要指定持久化目录。import chromadb client chromadb.PersistentClient(path./chroma_data) collection client.get_or_create_collection( namehermes_memory, metadata{hnsw:space: cosine} )这里指定了cosine距离函数适合文本向量的相似度计算。如果后面发现检索效果不理想可以换成l2或ip但要重新写入向量。如果你希望使用独立的 Chroma 服务可以用 Docker 启动docker run -d --name chroma \ -p 8000:8000 \ -v $(pwd)/chroma_data:/data \ chromadb/chroma启动后Python 端通过chromadb.HttpClient(hostlocalhost, port8000)连接。3.3 准备 Python 环境和 Embedding 依赖本文的记忆外挂使用 Python 编写除了chromadb和sentence-transformers还需要以下依赖torchsentence-transformers依赖 PyTorch。transformers用于加载 Embedding 模型。numpy向量运算时常用。pydantic或dataclasses定义数据结构。安装命令如下pip install torch transformers numpy注意PyTorch 在 CPU 环境下也能运行但会占用较多内存。如果机器内存不足可以选择更小的 Embedding 模型例如BAAI/bge-small-zh-v1.5是约 100MB 的模型相对轻量。加载 Embedding 模型的代码如下from sentence_transformers import SentenceTransformer embedder SentenceTransformer(BAAI/bge-small-zh-v1.5)首次运行时模型会从 Hugging Face Hub 下载需要网络能够访问相关域名。如果网络受限需要提前将模型下载到本地并指定本地路径加载。4. 实现记忆外挂从短时记忆到长时记忆4.1 定义记忆数据结构为了让记忆外挂更清晰先用dataclass定义两个基础结构对话轮次和记忆条目。from dataclasses import dataclass, field from datetime import datetime import uuid dataclass class ConversationTurn: 一次对话中用户和助手的消息 role: str # user 或 assistant content: str timestamp: str field(default_factorylambda: datetime.utcnow().isoformat()) dataclass class MemoryItem: 一条写入向量库的记忆 id: str field(default_factorylambda: str(uuid.uuid4())) text: str metadata: dict field(default_factorydict) timestamp: str field(default_factorylambda: datetime.utcnow().isoformat())MemoryItem的metadata会保存用户 ID、会话 ID、记忆类型等方便后续过滤和删除。text是写入向量的源文本这里为了演示直接使用用户消息原文。4.2 对话保存把重要的对话内容写入向量库保存对话时不能每条消息都写入。一个简单的策略是只保存用户消息中长度超过一定阈值、且不是纯寒暄的内容同时可以把助手的重要结论也保存为一条记忆。下面是一个MemoryStore类的核心实现。import chromadb from sentence_transformers import SentenceTransformer class MemoryStore: def __init__(self, persist_dir./chroma_data, collection_namehermes_memory): self.client chromadb.PersistentClient(pathpersist_dir) self.collection self.client.get_or_create_collection( namecollection_name, metadata{hnsw:space: cosine} ) self.embedder SentenceTransformer(BAAI/bge-small-zh-v1.5) def should_save(self, text: str) - bool: 判断一条消息是否值得存入长期记忆 if len(text) 10: return False # 跳过常见寒暄词 greetings [你好, 您好, 在吗, hello, hi] if text.strip().lower() in greetings: return False return True def add_memory(self, text: str, metadata: dict None) - str: 将一段文本写入向量库 if not self.should_save(text): return mem_id str(uuid.uuid4()) embedding self.embedder.encode(text).tolist() meta metadata or {} meta[timestamp] datetime.utcnow().isoformat() self.collection.add( ids[mem_id], embeddings[embedding], documents[text], metadatas[meta] ) return mem_id def save_conversation(self, turns: list[ConversationTurn], user_id: str, session_id: str): 保存一轮对话中的用户消息和助手摘要 for turn in turns: if turn.role user: self.add_memory( turn.content, metas{user_id: user_id, session_id: session_id, source: user} ) else: # 助手消息较长且包含结论时也可以保存 if len(turn.content) 50: self.add_memory( turn.content, metas{user_id: user_id, session_id: session_id, source: assistant} )这段代码有几个关键决策should_save过滤过短消息和寒暄避免把无意义的对话写入向量库。用户消息和助手消息都写入但助手消息只保存长度大于 50 的内容减少无效记忆。每条记忆都带上user_id在多用户场景中可以隔离不同用户的记忆。4.3 记忆召回对话开始时注入相关历史注入时机很关键。最简单的做法是在每次调用 Hermes 前先用用户的当前输入去检索历史记忆再把召回结果作为一段system或背景知识插入。检索函数如下。def recall_memories(self, query: str, user_id: str, top_k: int 5, score_threshold: float 0.6) - list[str]: 根据查询召回用户的历史记忆 if not query.strip(): return [] query_embedding self.embedder.encode(query).tolist() results self.collection.query( query_embeddings[query_embedding], n_resultstop_k, where{user_id: user_id} ) documents results.get(documents, [[]])[0] distances results.get(distances, [[]])[0] memories [] for doc, distance in zip(documents, distances): score 1 - distance # cosine 距离转相似度 if score score_threshold: memories.append(doc) return memories这里把余弦距离distance转换成相似度score 1 - distance并加了一个阈值过滤。距离越小相似度越高。阈值设得过高会召回太少设得过低会带进来无关信息实际项目需要根据测试数据调整。生成注入提示词时可以使用固定格式def build_memory_prompt(self, memories: list[str]) - str: if not memories: return block 以下是用户过去的对话记忆中与当前问题相关的内容请参考这些信息回答\n for i, mem in enumerate(memories, 1): block f{i}. {mem}\n block 如果这些记忆与当前问题无关请忽略它们。\n return block注入时将这个block放在用户消息之前或作为系统提示词的一部分。token 量取决于记忆条数和长度需要控制在合理范围内。4.4 把记忆外挂接入 Hermes 的调用链假设 Hermes 的调用接口是一个 Python 函数hermes.chat(messages)其中messages是标准 OpenAI 风格的列表。我们可以写一个包装类在每次调用前自动完成记忆保存和召回。class MemoryEnhancedHermes: def __init__(self, hermes_client, memory_store: MemoryStore, user_id: str): self.hermes hermes_client self.memory memory_store self.user_id user_id self.history: list[ConversationTurn] [] def chat(self, user_input: str) - str: # 1. 召回相关记忆 memories self.memory.recall_memories(user_input, self.user_id) memory_block self.memory.build_memory_prompt(memories) # 2. 组装消息 messages [] if memory_block: messages.append({role: system, content: memory_block}) # 将当前会话历史也带上短期记忆 for turn in self.history[-10:]: messages.append({role: turn.role, content: turn.content}) messages.append({role: user, content: user_input}) # 3. 调用 Hermes response self.hermes.chat(messages) # 4. 记录并保存记忆 self.history.append(ConversationTurn(roleuser, contentuser_input)) self.history.append(ConversationTurn(roleassistant, contentresponse)) self.memory.save_conversation(self.history[-2:], self.user_id, session_iddemo-session) return response这样一个最小的“记忆外挂”就完成了。它没有修改 Hermes 的任何核心逻辑只是在外部加了一个带记忆的壳。每次对话智能体既能获得当前的短期上下文又能拿到跨会话的长期记忆。5. 关键参数详解与调优5.1 核心参数速查表记忆外挂是否好用很大程度上取决于参数设置。下面是几个关键参数及其影响。参数推荐值说明调大影响调小影响top_k5每次检索返回的记忆条数token 占用增加更易引入噪声可能漏掉相关记忆score_threshold0.6相似度过滤阈值召回变少但更精准召回变多但噪声增加单条记忆最大长度500 字写入向量库的文本长度上限保留更多信息但检索可能不聚焦信息不完整记忆条数上限无限或按用户设置单个用户可存储的记录总数存储和检索变慢用户长期记忆不足保存消息最小长度10 字低于该长度不写入增加无效记忆可能漏掉短但重要的指令会话历史保留轮数10 轮短期上下文保留的对话轮数模型能看到更多背景上下文太短语义丢失5.2 相似度阈值怎么定score_threshold是最容易引发“记忆看起来不生效”或“答非所问”的参数。它的本质是衡量当前查询和存储记忆之间的语义相关程度。如果阈值设成 0.9大多数普通相似句子都会被过滤只有极其接近的文本才会被召回。结果是系统经常找不到记忆表现像没有外挂一样。如果阈值设成 0.1几乎所有记忆都会被无差别注入模型会被大量无关历史干扰回答变得碎片化。推荐做法是先用一个较小的阈值例如 0.4收集一批召回结果人工判断哪些相关、哪些无关再逐步提高阈值。也可以在日志中输出每次检索的相似度分数方便迭代调优。5.3 记忆合并与去重如果不加去重用户多次表达同一个意思向量库中会保存大量重复记忆。例如用户先后说“我喜欢简洁的回答”和“请用简短的方式回答”这两条语义接近会同时被检索出来造成冗余。简单的去重方案有两种写入前查重在写入时用当前文本去向量库检索 top1如果相似度高于 0.95就认为重复不再写入。写入后合并定时对向量库做聚类将相似度高的记忆合并成一条摘要。后者实现复杂适合生产环境。这里展示写入前查重的简化实现。def is_duplicate(self, text: str, threshold: float 0.95) - bool: embedding self.embedder.encode(text).tolist() results self.collection.query( query_embeddings[embedding], n_results1 ) if not results[documents][0]: return False distance results[distances][0][0] return (1 - distance) threshold在add_memory开头调用is_duplicate如果返回True则跳过写入。这样能显著控制数据量增长。6. 运行验证看看 AI 是否真的“越用越聪明”6.1 编写一个交互测试脚本为了验证记忆外挂是否生效可以编写一个简单的命令行测试脚本。class FakeHermes: 模拟 Hermes 的 chat 接口用于本地验证 def chat(self, messages): # 简化直接返回最后一条用户消息并提示参考了记忆 system_prompts [m[content] for m in messages if m[role] system] memory_ref | 已注入记忆 if system_prompts else return f模拟回答{memory_ref}: 我收到了你的最新消息{messages[-1][content]}用假对象可以快速验证记忆外挂的调用流程不需要真实的大模型接口。实际对接时把FakeHermes替换成真正的 Hermes 客户端即可。测试逻辑如下memory_store MemoryStore() hermes MemoryEnhancedHermes(FakeHermes(), memory_store, user_idu_1001) # 第一轮用户告知偏好 print(hermes.chat(我喜欢简洁的回答用列表形式输出。)) # 第二轮模拟新会话不包含上文直接询问偏好 print(hermes.chat(根据我过去的习惯你建议怎么输出))第一次调用会保存“我喜欢简洁的回答”这条记忆。第二次调用时recall_memories应该能检索到这条记忆并在消息列表中增加 system 提示。6.2 观察日志和向量库在验证过程中可以打印召回结果来确认记忆是否真的被找到。memories memory_store.recall_memories(我的输出偏好是什么, user_idu_1001) print(召回记忆, memories)预期输出类似召回记忆 [我喜欢简洁的回答用列表形式输出。]如果打印为空说明相似度阈值过高、向量库没有写入或者 Embedding 模型未正确加载。这时需要回到前几节排查。6.3 验证结果分析当第二次对话返回的内容中包含“已注入记忆”标识时说明记忆外挂已经完全跑通。在真实模型中这个标识会表现为模型更贴合用户偏好。比如用户过去说过“用列表输出”模型在新的回答中会自动使用列表结构而不是直接复述历史。需要注意模拟测试只能验证链路无法验证大模型对记忆的实际利用效果。建议在真实 Hermes 环境中准备 5 到 10 组测试对话覆盖以下场景用户告知明确偏好隔天重新提问。用户提到一个具体项目名隔天询问项目进展方式。用户纠正过助手一次隔天再次提出相关主题。与当前查询无关的历史记忆不应被注入。7. 常见问题排查清单7.1 记忆从不生效现象检索结果始终为空系统像没有记忆一样。排查顺序是否调用了save_conversation可以在add_memory里加日志确认向量库是否真正写入了数据。检查where条件。如果召回时传入了user_idA但写入时存的是user_idB则无法命中。检查score_threshold是否过高。可以先临时设为 0看能否召回如果能召回说明是阈值问题。检查 Embedding 模型是否加载成功。不同模型输出的向量维度不同如果模型加载失败encode会报错。7.2 检索出大量无关内容现象记忆注入后模型回答被带偏出现与当前问题无关的信息。排查顺序降低top_k从 5 减到 3 或 2观察效果。提高score_threshold将 0.6 调整到 0.7 或 0.8。检查写入时的should_save逻辑。如果保存了过多寒暄和中间过程需要增强过滤规则。在召回结果中打印相似度分数找出最低分的记忆判断它是否应该被过滤。7.3 向量数据库连接失败现象使用独立 Chroma 服务时Python 报连接拒绝错误。排查步骤确认 Docker 容器是否启动docker ps | grep chroma。确认端口映射宿主机端口和容器内--port是否一致。使用curl http://localhost:8000/api/v1/heartbeat测试服务是否可访问。检查 Python 端HttpClient的 host 和 port 是否写错。如果是嵌入式模式报错多为目录权限问题确保persist_dir目录有读写权限。7.4 对话性能明显变慢现象接入记忆外挂后每次请求耗时明显增加。原因和优化方向Embedding 编码耗时本地模型在 CPU 上每次编码可能耗时几百毫秒。可以对输入做个缓存或使用更小的模型。向量检索耗时数据量达到几十万条后需要给向量库建立索引或切换到服务化部署的 Qdrant/Milvus。注入的记忆过多导致 token 过长控制top_k和单条记忆长度避免把大量历史文本塞入提示词。性能优化时建议先通过日志统计“编码耗时”“检索耗时”“模型调用耗时”定位瓶颈后再动手不要盲目更换组件。8. 生产环境最佳实践与扩展方向8.1 区分学习环境和生产环境本文的嵌入式 Chroma 方案适合本地学习和功能验证。进入生产环境后需要注意以下几点使用独立的向量数据库服务避免多进程写入文件冲突。使用环境变量或密钥管理服务管理模型 API Key、数据库连接地址不硬编码到代码中。为记忆外挂配置单独的日志链路记录每一次写入和召回方便排查。增加数据备份机制定期导出向量库中的文档和元数据。考虑记忆数据的保留策略例如设置 TTL 清理过期记忆避免无限增长。8.2 记忆内容的安全与权限多用户场景下必须严格按用户隔离记忆。最简单的方式是每条记忆的元数据都包含user_id召回时强制加上where{user_id: user_id}。同时要注意不能把用户 A 的记忆注入到用户 B 的上下文中。敏感信息过滤也很重要。可以在写入前使用正则或敏感词库识别手机号、银行卡、API Key 等模式直接丢弃或脱敏。如果业务涉及隐私合规要求需要明确告知用户哪些内容会被记住并提供删除记忆的接口。8.3 后续扩展自动摘要、记忆等级、知识图谱当前实现保存的是原始文本长期运行后每个用户的记忆会非常零散。更成熟的记忆系统会引入自动摘要能力定期把过去一段时间的对话交给大模型生成结构化摘要并替换掉原始碎片。这样可以降低存储量提高召回准确率。记忆等级可以分成三层用户基础信息姓名、偏好、常用工具长期保留。项目上下文当前项目目标、技术栈、决定事项随项目结束而归档。瞬时状态最近一次操作、临时问题短时间保留后删除。更高级的记忆外挂还可以引入知识图谱把“用户A - 喜欢 - 简洁回答”这类关系存储成图结构与向量检索结合处理更复杂的关系推理。8.4 上生产前检查清单在把记忆外挂部署到生产环境前建议逐项确认下面的清单。[ ] 是否按用户隔离记忆召回时是否正确过滤user_id。[ ] 是否设置了合理的score_threshold和top_k并用真实数据验证过。[ ] 是否过滤了敏感信息是否有删除单条记忆的管理接口。[ ] 向量数据库是否做了持久化和备份。[ ] 是否对记忆写入失败有降级处理不能因为记忆库异常导致主流程不可用。[ ] 是否限制了单次注入的 token 总量避免超出模型上下文窗口。[ ] 是否记录记忆写入、召回、注入的日志方便线上排查。当这些检查项都通过后记忆外挂才算真正达到了可上线状态。对于一个刚完成最小实现的团队建议先以小范围用户灰度测试观察模型回答质量和检索命中率再逐步开放到全量用户。给 AI 加记忆并不是一次性工作而是一个需要持续调优的模块但只要能跑通从写入到刷新的闭环就已经迈出了最关键的一步。