機實戰(zhàn))
1. 先看三個翻車現(xiàn)場沒有上下文模式會怎樣context-mode這個詞最近在 LLM 應用開發(fā)的圈子里被頻繁提起。我最早看到它的時候以為只是一個簡單的開關——開一下AI 就能記住對話關一下就是普通的單輪問答。直到我自己在項目里連續(xù)踩了幾個大坑才意識到這東西遠比想象中復雜。它本質上是一套對話上下文的管理策略決定了模型在每一輪生成時到底能看到哪些歷史信息、以什么形態(tài)看到、以及這些信息如何被更新和淘汰。在做智能客服系統(tǒng)時我曾天真地認為只要把多輪對話的歷史消息全部塞進 prompt 就算是支持上下文了。然后很快碰到了三個典型的翻車現(xiàn)場幾乎每一個都讓我懷疑人生。1.1 場景 A多輪對話中的失憶用戶和客服機器人聊了十幾輪局面已經非常清楚——用戶要退一張機票并且明確說了退票原因是因為航班變動。結果因為我們把上下文窗口設置得太小前面的關鍵信息被擠出了窗口模型在第十輪的時候突然反問請問您是要退哪張機票那一刻用戶心態(tài)直接崩了。更麻煩的是這種失憶不是偶發(fā)的而是隨著對話輪數增長必然出現(xiàn)的。如果你采用最簡單的固定窗口截斷策略——只保留最近的八輪對話——那么第九輪開始第一輪的信息就被丟棄了。而用戶的關鍵訴求往往恰恰是在前幾輪里說清楚的。1.2 場景 B全局上下文的信息擁堵另一個項目是文檔問答助手。我把整份產品手冊全部塞進了 system prompt再加了用戶的問題一次性發(fā)給模型。結果同樣很慘。模型確實知道所有信息但它的注意力被稀釋了回答變得模棱兩可。更要命的是 token 消耗直線上升一次普通問答的調用成本是之前的五倍而且首字響應延遲從 0.8 秒拉到了 3 秒以上。這就是典型的上下文信息過載。你要知道Transformer 的注意力機制雖然是全局的但模型的表現(xiàn)會隨著 token 數量的增長而退化尤其是在關鍵信息埋在一大堆無關內容里的時候。不是信息越多越好而是關鍵信息足夠集中才最好。1.3 場景 C多 Agent 協(xié)作中的串臺這個場景更隱蔽。我在做多智能體協(xié)作系統(tǒng)時讓幾個 Agent 共享一個全局上下文容器。本來設計的是讀和寫都通過統(tǒng)一接口調用結果某個 Agent 在調試階段誤操作往共享區(qū)域里塞了一條完全無關的信息——另一個 Agent 在下一次決策時居然參考了這條信息給出了一個邏輯荒謬的建議。這個問題的本質是共享上下文缺乏隔離機制。不同 Agent 的關注點不同、生命周期不同、信息粒度不同把它們塞進同一個上下文池里等于讓所有人穿同一件衣服尺碼不對是必然的。這三個場景讓我下定決心認認真真設計一套可落地、可分層的context-mode管理方案。這篇文章就把我這幾個月的設計和踩坑經驗完整寫出來包含代碼級別的實現(xiàn)思路、模式切換的狀態(tài)機設計、token 預算計算方式以及測出來的那些讓人哭笑不得的邊界情況。適合所有正在做 LLM 應用開發(fā)尤其是對話系統(tǒng)、Agent 系統(tǒng)、RAG 問答的工程師參考。2. 四種核心模式按需選型而不是一把梭先明確一個基本認知不存在一種上下文模式能同時解決所有問題。單輪、滑動窗口、摘要壓縮、結構化記憶這四種模式各有各的適用場景而且它們不是互斥的是一個系統(tǒng)里可以根據情況動態(tài)切換的檔位。我最終落地的時候系統(tǒng)里同時跑了四種模式靠一個模式分發(fā)器根據當前對話的狀態(tài)來決定走哪條路。下面逐個說清楚每種模式的內存邏輯和適用邊界。2.1 單輪模式Stateless Mode適合一次性問答場景這是最簡單的一種模式。每一輪請求都是獨立的不攜帶任何歷史消息。模型只看到當前用戶輸入和 system prompt。聽上去很原始但在大量真實場景里它反而是最優(yōu)解。比如關鍵詞抽取、實體識別、意圖分類、單個事實問答。這些任務本身不依賴上下文強行加歷史反而會引入干擾。我見過有人在情感分類任務里把歷史聊天記錄全塞進去結果模型被歷史里的情緒帶偏把當前這一句的正面情緒判成了負面。代碼上單輪模式只需要在組裝請求時跳過歷史消息序列def build_messages(strict_mode: bool, current_input: str, history: list[Message]) - list[dict]: if strict_mode: # 單輪模式完全丟棄歷史 return [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: current_input} ] # 其他模式繼續(xù)走 history 拼接邏輯這個函數雖然簡單但它是我整個上下文管理器的入口。所有模式最終都要經過這一層來組裝 messages。2.2 滑動窗口模式Sliding Window Mode最常用的保底方案滑動窗口是最直覺、最容易實現(xiàn)、也是絕大多數項目默認在用的方案。核心邏輯就是一句話只保留最近 N 條歷史消息。這個 N 怎么定不是拍腦袋定的需要根據 token 預算反推。我習慣先定一個窗口的 token 上限然后往里塞消息塞不下就從最老的開始丟。具體計算方式我在第 4 章詳細展開?;瑒哟翱诘膬?yōu)點是實現(xiàn)簡單、延遲低、token 開銷穩(wěn)定。缺點是中期記憶必然丟失。假設窗口能裝 20 條消息那第 21 條消息進來的時候第 1 條就被擠出去了。如果用戶在第 1 條里說了自己的需求在第 30 條時又補充了一個關鍵細節(jié)模型大概率已經把第 1 條的約束忘了。我當前的做法是滑動窗口只作為默認檔位一旦檢測到對話涉及關鍵長期信息就升級到摘要模式或結構化記憶模式。2.3 摘要壓縮模式Summary Mode跑贏窗口上限的唯一辦法當對話輪數實在太多、無法在 token 預算內完整放進去時摘要壓縮是繞開上限的常規(guī)路徑。核心思路是用一條高度提煉的摘要代表那些已經超出窗口范圍的歷史對話讓模型在有限上下文里感知到全局信息。我第一版自己寫摘要邏輯用老辦法——每隔 5 輪調用一次模型把前面的聊天記錄壓縮成 150 字以內的要點存起來作為上下文的前綴。后來發(fā)現(xiàn)效果不穩(wěn)定因為模型會把很多細節(jié)丟掉導致后面問答的精確度下降。后面我加了結構化摘要的思路效果好了很多。所謂結構化摘要就是按照事先定義好的 schema 輸出而不是讓模型自由發(fā)揮。比如客服場景下摘要必須包含四個字段SUMMARY_SCHEMA { user_intention: 用戶當前的核心訴求, key_facts: [已確認的關鍵事實如訂單號、時間、金額], user_emotion: 情緒狀態(tài)平靜/不滿/憤怒, unresolved: 尚未解決的事項 }用 JSON 格式約束模型輸出之后摘要模式才變得真正可靠。后續(xù) Agent 從摘要里取信息時能穩(wěn)定地按照字段解析而不是在自由文本里大海撈針。2.4 結構化記憶模式Memory Mode長期對話的戰(zhàn)略儲備如果說摘要壓縮是壓縮過去的思路那么結構化記憶就是提煉資產的思路。摘要仍然要圍繞已有的對話內容轉述而記憶模式則是把重要信息顯式存成結構化條目越積越多永不丟失直到被主動更新或刪除。我在系統(tǒng)里給每個用戶維護了一個 Memory Bank里面長這樣{ user_id: u_1024, facts: { airline_preference: 國航, seat_preference: 靠窗, member_level: 金卡 }, current_order: { order_no: CA1234, status: refunding, reason: 航班變動 }, negations: [ 不接受改簽到第二天 ] }這個 Memory Bank 不是一次性全量塞進 prompt。它是按需讀取的——在組裝上下文時根據當前用戶問題動態(tài)挑選相關的條目填入。比如用戶下一次提到我要退票系統(tǒng)就把current_order、negations相關條目讀出來和最近的滑動窗口拼接形成最終上下文。這個按需讀取很關鍵避免把所有記憶全部塞入 prompt 導致的 token 膨脹。3. 狀態(tài)機設計四種模式是如何被調度起來的上面四種模式如果只是各跑各的那還談不上是系統(tǒng)。真正讓它成為一個完整方案的是中間那層模式調度邏輯——什么時候用單輪什么時候從滑動窗口升級到摘要什么時候把信息寫入 Memory Bank。我把這一層實現(xiàn)成了一個狀態(tài)機。每個對話 session 在任意時刻都處于某一種上下文模式下根據特定事件觸發(fā)模式切換。3.1 上下文生命周期從創(chuàng)建到回收每個 session 在創(chuàng)建時先處于STATELESS單輪模式因為此時還沒有任何歷史不存在維護上下文的必要。第一條用戶消息進來后系統(tǒng)判斷是否需要開啟多輪模式。如果用戶的問題是一個獨立任務比如翻譯這句話就保持單輪如果是幫我規(guī)劃行程這種天然需要后續(xù)追問的就切到SLIDING_WINDOW。我把生命周期分成了四個階段階段模式觸發(fā)條件退出條件初始化STATELESSsession 創(chuàng)建檢測到多輪意圖正常對話SLIDING_WINDOW多輪意圖觸發(fā)累計 token 超預算長對話壓縮SUMMARY窗口 token 超限摘要寫入成功記憶沉淀MEMORY出現(xiàn)可提煉的長期信息記憶條目落庫這個狀態(tài)機的切換方向常規(guī)下是單向流動的STALENESS → SLIDING_WINDOW → SUMMARY → MEMORY。但允許回退——比如用戶明確說我們換一個話題那么舊的 SUMMARY 和 MEMORY 都會清空或標記失效session 回到 SLIDING_WINDOW 重新積累。3.2 觸發(fā)條件到底在什么閾值下切換狀態(tài)機的價值不在于狀態(tài)定義而在于切換條件的合理性。先說從 SLIDING_WINDOW 切到 SUMMARY 的時機。我一開始的做法是當最近 5 輪對話的 token 數總和超過了窗口預算的 80%就觸發(fā)摘要。這樣做的壞處是頻繁觸發(fā)——用戶稍微多說幾句就壓縮一次壓縮本身要調一次模型延遲和成本都上去了。后來我把觸發(fā)方式改成了惰性壓縮def need_compress(session) - bool: # 當前對話總token含歷史已經接近窗口上限的 85% 時才觸發(fā) return session.estimated_tokens() session.window_budget() * 0.85這樣只有在真正快裝不下的時候才壓縮盡量減少無謂的模型調用。再說寫 Memory 的時機。不是每輪對話都值得寫入記憶。我使用了一個基于規(guī)則的特征過濾只有當對話中出現(xiàn)明確的偏好類關鍵詞我喜歡我不喜歡以后都千萬不要時才會觸發(fā)一次記憶抽取。這個規(guī)則雖然粗暴但召回率高而且?guī)缀醪粫z漏重要信息。3.3 狀態(tài)機實現(xiàn)一個極簡但完整的狀態(tài)核心實際代碼里我用了一個簡單的枚舉加一個 manager 類來管理狀態(tài)。沒有上復雜的狀態(tài)機框架因為目前的模式數量有限手寫更可控。from enum import Enum, auto class ContextMode(Enum): STATELESS auto() SLIDING_WINDOW auto() SUMMARY auto() MEMORY auto() class ContextManager: def __init__(self, user_id: str, window_budget: int 12000): self.mode ContextMode.STATELESS self.history: list[Message] [] self.summary: Summary | None None self.memory MemoryBank(user_id) self.window_budget window_budget def add_turn(self, user_msg: str, assistant_msg: str): self.history.append(Message(roleuser, contentuser_msg)) self.history.append(Message(roleassistant, contentassistant_msg)) # 1. 更新模式 if self.mode ContextMode.STATELESS: if detect_multi_turn_intent(user_msg): self.mode ContextMode.SLIDING_WINDOW elif self.mode ContextMode.SLIDING_WINDOW: if self.estimate_tokens() self.window_budget * 0.85: self.compress_history() # 觸發(fā)摘要壓縮 self.mode ContextMode.SUMMARY elif self.mode ContextMode.SUMMARY: if detect_long_term_facts(user_msg): self.memory.extract_and_store(user_msg) # 2. 管理窗口大小防溢出 self.trim_history(self.window_budget) def build_request_messages(self, current_input: str) - list[dict]: if self.mode ContextMode.STATELESS: base [{role: system, content: SYSTEM_PROMPT}] elif self.mode ContextMode.SUMMARY: base [ {role: system, content: SYSTEM_PROMPT}, {role: system, content: f[對話摘要] {self.summary.text}} ] base self.history[-8:] # 只保留最近部分細粒度消息 else: base [{role: system, content: SYSTEM_PROMPT}] base self.history[-self.recent_visiable_count():] # 記憶條目按需注入 relevant_memory self.memory.relevant_to(current_input) if relevant_memory: base.insert(1, {role: system, content: f[用戶長期偏好] {relevant_memory}}) base.append({role: user, content: current_input}) return base這段代碼是我實際項目里跑過的簡化版幾個設計取舍值得說add_turn先更新模式再處理歷史列表順序避免模式切換和消息追加互相踩。trim_history在每次追加后執(zhí)行確保self.history永遠處在窗口預算內防止內存膨脹。build_request_messages中摘要模式和記憶注入都在 system 層完成不用占用 user/assistant 消息位置模型能更穩(wěn)定地把它們當作背景設定而不是待回復內容。摘要模式下我只保留最近 8 條細粒度消息self.history[-8:]前面的一律交給摘要。這個 8 是我實測下來細粒度信息和摘要信息平衡得最好的一個值——太少模型會失憶太多又壓縮了摘要的作用。4. Token 預算計算與窗口規(guī)劃把賬算明白再動手上下文模式的設計如果脫離 token 預算基本就是空中樓閣。窗口開多大、摘要壓縮頻率多高、歷史消息保留多少條全部由預算決定。這一章我把整個計算過程展開你可以照著算自己項目的參數。4.1 一個完整的預算計算例子假設我使用的模型支持 32,768 token 的上下文窗口很多主流模型的標準配置。這個窗口里需要放下四類東西占用項數量說明system prompt1,500角色設定、回答規(guī)則、格式要求工具定義3,000如果涉及 function calling當前輸入~500本輪用戶輸入按需估算模型輸出4,096預留生成空間那么留給歷史上下文的安全預算就是32768 - 1500 - 3000 - 500 - 4096 23672但這還不是最終可用的全部我習慣再留 15% 的余量防止單條消息特別長導致的估算偏差23672 * 0.85 ≈ 20121所以滑動窗口的有效預算大約是20000 token。接下來要記住一個大數中文會話里1 個漢字大約等于 1.5 到 2 個 token。換句話說用戶說一句 40 字的自然語言模型回一句 120 字的回答一輪消息的 token 消耗大約在 350 到 500。按照這個估算20000 token 的窗口大約能容納40 到 55 輪對話。這比我最初想的能放多少放多少要少得多。這也是為什么滑動窗口在真實場景里不夠用——正??头υ挸^ 60 輪非常常見窗口必然被撐爆。4.2 怎么算摘要模式節(jié)省了多少摘要模式下我每 10 輪壓縮一次。前面 50 輪對話按原始形態(tài)需要大約 20000 到 25000 token壓縮成摘要后大約只需要 800 到 1200 token。這時上下文的結構變成摘要(1000) 最近8輪原始消息(約3200) 4200 token整個上下文被壓縮到了原來的五分之一還不到。這不是免費午餐成本在于中間調了 5 次模型做壓縮。但綜合考慮成本和延遲摘要模式的性價比依然很高——它換來的是模型對長對話的全局感不再因為早期信息被擠出而犯低級錯誤。4.3 預估偏差怎么處理算不準的問題token 估算永遠不可能 100% 準確。不同模型的分詞器對同一段文本的切分結果不同。好在工程上不需要那么精確。我用了兩套冗余機制上限硬保護在組裝 messages 時如果發(fā)現(xiàn)總 token 數用tiktoken或transformers的 tokenizer 精確計算超過窗口上限就觸發(fā)強制裁剪。裁剪順序是先丟最老的歷史消息再丟工具定義仍然超限就強制觸發(fā)摘要壓縮。軟閾值預警只要估算 token 超過預算的 85%就提前做一次壓縮或降級避免到了下一輪直接爆掉。def trim_history(self, budget: int): while self.estimate_actual_tokens(self.history) budget * 0.9: if len(self.history) 2: break # 丟棄最老的一條消息 self.history.pop(0)這段代碼是我在實際線上服務里直接運行的邏輯。它不完美——丟棄最老消息策略在信息價值上并不是最優(yōu)的但勝在簡單可控。更聰明的方案是按信息價值丟棄比如優(yōu)先保留包含用戶明確約束的消息但那個需要額外的語義判斷成本高我目前沒有在核心路徑上啟用。5. 實測中的翻車點與解決鏈路方案設計得再漂亮到了實測環(huán)節(jié)照樣會翻車。我在壓測和線上灰度階段碰到的這幾個問題每一個都值得單獨拿出來說。5.1 模式切換瞬間的信息斷裂第一次做模式切換時我遇到了一個非常尷尬的 bug在第 52 輪滑動窗口模式切換到摘要模式摘要生成完成之后模型突然忘了用戶的姓名。排查鏈路是這樣的先確認摘要內容——摘要里確實沒有包含用戶姓名。模型在壓縮時把它當作不重要的信息丟掉了。再看窗口裁剪——切換時trim_history把老消息全裁了姓名只存在于被裁掉的那部分里。最終定位這是模式切換本身的問題摘要生成時丟了一個對當前任務并不重要、但對后續(xù)對話有長期價值的字段。解決方案也簡單在觸發(fā)壓縮之前先檢測需要保留的關鍵實體清單把清單里的信息強制追加到摘要中CRITICAL_ENTITIES [user_name, order_no, contact_phone, invoice_required] def compress_history(self): critical_info self.extract_critical_entities(self.history) summary_text generate_summary(self.history) self.summary Summary(textsummary_text 關鍵信息: str(critical_info))這樣即使在摘要的正文把姓名丟掉后面的關鍵信息后綴也會兜底。5.2 上下文重復注入的回音壁第二個坑更隱蔽。在摘要模式和 Memory 模式同時開啟后我發(fā)現(xiàn)模型開始復讀某些信息。比如用戶明明只在第 3 輪說過一次我喜歡靠窗座位到了第 30 輪模型每次回答都會提到靠窗座位哪怕當前話題根本不涉及座位。查到最后發(fā)現(xiàn)同一個事實被同時存在了摘要里和 Memory Bank 里。摘要模式把靠窗寫進了摘要文本Memory 模式又把靠窗存成了一條結構化記憶。組裝上下文時兩處信息同時生效模型接受到的信息被重復加權就會傾向于過度強調它。解決方式有兩個層面。第一是去重在寫入 Memory 之前先查重如果摘要中已經存在該事實就不再重復寫入 Memory或者反過來一旦寫入 Memory就從摘要中移除該事實的顯式描述。第二是給上下文內容加權重在 prompt 中明確標注以下摘要中包含的信息已過時以 Memory 條目為準。第二個方案雖然有點粗暴但在工程實踐中反而更有效。因為摘要的去重是一件很難精確完成的事情——摘要文本是自由文本你要判斷它是否包含某條記憶信息又得做一次語義匹配成本太高。5.3 多會話并發(fā)的上下文污染這個問題出現(xiàn)在我把 ContextManager 接入 Web 服務之后。最初我把所有用戶的 ContextManager 實例存在一個全局字典里key 是 user_id??雌饋頉]問題直到某次線上事故兩個 user_id 恰好被某個上游服務寫錯了導致 B 用戶看到了 A 用戶的摘要直接串號。排查鏈路檢查代碼發(fā)現(xiàn) ContextManager 創(chuàng)建時把 MemoryBank 和 user_id 綁定了但全局字典的 key 用的是另一個 request 級別的 session_id。當 session_id 不重復時沒問題一旦 session_id 在網關層被復用連接池場景下常見就會串。這個問題的根本原因是上下文容器和會話標識的一致性沒有在同一層保證。修復方案是在 ContextManager 內部強制校驗class ContextManager: def __init__(self, session_id: str, user_id: str): if not session_id or not user_id: raise ValueError(session_id and user_id must not be empty) self.key f{user_id}:{session_id} self.memory MemoryBank(user_id) # 關鍵Memory 永遠綁 user不綁 session另外還加了一層字典訪問的防護在取出實例時檢查內部綁定的 user_id 是否與當前請求一致不一致就重建實例并記錄告警日志。自從上了這個校驗串號問題再沒出現(xiàn)過。5.4 滑動窗口在流式輸出場景下的著裝后置最后一個是很容易被人忽略的細節(jié)。我們的客服系統(tǒng)采用了流式輸出——模型一邊生成用戶一邊看到內容。這意味著模型輸出在最后一個 token 落地之前是未完成的。我最初的設計是在add_turn中立即把 assistant 消息追加到 history。但流式輸出時assistant 消息是逐步生成的如果在生成中途就追加會導致 history 里出現(xiàn)被截斷的半句話。下一輪請求時這些殘句會被模型當成正常內容產生很怪異的影響。修復方式是引入一個 pending 緩沖class ContextManager: def start_streaming(self): self.pending_assistant def append_stream_chunk(self, chunk: str): self.pending_assistant chunk def finish_streaming(self): if self.pending_assistant: self.history.append(Message(roleassistant, contentself.pending_assistant)) self.pending_assistant self.trim_history(self.window_budget)只有完整接收完整個生成結果后才把它寫入 history。這個細節(jié)看起來簡單但它直接影響下一輪問答質量——畢竟拿別人說了一半的話當參考誰都會理解錯。6. 進階調優(yōu)模式嗅探、熱冷分層與可觀測性基礎版本跑通之后我還有三個方向在做持續(xù)優(yōu)化這里一并分享一下思路和已落地的優(yōu)化點。6.1 按問題類型動態(tài)選模式模式嗅探前面提到的狀態(tài)機是被動式切換——先積累到閾值再切?,F(xiàn)在我在嘗試更主動的方式在第一輪請求進入時就通過一個快速分類器預測該對話的會話深度預期直接決定初始模式。比如用戶說幫我把這段文字翻譯成英文這是一個典型的單次任務直接把會話置為STATELESS省去了切來切去的開銷。而幫我比較一下三款手機哪個適合打游戲天然攜帶多輪屬性直接就置為SLIDING_WINDOW加摘要預備。實現(xiàn)上我用了一個極輕量的意圖分類層本質上是一個幾千條規(guī)則加一個小的 embedding 分類器判別速度在 10ms 以內。規(guī)則部分的幾個典型特征包含為什么怎么樣具體說說 → 多輪意圖包含翻譯一下總結一下提取關鍵詞 → 單輪任務包含記住以后都我不喜歡 → 啟用 Memory 模式包含 對比比較哪個更好 → 啟用長窗口模式這層嗅探不需要很精確。它有 80% 的準確率就能幫系統(tǒng)省下大量無謂的模式切換成本。剩下 20% 的不準會由狀態(tài)機在運行中自動糾正。6.2 上下文熱度分層熱、溫、冷三層另一個顯著提升效果的優(yōu)化是上下文熱度分層。不再把歷史消息簡單地留 N 條而是分為三層熱層最近 5 輪完整保留精細到字。溫層更早的歷史抽取關鍵信息以要點列表保留。冷層已經存入 Memory Bank 的長期事實按需讀取。熱層的消息直接拼接到 request溫層的要點放在摘要前綴里冷層的記憶條目按當前問題相關性動態(tài)注入。這樣一個三層結構能同時照顧到短期對話的連貫性、中期信息的可回溯性、長期知識的持久性。我測試下來這個分層結構比單一滑動窗口 摘要的效果穩(wěn)定得多。而且它天然配合狀態(tài)機——熱層由滑動窗口管理溫層由摘要模式管理冷層由 Memory 模式管理每一層各司其職。6.3 可觀測性不看數據就調不好上下文做上下文管理最怕的就是感覺不對但說不清哪里不對。我建議從一開始就把可觀測性納入設計至少要記錄以下指標指標獲取方式用途每輪 token 消耗組裝 request 時精確統(tǒng)計定位預算超支點模式切換次數狀態(tài)機事件埋點發(fā)現(xiàn)抖動切換摘要命中率人工抽測評估壓縮質量Memory 讀取命中率Memory 查詢日志判斷記憶條目是否對回答有實際幫助上下文裁剪次數trim_history調用日志判斷窗口是否長期偏小這些指標上線之后你會發(fā)現(xiàn)很多玄學問題其實都是數據問題。比如之前我總覺得某個用戶的對話質量時好時壞查日志才發(fā)現(xiàn)——他每次對話都會在窗口邊緣被裁剪說明窗口對該類用戶來說偏小這人應該走摘要模式而不是滑動窗口。7. 一些實用的兜底建議在你決定照抄上面的方案之前有幾條我這個過來人想多說兩句的坑。第一不要為了支持上下文強行上多輪模式。很多需求其實是單輪任務硬上多輪反而會把歷史里的噪聲帶進來。我的經驗是能單輪解決的問題就不要讓狀態(tài)機增加復雜度。第二摘要壓縮不能做得太頻繁。壓縮本身要調一次大模型是有成本的。我見過有人每三輪就壓縮一次結果就是 ContextManager 變成調模型狂魔成本翻了 20 倍效果卻沒有明顯提升。壓縮的觸發(fā)閾值寧可調低一點、保守一點讓摘要晚一點出現(xiàn)、大一點概括。第三Memory Bank 里的記憶條目一定要帶時間戳和置信度。我一開始沒帶結果用戶后來明確說我不喜歡靠窗了以后訂中間的位置系統(tǒng)不知道該信哪條。加上時間戳后邏輯非常清楚后來寫入的覆蓋先前的且我們可以設置權威覆蓋標志——某些字段比如會員等級以業(yè)務系統(tǒng)數據為準模型抽取的記憶不能覆蓋。第四嚴格處理好流式輸出和異步寫入的并發(fā)問題。如果不加鎖或者緩沖區(qū)流式產生半截消息進入 history 的情況幾乎必然出現(xiàn)。這屬于那種不炸不知道一炸就摸不著頭腦的隱形 bug。第五給 ContextManager 加上 trace_id 貫穿日志。我見過太多人調試上下文問題時因為找不到日志鏈路而無從下手。每組裝一次 request就輸出一條 trace_id 加消息骨架結構的日志后面排查問題能省一半時間。這幾個建議談不上優(yōu)雅但拿它們去兜底基本能保證你的上下文管理系統(tǒng)不往失控的方向跑。用戶對對話質量的感知往往非常敏感——一旦模型說出一句我不記得你剛才說了什么提升十個點準確率換來的信任也會當場歸零所以寧可謹慎不要冒進。