资讯动态

给AI助手装个长期记忆外挂:memos REST API封装实战

发布时间:2026/10/18 5:35:19 来源:尧图企业网站定制
1. 项目定位给 AI 装一个长期记忆的外挂你给智能助手配了再聪明的模型它一关对话就“失忆”——今天聊过的偏好、记下的待办、查过的资料下次会话全都归零。我自己的解决办法是把一个叫 memos 的轻量级开源备忘录项目改造成 AI 的“外挂记忆”。具体做法就是写一个 skill给它两个动作一个是写入记忆一个是读取记忆底层全部走 memos 的 REST API。这套方案实测下来相当稳而且完全自托管数据都在自己手里不依赖任何第三方存储。这篇文章适合两类人看。第一类是想给个人 AI 助手加长期记忆的开发者第二类是自己已经在用 memos、但只拿它当普通笔记工具、想进一步挖掘 API 价值的用户。我会从 API 细节讲起把“写入记忆”和“读取记忆”两个函数完整的封装思路、对接大模型的 function calling 配置、以及我踩过的坑全部展开。没有复杂的框架依赖一个 Python 脚本加一个 memos 实例就能跑起来。1.1 要解决的真实痛点现在的大模型对话本质上还是无状态的。每次会话开始时模型只能拿到当前上下文窗口里的内容之前聊过的关键信息全都不见了。你可以在每次对话时手动把背景资料贴进去但这样既费 token又容易漏。更关键的是很多信息是对话过程中才产生的用户说“我平时喝手冲咖啡比较多”“下周三有个项目评审会”“我不喜欢邮件里带表情符号”。这些细节当下很有价值但如果不持久化几小时后重新开一个会话就彻底丢掉了。我最早试着用本地 JSON 文件存这些信息后来改用 SQLite。但问题也很明显文件存储的检索能力太弱想按关键词找一条半年前的记录非常痛苦SQLite 又得自己写表结构、管理连接维护成本不低。于是我开始找现成的、带 API 的存储方案。memos 就是在对比一圈之后留下的选择。memos 本身就是一个定位为“记录碎片化想法”的开源应用支持 Markdown、标签、公开/私有可见性还自带时间线。它最吸引我的一点是 API 足够简单干净——创建一条记录只需要 POST 一个 JSON查询也只需要 GET返回结构清晰。这种简洁性特别适合当一个 skill 的后端存储我不需要自己维护数据库也不用考虑数据模型的演化memos 已经把这些事都做了。1.2 为什么选定 memos 而不是别的方案做一个记忆模块存储选型的关键指标其实就那么几个部署成本、API 复杂度、检索能力、数据归属、扩展性。先看部署成本。memos 官方提供 Docker 镜像一条命令就能起服务二进制发布包也支持直接跑。我自己的部署环境是一台跑着 Docker 的小主机docker run -d -p 5230:5230 -v memos-data:/var/opt/memos就完成了前后不超过五分钟。对比自建一套带数据库的笔记系统这个上手成本几乎可以忽略。再看 API 复杂度。memos 暴露的是标准的 REST 接口认证方式是一个 Bearer Token不需要 OAuth 跳转不需要签名逻辑。创建记录、查询列表、获取详情、更新、删除五个操作对应五个 HTTP 方法非常符合一个 skill 模块的调用场景。我封装成函数时每个函数的核心逻辑不超过二十行。检索能力方面memos 支持按关键词过滤也支持设置 limit/offset 做分页。虽然它的检索是基于简单匹配而不是全文索引但对于“记忆”这种非精确检索场景已经足够用了。如果你需要更精细的筛选还可以利用 memos 的标签机制——把标签设计成结构化的键值格式比如#pref/coffee、#todo/2025-06就能实现分组和定时归档。数据归属则是另一个关键点。memos 的数据库文件就在你的服务器上导出备份都直接可控不会出现“笔记存在别人那里哪天服务关了数据就没了”的情况。对于长期记忆这种带有隐私性质的数据自托管是刚需。我并不是说别的方案不好。比如用专门的向量数据库做记忆语义检索确实更强但基础设施复杂度也上来了用云上的 KV 存储又要处理账号、计费、数据出境这些问题。memos 恰好站在一个平衡点上足够轻足够私密API 足够规整。1.3 skill 到底是个什么东西聊到这里得把“skill”这个概念说清楚。在很多智能助手平台上skill 指的是给助手外挂的一个能力模块它定义了一组可以被大模型调用的函数每个函数都有名字、参数描述和实现逻辑。模型在对话过程中判断“现在需要调工具了”就会按函数声明生成调用参数然后由你的代码真正执行最后把执行结果返回给模型。所以这个项目的本质就是把 memos 封装成两个可被大模型调用的工具函数memos_write_memory(content, visibility)把一段文本写入 memos作为一条记忆。memos_read_memory(query, limit)从 memos 里按关键词检索若干条历史记录。模型负责判断“什么时候该记、什么时候该查”memos 负责真实存储中间的胶水就是这两个函数。这种分工很像我平时用的“检索增强生成”套路但更轻量——不需要嵌入模型不需要向量库一个可以执行 SQL 级别查询的 REST API 就够了。第一节先把定位说清楚。接下来我会把 memos 的 API 细节完整过一遍因为后面所有封装的正确性都建立在 API 行为之上。2. 摸清底牌memos 的 API 与鉴权2.1 准备一个可用的 memos 实例如果你还没有 memos 实例先花五分钟部署一个。我在文章里以 Docker 部署为例操作步骤很简单mkdir -p ~/memos-data docker run -d --name memos \ -p 5230:5230 \ -v ~/memos-data:/var/opt/memos \ memos/memos:latest启动之后浏览器访问http://127.0.0.1:5230首次访问会让你创建一个管理员账号。这个账号只用于登录后台真正和 API 打交道时用的不是它而是下面要说的 Access Token。如果你不想用 Docker也可以去项目发布页下载对应的二进制文件直接运行./memos。两种方式的数据目录都是同一个后续升级注意备份数据库文件就行。部署完以后建议先手动添加一条测试 memo确认服务本身能正常读写。这一步能帮你把“memos 的问题”和“后面 skill 的问题”区分开排查时不用两头猜。2.2 鉴权Token 的生成与请求头memos 的 API 鉴权用的是 Bearer Token不是 Cookie。Token 的生成位置在后台界面的“设置—我的账户—访问令牌”里点击创建后会弹出一串字符串只显示这一次建议马上保存到一个只有自己能读到的环境变量文件里。拿到 Token 之后所有 API 请求都需要在请求头里带上Authorization: Bearer 你的token Content-Type: application/json这里有一个新手最常见的翻车点memos 对 Token 的大小写和空格非常敏感。复制时不要把换行符带进去也不要在Bearer后面多打一个空格导致解析失败。我建议把 Token 放到.env文件中用环境变量读取而不是写死在代码里。2.3 写入记忆创建一条 memo写入是 skill 的重中之重。memos 创建 memo 的接口定义如下POST /api/v1/memos Content-Type: application/json { content: 要记录的内容, visibility: PRIVATE }三个字段分别为content正文支持 Markdown 格式里面出现的#标签会被 memos 自动识别为主题标签。visibility可见性常用的有PRIVATE仅自己可见和PUBLIC公开。某些版本还支持PROTECTED表示登录用户可见。对记忆类数据一律用PRIVATE。createdTs可选默认为当前时间不建议手动传除非你要做历史数据迁移。请求成功以后memos 会返回一个包含完整记录的对象关键字段包括id、content、visibility、createdTs、updatedTs。这个id非常重要后续的更新、删除、精确读取都靠它。2.4 读取记忆列表、检索与筛选读取是另一个核心动作。memos 提供了几种读取方式获取全部 memo 列表GET /api/v1/memos?limit20offset0按关键词过滤GET /api/v1/memos?keyword咖啡获取单条GET /api/v1/memos/{id}返回结果是一个 JSON 数组数组里的元素结构和创建时返回的单条记录一致。默认排序是创建时间倒序最新的在前面这正好符合“记忆读取”的直觉一般我们更关心最近记下的内容。keyword参数在 v0.23 之后的版本里才稳定可用。遇到老版本或者你想做更精细的过滤就在本地把列表数据取回来再用 Python 做二次筛选。我写的封装函数里两种方式都做了兼容后面会展示代码。另外limit和offset是分页控制。单次请求不要拉太多我的经验是一次取 20 条足够。真要翻更早的记忆再用offset翻页避免一次请求把整个库都拖回来。2.5 更新与删除记忆也需要维护有了写入和读取还差两个辅助操作更新和删除。因为记忆不是死的——用户可能会纠正之前的偏好也可能要求删除某条隐私信息。更新用 PATCH 请求路径是/api/v1/memos/{id}请求体里只需要传你要修改的字段比如PATCH /api/v1/memos/42 Content-Type: application/json { content: 修改后的内容 }删除用 DELETE 请求路径同样是/api/v1/memos/{id}。删除是不可恢复的所以我在 skill 里把删除函数设计成额外要求一个确认参数避免多轮对话中模型误删数据。到这里API 层的信息已经足够。下面是核心的封装工作把它们变成真正的“skill”。3. 把读写能力封装成 skill3.1 先定义 skill 的输入输出契约写任何可被外部调用的模块第一步都是把接口契约定清楚。我是这样定义两个函数的写入记忆函数名称memos_write_memory输入content必填字符串最长建议不超过 2000 字visibility可选默认PRIVATEtags可选字符串列表。输出写入成功返回新 memo 的id。失败抛出带状态码和响应体的异常。读取记忆函数名称memos_read_memory输入query可选关键词空值表示返回最近记录limit可选默认 20最大 100。输出一个列表每个元素是{id, content, created_ts, visibility}。失败抛出带状态码和响应体的异常。设计接口时有个原则给大模型看到的参数越少越好。模型不需要知道 offset 分页也不需要知道 visibility 的完整枚举只暴露它做决策时真正需要的参数。分页和默认值都在函数内部处理。3.2 写入记忆的函数实现写入函数的核心逻辑可以拆成三块参数处理、请求发送、结果校验。参数处理阶段如果传了tags我会把它们拼到 content 的末尾用空格分隔。例如标签[coffee, pref]会变成#coffee #pref追加在正文后面。这样做的原因是 memos 把标签当作 content 的一部分来解析没有单独的标签字段。把标签放末尾既不影响正文阅读又方便后续按标签检索。请求发送阶段直接构造 HTTP POST 请求关键点在于超时设置。我的网络环境里 memos 和调用方在同一内网timeout10已经足够如果你跨机器调用建议放宽到 30 秒。结果校验阶段最简单的做法是检查 HTTP 状态码4xx/5xx 直接抛异常。更稳妥一点是验证响应体里确实有id字段避免上游返回了异常结构而程序还不知道。完整代码我放在后面统一给出。这里先记一个容易踩的细节memos 的可见性字段大小写敏感必须是PRIVATE全大写。传private会直接 400。3.3 读取记忆的函数实现读取函数要处理的细节更多一些。首先是关键词参数memos 服务端的keyword参数是比较简单的子串匹配。如果你的 memos 版本较老这个参数不生效接口不会报错只是忽略它返回全量列表。我习惯的做法是同时传服务端keyword再在本地做一次query.lower() in content.lower()的兜底过滤。这样老版本也能用虽然多了一次遍历但数据量在几千条以内性能没有任何问题。其次是时间字段的处理。memos 返回的createdTs是 Unix 秒级时间戳不是格式化好的时间字符串。直接把这个数字返回给大模型模型很难直观判断“这条记忆是什么时候记的”。我的处理是把时间戳转成YYYY-MM-DD HH:mm这样可读的格式再放进返回结果里。转换时要注意时区默认用服务器本地时区我在代码里用time.localtime()而不是time.gmtime()。最后是结果列表的瘦身。memos 返回的字段很多有些版本还包含rowStatus、pinned这类内部字段。给大模型的返回结果应该足够干净所以我只保留了四个字段id、content、created_ts、visibility。这样上下文里不会塞满无意义的元数据token 也更省。3.4 对接大模型function calling 的声明写法函数写好了还要让大模型知道在什么情况下调用它们。以 OpenAI 兼容的 function calling 格式为例把工具声明成这样[ { type: function, function: { name: memos_write_memory, description: 写入一条长期记忆到 memos。当用户明确说出需要记住的信息、表达个人偏好、布置待办任务时调用。, parameters: { type: object, properties: { content: { type: string, description: 要记住的内容原文 }, visibility: { type: string, enum: [PRIVATE, PUBLIC], default: PRIVATE }, tags: { type: array, items: {type: string}, description: 附加标签如 coffee、todo } }, required: [content] } } }, { type: function, function: { name: memos_read_memory, description: 从 memos 中读取历史记忆。当用户询问之前说过的内容、查询个人偏好或回顾历史信息时调用。, parameters: { type: object, properties: { query: { type: string, description: 检索关键词如 咖啡、项目评审 }, limit: { type: integer, description: 返回条数默认20 } } } } } ]写 description 的时候要站在模型的角度想它怎么判断该不该调用因此描述里要写明触发场景。比如“用户明确说出需要记住的信息”就是一种触发条件而“询问之前说过的内容”则是读取的触发条件。描述写得越具体调用准确率越高。3.5 完整代码示例下面是我目前在生产环境里跑着的 skill 封装Python 实现只依赖requestsimport os import time import requests MEMOS_URL os.getenv(MEMOS_URL, http://127.0.0.1:5230).rstrip(/) MEMOS_TOKEN os.getenv(MEMOS_TOKEN, ) HEADERS { Authorization: fBearer {MEMOS_TOKEN}, Content-Type: application/json, } def memos_write_memory(content: str, visibility: str PRIVATE, tagsNone) - int: 写入一条记忆到 memos返回 memo id。 if not content.strip(): raise ValueError(content 不能为空) # 标签拼到 content 末尾memos 用 #标签 解析 if tags: tag_str .join([f#{tag} for tag in tags]) content f{content} {tag_str} payload { content: content, visibility: visibility.upper(), } resp requests.post( f{MEMOS_URL}/api/v1/memos, jsonpayload, headersHEADERS, timeout10, ) resp.raise_for_status() data resp.json() return int(data[id]) def memos_read_memory(query: str None, limit: int 20) - list[dict]: 从 memos 读取历史记忆支持关键词过滤。 params {limit: int(limit), offset: 0} if query: params[keyword] query resp requests.get( f{MEMOS_URL}/api/v1/memos, paramsparams, headersHEADERS, timeout10, ) resp.raise_for_status() items resp.json() # 本地二次过滤兼容老版本 keyword 参数失效的情况 if query: q query.lower() items [m for m in items if q in m.get(content, ).lower()] result [] for m in items: ts m.get(createdTs, 0) result.append( { id: m.get(id), content: m.get(content, ), created_ts: time.strftime(%Y-%m-%d %H:%M, time.localtime(ts)), visibility: m.get(visibility, ), } ) return result def memos_delete_memory(memo_id: int, confirm: bool False) - bool: 删除一条记忆需要显式确认。 if not confirm: raise PermissionError(删除操作需要 confirmTrue 确认) resp requests.delete( f{MEMOS_URL}/api/v1/memos/{memo_id}, headersHEADERS, timeout10, ) resp.raise_for_status() return True代码本身不复杂难点全在细节处理标签拼接、可见性大小写、时间戳转换、删除确认。这些都是在实际调用中被坑过之后补上的属于“不写不知道写了少踩坑”的部分。4. 端到端实操从零跑通记忆读写4.1 环境准备先把运行环境准备好。需要一台能访问 memos 的机器Python 3.9 以上然后安装requestspip install requests接着配置环境变量。在项目目录下创建一个.env文件内容这样写MEMOS_URLhttp://127.0.0.1:5230 MEMOS_TOKEN你的访问令牌然后在代码里读取。为了简单我在示例中直接用python-dotenv或手动设置环境变量都可以。实际项目中要注意.env文件必须加入.gitignore绝对不要提交到仓库。4.2 写入一条记忆并验证启动一个 Python 交互终端先把函数导入from memos_skill import memos_write_memory, memos_read_memory然后写入一条测试记忆memo_id memos_write_memory( content用户偏好喜欢手冲咖啡尤其偏爱浅烘焙的耶加雪菲, tags[pref, coffee], ) print(memo_id)正常情况下会打印一个数字比如42。这个时候去 memos 后台看一眼应该能看到这条记录正文后面跟着#pref #coffee两个标签可见性显示为私密。这个操作验证了两个点一是 API 鉴权配置正确二是标签拼接逻辑生效。如果后台看不到记录优先检查 Token 和网络连通性。4.3 读取记忆并验证继续在 Python 终端执行records memos_read_memory(query咖啡) for r in records: print(r[created_ts], r[content])预期会输出刚才写入的那条记录内容前面的时间戳格式类似2025-01-15 14:32。如果输出为空可能是两个原因一是关键词不一致memos 的匹配是子串匹配“咖啡”和“耶加雪菲”不互相匹配二是刚才写入的时候 tag 拼到了 content 末尾而关键词正好在 tag 里这时匹配到的其实是 tag 那一段。读取函数还有一个省 token 的设计它已经帮大模型把时间戳格式化好了返回结果是可直接阅读的字符串不需要模型再做二次换算。4.4 模拟一次对话插入记忆最后做一个完整的模拟假设你现在要跑一个带记忆的 AI 助手。用户第一句话是“帮我记一下我不吃香菜另外下周三下午三点要开会”。模型收到这句话通过 function calling 判断需要调用写入记忆工具于是生成这样一个调用参数{ name: memos_write_memory, arguments: { content: 用户表示不吃香菜下周三下午三点有个会议, tags: [pref, todo] } }你的代码执行memos_write_memory拿到id把结果返回给模型。模型这时回复用户“已经记住啦。”整个链路就用完了。过了几天用户又来问“我上次说的那个会议是什么时候”模型判断这是读取场景调用memos_read_memory(query会议)把检索结果拿回来再组织成自然语言回复。到这里“写入记忆—读取记忆”就闭环了。我实际跑这个流程时单次函数调用的耗时在 50 毫秒以内本机调用哪怕加上大模型本身的推理时间用户也感知不到记忆模块的存在。这种轻量方案的体验优势在工程上是很明显的。5. 常见问题与排查技巧实录5.1 鉴权失效与 HTTP 401这是我遇到最多的一类问题。现象是调用任何接口都返回 401 Unauthorized。排查顺序如下检查 Token 是否正确复制有没有多余空格或换行。检查请求头格式必须是Authorization: Bearer token注意 Bearer 首字母大写。检查 Token 是否已被删除或重新生成。memos 后台对 Token 有两个敏感操作删除和重置。一旦执行旧 Token 立刻全部失效。检查是不是用错了账号。API 的 Token 是跟着账号走的只有创建该 Token 的账号才能访问数据。我自己的一个教训是曾经把 Token 放在.env文件里但代码里读的是另一个环境变量名结果对标了半天才发现名字不匹配。所以遇到 401先把配置打出来对比一遍再怀疑代码逻辑。5.2 时间字段与时区问题memos 返回的createdTs是 Unix 时间戳而且单位是秒。如果你平时习惯处理毫秒级时间戳很容易犯一个错把秒当毫秒用datetime.fromtimestamp解析结果得到 1970 年附近的时间。处理方式就一条统一用time.localtime(ts)转换。如果你的服务部署在 UTC 时区而你在东八区使用显示出来会差八个小时。我的做法是在 memos 的部署环境里把时区设置成Asia/ShanghaiDocker 部署时加-e TZAsia/Shanghai。这样写入和读取都走本地时区不出现歧义。5.3 Markdown 内容与标签解析memos 的内容解析会把#开头的内容识别成标签。所以如果你记录的正文里本身含有井号比如“密码是 #abc123”memos 会把#abc123也当成一个标签。这个问题不影响存储但会影响标签统计和检索。我的经验是分情况处理如果井号是真实内容就别加多余的空格和标签如果想让某条记录更易检索给内容补上结构化标签比如#pref/coffee、#status/done。用斜杠分隔的标签在 memos 里会显示为嵌套标签在后台筛选时更清晰。5.4 检索不精准的改进方向memos 的 keyword 匹配是简单的区分大小写的子串匹配。中文场景下问题不大但英文场景里大小写不一致会漏查比如存了Coffee查coffee在部分版本里匹配不上。我的函数里把关键词和目标都转成了小写再比较就是为了规避这个问题。如果你想要更聪明的检索可以分两步走。第一步用 keyword 粗筛第二步对结果做简单打分——比如命中次数越多排越前、标题字段命中加分。这样的轻量排序比单纯依赖 API 返回顺序要实用得多。再往上走就是接向量检索了但那是另一个量级的复杂度普通记忆场景没有必要。5.5 高频写入时的重复记录问题当 skill 被大模型频繁调用时重复写入是一个容易出现的问题。比如用户说“我喜欢喝手冲咖啡”这一句话模型可能在一次对话里触发多次写入产生多条重复记忆。多次对话里反复提及更会造成冗余。我的处理方式是在写入前加一个简单去重先按关键词查询一遍如果最近一小时内有内容相似度超过一定阈值的记录就跳过写入或者改成更新原记录。这个逻辑我单独抽了一个函数_exists_recent_similar()在上面的基础代码里没有展开但实际使用中收益很大。6. 扩展思路从记忆到知识库这套 skill 跑通之后稍微改改就能扩展出不少玩法。我给你几个方向做参考。第一个方向是把 memos 当“个人事务日志”。除了记忆偏好让智能助手每天固定写一篇当天总结用#daily/2025-06-15这样的标签归档。月底要回顾时读取关键词“daily”并按标签过滤就能拉出整个月的记录。第二个方向是做多级记忆。简单记忆用 memos重要知识通过定时任务做二次整理把高频内容沉淀成文档。比如每周末写一个脚本读取本周所有带#pref标签的记录汇总去重后生成一份偏好清单。这种“碎片记录—定期整理”的模式比只做实时读写更接近真实的知识管理。第三个方向是结合提醒。memos 本身不带提醒功能但你可以写一个定时脚本把当天创建且带#todo标签的记录读取出来推送到你的 IM 工具或邮件。等于给 memos 加了一个轻量提醒层。实际上我在自己的环境里已经跑了这套方案两个多月。最大的体会是记忆模块真正的价值不在存储而在让大模型在合适的时机把信息写进去、在合适的时机查出来。后者完全依赖 tool description 写得好不好以及记忆数据的组织是否结构化。函数本身只有几十行但围绕它打磨标签规范、清理策略、去重逻辑才是让这套系统长期稳定运行的关键。如果你也想给助手加记忆我的建议是别急着上重方案先把 memos 这层摸透用最简单的方式跑通闭环再按你的实际需求慢慢加功能。毕竟记忆不是一个单点功能而是一套需要持续维护的数据体系开始得越轻越容易坚持优化下去。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑