基于向量数据库与SillyTavern的AI对话长期记忆系统实现 和AI聊天最让人出戏的瞬间是什么不是它偶尔的“一本正经胡说八道”也不是它有时答非所问。而是当你聊得正酣突然抛出一个半小时前提到的细节它却一脸茫然地问你“你刚才说的是什么” 这种“金鱼记忆”般的体验瞬间将沉浸感击得粉碎。你不得不像个复读机一样把前因后果再解释一遍对话的连贯性和深度就此中断。这正是当前绝大多数AI聊天应用的核心痛点缺乏真正有效的长期记忆。它们能基于上下文窗口比如最新的几千个词进行回应但一旦对话超出这个范围之前建立的角色设定、情感纽带、共同经历就仿佛从未存在过。这对于追求深度、个性化陪伴的用户来说是体验上的致命短板。最近B站AI创造公开赛中一个名为“Foreverse”的项目引起了我的注意。它的目标直指这个痛点为AI对话装上可扩展、可管理的“记忆库”。想象一下一个AI角色能记住你们初次见面的场景记得你最喜欢的咖啡口味甚至能在第500次对话时精准引用第1次聊天时你随口提到的童年趣事。这不再是科幻而是正在落地的工程实践。本文将从开发者的视角深度解析如何为AI聊天构建一个实用的记忆系统。我们将以SillyTavern一个流行的本地AI前端为例结合Foreverse项目的思路手把手实现一个从记忆存储、检索到应用的全流程。你将了解到记忆库的核心原理不仅仅是存储文本更是对记忆的“理解”与“索引”。工程化实现方案如何设计数据库如何将记忆向量化以便快速检索。与SillyTavern的深度集成如何让记忆在对话中自然地被唤醒和运用。避坑指南与最佳实践处理“记忆幻觉”、控制记忆权重、设计遗忘机制。如果你厌倦了与“健忘”的AI对话并希望亲手打造一个更具“灵魂”的AI伙伴那么这篇文章正是为你准备的。我们不止步于概念更聚焦于一行行代码和一个个可运行的配置。1. 记忆问题的本质我们到底需要AI记住什么在开始敲代码之前我们必须先厘清一个根本问题在AI对话中什么样的信息值得被“记忆”很多人第一反应是“记住所有对话历史”。但这不仅效率低下而且有害。想象一下每次对话AI都要在上万条历史记录中大海捞针响应速度会急剧下降更糟糕的是无关的历史信息会严重干扰AI当前的回应的相关性。因此一个高效的记忆库其核心是“选择性记忆”和“结构化记忆”。1.1 记忆的分类与价值我们可以将对话中产生的信息大致分为三类记忆类型示例存储价值检索频率核心身份与事实用户姓名、职业、AI角色设定、世界观背景极高。这是对话的基石需要长期、稳定存在。每次对话开场或涉及身份时。重要经历与事件“上周我们一起击败了恶龙”、“你昨天说最喜欢的电影是《星际穿越》”高。构成了对话的连续性和独特性是情感链接的关键。当对话涉及相关话题时。普通会话上下文“今天天气不错”、“我刚才吃了午饭”低。大部分即时性、无后续影响的信息无需长期存储。极低通常不需要。Foreverse项目的核心洞察就在于不是保存原始对话日志而是从中提取出高价值的“记忆点”Memory Nuggets并将其结构化存储。1.2 技术挑战从存储到智能唤醒实现记忆库面临两个主要技术挑战存储与检索效率如何在海量记忆点中快速找到与当前对话最相关的几条传统关键词匹配效果很差比如用户说“我心情不好”需要能联想到之前存储的“用户失恋了”这条记忆。这就需要用到向量检索技术。记忆的整合与表达检索到相关记忆后如何自然地“喂”给AI大模型让它能理解并运用这些记忆这涉及到提示词工程和上下文窗口的巧妙利用。接下来我们将围绕这两个挑战构建我们的解决方案。2. 核心架构设计一个可扩展的记忆系统我们的目标是构建一个独立于AI前端SillyTavern和后端如Ollama、OpenAI API的记忆服务。这样做的好处是解耦可以灵活替换任何组件。整体架构如下用户输入 | v [SillyTavern前端] ---(携带当前对话上下文)--- [记忆服务 (Memory Server)] | | | | 1. 提取记忆点 | | 2. 向量化 存储 v | [AI大模型后端] ---(增强后的上下文)---| 3. 检索相关记忆 | | v | AI回复输出 v [向量数据库] [记忆元数据库]核心流程SillyTavern将用户当前消息和最近的简短上下文发送给记忆服务。记忆服务首先判断当前对话是否产生了新的、值得存储的记忆点。如果有则进行提取和存储。记忆服务根据当前对话内容从向量数据库中检索出最相关的若干条历史记忆。记忆服务将检索到的记忆以一种精心设计的格式拼接到发送给AI大模型的最终提示词中。AI大模型基于“增强了长期记忆”的上下文生成回复。3. 环境准备与工具选型在开始实现前我们需要准备好以下工具和环境。3.1 基础运行环境操作系统推荐 Windows 10/11, macOS, 或 Linux (Ubuntu 22.04)。本文示例以 Windows 为例其他系统命令类似。Python版本 3.9 - 3.11。这是大多数AI相关库兼容性最好的范围。Node.js版本 18。用于运行SillyTavern前端。Git用于克隆项目代码。3.2 核心组件安装我们将使用以下关键库SillyTavern作为我们的聊天前端。它支持丰富的插件系统是我们集成记忆服务的入口。向量数据库我们选用ChromaDB。它轻量、易用且完全开源非常适合本地部署和原型开发。嵌入模型为了将文本记忆转化为向量我们需要一个嵌入模型。考虑到本地运行我们选用BAAI/bge-small-zh-v1.5模型它对中文支持好体积小性能不错。你也可以选择text-embedding-3-small等API模型如果使用OpenAI。记忆服务框架我们将用FastAPI快速搭建一个Web服务供SillyTavern调用。打开你的终端命令行让我们一步步搭建环境。# 1. 克隆 SillyTavern 项目 git clone https://github.com/SillyTavern/SillyTavern.git cd SillyTavern # 2. 安装 SillyTavern 依赖 (使用 npm 或 yarn) npm install # 3. 为记忆服务创建一个新的目录并初始化Python环境 cd .. mkdir ai_memory_server cd ai_memory_server python -m venv venv # 激活虚拟环境 (Windows) venv\Scripts\activate # 激活虚拟环境 (macOS/Linux) # source venv/bin/activate # 4. 安装记忆服务所需的Python库 pip install fastapi uvicorn chromadb sentence-transformers pydantic # sentence-transformers 用于运行本地嵌入模型 # 如果你想用OpenAI的嵌入模型还需要pip install openai4. 构建记忆服务从零到一现在我们开始编写记忆服务的核心代码。服务主要提供两个API端点POST /api/remember处理新记忆的存储。POST /api/recall根据当前对话检索相关记忆。4.1 定义记忆的数据结构首先我们创建一个models.py文件来定义记忆的数据模型。# models.py from pydantic import BaseModel from datetime import datetime from typing import Optional import uuid class MemoryItem(BaseModel): 单个记忆条目的数据模型 id: str str(uuid.uuid4()) # 唯一标识 content: str # 记忆的文本内容如“用户透露他是一名软件工程师擅长Python。” embedding: Optional[list[float]] None # 文本内容的向量表示 memory_type: str fact # 记忆类型fact(事实), event(事件), preference(偏好) importance: float 1.0 # 记忆重要性权重1.0为基准可用于后续的遗忘算法 timestamp: datetime datetime.now() # 记忆创建时间 source_context: str # 产生该记忆的原始对话片段用于追溯 class Config: arbitrary_types_allowed True class RecallRequest(BaseModel): 检索记忆的请求模型 query: str # 当前的对话查询文本 top_k: int 5 # 返回最相关的K条记忆 class RememberRequest(BaseModel): 存储记忆的请求模型 content: str # 要存储的记忆内容 memory_type: str fact importance: float 1.0 source_context: str 4.2 实现记忆的存储与检索引擎接下来创建memory_engine.py这是最核心的部分负责与ChromaDB交互。# memory_engine.py import chromadb from chromadb.config import Settings from sentence_transformers import SentenceTransformer import logging from typing import List from models import MemoryItem logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class MemoryEngine: def __init__(self, persist_directory: str ./chroma_db): # 初始化ChromaDB客户端数据持久化到本地目录 self.client chromadb.Client(Settings( chroma_db_implduckdbparquet, persist_directorypersist_directory )) # 获取或创建名为“conversation_memories”的集合相当于数据库的表 self.collection self.client.get_or_create_collection(nameconversation_memories) # 加载本地嵌入模型首次运行会自动下载 logger.info(正在加载嵌入模型...) self.embedder SentenceTransformer(BAAI/bge-small-zh-v1.5) logger.info(嵌入模型加载完毕。) def _get_embedding(self, text: str) - List[float]: 生成文本的向量嵌入 # 注意模型.encode()返回的是numpy数组需要转换为list return self.embedder.encode(text).tolist() def remember(self, memory: MemoryItem) - str: 存储一条记忆 # 为记忆内容生成向量 memory.embedding self._get_embedding(memory.content) # 向ChromaDB集合中添加数据 self.collection.add( documents[memory.content], # 原始文本 metadatas[{ type: memory.memory_type, importance: memory.importance, source: memory.source_context[:200] # 截断以避免过长 }], embeddings[memory.embedding], # 向量 ids[memory.id] # 唯一ID ) logger.info(f记忆已存储: {memory.content[:50]}...) return memory.id def recall(self, query: str, top_k: int 5) - List[MemoryItem]: 根据查询检索相关记忆 # 将查询文本也转化为向量 query_embedding self._get_embedding(query) # 在集合中进行相似性搜索 results self.collection.query( query_embeddings[query_embedding], n_resultstop_k ) memories [] if results[documents]: for i in range(len(results[documents][0])): mem MemoryItem( idresults[ids][0][i], contentresults[documents][0][i], memory_typeresults[metadatas][0][i].get(type, fact), importanceresults[metadatas][0][i].get(importance, 1.0), source_contextresults[metadatas][0][i].get(source, ) ) memories.append(mem) logger.info(f为查询 {query[:30]}... 检索到 {len(memories)} 条相关记忆。) return memories def list_memories(self, limit: int 20): 列出所有记忆用于调试和管理 results self.collection.get(limitlimit) for i, doc in enumerate(results[documents]): print(f{i1}. [{results[metadatas][i][type]}] {doc})4.3 创建FastAPI主应用最后创建main.py将引擎包装成Web API。# main.py from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from memory_engine import MemoryEngine from models import RecallRequest, RememberRequest, MemoryItem import uvicorn app FastAPI(titleAI Memory Server) # 允许跨域请求以便SillyTavern可以调用 app.add_middleware( CORSMiddleware, allow_origins[*], # 生产环境应限制为具体前端地址 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 初始化记忆引擎 memory_engine MemoryEngine() app.post(/api/remember) async def remember_memory(request: RememberRequest): 存储一条新记忆 try: memory_item MemoryItem( contentrequest.content, memory_typerequest.memory_type, importancerequest.importance, source_contextrequest.source_context ) memory_id memory_engine.remember(memory_item) return {status: success, memory_id: memory_id} except Exception as e: raise HTTPException(status_code500, detailstr(e)) app.post(/api/recall) async def recall_memories(request: RecallRequest): 检索与当前查询相关的记忆 try: memories memory_engine.recall(request.query, request.top_k) # 将MemoryItem列表转换为字典列表以便JSON序列化 memories_dict [mem.dict(exclude{embedding}) for mem in memories] return {status: success, memories: memories_dict} except Exception as e: raise HTTPException(status_code500, detailstr(e)) app.get(/api/list) async def list_memories(limit: int 20): 列出记忆调试用 # 这里简单返回实际可以格式化得更好 results memory_engine.collection.get(limitlimit) return results if __name__ __main__: uvicorn.run(app, host0.0.0.0, port8000)现在我们的记忆服务核心就完成了。在ai_memory_server目录下运行以下命令启动服务python main.py你应该看到服务在http://localhost:8000启动。你可以访问http://localhost:8000/docs查看自动生成的API文档并进行测试。5. 与SillyTavern深度集成让记忆“活”起来记忆服务已经就绪但如何让SillyTavern在每次对话时自动调用它呢这需要用到SillyTavern强大的插件系统和API连接器功能。5.1 创建SillyTavern记忆插件简化版我们创建一个简单的用户脚本User Script来桥接SillyTavern和我们的记忆服务。在SillyTavern的安装目录下找到public/scripts目录如果没有则创建新建一个文件memoryBridge.js。// public/scripts/memoryBridge.js // SillyTavern 记忆桥接脚本 (async function() { const MEMORY_SERVER_URL http://localhost:8000; // 你的记忆服务地址 // 1. 当用户发送消息前检索相关记忆 async function recallRelevantMemories(context) { try { const response await fetch(${MEMORY_SERVER_URL}/api/recall, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ query: context, // 将当前最新的对话内容作为查询 top_k: 3 // 每次检索3条最相关的记忆 }) }); const data await response.json(); if (data.status success) { return data.memories; } } catch (error) { console.error(检索记忆失败:, error); } return []; } // 2. 当AI回复后判断是否需要存储新记忆 async function evaluateAndStoreMemory(userInput, aiResponse, fullContext) { // 这是一个简化的启发式规则如果对话中包含明显的个人信息或重要事件则存储 const keywords [我是, 我叫, 我喜欢, 我讨厌, 我记得, 曾经, 总是]; const hasPersonalInfo keywords.some(keyword userInput.includes(keyword)); if (hasPersonalInfo) { // 更智能的做法是调用一个LLM来提取记忆点这里为简化直接存储片段 const memoryContent 用户提到“${userInput}”。AI回应“${aiResponse}”。; try { await fetch(${MEMORY_SERVER_URL}/api/remember, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ content: memoryContent, memory_type: fact, source_context: fullContext.slice(-500) // 附上部分上下文 }) }); console.log(已尝试存储新记忆。); } catch (error) { console.error(存储记忆失败:, error); } } } // 3. 将记忆格式化为AI可理解的提示词片段 function formatMemoriesForPrompt(memories) { if (!memories || memories.length 0) { return ; } let memoryText \n\n 以下是关于你和用户的过往记忆供本次对话参考 \n; memories.forEach((mem, idx) { // 可以根据memory_type添加不同前缀如[事实]、[事件] memoryText * ${mem.content}\n; }); memoryText 记忆部分结束 \n; return memoryText; } // 4. 钩子函数在消息发送前修改系统提示词插入记忆 async function onSystemPromptGeneration(prompt) { // 获取最近的几条消息作为检索上下文 const recentMessages getRecentMessages(5); // 假设这个函数能获取最近N条消息 const context recentMessages.join(\n); const relevantMemories await recallRelevantMemories(context); const memoryBlock formatMemoriesForPrompt(relevantMemories); // 将记忆块插入到系统提示词的合适位置例如在角色设定之后 return prompt.replace({{char}}的描述和设定。, {{char}}的描述和设定。${memoryBlock}); } // 5. 钩子函数在AI回复后评估是否存储记忆 function onAiResponseGenerated(userInput, aiResponse, fullMessageHistory) { // 异步执行不阻塞主线程 setTimeout(() { evaluateAndStoreMemory(userInput, aiResponse, fullMessageHistory.join(\n)); }, 0); } // 注册钩子到SillyTavern具体API名称可能因版本而异需查阅ST文档 if (typeof extendSystemPrompt ! undefined) { extendSystemPrompt(onSystemPromptGeneration); } if (typeof registerMessageCallback ! undefined) { registerMessageCallback(onAiResponseGenerated); } console.log(AI记忆桥接插件已加载。); })();5.2 在SillyTavern中启用插件启动SillyTavern。在SillyTavern目录下运行node server.js在浏览器中打开http://localhost:8000SillyTavern默认端口。进入设置Settings -用户脚本User Scripts。找到并启用memoryBridge.js脚本。确保你的记忆服务http://localhost:8000也在运行。5.3 配置AI后端连接器为了让记忆被AI模型使用最关键的一步是确保系统提示词包含了我们格式化好的记忆。SillyTavern的“API连接器”通常有一个“主要提示词”或“系统提示词”的配置区域。你需要在这里的模板中加入一个占位符比如{{memory}}。然后上述脚本中的onSystemPromptGeneration函数会动态地将{{memory}}替换为实际的记忆文本。例如在OpenAI API或Ollama的设置中系统提示词可以这样写你是一个名为{{char}}的AI角色。请根据以下角色设定和过往记忆进行对话。 {{char}}的设定[这里是你的人物设定描述] {{memory}} 现在开始和用户{{user}}对话吧。这样每次请求AI生成回复时最新的相关记忆都会被包含在系统指令中从而深刻地影响AI的回应。6. 运行验证与效果测试完成所有配置后让我们进行端到端的测试。启动所有服务终端1运行记忆服务 (python main.py)。终端2运行SillyTavern (node server.js)。进行首次对话在SillyTavern中向AI角色介绍一些关键信息。例如“我叫小明是一名住在北京的Python程序员我养了一只叫‘橘子’的猫。”发送消息。观察记忆服务终端的日志应该会显示“记忆已存储”。验证记忆存储打开浏览器访问http://localhost:8000/api/list?limit10。你应该能看到刚刚存储的记忆条目。测试记忆检索开启一个新的对话话题但稍后提及相关点。例如几轮对话后你问“对了你记得我养了什么宠物吗”观察AI的回复。一个成功的集成应该能让AI回答出“橘子”或“猫”。同时记忆服务终端会显示“检索到X条相关记忆”的日志。检查提示词在SillyTavern中打开“查看提示词/查看请求”这类调试功能通常位于AI回复区域附近。检查发送给AI模型的完整提示词你应该能看到 以下是关于你和用户的过往记忆 这样的区块里面包含了检索到的记忆。7. 常见问题与排查思路在集成过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案SillyTavern提示“无法连接到API”1. 记忆服务未启动。2. 端口被占用或防火墙阻止。3.memoryBridge.js中的URL错误。1. 检查python main.py是否在运行。2. 访问http://localhost:8000/docs看是否通。3. 浏览器F12打开开发者工具查看网络请求报错。1. 确保服务启动。2. 修改端口或配置防火墙。3. 修正MEMORY_SERVER_URL。AI回复中未出现记忆内容1. 用户脚本未启用或加载失败。2. 系统提示词模板未正确包含记忆占位符。3. 检索到的记忆为空。1. 在SillyTavern设置中确认脚本已启用。2. 检查“查看提示词”看{{memory}}是否被替换。3. 调用/api/recall接口手动测试看是否返回数据。1. 重新启用脚本刷新页面。2. 调整提示词模板和脚本中的替换逻辑。3. 确保记忆已成功存储且查询文本相关。记忆服务报错ModuleNotFoundErrorPython依赖未安装完全。查看服务启动时的完整错误日志。在虚拟环境中运行pip install -r requirements.txt需先创建requirements文件或手动安装缺失的包。存储的记忆检索不出来1. 嵌入模型未正确加载或生成向量。2. ChromaDB集合名称不一致。3. 查询文本与记忆内容语义差异太大。1. 检查服务启动日志确认嵌入模型加载成功。2. 检查memory_engine.py中的集合名。3. 尝试用更接近记忆原文的句子查询。1. 确保网络通畅能下载模型。2. 保持代码中集合名一致或清空./chroma_db目录重建。3. 优化记忆提取存储更核心的“事实”而非完整句子。AI回复变得混乱或矛盾1. 检索到的记忆过多或无关记忆被注入。2. 记忆文本格式混乱干扰了AI。1. 减少top_k参数如从5改为3。2. 检查“查看提示词”看记忆块是否格式清晰。1. 调整检索相关性阈值如果ChromaDB支持。2. 在formatMemoriesForPrompt函数中优化记忆的格式化输出使其更清晰。8. 进阶优化与最佳实践上面的实现是一个基础版本。要打造一个真正鲁棒、好用的记忆系统还需要考虑以下方面8.1 智能记忆提取我们之前的evaluateAndStoreMemory函数规则非常简单。在生产环境中应该使用一个轻量级的LLM甚至是同一个对话模型来实时分析对话并提取结构化的记忆点。例如# 伪代码使用LLM提取记忆 def extract_memory_from_dialogue(turn): prompt f 请从以下对话回合中提取出值得长期记忆的关于用户或角色的**新事实、偏好或重要事件**。 如果没有任何新信息值得记忆请输出“NO_MEMORY”。 对话 用户{turn[user]} AI{turn[ai]} 提取的记忆仅一条格式类型|内容 # 调用LLM API如OpenAI GPT-3.5-turbo或本地小模型 response call_llm(prompt) if NO_MEMORY not in response: memory_type, content response.split(|, 1) return MemoryItem(contentcontent.strip(), memory_typememory_type.strip()) return None8.2 记忆的衰减与遗忘人脑会遗忘AI的记忆库也需要。可以引入记忆强度或最后访问时间的概念。每次记忆被成功检索并用于生成回复就增加其“强度”或更新“最后访问时间”。定期运行一个清理任务将强度低于某个阈值或很久未被访问的记忆归档或删除防止记忆库无限膨胀。8.3 记忆的冲突与合并如果用户说“我喜欢苹果”后来又说“我讨厌苹果”系统应该能检测到冲突。更高级的实现可以在新记忆存入时检索是否有语义相近的旧记忆。如果存在冲突可以标记冲突或在更高层次上合并例如记录“用户对苹果的态度似乎发生了变化”。8.4 分角色、分场景的记忆隔离一个用户可能和多个AI角色聊天。记忆库应该按(user_id, character_id)进行隔离。在ChromaDB中可以通过在metadata中添加这些字段并在查询时过滤来实现。8.5 前端交互与管理可以为SillyTavern开发一个更完善的插件提供前端界面让用户能够查看浏览所有存储的记忆。编辑修正AI提取错误的记忆。删除手动移除不想要或错误的记忆。标记标记某些记忆为特别重要。9. 总结为AI聊天赋予长期记忆远不止是技术上的向量检索和数据库存储。它本质上是在为AI构建一个动态的、结构化的外部知识库这个知识库专门服务于“与这个用户的这段关系”。我们从AI“健忘”的痛点出发剖析了记忆的类型与价值然后设计并实现了一个解耦的、可扩展的记忆服务架构。通过ChromaDB存储向量化记忆通过FastAPI提供标准接口最后通过SillyTavern的插件系统无缝集成到对话流程中。这个过程清晰地展示了如何将前沿的AI概念向量检索、嵌入模型与成熟的工程实践Web服务、插件化结合起来解决一个具体的用户体验问题。关键收获记忆需要提炼存对话日志不如存结构化记忆点。检索重于存储基于语义的向量检索是让记忆“活”起来的关键。集成在于提示词记忆最终是通过系统提示词巧妙地“注入”到AI的思考上下文中的。工程化思维考虑隔离、衰减、冲突、管理界面才能让功能真正可用。你可以基于这个基础框架继续探索更智能的记忆提取、更复杂的记忆关系图谱、甚至让AI主动基于记忆发起对话。项目的完整代码包括更健壮的插件和记忆管理界面是下一步自然演进的方向。现在你的AI角色已经不再是那个“金鱼”而是一个能记住过往、让每一次对话都建立在历史之上的真正伙伴了。