:從能跑到穩(wěn)定跑的架構設計與避坑指南)
1. 為什么“能跑”和“穩(wěn)定跑”之間隔著一整個 Harness 工程我最早接觸 Agent 開發(fā)的時候和大多數(shù)人一樣覺得這東西的核心就是提示詞。把 System Prompt 寫得足夠細把工具描述寫得足夠清楚模型就能乖乖干活。結果第一次把它放到真實業(yè)務里跑當天就翻車了模型在第三步突然忘記了自己要干什么開始胡言亂語工具調用返回了一個超時錯誤整個鏈路直接崩掉沒有任何重試上下文越堆越長到后面模型開始重復之前已經(jīng)執(zhí)行過的操作。那一刻我才意識到Agent 的瓶頸從來不在模型本身而在于包裹在模型外面的那一層工程結構。這層結構業(yè)內現(xiàn)在越來越多地用一個詞來指代——Harness。你可以把它理解成“馬具”或者“線束”模型是那匹有力氣但方向感不穩(wěn)定的馬Harness 就是套在它身上、約束它、引導它、保護它、讓它能穩(wěn)定拉車的那一整套裝置。沒有 Harness 的 Agent就是一個能說會道但隨時可能失控的聊天機器人有了 Harness它才變成一個可以交付、可以運維、可以扛住真實流量的系統(tǒng)。這篇文章我想聊的就是這套 Harness 工程到底包含什么、每個部分為什么這么設計、實際落地時哪些坑必須提前踩過。適合已經(jīng)寫過一兩個 Demo、準備把 Agent 推向生產(chǎn)環(huán)境的開發(fā)者也適合正在做 Agent 架構選型的技術負責人。我不會只講概念會把參數(shù)怎么定、重試怎么做、上下文怎么裁剪、狀態(tài)怎么持久化這些實操細節(jié)都攤開講。Harness 和 Agent 的區(qū)別本質上就是“能不能穩(wěn)定交付”的區(qū)別這也是我踩了無數(shù)坑之后最深的體會。2. Harness 工程的整體設計與核心思路拆解2.1 先搞清楚 Harness 到底管什么很多人第一次聽到 Harness 會懵覺得這不就是“Agent 框架”換個說法嗎。其實不是??蚣鼙热?LangChain、LangGraph、Spring AI解決的是“怎么把模型、工具、記憶拼起來”的問題而 Harness 解決的是“拼起來之后怎么讓它穩(wěn)定運行”的問題。框架是骨架Harness 是神經(jīng)系統(tǒng)加免疫系統(tǒng)。我習慣把 Harness 拆成五個職責層每一層都對應一類真實故障職責層解決的問題缺失后的典型故障循環(huán)控制什么時候繼續(xù)、什么時候停無限循環(huán)、提前終止工具調度調用哪個工具、參數(shù)怎么校驗參數(shù)錯亂、調用不存在的工具上下文管理什么進上下文、什么被裁剪上下文溢出、關鍵信息丟失錯誤恢復出錯后怎么辦一次失敗全鏈路崩潰狀態(tài)與可觀測當前在哪、出過什么事無法調試、無法斷點續(xù)跑這五層不是并列的而是有依賴關系的。循環(huán)控制是主干工具調度和上下文管理掛在主干上錯誤恢復是橫切關注點狀態(tài)與可觀測是底座。設計 Harness 的時候我建議就按這個順序來搭先把循環(huán)跑通再逐步加厚其他層。2.2 為什么不能把控制權全交給模型新手最容易犯的錯是把“下一步做什么”完全交給模型決定。模型說調用工具就調用模型說結束就結束。這在 Demo 里沒問題在生產(chǎn)里是災難。原因很簡單模型的輸出是概率性的而生產(chǎn)系統(tǒng)需要確定性。我的做法是引入一個顯式的狀態(tài)機。Agent 的每一次迭代狀態(tài)只能在這幾個之間流轉IDLE → PLANNING → TOOL_CALLING → OBSERVING → REFLECTING → DONE/FAILED。模型只能在PLANNING和REFLECTING階段輸出內容而狀態(tài)之間的跳轉由 Harness 的代碼邏輯決定不由模型說了算。這樣即使模型抽風最壞情況也只是某個階段輸出質量差而不會讓整個流程失控。這個設計還有一個好處每個狀態(tài)都可以單獨打日志、單獨設超時、單獨做重試。調試的時候你能精確知道卡在哪一步而不是面對一坨“模型又亂說話了”的黑盒。2.3 工具調度為什么需要一層“中間人”直接讓模型輸出工具調用然后代碼去執(zhí)行這是最直覺的做法。但真實場景里模型給出的參數(shù)經(jīng)常有問題字段名拼錯、類型不對、必填項缺失、甚至調用一個根本沒注冊的工具。如果每次都靠 try-catch 兜底代碼會變得極其丑陋。所以我在模型和真實工具之間加了一層調度器。調度器的職責包括校驗工具名是否在白名單內、校驗參數(shù)是否符合 schema、對參數(shù)做必要的類型轉換和默認值填充、執(zhí)行前做權限檢查、執(zhí)行后統(tǒng)一包裝返回格式。這一層看起來是“多此一舉”但它把大量臟活從業(yè)務代碼里剝離出來了。提示工具 schema 一定要用嚴格的 JSON Schema 定義不要圖省事用自然語言描述參數(shù)。模型對結構化 schema 的遵循度遠高于自然語言描述這是實測下來最明顯的差異之一。2.4 上下文管理是 Harness 里最容易被低估的部分我見過太多 Agent 項目死在上下文上。要么是上下文無限增長導致成本和延遲飆升要么是裁剪策略太粗暴把關鍵信息刪了導致模型失憶。上下文管理的核心矛盾是你需要保留足夠的歷史讓模型理解當前處境但又不能讓它無限膨脹。我的策略是分層管理。把上下文分成三類系統(tǒng)層System Prompt、工具定義永遠保留、任務層當前目標、已完成步驟的摘要動態(tài)更新、對話層原始的工具調用和返回可裁剪。裁剪的時候優(yōu)先壓縮對話層把多輪工具調用合并成一句摘要而不是直接刪掉。任務層用結構化的方式維護比如一個 JSON 記錄“已完成哪些步驟、當前卡在哪、下一步計劃”這樣即使對話層被裁得很狠模型也不會失去方向感。3. 核心機制細節(jié)解析與實操要點3.1 循環(huán)控制終止條件必須多重保險Agent 的循環(huán)控制說白了就是回答“什么時候停”。最危險的情況是無限循環(huán)模型一直覺得任務沒完成一直調用工具燒錢又燒時間。我一般會設三重終止條件任何一重觸發(fā)都強制停止。第一重是最大迭代次數(shù)。這個值怎么定我的經(jīng)驗是看任務的復雜度。簡單的單工具任務5 到 8 次足夠多步驟的復雜任務15 到 20 次再往上就要警惕是不是任務定義本身有問題了。我通常默認設 15超過這個數(shù)還沒結束大概率是模型陷入了某種循環(huán)。第二重是無進展檢測。記錄最近幾輪的工具調用和返回如果連續(xù)三輪的調用參數(shù)高度相似、返回結果也高度相似說明模型在原地打轉直接終止。這個檢測用簡單的字符串相似度或者哈希比對就能實現(xiàn)不需要多復雜。第三重是顯式完成信號。要求模型在任務完成時輸出一個特定的結構化標記比如{status: done, result: ...}。Harness 解析到這個標記才認為任務真正結束而不是模型隨便說一句“我完成了”就信。MAX_ITERATIONS 15 NO_PROGRESS_THRESHOLD 3 def should_terminate(state): if state.iteration MAX_ITERATIONS: return True, max_iterations_reached if state.no_progress_count NO_PROGRESS_THRESHOLD: return True, no_progress_detected if state.explicit_done: return True, task_completed return False, None注意無進展檢測的閾值不要設得太小有些任務確實需要多次相似調用才能收斂比如輪詢某個狀態(tài)。3 次是我實測下來比較平衡的值既不會誤殺正常任務也能及時止損。3.2 工具調用的參數(shù)校驗與容錯工具調用出錯是家常便飯關鍵是怎么優(yōu)雅地處理。我的調度器里有一套固定的校驗流程按順序執(zhí)行工具名白名單檢查、參數(shù) schema 校驗、必填項檢查、類型轉換、默認值填充、權限檢查。任何一步失敗都不直接拋異常而是返回一個結構化的錯誤信息給模型讓模型有機會自我修正。這里有個細節(jié)很關鍵錯誤信息要寫得讓模型能看懂并據(jù)此修正。比如參數(shù)類型錯誤不要只返回“類型錯誤”而要返回“參數(shù) timeout 期望是整數(shù)實際收到字符串 30s請?zhí)峁┘償?shù)字”。模型看到這種明確的反饋下一輪大概率能改對。我實測下來這種“帶引導的錯誤返回”能把工具調用的首次成功率提升一大截。另一個技巧是給工具調用設超時。有些工具比如網(wǎng)絡請求、數(shù)據(jù)庫查詢可能卡住如果不設超時整個 Agent 就掛在那里了。我一般給單個工具調用設 30 秒超時超時后返回一個明確的超時錯誤讓模型決定是重試還是換方案。3.3 上下文裁剪的具體策略與參數(shù)上下文裁剪不是簡單地把老消息刪掉那樣會讓模型失去連貫性。我的做法是“摘要 保留關鍵節(jié)點”。具體來說當上下文長度超過閾值我一般設模型上下文窗口的 70%時觸發(fā)裁剪。裁剪時保留最近 N 輪的完整對話N 一般取 5更早的部分壓縮成摘要。摘要怎么生成可以用一個便宜的小模型來做把多輪工具調用壓縮成“調用了 X 工具得到 Y 結果用于 Z 目的”這樣的結構化描述。這樣既省 token又保留了關鍵信息。摘要的粒度要控制好太粗會丟信息太細等于沒壓縮。我的經(jīng)驗是每個已完成步驟壓縮成一句話整個摘要控制在 500 token 以內。還有一個容易被忽略的點工具定義本身也占上下文。如果你注冊了幾十個工具光工具描述就可能吃掉幾千 token。我的做法是按任務動態(tài)加載工具只把當前任務可能用到的工具放進上下文而不是一股腦全塞進去。這個優(yōu)化能省下大量 token也能減少模型選錯工具的概率。3.4 錯誤恢復重試、降級與人工兜底錯誤恢復是 Harness 里最能體現(xiàn)工程功力的部分。我把錯誤分成三類分別用不同策略處理。第一類是瞬時錯誤比如網(wǎng)絡抖動、限流、臨時超時。這類錯誤直接重試用指數(shù)退避重試 3 次。退避的基數(shù)我一般設 1 秒即 1s、2s、4s避免短時間內瘋狂重試把下游打掛。第二類是可修正錯誤比如參數(shù)錯誤、工具返回業(yè)務異常。這類錯誤不重試而是把錯誤信息返回給模型讓模型調整后重新調用。這其實就是前面說的“帶引導的錯誤返回”。第三類是致命錯誤比如工具不存在、權限不足、依賴服務徹底不可用。這類錯誤直接終止當前任務記錄詳細日志并觸發(fā)告警。如果任務支持斷點續(xù)跑就把當前狀態(tài)持久化等人工介入后再恢復。def handle_error(error, state): if error.type transient: return retry_with_backoff(state, max_retries3, base_delay1) elif error.type correctable: return feed_back_to_model(error.message, state) else: persist_state(state) alert(error) return terminate(state)提示重試一定要設上限并且要區(qū)分“重試整個任務”和“重試單個工具調用”。前者代價大后者代價小。能用后者解決的絕不用前者。4. 實操過程與核心環(huán)節(jié)實現(xiàn)4.1 從零搭一個最小可用的 Harness光講理論沒意思我把搭一個最小 Harness 的過程完整走一遍。假設我們用 Python模型走標準的對話接口工具就是幾個普通的函數(shù)。第一步定義狀態(tài)結構。這是整個 Harness 的核心數(shù)據(jù)結構所有信息都掛在上面。from dataclasses import dataclass, field from typing import List, Dict, Any dataclass class AgentState: task: str iteration: int 0 status: str IDLE messages: List[Dict] field(default_factorylist) completed_steps: List[str] field(default_factorylist) current_plan: str no_progress_count: int 0 last_tool_calls: List[str] field(default_factorylist) result: Any None第二步寫主循環(huán)。主循環(huán)的邏輯就是不斷推進狀態(tài)直到觸發(fā)終止條件。def run_agent(task: str, tools: dict, max_iter: int 15): state AgentState(tasktask) state.messages.append({role: system, content: build_system_prompt(tools)}) state.messages.append({role: user, content: task}) while True: terminate, reason should_terminate(state) if terminate: state.status DONE if reason task_completed else FAILED break state.iteration 1 response call_model(state.messages) if response.has_tool_call: state.status TOOL_CALLING tool_result dispatch_tool(response.tool_call, tools) state.messages.append({role: assistant, content: response.raw}) state.messages.append({role: tool, content: tool_result}) update_progress(state, response.tool_call, tool_result) else: state.status REFLECTING state.messages.append({role: assistant, content: response.raw}) if is_done_signal(response.raw): state.explicit_done True state.result extract_result(response.raw) state.messages maybe_trim_context(state.messages) return state第三步實現(xiàn)工具調度器。這是把模型輸出和真實函數(shù)連接起來的關鍵。def dispatch_tool(tool_call, tools): name tool_call.get(name) args tool_call.get(arguments, {}) if name not in tools: return json.dumps({error: f工具 {name} 不存在可用工具{list(tools.keys())}}) schema tools[name][schema] valid, msg validate_args(args, schema) if not valid: return json.dumps({error: f參數(shù)校驗失敗{msg}}) try: result tools[name][func](**args) return json.dumps({result: result}, ensure_asciiFalse) except Exception as e: return json.dumps({error: f工具執(zhí)行異常{str(e)}})這三步搭完一個最小 Harness 就能跑了。但能跑不等于穩(wěn)定接下來要往里加東西。4.2 上下文裁剪的實現(xiàn)細節(jié)裁剪邏輯我單獨抽成一個函數(shù)因為它涉及不少判斷。def maybe_trim_context(messages, max_tokens6000, keep_recent5): if count_tokens(messages) max_tokens: return messages system_msgs [m for m in messages if m[role] system] other_msgs [m for m in messages if m[role] ! system] recent other_msgs[-keep_recent:] older other_msgs[:-keep_recent] if older: summary summarize(older) summary_msg {role: system, content: f歷史步驟摘要{summary}} return system_msgs [summary_msg] recent return system_msgs recent這里count_tokens可以用 tiktoken 之類的庫也可以用簡單的字符數(shù)估算。summarize我一般調一個小模型來做把多輪對話壓縮成結構化摘要。注意摘要要保留“做了什么、得到什么、為什么這么做”這三個要素缺一不可。4.3 無進展檢測的具體實現(xiàn)無進展檢測的核心是判斷“最近幾輪是不是在做重復的事”。我用工具名加參數(shù)哈希來做比對。import hashlib def update_progress(state, tool_call, tool_result): signature hashlib.md5( f{tool_call[name]}:{json.dumps(tool_call[arguments], sort_keysTrue)}.encode() ).hexdigest() state.last_tool_calls.append(signature) if len(state.last_tool_calls) 3: state.last_tool_calls.pop(0) if len(state.last_tool_calls) 3 and len(set(state.last_tool_calls)) 1: state.no_progress_count 1 else: state.no_progress_count 0這個邏輯的意思是如果連續(xù)三次調用的工具和參數(shù)完全一樣就認為沒有進展。實際用的時候可以放寬一點比如參數(shù)相似度超過 90% 也算重復避免模型微調參數(shù)后反復試探。4.4 狀態(tài)持久化與斷點續(xù)跑生產(chǎn)環(huán)境的 Agent 必須支持斷點續(xù)跑否則一旦進程掛掉之前的工作全白費。我的做法是每完成一個步驟就把AgentState序列化存到數(shù)據(jù)庫或 Redis 里。def persist_state(state, store): store.set(fagent:{state.task_id}, json.dumps(asdict(state))) def load_state(task_id, store): raw store.get(fagent:{task_id}) if raw: return AgentState(**json.loads(raw)) return None恢復的時候從存儲里讀出狀態(tài)重新進入主循環(huán)即可。這里有個細節(jié)恢復后要重新構建上下文因為消息列表可能已經(jīng)被裁剪過。我的做法是把completed_steps和current_plan重新注入到 System Prompt 里讓模型快速恢復上下文感知。注意狀態(tài)持久化的頻率要權衡。每步都存會增加延遲存得太少又可能丟進度。我的經(jīng)驗是每個工具調用完成后存一次純模型推理的中間狀態(tài)可以不存。5. 常見問題與排查技巧實錄5.1 模型不調用工具只輸出文字怎么辦這是最常見的問題之一。模型明明有工具可用卻選擇用自然語言回答。原因通常有三個工具描述不夠清晰、System Prompt 沒有強調工具優(yōu)先、或者模型本身能力不足。排查順序是這樣的先看工具描述是不是寫得太抽象了。工具描述要具體到“什么時候用、輸入什么、輸出什么”最好帶一兩個例子。然后看 System Prompt有沒有明確說“當需要外部信息或執(zhí)行操作時必須調用工具不要憑記憶回答”。如果這兩點都沒問題那就是模型能力問題換一個工具調用能力更強的模型。我實測下來工具描述里加上“使用場景”這一項能顯著提升調用率。比如不要只寫“查詢天氣”而要寫“當用戶詢問某地天氣、溫度、是否下雨時使用此工具”。5.2 上下文溢出導致模型報錯上下文溢出通常發(fā)生在長任務里。表現(xiàn)是模型接口直接返回錯誤說 token 超限。這時候要檢查兩件事裁剪邏輯有沒有生效、工具定義是不是太多。裁剪邏輯不生效的常見原因是閾值設得太大或者count_tokens算得不準。我建議閾值設在模型窗口的 70%留 30% 的余量給模型輸出。工具定義太多的話就做動態(tài)加載按任務階段只加載相關工具。還有一個隱蔽的坑有些工具返回的結果特別長比如返回一大段 HTML如果不做截斷一次調用就能把上下文撐爆。我的做法是在工具調度器里對返回結果做長度限制超過閾值的部分截斷并加省略標記。5.3 工具調用參數(shù)總是出錯參數(shù)出錯的原因八成是 schema 定義不夠嚴格。我見過有人用自然語言描述參數(shù)比如“timeout 參數(shù)是超時時間”結果模型一會兒傳數(shù)字一會兒傳字符串。正確做法是用標準 JSON Schema明確 type、required、enum 等約束。如果 schema 已經(jīng)很嚴格了還是出錯那就是模型對 schema 的理解有問題。這時候可以在 System Prompt 里加一段“參數(shù)填寫規(guī)范”把容易出錯的參數(shù)單獨拎出來強調。另外調度器里的類型轉換和默認值填充也能兜住一部分錯誤不要指望模型每次都完美。5.4 常見問題速查表問題現(xiàn)象可能原因排查方向解決手段無限循環(huán)終止條件缺失檢查迭代計數(shù)和無進展檢測加多重終止條件上下文溢出裁剪未生效檢查閾值和 token 計算降低閾值、動態(tài)加載工具工具調用失敗schema 不嚴檢查參數(shù)定義用嚴格 JSON Schema任務中途失憶裁剪太粗暴檢查摘要質量保留關鍵節(jié)點、結構化摘要恢復后行為異常狀態(tài)不完整檢查持久化字段補全狀態(tài)、重建上下文響應特別慢工具阻塞檢查工具超時加超時、異步化5.5 幾個我踩過的坑第一個坑是過度依賴模型的自我反思。我一開始設計了一個“反思階段”讓模型自己檢查上一步做得對不對。結果發(fā)現(xiàn)模型經(jīng)?!胺此肌背鲆恍┎淮嬖诘膯栴}然后去修一個本來沒壞的東西。后來我把反思改成可選的只在特定條件下觸發(fā)比如工具返回錯誤時。第二個坑是工具粒度過細。我一開始把每個小操作都做成獨立工具結果模型在幾十個工具里挑花了眼經(jīng)常選錯。后來我把相關操作合并成粗粒度工具比如把“讀文件、寫文件、列目錄”合并成一個“文件操作”工具用參數(shù)區(qū)分具體動作。工具數(shù)量降下來之后調用準確率明顯提升。第三個坑是忽略并發(fā)場景。單線程跑得好好的 Agent一上并發(fā)就出問題。共享狀態(tài)被多個任務同時修改導致數(shù)據(jù)錯亂。解決辦法是每個任務一個獨立的AgentState實例狀態(tài)存儲用任務 ID 做隔離絕不共享可變狀態(tài)。提示并發(fā)場景下工具本身也要考慮線程安全。如果工具內部有共享資源比如數(shù)據(jù)庫連接池要做好隔離或加鎖。6. 關于 Harness 工程的一些個人體會寫到這里我想聊點不那么技術的東西。做 Agent 開發(fā)這兩年我最大的感受是這個領域的難點正在從“模型能力”轉移到“工程能力”。模型每隔幾個月就更新一代能力越來越強但 Harness 這一層的設計思路是相對穩(wěn)定的。你把循環(huán)控制、工具調度、上下文管理、錯誤恢復這幾件事做扎實了換什么模型都能跑得不錯反過來Harness 做得爛再強的模型也救不了。我現(xiàn)在的習慣是每接一個新 Agent 需求先不急著寫提示詞而是先把 Harness 的骨架搭出來把狀態(tài)機、終止條件、錯誤處理這些定好然后再往里填業(yè)務邏輯。這個順序看起來慢實際上省了大量后期調試的時間。因為 Harness 穩(wěn)了之后模型的行為就變得可預測了出問題也能快速定位到是哪一層的問題。還有一個體會是關于“度”的把握。Harness 不是越厚越好加太多約束會讓 Agent 變得僵化失去靈活性。我的原則是核心流程用代碼強約束邊緣決策交給模型。比如“必須調用工具獲取數(shù)據(jù)”這是強約束代碼來管“用哪個工具更合適”這是邊緣決策模型來定。這個邊界劃清楚了Agent 既穩(wěn)定又不失智能。最后分享一個小技巧給 Harness 加一個“回放”功能。把每次任務的完整狀態(tài)流轉記錄下來出問題的時候可以回放整個執(zhí)行過程一步步看模型在哪一步做了什么決策。這個功能在調試復雜任務時簡直是救命稻草比看日志高效十倍。實現(xiàn)起來也不難就是把每個狀態(tài)變更都追加到一個事件流里需要的時候按時間順序重放即可。