1. 项目概述一个为本地大模型注入记忆的智能代理最近在折腾本地部署的大语言模型LLM比如用 Ollama 跑 Llama 3 或者 Qwen 2.5玩久了就会发现一个核心痛点它记不住事儿。每次对话都像初次见面你得把上下文、背景信息、你的偏好甚至刚刚讨论过的结论一遍又一遍地喂给它。这严重限制了本地模型在构建个人知识库助手、长期任务规划、或者多轮复杂对话中的应用潜力。这正是djc00p/openclaw-ollama-memory这个项目要解决的核心问题。简单来说它是一个为基于 Ollama 运行的本地大语言模型设计的“记忆系统”。你可以把它想象成给一个健忘的天才配了一个永不遗忘的私人秘书。这个“秘书”即 Memory 模块会忠实地记录下你和模型交互的所有关键信息并在后续对话中智能地将相关记忆提取出来作为上下文的一部分提供给模型从而实现连续、连贯且个性化的对话体验。这个项目并非一个庞大的应用而是一个精巧的“中间件”或“插件”。它通过拦截和处理你与 Ollama API 之间的通信在请求中动态插入历史记忆在响应后自动提取和存储新的记忆。对于开发者、AI 爱好者或者任何希望将自己的本地模型从“一次性聊天玩具”升级为“长期协作伙伴”的人来说这个项目提供了开箱即用的解决方案和清晰的可扩展架构。2. 核心架构与设计思路拆解要理解openclaw-ollama-memory的价值我们得先拆解一下它要解决的“记忆”难题具体是什么以及它是如何设计来应对的。2.1 记忆的挑战从上下文窗口到向量检索本地模型尤其是消费级硬件上运行的模型其上下文窗口Context Window是有限的。虽然最新的 7B/8B 参数模型已经能支持 8K 甚至 32K 的上下文但把成百上千轮对话的全部历史都塞进去是不现实的这会急剧增加计算开销、拖慢响应速度并且大量无关历史还会干扰模型对当前问题的判断。因此一个高效的记忆系统不能是简单的“日志记录器”而必须是“智能摘要与检索系统”。它需要做到摘要与提取从每一轮对话中提炼出有价值的、结构化的信息点而不是存储原始对话文本。向量化与存储将这些信息点转换为计算机易于理解和比较的格式即向量嵌入并存入专门的数据库。相关性检索当新问题到来时系统能快速从海量记忆中找出与当前问题最相关的几条记忆。上下文注入将检索到的相关记忆以自然语言的形式巧妙地编织进发送给模型的提示词Prompt中。openclaw-ollama-memory的设计正是围绕这四个环节展开的。它没有重新发明轮子而是优雅地集成了当前最成熟的技术栈使用Sentence Transformers或OpenAI 的 Embeddings API进行向量化使用ChromaDB或Qdrant这类向量数据库进行存储和检索并通过一个代理服务器Agent Server来管理整个流程。2.2 方案选型为什么是向量数据库嵌入模型这里涉及几个关键的技术选型每一个背后都有其考量。为什么用向量数据库而不是传统数据库传统关系型数据库如 MySQL或文档数据库如 MongoDB擅长精确匹配如WHERE user_id abc但对于“找出与‘如何优化 Python 循环效率’相关的历史对话”这种语义相似度查询无能为力。向量数据库专为高维向量即嵌入的相似性搜索而设计。它将记忆的向量表示存储起来并建立高效的索引如 HNSW使得即使面对数十万条记忆也能在毫秒级内返回最相似的几条。ChromaDB因其轻量、易用和与 Python 生态的无缝集成常被选为入门首选而Qdrant则在分布式部署和生产环境性能方面更有优势。嵌入模型的选择本地 vs. 云端这是另一个核心决策点。项目支持两种方式本地嵌入模型例如通过sentence-transformers库调用all-MiniLM-L6-v2这类轻量级模型。优势是完全离线、零延迟、零成本且隐私性极佳。缺点是嵌入质量可能略低于顶级云端模型且需要额外的本地计算资源。云端嵌入 API如 OpenAI 的text-embedding-3-small。优势是嵌入质量高、稳定且不消耗本地算力。缺点是需要网络、产生 API 费用并且所有记忆文本需要发送到第三方服务器。注意对于openclaw-ollama-memory这类旨在强化本地隐私的项目强烈建议优先使用本地嵌入模型。这确保了你的所有对话历史和记忆数据从未离开你的机器与使用本地 Ollama 模型的初衷一脉相承。只有在嵌入质量成为瓶颈且你对隐私要求不那么严苛时才考虑云端方案。代理服务器Agent Server的角色这是整个系统的“大脑”。它扮演了中间人的角色接收来自前端如 Chatbot UI或直接 API 调用的用户消息。查询向量数据库获取与当前消息相关的历史记忆。将原始用户消息、检索到的相关记忆、以及系统预设的提示词模板组合成最终的“增强版提示词”。将增强版提示词发送给后端的 Ollama 服务。接收 Ollama 的回复并从中提取可能的新记忆点将其向量化后存入数据库。将 Ollama 的回复返回给前端。这种设计将记忆逻辑与模型推理逻辑解耦使得系统非常灵活易于维护和扩展。3. 核心组件解析与实操要点理解了宏观设计我们深入到各个核心组件看看它们具体如何工作以及在部署和配置时需要注意什么。3.1 记忆的生成与提取策略记忆不是自动生成的需要明确的策略来告诉系统“什么值得记”。项目通常通过“系统提示词System Prompt”和“后处理函数”来实现。系统提示词中的记忆指令在发送给 Ollama 的提示词中除了用户问题还会包含类似这样的指令你是一个有帮助的助手并且拥有记忆能力。 以下是与你过去对话相关的记忆可能对回答当前问题有帮助 [相关记忆列表例如 - 用户曾提到他最喜欢的编程语言是Python。 - 用户上周在研究如何用Docker部署PostgreSQL。] 当前对话 用户{当前用户问题} 助手这个指令做了两件事一是告知模型它拥有记忆二是以清晰的结构化格式提供了相关记忆。模型会自然地参考这些记忆来生成回复。后处理从回复中提取新记忆当模型回复后系统需要判断回复中是否包含了值得长期存储的信息。一个简单但有效的策略是让模型自己决定。我们可以在提示词中要求模型在回复的末尾以特定格式如[MEMORY: 这是一条新记忆]标注出它认为应该保存的信息。代理服务器在收到回复后会解析这个特定格式提取出记忆文本。更复杂的策略可以引入另一个轻量级模型甚至是一个规则引擎来分析对话回合自动提取关键实体人物、地点、项目、用户声明的偏好“我不喜欢...”、“我通常用...”、或达成的结论“我们决定采用方案A”。实操心得记忆提取的粒度很重要。不要试图记录每一句话。初期可以保守一点只记录用户明确表达的长期事实和偏好例如“我是前端开发主要用React”。过于琐碎的记忆会污染向量数据库降低检索质量。你可以从简单的关键词/规则触发开始再逐步迭代到更智能的提取方式。3.2 向量数据库的配置与优化以 ChromaDB 为例它的使用虽然简单但几个配置项直接影响性能和效果。持久化路径默认情况下ChromaDB 在内存中运行数据重启即失。对于记忆系统必须设置持久化。在初始化客户端时指定persist_directory参数例如./chroma_db。这样所有记忆向量和元数据都会保存到磁盘。集合Collection管理ChromaDB 中的数据存储在“集合”中。一个好的实践是为不同用户或不同对话类型创建独立的集合例如memory_user_{id}。这可以实现记忆隔离避免用户A的记忆被用户B检索到同时也让检索更高效。集合的名称可以作为元数据存储在每条记忆向量中。元数据Metadata的妙用除了向量本身每条记忆都可以附带丰富的元数据例如user_id: 记忆所属用户。timestamp: 记忆创建时间。memory_type: 记忆类型如fact,preference,plan。source: 来源对话ID。在检索时你不仅可以进行向量相似度搜索还可以过滤元数据。例如“找出用户A在过去一周内所有关于‘Docker’的类型为fact的记忆”。这极大地提升了记忆检索的精准度。在初始化 ChromaDB 集合时就应该规划好元数据模式。嵌入维度与距离函数你使用的嵌入模型决定了向量的维度如all-MiniLM-L6-v2是384维。在创建集合时需要指定这个维度embedding_function通常会处理但需知晓。距离函数如余弦相似度、欧氏距离决定了“相似性”的计算方式余弦相似度是最常用于文本语义匹配的。3.3 代理服务器的实现细节代理服务器是连接一切的胶水。一个健壮的代理服务器需要处理以下问题并发与异步一个典型的代理服务器需要同时处理接收HTTP请求、查询向量数据库I/O操作、调用 Ollama APII/O操作、处理嵌入模型可能是CPU/GPU计算。使用异步框架如 Python 的FastAPIhttpxasyncio可以极大地提高吞吐量避免在等待数据库或模型响应时阻塞其他请求。错误处理与降级记忆系统应该是“优雅降级”的。如果向量数据库连接失败或者嵌入模型出错服务器应该能够记录错误并继续将没有记忆增强的原始请求转发给 Ollama保证核心的聊天功能不受影响而不是直接让整个服务崩溃。记忆的更新与去重同一条信息可能在不同对话中被多次提及。系统需要有能力判断一条新提取的记忆是否是已知记忆的重复或更新。一个简单的方案是在存储新记忆前先进行一次相似度检索。如果存在高度相似如余弦相似度 0.95的旧记忆则用新记忆更新旧记忆的时间戳而非新增一条。这可以防止数据库被大量重复记忆淹没。配置化管理所有关键参数都应通过配置文件如config.yaml或环境变量管理Ollama 服务器的地址和端口。向量数据库的连接信息和集合名称。嵌入模型的名称或云端 API 密钥。记忆检索的数量例如每次检索前5条最相关的记忆。系统提示词模板。这样便于在不同环境开发、测试、生产中切换配置。4. 完整部署与核心环节实现让我们以一个典型的本地开发环境为例一步步实现openclaw-ollama-memory的核心流程。假设我们使用 Python、FastAPI、Sentence Transformers 和 ChromaDB。4.1 环境准备与依赖安装首先创建一个干净的 Python 虚拟环境并安装核心依赖。# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 安装核心依赖 pip install fastapi uvicorn httpx sentence-transformers chromadb pydantic这里fastapi和uvicorn用于构建异步 Web 服务器httpx用于异步调用 Ollama APIsentence-transformers提供本地嵌入模型chromadb是向量数据库pydantic用于数据验证和设置管理。4.2 构建记忆存储与检索层我们创建一个memory_manager.py文件封装所有与向量数据库交互的逻辑。import chromadb from chromadb.config import Settings from sentence_transformers import SentenceTransformer import uuid from typing import List, Dict, Any class MemoryManager: def __init__(self, persist_dir: str ./chroma_memory, embedding_model_name: str all-MiniLM-L6-v2): # 初始化嵌入模型 self.embedder SentenceTransformer(embedding_model_name) # 初始化ChromaDB客户端设置持久化路径 self.client chromadb.Client(Settings( chroma_db_implduckdbparquet, persist_directorypersist_dir )) # 获取或创建记忆集合。这里我们为默认用户创建一个集合。 self.collection self.client.get_or_create_collection( nameuser_default_memories, metadata{hnsw:space: cosine} # 使用余弦相似度 ) def create_memory(self, text: str, metadata: Dict[str, Any] None): 创建并存储一条记忆 if metadata is None: metadata {} # 生成唯一ID memory_id str(uuid.uuid4()) # 为记忆文本生成向量嵌入 embedding self.embedder.encode(text).tolist() # 添加默认元数据 metadata.update({timestamp: datetime.utcnow().isoformat()}) # 存入集合 self.collection.add( documents[text], embeddings[embedding], metadatas[metadata], ids[memory_id] ) return memory_id def search_memories(self, query: str, n_results: int 5, filter_metadata: Dict None) - List[Dict]: 根据查询文本检索相关记忆 # 将查询文本转换为向量 query_embedding self.embedder.encode(query).tolist() # 执行搜索 results self.collection.query( query_embeddings[query_embedding], n_resultsn_results, wherefilter_metadata # 可选的元数据过滤 ) # 格式化返回结果 memories [] if results[documents]: for doc, meta, dist in zip(results[documents][0], results[metadatas][0], results[distances][0]): memories.append({ text: doc, metadata: meta, relevance_score: 1 - dist # 余弦距离转换为相似度分数 }) return memories这个类提供了记忆的创建和检索两个最基本的功能。注意我们使用了all-MiniLM-L6-v2模型它在质量和速度之间取得了很好的平衡非常适合本地运行。4.3 实现代理服务器接下来我们创建主应用main.py使用 FastAPI 构建代理服务器。from fastapi import FastAPI, HTTPException from pydantic import BaseModel import httpx from memory_manager import MemoryManager import asyncio app FastAPI(titleOllama Memory Agent) # 初始化组件 memory_mgr MemoryManager() OLLAMA_API_URL http://localhost:11434/api/generate # Ollama 默认地址 # 定义请求/响应模型 class ChatRequest(BaseModel): message: str user_id: str default # 简单起见用user_id区分记忆集合 class ChatResponse(BaseModel): reply: str relevant_memories: List[Dict] [] app.post(/chat, response_modelChatResponse) async def chat_with_memory(request: ChatRequest): 核心端点接收用户消息检索记忆增强提示词调用Ollama存储新记忆。 # 1. 检索相关记忆 relevant_mems memory_mgr.search_memories( queryrequest.message, n_results3, filter_metadata{user_id: request.user_id} # 只检索当前用户的记忆 ) # 2. 构建增强版系统提示词 memory_context if relevant_mems: memory_context 以下是你与用户过往对话中的相关记忆供你参考\n for mem in relevant_mems: memory_context f- {mem[text]}\n system_prompt f你是一个有帮助的AI助手。{memory_context} 请基于以上信息如果存在和你的知识回答用户的问题。 在回复的最后如果本次对话产生了值得长期记住的新信息例如用户明确陈述的事实、偏好或决定请用以下格式单独标注出来[MEMORY: 记忆内容]。如果没有则不要添加。 当前对话 用户{request.message} 助手 # 3. 调用 Ollama API async with httpx.AsyncClient(timeout30.0) as client: try: ollama_payload { model: llama3:8b, # 指定你本地运行的模型 prompt: system_prompt, stream: False } response await client.post(OLLAMA_API_URL, jsonollama_payload) response.raise_for_status() ollama_result response.json() full_reply ollama_result[response] except httpx.RequestError as exc: raise HTTPException(status_code500, detailf无法连接Ollama服务: {exc}) # 4. 从回复中提取新记忆 reply_to_user full_reply new_memory_text None # 简单解析 [MEMORY: ...] 格式 import re memory_match re.search(r\[MEMORY:\s*(.?)\], full_reply, re.DOTALL) if memory_match: new_memory_text memory_match.group(1).strip() # 从回复给用户的文本中移除记忆标记行 reply_to_user re.sub(r\[MEMORY:\s*.?\], , full_reply, flagsre.DOTALL).strip() # 5. 存储新记忆 if new_memory_text: memory_mgr.create_memory( textnew_memory_text, metadata{user_id: request.user_id, source_query: request.message[:50]} # 存储部分源问题作为上下文 ) # 6. 返回响应 return ChatResponse( replyreply_to_user, relevant_memoriesrelevant_mems ) if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)这个服务器启动后监听在8000端口。任何前端或客户端都可以向http://localhost:8000/chat发送 POST 请求来与具有记忆能力的 Ollama 模型对话。服务器会处理记忆的检索、提示词组装、模型调用和记忆存储的全流程。4.4 前端集成示例你可以使用任何前端框架。这里提供一个极简的 HTML/JS 示例展示如何调用这个代理 API。!DOCTYPE html html body h2Ollama with Memory/h2 div idchat/div input typetext idmessage placeholder输入你的消息... button onclicksendMessage()发送/button script const chatDiv document.getElementById(chat); const messageInput document.getElementById(message); async function sendMessage() { const message messageInput.value; if (!message) return; // 显示用户消息 chatDiv.innerHTML pb你:/b ${message}/p; messageInput.value ; try { const response await fetch(http://localhost:8000/chat, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ message: message, user_id: alice }) }); const data await response.json(); // 显示助手回复 chatDiv.innerHTML pb助手:/b ${data.reply}/p; // 可选显示本次用到的记忆调试用 if (data.relevant_memories data.relevant_memories.length 0) { chatDiv.innerHTML pi本次参考了记忆:/ibr; data.relevant_memories.forEach(mem { chatDiv.innerHTML - ${mem.text}br; }); chatDiv.innerHTML /p; } } catch (error) { console.error(Error:, error); chatDiv.innerHTML p stylecolor:red;请求出错: ${error.message}/p; } chatDiv.scrollTop chatDiv.scrollHeight; } // 允许按回车发送 messageInput.addEventListener(keypress, function(e) { if (e.key Enter) { sendMessage(); } }); /script /body /html5. 常见问题与排查技巧实录在实际部署和运行过程中你几乎一定会遇到下面这些问题。这里记录了我踩过的坑和解决方案。5.1 记忆检索不准确或无关这是最常见的问题。你问“今天天气如何”它却返回一条“用户喜欢Python”的记忆。可能原因与排查嵌入模型不匹配你使用的嵌入模型如all-MiniLM-L6-v2是通用模型可能对某些专业领域如医学、法律术语的语义捕捉不佳。可以尝试领域相关的嵌入模型如针对代码的all-MiniLM-L6-v2其实也不错或者更大的模型如all-mpnet-base-v2效果更好但更慢。记忆文本质量差如果存储的记忆是冗长的、包含无关信息的句子检索效果会下降。在存储前对记忆文本进行清洗和摘要。例如将“用户说‘我觉得Python比Java更好用因为它的语法简洁库也丰富。’” 摘要为“用户偏好Python认为其语法简洁、库丰富。”检索数量k值不当n_results参数太小可能错过相关记忆太大会引入噪声。通常从3-5开始调整。缺乏元数据过滤如果你没有用user_id等元数据过滤所有用户的记忆都会混在一起检索。务必为每条记忆添加并利用元数据进行过滤。优化技巧实现“重排序Re-ranking”机制。先用向量数据库快速召回 top-20 条记忆再用一个更精细的交叉编码器Cross-Encoder模型对这20条进行精排选出最相关的3-5条。这能显著提升精度但会增加延迟。5.2 系统响应速度变慢随着记忆条数增加超过1万条检索速度可能变慢。排查与解决检查向量数据库索引ChromaDB 默认使用 HNSW 索引确保其参数如M和ef_construction设置合理。对于海量数据可以考虑使用Qdrant或Weaviate它们对大规模向量的支持更成熟。异步操作确保你的代理服务器是完全异步的使用async/await避免在等待 Ollama 或数据库时阻塞。记忆归档与清理不是所有记忆都需要永久活跃。实现一个策略将旧的、很少被访问的记忆移动到“冷存储”如另一个 ChromaDB 集合或普通数据库只对热记忆进行向量检索。或者定期清理低重要性、过时的记忆。嵌入模型瓶颈本地嵌入模型编码需要时间。如果使用CPU编码长文本会慢。考虑使用 GPU如果可用或者对于检索可以预先计算并缓存记忆的向量而不是每次检索时实时编码查询语句实际上项目中的search_memories方法已经实时编码了查询语句这是必要的。瓶颈在于模型计算本身。5.3 Ollama 回复中不生成[MEMORY: ...]格式模型“不听话”没有按照指令输出记忆标记。解决步骤强化系统提示词在提示词中更明确、更强调格式要求。可以放在提示词开头并用示例说明。系统指令你必须在回复的末尾以 [MEMORY: 具体记忆内容] 的格式总结出本次对话中需要长期记住的要点。如果没有则不要添加任何东西。 示例 用户我的生日是7月20日。 助手好的我记下了。[MEMORY: 用户的生日是7月20日。]调整模型参数尝试降低 Ollama 生成时的temperature如设为 0.1让模型输出更确定性、更遵循指令。避免使用过高的temperature导致输出随机。后处理容错在代码中对模型回复的解析要更加鲁棒。除了正则表达式可以尝试寻找最后一行是否包含[MEMORY关键字或者使用更灵活的字符串匹配。微调模型进阶如果格式要求非常严格可以考虑用少量包含正确格式的对话数据对模型进行 LoRA 微调让它彻底学会这个任务。但这属于高阶操作。5.4 记忆污染与冲突用户可能改变主意或者之前记忆的信息是错误的。管理策略记忆来源标识在记忆元数据中记录其来源如对话ID、消息ID。当用户说“我之前说错了其实是...”你可以通过检索找到相关的旧记忆并将其标记为“已覆盖”或直接删除。置信度与时效性为记忆添加“置信度”分数例如来自用户明确声明的置信度高来自模型推测的置信度低和“最后验证时间”。在检索时可以优先返回高置信度、较新的记忆。提供记忆管理接口为用户提供一个简单的界面或命令如“/forget 关于XX的记忆”允许他们查看和删除自己的特定记忆。这增加了系统的可控性和用户体验。部署这样一个系统从简单的原型到稳定可用的服务是一个持续迭代的过程。关键是从小处着手先实现核心的“存-查-用”循环然后逐步增加元数据、过滤、清理、优化等高级功能。每次迭代都进行测试和你的助手聊上几十轮看看它是否真的记住了关键信息并在合适的时机运用出来。当你发现它能准确地说出“你上周提到正在学习 Kubernetes需要我推荐进阶教程吗”时那种感觉就像你的本地模型真正拥有了灵魂。