一Agent觸達層的架構(gòu)設(shè)計與實踐)
1. 為什么要做Agent-Reach三個Agent就已經(jīng)亂成一鍋粥1.1 每個Agent都在“自己找路”我最初接觸Agent-Reach這個概念不是因為看了什么新框架的文檔而是被自己手里的多Agent系統(tǒng)整崩潰了。當時我們的AI協(xié)作群里掛了三個Agent一個負責輿情監(jiān)控一個負責工單回復(fù)一個負責數(shù)據(jù)分析。聽起來分工明確但問題全藏在“觸達”這一步。輿情Agent要讀新聞RSS、要抓網(wǎng)頁快照、還要把結(jié)果寫進數(shù)據(jù)庫工單Agent要調(diào)CRM接口、要發(fā)郵件、要回企業(yè)微信數(shù)據(jù)分析Agent更麻煩同時要連ClickHouse、要跑定時任務(wù)、還要把SQL結(jié)果推給BI系統(tǒng)。每個Agent都在用自己的方式做HTTP調(diào)用、做鑒權(quán)、做重試、做連接池管理。結(jié)果就是同一個CRM系統(tǒng)三個Agent各寫了一套Client同一個數(shù)據(jù)庫連接串在四個配置文件里反復(fù)出現(xiàn)某個內(nèi)網(wǎng)服務(wù)一抖動三個Agent的告警群里同時響。這不是我不夠勤快而是這種“每個Agent自己找路”的模式在數(shù)量一旦超過兩三個之后復(fù)雜度是指數(shù)級上漲的。你每接入一個新系統(tǒng)就要在N個Agent里各寫一遍適配代碼每改一次連接參數(shù)就要去N個Agent的配置中心里各改一遍。真正讓我決定動手做一個統(tǒng)一觸達層的觸發(fā)點是我發(fā)現(xiàn)排查一個“為什么工單Agent沒拿到用戶信息”的問題排查了整整兩個小時最后發(fā)現(xiàn)是數(shù)據(jù)分析Agent自己在重試時把目標API打滿了。連坐效應(yīng)已經(jīng)完全失控了。1.2 連接不應(yīng)該由Agent自己負責“Agent-Reach”這個名字拆開看就是“Agent的觸達”。我當時給自己定的目標是從今往后任何Agent都不直接知道下游服務(wù)的地址、密鑰和超時參數(shù)。它只需要聲明“我要做什么”觸達層負責“怎么連到那個地方、怎么保證安全、怎么失敗的”。這個思路很像微服務(wù)網(wǎng)關(guān)在服務(wù)領(lǐng)域里的角色。以前我們做服務(wù)治理都會把熔斷、限流、鑒權(quán)從業(yè)務(wù)代碼里抽出來扔給網(wǎng)關(guān)和Service Mesh。但到了Agent時代大家反而退回了一步每個Agent又變成了一個“業(yè)務(wù)服務(wù)全能客戶端”的巨無霸。Agent-Reach本質(zhì)上就是把那些所有Agent都會反復(fù)用到的連接能力下沉成一個獨立服務(wù)層。這一層要做的事情很明確把下游系統(tǒng)抽象成一個個“連接器”Agent不需要知道它是REST、gRPC還是JDBC。統(tǒng)一管控所有出站流量誰來調(diào)、能調(diào)什么、配額是多少、峰值是多少。把可觀測性集中在一起所有Agent觸達外部系統(tǒng)的日志、監(jiān)控、鏈路追蹤都從這兒過。我并不是說Agent不能保留少數(shù)“私有連接”。但在做一個平臺級的產(chǎn)品或者一個公司內(nèi)部的數(shù)字員工體系時如果沒有一個統(tǒng)一觸達層那么每增加一個Agent都等于給整個系統(tǒng)增加一份新的維護債。1.3 Agent-Reach的邊界當然Agent-Reach不是萬能的。我在設(shè)計之初就劃了一條邊界它只負責“觸達”不負責“思考”。大模型的調(diào)用、提示詞編排、決策邏輯這些仍然歸屬于Agent本身。Agent-Reach不會判斷“這個問題該不該回復(fù)”它只負責把Agent已經(jīng)決定要執(zhí)行的動作安全、穩(wěn)定、可控地送到對應(yīng)的外部系統(tǒng)里。因為邊界清晰所以可替換性也就強。如果某天我換了一個Agent框架只要這些Agent還說著同樣的觸達協(xié)議Agent-Reach這邊一行都不用改。反過來也一樣如果我把某個下游系統(tǒng)從自建工單換成第三方SaaS則只需要在Agent-Reach里面換一個連接器所有Agent都會自動切過去。換句話說Agent-Reach想解決的核心矛盾是Agent本身是可折疊的今天用A框架明天用B框架下游系統(tǒng)也是可更換的今天用這家云服務(wù)明天用另一家。只有觸達層必須是穩(wěn)的。穩(wěn)到所有Agent都能依賴它而不是各自再想辦法繞過它。2. Agent-Reach的架構(gòu)設(shè)計與核心組件2.1 連接器注冊中心先把能力登記起來Agent-Reach的第一塊地基是連接器注冊中心。我把它理解成一個“能力超市”的貨架每個下游系統(tǒng)在接入時都會以連接器的身份在注冊中心里登記自己支持的動作、支持的協(xié)議類型、需要的入?yún)⒏袷?、以及健康檢查的方式。注冊中心不存業(yè)務(wù)數(shù)據(jù)只存元數(shù)據(jù)。我使用了類似這樣的結(jié)構(gòu){ name: crm.contact.read, connector: crm-connector, protocol: http, endpoint: internal://contact-center/contacts/{id}, method: GET, auth_scheme: vault://secrets/crm/internal-token, timeout_ms: 1500, replay_policy: no-cache, health_check: { type: http_ping, path: /healthz, interval_ms: 30000 } }這個結(jié)構(gòu)里最關(guān)鍵的一條設(shè)計是“動作名”與“物理接口”分離。Agent只認識crm.contact.read這個語義化動作它不需要關(guān)心背后是HTTP還是某個私有協(xié)議。注冊中心在運行期根據(jù)動作名找到對應(yīng)的連接器實例和物理端點然后完成真正的調(diào)用。每個連接器在啟動時會向注冊中心發(fā)起心跳同時上報自己的版本、健康狀態(tài)、支持的動作列表。注冊中心把這些信息緩存在內(nèi)存里同時持久化一份到關(guān)系庫方便后續(xù)做審計和回溯。我用了很輕的ETCD做實時路由表沒有引入特別重的服務(wù)網(wǎng)格。對于Agent-Reach當前的體量ETCD已經(jīng)綽綽有余。2.2 統(tǒng)一請求協(xié)議讓所有Agent說同一種話在Agent-Reach里Agent發(fā)來的請求統(tǒng)一是這個格式{ request_id: ar_20250123_00000001, agent_id: agent-monitor-01, tenant: internal, action: crm.contact.read, params: {id: C-10086}, context: { trace_id: trace-7f9c123, user_stamp: batch_job|weekly-report, deadline_ms: 2000 } }看到?jīng)]有“上下文”里我不只帶了trace_id還帶了user_stamp。這很重要。因為Agent的執(zhí)行往往是批量、異步的下游系統(tǒng)在審計時很想知道“這次操作是哪個Agent、由哪個上游事件觸發(fā)的、最終是為誰服務(wù)的”。如果這類信息不統(tǒng)一帶上以后出了問題根本不知道找誰。統(tǒng)一請求協(xié)議的價值不在于格式好看而在于所有通用能力都能圍繞這一個格式做。協(xié)議里帶了deadline_msAgent-Reach就能統(tǒng)一做超時控制帶了request_id就能做冪等和去重帶了trace_id就能把整條鏈路串進日志系統(tǒng)。這些都是Agent以前各自為戰(zhàn)時做不到的。我最后選擇的是JSON而不是Protobuf作為線格式原因很樸素所有Agent的SDK都原生支持JSONDebug非常方便而且我們連接器的數(shù)量還不至于大到大流量高并發(fā)需要極致壓縮。等到以后如果真的出現(xiàn)性能瓶頸再在外層套一層二進制協(xié)議也不遲。2.3 鑒權(quán)、限流與用量預(yù)算不能裸奔出去Agent-Reach的另一個核心組件是策略引擎負責在請求真正送出之前做最后一道把關(guān)。每個連接器都可以綁定獨立的鑒權(quán)方式。有的下游系統(tǒng)用的是靜態(tài)Token有的是AK/SK簽名有的走內(nèi)部SSO。在Agent-Reach里這些信用憑據(jù)統(tǒng)一從Vault拉取不落盤到任何Agent的配置里。Agent側(cè)只知道“我可以調(diào)這個動作”至于密鑰是什么Agent不知道也不應(yīng)該知道。限流和用量預(yù)算則是另一道保障。我給每個Agent配了一個“月預(yù)算”例如“輿情監(jiān)控Agent每月最多調(diào)用TWITTER采集接口1萬次”。在注冊中心里這個配額被拆成了分鐘級令牌桶。一旦某個Agent的消耗速度超過預(yù)算Agent-Reach不會直接拒絕而是降級返回“預(yù)算不足請稍后再試”這樣Agent可以把自己的任務(wù)排隊或者拆分。這個設(shè)計參考了云廠商API Gateway的做法但做了一個針對Agent的調(diào)整我們不只按API Key來限制還按“Agent意圖”來限制。比如某個Agent發(fā)起的data.export動作我們會同時檢查目標表有沒有敏感字段、執(zhí)行時間段是否在允許窗口內(nèi)、以及這個導(dǎo)出的數(shù)據(jù)量是否超過單次上限。三道檢查都過了請求才會被放行。3. 從Agent調(diào)用到連接器執(zhí)行一個請求的完整旅程3.1 目錄路由是怎么匹配的Agent-Reach最核心的執(zhí)行鏈路是從接收請求到完成調(diào)用的那條“主徑”。我不畫復(fù)雜圖直接說實際運行的步驟。Agent通過SDK向Agent-Reach的網(wǎng)關(guān)節(jié)點發(fā)送統(tǒng)一協(xié)議包。網(wǎng)關(guān)先解析請求的基本信息校驗簽名和租戶身份然后把請求轉(zhuǎn)發(fā)給路由節(jié)點。路由節(jié)點以action字段為Key去注冊中心查連接器元數(shù)據(jù)。如果查不到直接返回“動作不存在”。路由節(jié)點把params里的參數(shù)和連接器的參數(shù)Schema做一次校驗缺參數(shù)或類型不符的在校驗階段就打回不給下游系統(tǒng)添亂。參數(shù)校驗通過后請求被包裝成內(nèi)部DAG節(jié)點交給執(zhí)行引擎。執(zhí)行引擎根據(jù)連接器協(xié)議類型選擇合適的調(diào)用器HTTP調(diào)用器、SQL調(diào)用器、消息隊列調(diào)用器并補全憑據(jù)和超時信息。這一步是整個鏈路里最容易出錯的地方。調(diào)用完成后把下游系統(tǒng)的響應(yīng)轉(zhuǎn)成統(tǒng)一格式返回給Agent。如果調(diào)用失敗則根據(jù)策略決定是否重試、是否降級、是否返回錯誤體。路由匹配看起來簡單真正麻煩的地方在于“模糊動作”而不是“精確動作”。比如Agent請求的是crm.contact.query但我們只注冊了crm.contact.read這時候要不要轉(zhuǎn)我的答案是“不要默認轉(zhuǎn)”。寧可讓Agent明確說出自己的意圖也不能讓路由層自作聰明。自作聰明是各種詭異線上問題的主要來源。3.2 連接器的健康檢查與自動摘除連接器的健康檢查是保證Agent不會在一根斷掉的管子上反復(fù)摩擦的關(guān)鍵。每個連接器實例每隔30秒向注冊中心上報一次健康狀態(tài)。上報信息包括最近成功率、平均延遲、當前隊列長度、以及對該下游系統(tǒng)可用性的主觀判斷。注冊中心不只看實例自己的說法還會通過Agent-Reach自身發(fā)出的“探活請求”來交叉驗證。這里有個細節(jié)值得拿出來說。早期我發(fā)現(xiàn)連接器的CPU很空閑但它實際依賴的那個下游系統(tǒng)已經(jīng)快不行了。如果只看連接器本身機器是健康的但看Agent-Reach發(fā)出的探測請求延遲已經(jīng)飆升到了原來10倍。所以后來我們改成雙層健康檢查第一層看連接器的計算資源第二層由Agent-Reach定時對下游系統(tǒng)發(fā)一個輕量級的探針請求。兩層都健康才標記為“可用”。一旦某個連接器連續(xù)三次健康檢查失敗注冊中心會在路由表里把它自動摘除。摘除不是粗暴地停用而是先把連接器的流量權(quán)重降為0讓新請求不再路由過去同時保留它處理在途請求的能力。這個做法在消息隊列連接器上特別重要因為你不能把一個還有積壓消息的連接器直接殺掉。3.3 自定義連接器15分鐘接入一個新系統(tǒng)接入一個新系統(tǒng)是Agent-Reach日常迭代里最高頻的折騰。為了把這件事的復(fù)雜度降下來我定義了一套非常小的連接器接口。一個連接器本質(zhì)上是一個實現(xiàn)了三個方法的小服務(wù)class BaseConnector: def declare_actions(self): 返回這個連接器支持的語義動作列表 pass def validate_params(self, action, params): 校驗參數(shù)格式和必填字段 pass def invoke(self, action, params, context): 真正執(zhí)行下游系統(tǒng)的調(diào)用 pass只要實現(xiàn)這三個方法再在啟動時連上注冊中心這個連接器就能被Agent-Reach使用了。我當時用這個接口接一個內(nèi)部郵件系統(tǒng)從寫代碼到在測試環(huán)境調(diào)通整個流程大概花了15分鐘。當然前提是我已經(jīng)對郵件系統(tǒng)的接口足夠熟悉否則主要的時間是花在看對方文檔上。自定義連接器最大的好處是你不必等Agent-Reach官方支持每一個系統(tǒng)。團隊里任何人遇到一套新系統(tǒng)都可以按這個規(guī)范寫一個連接器獨立測試通過后注冊上線。寫得好不好另說至少不會污染Agent的核心邏輯。4. 上線后的三個大坑超時、重試風暴與身份錯亂4.1 超時時間設(shè)短了Agent全在“無效焦慮”Agent-Reach上線不久我設(shè)了一個自以為合理的默認超時1.5秒。理由很簡單大部分內(nèi)部HTTP服務(wù)平時響應(yīng)都在幾百毫秒內(nèi)給1.5秒已經(jīng)算寬容了。結(jié)果第二天好幾個Agent同時開始報錯。一查日志全是deadline_exceeded。連的那個工單系統(tǒng)很老某個查詢接口平時沒問題但每天早晨會有一次批量數(shù)據(jù)預(yù)熱在那個窗口里單次查詢可能要跑到5秒。Agent-Reach嚴格執(zhí)行了1.5秒的截止時間把那些本該成功的請求全攔了下來。更麻煩的是Agent一看超時就會啟動自己的重試邏輯結(jié)果下游系統(tǒng)的壓力反而變大了。這個坑的根子在于“統(tǒng)一超時”這個想法本身。你說它不對吧全局設(shè)默認超時確實重要你說它對又不能一刀切。后來我把超時拆成三個值連接器級默認超時、動作級超時、單次請求超時。Agent在請求的context.deadline_ms里可以覆蓋動作級超時但必須池于連接器的可配置上限。上線前還專門跑了一遍“慢調(diào)用模擬”把下游服務(wù)的響應(yīng)時間人為拉長到各個檔位驗證超時策略的表現(xiàn)在預(yù)期內(nèi)。4.2 重試風暴好心辦了壞事Agent-Reach里一開始建了全局重試機制連接失敗或5xx錯誤碼會自動重試最多2次且采用簡單的固定間隔??雌饋砥狡綗o奇卻差點出了大事。某個周五下午數(shù)據(jù)分析Agent集中發(fā)起了一批報表導(dǎo)出任務(wù)。它先請求Agent-Reach去讀取一批歷史數(shù)據(jù)其中一個下游系統(tǒng)恰好在做全量索引重建響應(yīng)非常慢。Agent-Reach看到第一個請求超時觸發(fā)重試重試再超時再重試。其他Agent并不知道這個下游在抖也在同時發(fā)出大量類似請求。于是這個系統(tǒng)的入口網(wǎng)關(guān)被幾倍于平時的流量打垮索引重建因此也掛得更徹底最后變成了一場“越重試越失敗、越失敗越重試”的雪崩。教訓(xùn)很直接重試必須有上限上限之外必須慎重考慮目的。后來我把所有連接器分成了三類冪等寫類、非冪等寫類、讀類。讀類和冪等寫類允許有限重試但間隔使用指數(shù)退避并增加隨機抖動非冪等寫類一律不自動重試只上報需要人工介入的事件。另外我還加了一個“連接器熔斷”機制如果連接器的在途調(diào)用失敗率在1分鐘內(nèi)超過20%該連接器會自動進入半開狀態(tài)只允許少量探測請求通過其他請求直接快速失敗并緩存在Agent側(cè)。4.3 連接上下文丟失后端的身份全亂套了如果說前兩個坑是大流量帶來的那這個坑純粹是設(shè)計上的疏漏。我最初設(shè)計的統(tǒng)一協(xié)議里雖然有agent_id和tenant但某些老連接器在把請求轉(zhuǎn)發(fā)給下游系統(tǒng)時沒有把context里的身份信息透傳下去。也就是說下游系統(tǒng)看到的來源是Agent-Reach的通用IP而不是具體受哪個Agent在調(diào)用。后果很快顯現(xiàn)安全團隊來找我說某個內(nèi)部系統(tǒng)的審計日志里所有Agent-Reach的訪問都顯示為同一來源無法區(qū)分是輿情監(jiān)控還是工單處理。更要命的是有些系統(tǒng)基于調(diào)用者身份做了權(quán)限隔離比如普通數(shù)據(jù)查詢只能拉脫敏數(shù)據(jù)但管理員工具能拉全量數(shù)據(jù)。由于Agent-Reach用的是一個高權(quán)限憑據(jù)透傳請求等于所有Agent都擁有了管理員權(quán)限而我自己卻渾然不知。修復(fù)方法是給每個連接器增加一層“身份映射”。當Agent發(fā)起請求時Agent-Reach會查詢該Agent在這個連接器上被授予的最小權(quán)限身份然后使用這個身份去調(diào)用下游而不是一把共享Key打天下。我還在每個下游系統(tǒng)的響應(yīng)里強制注入X-Agent-Reach-Original-Caller頭方便安全團隊追溯。這個改動雖然又花了一輪回歸測試但我非常慶幸我們做了它。因為如果不做后面的合規(guī)審計這關(guān)根本過不了。5. Agent-Reach的實測數(shù)據(jù)與后續(xù)演進5.1 一組來自生產(chǎn)環(huán)境的對比數(shù)字Agent-Reach在線上穩(wěn)定運行了兩個多月后整個Agent集群的調(diào)用的整體面貌發(fā)生了明顯變化。為了讓你有個直觀感受我整理了上線前后同一業(yè)務(wù)周期內(nèi)的對比數(shù)據(jù)指標上線前各Agent直接連接上線后統(tǒng)一走Agent-Reach下游系統(tǒng)平均調(diào)用成功率93.2%99.1%運維排查一次故障的平均時間約40分鐘約8分鐘新增下游系統(tǒng)的平均接入時間按Agent數(shù)遞增約1~3天連接器注冊后當天可用因重試導(dǎo)致的下游限流事件每周約5次一個月約1次全局謂詞錯誤率4xx類誤調(diào)未能統(tǒng)計散落在各Agent日志0.68%且可逐條回溯數(shù)據(jù)漲得最猛的還不是成功率而是“排查效率”。以前一個請求出問題你得去Agent日志、下游系統(tǒng)日志、網(wǎng)絡(luò)代理日志里來回翻找現(xiàn)在所有從Agent發(fā)出的觸達請求在Agent-Reach的統(tǒng)一日志里都有完整記錄。搜索一個request_id就能看到它在Agent那邊的意圖、在Agent-Reach里的完整時間線、以及在下游系統(tǒng)上的響應(yīng)狀態(tài)。這40分鐘到8分鐘的縮并不夸張。連接成功率能提升到99%以上還有一個隱形原因是“共同防守”。過去A Agent把下游系統(tǒng)打滿了B Agent會莫名其妙跟著遭殃?,F(xiàn)在所有流量都過同一個觸達層熔斷和限流是整個Agent集群共享的。一個Agent的異常流量不會拖垮其他Agent的正常工作這在本質(zhì)上改變了系統(tǒng)的故障邊界。5.2 我不建議你在這些場景硬套Agent-ReachAgent-Reach不是銀彈。我踩過這些甜頭之后也認真想過它到底適合什么樣的團隊以及哪些場景其實沒必要用。如果你的Agent只有一兩個并且它們調(diào)用的系統(tǒng)總數(shù)不超過三個那么Agent-Reach大概率是一層多余的東西。你連一個統(tǒng)一網(wǎng)關(guān)反而把鏈路拉長了增加了一次網(wǎng)絡(luò)跳轉(zhuǎn)和一層調(diào)度復(fù)雜度。在這種情況下直接在Agent內(nèi)部寫一個精簡的調(diào)用封裝就夠了。如果你的Agent主要是做純文本生成、內(nèi)部知識庫問答這種“讀自己人”的任務(wù)不涉及大量異構(gòu)系統(tǒng)觸達那么Agent-Reach的價值也有限。它的核心能力建立在“連接多個差異很大的外部系統(tǒng)”這個前提上沒了這個前提它就是一個帶鑒權(quán)的HTTP代理而已。還有就是團隊規(guī)模的問題。如果只有一個人負責整個Agent平臺那么學(xué)一套新框架、維護一套新服務(wù)是有成本的。Agent-Reach這種服務(wù)層更適合兩三個人或以上的團隊來共同維護。一個人用你可能會覺得它在給你增加工作量而不是減少工作量。5.3 下一步請求回放和自動降級Agent-Reach目前還只是1.x版本下一步我最想做的兩件事是“請求回放”和“自動降級”。請求回放指的是把每個連接器接收到的請求和響應(yīng)堆疊起來在測試環(huán)境里模擬用戶流量驗證新發(fā)布的連接器版本不會出現(xiàn)兼容性問題。這件事已經(jīng)在手工做下一步想通過改造連接器的SDK在本地打一個快照文件然后有工具可以一鍵重放。這對做后端系統(tǒng)的開發(fā)同學(xué)來說應(yīng)該很熟悉道理跟HTTP流量回放工具是一樣的。自動降級則要溫柔得多。當某個連接器進入半開狀態(tài)的時候我希望Agent-Reach可以自動返回一個“能力降級”信號讓Agent選擇使用備用方案比如發(fā)站內(nèi)信而不是發(fā)郵件。我現(xiàn)在只實現(xiàn)了“快速失敗”還做不到“優(yōu)雅降級”。要讓Agent知道“現(xiàn)在這個渠道不行了你可以換個渠道”這需要Agent那邊配合做決策邏輯不是Agent-Reach單側(cè)能完成的。這個方向我會繼續(xù)做下去。到目前為止Agent-Reach教給我的最深刻的一件事是Agent本身的聰明程度最終會受限于它觸達世界的能力邊界。把觸達這條路修穩(wěn)了上面跑多少Agent心里都不慌。