設(shè)計(jì):分層模型與工程落地)
開頭做了一段時(shí)間大模型應(yīng)用開發(fā)我發(fā)現(xiàn)一個(gè)特別有意思的現(xiàn)象同樣是調(diào)用同一個(gè)模型有的產(chǎn)品用下來(lái)像冷冰冰的問答機(jī)有的卻像相處很久的老朋友。差別往往不在模型本身而在產(chǎn)品有沒有給這個(gè)模型配上“記憶”。最近我們團(tuán)隊(duì)在做一款名叫“小度想想”的智能體產(chǎn)品核心目標(biāo)就是讓智能體記住每個(gè)用戶的偏好、習(xí)慣和對(duì)話上下文真正實(shí)現(xiàn)千人千面的個(gè)性化體驗(yàn)。這篇文章把我們?cè)谠O(shè)計(jì)記憶系統(tǒng)時(shí)踩過的坑、理清的思路、落地的實(shí)現(xiàn)方式整理出來(lái)希望能給正在做智能體、AI助手或個(gè)性化對(duì)話系統(tǒng)的朋友一些參考。記憶的分層、寫入策略、召回調(diào)優(yōu)、隱私邊界這些我們都會(huì)展開聊內(nèi)容偏工程落地不堆概念看完可以直接照著搭一版。1. 為什么智能體需要記憶產(chǎn)品痛點(diǎn)與設(shè)計(jì)原點(diǎn)1.1 “失憶”的智能體有多讓人抓狂先講個(gè)真實(shí)場(chǎng)景。早期我們上線了一版沒有記憶能力的對(duì)話Demo用戶每天進(jìn)來(lái)都是陌生感拉滿。有用戶連續(xù)一周跟智能體說(shuō)“我膽固醇偏高早餐不要推薦油膩的”智能體每次都像第一次聽到一樣下一條推薦還是煎餅果子配油條。用戶后來(lái)在反饋里寫了一句很扎心的話“它還不如一個(gè)記事本。”這句話直接推動(dòng)我們把記憶系統(tǒng)提上了排期。失憶帶來(lái)的問題不只是體驗(yàn)差還會(huì)讓用戶對(duì)產(chǎn)品的信任感快速流失。試想用戶花了十分鐘告訴智能體自己的作息、飲食禁忌、健身目標(biāo)轉(zhuǎn)頭再問一句“我今天適合練什么”智能體完全接不上那一刻用戶就會(huì)覺得之前的信息全白給了。更現(xiàn)實(shí)的是很多智能體產(chǎn)品的核心價(jià)值并不是單輪問答的正確率而是長(zhǎng)期陪伴中形成的連貫服務(wù)。沒有記憶就等于每次對(duì)話都是初次見面產(chǎn)品永遠(yuǎn)停留在工具層面做不出情感黏性和個(gè)性化價(jià)值。1.2 從“會(huì)聊天”到“懂你”記憶的分層模型想清楚記憶要解決什么問題后我們一開始也犯過“什么都想存”的毛病。對(duì)話記錄、用戶點(diǎn)擊、停留時(shí)長(zhǎng)、頁(yè)面瀏覽全部塞進(jìn)一張表里結(jié)果又亂又沒法用。后來(lái)參考認(rèn)知科學(xué)里關(guān)于人類記憶的分層方式把智能體記憶拆成四類短時(shí)工作記憶、長(zhǎng)期情景記憶、語(yǔ)義知識(shí)記憶和用戶畫像記憶。短時(shí)工作記憶對(duì)應(yīng)當(dāng)前會(huì)話里的上下文比如用戶前一句提到“周末去杭州”后一句問“幫我查高鐵”智能體得知道“杭州”就是它要查的目的地。長(zhǎng)期情景記憶則是跨會(huì)話的關(guān)鍵事件比如“上個(gè)月用戶說(shuō)過在準(zhǔn)備省考”“去年年底換了新工作”。語(yǔ)義知識(shí)記憶更偏通用常識(shí)比如城市地圖、菜系分類、履歷常識(shí)這部分可以直接從外部知識(shí)庫(kù)掛載不一定要從對(duì)話里學(xué)。用戶畫像記憶是最高抽象層把用戶明確表達(dá)或行為推斷出的偏好沉淀成標(biāo)簽比如“偏愛清淡”“工作時(shí)間上午九點(diǎn)到下午六點(diǎn)”“家里有一只五歲的柯基”。這四類記憶不是并列關(guān)系而是層層匯總的關(guān)系。會(huì)話上下文經(jīng)過提煉變成情景記憶情景記憶再經(jīng)過歸納和校驗(yàn)沉淀為用戶畫像。分清楚層級(jí)之后后面設(shè)置存儲(chǔ)策略和更新邏輯都會(huì)省力很多。1.3 千人千面不是玄學(xué)記憶如何支撐個(gè)性化“千人千面”這個(gè)詞在產(chǎn)品文檔里出現(xiàn)頻率很高但落到智能體身上本質(zhì)就一句話讓系統(tǒng)在面對(duì)不同用戶時(shí)能夠調(diào)取不同的背景信息生成不同的回答策略和表達(dá)風(fēng)格。舉個(gè)例子同一個(gè)“晚上吃什么”的問題面對(duì)一位高血糖用戶和一位正在減肥的健身用戶智能體給出的答案完全不同。前者側(cè)重推薦的含糖量提示后者側(cè)重蛋白質(zhì)和熱量控制。如果不用記憶做區(qū)分模型只能給出一份模板化的“均衡飲食建議”看起來(lái)沒錯(cuò)但誰(shuí)都覺得沒說(shuō)透。記憶支撐個(gè)性化還有一層遞進(jìn)它讓模型越用越懂用戶的表達(dá)習(xí)慣。有人喜歡簡(jiǎn)潔答案有人希望先解釋原理再給結(jié)論有人習(xí)慣用短句提問。這些風(fēng)格特征完全可以被記憶系統(tǒng)捕獲然后在生成提示詞里作為顯性指令傳給模型。我們實(shí)測(cè)下來(lái)加了風(fēng)格記憶之后用戶的對(duì)話滿意度評(píng)分提升非常明顯比單純調(diào)模型參數(shù)還管用。這也是為什么我一直認(rèn)為大模型應(yīng)用拼到最后拼的不是模型本身而是模型外面的這套數(shù)據(jù)與記憶架構(gòu)。2. 記憶系統(tǒng)整體架構(gòu)與技術(shù)選型2.1 四層架構(gòu)采集、存儲(chǔ)、召回、應(yīng)用記憶系統(tǒng)在我們項(xiàng)目里并不是一個(gè)單獨(dú)的服務(wù)而是嵌在智能體主流程中的中間層。整體分成四層采集層、存儲(chǔ)層、召回層和應(yīng)用層。采集層負(fù)責(zé)從交互過程中提取原始線索包括用戶輸入、用戶反饋、主動(dòng)點(diǎn)擊行為、系統(tǒng)觸發(fā)的埋點(diǎn)等。這里有一個(gè)容易被忽略的點(diǎn)不是所有對(duì)話內(nèi)容都值得進(jìn)存儲(chǔ)層采集層需要先做一輪預(yù)過濾把噪音話題、閑聊寒暄、重復(fù)試探性內(nèi)容篩掉。存儲(chǔ)層負(fù)責(zé)把采集到的線索變成結(jié)構(gòu)化記錄寫入對(duì)應(yīng)的存儲(chǔ)介質(zhì)。召回層是記憶系統(tǒng)的核心它的任務(wù)是當(dāng)用戶發(fā)起新一輪對(duì)話時(shí)從海量記憶里快速找到與當(dāng)前問題相關(guān)的片段。應(yīng)用層再把召回結(jié)果轉(zhuǎn)成提示詞中的上下文塊交給大模型生成最終回答。四層之間的接口要盡量保持簡(jiǎn)單我們一開始設(shè)計(jì)時(shí)還加了消息隊(duì)列做異步解耦后面發(fā)現(xiàn)對(duì)話場(chǎng)景下的記憶寫入延遲其實(shí)沒那么敏感反而是同步寫入更容易排查問題。最后簡(jiǎn)化成了“采集層寫一份、存儲(chǔ)層存一份、召回層查一份、應(yīng)用層拼一份”的直線流水代碼清晰很多。這個(gè)經(jīng)歷也讓我意識(shí)到架構(gòu)是為場(chǎng)景服務(wù)的不要為了所謂的解耦而過度設(shè)計(jì)。2.2 技術(shù)選型向量庫(kù)、緩存庫(kù)和關(guān)系庫(kù)怎么搭配存儲(chǔ)層的選型是記憶系統(tǒng)設(shè)計(jì)里最花時(shí)間的環(huán)節(jié)。我們考慮過全用向量數(shù)據(jù)庫(kù)也考慮過全用關(guān)系型數(shù)據(jù)庫(kù)最后定下來(lái)的是“關(guān)系庫(kù) 向量庫(kù) 緩存庫(kù)”的組合方案。關(guān)系型數(shù)據(jù)庫(kù)用來(lái)存用戶畫像這類強(qiáng)結(jié)構(gòu)數(shù)據(jù)。它字段明確、支持復(fù)雜條件查詢、事務(wù)能力強(qiáng)標(biāo)簽體系變動(dòng)的時(shí)候也好做遷移。向量數(shù)據(jù)庫(kù)用來(lái)存文本類的記憶片段比如用戶說(shuō)過的一段話、事件摘要、偏好描述這類信息沒有固定結(jié)構(gòu)只能靠語(yǔ)義相似度來(lái)檢索。緩存庫(kù)用來(lái)存當(dāng)前會(huì)話內(nèi)的短時(shí)上下文需要低延遲讀寫所以對(duì)速度要求最高一般用鍵值型存儲(chǔ)就能滿足。三者的邊界其實(shí)和前面說(shuō)的記憶分層是對(duì)應(yīng)的畫像層偏結(jié)構(gòu)化情景層偏向量化會(huì)話層偏緩存化。選型時(shí)還有個(gè)容易被忽視的細(xì)節(jié)向量數(shù)據(jù)庫(kù)的索引類型和距離度量方式。如果記憶片段數(shù)量不大幾萬(wàn)條以內(nèi)用暴力檢索都能滿足需求一旦上了百萬(wàn)級(jí)就得考慮HNSW這類近似最近鄰索引同時(shí)要根據(jù)文本長(zhǎng)度選擇合適的歸一化方式。我們?cè)缙谠谠u(píng)估階段用默認(rèn)配置跑了一版結(jié)果中文長(zhǎng)文本的召回結(jié)果非常不穩(wěn)定后來(lái)發(fā)現(xiàn)距離度量選成了不適合文本向量的方式換成適用的度量之后效果立刻改善。這個(gè)坑后面細(xì)說(shuō)。2.3 記憶類型與生命周期管理不同記憶類型的生命周期差異很大不能一視同仁。會(huì)話內(nèi)的短時(shí)記憶只在當(dāng)前對(duì)話中有效對(duì)話結(jié)束就可以清理最多保留到用戶關(guān)閉頁(yè)面后十分鐘。用戶畫像層的記憶則需要長(zhǎng)期保存通常情況下只增不刪除非用戶主動(dòng)修改或要求清除。情景記憶和語(yǔ)義記憶處于中間狀態(tài)需要做時(shí)間衰減和定期歸檔。生命周期管理的核心是防止兩件事一是記憶無(wú)限膨脹導(dǎo)致召回時(shí)噪聲變大二是陳舊記憶誤導(dǎo)模型比如用戶兩年前喜歡某個(gè)品牌現(xiàn)在可能早就換口味了。我們的處理方式是給每條記憶打上時(shí)間戳和置信度字段召回時(shí)按時(shí)間衰減系數(shù)做加權(quán)同時(shí)后臺(tái)跑定期任務(wù)把超過半年的低置信度記憶降級(jí)超過一年且從未被命中的記憶直接歸檔。表格里可以看到我們對(duì)四類記憶的生命周期約定記憶類型存儲(chǔ)介質(zhì)默認(rèn)保存時(shí)長(zhǎng)更新策略召回優(yōu)先級(jí)短時(shí)工作記憶緩存庫(kù)會(huì)話結(jié)束 10分鐘每輪對(duì)話全量覆蓋最高長(zhǎng)期情景記憶向量庫(kù)180天事件抽取后追加中語(yǔ)義知識(shí)記憶向量庫(kù)/知識(shí)掛載持久定期同步外部知識(shí)低用戶畫像記憶關(guān)系庫(kù)持久顯式確認(rèn)后更新高這套生命周期管理規(guī)則上線之后最直觀的變化是召回精度的穩(wěn)定性變好了。之前出現(xiàn)過用戶上一秒說(shuō)“想去重慶”下一秒智能體突然引用他半年前說(shuō)過的“不喜歡火鍋”嚴(yán)重割裂。加了時(shí)間加權(quán)后近期的記憶天然占更大權(quán)重這種跨時(shí)空打架的情況明顯減少。3. 核心細(xì)節(jié)解析從用戶畫像到記憶槽位設(shè)計(jì)3.1 用戶畫像字段設(shè)計(jì)與標(biāo)簽體系用戶畫像不是簡(jiǎn)單堆字段而是要具備可擴(kuò)展性。我們把畫像表設(shè)計(jì)成三塊基礎(chǔ)屬性、動(dòng)態(tài)偏好、行為統(tǒng)計(jì)?;A(chǔ)屬性包括稱呼、所在城市、年齡范圍等這類信息通常由用戶主動(dòng)提供或?qū)υ捴酗@式說(shuō)出。動(dòng)態(tài)偏好包括飲食口味、溝通風(fēng)格、興趣話題、常用時(shí)間段等這類信息會(huì)隨著對(duì)話持續(xù)更新。行為統(tǒng)計(jì)則記錄統(tǒng)計(jì)類的指標(biāo)比如最近七天對(duì)話次數(shù)、平均對(duì)話輪數(shù)、主動(dòng)點(diǎn)擊次數(shù)。畫像字段里我們特別設(shè)計(jì)了一個(gè)“標(biāo)簽來(lái)源”字段記錄每個(gè)標(biāo)簽是被用戶明確說(shuō)出來(lái)的還是被系統(tǒng)推斷出來(lái)的。比如用戶直接說(shuō)“我不吃香菜”這就是顯式標(biāo)簽置信度直接拉滿如果是通過用戶多次提到“香菜好難吃”推斷出來(lái)的就是隱式標(biāo)簽置信度低一些。這個(gè)設(shè)計(jì)非常關(guān)鍵因?yàn)樵诤罄m(xù)的記憶更新沖突中我們要靠置信度來(lái)決定聽誰(shuí)的。JSON示例可以這樣表示{ user_id: u_10001, base_info: { nickname: A同學(xué), city: 杭州, age_range: 25-30 }, preferences: [ { tag: 飲食偏好:清淡, source: explicit, confidence: 0.95, updated_at: 2025-06-01 20:13:00 }, { tag: 溝通風(fēng)格:喜歡短答案, source: inferred, confidence: 0.72, updated_at: 2025-05-30 09:12:00 } ], behavior_stats: { recent_days_active: 12, avg_dialogue_rounds: 6.5, preferred_hour: 22:00-23:00 } }3.2 記憶寫入策略什么時(shí)候記、怎么記、記多少記憶寫入是整個(gè)系統(tǒng)里最需要克制的地方。我們的經(jīng)驗(yàn)是寧缺毋濫。寫入過多低質(zhì)量的記憶會(huì)讓系統(tǒng)變得“自作聰明”甚至產(chǎn)生錯(cuò)誤的個(gè)性化。寫入時(shí)機(jī)主要看三類信號(hào)。第一類是用戶顯式表達(dá)比如“我喜歡”“我不吃”“我住在”這類明確句這是最高優(yōu)級(jí)的寫入信號(hào)。第二類是重復(fù)行為同一個(gè)偏好在多輪對(duì)話或多次會(huì)話中反復(fù)出現(xiàn)系統(tǒng)可以自動(dòng)打上隱式標(biāo)簽。第三類是外部行為反饋比如用戶對(duì)某類推薦頻繁點(diǎn)擊或者對(duì)某類回答頻繁點(diǎn)踩這些行為也能反映偏好。記多少的問題同樣值得琢磨。我們嘗試過把用戶原話完整存進(jìn)向量庫(kù)后來(lái)發(fā)現(xiàn)效率極低且原話里的情緒化表達(dá)會(huì)干擾語(yǔ)義檢索。最終方案是讓大模型先做一次結(jié)構(gòu)化抽取每條記憶只保留“主體 屬性 偏好對(duì)象 時(shí)間上下文”的格式比如“用戶不喜歡吃香菜”而不是“我記得用戶上次吃飯的時(shí)候說(shuō)香菜好難吃”。這樣做至少帶來(lái)兩個(gè)好處一是記憶片段更干凈向量檢索的語(yǔ)義距離更準(zhǔn)確二是后續(xù)做用戶可讀的記憶管理界面時(shí)可以直接把這些結(jié)構(gòu)化描述展示給用戶體驗(yàn)更友好。提示記憶寫入要做冪等控制同一個(gè)標(biāo)簽如果已經(jīng)被寫入再次寫入時(shí)不要生成新記錄而是更新原記錄的置信度和時(shí)間戳否則畫像表很快就會(huì)堆滿重復(fù)數(shù)據(jù)。3.3 記憶召回相似度計(jì)算與閾值調(diào)優(yōu)召回模塊直接決定了記憶能不能被“用起來(lái)”。我們采用的是多路召回策略每一路單獨(dú)輸出一批候選再做融合排序。第一路是根據(jù)用戶當(dāng)前問題文本向量與向量庫(kù)中的記憶片段算相似度找語(yǔ)義上最接近的記憶。第二路是直接拉取當(dāng)前用戶的畫像標(biāo)簽把置信度高的畫像記錄全量取出。第三路是查最近幾輪會(huì)話的短時(shí)上下文。第四路是可選的根據(jù)當(dāng)前場(chǎng)景和時(shí)間段做規(guī)則召回比如用戶在晚間提問就把“晚間提問偏好”這一維度的記憶優(yōu)先提出來(lái)。多路召回的難點(diǎn)在融合排序。不同路的打分口徑不一樣不能直接相加。我們的做法是給每一路設(shè)一個(gè)基礎(chǔ)權(quán)重再用時(shí)間衰減因子加權(quán)。時(shí)間衰減采用指數(shù)衰減公式最終權(quán)重 基礎(chǔ)權(quán)重 × exp(-λ × 天數(shù)間隔)。λ取值我們調(diào)到0.02表示一條記憶經(jīng)過35天權(quán)重會(huì)衰減到原來(lái)的一半。這個(gè)參數(shù)可以根據(jù)產(chǎn)品節(jié)奏調(diào)整如果用戶活躍周期是周級(jí)λ可以適當(dāng)調(diào)大。閾值也不是拍腦袋定的。我們先拿了一批人工標(biāo)注的測(cè)試數(shù)據(jù)跑索引方式下的相似度分布發(fā)現(xiàn)相關(guān)記憶的相似度集中在0.78以上不相關(guān)記憶大多在0.65以下。所以初始閾值設(shè)為0.76然后再根據(jù)線上召回日志微調(diào)。調(diào)閾值的核心指標(biāo)是“召回命中率”也就是召回來(lái)的記憶里有多少條真正對(duì)回答有幫助。如果命中率低于40%說(shuō)明閾值偏低或排序有問題需要收緊。3.4 個(gè)性化生成提示詞組裝與沖突消解召回的最終目的是給大模型提供一份干凈的“記憶簡(jiǎn)報(bào)”。我們?cè)谔崾驹~組裝上迭代過很多版本總結(jié)出一個(gè)比較穩(wěn)的思路system提示詞中先固定角色人設(shè)然后放“用戶畫像”接著放“近期重要記憶”最后放“當(dāng)前會(huì)話上下文”。畫像是高置信度標(biāo)簽語(yǔ)氣肯定近期記憶用“據(jù)用戶最近提到”這類弱表述避免模型把過期信息當(dāng)硬事實(shí)。示例模板大致如下【角色設(shè)定】 你是“小度想想”一位溫暖、細(xì)心的個(gè)人智能助手。 【用戶畫像】 用戶偏好清淡飲食喜歡200字以內(nèi)的短答案常在工作日晚十點(diǎn)后提問。 【近期記憶】 用戶在本周二提到周末想爬山用戶在上次對(duì)話中反饋不喜歡氛圍嘈雜的餐廳。 【當(dāng)前上下文】 用戶現(xiàn)在問這周六有什么合適的戶外活動(dòng)推薦注意這里的措辭控制。如果記憶置信度偏低我們?cè)谏商崾驹~時(shí)會(huì)把“用戶偏好清淡飲食”改成“用戶近期似乎比較關(guān)注清淡飲食”給模型留出懷疑和追問的空間。這個(gè)細(xì)節(jié)很重要它避免了模型過度自信地依據(jù)錯(cuò)誤記憶生成答案。沖突消解是另一個(gè)大坑。最典型的情況是用戶今天說(shuō)“我要開始吃辣了”但歷史畫像里還寫著“不吃辣”。我們的策略是當(dāng)用戶在當(dāng)前對(duì)話中給出了與畫像相反的信息時(shí)以當(dāng)前對(duì)話為準(zhǔn)同時(shí)把畫像標(biāo)簽的置信度降低并且更新為“近期偏好可能改變”。但更新不是立刻覆蓋而是進(jìn)入一個(gè)“待確認(rèn)”狀態(tài)只有當(dāng)下一次對(duì)話再次驗(yàn)證后才算徹底覆蓋。這樣設(shè)計(jì)是為了防止用戶只是隨口說(shuō)了句違背習(xí)慣的話系統(tǒng)就永久修改畫像反而矯枉過正。4. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)4.1 環(huán)境準(zhǔn)備與工程目錄設(shè)計(jì)這套記憶系統(tǒng)我們用的是Python實(shí)現(xiàn)依賴并不多。下面是基礎(chǔ)的環(huán)境清單主要用到關(guān)系型數(shù)據(jù)庫(kù)做畫像存儲(chǔ)、向量索引庫(kù)做記憶片段檢索以及一個(gè)嵌入式向量構(gòu)建模塊。如果你想快速?gòu)?fù)現(xiàn)建議先把基礎(chǔ)環(huán)境跑通pip install numpy sqlalchemy redis工程目錄我們按功能模塊拆分核心就三個(gè)文件畫像存儲(chǔ)模塊、向量召回模塊、記憶組裝模塊。后期維護(hù)起來(lái)非常方便每個(gè)新同學(xué)接手后基本一天能看懂結(jié)構(gòu)。目錄設(shè)計(jì)如下memory_system/ ├── profile_store.py # 用戶畫像讀寫與沖突更新 ├── vector_memory.py # 記憶向量化、寫入、召回 ├── memory_composer.py # 提示詞組裝與記憶簡(jiǎn)報(bào)生成 ├── tests/ # 單元測(cè)試與回歸用例 └── config.py # 閾值、衰減系數(shù)等全局配置4.2 畫像存儲(chǔ)與記憶管理核心代碼實(shí)現(xiàn)先說(shuō)畫像存儲(chǔ)模塊。這一層最核心的是寫入時(shí)要判斷該更新還是該新建我們用一個(gè)簡(jiǎn)單的按標(biāo)簽名查重再更新的邏輯實(shí)現(xiàn)。核心代碼如下class ProfileStore: def __init__(self, db_connection): self.conn db_connection def upsert_tag(self, user_id, tag, source, confidence): sql SELECT id, confidence FROM user_tags WHERE user_id? AND tag? row self.conn.execute(sql, (user_id, tag)).fetchone() if row: new_conf max(confidence, row[confidence] * 0.9) update_sql (UPDATE user_tags SET confidence?, updated_atCURRENT_TIMESTAMP WHERE id?) self.conn.execute(update_sql, (new_conf, row[id])) else: insert_sql (INSERT INTO user_tags(user_id, tag, source, confidence, created_at) VALUES(?, ?, ?, ?, CURRENT_TIMESTAMP)) self.conn.execute(insert_sql, (user_id, tag, source, confidence)) self.conn.commit()這個(gè)實(shí)現(xiàn)里有一個(gè)小細(xì)節(jié)對(duì)于已存在的標(biāo)簽新置信度不是直接替換而是取原先置信度的九折和當(dāng)前置信度之間的較大值。這么寫是為了防止某次誤判導(dǎo)致標(biāo)簽被瞬間拉得過高給系統(tǒng)留一點(diǎn)“猶豫空間”。記憶向量模塊就更直接了。寫入時(shí)把文本丟給Embedding模型轉(zhuǎn)成向量隨向量一起存上原文、類型和時(shí)間戳召回時(shí)用當(dāng)前query向量去索引里做相似度搜索。核心代碼片段如下class VectorMemory: def __init__(self, index, embed_func, top_k5): self.index index self.embed embed_func self.top_k top_k def add_memory(self, memory_id, text, memory_type): vec self.embed(text) metadata {id: memory_id, text: text, type: memory_type} self.index.upsert(memory_id, vec, metadata) def recall(self, query_text, user_idNone, top_kNone): k top_k or self.top_k query_vec self.embed(query_text) hits self.index.query(query_vec, kk) return [{text: h.metadata[text], score: h.score, type: h.metadata[type]} for h in hits]這里我刻意沒有綁定具體向量庫(kù)的API因?yàn)椴煌桨附涌诓町惒淮蟆?shí)際工程里你需要把index.upsert和index.query替換成所用庫(kù)對(duì)應(yīng)的方法即可。4.3 完整示例模擬一次“千人千面”的對(duì)話做一個(gè)完整的走查。假設(shè)用戶A同學(xué)第一次使用“小度想想”在周二晚上說(shuō)了句“周末想去人少一點(diǎn)的山里走走”。采集層識(shí)別到“人少”“山里”“周末”三個(gè)關(guān)鍵偏好寫入情景記憶同時(shí)把“偏好安靜的自然環(huán)境”作為隱式畫像標(biāo)簽置信度設(shè)為0.6。第二天用戶再問“這周六有什么推薦的地方嗎”系統(tǒng)進(jìn)入召回流程第一步把用戶問題向量化在向量庫(kù)里做語(yǔ)義檢索召回“周末想去人少一點(diǎn)的山里走走”這條記憶相似度約0.83超過了我們?cè)O(shè)定的0.76閾值。 第二步從畫像表拉取用戶標(biāo)簽看到“偏好安靜的自然環(huán)境”和“喜歡短答案”。 第三步組裝記憶簡(jiǎn)報(bào)得到類似下面的文本用戶畫像喜歡安靜的自然環(huán)境答案偏好簡(jiǎn)潔。 近期記憶用戶提到周末想去人少一點(diǎn)的山里走走。 生成要求請(qǐng)圍繞江浙滬周邊、人少、適合徒步的景點(diǎn)給出推薦控制在150字以內(nèi)。模型依據(jù)這段簡(jiǎn)報(bào)生成答案給出的推薦自然就避開了熱門景區(qū)而傾向那些徒步路線相對(duì)冷門、風(fēng)景更自然的地點(diǎn)。用戶如果覺得推薦滿意點(diǎn)了贊這個(gè)點(diǎn)贊行為又會(huì)作為信號(hào)反饋給采集層再次提高“偏好安靜的自然環(huán)境”這個(gè)標(biāo)簽的置信度。這條閉環(huán)跑通之后產(chǎn)品就具備了一個(gè)最基礎(chǔ)的“越用越懂你”能力。4.4 效果評(píng)估別只盯著模型指標(biāo)記憶系統(tǒng)上線后怎么評(píng)估效果我們分成了離線指標(biāo)和在線指標(biāo)兩部分。離線階段跑了三組測(cè)試召回命中率、記憶標(biāo)簽準(zhǔn)確率、上下文沖突率。召回命中率看的是召回片段和當(dāng)前問題是否相關(guān)標(biāo)簽準(zhǔn)確率靠人工抽檢來(lái)打標(biāo)上下文沖突率是看提示詞里是否出現(xiàn)互相矛盾的信息。這三項(xiàng)在開發(fā)階段用回歸測(cè)試集持續(xù)盯著就夠用。在線階段則更關(guān)注業(yè)務(wù)指標(biāo)。我們重點(diǎn)看了四個(gè)用戶次日留存、平均對(duì)話輪數(shù)、回答采納率、差評(píng)率。記憶系統(tǒng)的上線會(huì)讓對(duì)話輪數(shù)和使用時(shí)長(zhǎng)明顯上升如果一段時(shí)間內(nèi)這些指標(biāo)沒有變化說(shuō)明記憶沒有形成正向體驗(yàn)。還有一類比較隱蔽的評(píng)估方式是“可解釋性回訪”隨機(jī)抽選用戶對(duì)照他們的記憶畫像讓用戶自己判斷畫像準(zhǔn)不準(zhǔn)。我們做了一輪訪談發(fā)現(xiàn)用戶對(duì)于“智能體記得我上次說(shuō)過什么”的正面感知特別強(qiáng)烈哪怕只對(duì)了一條也會(huì)提升對(duì)整個(gè)產(chǎn)品的信任。這比任何算法指標(biāo)都更能說(shuō)明問題。5. 常見問題與排查技巧實(shí)錄5.1 記憶污染聊錯(cuò)一次永久帶偏記憶污染是我們上線后遇到最頭疼的問題。典型癥狀是用戶某次閑聊時(shí)隨口說(shuō)了句氣話“我再也不喝咖啡了”系統(tǒng)立刻把“不喝咖啡”寫入畫像之后半個(gè)月的推薦全都避開咖啡用戶無(wú)奈地說(shuō)“我不是那個(gè)意思啊”。排查下來(lái)發(fā)現(xiàn)根因是寫入策略太激進(jìn)把低置信度的情緒化表達(dá)直接當(dāng)成了偏好。解決方案從三個(gè)層面入手。第一層在采集層加情緒識(shí)別帶有明顯負(fù)面情緒的句子不直接入畫像只進(jìn)情景記憶存檔。第二層對(duì)隱式標(biāo)簽設(shè)置“冷靜期”新標(biāo)簽先以低置信度寫入至少需要在三天內(nèi)的兩次對(duì)話中重復(fù)出現(xiàn)才會(huì)升級(jí)為正式標(biāo)簽。第三層提供用戶可見的“記憶管理面板”讓用戶能直接對(duì)每條記憶做刪除和修改。這個(gè)過程在智能體產(chǎn)品里不是額外功能而是必須項(xiàng)因?yàn)樽詣?dòng)記憶一定會(huì)犯錯(cuò)要給用戶一個(gè)糾錯(cuò)的出口。5.2 上下文爆炸長(zhǎng)對(duì)話下的記憶裁剪記憶系統(tǒng)上線后另一個(gè)高頻問題是上下文長(zhǎng)度。用戶的記憶越積越多提示詞里的記憶簡(jiǎn)報(bào)越拼越長(zhǎng)最終吃掉大量上下文配額甚至影響模型響應(yīng)速度。我們遇到一個(gè)極端案例某位高頻用戶對(duì)話三個(gè)月后畫像標(biāo)簽和記憶片段加起來(lái)接近8000字每次請(qǐng)求的延遲都比其他人高出近一倍。解決思路是給記憶簡(jiǎn)報(bào)設(shè)“總字?jǐn)?shù)值”通??刂圃?00字以內(nèi)。超出的部分通過優(yōu)先級(jí)截?cái)嘞缺A舾咧眯哦犬嬒裨俦A艚谟洃涀詈蟛欧胖虚L(zhǎng)期的情景記憶。與此同時(shí)后臺(tái)做一個(gè)記憶摘要任務(wù)把三個(gè)月前的事件摘要合并壓縮成一條全局摘要只保留“發(fā)生了什么、結(jié)果如何、涉及什么偏好”三要素。這個(gè)處理在實(shí)踐里非常管用記憶簡(jiǎn)報(bào)的平均長(zhǎng)度從1500字降到了500字左右模型生成質(zhì)量和響應(yīng)速度雙雙提升屬于少見的“減法優(yōu)化”。5.3 冷啟動(dòng)與召回不準(zhǔn)數(shù)據(jù)稀疏怎么辦新用戶沒有任何畫像和情景記憶時(shí)記憶系統(tǒng)基本是空轉(zhuǎn)的這個(gè)問題叫冷啟動(dòng)。我們嘗試的方案有兩個(gè)。第一是在第一次交互時(shí)主動(dòng)提問采集信息但不是生硬地讓用戶填問卷而是把問題融進(jìn)對(duì)話里比如用戶問天氣時(shí)順手問一句“您在南方還是北方換季的時(shí)候我會(huì)留意提醒您”。第二是為新用戶加載一套“通用常識(shí)包”把城市信息、季節(jié)信息、常見場(chǎng)景模板掛載到記憶上下文里雖然不是個(gè)性化內(nèi)容但至少能讓首輪回答不那么空洞。召回不準(zhǔn)的問題更多出現(xiàn)在向量索引的調(diào)參不當(dāng)。我們踩過的典型坑是文本長(zhǎng)度差異太大造成的匹配偏差。長(zhǎng)文本向量和短文本向量如果不做長(zhǎng)度截?cái)嗷驓w一化語(yǔ)義距離的計(jì)算結(jié)果會(huì)偏向長(zhǎng)度相近的文本而不是含義相近的文本。解決方法是寫入前把所有記憶片段做長(zhǎng)度歸一化統(tǒng)一截成256字符以內(nèi)的摘要再進(jìn)行向量化。這個(gè)改動(dòng)看似不起眼卻讓召回命中率提升了差不多十個(gè)百分點(diǎn)。5.4 隱私與合規(guī)底線哪些記憶不能碰記憶系統(tǒng)天然會(huì)觸碰個(gè)人信息這是逃不開的課題。我們的原則是能少存就少存能脫敏就脫敏能給用戶控制權(quán)就給控制權(quán)。身份證號(hào)、銀行卡、詳細(xì)住址這類強(qiáng)敏感信息一律不寫入記憶甚至在采集層就要做識(shí)別攔截。健康信息、政務(wù)信息等敏感類別只在用戶主動(dòng)提供且明確授權(quán)時(shí)才允許進(jìn)入情景記憶并且使用時(shí)只保留必要描述不保留原始隱私細(xì)節(jié)。我們還在產(chǎn)品里加入了“記憶透明度”設(shè)計(jì)。用戶在對(duì)話中隨時(shí)可以問智能體“你記得關(guān)于我的哪些信息”系統(tǒng)會(huì)把已存儲(chǔ)的記憶清單以通俗語(yǔ)言展示出來(lái)。用戶也可以一鍵清空某類記憶或全部記憶。這個(gè)設(shè)計(jì)雖然不能帶來(lái)直接的業(yè)務(wù)指標(biāo)但對(duì)用戶信任度和合規(guī)風(fēng)險(xiǎn)控制的價(jià)值非常大。很多從業(yè)者會(huì)忽略這一點(diǎn)我強(qiáng)烈建議把記憶可視化放到需求優(yōu)先級(jí)前列。一個(gè)連自己記住了什么都不愿意展示給用戶的智能體很難真正獲得用戶信賴。結(jié)尾最后再分享一點(diǎn)個(gè)人體會(huì)。在做“小度想想”這套記憶系統(tǒng)之前我一直以為最難的是算法和存儲(chǔ)架構(gòu)真正落地之后才發(fā)現(xiàn)難點(diǎn)其實(shí)是“克制”。克制寫入、克制召回?cái)?shù)量、克制對(duì)用戶隱私的過度挖掘每一條克制都在為更好的長(zhǎng)期體驗(yàn)讓路。我們?cè)?jīng)因?yàn)檎倩孛新什粔蚋叨つ考右?guī)則結(jié)果系統(tǒng)變得格外“自以為是”后來(lái)砍掉一半規(guī)則效果反而好了。所以給正在做類似項(xiàng)目的朋友一個(gè)建議先搭一個(gè)最簡(jiǎn)單的“畫像 情景記憶”的閉環(huán)跑通核心體驗(yàn)再逐步加復(fù)雜策略。另外一個(gè)小技巧給每一類記憶都留一個(gè)“用戶可解釋入口”用戶能看見的系統(tǒng)才更可信。這個(gè)領(lǐng)域還在快速演進(jìn)今天單條記憶的存取邏輯說(shuō)不定未來(lái)就會(huì)變成智能體之間協(xié)作的通用語(yǔ)言。