:解決大模型對話失憶的上下文管理策略)
我最早被“失憶”坑到是在做一個客服機器人項目。線上跑了兩個月前20輪對話一切正常到第35輪左右機器人突然開始一本正經(jīng)地編造訂單狀態(tài)甚至把A用戶的數(shù)據(jù)安到B用戶頭上。我第一反應是模型能力不行差點去換底座大模型。后來把請求日志翻出來對比才發(fā)現(xiàn)問題根本不在推理而在上下文——發(fā)給模型的消息列表里最早的約束已經(jīng)被擠出了窗口模型壓根沒“看見”它。從那天起我開始認真研究context-mode也就是上下文管理模式。簡單說就是你決定“把哪些內容放進模型這一次請求里”的策略。它解決的核心問題不是模型會不會推理而是模型能不能拿到足夠且正確的信息來推理。這篇文章我會把我在實際項目里落地的幾種context-mode方案、代碼、量化對比和踩過的坑全部寫出來希望能幫正在做Chatbot、AI Agent、RAG應用的人少走一點彎路。1. context-mode要解決的是哪個“失憶”問題1.1 一次讓我尷尬的項目事故先說前面提到的那個客服機器人。業(yè)務方要的是一個能處理“訂單查詢、退換貨、催開發(fā)票”的助手我們早期實現(xiàn)非常簡單把聊天歷史全部塞進prompt丟給模型讓它回答。demo階段一切美好因為大家測試最多聊十輪八輪。但上線后真實用戶不會按你的劇本走有人連著聊了五六天同一個會話里反復追問不同訂單的狀態(tài)。然后事故就來了。第30輪之后模型開始“忘記”用戶最開始強調的“我是白金會員所有優(yōu)惠都要按最高折扣算”。更離譜的是有一輪我手動翻日志發(fā)現(xiàn)模型把用戶A的收貨地址當成用戶B的來比對。Root cause非常清晰模型一次請求的上下文窗口有限消息列表只能容納最近的十幾輪內容早期關鍵信息被后面的對話給“擠沒”了。這個事故讓我意識到做AI應用和做傳統(tǒng)后端完全不同。你不只是在寫一套接口你是在替模型決定“它應該看什么材料”。這個決策本身就是應用的核心邏輯。1.2 上下文窗口的本質模型并沒有“記住”很多人對上下文窗口有個錯誤理解覺得模型聊得多了就“記得”前面聊了什么。實際上大語言模型的上下文窗口更像是一個臨時的“閱讀稿”。它只在這一次推理時把窗口里的內容全部讀一遍然后生成回復生成完了這次讀過的內容就丟了。下次推理你又重新把材料遞進去。所以所謂“長記憶”根本不是模型的能力項而是應用層不斷把歷史內容搬回窗口里。窗口總是有限的GPT-4o一類的模型常見上下文是128K token看著很大但真實業(yè)務里塞幾個長文檔、幾十輪歷史、一段工具返回結果很快就能吃滿。而且窗口越大單次請求的成本和延遲也跟著漲。context-mode的本質就是在這個“有限的閱讀稿”里做取舍決策。它需要回答三個問題哪些內容必須進窗口哪些內容可以降級處理哪些內容可以直接丟棄1.3 context-mode的三個核心指標我在項目里給自己定了三個可量化的指標所有模式優(yōu)化都圍繞它們來評估信息保留率模型回答需要的關鍵實體、約束、指令最終是否還留在上下文里。我一般用一組構造好的“必須知道”測試題來驗證比如把用戶設置的折扣約束放進第40輪歷史看第50輪模型還能不能復述出來。單輪token成本每發(fā)一次請求實際消耗的輸入token數(shù)量。這直接決定賬單大小特別是高頻客服場景成本差一倍月結單就完全不同。響應延遲上下文越長prefill階段耗時越長。正常情況下幾百token和幾千token差距不大但一旦塞入上萬token首字延遲會明顯上升。所有context-mode方案本質都是在三者之間找一個適合業(yè)務場景的平衡點。沒有最優(yōu)只有最合適。2. 三種主流context-mode的取舍滑動窗口、摘要壓縮、檢索增強2.1 滑動窗口把最近N輪留下滑動窗口是最容易想到的方案也是我當時第一個實現(xiàn)的方式。思路很簡單每條消息按時間排序構造請求時只取最新的若干條直到接近token預算就截止。它的最大優(yōu)勢是行為可預期。代碼量小邏輯直白不會出現(xiàn)“模型因為摘要過擬合而胡說”的情況。你放進去的就是原文模型讀到的東西沒有經(jīng)過二次加工信息失真風險低。但它有個非常致命的短板早期關鍵信息會被無差別丟棄??头永锏陌捉饡T約束如果是第2輪說的第35輪已經(jīng)不在窗口里了模型根本不知道這個約束存在。所以滑動窗口只適合那種“最新內容最重要”的場景比如閑聊機器人、短期任務助手。2.2 摘要壓縮把對話變成“工作筆記”摘要壓縮是我第二個嘗試的模式。它的動機很簡單模型真正需要的往往不是對話的逐字原文而是其中蘊含的“事實和意圖”。與其保留每一句話不如定期讓模型把歷史對話整理成一份結構化摘要下次請求時把摘要加原文一起放進窗口。舉個例子用戶在第3輪說“我周五要去上海出差幫我訂虹橋附近的酒店”后面又聊了一堆茶葉價格之類無關話題。第20輪用戶說“幫我看看之前說的酒店”這時候模型需要的是“周五、上海、虹橋附近”這幾個信息而不是中間17輪閑聊。我的實現(xiàn)方式是滾動摘要每隔一段時間或一定token量把當前摘要與新消息一起丟給模型生成更新的摘要。這類似工作里記筆記不斷往里補增量。摘要壓縮的優(yōu)點是信息保留率高能把大量歷史濃縮成幾百字并且保留全局語義。缺點也很明顯摘要本身有損壓縮過程中可能丟掉關鍵細節(jié)而且摘要調用本身也要花token和時間。2.3 檢索增強讓上下文不再連續(xù)而是“按需取用”檢索增強是我后來最常用的模式也就是把上下文從“連續(xù)的切面”變成“可查詢的知識庫”。核心思路是每條歷史消息都向量化存入向量數(shù)據(jù)庫當用戶提出新問題時先對問題做語義檢索只把相關的歷史消息片段取出來放進上下文。這個模式和RAG在外觀上很像但目的不同。RAG檢索的是外部知識庫檢索增強檢索的是對話自身的記憶。它解決的是“歷史很長但高度離散”的場景比如一個用戶零零散散地在不同時間問過多個不同訂單的問題每個問題之間沒有強關聯(lián)。檢索增強的優(yōu)勢是信息保留率可調你可以只取top-k相關片段也可以額外加一個關鍵詞過濾條件。它不會像滑動窗口那樣犧牲早期的關鍵信息也不會像摘要那樣把細節(jié)抽象掉。缺點是系統(tǒng)復雜度明顯上升需要維護向量庫、 embedding、檢索鏈路并且檢索質量直接決定回復質量檢索不到模型就真的不知道。2.4 三種模式的量化對比我把自己項目里的實測數(shù)據(jù)整理成了一張表方便你在方案選型時直接參考基于128K窗口、中文對話場景模式信息保留能力單輪token成本延遲影響實現(xiàn)復雜度典型適用場景滑動窗口低早期信息易丟低低最簡單閑聊、短期任務、客服的“最近意向”判斷摘要壓縮中高但細節(jié)有損中額外摘要調用中中等長會話總結、需要全局語義、關鍵約束密集檢索增強高但依賴檢索質量低-中中檢索耗時較高知識密集、問題高度離散、長期多主題對話需要注意這三種模式不是互斥的。我在最終版本里做的是混合模式全局摘要保底 滑動窗口保近期 向量檢索補細節(jié)后面我會寫具體實現(xiàn)。3. 我的落地實現(xiàn)一套可切換的context-mode框架3.1 數(shù)據(jù)結構給每條消息打上“標簽”和“權重”在寫任何模式之前我先把消息數(shù)據(jù)結構重新設計了。這一步非常重要后期所有策略都建立在消息的meta信息之上。我的每條消息包括五個字段dataclass class ChatMessage: role: str # system / user / assistant / tool content: str msg_id: str # 全局唯一 ts: float # 時間戳用于排序和時效判斷 category: str chat # chat / constraint / fact / tool_result / greeting retention: int 1 # 保留權重0可選丟棄1默認2重要3絕不可丟category和retention是我自己加的。“constraint”是用戶明確的約束性指令比如“不要推薦含糖飲料”“fact”是事實型實體比如訂單號、日期、金額“tool_result”是工具返回的原始結果“greeting”是寒暄。retention用于告訴上下文構造器這條消息的丟棄優(yōu)先級。有了這兩個字段構造上下文時就靈活多了。系統(tǒng)約束永遠保留retention3用戶明確指令保留retention2工具結果和事實按需保留retention1寒暄直接丟棄retention0。3.2 token預算的精確計算構建上下文的第一步永遠是算預算。我封裝了一個token計數(shù)器用tiktoken來估算import tiktoken enc tiktoken.encoding_for_model(gpt-4o) def count_tokens(text: str) - int: if not text: return 0 return len(enc.encode(text))然后定義預算結構。核心原則是先留出模型回復的空間再裝system prompt再裝本次新消息最后剩下的空間才分給歷史內容。def build_sliding_context( system_prompt: str, history: list, pending: list, max_tokens: int 16000, reserve_output_tokens: int 2000, ): budget max_tokens - reserve_output_tokens system_msg {role: system, content: system_prompt} budget - count_tokens(system_prompt) # 本次必須帶上的新消息 for msg in pending: budget - count_tokens(msg[content]) # 從歷史最末端最新的消息往回收集塞滿剩余預算 selected [] for msg in reversed(history): cost count_tokens(msg[content]) if budget - cost 0: break selected.append(msg) budget - cost selected.reverse() return [system_msg] selected pending有兩個細節(jié)容易被忽略。第一reserve_output_tokens不能設得太小否則模型生成到一半就被截斷。我一般根據(jù)業(yè)務回答長度估算客服場景設2000長文生成場景至少4000。第二budget - cost 0才break而不是0這樣能盡量利用最后一點剩余空間避免白白浪費。3.3 摘要壓縮的實現(xiàn)細節(jié)摘要模式我并沒有簡單地把整段歷史一次性丟給模型總結那樣很容易超窗口。我采用的是“分批滾動摘要”的方式def incremental_summary(prev_summary: str, new_messages: list, client) - str: content \n.join(f{m[role]}: {m[content]} for m in new_messages) prompt f你正在管理一段長時間對話的記憶。請合并以下兩部分內容輸出一份新的結構化摘要。 要求: 1. 保留所有用戶明確的指令、偏好和約束 2. 保留關鍵實體訂單號、日期、金額、人名、地址等 3. 去掉寒暄、重復表達和低信息量內容 4. 摘要保持在300字以內。 【已有摘要】 {prev_summary} 【新對話】 {content} resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0.2, ) return resp.choices[0].message.content每次調用前我先統(tǒng)計新消息的總token量如果超過4000就自動切成多個小批然后循環(huán)調用incremental_summary把上一輪的摘要作為下一輪的“已有摘要”傳進去。這樣做的好處是無論對話多長摘要調用的token開銷都是可控的不會出現(xiàn)“為了省token反而燒掉更多token”的尷尬。另外一個我踩出來的經(jīng)驗摘要生成用mini模型就行不必用旗艦模型。因為摘要任務本身對推理要求不高關鍵是提取準確性。我個人用gpt-4o-mini實測信息保留率和旗艦模型相差不大但成本低了差不多一個數(shù)量級。如果你用的是開源模型做底座摘要生成也可以復用同一套模型只是速度會慢一些。3.4 自動切換策略什么情況用什么模式做了三種模式之后新的問題來了每次請求到底用哪個我最后寫了一個簡單的策略決策器規(guī)則不復雜但足夠有效def decide_mode(app_state): # 歷史占總窗口預算的比例 ratio app_state.history_tokens / app_state.max_tokens if ratio 0.4: return full # 歷史遠沒到預算直接全量 if app_state.fact_density 0.2: return sliding # 事實密度低大多是無關聯(lián)的閑聊 if app_state.query_discrete: return hybrid # 問題之間離散單點查詢居多用檢索 return summary # 默認用摘要壓縮這個決策器依賴兩個額外信號fact_density我給每條消息打了category標簽統(tǒng)計最近N條消息里“fact”和“constraint”占比占比高說明這段對話信息密集不能隨便丟。query_discrete用戶在連續(xù)多輪里是否在問完全不同的問題。這個可以通過向量相似度來算如果相鄰問題間的平均相似度低于閾值就認為是離散多頭查詢。hybrid模式是我最終推薦的方式系統(tǒng)約束全量保留近期消息用滑動窗口再疊加一層向量檢索補充細節(jié)。它稍微復雜一點但在長會話場景里效果最穩(wěn)。4. 實測中踩過的坑與優(yōu)化建議4.1 上下文污染摘要把工具輸出當成了“事實”第一個坑出現(xiàn)在摘要模式上線當天??头C器人在查詢天氣工具時返回了“明日有暴風雨”摘要把它記成了一條事實。第二天用戶問“明天適合戶外活動嗎”模型根據(jù)摘要里的“暴風雨”給出否定建議。問題在于那條工具結果是2小時前的天氣早就變了。這種問題我稱為“上下文污染”模型把某個時刻的工具輸出當成了永久事實。工具結果天然有時效性不能進入長期摘要至少必須帶上時間戳。我的解決方案是給工具結果單獨設置categorytool_result并且默認retention1。在做摘要壓縮時把tool_result排除在“摘要對象”之外只保留“這條工具結果回答了什么問題”這樣的元信息。比如不記“明日有暴風雨”而是記“用戶詢問了明日天氣已返回預報結果”。等到需要精確數(shù)據(jù)時走檢索增強去拿原始記錄。4.2 摘要的“歧義塌縮”關鍵實體被抽象掉了第二個坑比較隱蔽也最難發(fā)現(xiàn)。在某次長會話里用戶先提到“張總”后來又提到“張經(jīng)理”其實指的是同一個人。摘要模型在壓縮時統(tǒng)一成了“張總”這沒問題。但另一段對話里“張經(jīng)理”是另一個部門的人摘要也統(tǒng)一成了“張總”。結果模型在回答“張經(jīng)理負責什么業(yè)務”時把兩個部門的信息混在了一起。這就是摘要壓縮的“歧義塌縮”。模型為了把內容壓縮進有限字數(shù)會傾向于做實體合并而合并產生的歧義水平無法被下游輕易察覺。我的對策是雙重的第一摘要prompt里強制要求“實體出現(xiàn)時保留完整稱謂不縮寫不合并若不確定則并列保留”第二在摘要之外單獨維護一個“關鍵實體表”用正則和命名實體識別抽取出訂單號、人名、日期、金額這類數(shù)據(jù)不做壓縮始終原始保留。這個實體表非常值得單獨做。需要精確查詢時它比摘要可靠得多而且是結構化數(shù)據(jù)可以直接參與規(guī)則判斷不一定要經(jīng)過模型。4.3 token成本失控摘要調用反而把賬單推高了第三個坑和錢有關。我最初設計是“每當歷史超過閾值就觸發(fā)一次摘要”結果遇到一個話癆用戶幾乎每三四輪就觸發(fā)一次摘要調用而且每輪主請求還是全量發(fā)送。月底一看賬單摘要花費占了總成本的35%。這個教訓是摘要不能是“高頻操作”它應該是“低頻兜底操作”。我把觸發(fā)閾值從4000token調高到12000token并且加了冷卻時間——兩次摘要之間至少間隔30分鐘或20條新消息。另外一個優(yōu)化是不把摘要結果立即放進下一輪請求而是先讓它存儲在內存只有當歷史即將撐爆窗口時才加載。4.4 精心設計評估怎么判斷context-mode改好了還是改壞了context-mode做得對不對不能靠感覺。我后來搭了一套專門的評估集這里分享一個最值得做的“關鍵信息保持”測試準備一組20個“關鍵約束”分別散布在對話歷史的第1、第10、第30、第50輪。讓測試腳本自動運行對話到第60輪然后向模型提問每個約束對應一個問題。統(tǒng)計答對率對比不同模式下的得分。我實測下來單純用滑動窗口時第30輪之前的約束答對率只有約40%摘要壓縮能到75%左右混合檢索模式可以穩(wěn)定在85%以上。這組數(shù)據(jù)很有說服力也很容易讓業(yè)務方理解“為什么需要做context-mode”。同時還要監(jiān)控兩個反向指標回復的“串線率”把不同主題的信息混在一起的比例和每條消息的平均成本。有時候一個方案信息保留率很高但成本高得離譜那就得調整策略。5. 場景化落地建議不是所有應用都需要最強模式5.1 先判斷你的業(yè)務屬于哪一型我見過很多團隊一上來就上向量檢索結果項目跑了一個月發(fā)現(xiàn)瓶頸根本不在上下文而在意圖識別。這里我把常見應用分成四類你可以對照著選模式應用類型典型特征推薦context-mode客服輔助會話長、問題離散、事實密集混合模式摘要檢索個人助理短期任務多、最新意圖重要滑動窗口輕量摘要文檔問答外部知識為主、對話歷史短滑動窗口即可重點是RAG角色扮演/閑聊全局人設重要、細節(jié)容忍度高摘要壓縮這個表不是絕對標準但能幫你快速定位。最重要的是先量化你的歷史特征再選模式。我每次接到新項目都會先跑一遍真實對話日志統(tǒng)計平均會話長度、事實密度、問題相似度然后才動手設計。5.2 給初次落地的人一個靠譜的推進路徑如果你是第一次做context-mode我建議不要直接堆復雜方案。按下面的順序演進第一版只做滑動窗口但把消息加好category和retention標簽。這步很快一兩天就能完成。上線后收集真實日志統(tǒng)計“最早出現(xiàn)的關鍵約束在第幾輪被擠出窗口”找到一個可復現(xiàn)的失憶案例。加入摘要壓縮配上實體表。先讓摘要只保留“關鍵指令和事實”不要貪全。如果還有離散查詢場景回答不好再上向量檢索。每一步都留出觀察時間至少跑一周真實流量用上一節(jié)說的評估集量化效果。不要跳步否則出了問題你根本沒法定位是摘要丟信息還是檢索沒召回。5.3 后續(xù)可以繼續(xù)擴展的方向context-mode這套東西寫完并不代表一勞永逸。我接下來計劃做幾件事第一把摘要和檢索的觸發(fā)條件從規(guī)則改成可學習策略?,F(xiàn)在已經(jīng)積累了不少“哪些消息最終幫助正確回答”的日志數(shù)據(jù)后續(xù)可以用這些數(shù)據(jù)訓練一個輕量級模型來決定上下文構成而不是靠人工閾值。第二做跨會話記憶?,F(xiàn)在的context-mode只解決單會話內部的消息管理但很多用戶會多次回訪下一次會話其實也應該帶上之前會話的關鍵摘要。這個方向我已經(jīng)在規(guī)劃本質上是把“會話級摘要”升級成“用戶級長期記憶”。第三把上下文預算做成可觀測的可視化儀表盤讓運營人員能實時看到每一輪請求里“system占多少、歷史占多少、檢索片段占多少”這樣調參就有數(shù)據(jù)支撐。如果讓我重新來一次我會在項目第一天就加一個“輸入輸出token計數(shù)”的中間件把每條消息的類別和保留權重在入庫時打好。context-mode不是一個開關而是一套工程習慣在寫代碼之前先想清楚哪些信息不能丟哪些可以壓縮哪些壓根不用看。你把這個想明白了后面所有模式實現(xiàn)都會順很多。