
在前面的文章中我們已經(jīng)理解了 Agent 中的 Message、Tool 以及長、短期記憶。這些內(nèi)容分別用于解決不同的問題當(dāng) Agent 真正運行起來時它們最終都會匯到同一個地方模型的上下文。模型這一輪應(yīng)該看到哪些 Message哪些 Tool Result 還需要保留長期記憶要不要讀取知識庫檢索出的內(nèi)容應(yīng)該放多少運行時信息又該以什么方式加入這些問題共同構(gòu)成了上下文工程Context Engineering要解決的事情根據(jù)當(dāng)前任務(wù)從不同的信息來源中選擇、整理和補充內(nèi)容為每一次模型調(diào)用準(zhǔn)備合適的上下文。一、什么是上下文對于大模型來說上下文Context可以簡單理解為模型在生成當(dāng)前這次回答之前能夠看到的全部信息。例如用戶發(fā)送幫我分析一下這個接口為什么返回 401。如果模型只看到這一句話它幾乎無法判斷問題出在哪里。但如果同時看到實現(xiàn)該接口的代碼 項目的認(rèn)證配置 請求 Header 錯誤日志 之前的相關(guān)對話它才有足夠的信息進(jìn)行分析。這些內(nèi)容共同構(gòu)成了模型當(dāng)前的上下文。在普通 LLM 調(diào)用中上下文通常比較簡單可能只有System Prompt 用戶輸入而在 Agent 中情況就復(fù)雜一些。一次模型調(diào)用看到的內(nèi)容可能包括System Prompt 歷史 Message 當(dāng)前用戶請求 Tool 定義 Tool Result 短期記憶 長期記憶 RAG 檢索結(jié)果 運行時信息而且這些內(nèi)容會隨著 Agent 的運行不斷變化。所以上下文并不是某一段 Prompt也不是單指聊天記錄。只要某段信息最終進(jìn)入了當(dāng)前這次模型調(diào)用它就是模型上下文的一部分。理解了這一點我們再來看什么是上下文工程。二、上下文工程是什么以前做大模型應(yīng)用時經(jīng)常會討論 Prompt Engineering。它主要解決的問題是Prompt 應(yīng)該怎么寫模型才能更準(zhǔn)確地理解任務(wù)。例如補充角色、目標(biāo)、約束條件、Few-shot 示例以及輸出格式。對于簡單的模型調(diào)用這往往已經(jīng)夠用了。但 Agent 不一樣。Agent 會持續(xù)對話、調(diào)用工具、讀取記憶、檢索知識庫還可能根據(jù)運行狀態(tài)動態(tài)改變自己的行為。所以真正需要考慮的問題已經(jīng)從Prompt 應(yīng)該怎么寫變成了這一輪調(diào)用模型時 到底應(yīng)該給它哪些信息這就是上下文工程??梢园阉斫獬筛鶕?jù)當(dāng)前任務(wù)從各種信息來源中挑選、整理和補充內(nèi)容組成這一輪模型真正需要的上下文。對比 Prompt Engineering 和 Context Engineering它們的區(qū)別如下表維度Prompt EngineeringContext Engineering主要對象Prompt整個模型輸入處理內(nèi)容指令、示例、格式Prompt、Message、Tool、Memory、RAG 等是否動態(tài)通常比較固定經(jīng)常隨運行過程變化主要目標(biāo)把任務(wù)說清楚把當(dāng)前需要的信息準(zhǔn)備好需要說明的是Prompt Engineering 仍然是重要的。只是到了 Agent 中它只是上下文工程的一部分。三、上下文從哪里來要管理上下文先要知道 Agent 每一次調(diào)用模型時信息可能從哪里來。在前面的多篇文章中我們多次提到 Agent 的運行過程調(diào)用模型 ↓ 判斷是否調(diào)用 Tool ↓ 執(zhí)行 Tool ↓ 返回 Tool Result ↓ 再次調(diào)用模型這個過程會重復(fù)進(jìn)行直到模型生成最終結(jié)果。在這期間模型每一次拿到的上下文都可能不同。System PromptSystem Prompt 用來說明 Agent 的基本職責(zé)和行為要求。例如你是一個企業(yè)知識庫助手。 回答公司制度問題時優(yōu)先查詢內(nèi)部知識庫。 資料不足時不要自行補充事實。這類內(nèi)容通常比較穩(wěn)定。但在實際項目中System Prompt 也可能動態(tài)變化。例如管理員和普通員工看到的操作權(quán)限不同那么 Agent 可以根據(jù)當(dāng)前用戶身份在模型調(diào)用前臨時加入不同的說明。所以 System Prompt 不一定要在應(yīng)用啟動時全部寫死。當(dāng)然如沒有特殊目的不建議動態(tài)調(diào)整 System Prompt。MessageMessage 是當(dāng)前 Thread 中已經(jīng)發(fā)生過的對話和執(zhí)行記錄。常見的消息包括HumanMessage AIMessage ToolMessage例如用戶查詢訂單 10001 AI我?guī)湍悴橐幌?Tool訂單狀態(tài)為已發(fā)貨 AI訂單已經(jīng)發(fā)貨這些消息會成為后續(xù)模型調(diào)用的重要上下文。對話持續(xù)得越久Message History 通常也會越來越長。Tool ResultAgent 調(diào)用工具后工具返回的數(shù)據(jù)通常也會進(jìn)入后續(xù)模型調(diào)用。這里需要注意工具返回的數(shù)據(jù)會極大影響上下文的長度。例如搜索工具返回了十幾 KB 的網(wǎng)頁內(nèi)容數(shù)據(jù)庫工具一次查回幾百條記錄這些內(nèi)容都可能繼續(xù)留在上下文里。所以很多 Agent 后期 Token 增長很快并不只是因為聊天記錄太長還有一個常見原因Tool Result 太多、太大。Runtime Context運行時上下文Runtime Context保存的是程序本次運行需要使用的信息例如user_id 用戶角色 租戶 ID 權(quán)限 數(shù)據(jù)庫連接 API Client 環(huán)境配置這類信息不一定需要直接給模型看。例如數(shù)據(jù)庫連接對象只是 Tool 執(zhí)行數(shù)據(jù)庫查詢時需要模型根本不需要知道它的具體內(nèi)容。所以 Runtime Context 更像程序運行時的依賴。需要時Middleware 或 Tool 可以讀取它再決定哪些信息需要轉(zhuǎn)換成模型能夠使用的上下文。State 與 StoreState 保存當(dāng)前 Thread 的狀態(tài)也是短期記憶的重要載體。例如當(dāng)前 Message 當(dāng)前任務(wù)狀態(tài) 已經(jīng)執(zhí)行過哪些步驟 中間結(jié)果Store 更適合保存跨 Thread 的長期信息例如用戶偏好 歷史事實 長期配置 用戶畫像數(shù)據(jù)放在哪里是一回事。這一輪要不要給模型看是另一回事。上下文工程的核心就是決定上述這些數(shù)據(jù)中哪些應(yīng)該在當(dāng)前任務(wù)中提供給模型。四、為什么不能什么都給理論上來說我們剛才提到的那些數(shù)據(jù)都有用因此很容易形成一種思路既然這些信息以后可能有用那就都給模型。剛開始這樣做確實很省事。但系統(tǒng)稍微復(fù)雜以后問題就會越來越明顯。最直接的是 Context Window 有長度限制。Message、檢索文檔和 Tool Result 一直增加最終可能超過模型能夠接受的最大輸入長度。但即使沒有達(dá)到這個上限上下文過多也不一定有幫助。例如用戶問幫我看看這個接口為什么返回 401。模型真正需要的可能只有接口代碼 認(rèn)證方式 請求 Header 最近的錯誤日志 相關(guān)配置如果同時再給它三天前討論過的數(shù)據(jù)庫設(shè)計 完整項目 README 幾十個 Tool 定義 完整用戶畫像 以前的所有搜索結(jié)果這些東西雖然都屬于同一個項目但對當(dāng)前問題幫助不大。它們反而會增加輸入 Token 響應(yīng)時間 無關(guān)信息干擾 工具選擇難度所以上下文工程不是簡單地解決“窗口不夠大”。它要解決的是模型這一輪判斷時到底需要知道多少。即使模型支持很大的 Context Window也沒有必要把所有能找到的信息都塞進(jìn)去。窗口更大只代表可以放更多東西并不代表應(yīng)該放更多東西。五、上下文怎么控制實際開發(fā)中上下文管理常見的做法主要有選擇、裁剪、摘要和動態(tài)注入。選擇最理想的方式不是先把所有內(nèi)容放進(jìn)去再刪除而是一開始就只取當(dāng)前需要的信息。例如一個企業(yè) Agent 有 40 個 Tool。如果用戶現(xiàn)在只是查詢訂單那么這一輪可能只需要訂單查詢 物流查詢 退款查詢財務(wù)報表、代碼倉庫、服務(wù)器運維等 Tool沒有必要一起暴露給模型。Tool 本身也會占用上下文。而且 Tool 越多模型需要做的選擇越復(fù)雜選錯工具的可能性也會增加。Memory 和 RAG 也是同樣的道理不是讀取全部記憶 而是讀取相關(guān)記憶 不是加載整個知識庫 而是檢索相關(guān)文檔 不是保留全部歷史 而是保留當(dāng)前任務(wù)需要的歷史裁剪對于 Message History最簡單的方法是直接裁剪。例如只保留最近 20 條 Message或者只保留最近 8000 Token 的歷史這種方式實現(xiàn)簡單也不會多產(chǎn)生一次模型調(diào)用。問題也很明顯被刪掉的信息當(dāng)前這一輪就看不到了。所以它更適合那些主要依賴近期對話的場景。摘要如果舊消息不能直接刪除就可以把較早的內(nèi)容壓縮成摘要。例如前 40 條 Message ↓ 一段會話摘要 最近 10 條原始 MessageLangChain 提供了SummarizationMiddleware來處理這類長對話。from langchain.agents import create_agent from langchain.agents.middleware import SummarizationMiddleware agent create_agent( modelgpt-5.5, tools[], middleware[ SummarizationMiddleware( modelgpt-5.4-mini, trigger(tokens, 4000), keep(messages, 20), ) ], )運行結(jié)果歷史消息超過設(shè)定閾值后 較早消息 → 壓縮成摘要 最近消息 → 保留原文 后續(xù)調(diào)用 → 使用“摘要 最近消息”這里要區(qū)分兩種處理方式。如果只是模型調(diào)用前臨時刪除一些 Message影響的只是這一次調(diào)用。State 中原來的消息仍然可以保留。而SummarizationMiddleware會更新 State 中的消息歷史所以后續(xù)模型調(diào)用看到的也是壓縮后的結(jié)果。雖然都能減少上下文但兩者對后續(xù)運行的影響并不一樣。六、上下文可以按需生成真實系統(tǒng)里還有很多信息并不適合長期寫在 System Prompt 中。例如當(dāng)前用戶角色 用戶回答偏好 當(dāng)前上傳的文件 任務(wù)執(zhí)行階段 業(yè)務(wù)權(quán)限 當(dāng)前環(huán)境這些信息每一次運行都可能不同。更合適的方式是在模型調(diào)用之前根據(jù)當(dāng)前狀態(tài)臨時生成。LangChain 可以通過 Middleware 或dynamic_prompt讀取 State、Store 和 Runtime Context再構(gòu)造這一輪需要的 System Prompt。假設(shè) Store 中保存了用戶的回答偏好而 Runtime Context 中提供當(dāng)前user_id。from dataclasses import dataclass from langchain.agents import create_agent from langchain.agents.middleware import dynamic_prompt, ModelRequest from langgraph.store.memory import InMemoryStore dataclass class UserContext: user_id: str store InMemoryStore() store.put( (preferences,), user-001, { communication_style: concise, }, ) dynamic_prompt def build_system_prompt(request: ModelRequest) - str: user_id request.runtime.context.user_id preference request.runtime.store.get( (preferences,), user_id, ) prompt You are a technical assistant. if preference: style preference.value.get( communication_style, balanced, ) if style concise: prompt \nKeep answers concise and avoid unnecessary background. return prompt agent create_agent( modelgpt-5.5, tools[], middleware[build_system_prompt], context_schemaUserContext, storestore, ) result agent.invoke( { messages: [ { role: user, content: 解釋一下 HTTP 401 和 403 的區(qū)別。, } ] }, contextUserContext(user_iduser-001), ) print(result[messages][-1].content)運行結(jié)果大致如下401 表示身份認(rèn)證失敗或尚未完成認(rèn)證 例如 Token 缺失、無效或已經(jīng)過期。 403 表示服務(wù)器已經(jīng)識別用戶身份 但當(dāng)前用戶沒有訪問這個資源的權(quán)限。這里沒有把整個用戶畫像放進(jìn)模型。Middleware 先通過user_id找到用戶偏好再把回答盡量簡潔這一條真正有用的信息加入當(dāng)前 System Prompt。如果下一個用戶偏好詳細(xì)解釋那么下一次調(diào)用生成的 Prompt 又可以不同。這樣一來上下文不再是一份長期不變的模板而是根據(jù)當(dāng)前任務(wù)臨時組裝。七、Memory 和 RAG 怎么參與從上下文工程的角度看它們其實都是信息來源只是解決的問題不同。短期記憶關(guān)心的是這個 Thread 前面發(fā)生過什么例如用戶剛才說了什么 Agent 調(diào)用了哪些工具 任務(wù)已經(jīng)做到哪一步LangChain 中這些內(nèi)容通常保存在 State并可以通過 Checkpointer 持久化。長期記憶關(guān)心的是換一個 Thread 以后還應(yīng)該記住什么例如用戶喜歡簡潔回答 用戶所在部門 以前確認(rèn)過的偏好 長期保存的用戶信息這類數(shù)據(jù)可以保存在 Store 中需要時再讀取。RAG 解決的是回答當(dāng)前問題需要查哪些外部資料例如公司制度 產(chǎn)品文檔 技術(shù)手冊 數(shù)據(jù)庫記錄 網(wǎng)頁內(nèi)容于是一個 Agent 完全可以同時使用三種來源State ↓ 當(dāng)前會話的信息 Store ↓ 跨會話保存的信息 Retriever ↓ 當(dāng)前問題需要的外部資料 ↓ 篩選和整理 ↓ LLM重要的原則依然是不能因為這些信息都有用就全部取出來。實際開發(fā)中更常見的做法是按需分批獲取。例如只取最近幾輪聊天記錄而不是全部歷史用戶記憶按活躍度或相關(guān)性篩選RAG 文檔限制在 Top 5工具結(jié)果僅保留最近一次調(diào)用。也可以通過設(shè)置上下文窗口預(yù)算為不同類型信息分配額度超出的部分丟棄或壓縮。這樣做能避免上下文失控。八、Middleware 怎么管理理解到這里再看 Middleware 就比較容易了。create_agent構(gòu)建在 LangGraph 之上Middleware 可以插入 Agent 的運行過程在模型調(diào)用和 Tool 調(diào)用前后處理數(shù)據(jù)。模型調(diào)用之前可以做修改 System Prompt 篩選 Message 讀取長期記憶 選擇 Tool 選擇 Model 設(shè)置 Response FormatTool 執(zhí)行之后也可以做整理 Tool Result 更新 State 寫入 Store 提取需要長期保存的信息對話越來越長時還可以摘要舊消息 清理歷史 Tool Result 限制上下文大小因此一個稍完整的 Agent 可以形成這樣的結(jié)構(gòu)State / Store / Runtime Context / Retriever ↓ Middleware ↓ Prompt / Message / Tools ↓ LLMMiddleware 更適合處理的是“模型調(diào)用前后怎么整理信息”而不是負(fù)責(zé)保存所有信息。例如 Store 中有幾十條用戶記憶。Middleware 可以先找到與當(dāng)前問題有關(guān)的兩三條再放進(jìn) Prompt。一個 Agent 有幾十個 Tool也可以根據(jù)當(dāng)前任務(wù)只把真正需要的工具交給模型。如果這些邏輯散落在每一個業(yè)務(wù)調(diào)用中很快就會變得難以維護(hù)。放到 Middleware 中統(tǒng)一處理會清晰很多。九、多 Agent 中的上下文當(dāng)一個 Agent 要做的事情越來越多上下文往往也會越來越復(fù)雜。例如一個研究任務(wù)可能需要搜索網(wǎng)頁 讀取十幾個頁面 分析內(nèi)容 比較多個來源 整理結(jié)論如果這些步驟全部發(fā)生在主 Agent 中那么搜索結(jié)果、網(wǎng)頁正文、工具調(diào)用記錄都會不斷進(jìn)入主 Agent 的 Message History。等真正開始寫答案時上下文中已經(jīng)堆滿了大量中間過程。這時 Multi-Agent 有一個很實用的價值把不同任務(wù)的上下文分開。例如Main Agent │ ├── Research Agent │ ├── Search │ ├── Read │ ├── Analyze │ └── Compare │ ←── 研究結(jié)果Research Agent 可以在自己的上下文里完成搜索和分析。主 Agent 不需要看到幾十次 Tool 調(diào)用只需要拿到最后整理好的研究結(jié)果。LangChain 的 Subagents 模式就是這種思路。不過上下文隔離不等于什么都不傳。真正需要考慮的是主 Agent 給 Subagent 什么 Subagent 自己保留什么 最后返回給主 Agent 什么如果主 Agent 把完整 Message History 原樣傳給所有 Subagent隔離的意義就小了。反過來如果只給一句幫我研究這個問題Subagent 又可能缺少完成任務(wù)所需的背景。所以 Multi-Agent 設(shè)計中一個很重要的問題就是上下文應(yīng)該在哪里斷開又應(yīng)該在哪里傳遞。十、實際開發(fā)中的選擇上下文工程沒有固定模板。不同 Agent 需要的信息完全不同但有一些做法比較實用。第一先從簡單上下文開始。不要一上來就同時加入長期記憶 自動摘要 動態(tài) Prompt 十幾個 Middleware Multi-Agent 復(fù)雜 RAG先把最基本的 Agent 跑通。模型缺什么再補什么上下文真的變長了再做裁剪和摘要。第二區(qū)分“系統(tǒng)里有什么”和“模型看到什么”。數(shù)據(jù)庫可以保存十萬條歷史記錄但當(dāng)前模型調(diào)用可能只需要三條。Store 負(fù)責(zé)保存。Retriever 或業(yè)務(wù)代碼負(fù)責(zé)查找。Middleware 再決定怎樣交給模型。第三Tool Result 盡量只保留后續(xù)推理真正需要的數(shù)據(jù)。例如數(shù)據(jù)庫查出了 500 行記錄但模型只需要總數(shù) 最近 5 條 幾個統(tǒng)計值那就應(yīng)該在 Tool 內(nèi)先整理而不是直接返回完整 JSON。第四長期信息盡量按需加載。姓名、語言、固定回答偏好這類經(jīng)常使用的信息可以動態(tài)加入 Prompt。歷史訂單、以前討論的問題、大量用戶事實則更適合需要時再查。第五不要只看 Token。實際運行中還應(yīng)該一起觀察輸入 Token 響應(yīng)時間 Tool 調(diào)用次數(shù) 任務(wù)成功率 工具誤選情況 回答準(zhǔn)確性把上下文壓得太短也可能導(dǎo)致模型缺少必要信息??偨Y(jié)上下文工程不是獨立于 Agent 的模塊而是設(shè)計 Agent 輸入信息的方式。對模型而言上下文就是這一輪調(diào)用中它能看到的全部內(nèi)容包括 System Prompt、Message、Tool Result、短期與長期記憶、RAG 檢索結(jié)果及部分運行時信息。核心問題不是“讓模型看到更多”而是“這一輪它應(yīng)該看到什么”。State 記錄當(dāng)前會話Store 保存跨會話信息Retriever 查找外部知識Middleware 在模型調(diào)用前進(jìn)行篩選、裁剪、摘要和注入。多 Agent 同理拆開任務(wù)可避免中間信息堆滿 Context Window。開發(fā)時可先檢查每次調(diào)用帶了哪些 Message、Tool Result 返回了多少內(nèi)容。上下文工程是讓 Agent 在需要時剛好拿到所需信息。