
给AI助手做“记忆”这件事我前后折腾了不少时间。最开始是把整理好的文档塞进知识库后来发现对话里那些真正重要的东西——用户随口提的偏好、某个项目的背景、下周要交稿的方向——根本不适合当成知识文档来管。它们更像便利贴短、碎、带时间属性、需要按标签翻找。于是我给Agent配了一套外接记忆以memos作为存储后端再写一个专门的记忆skill让助手既能往memos里写入记忆也能在需要时把历史记忆读回来。这套方案跑通之后对话体验完全不一样了。如果你也在做Agent应用正头疼“每次聊天都白纸开局”的问题这篇就把完整的实现思路、脚本、踩坑记录都摊开讲。1. 为什么是memosAgent的“白纸开局”缺一块可持久的外接记忆1.1 会话失忆是Agent落地的第一道坎做过实际AI应用的人应该都有这种体会单次会话里模型表现再聪明只要窗口一关下次对话它又把你当陌生人。我以前试图把所有历史都塞进上下文里效果很差一是token成本根本扛不住二是早期无关信息会严重干扰后续输出质量。更合理的思路是设置一道“记忆筛选”机制从对话中抽取出真正有长期价值的信息单独存起来下次需要时再精准取回。这个需求落到工程上就是一个“外接记忆库”能写入、能检索、能按时间排序、能分类打标。知识库工具适合存放成体系的文档但存一句话、一条偏好、一个时间节点多少有点大材小用。普通对话记录导出又太臃肿检索时噪声极大。我需要的其实是“速记”能力——这正好是我熟悉的memos这个自托管备忘工具的核心场景。1.2 memos恰好长在“记一笔”的场景上memos的设计哲学就是“快速记一条”界面极简核心数据模型只有content、visibility、createdTs这几个字段。很多人在自己服务器上部署它主要是用来替代手机备忘录或者搭私人的flomo式记录流。但对我这类技术用户来说它真正值钱的地方是那套干净的REST API一条内容、一个时间戳、一组标签正好能映射成Agent记忆的最小单元。拿它跟其他方案对比一下优劣很清楚方案优点不适合当记忆库的原因云笔记类应用生态成熟客户端多API普遍封闭笔记结构太重不适合程序化高频读写本地Markdown文件数据完全私有格式自由并发写入、标签检索、时间线过滤都需要自己造轮子向量数据库语义检索强大部署成本高一条记忆需要embedding、索引明显杀鸡用牛刀memos自托管、API干净、标签时间线兼顾没有语义检索但关键词检索对记忆场景够用我实际用下来记忆条目往往是“用户喜欢把结论放前面”“项目A的验收时间是周三”这类短文本关键词精确匹配完全够用没必要上向量。而且memos自托管数据在自己手里写隐私信息也不心虚。另外一个重要的点是memos有完整的visibility控制单条数据可以设为PRIVATE或PUBLIC。Agent记录的记忆绝大多数都应该私有这个字段天然就把隐私边界划好了。1.3 “Skill”在这里扮演什么角色所谓skill本质上是一份“写给AI看的操作文档一组可执行脚本”。脚本普通人也会写但skill的价值在于Agent在对话中读到说明文件后能自己判断“现在该调用哪个脚本、传什么参数、返回结果怎么解读”。放到记忆场景里这个设计很关键。我不想每次对话都在系统prompt里手写一大段“请把新信息保存到memos”的复杂指令而是希望Agent看到skill后自动掌握三件事什么时候该写入、什么时候该读取、写入内容用什么格式。把这些决策逻辑固化在skill文件里既减少prompt占用的token也让行为更可预期。2. Skill的里子读写两条链路和记忆条目的结构设计2.1 先解决“记什么”记忆条目该有的结构动手写脚本之前我先花了不少时间想一个更前置的问题什么信息才值得被Agent写进记忆库如果让Agent把每句话都记下来第二天记忆库就是垃圾场。我后来定了一套筛选标准凡是通过的标准才写入用户主动要求记住的比如“以后写方案默认用短句”用户稳定偏好比如“我负责电商类目文案”“我喜欢图片放左边”项目级背景事实比如“下周三要给某品牌交一篇公众号文章强调性价比”跨会话需要延续的状态比如“上次讨论的方案已经定了用双列布局”反过来一次性的对话指令、临时闲聊、当轮就能完成的任务都不该入库。记忆内容本身也不能是对话原文的复制需要经过整理。我给它定了一个极简模板[记忆类型] 客观描述获取时间举例[用户偏好] 写作时偏爱短段落每段不超过四行2025-06-10 获取 [项目背景] 某品牌公众号文章下周三交稿重点是突出性价比2025-06-10 获取 [用户身份] 内容运营主要做电商类目文案2025-06-11 获取记忆类型我用中文方括号标注配合memos本身的#标签再做一层聚合。标签是给机器看的方括号类型是给模型看的双层结构检索时很顺手。2.2 两条链路写入路径和读取路径整体架构并不复杂两条链路各自独立写入链路是“Agent判断信息有价值 → 调用写入脚本 → POST到memos API → 返回id和创建时间”。读取链路是“会话开始或用户触发回忆 → 调用读取脚本 → 按关键词或最近时间拉取 → 结果注入当前对话上下文”。两条链路不需要共享状态各跑各的增加了系统的稳定性。读取方式我分了两种模式。一种是开机预载新会话开始时读取最近10条记忆让Agent先获得一个大致的背景感另一种是按需搜索用户提到“之前说过的那个项目”时用关键词精确过滤。两种模式对应两个不同的调用参数内部可以复用同一个API请求。2.3 涉及的关键API与鉴权memos从某个版本开始提供了标准的REST API。我用的核心端口是这些认证状态检查GET /api/v1/auth/status创建一条POST /api/v1/memos获取列表GET /api/v1/memos?limit10offset0获取单条GET /api/v1/memos/{id}更新内容PATCH /api/v1/memos/{id}鉴权方式是在memos网页端的个人设置里生成Access Token请求时放进HeaderAuthorization: Bearer 你的token这个token建议放在环境变量里脚本通过os.environ读取绝对不要硬编码进代码文件不然哪天把脚本推到git仓库里就是事故。2.4 写入格式与读取格式之间的约定记忆能被顺利读回来靠的是写入时有统一格式。我在skill的说明文件里明确要求Agent按[类型] 描述获取时间的模板整理内容再把完整内容作为一条memo写入。标签作为内容的一部分直接拼在文本末尾比如#记忆/用户偏好。这样读取脚本只需要做关键词匹配不需要额外解析标签字段兼容性更好。读取脚本返回给Agent的结果我会做一层简化只保留id、content、createdTs三个字段。createdTs是Unix时间戳Agent可以自己换算成人类时间也可以提示脚本直接输出格式化时间。我的做法是脚本就把时间转好省得模型再算一次。3. 代码级落地SKILL.md、写入脚本、读取脚本一次给全3.1 目录结构先摆好我的记忆skill放在Agent的skills目录下结构是这样skills/memos-memory/ ├── SKILL.md ├── write_memo.py ├── read_memos.py └── requirements.txtrequirements.txt里只要requests一个依赖保持轻量。3.2 SKILL.md的编写要点这个文件是整个skill的灵魂。Agent不会先读代码它首先读的是这份说明。所以说明文件里必须写清楚三个维度触发条件、调用方式、内容规范。--- name: memos-memory description: 将对话中值得长期保存的信息写入memos记忆库并支持按关键词或最新时间读取历史记忆。当用户透露偏好、补充背景、布置跨会话任务时可用。 --- # memos 记忆读写 该skill用于维护Agent的长期记忆。所有操作通过脚本完成。 ## 什么时候写入 - 用户明确要求记住某事 - 用户透露了稳定的身份、偏好、习惯 - 用户给出后续会话需要使用的项目信息、时间安排 - 注意不记录一次性指令与临时闲聊 ## 内容格式 请把信息整理为一行 [类型] 客观描述获取时间 类型可选值用户偏好、用户身份、项目背景、计划安排、决策记录 ## 什么时候读取 - 新会话开始有明确主题时先按关键词搜索历史记忆 - 用户使用之前说过还记得吗等表达时绝对先读取再回答 ## 调用方式 - 写入一条python write_memo.py 内容 - 读取最近python read_memos.py --mode recent --limit 10 - 关键词搜索python read_memos.py --mode search --keyword 关键词description字段我写得比较克制没用“请务必调用”这类命令式措辞而是用“当……时可用”来暗示触发条件。不同Agent框架对description的理解方式略有差异但这种表述通常不容易误触发。3.3 写入脚本write_memo.py写入脚本只做一件事接收内容POST到memos API。它不负责判断该不该写那是Agent结合SKILL.md判断的事。职责分离能让脚本保持简单也方便单独调试。#!/usr/bin/env python3 写入一条记忆到memos。 用法 python write_memo.py 记忆内容 python write_memo.py 记忆内容 --visibility PRIVATE import argparse import os import sys import requests BASE_URL os.environ.get(MEMOS_BASE_URL, https://memos.example.com).rstrip(/) TOKEN os.environ.get(MEMOS_TOKEN, ) def write_memo(content: str, visibility: str PRIVATE): if not content.strip(): return {ok: False, message: 内容为空拒绝写入} headers { Authorization: fBearer {TOKEN}, Content-Type: application/json, } payload { content: content.strip(), visibility: visibility, } resp requests.post( f{BASE_URL}/api/v1/memos, headersheaders, jsonpayload, timeout10, ) if resp.status_code ! 200: return {ok: False, status: resp.status_code, body: resp.text} data resp.json() return { ok: True, id: data.get(id), createdTs: data.get(createdTs), content: data.get(content), } def main(): parser argparse.ArgumentParser() parser.add_argument(content, help要写入的memo内容) parser.add_argument(--visibility, defaultPRIVATE) args parser.parse_args() result write_memo(args.content, args.visibility) print(result) sys.exit(0 if result.get(ok) else 1) if __name__ __main__: main()脚本对这个场景已经够用了。注意requests.post里必须带超时参数否则memos服务假死时脚本会卡住连带Agent的执行也跟着卡住。超时设成10秒够用。3.4 读取脚本read_memos.py读取脚本我做了两种模式recent直接拉最近N条search先拉一批在内存里按关键词过滤。本来想用memos API自带的filter参数但不同版本支持程度不同内存过滤最稳数据量也不大。#!/usr/bin/env python3 从memos读取记忆。 用法 python read_memos.py --mode recent --limit 10 python read_memos.py --mode search --keyword 电商 --limit 20 import argparse import os import sys import requests BASE_URL os.environ.get(MEMOS_BASE_URL, https://memos.example.com).rstrip(/) TOKEN os.environ.get(MEMOS_TOKEN, ) def fetch_memos(limit: int, offset: int 0): headers {Authorization: fBearer {TOKEN}} params {limit: limit, offset: offset} resp requests.get( f{BASE_URL}/api/v1/memos, headersheaders, paramsparams, timeout10, ) if resp.status_code ! 200: return {ok: False, status: resp.status_code, body: resp.text} data resp.json() memos data.get(memos, data) if isinstance(data, dict) else data return {ok: True, memos: memos} def filter_by_keyword(memos, keyword: str): word keyword.strip().lower() if not word: return memos result [] for memo in memos: content (memo.get(content) or ).lower() if word in content: result.append(memo) return result def simplify(memos): out [] for m in memos: out.append( { id: m.get(id), content: m.get(content), createdTs: m.get(createdTs), } ) return out def main(): parser argparse.ArgumentParser() parser.add_argument(--mode, choices[recent, search], defaultrecent) parser.add_argument(--keyword, default) parser.add_argument(--limit, typeint, default10) args parser.parse_args() result fetch_memos(args.limit) if not result.get(ok): print(result) sys.exit(1) memos result[memos] if args.mode search: memos filter_by_keyword(memos, args.keyword) print(simplify(memos)) sys.exit(0) if __name__ __main__: main()读取结果打印成一个列表每个元素是{id, content, createdTs}。Agent拿到这个结构后自己会选择哪些内容与当前话题相关。id这个字段很有用如果后续要做“更新旧记忆”的操作可以直接用id定位。3.5 环境变量与配置两个脚本都从环境变量读取MEMOS_BASE_URL和MEMOS_TOKEN。我通常在启动Agent服务前把它们写进启动脚本或者systemd环境文件。本机调试时也可以放在.env文件里但记得把.env加进.gitignore。export MEMOS_BASE_URLhttps://memos.example.com export MEMOS_TOKEN你的token如果你不确定token到底能不能用先跑一条命令验证curl -s -H Authorization: Bearer $MEMOS_TOKEN $MEMOS_BASE_URL/api/v1/auth/status能返回当前用户名就说明鉴权通了再往下排错就简单了。4. 实测一轮完整闭环从“请记住”到“你还记得吗”4.1 第一轮对话信息沉淀脚本就绪后我在实际Agent环境里跑了一轮测试把整个闭环走通。第一轮对话用户跟Agent说“我是做内容运营的主要写电商类目文案喜欢用短句。对了下周要给某品牌写公众号文章重点是强调性价比。”Agent根据SKILL.md判断出这里含三条稳定记忆用户身份、写作偏好、一个项目安排。它连续调了三次写入脚本python write_memo.py [用户身份] 内容运营主要是电商类目文案2025-06-11 获取 python write_memo.py [用户偏好] 写作时偏爱短段落2025-06-11 获取 python write_memo.py [计划安排] 某品牌公众号文章下周三交稿重点是突出性价比2025-06-11 获取每次脚本返回的json里都有新生成的memo id和createdTs。Agent在回复里顺带告诉我“已记录”但不会把记忆内容复述一遍避免对话过载。回头我打开memos网页端能看到这三条按时间排得整整齐齐还带着#记忆的标签簇。这里有个小细节Agent写的内容是整理后的短句而不是对话原文。对话原文是口语化的包含大量无关词直接存进去只会增加后续检索的干扰。4.2 第二轮对话记忆被主动取回隔了几个小时我开了一个全新会话完全清空上下文直接问“帮我想想那篇公众号文章的开头重点是我们之前讨论过的产品卖点。”Agent的skill说明里有一条规则当用户提到“之前讨论过”这类表达时必须先读取记忆再回答。它实际执行的是python read_memos.py --mode search --keyword 公众号脚本返回了匹配到的memmo列表其中就包括第一条会话记录里保存的“某品牌公众号文章下周三交稿重点是突出性价比”。Agent把这条记忆和“用户偏爱短段落”这个偏好一起用于回答写出的开头明显是变短的句式结构还主动提了一句“按你偏好的短段落风格来写”。这个结果就是我想要的效果Agent不再白纸开局它知道对话者是谁、之前聊过什么、下一步往哪走。从用户视角来看AI好像“记得”我信任感和交互效率都会明显提升。4.3 预载模式的实际表现我还额外测试了recent模式的预载效果。在会话一开始就执行python read_memos.py --mode recent --limit 10把最近十条记忆全部注入到系统上下文中。这个做法适合让Agent对用户近期动态有一个宽泛了解但它有副作用如果最近记忆里有大量过期项目信息反而会干扰当前话题的判断。所以我把预载作为可选项默认关闭只有面对“用户经常进行同一领域连续创作”这类场景时才会打开。5. 踩坑与取舍API版本、鉴权、冲突和记忆膨胀5.1 API版本差异曾经把脚本搞挂过这是我踩得最重的一个坑。memos不同版本的API路径和响应结构差异不小网上搜到的不少资料还停留在老版本阶段。老版本用的是/api/memo这种不带版本前缀的路径返回结果直接就是数组新版本改成/api/v1/memos返回体包裹在memos字段里。如果你的memos部署版本比较旧脚本就会404或者解析出错。我的兼容方案是写了一个极小工具函数统一处理响应def extract_memos(data): if isinstance(data, dict): return data.get(memos, []) return data但如果路径都换了解析函数也救不了。更稳妥的做法是写脚本前先确认memos版本或者干脆先在浏览器里开开发者工具手动请求一次/api/v1/auth/status看返回码。这次验证花不了两分钟能省一晚上的排错时间。5.2 鉴权细节Bearer前缀不能漏有次脚本报401我检查半天最后发现请求头里写了Authorization: Token xxx而不是Authorization: Bearer xxx。网上示例鱼龙混杂有的确实用别的写法。memos目前的API是认Bearer前缀的这个必须跟它对齐。另外要注意token本身是有“有效期”和“撤销机制”的。我遇到过一个情况memos服务重装后旧的token全部失效Agent的读写脚本一夜之间全部401。如果日志里没有把报错信息打全排查起来非常痛苦。所以我后来在读写脚本里都强制把状态码和响应体一起输出而不是只打印一个“请求失败”。5.3 内容清洗Markdown换行和检索噪声memos存的是Markdown文本换行符在API响应里会以\n的形式出现读取时不做处理印出来就是一团乱。更重要的是Agent在写入时如果保留了大量口语化内容比如“emmm”“然后然后”这类语气词后续关键词搜索的命中率会很低因为用户第二次会话时用的词跟第一轮口语用词经常对不上。我在SKILL.md里强制要求写入内容必须是客观描述句不允许出现语气词。另外读取时对content做了一次空白字符压缩把连续的换行和空格合并既给Agent省token也让语义更干净。5.4 记忆冲突新信息到底该不该覆盖旧信息用户是很善变的。周一他说“我不太喜欢用列表回答问题”周三他又说“以后回答尽量用列表”。如果Agent只会无脑追加记忆库里就会同时存在两条互相矛盾的信息下次读取时模型就会左右为难。我的处理方式是Agent在写入新记忆前先跑一次关键词搜索查看同主题下有没有旧记忆。如果有且明显冲突就在新条目末尾追加一行此信息更新于日期与之前的记录不同以本条为准。这样模型读到两条时至少能根据“以本条为准”的标记做出正确选择。覆盖删除旧条目这个操作我尽量不做因为保留变更历史有时是有价值的。5.5 记忆膨胀记忆库也需要定期“断舍离”跑了一个多月后我的memos记忆库里有上千条记录其中相当一部分是早已过期的临时安排。记忆太多会让搜索命中率下降也会让读回的内容缺乏重点。我建立的策略是每个月做一次归档把超过90天、且不属于用户身份和稳定偏好的条目导出备份后删除只保留真正长期有效的记忆。如果追求自动化可以让读取脚本支持一个--before参数按时间筛选出过期条目再通过删除脚本处理。但这必须谨慎我建议手动确认一遍毕竟记忆误删很难恢复。5.6 隐私边界的取舍记忆内容默认PRIVATE这点务必在写入脚本里封死。即使未来要做多用户协同我也不建议把Agent的记忆默认设为公开。另外一个容易被忽略的点是memos是自托管服务如果服务器本身没做备份记忆库的安全性就全押在那台机器上。我给memos数据目录做了定时快照对个人应用来说这个成本很低但换回的安心感很高。这套记忆skill我从搭好到现在跑了将近两个月最大的感受是真正让Agent显得“懂你”的往往不是模型本身多聪明而是它能在合适的时机说出“我记得你之前提过这件事”。memos这种轻量速记工具当记忆后端没有重型基础设施带来的心理负担也没有复杂依赖链一个脚本加一个API就撑起了全部能力。最后分享一个小技巧我会在每周日让Agent对记忆库做一次“回顾整理”找到明显过期的项目条目并标记而不是删除。这样记忆库保持活跃Agent每次读取时也更清楚哪些是当下真正有用的信息。