用中間層:從注冊中心到權(quán)限審計(jì)的完整實(shí)踐)
做了兩年多智能體應(yīng)用我最大的感受是讓大模型開口說話不難難的是讓它的手“夠得著”你要的東西。我最近在整理一個(gè)項(xiàng)目代號 Agent-Reach直白一點(diǎn)講它解決的是一個(gè)很具體的問題AI Agent 憑什么能穩(wěn)定、安全、可追溯地觸達(dá)外部的工具、數(shù)據(jù)源以及另外一批 Agent。很多 Agent 看起來能聊能寫一旦面對真實(shí)業(yè)務(wù)——查庫存、發(fā)郵件、調(diào)第三方接口、取數(shù)據(jù)庫里的某條記錄——就開始原地打轉(zhuǎn)。問題不在模型本身而在 Agent 和外部世界之間缺了一條可靠的觸達(dá)通道。這篇文章是我把這個(gè)項(xiàng)目從零搭起來、跑通、踩坑之后的完整復(fù)盤包含設(shè)計(jì)思路、關(guān)鍵代碼和排查記錄適合正在做 Agent 應(yīng)用、工具調(diào)用或多智能體協(xié)作的開發(fā)者參考。1. 項(xiàng)目定位Agent-Reach 解決的是“夠得著”的問題1.1 從場景聊起Agent 卡在哪一步先說一個(gè)很典型的例子。之前我?guī)鸵粋€(gè)電商團(tuán)隊(duì)做售前客服 Agent用戶問“這個(gè)訂單還能改地址嗎”模型能理解意圖也能生成一段像模像樣的回答但真正的難題是改地址要調(diào)訂單系統(tǒng)的 API要校驗(yàn)時(shí)間窗口要判斷是否已發(fā)貨還要把操作結(jié)果寫進(jìn)工單。沒有一條可靠的觸達(dá)通道模型說得再漂亮最后也只能給用戶一句“我?guī)湍匆幌抡埳院蟆比缓缶蜎]有然后了。我們把這類問題拆開看Agent 卡住的位置非常統(tǒng)一不是“思考”出了問題而是“行動”斷了線。具體有三層斷點(diǎn)工具不可發(fā)現(xiàn)。模型不知道系統(tǒng)里有哪些能力更不知道每個(gè)能力的入?yún)?、出參長什么樣。調(diào)用不可控。就算知道有工具誰在調(diào)、能不能調(diào)、調(diào)得頻不頻繁完全沒有約束。鏈路不可追。一次調(diào)用背后經(jīng)過了哪些系統(tǒng)、花了多長時(shí)間、返回了什么沒有任何審計(jì)記錄。市面上常見的做法是在 system prompt 里把工具描述堆進(jìn)去再用 Function Calling 直接調(diào)后端服務(wù)。這個(gè)思路在小規(guī)模演示里很順一旦工具數(shù)量超過二十個(gè)、服務(wù)拆成多個(gè)團(tuán)隊(duì)維護(hù)Prompt 會越來越胖權(quán)限和灰度又沒人管最后變成一團(tuán)解不開的線。Agent-Reach 的做法是在模型和外部系統(tǒng)之間加一層專門管“觸達(dá)”的中間層統(tǒng)一負(fù)責(zé)注冊、路由、策略和審計(jì)。1.2 方案選型里的兩次取舍做這個(gè)項(xiàng)目之前我先把候選路線列了一遍最后的選擇不是拍腦袋定的而是被真實(shí)場景逼出來的。方案優(yōu)點(diǎn)主要代價(jià)全部工具描述塞進(jìn) Prompt零依賴見效快Token 爆炸維護(hù)成本隨工具數(shù)量指數(shù)上升只用 Function Calling 直接調(diào)用原生支持鏈路短無權(quán)限、無審計(jì)、無路由多服務(wù)場景很難管理直接用 MCP 標(biāo)準(zhǔn)協(xié)議生態(tài)好社區(qū)活躍靈活度高但約束弱細(xì)粒度權(quán)限和可觀測性還是得自己補(bǔ)Agent-Reach 中間層可控、可審計(jì)、可插拔多一層服務(wù)初期工程量稍大第一次取舍發(fā)生在“標(biāo)準(zhǔn)化”和“可控性”之間。MCP 是很好的協(xié)議但它解決的是“接口長什么樣”的問題不解決“誰允許調(diào)”和“調(diào)完怎么復(fù)盤”的問題。Agent-Reach 把 MCP 這類標(biāo)準(zhǔn)當(dāng)成底層接入方式之一同時(shí)在上層加了策略引擎和審計(jì)管道相當(dāng)于給 Agent 的每只“手”都裝了一道閘門。第二次取舍發(fā)生在“中心化”和“去中心化”之間。早期我試用過直接在 Agent 進(jìn)程里嵌注冊表的路子輕是真輕但多個(gè) Agent 共享能力時(shí)需要各自維護(hù)一份配置很快就漂移了。后來改成中心化注冊加去中心化執(zhí)行的架構(gòu)注冊表集中管理工具實(shí)際運(yùn)行在各自的服務(wù)里中間層只做編排和策略判斷。這樣既避免了重復(fù)配置又不用把所有調(diào)用流量都拉進(jìn)同一個(gè)進(jìn)程代價(jià)只是多維護(hù)一個(gè)網(wǎng)關(guān)服務(wù)。提示如果你手頭只有兩三個(gè)工具不要急著上這套東西。Agent-Reach 的價(jià)值要從“工具數(shù)量多、權(quán)限要求細(xì)、鏈路需要審計(jì)”這三個(gè)條件同時(shí)成立時(shí)才開始顯現(xiàn)。2. 核心機(jī)制拆解注冊中心、路由與安全邊界2.1 能力注冊讓每個(gè)工具自帶“說明書”Agent-Reach 里所有的工具都不是寫死在代碼里的而是通過注冊中心動態(tài)加進(jìn)去的。注冊信息包含四個(gè)部分名字、描述、參數(shù) Schema、執(zhí)行函數(shù)。前兩項(xiàng)是給模型看的參數(shù) Schema 用來做校驗(yàn)執(zhí)行函數(shù)才是真正干活的代碼。名字的命名我強(qiáng)烈建議用“域.動作.對象”三段式比如warehouse.check_stock、order.update_address、payment.refund_apply。這樣命名有三個(gè)好處路由策略可以用通配符批量授權(quán)日志里掃一眼就能定位到業(yè)務(wù)域模型在工具太多時(shí)也能減少混淆。描述這一欄非常關(guān)鍵。不要寫“查詢庫存”這種一句話要寫清楚“什么時(shí)候用、有什么前置條件、失敗時(shí)可能因?yàn)槭裁础?。原因很簡單模型是靠描述來決定要不要調(diào)用這個(gè)工具的描述敷衍模型就會瞎猜。我見過一個(gè)工具描述沒寫“僅支持已支付訂單”結(jié)果模型在未支付訂單上反復(fù)調(diào)用生成了一堆無意義的報(bào)錯。參數(shù) Schema 直接采用 JSON Schema 標(biāo)準(zhǔn)。這里有一個(gè)新手容易忽略的點(diǎn)每個(gè)字段都要寫 description枚舉值要盡量給全。模型雖然不至于把字符串參數(shù)拼錯但面對枚舉值的時(shí)候如果你不告訴它可選項(xiàng)它真的會自己發(fā)明一個(gè)值出來。執(zhí)行函數(shù)是唯一的真實(shí)邏輯入口。在 Agent-Reach 里我要求注冊進(jìn)來的 handler 只做一件事接收已經(jīng)校驗(yàn)過的參數(shù)調(diào)用后端系統(tǒng)返回結(jié)果。所有重試、降級、超時(shí)邏輯都放在中間層的路由網(wǎng)關(guān)里不讓業(yè)務(wù)函數(shù)自己處理這樣才能保證每一個(gè)工具的行為是統(tǒng)一、可控的。2.2 動態(tài)路由一次調(diào)用背后的完整鏈路路由層是整個(gè)項(xiàng)目的核心。模型產(chǎn)生一個(gè)工具調(diào)用請求后請求不會直接打到業(yè)務(wù)服務(wù)而是先進(jìn)路由網(wǎng)關(guān)走五步流程權(quán)限判斷。根據(jù)調(diào)用方身份、所在 scope、目標(biāo)工具名查策略表不允許就直接拒絕并記錄原因。參數(shù)校驗(yàn)。用注冊時(shí)的 JSON Schema 校驗(yàn)?zāi)P蜕傻膮?shù)缺字段、類型不對都在這一步攔截。限流計(jì)數(shù)。從 Redis 里扣減調(diào)用額度超限就返回“頻率超限請稍后重試”。執(zhí)行調(diào)用。把校驗(yàn)后的參數(shù)傳給工具 handler同時(shí)啟動超時(shí)計(jì)時(shí)。審計(jì)落庫。記錄調(diào)用方、工具名、入?yún)?、出參摘要、耗時(shí)、成功失敗標(biāo)記最終寫進(jìn)審計(jì)日志庫。這五步看起來多但每一步都有明確的職責(zé)少了哪一個(gè)線上都會出事。尤其是第 5 步很多人嫌麻煩想省掉真出了“Agent 亂調(diào)工具”的投訴時(shí)沒有審計(jì)日志你連定位都沒法定位。路由過程里有個(gè)細(xì)節(jié)出參摘要。工具返回值可能會很大比如一個(gè)訂單查詢返回了三百行明細(xì)直接把全量內(nèi)容塞回給模型一方面浪費(fèi) Token另一方面會干擾模型對后續(xù)對話的判斷。我采用的策略是每個(gè)工具返回都做兩層裁剪先由 handler 自己生成一個(gè)summary字段路由層再做一次按字符數(shù)的截?cái)嗄J(rèn) 2000 字符以內(nèi)。這個(gè)參數(shù)我放在后面詳細(xì)講。2.3 安全邊界權(quán)限、限流與審計(jì)很多 Agent 項(xiàng)目把安全想得太簡單以為“只有模型能調(diào)工具所以不需要鑒權(quán)”。這是非常危險(xiǎn)的想法。Agent 本身可能被提示注入一個(gè)惡意的用戶輸入完全可以誘導(dǎo)模型去調(diào)用高權(quán)限工具。Agent-Reach 在安全邊界上做了四件事。第一件最小權(quán)限。給每個(gè) Agent 分配一個(gè)身份策略表只允許它訪問完成業(yè)務(wù)必需的工具。客服 Agent 能查訂單、能改收貨地址但絕不能直接調(diào)用退款接口。退款要走獨(dú)立的審批 Agent兩邊通過路由網(wǎng)關(guān)銜接。第二件分 scope 隔離。同一個(gè)工具可以注冊多個(gè) scope比如warehouse.check_stock在零售域和服務(wù)臺域可以有不同的并發(fā)額度。scope 本質(zhì)上是租戶隔離防止一個(gè)域的高峰流量把另一個(gè)域的額度吃光。第三件調(diào)用鏈上下文。每個(gè) Agent 會話都有唯一的agent_id每次路由都會生成trace_id這兩個(gè) ID 貫穿所有審計(jì)日志。出問題的時(shí)候只要拿到用戶的一句話就能順著 trace_id 把所有工具調(diào)用記錄串起來。第四件敏感信息過濾。工具的原始返回里可能包含身份證號、手機(jī)號、內(nèi)部備注。路由層在把結(jié)果交給模型之前會做一次敏感字段掩碼。這一步不能依賴模型自覺必須在系統(tǒng)層面強(qiáng)制。注意審計(jì)日志同樣需要權(quán)限保護(hù)。日志里記錄了入?yún)⒑统鰠⒄绻罩編毂旧聿辉O(shè)防等于把鑰匙掛在門旁邊。我見過不止一個(gè)團(tuán)隊(duì)把審計(jì)日志存在同一個(gè)庫同一個(gè)賬號下最后排查問題時(shí)看到權(quán)限混亂反而不敢信日志了。2.4 關(guān)鍵參數(shù)表和配置建議參數(shù)配置是 Agent-Reach 里最容易被忽略、但影響最大的部分。先給一份我在生產(chǎn)環(huán)境用的推薦值參數(shù)默認(rèn)值推薦值說明tool_call_timeout10s15s單次工具調(diào)用的超時(shí)時(shí)間超過則返回錯誤max_retry12網(wǎng)絡(luò)類錯誤的自動重試次數(shù)業(yè)務(wù)錯誤不重試context_budget2000 字符2000 字符每個(gè)工具結(jié)果回傳給模型的最大長度max_iterations88Agent 單輪對話中最多連續(xù)調(diào)用工具的次數(shù)rate_limit60/min按業(yè)務(wù)定每個(gè) Agent 每分鐘最多調(diào)用某一類工具的次數(shù)allow_reroutefalsefalse是否允許工具結(jié)果再次觸發(fā)其他工具默認(rèn)關(guān)閉max_iterations是防“鬼打墻”的關(guān)鍵參數(shù)。模型有時(shí)候會在回答不出來的時(shí)候反復(fù)調(diào)用同一個(gè)工具不加這個(gè)上限一次對話能燒掉你幾百次 API 調(diào)用。allow_reroute默認(rèn)關(guān)閉也是同樣的原因工具結(jié)果自動觸發(fā)下一個(gè)工具鏈路一旦出現(xiàn)邏輯環(huán)很難打斷所以多 Agent 場景下的鏈?zhǔn)秸{(diào)用我都改成顯式路由讓 Agent 自己決定下一步調(diào)什么。context_budget的設(shè)置要結(jié)合模型上下文窗口來看。窗口大不代表可以隨意塞工具返回的信息是為了讓模型做決策的不是讓它背誦的。我在實(shí)驗(yàn)里把預(yù)算從 2000 調(diào)到 8000模型的回答準(zhǔn)確率沒有明顯提升反而更容易在冗余信息里抓到次要字段。3. 實(shí)操落地從零搭一個(gè)最小可用的 Agent-Reach3.1 目錄結(jié)構(gòu)和基礎(chǔ)依賴先給目錄結(jié)構(gòu)。我沒有用復(fù)雜的微服務(wù)框架而是把網(wǎng)關(guān)、注冊、策略、審計(jì)拆成包方便各自演進(jìn)。agent-reach/ ├── gateway/ # FastAPI 服務(wù)統(tǒng)一入口 │ └── router.py ├── registry/ # 工具注冊與管理 │ ├── manager.py │ └── schema.py ├── connectors/ # 外部系統(tǒng)適配器 │ ├── warehouse.py │ └── order.py ├── policies/ # 權(quán)限與限流策略 │ ├── default.yaml │ └── engine.py ├── audit/ # 審計(jì)日志 │ └── logger.py ├── agents/ # Agent 調(diào)用循環(huán) │ └── loop.py └── examples/ # 場景示例 ├── single_agent.py └── approval_chain.py基礎(chǔ)依賴盡量精簡我用的是 Python 3.10、FastAPI、Redis 客戶端、SQLAlchemy。大模型接口做了適配層統(tǒng)一走 OpenAI 兼容格式方便切不同廠商的模型。Redis 在這里的用途是存限流計(jì)數(shù)和路由表緩存審計(jì)日志寫 PostgreSQL。3.2 工具注冊模塊實(shí)現(xiàn)注冊模塊最關(guān)鍵的是保存 JSON Schema 和執(zhí)行函數(shù)同時(shí)維護(hù)一份給模型看的工具列表。下面這段是我實(shí)際使用的簡化版# registry/manager.py from __future__ import annotations import time from typing import Awaitable, Callable, Dict ToolHandler Callable[..., Awaitable[dict]] class ToolRegistry: def __init__(self) - None: self._tools: Dict[str, dict] {} def register( self, name: str, description: str, parameters: dict, handler: ToolHandler, scope: str default, ) - str: self._tools[name] { description: description, parameters: parameters, handler: handler, scope: scope, created_at: int(time.time()), invoke_count: 0, } return name def lookup(self, name: str, scope: str): tool self._tools.get(name) if tool is None or tool[scope] ! scope: return None return tool def list_for_llm(self, scope: str) - list: tools [] for name, info in self._tools.items(): if info[scope] ! scope: continue tools.append( { type: function, function: { name: name, description: info[description], parameters: info[parameters], }, } ) return tools注冊時(shí)有兩個(gè)細(xì)節(jié)提醒一下。scope 字段如果不傳默認(rèn)是default我建議所有業(yè)務(wù)注冊時(shí)都顯式傳 scope避免后面策略配置時(shí)默認(rèn)權(quán)限搞錯。invoke_count目前只是內(nèi)存計(jì)數(shù)生產(chǎn)環(huán)境我會丟到 Redis 里做累加否則重啟就清零了。3.3 路由網(wǎng)關(guān)實(shí)現(xiàn)路由網(wǎng)關(guān)是請求進(jìn)入后的守門人。下面這段省略了數(shù)據(jù)庫操作和細(xì)節(jié)異常處理保留核心流程# gateway/router.py import time import uuid from dataclasses import dataclass dataclass class ToolContext: agent_id: str scope: str trace_id: str async def route_tool_call(registry, call, context, policies): tool_name call[name] args call.get(arguments, {}) # 1. 權(quán)限檢查 decision policies.check(context.agent_id, tool_name, context.scope) if not decision.allowed: audit_log(deny, context, tool_name, {reason: decision.reason}) raise PermissionError(f{tool_name} is not allowed for {context.agent_id}) # 2. 參數(shù)校驗(yàn) tool registry.lookup(tool_name, context.scope) validated_args validate_with_schema(tool[parameters], args) # 3. 限流與計(jì)數(shù) policies.consume_quota(context.agent_id, tool_name) # 4. 執(zhí)行并計(jì)時(shí) started time.perf_counter() result await tool[handler](**validated_args) elapsed_ms int((time.perf_counter() - started) * 1000) # 5. 審計(jì) audit_log( allow, context, tool_name, { args: validated_args, result_summary: summarize(result, max_chars2000), elapsed_ms: elapsed_ms, }, ) return result這段代碼里最有價(jià)值的一點(diǎn)是權(quán)限、校驗(yàn)、限流每個(gè)環(huán)節(jié)失敗都會走明確的異常路徑并且立刻寫審計(jì)。實(shí)際使用時(shí)我還會在每個(gè)環(huán)節(jié)加一個(gè)起始時(shí)間戳方便查看一次調(diào)用到底卡在權(quán)限還是卡在外部接口。3.4 與 LLM 的循環(huán)調(diào)用實(shí)現(xiàn)路由網(wǎng)關(guān)本身不產(chǎn)生模型調(diào)用真正驅(qū)動它的是 Agent 的循環(huán)。這里實(shí)現(xiàn)了標(biāo)準(zhǔn)的“模型決定工具 - 路由執(zhí)行 - 結(jié)果回填 - 再交給模型”的循環(huán)# agents/loop.py import json async def run_agent_with_reach( llm, registry, policies, system_prompt: str, user_message: str, scope: str, agent_id: str, max_iterations: int 8, ): tools registry.list_for_llm(scope) messages [ {role: system, content: system_prompt}, {role: user, content: user_message}, ] for step in range(max_iterations): response await llm.chat(messagesmessages, toolstools) if not response.tool_calls: return response.content messages.append(response.message) for call in response.tool_calls: context ToolContext(agent_idagent_id, scopescope, trace_iduuid.uuid4().hex) try: result await route_tool_call(registry, call.function, context, policies) content summarize(result, max_chars2000) except Exception as exc: content f__TOOL_ERROR__: {exc} messages.append( { role: tool, tool_call_id: call.id, content: json.dumps(content, ensure_asciiFalse), } ) raise RuntimeError(fexceeded max_iterations{max_iterations})循環(huán)里有兩個(gè)細(xì)節(jié)很影響穩(wěn)定性。第一工具返回內(nèi)容必須序列化成字符串再放回消息列表不能用 dict 直接傳否則不同模型 API 的兼容性會出問題。第二異常信息不能直接透傳原始報(bào)錯否則數(shù)據(jù)庫密碼、內(nèi)網(wǎng)地址可能被模型看到甚至復(fù)述出來。我上面用了__TOOL_ERROR__前綴并且只放了一條安全的消息摘要。3.5 單 Agent 場景實(shí)測搭好之后我先跑了一個(gè)最樸素的場景讓 Agent 查某個(gè) SKU 的實(shí)時(shí)庫存。注冊工具# connectors/warehouse.py async def check_stock(sku: str): # 真實(shí)項(xiàng)目里這里換成對 ERP/WMS 接口的 HTTP 調(diào)用 return {sku: sku, available: 128, updated_at: 2025-01-12 10:23} registry.register( namewarehouse.check_stock, description查詢某個(gè) SKU 的實(shí)時(shí)可用庫存。當(dāng)用戶問還剩多少、夠不夠發(fā)貨時(shí)使用。, parameters{ type: object, properties: {sku: {type: string, description: 商品 SKU 編碼}}, required: [sku], }, handlercheck_stock, scoperetail, )用戶輸入是“SKU 10086 的庫存夠明天發(fā)貨嗎”模型會先生成warehouse.check_stock調(diào)用路由器校驗(yàn)權(quán)限執(zhí)行 handler把結(jié)果回填模型再基于庫存數(shù)字給出判斷。整個(gè)過程里用戶無感知但審計(jì)日志里已經(jīng)完整記錄了模型看到了什么、調(diào)了什么、每條結(jié)果耗時(shí)多少。第一次跑通這個(gè)循環(huán)時(shí)系統(tǒng)日志里多了十幾個(gè)trace_id那種感覺就像給 Agent 裝上了一張能看到“手”在動的儀表盤。這也是我后來堅(jiān)持所有工具必須走統(tǒng)一路由的原因——沒有這一步你永遠(yuǎn)不知道模型到底干了什么。4. 進(jìn)階場景多 Agent 協(xié)作與權(quán)限審批鏈4.1 設(shè)計(jì)思路Agent 注冊為工具單 Agent 跑通以后自然想解決更復(fù)雜的問題多個(gè) Agent 之間怎么協(xié)作。我在 Agent-Reach 里采用了一個(gè)非常樸素的設(shè)計(jì)——把一個(gè) Agent 的能力也注冊成工具。也就是說Agent 和 Agent 之間不是私聊而是通過路由網(wǎng)關(guān)互相調(diào)用。這個(gè)設(shè)計(jì)聽起來很直接但它避開了很多協(xié)作框架里的坑。Agent 之間不直接傳 API Key不直接約定接口地址一切都走注冊發(fā)現(xiàn)。下游 Agent 上線新能力只需要注冊一個(gè)新的工具上游 Agent 想用只需要策略表里加一條允許規(guī)則。協(xié)作關(guān)系從代碼耦合變成了配置聲明。以客服場景為例??头?Agent 被用戶要求退款時(shí)它自己不能調(diào)支付退款接口只能調(diào)用一個(gè)名為agent.approval的工具這個(gè)工具對應(yīng)的 handler 會喚醒審批 Agent。審批 Agent 看到退款申請核對訂單和金額然后通過工具調(diào)用返回“同意”或“拒絕”。整個(gè)鏈路里每一步都有跡可循。4.2 場景實(shí)現(xiàn)審批鏈路下面是一個(gè)簡化版的審批鏈路注冊# examples/approval_chain.py async def request_refund_approval(order_id: str, amount: float): # 這里通過內(nèi)部代理再調(diào)起審批 Agent return await route_tool_call( registry, { name: agent.approval, arguments: { target: refund, order_id: order_id, amount: amount, }, }, contextToolContext( agent_idcustomer_service, scopeops, trace_id..., ), policiesapproval_policies, ) registry.register( nameagent.approval, description發(fā)起一筆退款審批??头荒苤苯油丝畋仨毾热〉脤徟ㄟ^。, parameters{ type: object, properties: { target: {type: string, enum: [refund], description: 審批類型}, order_id: {type: string, description: 訂單號}, amount: {type: number, description: 退款金額單位元}, }, required: [target, order_id, amount], }, handlerrequest_refund_approval, scopeops, )配套的策略表長這樣# policies/default.yaml policies: - agent: customer_service allow: - order.query - order.update_address - agent.approval deny: - payment.refund quota: 120/min這個(gè)策略文件我要特別解釋一下。deny規(guī)則比allow優(yōu)先這是安全設(shè)計(jì)上的保守選擇。哪怕你以后想在allow里用通配符給客服 Agent 開放一大類工具只要deny里有payment.refund這條權(quán)限依然會被攔下來。審批 Agent 被調(diào)起來之后它會先查訂單信息再判斷退款金額是否在閾值內(nèi)最終返回一個(gè)結(jié)構(gòu)化結(jié)果。因?yàn)閷徟?Agent 的能力也是通過注冊中心暴露的所以審計(jì)日志里能看到用戶說“我要退單” - 客服 Agent 調(diào)用審批工具 - 審批 Agent 查訂單 - 審批 Agent 返回同意 - 客服 Agent 回復(fù)用戶。完整憑證鏈一目了然。4.3 常見問題速查表癥狀可能原因排查方法解決方案Agent 反復(fù)調(diào)用同一工具參數(shù)校驗(yàn)失敗但模型沒拿到提示看審計(jì)日志里 deny 記錄在工具錯誤消息里明確返回缺失字段返回內(nèi)容過長模型抓不住重點(diǎn)沒有設(shè)置 context_budget檢查 messages 長度在路由層強(qiáng)制 summarize工具調(diào)用一直超時(shí)外部接口慢超時(shí)參數(shù)過短看耗時(shí)分布調(diào)大 tool_call_timeout同時(shí)做上下游超時(shí)隔離權(quán)限拒絕風(fēng)暴scope 或 policy 配置漂移對比不同環(huán)境的策略表用配置文件做版本管理部署前跑策略單測多 Agent 鏈路死循環(huán)誤開了 allow_reroute查 trace_id 調(diào)用鏈關(guān)閉自動重路由改用顯式工具調(diào)用這張表不是憑空寫的每一條都在我自己的項(xiàng)目里真實(shí)出現(xiàn)過。尤其是“參數(shù)校驗(yàn)失敗但模型沒拿到提示”那條第一次遇到時(shí)排查了很久最后發(fā)現(xiàn)是異常信息里沒有告訴模型到底缺了什么字段模型以為參數(shù)沒問題就一遍遍重試。后來我把異常信息改成“缺少參數(shù): order_id”問題立刻消失。5. 調(diào)優(yōu)與排障實(shí)錄三個(gè)最值得說的案例5.1 流量放大問題的解決上線第一周我就發(fā)現(xiàn)一個(gè)反直覺的現(xiàn)象Agent 調(diào)用工具的 QPS 比用戶請求量高了將近十倍。審計(jì)日志一查絕大多數(shù)都是同一類情況——用戶問一句Agent 連續(xù)調(diào)三次庫存接口。原因在上下文模型的上下文窗口里如果只保留了最近一次庫存結(jié)果它在做跨商品比較時(shí)就會重新調(diào)用。這不是模型笨是工具結(jié)果的記憶太短。解決辦法不是加上下文而是給常用工具加緩存。我在注冊中心給warehouse.check_stock加了 30 秒的 Redis 緩存同 SKU 的查詢直接命中緩存QPS 立刻降了下來。這里有一個(gè)關(guān)鍵前提緩存只適合冪等查詢類工具任何有副作用的寫操作都絕對不能緩存。給寫操作加緩存等于讓 Agent 在一個(gè)錯誤前提上繼續(xù)往下走后果非常難追溯。5.2 上下文預(yù)算的重要性另一個(gè)項(xiàng)目里Agent 查訂單返回了明細(xì)列表我嫌每個(gè)字段都有用就沒做截?cái)?。結(jié)果用戶多問幾輪模型開始出現(xiàn)“記憶稀釋”——前面的約束條件忘了工具調(diào)用開始頻頻出錯。這個(gè)現(xiàn)象的本質(zhì)是上下文窗口被工具返回占滿留給對話和推理的空間不夠了。后來我把context_budget從默認(rèn)值改成顯式按工具配置。訂單查詢這個(gè)工具返回給模型的內(nèi)容只保留訂單狀態(tài)、金額、關(guān)鍵時(shí)間點(diǎn)三個(gè)字段其余明細(xì)一律省略。模型回答的準(zhǔn)確率反而明顯提升。這件事讓我確認(rèn)了一個(gè)原則工具返回要的是“夠用”不是“完整”。5.3 一次線上排查的完整復(fù)盤最難忘的一次排障大約花了四個(gè)小時(shí)?,F(xiàn)象是客服 Agent 偶爾反饋“查不到訂單”但同樣的訂單號在管理后臺明明能看到。第一反應(yīng)是看模型有沒有正確傳參。審計(jì)日志一翻傳參沒問題訂單接口也返回了正常數(shù)據(jù)。再看時(shí)間發(fā)現(xiàn)一個(gè)規(guī)律每次查不到訂單都發(fā)生在下午三點(diǎn)左右而且集中在同一個(gè)倉庫的訂單上。順著 trace_id 繼續(xù)查最后定位到端側(cè)緩存。倉庫那邊有一個(gè)老接口查詢結(jié)果緩存一小時(shí)緩存擊穿的時(shí)候返回了空列表。問題不在 Agent 側(cè)但 Agent 把空結(jié)果當(dāng)成了“訂單不存在”并非常自信地告訴了用戶。這次排障的最大收獲是Agent 應(yīng)用的排查不能只盯著模型和路由層外部的老系統(tǒng)可能是整個(gè)鏈路里最不可控的一環(huán)。后來我在 Agent-Reach 里加了一個(gè)約定所有工具返回必須帶數(shù)據(jù)時(shí)間戳并且路由層會把“數(shù)據(jù)更新于 XX 分鐘前”拼進(jìn)結(jié)果摘要。模型看到這個(gè)信息后回答就會帶上“根據(jù) X 分鐘前的數(shù)據(jù)”這樣的限定語不會再百分百確定地給結(jié)論。6. 我再聊幾句個(gè)人體會6.1 這個(gè)項(xiàng)目真正有價(jià)值的產(chǎn)出做 Agent-Reach 這段時(shí)間我越來越明確一件事真正讓項(xiàng)目落地見效的不是花哨的模型配置而是把工具邊界、權(quán)限模型和審計(jì)機(jī)制想清楚。很多團(tuán)隊(duì)一開始追求“什么都能干”的大 Agent結(jié)果上線后連基本的可觀測性都沒有。我這套方案最大的產(chǎn)出是把“Agent 干了什么”從黑盒變成了白盒——每次工具調(diào)用都有記錄每個(gè)權(quán)限決定都有依據(jù)每個(gè)異常都能順著 trace_id 找到根因。6.2 給后來者的幾條建議從我的經(jīng)驗(yàn)來看如果你想在自己的項(xiàng)目里借鑒 Agent-Reach不要一上來就鋪開做全套。先讓一個(gè) Agent 穩(wěn)定觸達(dá)兩三個(gè)高頻工具跑通權(quán)限和審計(jì)再逐步加工具、加 Agent。另外工具描述值得你花時(shí)間字斟句酌它的質(zhì)量直接決定模型調(diào)用工具的準(zhǔn)確率。最后保持默認(rèn)參數(shù)的保守主義max_iterations和allow_reroute這種參數(shù)寧可一開始限制得緊一點(diǎn)也不要讓模型自由發(fā)揮。這套東西后邊還有很多可以擴(kuò)展的方向比如把策略表做成可視化配置、把審計(jì)日志接進(jìn)告警體系、給工具調(diào)用做成本預(yù)估。我目前還在持續(xù)迭代后續(xù)有新的收獲會再整理出來。