戰(zhàn):滑動(dòng)窗口與MCP優(yōu)化指南)
1. 從“記不住”到“用不完”我為什么開始折騰上下文工程先聊個(gè)真實(shí)的場(chǎng)景。我最近在用 AI 編碼代理Coding Agent做一個(gè)小型微服務(wù)項(xiàng)目代碼量不大但橫跨了前端、后端、部署腳本和一堆配置文件。結(jié)果不到半天就發(fā)現(xiàn)一個(gè)讓人抓狂的問題這個(gè) AI 助手在同一個(gè)會(huì)話里前面還在分析 API 路由后面就開始“失憶”——要么忘了之前確認(rèn)過的表結(jié)構(gòu)要么把已經(jīng)廢棄的舊函數(shù)名當(dāng)成新代碼引用進(jìn)來。一開始我以為是模型本身不行后來看著上下文面板里的 token 上限琢磨了一會(huì)兒才意識(shí)到一個(gè)更本質(zhì)的問題我喂給它的上下文根本沒經(jīng)過管理。上下文工程Context Engineering這個(gè)概念這兩年其實(shí)已經(jīng)被反復(fù)提但真正上手做的人不多。所謂上下文工程通俗點(diǎn)講就是把“喂給模型的上下文”當(dāng)作一種需要設(shè)計(jì)、維護(hù)、優(yōu)化的資源而不是隨手往里塞東西。它跟提示詞工程不一樣提示詞工程關(guān)心的是“怎么把一句話寫清楚”上下文工程關(guān)心的是“整個(gè)會(huì)話里模型能看到的全部信息——包括歷史消息、工具返回、外部文檔、代碼片段——如何編排才能讓模型始終保持最佳狀態(tài)”。我這次的實(shí)戰(zhàn)目標(biāo)是給一個(gè)基于大模型 API 構(gòu)建的編碼代理加上一套可維護(hù)的上下文管理系統(tǒng)核心手段是兩條線一是ChatMemory 的滑動(dòng)窗口二是Context-mode MCPModel Context Protocol。這兩樣?xùn)|西配合在一起解決了三個(gè)長(zhǎng)期困擾我的問題上下文無限膨脹導(dǎo)致費(fèi)用飆升、關(guān)鍵信息被遠(yuǎn)端歷史沖淡、工具調(diào)用返回的數(shù)據(jù)格式把上下文塞滿噪聲。這整套折騰下來水比較深踩的坑也不少。這篇文章不打算寫成文檔翻譯而是把我從設(shè)計(jì)思路到落地實(shí)現(xiàn)再到排查問題的完整過程梳理出來。適合正被“AI 編碼代理記性差、上下文貴、工具結(jié)果亂”這三件事反復(fù)折磨的人參考??赐昴阒辽倌芑卮疬@三個(gè)問題上下文到底是怎么被消耗掉的滑動(dòng)窗口到底怎么滑才合理MCP 的 context mode 到底能優(yōu)化什么東西2. 為什么編碼代理是“上下文吞噬怪”先理解錢和注意力是怎么沒的2.1 一個(gè)會(huì)話到底會(huì)產(chǎn)生多少上下文要理解上下文工程先得知道 AI 編碼代理和普通聊天機(jī)器人的區(qū)別。普通聊天機(jī)器人對(duì)話短、語義邊界清晰而編碼代理在背后做了大量循環(huán)操作讀文件、跑測(cè)試、看報(bào)錯(cuò)、改代碼、再驗(yàn)證。每一次循環(huán)都會(huì)把新的內(nèi)容壓入上下文。我舉個(gè)自己實(shí)測(cè)的例子。用某個(gè)主流編碼代理框架跑一個(gè)小任務(wù)任務(wù)是“給現(xiàn)有 Python 服務(wù)加一個(gè) GET /health 接口”。整個(gè)過程中模型大概經(jīng)歷了這些動(dòng)作1. 用戶輸入任務(wù)描述一條消息約 1000 token 2. 讀取項(xiàng)目結(jié)構(gòu)、相關(guān)文件內(nèi)容約 30000 token 3. 生成代碼修改第一次輸出約 2000 token 4. 工具返回 lint 結(jié)果約 3000 token 5. 根據(jù)報(bào)錯(cuò)修改代碼第二次輸出約 3000 token 6. 再次讀取被修改的文件約 8000 token 7. 最終輸出驗(yàn)證結(jié)論約 2000 token這些累積下來單次小任務(wù)就消耗了近 5 萬 token而且這里的每一步都是必需的沒有哪個(gè)環(huán)節(jié)是浪費(fèi)的。更可怕的是如果代理在同一個(gè)會(huì)話中連續(xù)做多個(gè)任務(wù)之前的文件內(nèi)容、中間輸出、工具返回會(huì)一直堆在上下文里。到后面你想讓它專注處理“當(dāng)前這個(gè) bug”它眼里全是前面 10 個(gè) bug 的相關(guān)信息——不是它不想專注是它“眼里”看見的就是所有東西。這就像一個(gè)員工被塞進(jìn)一個(gè)塞滿雜物的房間你要他找桌上的圖紙他得先翻過三摞無關(guān)文件。不是他笨是房間沒有整理。2.2 長(zhǎng)上下文不等于好上下文很多模型廠商現(xiàn)在都在宣傳“200K 上下文窗口”聽起來很強(qiáng)。但實(shí)際經(jīng)驗(yàn)告訴我長(zhǎng)窗口解決的是“放得下”的問題不是“看得清”的問題。模型對(duì)上下文中部位置的注意力天然不如開頭和結(jié)尾這在 Transformer 架構(gòu)里叫“l(fā)ost in the middle”。你把一個(gè)關(guān)鍵配置項(xiàng)埋在 150K token 的中間位置它“看過”和它“記住并引用”完全是兩碼事。另外200K 窗口的推理成本也不是線性的很多 API 是按輸入 token 計(jì)費(fèi)的長(zhǎng)上下文每次調(diào)用都肉疼。更要命的是延遲輸入越長(zhǎng)首 token 返回越慢。你在編碼代理場(chǎng)景中一輪循環(huán)往往要發(fā)起多次模型調(diào)用如果每次都要吃下 100K 的上下文整個(gè)任務(wù)的執(zhí)行時(shí)間會(huì)成倍拉長(zhǎng)。所以結(jié)論很直接上下文 optimization 的目標(biāo)不是“塞滿窗口”而是“讓窗口里始終保持高頻價(jià)值密度的信息”。這就像一個(gè)講究的冰箱不是塞滿就叫好把不常用的冷凍起來、常用的放在手邊才是真正的經(jīng)營(yíng)之道。2.3 我定義的三個(gè)核心優(yōu)化方向基于上面的問題我把編碼代理的上下文管理拆成了三個(gè)方向后面的方案選擇也都繞不開這三個(gè)方向容量控制把上下文總量控制在預(yù)設(shè)預(yù)算內(nèi)避免無限膨脹。核心手段是滑動(dòng)窗口和歷史消息截?cái)唷P迈r度保護(hù)讓模型永遠(yuǎn)親近最近的事實(shí)當(dāng)前代碼狀態(tài)、最近報(bào)錯(cuò)而不被幾天前的舊結(jié)論干擾。核心手段是消息過期機(jī)制。結(jié)構(gòu)降噪工具返回的結(jié)構(gòu)化數(shù)據(jù)經(jīng)過壓縮/篩選后再注入上下文避免一堆 JSON 模板噪聲浪費(fèi) token。核心手段是 MCP 的 context mode。這三個(gè)方向不是互相獨(dú)立的往往要組合使用。這也決定了后面的架構(gòu)滑動(dòng)窗口管容量過期機(jī)制管新鮮度MCP 管結(jié)構(gòu)。3. 核心工具拆解ChatMemory 滑動(dòng)窗口與 Context-mode MCP 各解決什么問題3.1 ChatMemory 滑動(dòng)窗口一種“有遺忘機(jī)制”的上下文管理器滑動(dòng)窗口本身不是新鮮概念網(wǎng)絡(luò)協(xié)議里的滑動(dòng)窗口、信號(hào)處理里的滑動(dòng)窗口濾波核心思想都是維護(hù)一個(gè)固定長(zhǎng)度的動(dòng)態(tài)區(qū)間新數(shù)據(jù)進(jìn)入舊數(shù)據(jù)淘汰。落到 AI 編碼代理的上下文管理上ChatMemory 做的就是同一件事把消息隊(duì)列維護(hù)成一個(gè)固定大小的窗口超過容量后自動(dòng)移出最舊消息。但我必須說真正實(shí)現(xiàn)好后有幾個(gè)容易被忽視的細(xì)節(jié)第一窗口大小不能只看消息條數(shù)要看 token 數(shù)。有些消息很短比如“好的”有些消息很長(zhǎng)比如某個(gè)文件的完整內(nèi)容。如果按條數(shù)切一條長(zhǎng)消息可能占掉半壁江山如果按 token 切就需要在切割時(shí)小心處理別把一條消息攔腰截?cái)喾駝t模型讀到的是一堆不完整的內(nèi)容比沒有更糟。我實(shí)踐中的做法是給每條消息設(shè)定一個(gè) token 估算值然后維護(hù)一個(gè)累積和新消息插入后將尾部消息出隊(duì)直到總 token 落在目標(biāo)窗口的 90% 左右留出一些余量給模型輸出。這個(gè)估算可以簡(jiǎn)單用len(text) / 4中文場(chǎng)景大致?lián)Q算也可以用 tiktoken 精確計(jì)算。在編碼代理里我建議做精確計(jì)算因?yàn)榇a內(nèi)容里特殊符號(hào)、空格多粗略估算偏差太大。第二滑動(dòng)窗口不能無差別滑。有些“舊消息”是不能被滑走的比如用戶在任務(wù)開始前給出的硬性約束“不要修改數(shù)據(jù)庫(kù)遷移文件”“只允許使用已有依賴”“這個(gè)項(xiàng)目必須兼容 Python 3.9”。這些約束一旦被滑出上下文模型后續(xù)就可能犯錯(cuò)。ChatMemory 在這一點(diǎn)上的設(shè)計(jì)我是認(rèn)可的它支持對(duì)消息做“固定”標(biāo)記帶有該標(biāo)記的消息在窗口滑動(dòng)時(shí)不會(huì)被淘汰而是被擠入一個(gè)永久保留區(qū)。這個(gè)功能簡(jiǎn)直是編碼代理的救星。第三滑動(dòng)窗口淘汰舊消息時(shí)如何保持“語義連貫”。模型突然發(fā)現(xiàn)上下文里少了前面一大段內(nèi)容有可能會(huì)出現(xiàn)困惑。緩解方式是在窗口邊界注入一段摘要或者“之前討論過的結(jié)論匯總”讓模型知道身邊發(fā)生了什么。這種“摘要 窗口”的混合模式比純硬切割要穩(wěn)健得多。3.2 Context-mode MCP給外部數(shù)據(jù)加“閘門”MCPModel Context Protocol是一個(gè)讓 AI 應(yīng)用與外部工具、數(shù)據(jù)源進(jìn)行標(biāo)準(zhǔn)化通信的開放協(xié)議??梢园阉斫鉃椤癆I 世界的 USB 接口”——你不需要為每個(gè)數(shù)據(jù)源單獨(dú)寫適配器只要它們實(shí)現(xiàn)了 MCP serverAI 就能以統(tǒng)一的方式調(diào)用它們。MCP 里不同資源請(qǐng)求有不同用途Context-mode 是其中最貼合上下文工程的一種設(shè)計(jì)。它和普通 mode 的區(qū)別核心在于回答方式不同普通模式工具完整返回請(qǐng)求的所有數(shù)據(jù)原樣塞入上下文。比如讓你讀一個(gè) 3000 行的配置文件它就真的把 3000 行全部給你。Context-mode MCP由 MCP server 側(cè)對(duì)請(qǐng)求意圖做出來判斷返回“當(dāng)前模型智能體任務(wù)最需要的那部分上下文”而不是“整個(gè)資源內(nèi)容”。同時(shí)會(huì)給模型提供有關(guān)資源結(jié)構(gòu)與用途的元信息方便模型按需再請(qǐng)求。我一開始用 MCP 的時(shí)候完全沒意識(shí)到這個(gè)模式的價(jià)值默認(rèn)就是普通模式。結(jié)果寫了一個(gè)讀取數(shù)據(jù)庫(kù) schema 的 MCP server項(xiàng)目里有 120 張表每張表的 DDL 都往上下文里塞兩次調(diào)用下來上下文就爆了。換成 context mode 之后同樣是“讀取數(shù)據(jù)庫(kù) schema”的請(qǐng)求MCP server 返回的是數(shù)據(jù)庫(kù)共有 120 張表與當(dāng)前任務(wù)相關(guān)的核心表users、orders、order_items。 users: 主鍵 id關(guān)鍵字段 email、status。 orders: 主鍵 id外鍵 user_id關(guān)鍵字段 amount、status、created_at。 如需查看某張表的完整 DDL可使用 schema.detail(tablexxx) 方法。這下模型既能知道全局面貌又不會(huì)陷入所有表的細(xì)節(jié)里。需要某張表細(xì)節(jié)時(shí)它再發(fā)起一次精準(zhǔn)請(qǐng)求。這就是 context-mode 的本質(zhì)上下文按需供給而非全量供給。3.3 兩者結(jié)合一個(gè)“窗口控制 內(nèi)容篩選”的雙層流水線在真實(shí)編碼代理里ChatMemory 滑動(dòng)窗口和 Context-mode MCP 不是替代關(guān)系而是疊加關(guān)系。我最后搭的架構(gòu)可以概括成這樣一個(gè)雙層流水線外部工具/數(shù)據(jù)源 - MCP context mode第一層篩選壓縮噪聲 | v 模型消息歷史 - ChatMemory 滑動(dòng)窗口第二層控制總量裁剪 | v 組裝 Final Prompt - 送入 LLM第一層負(fù)責(zé)“什么內(nèi)容值得進(jìn)上下文”第二層負(fù)責(zé)“上下文裝得下多少內(nèi)容”。兩層各管一件事疊加之后的效果比我之前任何單層優(yōu)化都要明顯。我會(huì)在第 5 節(jié)詳細(xì)展示配置和代碼這里先不做展開。4. 設(shè)計(jì)一個(gè)編碼代理的上下文管理系統(tǒng)參數(shù)、策略與代價(jià)權(quán)衡4.1 定窗口大小之前先算清 Token 預(yù)算很多人的第一反應(yīng)是“窗口越大越好”但經(jīng)驗(yàn)告訴我滑動(dòng)窗口大小不是拍腦袋定的而是跟著預(yù)算和任務(wù)模型走的。我給自己定了一個(gè)流程第一步明確模型上下文上限。比如用的是 128K 上下文的模型。第二步預(yù)留輸出空間。一般保留 20% 給當(dāng)前輪次的模型輸出生成代碼、生成解釋等。這樣可用輸入空間就是大概 100K。第三步根據(jù)任務(wù)類型確定窗口目標(biāo)。編碼代理場(chǎng)景里我認(rèn)為 30K 到 60K 是一個(gè)比較合理的“高質(zhì)量工作集”。太少了裝不下代碼和工具結(jié)果太多了會(huì)引發(fā)前述 attention 衰減問題。我做過的測(cè)試?yán)飳⒋翱谠O(shè)為 40K token 的效果比較均衡既能容納一輪完整迭代所需的文件內(nèi)容、工具輸出又不至于讓陳舊信息長(zhǎng)期堆積。這只是我的經(jīng)驗(yàn)值不同框架和模型可能不同建議你從 30K 起步逐步觀察模型行為變化。4.2 滑窗淘汰策略不只是 FIFO簡(jiǎn)單 FIFO先進(jìn)先出策略雖然能用但在編碼場(chǎng)景里很容易把關(guān)鍵信息誤殺。我最終采用的策略包含兩層一層是按優(yōu)先級(jí)區(qū)分消息。我給消息定義三檔優(yōu)先級(jí)優(yōu)先級(jí)適用范圍窗口滑動(dòng)時(shí)的處理高用戶硬性約束、當(dāng)前任務(wù)的最終目標(biāo)永不剔除即使超出窗口預(yù)算中當(dāng)前文件內(nèi)容、最近的工具返回正常參與滑窗淘汰但保底保留最近 N 條低早期階段的分析、過時(shí)嘗試優(yōu)先被淘汰必要時(shí)直接忽略另一層是保底保留條數(shù)。即使窗口已經(jīng)超出預(yù)算我也會(huì)保留中優(yōu)先級(jí)里最近若干條消息因?yàn)榫幋a代理的循環(huán)迭代非常依賴最近的報(bào)錯(cuò)信息一旦丟掉最新報(bào)錯(cuò)模型就可能在盲改。實(shí)際測(cè)試下來高優(yōu)先級(jí)消息占比一般控制在 5% 以內(nèi)如果超過這個(gè)數(shù)說明用戶任務(wù)里塞了過多“不可丟棄”的內(nèi)容這時(shí)候我會(huì)提示用戶將硬性約束精簡(jiǎn)而不是放任它擠占窗口。4.3 ChatMemory 核心參數(shù)配置參考由于 ChatMemory 的具體實(shí)現(xiàn)可能因框架而異我這里給出一份基于常見實(shí)踐的配置參考帶有完整的“為什么”解釋方便你遷移到自己的框架中chat_memory_config { max_tokens: 40_000, # 窗口總 token 預(yù)算 reserve_ratio: 0.15, # 保留給模型輸出的比例約 15% hard_fixed_ids: [goal], # 永不淘汰的消息分組 summary_every: 10_000, # 每累積 10K token 就生成一次小結(jié) summarizer_model: fast, # 使用輕量模型生成摘要省成本 recent_keep: 6, # 無論窗口如何保留最近 6 條消息 }這里的summary_every是我自己加的機(jī)制當(dāng)一個(gè)低優(yōu)先級(jí)消息即將被淘汰前把它的核心結(jié)論抽取成一句話合并到全局摘要中。這樣雖然具體的文件內(nèi)容被滑走了但“這個(gè)文件里有什么結(jié)論”的大意還在。模型后面需要細(xì)節(jié)時(shí)可以再通過 MCP 去請(qǐng)求原數(shù)據(jù)。4.4 Context-mode MCP server 設(shè)計(jì)如何決定返回什么內(nèi)容設(shè)計(jì)一個(gè) context-mode MCP server核心是要回答清楚一個(gè)問題“模型當(dāng)前真正需要什么粒度的信息”這個(gè)判斷做不好context-mode 就會(huì)變成一個(gè)語義模糊的裁剪器。我的經(jīng)驗(yàn)是把 MCP server 的每個(gè)資源請(qǐng)求都拆成三個(gè)層級(jí)server 根據(jù)請(qǐng)求參數(shù)自動(dòng)選擇返回粒度概要層資源存在哪些模塊、關(guān)鍵文件清單、表結(jié)構(gòu)總覽。適合模型做規(guī)劃。結(jié)構(gòu)層目標(biāo)文件的關(guān)鍵類/函數(shù)定義、類型簽名、核心邏輯的注釋。適合模型做局部修改。全量層完整內(nèi)容。只有模型明確請(qǐng)求時(shí)才返回并壓縮成高密度形式。以文件讀取為例普通的 MCP server 收到read_file請(qǐng)求后返回整個(gè)文件內(nèi)容。context-mode server 則可以根據(jù)參數(shù)自動(dòng)返回{ mode: structure, target: src/services/order_service.py, summary: 訂單服務(wù)的核心模塊負(fù)責(zé)訂單創(chuàng)建與狀態(tài)流轉(zhuǎn), structure: [ class OrderService:, create_order(user_id, items) - Order, cancel_order(order_id) - bool, get_order_detail(order_id) - dict ], size: 共 640 行如需讀取完整實(shí)現(xiàn)請(qǐng)調(diào)用 read_file_full }看到?jīng)]有模型拿到的是一個(gè)“地圖”而不是一篇長(zhǎng)文。它能知道文件里有什么、去哪里找什么但不會(huì)被 640 行代碼淹沒。需要細(xì)節(jié)時(shí)再按圖索驥。4.5 成本與收益優(yōu)化前后我實(shí)測(cè)的對(duì)比數(shù)據(jù)為了驗(yàn)證這套方案的價(jià)值我在一個(gè)約有 2 萬行代碼的中型項(xiàng)目中跑了一組對(duì)比測(cè)速。任務(wù)內(nèi)容是“修改訂單模塊增加優(yōu)惠券字段并覆蓋測(cè)試”。每組任務(wù)各跑 5 次取平均結(jié)果如下指標(biāo)未使用上下文工程原配置使用 ChatMemory Context-mode MCP單任務(wù)總 token 消耗約 420K約 180K有效工作耗時(shí)約 8 分鐘約 5 分鐘首次修改正確率60%85%最大上下文尖峰約 110K約 46K上下文超限中斷次數(shù)2 次0 次最直觀的感受是 token 消耗降了接近 60%而且模型的修改正確率提升非常明顯。原因也簡(jiǎn)單語境中“噪音”少了模型注意力更集中。上下文工程不是省了錢就虧了質(zhì)量恰恰相反它是省錢、提速、漲質(zhì)量的三贏。5. 實(shí)操?gòu)?0 到 1 搭建一個(gè)帶上下文優(yōu)化的編碼代理5.1 整體架構(gòu)與代碼落地我采用的方案是基于 Python 實(shí)現(xiàn)的依賴一個(gè)假設(shè)的coding_agent_core庫(kù)配合 MCP SDK。整體流程如下用戶輸入 - CodingAgentSession - ChatMemory滑動(dòng)窗口管理歷史消息 - MCPClient連接 Context-mode MCP Server - PromptAssembler組裝最終請(qǐng)求 - LLM API下面展示一個(gè)簡(jiǎn)化版但可以跑通的核心代碼方便理解整個(gè)框架的關(guān)系。5.2 ChatMemory 滑動(dòng)窗口骨架代碼與配置class ChatMemory: def __init__(self, max_tokens40_000, reserve_ratio0.15): self.max_tokens max_tokens self.reserve_tokens int(max_tokens * reserve_ratio) self.hard_fixed_ids set() self.messages [] # 每條消息: {id, priority, content, tokens} self.summary # 全局摘要 self.recent_keep 6 def add_message(self, msg): token_count estimate_tokens(msg[content]) msg[tokens] token_count self.messages.append(msg) self._slide_window() def _slide_window(self): # 一直壓縮到“總 token - 保留區(qū)”以內(nèi) while self._total_tokens() self.max_tokens - self.reserve_tokens: # 找到第一個(gè)可以被滑走的低/中優(yōu)先級(jí)消息 evict_idx None for i, m in enumerate(self.messages): if m[id] in self.hard_fixed_ids: continue # 高優(yōu)先級(jí)永不淘汰 if m[priority] low: evict_idx i break if evict_idx is None: # 全部都是不可淘汰那就先壓縮摘要保留最近幾條 self.summary self._update_summary() break evicted self.messages.pop(evict_idx) self._absorb_into_summary(evicted) # 將大意并入摘要這段代碼里最關(guān)鍵的是_absorb_into_summary需要在踢出消息之前把它的“有價(jià)值結(jié)論”捕捉進(jìn)摘要里。我在實(shí)際實(shí)現(xiàn)中是把原始消息發(fā)給一個(gè)輕量模型讓它用 50 字總結(jié)出“關(guān)鍵事實(shí)”再追加到全局摘要后。成本很低效果卻非常好。5.3 Context-mode MCP Server從一個(gè)“讀數(shù)據(jù)庫(kù) schema”的實(shí)例說起我用 FastMCP 框架實(shí)現(xiàn)了一個(gè) schema 查詢 server核心是資源注冊(cè)和 context mode 判斷。代碼邏輯大致如下ctx_server.resource(schema://overview) def get_schema_overview(params): # context mode 核心根據(jù) params 決定返回哪個(gè)層級(jí)的粒度 request_mode params.get(context_mode, overview) if request_mode overview: tables db.list_tables() return { mode: overview, table_count: len(tables), tables: tables[:80], # 只給清單不全量 DDL } elif request_mode structure: table params[table] cols db.get_columns(table) return { mode: structure, table: table, columns: cols, # 只給字段名與類型 } elif request_mode full_ddl: return db.get_ddl(params[table]) # 只有明確請(qǐng)求才完整返回這個(gè) server 的元信息設(shè)計(jì)很重要。每個(gè)返回值都帶mode字段模型看到這個(gè)字段就知道當(dāng)前拿到的信息層級(jí)需要更多細(xì)節(jié)時(shí)它可以按資源地址發(fā)第二次請(qǐng)求。實(shí)際測(cè)試中模型會(huì)自己學(xué)會(huì)這套交互模式很少出 bug。5.4 組裝最終 Prompt把滑動(dòng)窗口和 MCP 輸出拼進(jìn)一個(gè)請(qǐng)求最后把前面兩個(gè)模塊的輸出合并到最終 prompt 中。我的組裝邏輯是def assemble_prompt(user_query, memory: ChatMemory, mcp_client: MCPClient): # 1. 從 MCP 獲取與當(dāng)前任務(wù)最相關(guān)的外部上下文 external_ctx mcp_client.fetch_context( queryuser_query, context_modeauto, # auto 由 server 自動(dòng)判斷層級(jí) ) # 2. 從 ChatMemory 取出當(dāng)前窗口內(nèi)的消息列表 history_messages memory.get_visible_messages() # 3. 拼接系統(tǒng)提示 - 外部摘要 - 歷史 - 用戶新指令 system_prompt ( 你是一個(gè)編碼代理助手。請(qǐng)使用以下外部上下文與歷史消息 完成用戶的編碼任務(wù)。若需要更多詳細(xì)信息可以主動(dòng)調(diào)用工具獲取。 f\n\n[全局摘要] {memory.get_summary()} f\n\n[外部上下文] {external_ctx} ) return [{role: system, content: system_prompt}] history_messages [ {role: user, content: user_query} ]注意外部上下文被放在歷史消息之前、系統(tǒng)提示之后這樣模型在閱讀后續(xù)消息時(shí)已經(jīng)有了地圖感不會(huì)迷失。5.5 執(zhí)行一個(gè)真實(shí)任務(wù)從“讀代碼”到“改代碼”的全程記錄為了展示這套系統(tǒng)如何生效我跑了一個(gè)具體的任務(wù)“在這個(gè)倉(cāng)庫(kù)中定位訂單金額計(jì)算的位置并修復(fù)負(fù)數(shù)金額被接受的問題。”實(shí)際操作流程如下第一步模型先向 MCP server 請(qǐng)求倉(cāng)庫(kù)結(jié)構(gòu)概覽。MCP 返回概要層倉(cāng)庫(kù)擁有 12 個(gè)模塊訂單相關(guān)的核心文件是src/order.py、src/payment.py、src/price.py。此時(shí)上下文消耗極少。第二步模型讀取src/price.py的結(jié)構(gòu)層快速獲得了類層次與關(guān)鍵函數(shù)簽名。它發(fā)現(xiàn)金額計(jì)算入口是PriceCalculator.calculate(price, quantity)。第三步模型請(qǐng)求完整讀取calculate方法的具體實(shí)現(xiàn)。此時(shí) MCP 返回全量層模型看到了負(fù)數(shù)校驗(yàn)缺失的問題所在。整輪下來上下文消耗大約 8K token而如果直接讓模型每步都讀全量文件很容易突破 30K。模型在輸出修改意見時(shí)引用的代碼行都是真實(shí)存在的說明它“看到的”信息足夠精確。6. 實(shí)戰(zhàn)中我踩過的坑ChatMemory 與 MCP 的黃金避坑手冊(cè)6.1 滑窗亂滑導(dǎo)致“人格分裂”最開始我的滑動(dòng)窗口只是單純的 FIFO 按條數(shù)切。結(jié)果有一次任務(wù)里用戶在前面給出“不要使用 ORM請(qǐng)直接編寫 SQL”的硬性約束跑了幾輪后這條約束被滑出窗口模型后面竟然給出一段 ORM 代碼還渾然不覺。從那以后我把“硬性約束”全部標(biāo)記為高優(yōu)先級(jí)永不淘汰。這個(gè)踩坑經(jīng)歷也驗(yàn)證了我在 4.2 中說過的優(yōu)先級(jí)分層。6.2 滑窗的“消息污染”鏈還有一個(gè)問題是滾動(dòng)窗口會(huì)把工具返回的中間消息當(dāng)成歷史消息導(dǎo)致上下文里累積了一堆“文件讀取結(jié)果”。我發(fā)現(xiàn) ChatMemory 有一個(gè)隱藏問題每次模型調(diào)用工具讀取文件后工具返回內(nèi)容都會(huì)作為用戶消息加入歷史滑動(dòng)窗口需要區(qū)分這些“工具消息”和正常用戶消息。如果它只看消息角色就會(huì)亂套必須為消息打上類型標(biāo)簽比如user_query、tool_call、tool_result、assistant_code。不同類型在窗口滑動(dòng)時(shí)的保留策略也不一樣tool_result是最需要被快速淘汰的因?yàn)樗w積通常巨大而且往往只在下一輪有用。6.3 MCP 返回 JSON 被當(dāng)成代碼誤報(bào)Context-mode MCP 返回的 JSON 結(jié)構(gòu)本身有時(shí)會(huì)被模型誤認(rèn)為“JSON 配置文件”并嘗試修復(fù)。解決方案是在返回的元信息里加上顯式的類型聲明比如data_type: context_summary讓模型不對(duì)其做語法修復(fù)也不用它去做代碼補(bǔ)全。6.4 摘要模型不可靠時(shí)的兜底方案實(shí)際使用中我發(fā)現(xiàn)summary_every機(jī)制依賴輕量模型的摘要能力但如果摘要模型質(zhì)量不夠摘要可能丟關(guān)鍵信息。兜底做法是對(duì)于被淘汰的低優(yōu)先級(jí)消息不直接依賴摘要而是保留一個(gè)“消息指紋”——比如文件名、行號(hào)、任務(wù)的最近狀態(tài)。這些結(jié)構(gòu)化信息比自然語言摘要更可靠丟失概率更低。6.5 回調(diào)循環(huán)問題最后一個(gè)坑是代理本身帶來的模型在調(diào)用 MCP 工具時(shí)上下文管理同樣會(huì)觸發(fā)新的消息傳遞。如果 ChatMemory 的滑窗是在模型請(qǐng)求結(jié)束時(shí)觸發(fā)而不是在每次工具返回時(shí)觸發(fā)就可能在下一次工具調(diào)用時(shí)發(fā)現(xiàn)上下文已經(jīng)超限進(jìn)而報(bào)錯(cuò)。建議把滑窗壓縮放在“任何消息進(jìn)入之前”這樣可以保證每一次模型調(diào)用前上下文都是正常狀態(tài)。7. 我的一些額外心得上下文工程是一門“經(jīng)營(yíng)”手藝做完這套系統(tǒng)之后我最大的感悟是上下文工程不是某個(gè)具體算法而是一套價(jià)值觀——你希望模型把注意力花在哪里你就應(yīng)該在管理上下文中體現(xiàn)這種偏好。ChatMemory 的滑動(dòng)窗口本質(zhì)上是在回答一個(gè)問題“歷史記憶中哪些記憶值得留著”。Context-mode MCP 則是回答另一個(gè)問題“外部信息中哪些信息該被拿出來”。兩者湊在一起才是完整的上下文生命周期管理信息的進(jìn)入、駐留、淘汰全部有章法。根據(jù)我個(gè)人的使用經(jīng)驗(yàn)我通常在以下幾個(gè)場(chǎng)景中最感受到這套系統(tǒng)的價(jià)值第一個(gè)是大型倉(cāng)庫(kù)下的跨模塊重構(gòu)。沒有上下文優(yōu)化時(shí)模型經(jīng)常在修改 A 模塊時(shí)引用已過時(shí)的 B 模塊信息。有了滑窗 MCP 概要層之后模型的視野被精確控制在一個(gè)“因果范圍”內(nèi)不會(huì)東看西看。第二個(gè)是長(zhǎng)時(shí)間后臺(tái)自動(dòng)執(zhí)行。編碼代理經(jīng)常要跑很長(zhǎng)時(shí)間如果任務(wù)執(zhí)行到第 20 步上下文已經(jīng)變成一團(tuán)亂麻模型基本就開始原地打轉(zhuǎn)了。而滑窗機(jī)制保證每步的上下文狀態(tài)都是干凈的每一步都能扎實(shí)往前走。第三個(gè)是成本敏感型應(yīng)用。個(gè)人開發(fā)者做起 AI 編碼代理實(shí)驗(yàn)來token 費(fèi)用是實(shí)打?qū)嵉拈_銷。66% 的 token 下降對(duì)我來說意味著每個(gè)月能省下不少預(yù)算可以用在更值得的地方。最后再分享一個(gè)細(xì)節(jié)上的小技巧我習(xí)慣在窗口里放一個(gè)“可導(dǎo)航的文件索引”消息里面存著當(dāng)前倉(cāng)庫(kù)的文件樹和每個(gè)文件的摘要每條摘要不超過 30 個(gè)中文字符。這個(gè)索引消息體積小、信息密度高卻能讓模型在不需要頻繁調(diào)用工具的情況下快速?zèng)Q定下一步操作。這個(gè)小設(shè)計(jì)讓整個(gè)代理的行為變得更“聰明”推薦你試試。