系統(tǒng)觸達(dá)的連接層架構(gòu))
幾個(gè)月前我在調(diào)一個(gè)內(nèi)部Agent項(xiàng)目最上頭的不是模型幻覺(jué)而是“夠不著”任務(wù)拆得好好的真正去調(diào)企業(yè)內(nèi)部接口時(shí)要么認(rèn)證對(duì)不上要么接口契約和模型理解的不一致要么工具返回了一坨幾千行JSON直接把上下文窗口塞爆。那段時(shí)間我意識(shí)到一件事Agent能不能真正落地瓶頸往往不在“大腦”而在“觸達(dá)”。Agent-Reach就是我從那段時(shí)間的實(shí)踐中沉淀出來(lái)的一套連接層方案。它的核心定位很樸素把Agent的思考能力和外部世界的資源觸達(dá)能力解耦讓模型專心做決策讓一套獨(dú)立的執(zhí)行層負(fù)責(zé)“夠到”真實(shí)系統(tǒng)。這篇文章我會(huì)把Agent-Reach的完整設(shè)計(jì)思路、從零搭建的過(guò)程、數(shù)據(jù)觸達(dá)方案、以及我在落地過(guò)程中踩過(guò)的六個(gè)坑全部攤開(kāi)來(lái)講。適合正在做Agent工程化落地的開(kāi)發(fā)者、方案架構(gòu)師以及那些被“Demo一時(shí)爽接入火葬場(chǎng)”折磨過(guò)的團(tuán)隊(duì)參考。1. 先聊聊“Agent-Reach”到底要解決什么問(wèn)題1.1 Agent的兩難想得明白但夠不著過(guò)去一年我見(jiàn)過(guò)不少Agent項(xiàng)目大部分都卡在同一個(gè)地方模型本身不缺能力缺的是和真實(shí)系統(tǒng)的握手協(xié)議。打個(gè)比方你現(xiàn)在讓一個(gè)聰明人坐在辦公室里給他一臺(tái)沒(méi)裝任何軟件的電腦讓他去“把上個(gè)月的銷售報(bào)表整理出來(lái)發(fā)給相關(guān)負(fù)責(zé)人”。他當(dāng)然知道該怎么做但電腦上沒(méi)有Excel、沒(méi)有郵箱、沒(méi)有數(shù)據(jù)接口他什么都做不了。現(xiàn)在的Agent就是那個(gè)聰明人模型知道怎么拆解任務(wù)、怎么規(guī)劃步驟但一旦需要讀數(shù)據(jù)庫(kù)、調(diào)內(nèi)部API、發(fā)消息、改配置它就抓瞎了。你可能會(huì)說(shuō)那我直接把工具封裝成函數(shù)給模型調(diào)用不就行了Function Calling確實(shí)解決了“模型能請(qǐng)求調(diào)用函數(shù)”的問(wèn)題但你很快會(huì)發(fā)現(xiàn)這只是第一步。真實(shí)場(chǎng)景里你面對(duì)的是幾十上百個(gè)接口每個(gè)接口的鑒權(quán)方式不一樣、參數(shù)契約五花八門、返回體有的幾百字節(jié)有的幾十兆還有的接口有調(diào)用頻控、有的會(huì)自動(dòng)超時(shí)。如果這些復(fù)雜性全部堆在模型那一層Agent的失敗率會(huì)隨著工具數(shù)量線性上升最終完全不可控。我在一個(gè)Demo項(xiàng)目里測(cè)試過(guò)只接三個(gè)工具的時(shí)候模型幾乎百發(fā)百中接到八個(gè)工具的時(shí)候開(kāi)始出現(xiàn)選錯(cuò)工具的情況接到十五個(gè)以上的時(shí)候光是工具描述占用的token就已經(jīng)非??捎^了更不要說(shuō)模型經(jīng)常拿這個(gè)接口的參數(shù)去調(diào)那個(gè)接口。這就是Agent的“兩難”能力邊界越大調(diào)度復(fù)雜度越高兩者之間必須有一層?xùn)|西來(lái)緩沖。1.2 連接層方案的基本盤Agent-Reach的定位就是這層緩沖。它不是模型不是Agent框架也不替代業(yè)務(wù)系統(tǒng)它是一個(gè)夾在Agent和外部資源之間的連接層。你可以把它理解成“神經(jīng)和肌肉之間的接口板”大腦模型只發(fā)指令不關(guān)心肌肉怎么收縮連接層負(fù)責(zé)把指令翻譯成具體動(dòng)作并且保證動(dòng)作安全、可控、可追蹤。我把連接層要解決的基本盤抽象成四個(gè)問(wèn)題Agent能發(fā)現(xiàn)什么工具外部能力必須有一個(gè)標(biāo)準(zhǔn)化的注冊(cè)和發(fā)現(xiàn)機(jī)制而不是散落在代碼里。Agent決定用什么工具在候選工具很多的時(shí)候需要一套高效的意圖匹配和工具路由邏輯不能把所有工具描述一股腦塞給模型。系統(tǒng)如何安全地執(zhí)行鑒權(quán)、權(quán)限校驗(yàn)、超時(shí)、重試、限流這些執(zhí)行層的臟活不能交給模型操心。執(zhí)行結(jié)果如何回到Agent的認(rèn)知中結(jié)果需要被壓縮、裁剪、格式化成為模型能高效利用的上下文而不是把原始響應(yīng)直接砸給模型。這四個(gè)問(wèn)題就是Agent-Reach的四個(gè)支柱。下面的內(nèi)容全部圍繞它們展開(kāi)。這么設(shè)計(jì)還有一個(gè)額外好處模型可以隨便換Agent框架可以隨便升級(jí)只要連接層的契約不變整個(gè)系統(tǒng)就是穩(wěn)定的。我在項(xiàng)目里把LLM從閉源換到開(kāi)源又換了一套Agent編排框架底層工具幾乎沒(méi)動(dòng)這就是連接層帶來(lái)的安全感。2. 核心架構(gòu)把“觸達(dá)”拆成三個(gè)平面Agent-Reach的架構(gòu)我習(xí)慣拆成三個(gè)平面來(lái)看工具面、交互面、連接面。每個(gè)平面解決不同層次的問(wèn)題這樣拆的好處是出了問(wèn)題你能很快定位在哪一層。2.1 工具面外部能力的標(biāo)準(zhǔn)化注冊(cè)工具面解決的是“Agent能發(fā)現(xiàn)什么”。所有外部能力不管是內(nèi)部API、第三方服務(wù)、數(shù)據(jù)庫(kù)查詢、還是瀏覽器操作都要在Agent-Reach里注冊(cè)成一份統(tǒng)一的工具描述。我用的描述標(biāo)準(zhǔn)是OpenAPI風(fēng)格但不是完整引入Swagger那一套而是裁減出一個(gè)”夠用子集”。每個(gè)工具描述包含五個(gè)核心字段字段作用說(shuō)明name工具的唯一標(biāo)識(shí)模型調(diào)用時(shí)使用的名字必須短且無(wú)歧義description工具的能力說(shuō)明告訴模型這個(gè)工具做什么、適合什么場(chǎng)景、不適合什么場(chǎng)景endpoint實(shí)際調(diào)用的接口地址由Agent-Reach統(tǒng)一管理不暴露給模型input_schema入?yún)⒌腏SON Schema定義參數(shù)名、類型、必填項(xiàng)、取值范圍output_format出參的裁剪規(guī)則定義返回結(jié)果給模型看什么、隱藏什么這里有一個(gè)關(guān)鍵設(shè)計(jì)模型看到的工具描述和真實(shí)調(diào)用的接口定義是分離的。模型的工具列表里只出現(xiàn)name、description、input_schema和output_formatendpoint、鑒權(quán)方式、內(nèi)部參數(shù)映射這些細(xì)節(jié)全部留在Agent-Reach的注冊(cè)表里。舉個(gè)例子我們內(nèi)部有個(gè)“查詢員工請(qǐng)假余額”的接口真實(shí)路徑是/internal/api/v2/hr/leave-balance入?yún)⑿枰氖枪ぬ?hào)和釘釘U(kuò)serId的映射關(guān)系。但在模型看來(lái)這個(gè)工具的描述只是name: query_leave_balance description: 查詢某位員工當(dāng)前的年度剩余請(qǐng)假天數(shù)適合在處理休假申請(qǐng)、考勤異常時(shí)使用。不支持查詢歷史記錄。 input_schema: employee_name: string output_format: - employee_name - total_days - used_days - remaining_days員工姓名到工號(hào)、工號(hào)到釘釘U(kuò)serId的映射發(fā)生在Agent-Reach執(zhí)行層模型根本不需要知道。這既減少了模型的理解負(fù)擔(dān)也降低了敏感信息泄露的風(fēng)險(xiǎn)。2.2 交互面意圖與應(yīng)用場(chǎng)景的匹配交互面解決的是“Agent決定用什么工具”的問(wèn)題。這是Agent-Reach最值得講的部分因?yàn)榻^大多數(shù)Agent項(xiàng)目都是在這里開(kāi)始失控的。最樸素的做法是把所有工具描述都塞進(jìn)系統(tǒng)提示詞讓模型自己挑。工具少的時(shí)候沒(méi)問(wèn)題工具超過(guò)三十個(gè)以后光描述就有好幾千token既貴又容易讓模型注意力渙散。我在測(cè)試?yán)镉龅竭^(guò)模型在三十個(gè)工具列表里反復(fù)糾結(jié)最后選了八竿子打不著的那個(gè)。Agent-Reach的交互面做了兩層處理召回和排序。當(dāng)Agent收到用戶請(qǐng)求后不會(huì)直接把全部工具描述交給模型而是先在工具注冊(cè)表里做一輪語(yǔ)義檢索召回最相關(guān)的N個(gè)候選工具N一般在3到8之間然后再把這批候選工具的描述交給模型做最終選擇。召回層我用的是embedding向量相似度結(jié)合關(guān)鍵詞加權(quán)的方式。每個(gè)工具描述在注冊(cè)時(shí)會(huì)生成一個(gè)向量索引同時(shí)人工維護(hù)一個(gè)關(guān)鍵詞映射表比如“請(qǐng)假”、“休假”、“年假”都會(huì)關(guān)聯(lián)到query_leave_balance。用戶請(qǐng)求進(jìn)來(lái)后先做向量召回再做關(guān)鍵詞加權(quán)排序取Top N。這一步優(yōu)化帶來(lái)的效果非常明顯。首先模型的決策空間大幅縮小工具選擇準(zhǔn)確率明顯提升其次每次任務(wù)消耗的token大幅下降最后工具數(shù)量增加到幾百個(gè)也不會(huì)對(duì)模型造成壓力因?yàn)槟P陀肋h(yuǎn)只看到一小撮候選工具。2.3 連接面執(zhí)行、反饋與審計(jì)連接面是Agent-Reach真正干活的地方。模型給出了調(diào)用某個(gè)工具的結(jié)構(gòu)化指令后指令會(huì)先經(jīng)過(guò)一層校驗(yàn)確認(rèn)參數(shù)完整、格式合法然后由執(zhí)行器發(fā)起真實(shí)調(diào)用。我在連接面里設(shè)計(jì)了兩個(gè)角色執(zhí)行器和審計(jì)者。執(zhí)行器負(fù)責(zé)打通最后的物理鏈路——構(gòu)造請(qǐng)求、注入鑒權(quán)信息、處理重試、解析響應(yīng)、按output_format裁剪結(jié)果。審計(jì)者負(fù)責(zé)把所有動(dòng)作記錄下來(lái)并生成一個(gè)trace_id串聯(lián)一次任務(wù)從開(kāi)始到結(jié)束的所有工具調(diào)用。這里有一個(gè)非常重要的細(xì)節(jié)工具真實(shí)返回的響應(yīng)體和最終喂給模型的上下文是完全不同的兩份數(shù)據(jù)。真實(shí)響應(yīng)體可能是一個(gè)幾千行的嵌套JSON但喂給模型的可能是三行摘要。壓縮邏輯在輸出裁剪規(guī)則里定義可以是截?cái)嘧侄?、匯總統(tǒng)計(jì)、或者調(diào)用一個(gè)專用的格式化函數(shù)。舉個(gè)我常用的例子工具返回了原始數(shù)據(jù){ code: 0, data: { total: 157, items: [ { task_id: TK-2025-0113, title: 修復(fù)登錄頁(yè)樣式錯(cuò)位, status: pending, assignee: 張三, created_at: 2025-01-08T09:31:22Z }, ... ] } }但經(jīng)過(guò)output_format裁剪后模型看到的只是任務(wù)積壓數(shù)157 處理最慢的3個(gè)任務(wù) 1. TK-2025-0113 修復(fù)登錄頁(yè)樣式錯(cuò)位已等待5天負(fù)責(zé)人張三 2. TK-2025-0110 對(duì)接支付回調(diào)已等待4天負(fù)責(zé)人李四 3. TK-2025-0108 優(yōu)化首頁(yè)加載速度已等待3天負(fù)責(zé)人王五模型不需要關(guān)注原始JSON結(jié)構(gòu)它只需要理解“發(fā)生了什么”然后決定下一步怎么做。連接面的存在本質(zhì)上是在模型和真實(shí)世界之間加了一道“翻譯閘門”。3. 從零搭建Agent-Reach一次實(shí)際落地記錄這一節(jié)我記錄一次真實(shí)的落地過(guò)程從選型到跑通把操作路徑完整走一遍。我盡量寫細(xì)因?yàn)槲抑篮芏嗳丝醇軜?gòu)圖都能看懂真正動(dòng)手時(shí)還是會(huì)卡在一些細(xì)節(jié)上。3.1 最小可行技術(shù)棧我搭建Agent-Reach最小版本用的技術(shù)棧是Python FastAPI YAML工具注冊(cè)表 SQLite審計(jì)日志。選這套組合有我的理由。FastAPI對(duì)OpenAPI有原生支持天然和工具契約的思想契合Python生態(tài)里做大模型集成最方便不管是OpenAI的function calling還是開(kāi)源的vLLMPython都是第一公民工具注冊(cè)表用YAML而不是數(shù)據(jù)庫(kù)是為了讓非開(kāi)發(fā)角色的同事也能參與維護(hù)工具描述SQLite做審計(jì)日志零部署成本。Agent側(cè)我用的是OpenAI格式的function calling協(xié)議但Agent-Reach對(duì)上層提供了抽象的接口后面換模型框架不需要改連接層本身。目錄結(jié)構(gòu)大致是這樣的agent-reach/ ├── registry/ # 工具注冊(cè)表YAML文件 │ ├── hr.yaml │ ├── task.yaml │ └── database.yaml ├── core/ │ ├── router.py # 召回與排序 │ ├── executor.py # 執(zhí)行器 │ ├── auditor.py # 審計(jì)與日志 │ └── compressor.py # 響應(yīng)裁剪 ├── integrations/ # 每種工具類型的接入代碼 ├── server.py # FastAPI服務(wù)入口 └── config.yaml3.2 接入第一個(gè)工具創(chuàng)建工單接入工具的動(dòng)作本身不復(fù)雜但有幾個(gè)坑值得提前說(shuō)。第一個(gè)工具我選了“創(chuàng)建內(nèi)部工單”因?yàn)樗臉I(yè)務(wù)邏輯足夠簡(jiǎn)單同時(shí)又涉及鑒權(quán)和寫操作能完整暴露問(wèn)題。第一步是在registry/task.yaml里注冊(cè)工具描述- name: create_task description: 創(chuàng)建一條新的內(nèi)部工單記錄用于分配任務(wù)給指定負(fù)責(zé)人。適合在需要將工作項(xiàng)落實(shí)到具體責(zé)任人時(shí)使用。不適合查詢已有工單狀態(tài)。 endpoint: https://internal-api.example.com/v1/tasks method: POST auth: type: service_account credential_env: REACH_TASK_SERVICE_TOKEN input_schema: title: type: string required: true max_length: 120 assignee: type: string required: true priority: type: string enum: [low, medium, high, urgent] default: medium output_format: - task_id - created_at timeout_seconds: 10 retry_policy: max_retries: 1 retrable_codes: [502, 503, 504]這里有兩個(gè)細(xì)節(jié)值得注意。第一auth字段里沒(méi)有放真實(shí)Token只指定了從環(huán)境變量讀取Token永遠(yuǎn)不出現(xiàn)在代碼和配置庫(kù)里。第二retry_policy限制只對(duì)5xx錯(cuò)誤重試因?yàn)橹挥蟹?wù)端錯(cuò)誤才可能是臨時(shí)故障POST請(qǐng)求如果收到4xx重試只會(huì)放大問(wèn)題。第二步是寫這個(gè)工具的接續(xù)代碼放在integrations/目錄下。每個(gè)集成模塊實(shí)現(xiàn)兩個(gè)方法execute()處理真實(shí)調(diào)用compress()處理響應(yīng)裁剪。# integrations/task_integration.py class TaskIntegration(BaseIntegration): def execute(self, params: dict, context: ExecutionContext) - dict: headers { Authorization: fBearer {os.environ[REACH_TASK_SERVICE_TOKEN]}, Content-Type: application/json, } payload { title: params[title], assignee: params[assignee], priority: params.get(priority, medium), } resp httpx.post( self.endpoint, jsonpayload, headersheaders, timeoutcontext.timeout_seconds, ) resp.raise_for_status() return resp.json() def compress(self, raw: dict) - str: return f已創(chuàng)建工單 {raw[task_id]}負(fù)責(zé)人{(lán)raw[assignee]}優(yōu)先級(jí){raw[priority]}這樣一張一縮兩端邏輯分離模型看到的永遠(yuǎn)是壓縮后的簡(jiǎn)潔文本。3.3 一個(gè)完整任務(wù)的內(nèi)部軌跡接入第一個(gè)工具之后我們需要讓這些工具真正協(xié)作起來(lái)。我設(shè)計(jì)了一個(gè)測(cè)試任務(wù)用戶說(shuō)“查一下上周工單積壓情況并給處理最慢的負(fù)責(zé)人發(fā)一條提醒”。這個(gè)任務(wù)在Agent-Reach里的執(zhí)行軌跡是這樣的用戶請(qǐng)求進(jìn)入Agent側(cè)Agent把請(qǐng)求轉(zhuǎn)給Agent-Reach的router節(jié)點(diǎn)。router在工具注冊(cè)表里做語(yǔ)義召回命中了三個(gè)工具query_task_overview、list_overdue_tasks、send_instant_message。Agent拿到候選工具描述后規(guī)劃出兩步調(diào)用先查積壓和處理速度再發(fā)送消息。第一步調(diào)用list_overdue_tasks執(zhí)行器注入服務(wù)賬號(hào)Token調(diào)用內(nèi)部接口拿到積壓的工單列表經(jīng)compress壓縮后返回給Agent。Agent根據(jù)結(jié)果判斷出需要提醒的負(fù)責(zé)人發(fā)起第二步調(diào)用send_instant_message。執(zhí)行器完成消息發(fā)送壓縮結(jié)果為“已向張三發(fā)送提醒”審計(jì)日志把兩次調(diào)用的trace串聯(lián)起來(lái)。整個(gè)過(guò)程中模型做出的決策只有兩件事選哪些工具、傳什么參數(shù)。至于怎么鑒權(quán)、怎么構(gòu)造請(qǐng)求、怎么解析嵌套JSON模型一概不知也不需要知道。我特別想強(qiáng)調(diào)最后一步的審計(jì)價(jià)值。如果沒(méi)有trace_id串聯(lián)出了問(wèn)題你只能對(duì)著聊天記錄瞎猜。有了審計(jì)日志你能精確地回溯到用戶在什么時(shí)刻說(shuō)了什么模型基于什么信息做出了什么決策執(zhí)行器實(shí)際調(diào)用了哪個(gè)接口、返回了什么到底是模型選錯(cuò)了工具、參數(shù)傳錯(cuò)了、還是上游接口報(bào)錯(cuò)一目了然。3.4 工程細(xì)節(jié)超時(shí)、重試與限流連接層的工程細(xì)節(jié)決定了Agent在真實(shí)環(huán)境下的存活率。我在第一版實(shí)現(xiàn)里就吃過(guò)沒(méi)做超時(shí)控制的虧有一個(gè)第三方接口偶爾卡頓不返回Agent等了一分鐘才超時(shí)用戶體驗(yàn)直接打折扣還白白浪費(fèi)一次調(diào)用費(fèi)用。Agent-Reach里我用了分層超時(shí)策略。每個(gè)工具可以單獨(dú)配置timeout_seconds沒(méi)有配置的走全局默認(rèn)值15秒??傛溌愤€有一個(gè)兜底超時(shí)防止多個(gè)工具串行調(diào)用時(shí)整體耗時(shí)失控。重試策略需要警惕不是所有錯(cuò)誤都值得重試。只有符合兩個(gè)條件時(shí)才重試請(qǐng)求未實(shí)際生效比如網(wǎng)絡(luò)斷開(kāi)、超時(shí)、5xx且該服務(wù)具備冪等性。對(duì)于POST創(chuàng)建類的接口如果你不確定上游是否支持冪等鍵寧可失敗也不要重試否則就有創(chuàng)建重復(fù)工單的風(fēng)險(xiǎn)。這一點(diǎn)我們?cè)诤竺娌瓤诱鹿?jié)會(huì)展開(kāi)講。限流方面Agent-Reach在連接面做了一個(gè)簡(jiǎn)單的兩級(jí)限流按用戶維度限制QPS防止單用戶把上游打爆按工具維度限制每分鐘調(diào)用次數(shù)保護(hù)關(guān)鍵業(yè)務(wù)系統(tǒng)。限流值的配置寫在工具注冊(cè)表里比如query_task_overview的限流是60次/分鐘send_instant_message是20次/分鐘。實(shí)測(cè)下來(lái)這個(gè)配置夠用遇到需要更高吞吐量的場(chǎng)景再動(dòng)態(tài)調(diào)整。4. 數(shù)據(jù)觸達(dá)讓Agent擁有長(zhǎng)期記憶工具觸達(dá)解決的是“Agent能做事”的問(wèn)題數(shù)據(jù)觸達(dá)解決的是“Agent能記住、能查閱”的問(wèn)題。前者是手后者是記憶和知識(shí)庫(kù)。這兩個(gè)分開(kāi)做后面的迭代省很多事。4.1 向量檢索接入Agent-Reach里的數(shù)據(jù)觸達(dá)層第一個(gè)接入的是向量檢索。用途是讓Agent在決策前能先“回憶”一下相關(guān)的歷史知識(shí)之前處理類似問(wèn)題時(shí)的結(jié)論、業(yè)務(wù)規(guī)則文檔、知識(shí)庫(kù)條目等。我沒(méi)有直接用開(kāi)源向量數(shù)據(jù)庫(kù)而是用PostgreSQL的pgvector插件原因是我們團(tuán)隊(duì)已經(jīng)有PostgreSQL運(yùn)維經(jīng)驗(yàn)少一個(gè)中間件就少一份運(yùn)維成本。建表邏輯大致是這樣CREATE TABLE reach_memory ( id BIGSERIAL PRIMARY KEY, entry_type VARCHAR(20), -- doc / conversation / insight content TEXT, -- 原始內(nèi)容 content_vector VECTOR(1536), -- embedding向量 meta JSONB, -- 來(lái)源、時(shí)間、關(guān)聯(lián)實(shí)體 created_at TIMESTAMP DEFAULT NOW() );檢索流程也比較簡(jiǎn)單用戶請(qǐng)求進(jìn)來(lái)之后先把請(qǐng)求轉(zhuǎn)換成embedding在reach_memory表里做相似度查詢?nèi)op K結(jié)果拼進(jìn)上下文。這個(gè)動(dòng)作發(fā)生在Agent正式?jīng)Q策之前相當(dāng)于先“翻閱筆記”再“做判斷”。有一個(gè)從實(shí)踐里得出的建議不要無(wú)腦把檢索到的內(nèi)容全部塞給模型。很多團(tuán)隊(duì)把Top K設(shè)成5每條內(nèi)容幾百字一下塞進(jìn)去好幾十KB既擠占上下文窗口還可能引入和當(dāng)前任務(wù)無(wú)關(guān)的干擾信息。我在Agent-Reach里給每個(gè)檢索結(jié)果加了相關(guān)性分值只有超過(guò)閾值的才進(jìn)入候選然后再交給一個(gè)過(guò)濾模型做一輪粗篩確保進(jìn)入上下文的每一條信息都是“真有用”。4.2 結(jié)構(gòu)化數(shù)據(jù)查詢向量檢索處理的是非結(jié)構(gòu)化知識(shí)但Agent在真實(shí)場(chǎng)景里還需要查結(jié)構(gòu)化數(shù)據(jù)庫(kù)存數(shù)量、訂單狀態(tài)、人員信息、財(cái)務(wù)數(shù)據(jù)等。這里要回應(yīng)一個(gè)非?,F(xiàn)實(shí)的安全問(wèn)題你不能讓Agent直接拼SQL去連生產(chǎn)庫(kù)。Agent-Reach的做法是加了一層受控的查詢接口。底層數(shù)據(jù)庫(kù)的表結(jié)構(gòu)會(huì)被整理成元數(shù)據(jù)字典Agent可以自己寫查詢邏輯但必須通過(guò)查詢層執(zhí)行查詢層做四件事強(qiáng)制只讀用獨(dú)立數(shù)據(jù)庫(kù)賬號(hào)只有SELECT權(quán)限沒(méi)有寫入權(quán)限限行限列禁止沒(méi)有LIMIT的查詢默認(rèn)最大返回100行禁止SELECT *超時(shí)控制單條查詢最長(zhǎng)3秒超過(guò)就終止敏感字段脫敏手機(jī)號(hào)、身份證、薪資字段在返回前自動(dòng)打碼。查詢層的接口長(zhǎng)這樣POST /reach/db/query { query: SELECT department, COUNT(*) FROM employees GROUP BY department, max_rows: 50 }Agent只需要告訴查詢層“我想查什么”查詢層負(fù)責(zé)SQL解析、攔截危險(xiǎn)語(yǔ)句、執(zhí)行、脫敏、返回。SQL注入、超量數(shù)據(jù)返回、敏感信息泄露這些風(fēng)險(xiǎn)都被隔離在查詢層里Agent完全碰不到原始數(shù)據(jù)庫(kù)。這里我特別想說(shuō)一個(gè)觀點(diǎn)Agent讀數(shù)據(jù)的能力應(yīng)該被設(shè)計(jì)成“圖書館管理員幫你查資料”而不是“把圖書館鑰匙直接給Agent”。查詢層就是那個(gè)管理員Agent可以提要求但管理員有判斷權(quán)。4.3 記憶分區(qū)與生命周期記憶分區(qū)是我在Agent-Reach落地中后期才補(bǔ)上的設(shè)計(jì)。一開(kāi)始所有記憶都塞進(jìn)一個(gè)向量表后來(lái)發(fā)現(xiàn)會(huì)話級(jí)的信息和長(zhǎng)期知識(shí)混在一起清理和檢索都不方便于是分成了三個(gè)區(qū)。短期會(huì)話記憶存的是當(dāng)前對(duì)話輪次里的臨時(shí)信息任務(wù)結(jié)束就過(guò)期不需要持久化通常直接放在Agent側(cè)的上下文里。工作記憶存的是跨會(huì)話的任務(wù)狀態(tài)比如一個(gè)任務(wù)鏈執(zhí)行到一半被一個(gè)API超時(shí)打斷了下次繼續(xù)的時(shí)候Agent需要知道“之前的進(jìn)展在哪、還剩哪些步驟”。這類記憶保留到任務(wù)完成就清理用任務(wù)ID做索引。長(zhǎng)期記憶存的是穩(wěn)定知識(shí)包括用戶偏好、業(yè)務(wù)規(guī)則、歷史決策結(jié)論等通過(guò)向量檢索在每次任務(wù)開(kāi)始時(shí)拉取。長(zhǎng)期記憶需要定期做去重和過(guò)期清理我用一個(gè)定時(shí)任務(wù)掃描meta里的時(shí)間戳超過(guò)半年的自動(dòng)轉(zhuǎn)入冷存儲(chǔ)不再參與在線檢索。分區(qū)的好處是清理策略各有不同檢索的目標(biāo)也更明確不會(huì)出現(xiàn)“查一單貨品庫(kù)存時(shí)把三個(gè)月前的會(huì)話摘要也撈出來(lái)”的尷尬。5. 落地過(guò)程中的六個(gè)坑這六個(gè)坑全部來(lái)自我實(shí)際經(jīng)歷過(guò)的故障和返工每一個(gè)都對(duì)應(yīng)著一輪真實(shí)的排查和修復(fù)。我按“癥狀—根因—解法”的格式寫方便你對(duì)照自己項(xiàng)目里的情況。5.1 工具描述模糊導(dǎo)致模型選錯(cuò)工具第一個(gè)坑出現(xiàn)在工具數(shù)量剛超過(guò)十五個(gè)的時(shí)候?,F(xiàn)象是模型頻繁選錯(cuò)工具用戶問(wèn)“幫我查下庫(kù)存”模型調(diào)用了一個(gè)查詢訂單狀態(tài)的工具然后一本正經(jīng)地說(shuō)“沒(méi)有查到庫(kù)存信息”。根因是工具描述寫得不夠有區(qū)分度?!安樵儙?kù)存”和“查詢訂單狀態(tài)”這兩個(gè)工具如果description里都寫著“查詢產(chǎn)品信息”模型的語(yǔ)義區(qū)分就會(huì)非常困難特別是底層接口返回的結(jié)構(gòu)還很相似的時(shí)候。我的解法是重寫所有工具描述在description里強(qiáng)制加入兩層信息這個(gè)工具適合什么場(chǎng)景、不適合什么場(chǎng)景。同時(shí)在input_schema里給每個(gè)參數(shù)加上詳細(xì)注釋和枚舉值說(shuō)明。重寫完成后工具選擇的準(zhǔn)確率從84%提到了96%左右代價(jià)只是描述變長(zhǎng)了一點(diǎn)。5.2 鑒權(quán)信息流入模型上下文差點(diǎn)引發(fā)安全事故這個(gè)坑是我見(jiàn)過(guò)最危險(xiǎn)的。某次線上排查時(shí)我發(fā)現(xiàn)有一個(gè)工具的example示例里帶著真實(shí)的內(nèi)部Token字段而這條示例會(huì)被拼進(jìn)發(fā)給模型的上下文中。模型回復(fù)用戶時(shí)把Token原樣復(fù)述了出來(lái)。雖然只出現(xiàn)在內(nèi)部測(cè)試環(huán)境但這件事讓我后怕了很久。從此Agent-Reach立了三條規(guī)矩第一鑒權(quán)字段一律從環(huán)境變量讀取禁止硬編碼進(jìn)工具描述或示例第二工具注冊(cè)表里不允許出現(xiàn)任何真實(shí)憑證只允許出現(xiàn)credential_env引用第三審計(jì)日志里對(duì)響應(yīng)體做脫敏凡是匹配到Token格式的字段自動(dòng)替換成星號(hào)。5.3 工具返回大響應(yīng)體上下文窗口被塞爆有一次Agent在處理“導(dǎo)出所有項(xiàng)目成員信息”的任務(wù)時(shí)工具返回了一個(gè)接近2萬(wàn)行的JSON文件。執(zhí)行器沒(méi)做壓縮直接把原始響應(yīng)懟給了模型結(jié)果上下文窗口直接被打滿模型開(kāi)始胡言亂語(yǔ)任務(wù)徹底崩了。這次事故讓我把響應(yīng)壓縮提到了最高優(yōu)先級(jí)。Agent-Reach現(xiàn)在強(qiáng)制要求每個(gè)工具必須定義output_format執(zhí)行器只把壓縮后的結(jié)果交給模型。對(duì)于不可避免的大數(shù)據(jù)量場(chǎng)景我會(huì)額外提供一個(gè)paginate參數(shù)把結(jié)果按頁(yè)切分模型可以主動(dòng)請(qǐng)求下一頁(yè)但絕不允許一次性灌入全部原始數(shù)據(jù)。5.4 多步任務(wù)鏈中Agent出現(xiàn)“目標(biāo)漂移”“目標(biāo)漂移”這個(gè)坑很難復(fù)現(xiàn)但確實(shí)存在。有一次Agent的任務(wù)是“查詢這個(gè)月所有未支付的訂單”它在執(zhí)行過(guò)程中發(fā)現(xiàn)了幾個(gè)異常訂單就開(kāi)始主動(dòng)去查客戶的信用記錄、聯(lián)系記錄、售后歷史最后跑出了十幾步子任務(wù)已經(jīng)完全偏離原始目標(biāo)了。根因是多步任務(wù)鏈中Agent的短期視野壓過(guò)了長(zhǎng)期目標(biāo)。我在Agent-Reach里加了兩個(gè)緩解措施第一給每個(gè)任務(wù)設(shè)置最大步數(shù)預(yù)算默認(rèn)10步超過(guò)閾值強(qiáng)制Agent停下來(lái)重新審視原始目標(biāo)第二步在每步?jīng)Q策時(shí)把原始任務(wù)描述和當(dāng)前進(jìn)度一起拼進(jìn)上下文中讓模型時(shí)刻知道“我們最初要的是什么”。這兩個(gè)措施搭配后目標(biāo)漂移的發(fā)生頻率大幅下降。5.5 工具數(shù)量膨脹后的調(diào)度失衡工具超過(guò)五十個(gè)之后召回層的準(zhǔn)確率開(kāi)始下滑。癥狀是Agent明明只需要查一個(gè)簡(jiǎn)單的天氣信息召回出來(lái)的候選工具里卻混著“天氣預(yù)警管理”“室內(nèi)環(huán)境監(jiān)控”這類不相關(guān)的工具導(dǎo)致模型在決策時(shí)猶豫經(jīng)常選錯(cuò)。我重新看了召回鏈路的各個(gè)環(huán)節(jié)發(fā)現(xiàn)問(wèn)題是關(guān)鍵詞加權(quán)表太粗糙“天氣”這個(gè)詞關(guān)聯(lián)了太多工具。解法是把工具按業(yè)務(wù)域分組召回階段先在域級(jí)別過(guò)濾再在域內(nèi)做Top N排序。比如“查詢天氣”屬于“氣象域”“室內(nèi)環(huán)境監(jiān)控”屬于“樓宇域”兩者在域級(jí)別就已經(jīng)分開(kāi)。調(diào)整之后召回準(zhǔn)確率恢復(fù)到了99%模型決策也干凈了。5.6 權(quán)限粒度不過(guò)關(guān)一次誤操作引發(fā)的整改最后一個(gè)坑比較敏感但值得寫出來(lái)。某次測(cè)試中一個(gè)普通用戶通過(guò)Agent發(fā)起了一個(gè)批量刪除數(shù)據(jù)的請(qǐng)求Agent成功執(zhí)行了。排查發(fā)現(xiàn)工具注冊(cè)表里沒(méi)有配置權(quán)限級(jí)別所有工具的權(quán)限默認(rèn)放通只要模型決定調(diào)用就能調(diào)。這次問(wèn)題讓我意識(shí)到權(quán)限控制必須在連接層強(qiáng)制實(shí)施不能依賴模型的自我約束。Agent-Reach現(xiàn)在給每個(gè)工具增加了一個(gè)allowed_roles字段執(zhí)行階段會(huì)根據(jù)當(dāng)前用戶上下文做校驗(yàn)普通用戶試圖調(diào)用管理員工具時(shí)直接拒絕并返回錯(cuò)誤給Agent。雙層校驗(yàn)的原則是模型可以“請(qǐng)求”調(diào)用任何它想調(diào)的工具但連接層只“執(zhí)行”當(dāng)前用戶有權(quán)調(diào)用的工具。這六個(gè)坑的共性其實(shí)是一個(gè)Agent的可靠性不是靠模型聰明而是靠外圍系統(tǒng)的約束和兜底。連接層的價(jià)值就在于此。6. 安全邊界與后續(xù)演進(jìn)Agent-Reach落地到生產(chǎn)環(huán)境后我開(kāi)始收緊安全邊界同時(shí)也看到了幾塊可以繼續(xù)深挖的部分簡(jiǎn)單寫一下我目前的做法和計(jì)劃。6.1 三層隔離安全隔離我拆了三層來(lái)做。容器層的隔離是讓每個(gè)工具的執(zhí)行環(huán)境跑在獨(dú)立的worker進(jìn)程里工具之間互不共享內(nèi)存和文件系統(tǒng)一個(gè)工具被注入惡意代碼不會(huì)波及其他工具。網(wǎng)絡(luò)層的隔離是把Agent-Reach放在業(yè)務(wù)內(nèi)網(wǎng)和公網(wǎng)之間的一個(gè)“跳出區(qū)”內(nèi)網(wǎng)工具只能通過(guò)這個(gè)區(qū)訪問(wèn)外部資源外部請(qǐng)求無(wú)法直接觸達(dá)內(nèi)網(wǎng)核心系統(tǒng)。內(nèi)容層的隔離是把模型輸出視為不可信輸入凡是Agent生成的參數(shù)都要經(jīng)過(guò)schema校驗(yàn)凡是工具返回的內(nèi)容都要經(jīng)過(guò)脫敏和裁剪再進(jìn)入上下文。這三層隔離里面內(nèi)容層往往被忽視。但實(shí)際測(cè)試中模型輸出的參數(shù)經(jīng)常會(huì)出現(xiàn)格式非法、越界枚舉值等問(wèn)題沒(méi)有校驗(yàn)層的話這些臟數(shù)據(jù)會(huì)直接打進(jìn)真實(shí)系統(tǒng)。6.2 全鏈路可觀測(cè)可觀測(cè)性是我在搭完第一版后的第二天就意識(shí)到必須補(bǔ)齊的東西。Agent-Reach里的每個(gè)環(huán)節(jié)都往審計(jì)日志里打結(jié)構(gòu)化數(shù)據(jù)請(qǐng)求ID、工具名稱、輸入?yún)?shù)、響應(yīng)摘要、耗時(shí)、費(fèi)用、錯(cuò)誤信息。用這個(gè)日志可以回答四類問(wèn)題這次任務(wù)為什么這么慢為什么選了A工具而不是B工具這個(gè)報(bào)錯(cuò)是模型造成的還是工具造成的這個(gè)月的Agent調(diào)用費(fèi)用流向了哪里我現(xiàn)在跑了一套簡(jiǎn)單的巡檢腳本每天掃描審計(jì)日志主動(dòng)找出耗時(shí)異常的慢調(diào)用、頻繁失敗的脆弱工具、以及參數(shù)重復(fù)報(bào)錯(cuò)的異常模式。這些數(shù)據(jù)是Agent-Reach后續(xù)迭代的最重要依據(jù)。6.3 幾個(gè)值得繼續(xù)深挖的方向后續(xù)演進(jìn)上我目前在看三個(gè)方向。第一個(gè)是多Agent協(xié)作時(shí)的工具復(fù)用?,F(xiàn)在每個(gè)Agent都掛一套工具注冊(cè)表但團(tuán)隊(duì)里可能有多個(gè)Agent服務(wù)工具重復(fù)維護(hù)成本很高。下一步計(jì)劃把Agent-Reach做成一個(gè)獨(dú)立的共享服務(wù)多個(gè)Agent通過(guò)標(biāo)準(zhǔn)接口接入同一套工具目錄。第二個(gè)是工具描述的自優(yōu)化閉環(huán)。當(dāng)前的工具描述是人工維護(hù)的但實(shí)際調(diào)用數(shù)據(jù)一直積累在審計(jì)日志里。我計(jì)劃做一個(gè)離線分析定期找出調(diào)用失敗率高的工具自動(dòng)提取失敗案例輔助人工診斷是描述問(wèn)題還是接口問(wèn)題形成“調(diào)用數(shù)據(jù)驅(qū)動(dòng)描述優(yōu)化”的閉環(huán)。第三個(gè)是實(shí)時(shí)反饋通道。目前Agent執(zhí)行完一個(gè)工具之后連接層的任務(wù)就結(jié)束了。但工具端產(chǎn)生的業(yè)務(wù)結(jié)果比如工單狀態(tài)變更、審批流程推進(jìn)是否要反向通知給Agent以便后續(xù)決策這塊還沒(méi)有做閉環(huán)。計(jì)劃是引入事件回調(diào)機(jī)制讓Agent-Reach不僅能觸達(dá)外部世界也能感知外部世界的變化。這三個(gè)方向做完Agent-Reach就能從“一個(gè)調(diào)用閥門”變成“Agent和真實(shí)世界之間的高速公路”。現(xiàn)在回看這套方案最核心的價(jià)值可能不在于技術(shù)多復(fù)雜而在于把很多原本靠模型“臨場(chǎng)發(fā)揮”的事提前變成了工程量產(chǎn)的事。這大概是Agent工程化落地最值得投入的部分。