
1. 從“臃腫”到“精干”一次Agent上下文管理的實戰復盤最近在折騰一個基于LangChain的智能客服Agent項目遇到了一個典型問題隨著對話輪次增加上下文Context像滾雪球一樣越來越大。每次調用大模型都得把過去十幾輪甚至幾十輪的對話歷史一股腦塞進去Token消耗直線上升成本看著心疼。更糟的是我發現Agent的“記憶力”反而變差了——它開始混淆不同用戶的訴求甚至把幾天前的舊指令當作當前任務來執行出現了典型的“記憶亂竄”現象。這讓我意識到單純地堆疊歷史對話并不是構建長期記憶的好方法反而是一種資源浪費和性能負擔。于是我啟動了一個代號為“Agent Harness”的優化項目目標很明確給Agent的上下文“瘦身”。不是簡單地截斷或丟棄而是通過更智能的結構化與壓縮讓有限的Token承載更精準、更持久的記憶。經過幾輪迭代最終實現了Token用量下降約40%同時任務完成準確率和上下文關聯度顯著提升的效果。這個過程涉及對上下文本質的重新思考、多種工程化手段的嘗試以及記憶架構的設計。如果你也在為Agent的上下文膨脹和記憶混亂而頭疼希望這篇實戰復盤能給你帶來一些直接的思路和可操作的代碼。2. 診斷為什么你的Agent上下文會“虛胖”且“健忘”在動手優化之前我們必須先搞清楚問題出在哪里。Agent的上下文通常指的是為了完成當前任務需要提供給大模型LLM的所有相關信息。在對話系統中這幾乎就等于完整的對話歷史。這種設計簡單粗暴但隱藏著幾個致命缺陷。2.1 Token消耗的“元兇”冗余與低信息密度首先最直觀的問題是冗余。想象一下人類對話我們不會在每次開口前都把之前的對話全文復述一遍。但在大多數Agent實現中正是這么做的。用戶問“今天的天氣怎么樣”Agent回答“北京晴15-25度”。五分鐘后用戶又問“那明天呢”標準的做法是把第一輪問答完整地再次輸入。這里面“今天的天氣怎么樣”和“北京晴15-25度”對于回答明天天氣這個問題信息價值極低但它們卻占據了寶貴的Token。這種逐輪追加的模式使得上下文長度呈線性甚至更快的速度增長。其次是信息的低密度化。自然語言本身就有大量的冗余比如語氣詞、重復性描述、客套話等。在長上下文中這些低信息密度的內容會不斷累積稀釋了真正關鍵信息如用戶意圖、實體、決策點的濃度。大模型需要花費額外的“注意力”去處理這些噪音影響了核心任務的表現。2.2 記憶“亂竄”的根源缺乏結構化與隔離記憶混亂是另一個棘手問題。其根源在于平鋪直敘的對話歷史是一種“非結構化”的記憶。當Agent需要回答“我上次提到的那個項目進度如何”時它需要在冗長的文本中尋找“項目”這個關鍵詞并關聯到“上次”這個模糊的時間點。這個過程極易出錯可能關聯到錯誤用戶的項目或者錯誤時間點的項目。更深層的原因是缺乏記憶隔離。一個服務于多用戶的Agent其上下文在邏輯上應該是多個獨立“會話線程”的集合。但如果簡單地將所有對話拼接不同用戶的信息就會在Token序列中相互“污染”。模型可能會把用戶A對產品的抱怨錯誤地應用到用戶B的咨詢中。這就是為什么你的WorkBuddy一個假設的AI助手的記憶會“亂竄”——因為沒有在底層為不同用戶、不同會話建立清晰的隔離邊界。2.3 重新定義“上下文”從聊天記錄到知識圖譜因此優化的第一步是轉變思維上下文不等于聊天記錄。上下文應該是為完成當前任務所必需的、經過提煉的知識狀態。這引出了兩個關鍵概念工作記憶Working Memory處理當前任務直接需要的、高度活躍的信息。它應該是精簡、聚焦的。長期記憶Long-term Memory存儲歷史經驗、用戶畫像、領域知識等。它需要被結構化存儲并能按需精準檢索而非全量加載。我們的目標就是建立一套機制將原始的、冗長的對話流動態地轉化為“工作記憶”“按需檢索的長期記憶”的組合。這正是“上下文工程”的核心。3. 瘦身方案一無損壓縮與智能摘要直接對原始文本進行壓縮是減少Token最立竿見影的方法。這里有幾個層次的操作。3.1 對話輪次合并與關鍵信息提取不要逐條存儲用戶和AI的發言。我們可以按“話題”或“意圖”對對話進行分組和摘要。# 示例一個簡單的對話摘要函數 from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI def summarize_conversation_turn(user_input, ai_response, llm): 將一輪對話壓縮為一條結構化記錄。 prompt ChatPromptTemplate.from_messages([ (“system”, “你是一個高效的對話記錄員。請將以下一輪對話提煉成一條簡潔的記錄包含用戶核心意圖、AI回復的要點、以及涉及的關鍵實體如人名、地點、任務名。以JSON格式輸出。”), (“human”, “用戶: {user_input}\nAI: {ai_response}”) ]) chain prompt | llm result chain.invoke({“user_input”: user_input, “ai_response”: ai_response}) # 假設LLM返回了合法的JSON字符串 import json summary_record json.loads(result.content) return summary_record # 使用示例 llm ChatOpenAI(model“gpt-3.5-turbo”) record summarize_conversation_turn(“幫我訂一張明天北京飛上海的機票”, “好的已為您查詢。明天上午9點國航CA1501航班有余票經濟艙價格1200元。請問需要預訂嗎”, llm) print(record) # 輸出可能類似{“intent”: “預訂機票”, “user_query”: “明天北京飛上海”, “ai_action”: “查詢航班CA1501并報價”, “entities”: {“date”: “明天”, “departure”: “北京”, “arrival”: “上海”, “flight”: “CA1501”, “price”: “1200”}}通過這種方式原來需要幾十個Token的一輪對話被壓縮成了一個包含關鍵信息的JSON對象可能只需要十幾個Token。歷史對話不再是一堆文本而是一個結構化的記錄列表。注意摘要的粒度需要權衡。過于粗略會丟失細節如具體航班號過于詳細則壓縮效果不佳。通常針對任務類型設計不同的摘要模板。3.2 利用大模型自身的上下文窗口管理工具一些先進的大模型API或框架提供了原生的上下文管理功能。例如Anthropic的Claude Code或類似工具可能支持“上下文分層”或“標記重要片段”。雖然我們不能直接使用未經確認的工具但思路可以借鑒在發送請求前對長上下文進行分析識別出與當前問題最相關的段落并為其添加高權重標記同時將不相關的段落移至低優先級或排除在外。這需要利用LLM本身做一個輕量的預處理。# 概念性代碼相關性篩選 def filter_relevant_context(full_history, current_query, llm): 從完整歷史中篩選出與當前問題最相關的部分。 prompt f“”” 給定以下對話歷史和一個當前問題請從歷史中選出最直接相關的1-3輪對話。僅輸出所選對話的索引號從0開始用逗號分隔。 對話歷史 {full_history} 當前問題{current_query} “”” response llm.invoke(prompt) relevant_indices [int(i.strip()) for i in response.content.split(‘,’)] relevant_context “\n”.join([full_history[i] for i in relevant_indices]) return relevant_context這個方法可以將萬字符的上下文動態縮減到千字符以內顯著節省Token。4. 瘦身方案二結構化記憶與向量檢索如果說壓縮是“節流”那么建立結構化的長期記憶系統就是“開源”。它允許我們存儲海量信息但只在需要時精準提取徹底告別全量加載。4.1 構建記憶圖譜從文本到向量我們不再保存原始對話文本而是將其核心信息轉換為向量Embeddings存入向量數據庫如Chroma, Pinecone, Weaviate。這就是構建Agent的“長期記憶庫”。記憶單元設計每條記憶不應是一整段對話而是一個個細粒度的“事實”或“事件”。例如“用戶偏好靠窗座位”、“項目A的截止日期是2024-06-01”、“用戶曾反饋過登錄頁面加載慢”。每個單元包含內容事實的文本描述。元數據來源會話ID、用戶ID、時間戳、類型偏好、事實、任務等、重要性分數。向量內容的嵌入表示。存入向量庫當一輪對話被摘要和結構化后將其拆解成多個記憶單元生成向量并存入數據庫。from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings embeddings OpenAIEmbeddings() vectorstore Chroma(embedding_functionembeddings, persist_directory“./mem_db”) def create_memory_units(summary_record, session_id, user_id): units [] # 示例從摘要記錄中創建多個記憶單元 if “entities” in summary_record: for key, value in summary_record[“entities”].items(): content f“用戶提到了{key}: {value}” metadata {“session_id”: session_id, “user_id”: user_id, “type”: “entity”, “source”: “conversation”} # 這里可以更精細地處理比如“預訂機票”作為一個意圖單元 units.append({“content”: content, “metadata”: metadata}) # 將意圖也作為一個單元 intent_content f“用戶意圖是: {summary_record.get(‘intent’, ‘N/A’)}” units.append({“content”: intent_content, “metadata”: {“session_id”: session_id, “user_id”: user_id, “type”: “intent”}}) return units def store_memories(units, vectorstore): texts [u[“content”] for u in units] metadatas [u[“metadata”] for u in units] vectorstore.add_texts(textstexts, metadatasmetadatas)4.2 精準檢索讓記憶“隨用隨取”當新的用戶查詢到來時我們不再拼接全部歷史而是將當前查詢轉換為向量。在向量數據庫中嚴格限定檢索范圍例如通過元數據過濾session_id和user_id搜索最相關的K條記憶。將這些檢索到的記憶作為“補充上下文”或“長期記憶片段”與當前的“工作記憶”即最近一兩輪對話一起構成最終的提示詞。def retrieve_relevant_memories(query, vectorstore, session_id, user_id, k5): # 關鍵使用元數據過濾器實現記憶隔離 filter_dict {“session_id”: session_id, “user_id”: user_id} relevant_docs vectorstore.similarity_search(query, kk, filterfilter_dict) # 將檢索到的記憶文本組合起來 memory_context “\n”.join([doc.page_content for doc in relevant_docs]) return memory_context這種方法完美解決了“記憶亂竄”。因為檢索時通過session_id和user_id進行了強制隔離用戶A的記憶絕不會出現在用戶B的上下文中。同時由于只檢索相關的幾條記憶上下文的長度是可控的通常只有幾百個Token而不是幾千個。4.3 關于向量庫中詞義統一的思考在構建記憶單元時一個常見問題是像“上下文理解”和“語境推測”這類意思相近的詞需要統一嗎我的經驗是不需要強制統一但可以增強關聯。大模型的嵌入模型如OpenAI的text-embedding-3本身對同義詞和近義詞就有很好的語義捕捉能力。“上下文理解”和“語境推測”的向量在空間上應該是接近的。當你用其中一個查詢時很可能也會檢索到另一個。如果你追求極致的精確度可以做一些后處理同義詞擴展在檢索時不僅用原始查詢也用其同義詞進行查詢合并結果。知識圖譜關聯在元數據中記錄這兩個概念是相關的但這會引入額外的復雜度。對于大多數應用相信嵌入模型的能力就夠了。過度工程化反而可能引入新的問題。5. 瘦身方案三記憶的分層、衰減與更新人的記憶是會遺忘的Agent的記憶也應該有生命周期。一個只增不減的記憶庫最終會變得臃腫且充滿過時信息。5.1 設計三層記憶架構我借鑒了認知科學中的一些概念為Agent設計了一個簡單的三層記憶架構感官記憶/工作記憶保存當前正在處理的對話輪次最近1-3輪。信息鮮活但容量極小隨著對話推進快速被覆蓋或轉移到下一層。短期記憶存放近期活躍的、與當前會話高度相關的記憶單元通過向量檢索得到。這些是當前任務的“背景板”會話結束后其活性會逐漸降低。長期記憶向量數據庫中存儲的所有記憶單元。容量大但信息處于“休眠”狀態需要時通過檢索激活。在每次交互中Agent的上下文主要由“工作記憶”和“短期記憶”即檢索結果構成長期記憶庫是背后的支撐。5.2 實現記憶衰減與重要性加權不是所有記憶都同等重要。我們可以為每個記憶單元引入“重要性”分數和“最后訪問時間”。重要性可以在創建時由LLM初步判斷例如用戶明確說“這很重要”也可以根據后續被檢索到的頻率動態更新。高頻被檢索的記憶重要性提高。最后訪問時間每次該記憶被檢索到就更新這個時間戳。我們可以定期運行一個“記憶整理”后臺任務def cleanup_memories(vectorstore, decay_factor0.9, retention_threshold0.1): # 這是一個概念性函數實際實現可能需要遍歷所有記憶 # 模擬衰減邏輯新分數 舊重要性分數 * decay_factor # 如果分數低于retention_threshold則考慮歸檔或刪除 pass通過這種機制長期不用的、不重要的記憶會逐漸“淡出”從而控制記憶庫的總規模和質量。對于關鍵記憶如用戶的核心偏好可以通過手動標記或高頻訪問保持其高重要性避免被衰減。5.3 記憶的更新與沖突解決當新信息與舊記憶沖突時怎么辦例如用戶之前說“我喜歡藍色”現在又說“我其實更喜歡綠色”。時間戳優先最簡單的策略是相信最新的信息。在存儲新記憶“喜歡綠色”的同時可以降低舊記憶“喜歡藍色”的重要性分數或在元數據中標記其為“已過時”。事實融合對于更復雜的情況可以引入一個“記憶融合”步驟。當檢測到沖突時觸發一個LLM調用分析這兩條記憶生成一條更準確的新記憶例如“用戶對顏色的偏好可能從藍色轉變為了綠色或視上下文而定”然后替換或補充舊記憶。6. 工程集成打造Agent Harness框架將上述所有策略組合起來就需要一個輕量的框架來“駕馭”HarnessAgent的上下文和記憶。這個框架負責在每次調用LLM前智能地組裝上下文。6.1 上下文組裝流水線我設計了一個處理流水線它的工作流程如下輸入當前用戶查詢 用戶ID 會話ID。檢索長期記憶用當前查詢去向量庫檢索嚴格過濾user_id和session_id得到N條相關記憶。獲取工作記憶從緩存或數據庫中取出最近2-3輪原始對話或它們的摘要。生成摘要/壓縮可選如果工作記憶較長可以對其進行一輪壓縮摘要。組裝最終提示將系統指令、檢索到的記憶、壓縮后的工作記憶、當前查詢按順序組合成最終的提示詞。調用LLM發送組裝好的提示詞獲得回復。更新記憶將本輪交互的核心信息創建為新的記憶單元存入向量庫。同時更新工作記憶緩存。class AgentHarness: def __init__(self, llm, vectorstore, memory_manager): self.llm llm self.vectorstore vectorstore self.memory_manager memory_manager # 負責記憶的存儲、檢索、衰減 def process_query(self, user_query, user_id, session_id): # 1. 檢索相關長期記憶 long_term_memories self.memory_manager.retrieve(user_query, user_id, session_id) # 2. 獲取工作記憶最近對話 working_memory self.memory_manager.get_working_memory(session_id) # 3. (可選)壓縮工作記憶 if len(working_memory) 3: # 假設超過3輪則壓縮 working_memory self._compress_working_memory(working_memory) # 4. 組裝提示詞 prompt self._assemble_prompt(system_msg, long_term_memories, working_memory, user_query) # 5. 調用LLM response self.llm.invoke(prompt) # 6. 更新記憶 self.memory_manager.store_turn(user_id, session_id, user_query, response.content) return response.content6.2 關鍵配置參數與調優在實際使用中有幾個參數對效果影響巨大需要仔細調優檢索數量 (k)每次從向量庫取多少條記憶太少可能信息不全太多則引入噪音。通常從3-5條開始測試。工作記憶長度保留最近幾輪原始對話對于需要緊密跟隨上下文的任務如代碼調試可能需要3-5輪對于開放式聊天1-2輪可能就夠了。記憶衰減因子決定舊記憶被遺忘的速度。需要根據業務場景調整。摘要的粒度摘要得太粗會丟失細節太細則壓縮效果差。可能需要為不同類型的對話設計不同的摘要模板。我的經驗是建立一個簡單的評估體系在測試集上同時監控Token消耗、任務完成準確率、以及人工評估的“記憶相關性”。通過A/B測試找到最適合你場景的參數組合。7. 實戰效果與避坑指南經過上述改造我的智能客服Agent煥然一新。最明顯的效果是平均每次API調用的Token輸入量下降了約40%因為上下文從完整的“史記”變成了精煉的“摘要”加“相關史料”。成本壓力驟減。更驚喜的是效果提升。由于記憶檢索是精準的、隔離的Agent混淆用戶和任務的情況基本消失。當用戶問起“我上次咨詢的那個問題”時Agent能準確找到對應會話中的相關記憶而不是胡亂抓取一個相似話題。任務完成的準確率提升了約15%。當然踩坑是必不可少的摘要失真早期使用的摘要Prompt不夠好導致關鍵信息如訂單號、具體時間丟失。對策設計Prompt時明確列出必須保留的信息類型。最好能用一批樣本進行測試檢查摘要的保真度。向量檢索不準當用戶使用模糊指代如“那個”、“這東西”時直接檢索效果很差。對策在檢索前先用LLM對當前查詢進行一次“查詢重寫”將其擴展成更完整、包含更多實體信息的描述再用這個描述去檢索。記憶更新延遲存入向量庫的記憶并非立即可用于下一次檢索取決于向量化的批次和索引更新策略。對策對于當前會話中剛產生的、極其重要的信息可以同時放在工作記憶和一份“臨時記憶緩存”中確保下一輪對話能立刻用到。系統復雜度增加引入了向量數據庫、記憶管理模塊系統架構變復雜了。對策做好模塊化設計并將記憶相關的操作封裝成獨立服務便于維護和升級。這次“瘦身”實踐讓我深刻體會到對于AI Agent而言“更多”并不總是意味著“更好”。尤其是在上下文窗口有限、Token成本高昂的現實約束下如何高效、智能地組織和使用信息比盲目地堆砌信息更重要。通過結構化記憶、精準檢索和動態管理我們完全可以讓Agent在更小的“腦容量”下表現出更強大的“記憶力”和“專注力”。這不僅僅是成本的優化更是Agent智能體健壯性和實用性的關鍵一步。