級Agent Harness實(shí)戰(zhàn):基于PI構(gòu)建穩(wěn)定可控的任務(wù)執(zhí)行框架)
“把 Agent 框架拆開”這句話我在不同場合說了不下十次。做 agent 項(xiàng)目做得越久越清楚真正難的不是讓模型跑起來而是讓 agent 在線上沒人盯著的時(shí)候也能穩(wěn)定、安全、可控地完成一整套任務(wù)。我最近把線上跑了半年多的 agent 服務(wù)做了一次大重構(gòu)核心工作就是把這層“包殼”單獨(dú)抽出來用 PI 重新做成了一套生產(chǎn)級 Harness。先說結(jié)論免得你繞彎路框架解決的是“讓 agent 能想”Harness 解決的是“讓它能在生產(chǎn)環(huán)境里安全、穩(wěn)定、可控地跑”。PI 在這里是一個(gè)輕量級 agent 運(yùn)行時(shí)社區(qū)里有人叫它 Personal Intelligence也有人干脆叫 PI runtime它把會話、事件流、工具調(diào)用這些基礎(chǔ)能力收斂得很干凈特別適合在上面自己包一層工程外殼。這篇文章適合正在做 agent 落地的讀者也適合那些想擺脫對全家桶框架依賴、自己做執(zhí)行控制的團(tuán)隊(duì)。下面直接講我對 PI 的拆解以及生產(chǎn)級 Harness 的完整搭建過程。1. 為什么框架之外還要有 Harness很多人第一次接觸 agent 開發(fā)都是從跑通一個(gè) Demo 開始的。模型能回答、工具能調(diào)用、上下文能記住就覺得差不多了。但把這些東西放到生產(chǎn)環(huán)境你會發(fā)現(xiàn)框架給不了你三樣?xùn)|西任務(wù)生命周期管理、異?;謴?fù)機(jī)制、管控與審計(jì)能力。這三樣恰恰是生產(chǎn)級系統(tǒng)的及格線也正是 Harness 要補(bǔ)上的。1.1 Agent 框架通常不給你的三樣?xùn)|西你可以把大模型框架理解成一臺發(fā)動機(jī)它負(fù)責(zé)把“意圖”轉(zhuǎn)成“行動”負(fù)責(zé)對話、推理、調(diào)用工具。但一輛能上路的車除了發(fā)動機(jī)還需要底盤、剎車、儀表盤和安全帶。Harness 就是這輛車的外殼和控制系統(tǒng)。第一框架通常不幫你管“這個(gè)任務(wù)現(xiàn)在到底處于什么階段”。Demo 里一次對話結(jié)束就完了但生產(chǎn)環(huán)境里一個(gè)任務(wù)可能要執(zhí)行幾分鐘甚至幾十分鐘中間要調(diào)多個(gè)工具、可能還要等人審批。如果沒有顯式的狀態(tài)機(jī)任務(wù)一旦卡住你根本不知道它卡在哪個(gè)環(huán)節(jié)。第二框架通常不具備故障恢復(fù)能力。模型調(diào)用超時(shí)、工具返回異常、進(jìn)程崩潰這些都是生產(chǎn)環(huán)境的常態(tài)??蚣軙佉粋€(gè)異常給你但不會幫你自動重試、把失敗任務(wù)轉(zhuǎn)入死信隊(duì)列、或者從檢查點(diǎn)恢復(fù)。這些邏輯必須由 Harness 來實(shí)現(xiàn)。第三框架缺乏管控面。誰在什么時(shí)間調(diào)用了哪個(gè)工具、消耗了多少 token、執(zhí)行了幾次重試這些數(shù)據(jù)如果沒有統(tǒng)一收口出問題的時(shí)候你只能對著日志猜。Harness 的作用就是用統(tǒng)一的事件流把這些信息全部沉淀下來。1.2 生產(chǎn)級這個(gè)詞的及格線我在評審一個(gè) agent 項(xiàng)目能不能上線時(shí)只看四件事不丟任務(wù)任務(wù)進(jìn)來之后無論進(jìn)程怎么崩任務(wù)本身不能丟。要么完成要么明確失敗并進(jìn)入可處理隊(duì)列絕不能不明不白消失。能恢復(fù)單次模型調(diào)用失敗不意味著整個(gè)任務(wù)失敗。要有重試策略要有退避算法還要有上下文保留機(jī)制??捎^測任何一個(gè)環(huán)節(jié)出問題都能通過結(jié)構(gòu)化日志和鏈路追蹤快速定位。不能等到用戶投訴了才發(fā)現(xiàn)任務(wù)掛了。可干預(yù)涉及高風(fēng)險(xiǎn)操作下單、刪除、轉(zhuǎn)賬時(shí)系統(tǒng)能把任務(wù)掛起等待人工審批后再繼續(xù)。這四條就是“生產(chǎn)級”的底線。很多團(tuán)隊(duì) Demo 跑得飛起上了生產(chǎn)就出事基本都是在這些地方欠了債。1.3 為什么選 PI 而不是自研引擎做 Harness 之前我認(rèn)真考慮過兩個(gè)方案一個(gè)是直接用 LangChain、LlamaIndex 這類全家桶框架在上面做二次封裝另一個(gè)是自己寫一個(gè)運(yùn)行時(shí)。最后選了 PI原因是它在兩者之間找到了一個(gè)比較舒服的位置。PI 給我的核心價(jià)值是 session 管理和事件流抽象。它把一次完整的 agent 執(zhí)行過程建模成 session、turn、event 三個(gè)層次session 是完整任務(wù)turn 是一次模型交互event 是流式輸出過程中的每一個(gè)事件。這個(gè)模型足夠簡單但也有足夠的表現(xiàn)力——我可以基于它去構(gòu)建狀態(tài)機(jī)、鉤子函數(shù)和事件訂閱而不用被框架內(nèi)部的重型抽象綁架。換句話說PI 給我的是“可控的零件”引擎內(nèi)部的組裝工作留給我自己這樣我就有空間去建一套真正貼合業(yè)務(wù)的 Harness。2. PI Harness 整體設(shè)計(jì)與分層思路Harness 不是某一個(gè)單獨(dú)的模塊它是一整套分層結(jié)構(gòu)。我設(shè)計(jì)時(shí)的原則是上層不直接碰模型下層不直接對業(yè)務(wù)暴露接口所有交互都通過統(tǒng)一事件流來驅(qū)動。這樣每一層都可以獨(dú)立替換、獨(dú)立測試。2.1 先拆 PI 運(yùn)行時(shí)的工作模型要理解 Harness得先理解 PI 是怎么跑起來的。PI 的工作模型可以概括為三個(gè)層次Session會話代表一個(gè)完整的任務(wù)實(shí)例。它持有上下文、狀態(tài)、元數(shù)據(jù)是 Harness 生命周期管理的最小單位。Turn輪次一次完整的“用戶輸入 → 模型推理 → 可能的工具調(diào)用 → 模型輸出”循環(huán)。一個(gè) Session 里可以有多個(gè) Turn。Event事件PI 在執(zhí)行過程中持續(xù)產(chǎn)生的事件流比如 token 增量、工具調(diào)用開始、工具調(diào)用結(jié)束、錯(cuò)誤發(fā)生等。對 Harness 來說最關(guān)鍵的是事件流。我不需要侵入 PI 內(nèi)部去改邏輯只需要訂閱它的事件然后驅(qū)動我自己的狀態(tài)機(jī)。這種設(shè)計(jì)讓 Harness 和運(yùn)行時(shí)之間形成了一種松耦合升級 PI 版本的時(shí)候我的業(yè)務(wù)邏輯基本不用動。2.2 Harness 四層結(jié)構(gòu)我這里把 Harness 整體分成四層從上到下依次是接入層負(fù)責(zé)接收外部請求做鑒權(quán)和限流把請求包裝成標(biāo)準(zhǔn)任務(wù)放入隊(duì)列。這一層可以是 HTTP API也可以直接消費(fèi)消息隊(duì)列。編排層這是 Harness 的核心負(fù)責(zé)狀態(tài)機(jī)流轉(zhuǎn)、重試策略、審批流程、配額控制。它不關(guān)心模型細(xì)節(jié)只關(guān)心“這個(gè)任務(wù)現(xiàn)在應(yīng)該做什么”。執(zhí)行層持有 PI 運(yùn)行時(shí)實(shí)例負(fù)責(zé)實(shí)際驅(qū)動模型會話、執(zhí)行工具調(diào)用。工具在這里被包裝成標(biāo)準(zhǔn)接口由編排層統(tǒng)一調(diào)度。底座層提供存儲任務(wù)狀態(tài)、事件日志、緩存分布式鎖、冪等、可觀測性指標(biāo)、日志、鏈路追蹤。這個(gè)分層最直接的好處是我可以單獨(dú)擴(kuò)容執(zhí)行層也可以單獨(dú)升級編排策略互不影響。有一次我調(diào)整重試參數(shù)只是改了編排層的一個(gè)配置項(xiàng)執(zhí)行層完全沒動就完成了上線。2.3 三個(gè)核心數(shù)據(jù)結(jié)構(gòu)Harness 的數(shù)據(jù)結(jié)構(gòu)不需要很復(fù)雜但一定要穩(wěn)定。我最核心的三個(gè)數(shù)據(jù)結(jié)構(gòu)是結(jié)構(gòu)關(guān)鍵字段作用HarnessConfigworker_concurrency, retry_policy, quota_policy, tool_timeoutHarness 的總配置運(yùn)行時(shí)不可變變更走配置中心灰度TaskEnvelopetask_id, session_id, payload, priority, created_at, state統(tǒng)一任務(wù)包裝無論任務(wù)來源是 HTTP 還是隊(duì)列都轉(zhuǎn)成這個(gè)結(jié)構(gòu)AgentContextsession_ref, state, turn_count, token_usage, tool_call_logs任務(wù)執(zhí)行過程中的全量上下文持久化存儲支持恢復(fù)這三個(gè)結(jié)構(gòu)基本覆蓋了我的全部需求。TaskEnvelope 保證任務(wù)在系統(tǒng)里的流轉(zhuǎn)是統(tǒng)一的AgentContext 保證任何時(shí)刻進(jìn)程崩潰都能重建現(xiàn)場HarnessConfig 保證行為可控可灰度。做生產(chǎn)級 Harness先把這三個(gè)東西定死后面寫代碼輕松一半。3. 生產(chǎn)級 Harness 的核心實(shí)現(xiàn)細(xì)節(jié)設(shè)計(jì)定完之后真正花時(shí)間的是實(shí)現(xiàn)細(xì)節(jié)。下面這幾個(gè)點(diǎn)每一個(gè)都是我在生產(chǎn)環(huán)境踩過坑之后才定下來的方案直接給你可復(fù)制的寫法。3.1 生命周期狀態(tài)機(jī)怎么落地任務(wù)生命周期是 Harness 的骨架。我用的是顯式狀態(tài)機(jī)而不是散落的 if/else 判斷。狀態(tài)機(jī)的好處是非法流轉(zhuǎn)在進(jìn)入狀態(tài)之前就能被發(fā)現(xiàn)。# harness/state.py import enum class HarnessState(str, enum.Enum): PENDING PENDING RUNNING RUNNING WAITING_INPUT WAITING_INPUT WAITING_APPROVAL WAITING_APPROVAL COMPLETED COMPLETED FAILED FAILED CANCELLED CANCELLED # 合法的狀態(tài)流轉(zhuǎn)表 TRANSITIONS { HarnessState.PENDING: { HarnessState.RUNNING, HarnessState.CANCELLED, }, HarnessState.RUNNING: { HarnessState.WAITING_INPUT, HarnessState.WAITING_APPROVAL, HarnessState.COMPLETED, HarnessState.FAILED, }, HarnessState.WAITING_INPUT: { HarnessState.RUNNING, HarnessState.CANCELLED, }, HarnessState.WAITING_APPROVAL: { HarnessState.RUNNING, HarnessState.CANCELLED, }, HarnessState.FAILED: { HarnessState.RUNNING, # 允許人工介入后重跑 }, } def validate_transition(current: HarnessState, target: HarnessState) - bool: return target in TRANSITIONS[current]你可能會問為什么 WAITING_APPROVAL 要單獨(dú)做一個(gè)狀態(tài)而不是掛在 RUNNING 下面因?yàn)檫@兩個(gè)狀態(tài)在運(yùn)維語義上完全不同RUNNING 是正在花錢的階段模型在調(diào)用、工具在執(zhí)行WAITING_APPROVAL 是已經(jīng)暫停的階段不產(chǎn)生成本需要人介入。我一開始把審批狀態(tài)掛在 RUNNING 里結(jié)果超時(shí)策略和計(jì)費(fèi)統(tǒng)計(jì)全亂套了。拆開之后超時(shí)策略可以分別配置RUNNING 階段嚴(yán)格執(zhí)行調(diào)用超時(shí)WAITING_APPROVAL 階段則允許長期掛起。3.2 超時(shí)、重試和熔斷的參數(shù)怎么定大模型場景的重試比普通 RPC 復(fù)雜因?yàn)樗恢皇恰爸卦囈淮握埱蟆边€關(guān)系到上下文污染、token 浪費(fèi)、上游限流。我使用的重試策略參數(shù)如下參數(shù)推薦值說明首 token 超時(shí)15s超過直接重試多半是上游排隊(duì)總響應(yīng)超時(shí)120s流式場景也必須有總時(shí)長上限最大重試次數(shù)3 次超過后進(jìn)入死信隊(duì)列不無限重試基礎(chǔ)退避1s指數(shù)退避的初始值最大退避60s防止退避過長拖死任務(wù)重試的代碼實(shí)現(xiàn)可以很簡單但有一個(gè)關(guān)鍵點(diǎn)重試不能改變上下文。同一輪 Turn 的重試必須使用完全相同的輸入并且要保證冪等——工具調(diào)用如果有副作用重試前要確認(rèn)上一次調(diào)用真的沒生效。# harness/retry.py import random def next_backoff(retry_count: int, base: float 1.0, cap: float 60.0) - float: backoff min(base * (2 ** retry_count), cap) jitter random.uniform(0, backoff * 0.1) return backoff jitter def should_retry(retry_count: int, max_retries: int 3) - bool: return retry_count max_retriesjitter 一定要加。不加 jitter 的話一旦上游模型服務(wù)出問題所有 worker 會同時(shí)重試形成重試風(fēng)暴直接把上游打掛。加了隨機(jī)抖動之后重試請求會自然散開實(shí)測效果明顯。熔斷方面我比較粗暴單 worker 在 5 分鐘內(nèi)如果連續(xù) 10 次模型調(diào)用超時(shí)就觸發(fā)熔斷暫停從隊(duì)列拉取新任務(wù) 30 秒。這個(gè)閾值不復(fù)雜但配合退避算法之后整個(gè)系統(tǒng)的穩(wěn)定性提升了一個(gè)級別。3.3 Human-in-the-loop 怎么插入執(zhí)行流生產(chǎn)級的 agent 系統(tǒng)基本都繞不開人工審批。比如自動生成內(nèi)容后要發(fā)布、自動下單、自動刪除數(shù)據(jù)這些高風(fēng)險(xiǎn)操作不能完全交給模型決定。我的方案是讓 Harness 在工具調(diào)用前檢查策略命中高風(fēng)險(xiǎn)策略就掛起任務(wù)等人審批。流程是這樣的編排層解析 PI 發(fā)來的工具調(diào)用事件先走權(quán)限校驗(yàn)和策略判斷。如果該工具被標(biāo)記為“需要審批”Harness 將任務(wù)從 RUNNING 狀態(tài)轉(zhuǎn)到 WAITING_APPROVAL 狀態(tài)同時(shí)持久化當(dāng)前 AgentContext。系統(tǒng)發(fā)送審批通知企業(yè)微信、釘釘、郵件都可以看你公司用什么。審批人通過內(nèi)部管理后臺查看待審批任務(wù)可以看到即將執(zhí)行的工具、參數(shù)、上下文摘要。審批通過后Harness 恢復(fù) AgentContext從原 checkpoint 繼續(xù)執(zhí)行審批拒絕則任務(wù)轉(zhuǎn)入 CANCELLED。這里有一個(gè)非常容易踩的坑審批中的上下文一定要持久化到數(shù)據(jù)庫不能只放在進(jìn)程內(nèi)存里。你想一下任務(wù)掛起后如果 worker 剛好發(fā)布重啟內(nèi)存里的上下文就全沒了審批通過之后根本沒法恢復(fù)。我一開始就是這個(gè)坑后來把所有 WAITING_APPROVAL 狀態(tài)的任務(wù)上下文都寫進(jìn)了數(shù)據(jù)庫才徹底解決。3.4 可觀測性、配額與成本控制Agent 系統(tǒng)的成本控制是生產(chǎn)中很現(xiàn)實(shí)的問題。模型調(diào)用是收錢的一個(gè)失控的 agent 循環(huán)調(diào)用工具可能幾分鐘就燒掉上千塊。我的做法是所有 token 消耗和工具調(diào)用都統(tǒng)計(jì)到任務(wù)級別并且設(shè)置硬性配額。每個(gè)任務(wù)配額固定為配額項(xiàng)默認(rèn)值超過后行為max_turns30強(qiáng)制終止任務(wù)max_prompt_tokens10000觸發(fā)上下文壓縮max_tool_calls50后續(xù)工具調(diào)用直接拒絕max_total_cost_usd2.0強(qiáng)制終止并通知管理員日志方面我采用 JSON lines 格式每個(gè)事件一行。標(biāo)準(zhǔn)字段包括task_id、session_id、turn_id、event_type、model、prompt_tokens、completion_tokens、tool_name、tool_latency_ms、state_from、state_to。這樣可以直接喂給日志采集系統(tǒng)做監(jiān)控和告警也可以拿來做審計(jì)回溯??捎^測性做得好的標(biāo)志是一個(gè)任務(wù)出問題你在監(jiān)控面板上從 task_id 進(jìn)去能看到它整個(gè)生命周期的完整時(shí)間線——什么時(shí)候進(jìn)的隊(duì)列、模型調(diào)用花了多久、哪個(gè)工具超時(shí)了、重試了幾次、最終落在哪個(gè)狀態(tài)。到了這個(gè)程度排障基本上就不是靠猜了。4. 從 Demo 到線上部署形態(tài)與穩(wěn)定化Harness 寫完之后怎么部署同樣有講究。我見過不少團(tuán)隊(duì)把 agent 服務(wù)寫成單體進(jìn)程一個(gè)進(jìn)程既接收 HTTP 請求又跑模型調(diào)用出問題連是入流量打爆還是模型調(diào)用卡死都分不清。4.1 最小可行部署拓?fù)湮业牟渴鹦螒B(tài)是“worker 模型”任務(wù)先進(jìn)入隊(duì)列worker 進(jìn)程從隊(duì)列里取任務(wù)執(zhí)行執(zhí)行結(jié)果再寫回存儲。它帶來的直接好處是隊(duì)列天然提供了削峰填谷能力模型服務(wù)變慢時(shí)任務(wù)會在隊(duì)列里積壓而不會把 HTTP 入口拖死。一個(gè)最少可行的拓?fù)涫墙尤雽覰ginx 對外提供 API做鑒權(quán)和基礎(chǔ)限流。隊(duì)列Redis Stream 做任務(wù)隊(duì)列。選擇 Redis Stream 而不是普通 List是因?yàn)樗С窒M(fèi)者組、消息確認(rèn)和 Pending Entries List這些特性天然契合任務(wù)系統(tǒng)的可靠性要求。執(zhí)行層3 到 5 個(gè) worker 進(jìn)程橫向部署每個(gè) worker 同時(shí)跑 2 到 4 個(gè) session。存儲PostgreSQL 存任務(wù)狀態(tài)和審批上下文Redis 同時(shí)承擔(dān)分布式鎖角色。這里要特別注意不要用“長連接直連模型服務(wù)”的形態(tài)。也就是每個(gè) worker 進(jìn)程在啟動時(shí)建立一個(gè)到模型服務(wù)的長連接然后所有 session 共用。這樣一旦網(wǎng)絡(luò)抖動所有 session 會同時(shí)斷連整個(gè) worker 直接進(jìn)入重試風(fēng)暴。我現(xiàn)在的做法是每次 Turn 都從連接池里取連接用完歸還池大小 10 到 20。這樣單次連接異常只會影響一個(gè) Turn而不是整個(gè)進(jìn)程。4.2 Tool 與 Skill 的注冊管理Tool 和 Skill 是 agent 系統(tǒng)里很容易混淆的兩個(gè)概念。我用一句話區(qū)分Tool 是原子能力Skill 是帶流程編排的可復(fù)用技能包。Tool 的例子是“查詢訂單狀態(tài)”“獲取天氣”“發(fā)送 HTTP 請求”。Skill 的例子是“寫周報(bào)”——它內(nèi)部會先讀取本周工作記錄再調(diào)用總結(jié)模型生成內(nèi)容最后按模板格式化這是多個(gè)步驟的編排。在 Harness 里Tool 和 Skill 都通過 manifest 文件注冊。每個(gè) Tool 聲明自己的名字、描述、權(quán)限范圍、超時(shí)時(shí)間和輸入輸出格式# tools/order_query.yaml name: order_query version: v1 description: 根據(jù)訂單ID查詢訂單狀態(tài) permissions: scope: order:read rate_limit: 20/min timeout: 8s input_schema: type: object properties: order_id: type: string required: - order_idHarness 加載 manifest 時(shí)做三層校驗(yàn)格式合法性、權(quán)限范圍是否超出當(dāng)前租戶所授權(quán)限、輸入 schema 是否完整。校驗(yàn)通過之后才注冊到執(zhí)行環(huán)境。這樣做的好處是工具接入變成了寫配置文件不需要改代碼而且權(quán)限邊界在接入那一刻就確定了。4.3 灰度發(fā)布與版本回滾Harness 本身的升級同樣需要流程。我的經(jīng)驗(yàn)是把 Harness 的配置和代碼分開管理。代碼升級走鏡像灰度比如先發(fā)布一個(gè) node 到預(yù)發(fā)環(huán)境跑自動化測試再逐步放量到生產(chǎn)。配置變更走配置中心灰度比如調(diào)整重試參數(shù)時(shí)先在 5% 的任務(wù)上生效觀察錯(cuò)誤率沒有變化再推到全量?;貪L策略要特別關(guān)注狀態(tài)兼容性。比如你升級了狀態(tài)機(jī)的定義加了一個(gè)新狀態(tài)那舊版本代碼是無法識別這個(gè)新狀態(tài)的。所以每次 Harness 代碼升級前我都要檢查數(shù)據(jù)庫里有沒有處于“新狀態(tài)”的任務(wù)如果有要么先把這些任務(wù)處理完再升級要么做一個(gè)狀態(tài)遷移腳本。這個(gè)檢查我寫進(jìn)了發(fā)布手冊每次發(fā)版必看。4.4 穩(wěn)定性驗(yàn)收清單每次改動上線前我會跑一遍這個(gè)清單檢查項(xiàng)方法通過標(biāo)準(zhǔn)任務(wù)不丟失殺掉 worker 后檢查隊(duì)列中待處理任務(wù)重啟后任務(wù)全部繼續(xù)執(zhí)行冪等重試模擬工具調(diào)用成功但響應(yīng)丟失不重復(fù)產(chǎn)生副作用超時(shí)隔離讓單個(gè)工具 sleep 超過超時(shí)時(shí)間只失敗當(dāng)前 Turn不影響其他 session審批恢復(fù)掛起任務(wù)后重啟 worker審批后任務(wù)可從 checkpoint 恢復(fù)配額生效將 max_turns 調(diào)為 1 觸發(fā)限制任務(wù)被強(qiáng)制終止并記錄原因流式斷連在模型響應(yīng)中途斷網(wǎng) 5 秒觸發(fā)重試并最終成功限流保護(hù)高并發(fā)壓測接入層超額請求被丟棄worker 不崩日志完整性抽樣檢查任務(wù)時(shí)間線每個(gè)狀態(tài)變更都有對應(yīng)事件日志這套清單執(zhí)行一次不到 30 分鐘但它保住了我很多次上線。穩(wěn)定性不是靠寫代碼寫出來的是靠一次次模擬故障驗(yàn)出來的。5. 常見問題與排查實(shí)錄最后這部分是我在實(shí)際運(yùn)營中遇到的真實(shí)問題每個(gè)都讓我改過代碼或調(diào)整過配置。整理成速查表放最后建議收藏備用。5.1 流式響應(yīng)中途 malformed 怎么處理這是我最常遇到的問題報(bào)錯(cuò)信息類似the response stream was malformed and no response was produced. try again.。一開始我以為是模型服務(wù)的問題后來發(fā)現(xiàn)很多時(shí)候是連接層的問題模型以流式方式返回?cái)?shù)據(jù)中間某個(gè) chunk 在傳輸層被截?cái)嗔擞绕涫蔷W(wǎng)絡(luò)代理、網(wǎng)關(guān)緩沖設(shè)置不當(dāng)?shù)臅r(shí)候特別容易出現(xiàn)。排查步驟是這樣的先看是不是網(wǎng)絡(luò)層截?cái)唷z查網(wǎng)關(guān)有沒有設(shè)置proxy_read_timeout如果超時(shí)過短流式響應(yīng)很容易中間斷掉。把超時(shí)從 30s 調(diào)到 120s 能解決相當(dāng)一部分問題。再看是不是模型服務(wù)監(jiān)控顯示有異常。如果上游自己沒問題那就是連接層的鍋。最后看是不是代碼層錯(cuò)誤處理不當(dāng)。流式讀取時(shí)如果遇到 EOF 或 JSON 解析失敗正確做法是把這個(gè) Turn 標(biāo)記為可重試而不是直接把任務(wù)置為 FAILED。后來我實(shí)現(xiàn)了“斷流自動重建”機(jī)制Harness 捕獲到 malformed 錯(cuò)誤后保留原始輸入上下文重新開啟一個(gè) Turn 重試重試次數(shù) 1 次。如果重試仍然失敗才進(jìn)入死信隊(duì)列。這個(gè)機(jī)制上線后這類錯(cuò)誤對線上任務(wù)的影響降到了幾乎為零。5.2 任務(wù)卡在 RUNNING 狀態(tài)有一次線上收到告警說某個(gè)任務(wù)執(zhí)行時(shí)間已經(jīng)超過了一個(gè)小時(shí)。我進(jìn)去查狀態(tài)發(fā)現(xiàn)任務(wù)一直停在 RUNNING但 worker 日志里根本沒有這個(gè)任務(wù)的執(zhí)行記錄——進(jìn)程早就不在了但任務(wù)狀態(tài)沒被更新。原因是 worker 崩潰時(shí)沒有“租約過期”機(jī)制。worker 從隊(duì)列里取走任務(wù)時(shí)任務(wù)狀態(tài)變成了 RUNNING但如果 worker 在任務(wù)還沒執(zhí)行完就崩潰這個(gè)狀態(tài)會一直卡在那里。解決方案是給任務(wù)加租約機(jī)制worker 拿到任務(wù)時(shí)寫入一個(gè)租約過期時(shí)間比如 5 分鐘然后每 30 秒續(xù)租一次。如果租約過期且沒有續(xù)租說明 worker 已經(jīng)不在了其他 worker 可以接管這個(gè)任務(wù)。接管時(shí)重新記錄執(zhí)行狀態(tài)并把任務(wù)重新置為 RUNNING。5.3 Token 超限、上下文污染和工具輸出爆炸模型上下文窗口是有限的但工具可以返回?zé)o限大的數(shù)據(jù)。比如一個(gè)查詢工具返回了 10 萬字的 JSON如果直接把它塞進(jìn)上下文下一個(gè) Turn 不僅浪費(fèi) token還可能讓模型產(chǎn)生幻覺。我的做法是給所有工具輸出加“體積保險(xiǎn)絲”工具輸出超過 2000 字符時(shí)自動截?cái)嗖⒏郊印耙呀財(cái)嗤暾麛?shù)據(jù)可調(diào)用 get_more 工具獲取”的提示。同時(shí)每輪 Turn 結(jié)束后檢查上下文總 token 數(shù)超過閾值的部分按“最早的消息優(yōu)先丟棄”策略壓縮但會保留系統(tǒng)提示詞和對當(dāng)前任務(wù)最關(guān)鍵的信息。這個(gè)策略犧牲了一點(diǎn)點(diǎn)“記憶力”但換來了每個(gè)任務(wù)的成本可控、響應(yīng)速度穩(wěn)定。真實(shí)業(yè)務(wù)里絕大多數(shù)任務(wù)的關(guān)鍵信息在最近幾輪上下文里就夠了。5.4 排查速查表現(xiàn)象定位方法處理建議任務(wù)成功但用戶說沒收到結(jié)果查任務(wù)狀態(tài)和事件時(shí)間線看最后一步回調(diào)是否失敗檢查回調(diào)錯(cuò)誤處理增加回調(diào)失敗重試模型響應(yīng)經(jīng)常中斷查網(wǎng)絡(luò)層的超時(shí)和緩沖配置調(diào)大流式超時(shí)斷流自動重試 1 次任務(wù)大量進(jìn)入死信隊(duì)列看死信隊(duì)列里的錯(cuò)誤分類按類型聚合統(tǒng)計(jì)對 Top 錯(cuò)誤逐個(gè)解決不要零散處理成本超支按 task_id 查 token 消耗和模型調(diào)用次數(shù)收緊 max_turns 或 max_total_cost_usd同一工具頻繁超時(shí)查工具執(zhí)行日志里的耗時(shí)分布優(yōu)化工具本身或減少該工具的并發(fā)配額最后再說一個(gè)我反復(fù)踩坑后養(yǎng)成的習(xí)慣每次上線前我都會人為殺掉 worker、斷掉 Redis 連接、模擬模型服務(wù) 5 秒超時(shí)把線上搞亂一遍再讓 Harness 自恢復(fù)。Harness 不是寫出來的是練出來的。PI 把運(yùn)行時(shí)的復(fù)雜性控制得很小你才有精力把外面這層殼做得足夠結(jié)實(shí)。Agent 上線這件事框架選誰其實(shí)沒那么重要Harness 能不能兜住底才是決定一個(gè) agent 項(xiàng)目能不能長期穩(wěn)定跑下去的關(guān)鍵。