:用memos+Skill實現(xiàn)持久記憶讀寫)
給AI助手做“記憶”這件事我前后折騰了不少時間。最開始是把整理好的文檔塞進(jìn)知識庫后來發(fā)現(xiàn)對話里那些真正重要的東西——用戶隨口提的偏好、某個項目的背景、下周要交稿的方向——根本不適合當(dāng)成知識文檔來管。它們更像便利貼短、碎、帶時間屬性、需要按標(biāo)簽翻找。于是我給Agent配了一套外接記憶以memos作為存儲后端再寫一個專門的記憶skill讓助手既能往memos里寫入記憶也能在需要時把歷史記憶讀回來。這套方案跑通之后對話體驗完全不一樣了。如果你也在做Agent應(yīng)用正頭疼“每次聊天都白紙開局”的問題這篇就把完整的實現(xiàn)思路、腳本、踩坑記錄都攤開講。1. 為什么是memosAgent的“白紙開局”缺一塊可持久的外接記憶1.1 會話失憶是Agent落地的第一道坎做過實際AI應(yīng)用的人應(yīng)該都有這種體會單次會話里模型表現(xiàn)再聰明只要窗口一關(guān)下次對話它又把你當(dāng)陌生人。我以前試圖把所有歷史都塞進(jìn)上下文里效果很差一是token成本根本扛不住二是早期無關(guān)信息會嚴(yán)重干擾后續(xù)輸出質(zhì)量。更合理的思路是設(shè)置一道“記憶篩選”機(jī)制從對話中抽取出真正有長期價值的信息單獨(dú)存起來下次需要時再精準(zhǔn)取回。這個需求落到工程上就是一個“外接記憶庫”能寫入、能檢索、能按時間排序、能分類打標(biāo)。知識庫工具適合存放成體系的文檔但存一句話、一條偏好、一個時間節(jié)點(diǎn)多少有點(diǎn)大材小用。普通對話記錄導(dǎo)出又太臃腫檢索時噪聲極大。我需要的其實是“速記”能力——這正好是我熟悉的memos這個自托管備忘工具的核心場景。1.2 memos恰好長在“記一筆”的場景上memos的設(shè)計哲學(xué)就是“快速記一條”界面極簡核心數(shù)據(jù)模型只有content、visibility、createdTs這幾個字段。很多人在自己服務(wù)器上部署它主要是用來替代手機(jī)備忘錄或者搭私人的flomo式記錄流。但對我這類技術(shù)用戶來說它真正值錢的地方是那套干凈的REST API一條內(nèi)容、一個時間戳、一組標(biāo)簽正好能映射成Agent記憶的最小單元。拿它跟其他方案對比一下優(yōu)劣很清楚方案優(yōu)點(diǎn)不適合當(dāng)記憶庫的原因云筆記類應(yīng)用生態(tài)成熟客戶端多API普遍封閉筆記結(jié)構(gòu)太重不適合程序化高頻讀寫本地Markdown文件數(shù)據(jù)完全私有格式自由并發(fā)寫入、標(biāo)簽檢索、時間線過濾都需要自己造輪子向量數(shù)據(jù)庫語義檢索強(qiáng)大部署成本高一條記憶需要embedding、索引明顯殺雞用牛刀memos自托管、API干凈、標(biāo)簽時間線兼顧沒有語義檢索但關(guān)鍵詞檢索對記憶場景夠用我實際用下來記憶條目往往是“用戶喜歡把結(jié)論放前面”“項目A的驗收時間是周三”這類短文本關(guān)鍵詞精確匹配完全夠用沒必要上向量。而且memos自托管數(shù)據(jù)在自己手里寫隱私信息也不心虛。另外一個重要的點(diǎn)是memos有完整的visibility控制單條數(shù)據(jù)可以設(shè)為PRIVATE或PUBLIC。Agent記錄的記憶絕大多數(shù)都應(yīng)該私有這個字段天然就把隱私邊界劃好了。1.3 “Skill”在這里扮演什么角色所謂skill本質(zhì)上是一份“寫給AI看的操作文檔一組可執(zhí)行腳本”。腳本普通人也會寫但skill的價值在于Agent在對話中讀到說明文件后能自己判斷“現(xiàn)在該調(diào)用哪個腳本、傳什么參數(shù)、返回結(jié)果怎么解讀”。放到記憶場景里這個設(shè)計很關(guān)鍵。我不想每次對話都在系統(tǒng)prompt里手寫一大段“請把新信息保存到memos”的復(fù)雜指令而是希望Agent看到skill后自動掌握三件事什么時候該寫入、什么時候該讀取、寫入內(nèi)容用什么格式。把這些決策邏輯固化在skill文件里既減少prompt占用的token也讓行為更可預(yù)期。2. Skill的里子讀寫兩條鏈路和記憶條目的結(jié)構(gòu)設(shè)計2.1 先解決“記什么”記憶條目該有的結(jié)構(gòu)動手寫腳本之前我先花了不少時間想一個更前置的問題什么信息才值得被Agent寫進(jìn)記憶庫如果讓Agent把每句話都記下來第二天記憶庫就是垃圾場。我后來定了一套篩選標(biāo)準(zhǔn)凡是通過的標(biāo)準(zhǔn)才寫入用戶主動要求記住的比如“以后寫方案默認(rèn)用短句”用戶穩(wěn)定偏好比如“我負(fù)責(zé)電商類目文案”“我喜歡圖片放左邊”項目級背景事實比如“下周三要給某品牌交一篇公眾號文章強(qiáng)調(diào)性價比”跨會話需要延續(xù)的狀態(tài)比如“上次討論的方案已經(jīng)定了用雙列布局”反過來一次性的對話指令、臨時閑聊、當(dāng)輪就能完成的任務(wù)都不該入庫。記憶內(nèi)容本身也不能是對話原文的復(fù)制需要經(jīng)過整理。我給它定了一個極簡模板[記憶類型] 客觀描述獲取時間舉例[用戶偏好] 寫作時偏愛短段落每段不超過四行2025-06-10 獲取 [項目背景] 某品牌公眾號文章下周三交稿重點(diǎn)是突出性價比2025-06-10 獲取 [用戶身份] 內(nèi)容運(yùn)營主要做電商類目文案2025-06-11 獲取記憶類型我用中文方括號標(biāo)注配合memos本身的#標(biāo)簽再做一層聚合。標(biāo)簽是給機(jī)器看的方括號類型是給模型看的雙層結(jié)構(gòu)檢索時很順手。2.2 兩條鏈路寫入路徑和讀取路徑整體架構(gòu)并不復(fù)雜兩條鏈路各自獨(dú)立寫入鏈路是“Agent判斷信息有價值 → 調(diào)用寫入腳本 → POST到memos API → 返回id和創(chuàng)建時間”。讀取鏈路是“會話開始或用戶觸發(fā)回憶 → 調(diào)用讀取腳本 → 按關(guān)鍵詞或最近時間拉取 → 結(jié)果注入當(dāng)前對話上下文”。兩條鏈路不需要共享狀態(tài)各跑各的增加了系統(tǒng)的穩(wěn)定性。讀取方式我分了兩種模式。一種是開機(jī)預(yù)載新會話開始時讀取最近10條記憶讓Agent先獲得一個大致的背景感另一種是按需搜索用戶提到“之前說過的那個項目”時用關(guān)鍵詞精確過濾。兩種模式對應(yīng)兩個不同的調(diào)用參數(shù)內(nèi)部可以復(fù)用同一個API請求。2.3 涉及的關(guān)鍵API與鑒權(quán)memos從某個版本開始提供了標(biāo)準(zhǔn)的REST API。我用的核心端口是這些認(rèn)證狀態(tài)檢查GET /api/v1/auth/status創(chuàng)建一條POST /api/v1/memos獲取列表GET /api/v1/memos?limit10offset0獲取單條GET /api/v1/memos/{id}更新內(nèi)容PATCH /api/v1/memos/{id}鑒權(quán)方式是在memos網(wǎng)頁端的個人設(shè)置里生成Access Token請求時放進(jìn)HeaderAuthorization: Bearer 你的token這個token建議放在環(huán)境變量里腳本通過os.environ讀取絕對不要硬編碼進(jìn)代碼文件不然哪天把腳本推到git倉庫里就是事故。2.4 寫入格式與讀取格式之間的約定記憶能被順利讀回來靠的是寫入時有統(tǒng)一格式。我在skill的說明文件里明確要求Agent按[類型] 描述獲取時間的模板整理內(nèi)容再把完整內(nèi)容作為一條memo寫入。標(biāo)簽作為內(nèi)容的一部分直接拼在文本末尾比如#記憶/用戶偏好。這樣讀取腳本只需要做關(guān)鍵詞匹配不需要額外解析標(biāo)簽字段兼容性更好。讀取腳本返回給Agent的結(jié)果我會做一層簡化只保留id、content、createdTs三個字段。createdTs是Unix時間戳Agent可以自己換算成人類時間也可以提示腳本直接輸出格式化時間。我的做法是腳本就把時間轉(zhuǎn)好省得模型再算一次。3. 代碼級落地SKILL.md、寫入腳本、讀取腳本一次給全3.1 目錄結(jié)構(gòu)先擺好我的記憶skill放在Agent的skills目錄下結(jié)構(gòu)是這樣skills/memos-memory/ ├── SKILL.md ├── write_memo.py ├── read_memos.py └── requirements.txtrequirements.txt里只要requests一個依賴保持輕量。3.2 SKILL.md的編寫要點(diǎn)這個文件是整個skill的靈魂。Agent不會先讀代碼它首先讀的是這份說明。所以說明文件里必須寫清楚三個維度觸發(fā)條件、調(diào)用方式、內(nèi)容規(guī)范。--- name: memos-memory description: 將對話中值得長期保存的信息寫入memos記憶庫并支持按關(guān)鍵詞或最新時間讀取歷史記憶。當(dāng)用戶透露偏好、補(bǔ)充背景、布置跨會話任務(wù)時可用。 --- # memos 記憶讀寫 該skill用于維護(hù)Agent的長期記憶。所有操作通過腳本完成。 ## 什么時候?qū)懭?- 用戶明確要求記住某事 - 用戶透露了穩(wěn)定的身份、偏好、習(xí)慣 - 用戶給出后續(xù)會話需要使用的項目信息、時間安排 - 注意不記錄一次性指令與臨時閑聊 ## 內(nèi)容格式 請把信息整理為一行 [類型] 客觀描述獲取時間 類型可選值用戶偏好、用戶身份、項目背景、計劃安排、決策記錄 ## 什么時候讀取 - 新會話開始有明確主題時先按關(guān)鍵詞搜索歷史記憶 - 用戶使用之前說過還記得嗎等表達(dá)時絕對先讀取再回答 ## 調(diào)用方式 - 寫入一條python write_memo.py 內(nèi)容 - 讀取最近python read_memos.py --mode recent --limit 10 - 關(guān)鍵詞搜索python read_memos.py --mode search --keyword 關(guān)鍵詞description字段我寫得比較克制沒用“請務(wù)必調(diào)用”這類命令式措辭而是用“當(dāng)……時可用”來暗示觸發(fā)條件。不同Agent框架對description的理解方式略有差異但這種表述通常不容易誤觸發(fā)。3.3 寫入腳本write_memo.py寫入腳本只做一件事接收內(nèi)容POST到memos API。它不負(fù)責(zé)判斷該不該寫那是Agent結(jié)合SKILL.md判斷的事。職責(zé)分離能讓腳本保持簡單也方便單獨(dú)調(diào)試。#!/usr/bin/env python3 寫入一條記憶到memos。 用法 python write_memo.py 記憶內(nèi)容 python write_memo.py 記憶內(nèi)容 --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: 內(nèi)容為空拒絕寫入} 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內(nèi)容) 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()腳本對這個場景已經(jīng)夠用了。注意requests.post里必須帶超時參數(shù)否則memos服務(wù)假死時腳本會卡住連帶Agent的執(zhí)行也跟著卡住。超時設(shè)成10秒夠用。3.4 讀取腳本read_memos.py讀取腳本我做了兩種模式recent直接拉最近N條search先拉一批在內(nèi)存里按關(guān)鍵詞過濾。本來想用memos API自帶的filter參數(shù)但不同版本支持程度不同內(nèi)存過濾最穩(wěn)數(shù)據(jù)量也不大。#!/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()讀取結(jié)果打印成一個列表每個元素是{id, content, createdTs}。Agent拿到這個結(jié)構(gòu)后自己會選擇哪些內(nèi)容與當(dāng)前話題相關(guān)。id這個字段很有用如果后續(xù)要做“更新舊記憶”的操作可以直接用id定位。3.5 環(huán)境變量與配置兩個腳本都從環(huán)境變量讀取MEMOS_BASE_URL和MEMOS_TOKEN。我通常在啟動Agent服務(wù)前把它們寫進(jìn)啟動腳本或者systemd環(huán)境文件。本機(jī)調(diào)試時也可以放在.env文件里但記得把.env加進(jìn).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能返回當(dāng)前用戶名就說明鑒權(quán)通了再往下排錯就簡單了。4. 實測一輪完整閉環(huán)從“請記住”到“你還記得嗎”4.1 第一輪對話信息沉淀腳本就緒后我在實際Agent環(huán)境里跑了一輪測試把整個閉環(huán)走通。第一輪對話用戶跟Agent說“我是做內(nèi)容運(yùn)營的主要寫電商類目文案喜歡用短句。對了下周要給某品牌寫公眾號文章重點(diǎn)是強(qiáng)調(diào)性價比?!盇gent根據(jù)SKILL.md判斷出這里含三條穩(wěn)定記憶用戶身份、寫作偏好、一個項目安排。它連續(xù)調(diào)了三次寫入腳本python write_memo.py [用戶身份] 內(nèi)容運(yùn)營主要是電商類目文案2025-06-11 獲取 python write_memo.py [用戶偏好] 寫作時偏愛短段落2025-06-11 獲取 python write_memo.py [計劃安排] 某品牌公眾號文章下周三交稿重點(diǎn)是突出性價比2025-06-11 獲取每次腳本返回的json里都有新生成的memo id和createdTs。Agent在回復(fù)里順帶告訴我“已記錄”但不會把記憶內(nèi)容復(fù)述一遍避免對話過載?;仡^我打開memos網(wǎng)頁端能看到這三條按時間排得整整齊齊還帶著#記憶的標(biāo)簽簇。這里有個小細(xì)節(jié)Agent寫的內(nèi)容是整理后的短句而不是對話原文。對話原文是口語化的包含大量無關(guān)詞直接存進(jìn)去只會增加后續(xù)檢索的干擾。4.2 第二輪對話記憶被主動取回隔了幾個小時我開了一個全新會話完全清空上下文直接問“幫我想想那篇公眾號文章的開頭重點(diǎn)是我們之前討論過的產(chǎn)品賣點(diǎn)。”Agent的skill說明里有一條規(guī)則當(dāng)用戶提到“之前討論過”這類表達(dá)時必須先讀取記憶再回答。它實際執(zhí)行的是python read_memos.py --mode search --keyword 公眾號腳本返回了匹配到的memmo列表其中就包括第一條會話記錄里保存的“某品牌公眾號文章下周三交稿重點(diǎn)是突出性價比”。Agent把這條記憶和“用戶偏愛短段落”這個偏好一起用于回答寫出的開頭明顯是變短的句式結(jié)構(gòu)還主動提了一句“按你偏好的短段落風(fēng)格來寫”。這個結(jié)果就是我想要的效果Agent不再白紙開局它知道對話者是誰、之前聊過什么、下一步往哪走。從用戶視角來看AI好像“記得”我信任感和交互效率都會明顯提升。4.3 預(yù)載模式的實際表現(xiàn)我還額外測試了recent模式的預(yù)載效果。在會話一開始就執(zhí)行python read_memos.py --mode recent --limit 10把最近十條記憶全部注入到系統(tǒng)上下文中。這個做法適合讓Agent對用戶近期動態(tài)有一個寬泛了解但它有副作用如果最近記憶里有大量過期項目信息反而會干擾當(dāng)前話題的判斷。所以我把預(yù)載作為可選項默認(rèn)關(guān)閉只有面對“用戶經(jīng)常進(jìn)行同一領(lǐng)域連續(xù)創(chuàng)作”這類場景時才會打開。5. 踩坑與取舍API版本、鑒權(quán)、沖突和記憶膨脹5.1 API版本差異曾經(jīng)把腳本搞掛過這是我踩得最重的一個坑。memos不同版本的API路徑和響應(yīng)結(jié)構(gòu)差異不小網(wǎng)上搜到的不少資料還停留在老版本階段。老版本用的是/api/memo這種不帶版本前綴的路徑返回結(jié)果直接就是數(shù)組新版本改成/api/v1/memos返回體包裹在memos字段里。如果你的memos部署版本比較舊腳本就會404或者解析出錯。我的兼容方案是寫了一個極小工具函數(shù)統(tǒng)一處理響應(yīng)def extract_memos(data): if isinstance(data, dict): return data.get(memos, []) return data但如果路徑都換了解析函數(shù)也救不了。更穩(wěn)妥的做法是寫腳本前先確認(rèn)memos版本或者干脆先在瀏覽器里開開發(fā)者工具手動請求一次/api/v1/auth/status看返回碼。這次驗證花不了兩分鐘能省一晚上的排錯時間。5.2 鑒權(quán)細(xì)節(jié)Bearer前綴不能漏有次腳本報401我檢查半天最后發(fā)現(xiàn)請求頭里寫了Authorization: Token xxx而不是Authorization: Bearer xxx。網(wǎng)上示例魚龍混雜有的確實用別的寫法。memos目前的API是認(rèn)Bearer前綴的這個必須跟它對齊。另外要注意token本身是有“有效期”和“撤銷機(jī)制”的。我遇到過一個情況memos服務(wù)重裝后舊的token全部失效Agent的讀寫腳本一夜之間全部401。如果日志里沒有把報錯信息打全排查起來非常痛苦。所以我后來在讀寫腳本里都強(qiáng)制把狀態(tài)碼和響應(yīng)體一起輸出而不是只打印一個“請求失敗”。5.3 內(nèi)容清洗Markdown換行和檢索噪聲memos存的是Markdown文本換行符在API響應(yīng)里會以\n的形式出現(xiàn)讀取時不做處理印出來就是一團(tuán)亂。更重要的是Agent在寫入時如果保留了大量口語化內(nèi)容比如“emmm”“然后然后”這類語氣詞后續(xù)關(guān)鍵詞搜索的命中率會很低因為用戶第二次會話時用的詞跟第一輪口語用詞經(jīng)常對不上。我在SKILL.md里強(qiáng)制要求寫入內(nèi)容必須是客觀描述句不允許出現(xiàn)語氣詞。另外讀取時對content做了一次空白字符壓縮把連續(xù)的換行和空格合并既給Agent省token也讓語義更干凈。5.4 記憶沖突新信息到底該不該覆蓋舊信息用戶是很善變的。周一他說“我不太喜歡用列表回答問題”周三他又說“以后回答盡量用列表”。如果Agent只會無腦追加記憶庫里就會同時存在兩條互相矛盾的信息下次讀取時模型就會左右為難。我的處理方式是Agent在寫入新記憶前先跑一次關(guān)鍵詞搜索查看同主題下有沒有舊記憶。如果有且明顯沖突就在新條目末尾追加一行此信息更新于日期與之前的記錄不同以本條為準(zhǔn)。這樣模型讀到兩條時至少能根據(jù)“以本條為準(zhǔn)”的標(biāo)記做出正確選擇。覆蓋刪除舊條目這個操作我盡量不做因為保留變更歷史有時是有價值的。5.5 記憶膨脹記憶庫也需要定期“斷舍離”跑了一個多月后我的memos記憶庫里有上千條記錄其中相當(dāng)一部分是早已過期的臨時安排。記憶太多會讓搜索命中率下降也會讓讀回的內(nèi)容缺乏重點(diǎn)。我建立的策略是每個月做一次歸檔把超過90天、且不屬于用戶身份和穩(wěn)定偏好的條目導(dǎo)出備份后刪除只保留真正長期有效的記憶。如果追求自動化可以讓讀取腳本支持一個--before參數(shù)按時間篩選出過期條目再通過刪除腳本處理。但這必須謹(jǐn)慎我建議手動確認(rèn)一遍畢竟記憶誤刪很難恢復(fù)。5.6 隱私邊界的取舍記憶內(nèi)容默認(rèn)PRIVATE這點(diǎn)務(wù)必在寫入腳本里封死。即使未來要做多用戶協(xié)同我也不建議把Agent的記憶默認(rèn)設(shè)為公開。另外一個容易被忽略的點(diǎn)是memos是自托管服務(wù)如果服務(wù)器本身沒做備份記憶庫的安全性就全押在那臺機(jī)器上。我給memos數(shù)據(jù)目錄做了定時快照對個人應(yīng)用來說這個成本很低但換回的安心感很高。這套記憶skill我從搭好到現(xiàn)在跑了將近兩個月最大的感受是真正讓Agent顯得“懂你”的往往不是模型本身多聰明而是它能在合適的時機(jī)說出“我記得你之前提過這件事”。memos這種輕量速記工具當(dāng)記憶后端沒有重型基礎(chǔ)設(shè)施帶來的心理負(fù)擔(dān)也沒有復(fù)雜依賴鏈一個腳本加一個API就撐起了全部能力。最后分享一個小技巧我會在每周日讓Agent對記憶庫做一次“回顧整理”找到明顯過期的項目條目并標(biāo)記而不是刪除。這樣記憶庫保持活躍Agent每次讀取時也更清楚哪些是當(dāng)下真正有用的信息。