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