與調(diào)度中間層的設(shè)計(jì)與實(shí)踐)
如果你也試過(guò)把兩個(gè)以上的AI代理拼在一起干活你大概率碰到過(guò)這種場(chǎng)景代理A從用戶(hù)問(wèn)題里抽出了一個(gè)訂單號(hào)需要讓代理B去查數(shù)據(jù)庫(kù)C去調(diào)用售后接口D負(fù)責(zé)匯總輸出。問(wèn)題是A怎么找到BB的接口地址寫(xiě)在哪調(diào)用失敗了要不要重試B忙不過(guò)來(lái)會(huì)不會(huì)把A卡死C返回的結(jié)果格式和A預(yù)期不一致怎么辦這些問(wèn)題單靠“給每個(gè)代理寫(xiě)死對(duì)方地址”能撐過(guò)前期驗(yàn)證規(guī)模一上來(lái)就全崩。Agent-Reach就是我在這個(gè)背景下折騰出來(lái)的一套代理間觸達(dá)與調(diào)度中間層這篇文章我會(huì)把它的設(shè)計(jì)思路、核心實(shí)現(xiàn)、實(shí)測(cè)數(shù)據(jù)和踩過(guò)的大坑完整記錄下來(lái)。先交代一下背景我所在的團(tuán)隊(duì)做的是面向企業(yè)客戶(hù)的客服智能化系統(tǒng)原來(lái)只有一個(gè)大模型代理在跑意圖識(shí)別和問(wèn)答。隨著業(yè)務(wù)擴(kuò)張這個(gè)“全能代理”開(kāi)始暴露問(wèn)題——單代理需要掛幾十個(gè)工具每次請(qǐng)求的上下文幾乎被撐爆響應(yīng)延遲肉眼可見(jiàn)地上升而且任意一個(gè)工具出錯(cuò)都會(huì)影響整體可用性。于是我們決定拆成多個(gè)專(zhuān)職代理訂單代理、售后代理、知識(shí)庫(kù)代理、工單代理、報(bào)表代理等等每個(gè)代理只負(fù)責(zé)自己那一畝三分地。代理數(shù)量從1個(gè)變成十幾個(gè)再到幾十個(gè)之后原先“把所有邏輯寫(xiě)在一個(gè)進(jìn)程里”的做法徹底行不通了。Agent-Reach這個(gè)項(xiàng)目就是為了解決“代理之間如何可靠地互相觸達(dá)”而生的。1. 為什么需要Agent-Reach單代理很強(qiáng)代理之間很亂1.1 代理編排的三種“常規(guī)做法”及各自的極限在動(dòng)手寫(xiě)Agent-Reach之前我把市面上常見(jiàn)的多代理協(xié)作方案挨個(gè)試了一遍大致可以歸成三類(lèi)。第一類(lèi)是中心化調(diào)度器。也就是用一個(gè)總控Agent接收所有請(qǐng)求再由它判斷該調(diào)用哪個(gè)子代理把結(jié)果輾轉(zhuǎn)傳遞。這種做法在代理數(shù)量不超過(guò)五六個(gè)的時(shí)候很好用因?yàn)檎{(diào)度器的上下文能覆蓋所有子代理的能力說(shuō)明。但代理一多調(diào)度器自己就變成了新的瓶頸——它既要理解每個(gè)子代理的能力邊界又要維護(hù)它們的狀態(tài)上下文消耗比原來(lái)的單代理方案還夸張。第二類(lèi)是直連調(diào)用也就是代理A的代碼里直接寫(xiě)死代理B的HTTP地址用最原始的方式互相調(diào)接口。好處是快壞處是維護(hù)成本隨代理數(shù)量呈指數(shù)級(jí)上升。我實(shí)際數(shù)過(guò)十幾個(gè)代理兩兩之間如果都要打通連線(xiàn)條數(shù)能到幾十條每次改一個(gè)代理的接口格式波及面大得嚇人。這種方案純粹是“能跑”和“能維護(hù)”之間的距離在拉大。第三類(lèi)是消息隊(duì)列廣播。所有代理訂閱同一個(gè)Topic誰(shuí)聽(tīng)了誰(shuí)知道。這個(gè)方案解耦是解耦了但帶來(lái)了兩個(gè)新問(wèn)題一是消息無(wú)差別廣播每個(gè)代理都要把每條消息過(guò)一遍上下文去判斷“這跟我有關(guān)嗎”浪費(fèi)極其嚴(yán)重二是沒(méi)有響應(yīng)的概念A(yù)問(wèn)B要數(shù)據(jù)B算完了怎么把結(jié)果準(zhǔn)確送回給A簡(jiǎn)單場(chǎng)景可以靠correlation ID硬湊但一旦涉及多跳傳遞、條件分支這套機(jī)制基本不夠看。1.2 Agent-Reach的核心設(shè)計(jì)前提分工自治、觸達(dá)統(tǒng)一折騰完上面三種方案我得出的結(jié)論是多代理系統(tǒng)需要的是代理之間保持自治但觸達(dá)路徑統(tǒng)一。什么意思所謂自治就是每個(gè)代理內(nèi)部可以有自己獨(dú)立的模型配置、提示詞體系、上下文管理策略和工具集我不需要也不應(yīng)該把它們強(qiáng)行統(tǒng)一成一個(gè)模板。所謂觸達(dá)統(tǒng)一則是代理之間的“尋址、路由、消息傳遞、狀態(tài)同步、錯(cuò)誤處理”這五件事不能由每個(gè)代理各自實(shí)現(xiàn)一套必須收斂到一個(gè)公共層來(lái)做。Agent-Reach的定位就是這個(gè)公共層。它不替代任何代理的決策能力也不約束代理內(nèi)部怎么運(yùn)行它只負(fù)責(zé)兩件事第一讓每個(gè)代理能夠注冊(cè)自己的能力和當(dāng)前可用狀態(tài)第二讓任意代理發(fā)出的一條觸達(dá)請(qǐng)求能夠在合適的時(shí)機(jī)、以合適的方式送達(dá)合適的代理并拿到響應(yīng)。這個(gè)設(shè)計(jì)原則聽(tīng)著簡(jiǎn)單落地的時(shí)候牽扯出的細(xì)節(jié)非常多。后面幾節(jié)我會(huì)逐一拆開(kāi)講這里先把一個(gè)最容易犯的誤區(qū)說(shuō)掉很多人一聽(tīng)“公共層”就覺(jué)得要搞成ESB企業(yè)服務(wù)總線(xiàn)那樣的大而全中間件把消息轉(zhuǎn)換、協(xié)議適配、流程編排全部塞進(jìn)去。我個(gè)人的強(qiáng)烈建議是——千萬(wàn)別這么做。Agent-Reach只做“觸達(dá)”層面的活凡是涉及到業(yè)務(wù)語(yǔ)義轉(zhuǎn)換、數(shù)據(jù)格式適配的東西都推給代理自己處理。公共層越薄越容易穩(wěn)定。2. Agent-Reach的架構(gòu)與協(xié)議設(shè)計(jì)2.1 控制面與數(shù)據(jù)面分離的總體思路Agent-Reach的架構(gòu)沿用了網(wǎng)絡(luò)設(shè)備里很經(jīng)典的控制面/數(shù)據(jù)面分離思路??刂泼尕?fù)責(zé)“知道該往哪發(fā)”數(shù)據(jù)面負(fù)責(zé)“把消息實(shí)實(shí)在在傳過(guò)去”。兩者分開(kāi)部署、分開(kāi)擴(kuò)容互不拖累。控制面核心是一個(gè)注冊(cè)中心。每個(gè)代理啟動(dòng)時(shí)向注冊(cè)中心發(fā)送自己的能力聲明Capability Declaration內(nèi)容包括代理ID、可處理的請(qǐng)求類(lèi)型、當(dāng)前負(fù)載等級(jí)、健康狀態(tài)、回調(diào)地址等。注冊(cè)中心把這些信息整理成路由表同時(shí)通過(guò)心跳機(jī)制持續(xù)探測(cè)每個(gè)代理的存活狀態(tài)。這里有一個(gè)關(guān)鍵設(shè)計(jì)路由表不是由某個(gè)中心節(jié)點(diǎn)“下發(fā)”給代理而是代理在需要觸達(dá)時(shí)實(shí)時(shí)查詢(xún)。我最初做成下發(fā)模式結(jié)果每次路由表更新都要推送幾十個(gè)代理消息風(fēng)暴和處理延遲都上來(lái)了。改成查詢(xún)模式之后只有真正發(fā)出觸達(dá)請(qǐng)求的代理才會(huì)去拉取路由信息壓力小了一個(gè)量級(jí)。數(shù)據(jù)面則是一套輕量級(jí)消息傳輸通道。每個(gè)代理通過(guò)Agent-Reach SDK與本地的Reach Node我習(xí)慣叫它觸達(dá)節(jié)點(diǎn)簡(jiǎn)稱(chēng)RN建立長(zhǎng)連接觸達(dá)請(qǐng)求統(tǒng)一交給RN轉(zhuǎn)發(fā)。RN之間可以點(diǎn)對(duì)點(diǎn)直連也可以走中轉(zhuǎn)具體路徑由控制面路由表決定。這里有個(gè)細(xì)節(jié)值得展開(kāi)觸達(dá)消息走的路徑和路由查詢(xún)走的路徑是完全獨(dú)立的兩條鏈路。路由查詢(xún)是HTTP同步請(qǐng)求觸達(dá)消息則是異步長(zhǎng)連接推送。為什么這么設(shè)計(jì)因?yàn)榇淼挠|達(dá)消息往往帶有明顯的請(qǐng)求-響應(yīng)特征但又不像傳統(tǒng)RPC那樣要求毫秒級(jí)返回——AI代理處理一條消息動(dòng)輒幾秒鐘同步阻塞等待的代價(jià)極高。異步推送配合后續(xù)的回執(zhí)機(jī)制才能把復(fù)雜的觸發(fā)鏈?zhǔn)秸{(diào)用撐起來(lái)。2.2 能力注冊(cè)與發(fā)現(xiàn)讓代理學(xué)會(huì)“自我介紹”注冊(cè)中心里存儲(chǔ)的不是簡(jiǎn)單的“代理地址表”而是能力目錄。每個(gè)代理在注冊(cè)時(shí)必須用一段結(jié)構(gòu)化描述聲明自己能處理什么類(lèi)型的請(qǐng)求。比如訂單代理可以聲明{ agent_id: agent-order-01, capabilities: [ { type: query_order, params_schema: { order_id: string, include_items: boolean }, return_schema: { status: string, order_info: object }, estimated_latency: 3s }, { type: cancel_order, params_schema: { order_id: string, reason: string }, danger_level: high, require_approval: true } ], load: 0.4, status: healthy }為什么必須用結(jié)構(gòu)化schema而不用自然語(yǔ)言描述我一開(kāi)始確實(shí)試過(guò)用自然語(yǔ)言寫(xiě)“我能查訂單、能取消訂單、能改收貨地址”讓發(fā)出的代理通過(guò)大模型去理解。效果非常不穩(wěn)定同一個(gè)描述在上下文長(zhǎng)度不同的時(shí)候被解析出來(lái)的結(jié)果差異很大。后來(lái)改成JSON Schema路由匹配用規(guī)則判斷而不是模型推理準(zhǔn)確率一下就到了99%以上。這也印證了前面說(shuō)的公共層的邏輯要盡量用確定性算法把大模型的靈活性留給代理內(nèi)部。2.3 觸達(dá)消息的三類(lèi)模式請(qǐng)求、廣播、回執(zhí)Agent-Reach的觸達(dá)消息定義了三種模式覆蓋了我在實(shí)際業(yè)務(wù)中遇到的所有協(xié)作場(chǎng)景。第一種是請(qǐng)求/響應(yīng)模式。代理A向代理B發(fā)送一條帶唯一消息ID的請(qǐng)求B處理后回復(fù)對(duì)應(yīng)ID的結(jié)果。這是最基礎(chǔ)的模式適合“幫我查一下訂單狀態(tài)”“把這份工單轉(zhuǎn)給售后”這類(lèi)一對(duì)一的協(xié)作。第二種是廣播/聚合模式。代理A向滿(mǎn)足某個(gè)能力條件的多個(gè)候選代理同時(shí)廣播消息然后收集所有回復(fù)再做聚合。這個(gè)模式特別適合做方案對(duì)比比如用戶(hù)問(wèn)“最快能什么時(shí)候發(fā)貨”發(fā)貨代理可以同時(shí)觸達(dá)庫(kù)存代理、物流代理和訂單代理把三家返回的時(shí)間拼在一起算最優(yōu)解。需要注意廣播模式對(duì)候選代理的篩選必須嚴(yán)格依賴(lài)能力目錄不要搞成全量廣播否則上下文和計(jì)算資源都會(huì)被白白吃掉。第三種是回執(zhí)/回調(diào)模式。代理A發(fā)出觸達(dá)請(qǐng)求后不等待立即返回而是告訴對(duì)方“算完了你回調(diào)我”。這種模式適合耗時(shí)長(zhǎng)、需要多步處理的任務(wù)比如售后代理讓報(bào)表代理生成一份復(fù)雜的分析報(bào)告報(bào)表代理可能要跑好幾分鐘不可能讓觸達(dá)通道一直保持占用。Agent-Reach里的回執(zhí)是可以攜帶上下文的也就是說(shuō)B在回調(diào)A時(shí)A可以要求附帶上一次交互的會(huì)話(huà)片段避免A這邊已經(jīng)聊過(guò)好幾輪之后B卻只拿著一份孤立結(jié)果回來(lái)導(dǎo)致A上下文斷層。3. 核心實(shí)現(xiàn)路由、隊(duì)列與超時(shí)控制這部分是整個(gè)Agent-Reach最花功夫的地方因?yàn)槲覐囊婚_(kāi)始就認(rèn)定理論設(shè)計(jì)再漂亮最終都要落實(shí)到具體的路由策略、隊(duì)列策略和超時(shí)策略上這三樣?xùn)|西直接決定了系統(tǒng)跑起來(lái)順不順。3.1 路由表的構(gòu)建能力匹配而非硬編碼地址Agent-Reach的路由匹配分兩層。第一層是類(lèi)型匹配根據(jù)請(qǐng)求里的capability_type在能力目錄里找到所有聲明支持的代理。第二層是約束過(guò)濾過(guò)濾條件包括代理當(dāng)前負(fù)載、健康狀態(tài)、距離上次心跳的時(shí)間、以及調(diào)用方指定的優(yōu)先級(jí)。約束過(guò)濾里面有一個(gè)很實(shí)用的手段給每個(gè)代理設(shè)置“最大并發(fā)觸達(dá)數(shù)”。一個(gè)正在處理五條請(qǐng)求的忙碌代理按理說(shuō)應(yīng)該從路由候選中摘掉但實(shí)操中發(fā)現(xiàn)完全摘掉也不行——有些能力只有這個(gè)代理有摘掉了請(qǐng)求就直接失敗。所以我改成了軟摘除機(jī)制不是不讓它接活而是給它降權(quán)。路由返回的候選列表會(huì)帶上每個(gè)代理的負(fù)載優(yōu)先級(jí)發(fā)起方可以自己決定是等待還是換一個(gè)。這個(gè)“降權(quán)而非摘除”的設(shè)計(jì)在代理數(shù)量少、能力高度專(zhuān)精的場(chǎng)景里非常救命。路由表還有個(gè)隱藏問(wèn)題緩存一致性。每個(gè)觸達(dá)節(jié)點(diǎn)都會(huì)本地緩存一份路由表來(lái)減少對(duì)注冊(cè)中心的高頻查詢(xún)但代理上下線(xiàn)、負(fù)載變化是隨時(shí)發(fā)生的緩存必然有滯后。我的做法是給每條緩存記錄加一個(gè)TTL同時(shí)注冊(cè)中心在代理狀態(tài)發(fā)生跳變時(shí)主動(dòng)推送一個(gè)增量更新信號(hào)。TTL設(shè)成5秒推送負(fù)責(zé)秒級(jí)響應(yīng)兩者配合實(shí)測(cè)下來(lái)路由信息的誤差窗口能控制在幾百毫秒以?xún)?nèi)。3.2 隊(duì)列與背壓別讓慢代理拖垮全局代理之間的處理速度天然是不均衡的。知識(shí)庫(kù)代理查一篇文檔可能只要一兩秒報(bào)表代理跑一份聚合分析可能要幾十秒如果前者瘋狂向后者的隊(duì)列里塞觸達(dá)消息后者的內(nèi)存和上下文池很快就爆了。Agent-Reach在每個(gè)觸達(dá)節(jié)點(diǎn)里維護(hù)了一個(gè)有界隊(duì)列隊(duì)列長(zhǎng)度上限是可配置的默認(rèn)1000條。當(dāng)隊(duì)列塞滿(mǎn)時(shí)新到的觸達(dá)請(qǐng)求不會(huì)直接丟而是進(jìn)入“背壓模式”——向發(fā)送方的RN返回一個(gè)明確的backpressure信號(hào)發(fā)送方收到這個(gè)信號(hào)后會(huì)暫緩發(fā)送同時(shí)降低自己的請(qǐng)求頻率。這里有個(gè)容易踩坑的細(xì)節(jié)很多初版實(shí)現(xiàn)為了省事背壓信號(hào)和正常響應(yīng)用的是同一個(gè)返回碼結(jié)果發(fā)送方傻傻分不清是“處理完了”還是“別發(fā)了”直接把語(yǔ)義搞亂。Agent-Reach里背壓和正常響應(yīng)在協(xié)議層就是兩種不同的消息類(lèi)型發(fā)送方一看到背壓就立刻切換策略絕不能混在一起。隊(duì)列的調(diào)度策略我最終選了帶優(yōu)先級(jí)的WQ加權(quán)隊(duì)列改進(jìn)型而不是最簡(jiǎn)單的FIFO。原因是客服場(chǎng)景里有些觸達(dá)請(qǐng)求是用戶(hù)在線(xiàn)等著答案的有些則是后臺(tái)異步任務(wù)兩者混在一個(gè)FIFO里會(huì)導(dǎo)致在線(xiàn)體驗(yàn)被后臺(tái)任務(wù)堵死。我給在線(xiàn)類(lèi)請(qǐng)求一個(gè)高優(yōu)先級(jí)權(quán)重后臺(tái)任務(wù)雖然也能進(jìn)隊(duì)列但出隊(duì)速率會(huì)被限流。實(shí)測(cè)下來(lái)高優(yōu)先級(jí)請(qǐng)求的P99延遲從6秒降到了2秒左右后臺(tái)任務(wù)的總吞吐量只損失了不到10%。3.3 超時(shí)與重試參數(shù)一組實(shí)測(cè)下來(lái)的穩(wěn)妥配置超時(shí)策略大概是這個(gè)項(xiàng)目里被問(wèn)得最多、也最容易被拍腦袋決定的部分。我給出自己測(cè)過(guò)的一組數(shù)據(jù)供參考但強(qiáng)烈建議大家按自己業(yè)務(wù)調(diào)整別直接抄。首先是單跳觸達(dá)超時(shí)從A的RN發(fā)出請(qǐng)求到B的RN確認(rèn)接收這一段的超時(shí)我設(shè)的是3秒。注意這里只算“確認(rèn)接收”不算“處理完成”因?yàn)锽接收之后可能要花很久做內(nèi)部推理那不屬于觸達(dá)層的職責(zé)。其次是處理完成超時(shí)A允許B在多長(zhǎng)時(shí)間內(nèi)返回結(jié)果我設(shè)的是60秒超過(guò)這個(gè)時(shí)間A會(huì)默認(rèn)這次觸達(dá)失敗并自行處理。這個(gè)值定得比較寬因?yàn)閳?bào)表類(lèi)代理確實(shí)可能跑很久寧可在業(yè)務(wù)層容忍慢也不能在觸達(dá)層過(guò)早掐斷。然后是重試參數(shù)這里踩坑最深。早期我設(shè)的是“失敗后重試3次間隔1秒、2秒、4秒”聽(tīng)著還算合理但在故障場(chǎng)景下這組參數(shù)會(huì)害死人。舉例訂單代理因?yàn)閿?shù)據(jù)庫(kù)連接池耗盡而拒絕新觸達(dá)結(jié)果另外五個(gè)代理同時(shí)發(fā)現(xiàn)它失敗每個(gè)代理按自己的節(jié)奏重試疊加起來(lái)等于每秒鐘有幾十條重試請(qǐng)求涌向訂單代理本來(lái)還能慢慢恢復(fù)的連接池直接被擊穿。后來(lái)我把重試策略改成了指數(shù)退避抖動(dòng)首次重試等2秒之后按指數(shù)翻倍并在上下30%范圍內(nèi)隨機(jī)抖動(dòng)單次任務(wù)的總體重試上限是3次。同時(shí)加了一條關(guān)鍵約束——重試只允許在發(fā)送方RN這一層發(fā)生不允許業(yè)務(wù)代理無(wú)限重試。你可能會(huì)問(wèn)為什么不讓業(yè)務(wù)代理感知重試因?yàn)闃I(yè)務(wù)代理感知得越多每一層都可能自作主張重試重試風(fēng)暴就是這么來(lái)的。把重試收斂到公共層之后行為可控多了故障期間的請(qǐng)求量立刻平穩(wěn)下來(lái)。4. 我踩過(guò)的三個(gè)大坑及完整排查過(guò)程4.1 循環(huán)調(diào)用A找BB找CC又找回A這個(gè)坑是Agent-Reach上線(xiàn)后遇到的第一起線(xiàn)上事故印象極其深刻。起因是一個(gè)新加入的運(yùn)營(yíng)配置小助手代理它發(fā)現(xiàn)用戶(hù)問(wèn)“我的優(yōu)惠券為什么不能用”時(shí)自己拿不準(zhǔn)于是觸達(dá)了訂單代理。訂單代理為了確認(rèn)優(yōu)惠券對(duì)應(yīng)的訂單狀態(tài)又觸達(dá)了運(yùn)營(yíng)配置代理——因?yàn)樗J(rèn)為優(yōu)惠券規(guī)則是運(yùn)營(yíng)配置代理維護(hù)的。兩個(gè)代理就這么你來(lái)我往每條消息都有合法的唯一ID每次觸達(dá)都在超時(shí)時(shí)間內(nèi)返回從單次看沒(méi)有任何問(wèn)題。但半小時(shí)后這兩條代理通道之間的消息積壓了好幾萬(wàn)條系統(tǒng)資源直線(xiàn)飆升。排查鏈路是這樣的我先在監(jiān)控面板上看到這兩個(gè)代理的RN隊(duì)列長(zhǎng)度異常隨后在日志里抓到了它們互相觸達(dá)的消息軌跡打印出來(lái)一看一個(gè)完整的循環(huán)鏈條清晰可見(jiàn)。當(dāng)時(shí)第一反應(yīng)是加“每個(gè)請(qǐng)求最大跳數(shù)”的限制但這個(gè)方案只能止損不能根治——因?yàn)檠h(huán)觸達(dá)的消息ID每次都是新的跳數(shù)限制只能限制鏈條長(zhǎng)度沒(méi)法阻止它們持續(xù)生成新消息。真正的修復(fù)是兩步。第一步在每個(gè)代理的上下文里顯式聲明**“我當(dāng)前正在處理的任務(wù)上下文”**觸達(dá)請(qǐng)求必須攜帶這個(gè)上下文ID當(dāng)接收方發(fā)現(xiàn)自己已經(jīng)在上下文中處理過(guò)這條鏈路時(shí)直接返回一個(gè)“dependency loop detected”的信號(hào)不再繼續(xù)往下傳遞。第二步更底層注冊(cè)中心會(huì)根據(jù)每個(gè)代理過(guò)去5分鐘內(nèi)的行為計(jì)算“依賴(lài)熵”如果發(fā)現(xiàn)兩個(gè)代理之間存在高頻雙向觸達(dá)自動(dòng)在控制面打出告警并降級(jí)其中一條觸達(dá)鏈路的優(yōu)先級(jí)。這套組合拳上線(xiàn)后再也沒(méi)出現(xiàn)過(guò)循環(huán)風(fēng)暴。4.2 狀態(tài)不同步導(dǎo)致“幽靈觸達(dá)”代理以為會(huì)話(huà)已結(jié)束第二個(gè)坑和代理的會(huì)話(huà)狀態(tài)管理有關(guān)問(wèn)題隱藏得很深花了將近兩周才徹底定位清楚?,F(xiàn)象是用戶(hù)在網(wǎng)頁(yè)端關(guān)閉了對(duì)話(huà)窗口但后臺(tái)仍然每隔幾分鐘就有一條觸達(dá)消息從訂單代理發(fā)往知識(shí)庫(kù)代理去查那些已經(jīng)沒(méi)人關(guān)心的文檔。第一直覺(jué)是前端沒(méi)有正確發(fā)送會(huì)話(huà)關(guān)閉信號(hào)但我翻遍了前端代碼信號(hào)確實(shí)發(fā)了。接著懷疑是會(huì)話(huà)狀態(tài)存到了Redis但沒(méi)設(shè)置過(guò)期時(shí)間翻代碼發(fā)現(xiàn)也做了。直到我靜下心打印了訂單代理的完整日志鏈路才發(fā)現(xiàn)問(wèn)題出在代理內(nèi)部的對(duì)話(huà)循環(huán)機(jī)制上訂單代理處理用戶(hù)問(wèn)題時(shí)先自己推理了一輪然后觸達(dá)知識(shí)庫(kù)代理知識(shí)庫(kù)返回之后訂單代理又做了一輪推理并輸出了回復(fù)。用戶(hù)關(guān)閉窗口后訂單代理的推理循環(huán)并沒(méi)有被外部信號(hào)打斷——它認(rèn)為“這次對(duì)話(huà)還沒(méi)完成我的回復(fù)用戶(hù)還沒(méi)看到”于是繼續(xù)執(zhí)行后續(xù)的觸達(dá)計(jì)劃。說(shuō)白了觸達(dá)請(qǐng)求的生命周期綁定錯(cuò)了對(duì)象。它應(yīng)該跟隨“用戶(hù)會(huì)話(huà)”的生命周期而不是跟隨“代理推理循環(huán)”的生命周期。修復(fù)方案是在Agent-Reach的SDK里增加了一個(gè)會(huì)話(huà)心跳檢查接口任何代理在發(fā)出觸達(dá)請(qǐng)求前必須先校驗(yàn)當(dāng)前會(huì)話(huà)是否仍然活躍如果會(huì)話(huà)已關(guān)閉則直接中止后續(xù)動(dòng)作并回收上下文資源。同時(shí)我在注冊(cè)中心加了一份“會(huì)話(huà)活躍表”由業(yè)務(wù)前端在會(huì)話(huà)生命周期變化時(shí)推送狀態(tài)。這個(gè)修復(fù)除了解決幽靈觸達(dá)還意外地省了一大筆模型上下文費(fèi)用——之前那些無(wú)效觸達(dá)背后都跟著一輪又一輪的模型請(qǐng)求錢(qián)就是這么悄悄流走的。4.3 重試風(fēng)暴一次瞬時(shí)故障引發(fā)的連鎖雪崩第三個(gè)坑就是前面3.3節(jié)提到的重試風(fēng)暴那次事故的完整排查過(guò)程值得再細(xì)說(shuō)一遍。那天訂單數(shù)據(jù)庫(kù)發(fā)生了一次40秒的主從切換按理說(shuō)業(yè)務(wù)影響不應(yīng)該太大但Agent-Reach初版的重試策略立刻讓事態(tài)升級(jí)。先是訂單代理收到數(shù)據(jù)庫(kù)連接失敗它自己內(nèi)部有一個(gè)重試機(jī)制嘗試了3次。與此同時(shí)客服主代理、售后代理、庫(kù)存代理都在自己的推理流程里觸達(dá)了訂單查詢(xún)接口每個(gè)代理都各自重試每輪重試失敗之后還觸發(fā)下一輪的“重新分析”。最多的時(shí)候四個(gè)代理同時(shí)對(duì)訂單代理發(fā)出每秒上百次的查詢(xún)觸達(dá)。我在監(jiān)控面板上看到訂單代理的請(qǐng)求量曲線(xiàn)像坐火箭一樣往上沖第一時(shí)間把業(yè)務(wù)代理的入口全部切到了降級(jí)模式才把請(qǐng)求量按下來(lái)。事后復(fù)盤(pán)時(shí)我畫(huà)了一條請(qǐng)求量時(shí)間線(xiàn)系統(tǒng)正常時(shí)峰值請(qǐng)求量是每秒30次左右故障期間最高沖到每秒470多次其中真正用戶(hù)產(chǎn)生的請(qǐng)求只有不到5%剩下全是代理之間的自愈性重試。這次事故之后我把重試機(jī)制整個(gè)重構(gòu)了一遍所有代理SDK內(nèi)置的重試邏輯全部移除統(tǒng)一交給RN層處理。RN層設(shè)置每對(duì)觸達(dá)關(guān)系獨(dú)立的重試計(jì)數(shù)器而不是全局共享一個(gè)同時(shí)引入故障熔斷——如果某個(gè)目標(biāo)代理在10秒內(nèi)的失敗率超過(guò)50%RN會(huì)直接短路所有指向它的觸達(dá)請(qǐng)求快速失敗返回給上游而不是繼續(xù)傻等或者傻重試。熔斷狀態(tài)每30秒嘗試放行一次探測(cè)請(qǐng)求成功一次就慢慢恢復(fù)流量。這些機(jī)制組合起來(lái)之后再做了一次故障演練同樣殺訂單數(shù)據(jù)庫(kù)40秒系統(tǒng)P99響應(yīng)時(shí)間只從1.8秒升到了4.2秒沒(méi)有出現(xiàn)任何雪崩。5. 權(quán)限與安全邊界代理能觸達(dá)的不等于可以執(zhí)行多代理系統(tǒng)帶來(lái)的一個(gè)隱性問(wèn)題是觸達(dá)范圍的擴(kuò)大直接等于攻擊面的擴(kuò)大。在一個(gè)單體應(yīng)用里一個(gè)函數(shù)能不能調(diào)用另一個(gè)函數(shù)編譯器替你管著在多代理系統(tǒng)里每次觸達(dá)都是一次跨信任邊界的調(diào)用必須有明確的權(quán)限控制。5.1 信任分級(jí)與令牌傳遞Agent-Reach把代理之間的信任關(guān)系分成三個(gè)級(jí)別。L1級(jí)是核心代理群包括訂單、支付這些涉及到錢(qián)的代理它們之間的觸達(dá)必須使用雙向mTLS認(rèn)證同時(shí)每次觸達(dá)請(qǐng)求都要附帶由注冊(cè)中心簽發(fā)的短期令牌。L2級(jí)是常規(guī)業(yè)務(wù)代理觸達(dá)時(shí)只需要單向認(rèn)證加令牌。L3級(jí)是外部插件代理它們?cè)谏诚洵h(huán)境中運(yùn)行只允許通過(guò)受限的API接口對(duì)外觸達(dá)且所有觸達(dá)內(nèi)容都要過(guò)內(nèi)容審計(jì)。令牌傳遞的設(shè)計(jì)我踩過(guò)一次坑最初為了省事令牌的有效期設(shè)成了24小時(shí)結(jié)果某天一個(gè)代理的日志泄露到了測(cè)試環(huán)境攻擊者直接拿著有效令牌在內(nèi)部網(wǎng)絡(luò)里橫向觸達(dá)了好幾個(gè)代理。后來(lái)我把令牌有效期縮到15分鐘同時(shí)規(guī)定令牌只能由發(fā)起觸達(dá)的代理持有禁止目標(biāo)代理將令牌轉(zhuǎn)發(fā)給第三方。要做級(jí)聯(lián)觸達(dá)時(shí)目標(biāo)代理必須重新向注冊(cè)中心申請(qǐng)新的令牌而不是復(fù)用上游傳來(lái)的令牌。這樣每個(gè)跳轉(zhuǎn)鏈路都是獨(dú)立的憑證鏈單一環(huán)泄露不會(huì)導(dǎo)致整條鏈?zhǔn)Э亍?.2 高危動(dòng)作的雙人復(fù)核閘門(mén)能力目錄里我在聲明時(shí)加了一個(gè)danger_level字段就是為了在觸達(dá)層做高危操作的攔截。比如“取消訂單”“發(fā)起退款”“刪除工單”這些操作不允許因?yàn)槟硞€(gè)代理被另一個(gè)代理觸達(dá)就能直接執(zhí)行。Agent-Reach里實(shí)現(xiàn)了一個(gè)復(fù)核閘門(mén)機(jī)制當(dāng)目標(biāo)代理接收到danger_levelhigh的觸達(dá)請(qǐng)求時(shí)它不會(huì)直接執(zhí)行操作而是向操作審批服務(wù)發(fā)起一個(gè)復(fù)核請(qǐng)求。審批服務(wù)會(huì)在企業(yè)IM群里拉起一條審批卡片需要指定的業(yè)務(wù)負(fù)責(zé)人點(diǎn)擊確認(rèn)后操作才會(huì)繼續(xù)執(zhí)行。這個(gè)看起來(lái)“不AI”的設(shè)計(jì)實(shí)際運(yùn)行中反而給整個(gè)系統(tǒng)帶來(lái)了更多信任度——業(yè)務(wù)方終于不再把代理之間的自動(dòng)觸達(dá)看成黑盒怪物了。有一件印象很深的事上線(xiàn)復(fù)核閘門(mén)的第一周某天下午突然有一條“批量發(fā)送優(yōu)惠券”的高危觸達(dá)請(qǐng)求發(fā)起復(fù)核審批群里一個(gè)主管點(diǎn)了同意事后發(fā)現(xiàn)其實(shí)是運(yùn)營(yíng)誤操作觸發(fā)了一個(gè)錯(cuò)誤的消息鏈路。因?yàn)橛写竽P蛥⑴c的推理過(guò)程本身可能有幻覺(jué)這種閘門(mén)給了人類(lèi)一個(gè)糾錯(cuò)窗口成本和收益相比完全劃算。6. 性能實(shí)測(cè)與調(diào)優(yōu)記錄最后用實(shí)測(cè)數(shù)據(jù)收個(gè)尾這部分直接關(guān)系到Agent-Reach到底能不能扛住真實(shí)業(yè)務(wù)流量。6.1 端到端延遲與吞吐量壓測(cè)壓測(cè)環(huán)境是20個(gè)代理節(jié)點(diǎn)分布在同一地域的三臺(tái)服務(wù)器上每臺(tái)服務(wù)器跑了一個(gè)RN節(jié)點(diǎn)和6-7個(gè)代理進(jìn)程。壓測(cè)場(chǎng)景模擬了客服系統(tǒng)的混合負(fù)載60%的簡(jiǎn)單觸達(dá)單跳查詢(xún)30%的鏈?zhǔn)接|達(dá)兩跳A→B→C10%的廣播聚合觸達(dá)。結(jié)果是場(chǎng)景QPSP50延遲P95延遲P99延遲單跳觸達(dá)50045ms90ms180ms兩跳鏈?zhǔn)接|達(dá)200220ms500ms920ms廣播聚合5個(gè)目標(biāo)501.2s2.4s3.6s注意這里測(cè)的只是觸達(dá)層的延遲不包含代理內(nèi)部調(diào)用大模型的時(shí)間。實(shí)際生產(chǎn)環(huán)境中跨代理調(diào)用大模型會(huì)把P95拉到十幾秒這時(shí)候觸達(dá)層的幾百毫秒延遲基本被淹沒(méi)但它決定了系統(tǒng)的穩(wěn)定上限——如果觸達(dá)層都在抖后面的大模型推理就不可能穩(wěn)定。我個(gè)人的判斷標(biāo)準(zhǔn)是觸達(dá)層P95延遲必須控制在代理自身推理時(shí)間的十分之一以?xún)?nèi)這層才算是合格的。6.2 三個(gè)最有效的調(diào)優(yōu)點(diǎn)調(diào)優(yōu)過(guò)程中有三個(gè)改動(dòng)效果特別明顯給后面想做類(lèi)似系統(tǒng)的人一個(gè)參照。第一個(gè)是把路由查詢(xún)從同步阻塞改成異步預(yù)取。最初代理發(fā)出觸達(dá)請(qǐng)求時(shí)RN會(huì)先去注冊(cè)中心查路由查完再轉(zhuǎn)發(fā)整個(gè)過(guò)程是串行的。后來(lái)改成RN本地緩存路由表并周期性預(yù)取絕大多數(shù)觸達(dá)請(qǐng)求不需要再實(shí)時(shí)訪(fǎng)問(wèn)注冊(cè)中心單跳延遲直接降了60%以上。第二個(gè)是使用連接池復(fù)用來(lái)替代每條消息新建連接。這個(gè)是很樸素但收益巨大的一步。RN之間的長(zhǎng)連接保持Time wait狀態(tài)處理得好不好直接影響系統(tǒng)的并發(fā)上限。我專(zhuān)門(mén)看了一下連接復(fù)用之前的TIME_WAIT套接字?jǐn)?shù)量在高峰期能到3000多復(fù)用之后降到幾十。第三個(gè)是壓縮廣播聚合的冗余回執(zhí)。廣播模式很容易出現(xiàn)5個(gè)目標(biāo)代理都回了內(nèi)容但最終聚合時(shí)只用到2-3個(gè)結(jié)果的情況。我從兩個(gè)方向優(yōu)化一是在廣播請(qǐng)求里附加“已有結(jié)果摘要”其他目標(biāo)代理看到摘要后可以決定是否還要認(rèn)真計(jì)算二是聚合階段做了流式處理先返回的結(jié)果先參與判定如果前兩個(gè)結(jié)果已經(jīng)能形成明確答案就不再等待剩余回執(zhí)。這兩個(gè)優(yōu)化讓廣播聚合的平均完成時(shí)間縮短了大約1倍。Agent-Reach這個(gè)項(xiàng)目的核心收獲說(shuō)來(lái)說(shuō)去就是一句話(huà)多代理協(xié)作的復(fù)雜度不以代理數(shù)量線(xiàn)性增長(zhǎng)而是以觸達(dá)關(guān)系的數(shù)量指數(shù)級(jí)增長(zhǎng)。與其把精力花在讓每個(gè)代理變得更聰明上不如先確保代理之間的每一條通路都可靠、可控、可觀測(cè)。我現(xiàn)在已經(jīng)不太擔(dān)心某個(gè)代理的推理能力不夠了反而更擔(dān)心哪條觸達(dá)鏈路在無(wú)人注意的角落里悄悄累積風(fēng)險(xiǎn)。這個(gè)項(xiàng)目后續(xù)我會(huì)接著做故障注入演練和更精細(xì)的鏈路追蹤等有新的進(jìn)展再寫(xiě)文章分享。