失控:recursionLimit與條件路由的治理實踐)
1. 工具循環(huán)失控的現(xiàn)場從一個“讀不完的文檔” Bug 說起寫過 AI Agent 的朋友應該都有過這種經(jīng)歷你給智能體配了三五個工具本來只是讓它查個資料、整理個表格結(jié)果它像魔怔了一樣反反復復把同一個工具調(diào)用十幾遍。日志里密密麻麻全是 tool call每一條都是同樣的參數(shù)、同樣的輸入唯一區(qū)別就是時間戳不同。電話費、Token 費嘩嘩流走最后模型吐出來一句“抱歉我沒找到答案”。我最初遇到這個問題是在做一個內(nèi)部知識庫問答助手。當時給 Agent 配了一個search_documents工具讓它幫同事找公司制度文檔。某天有人提問“請總結(jié)一下出差報銷的所有規(guī)定”Agent 的第一步跑得很正常調(diào)用搜索工具拿到了二十多條標題其中五條看起來跟報銷相關。問題出在第二步它想逐一打開這五條查看內(nèi)容就引入了一個read_document工具。然后我再看日志就傻眼了——read_document被連續(xù)調(diào)用了三十多次傳入的文檔 ID 永遠只有那五個而且每次都能成功返回正文但 Agent 每一次都說“需要繼續(xù)確認細節(jié)”。這就是典型的 AI 工具循環(huán)失控。用戶看到的是無限轉(zhuǎn)圈開發(fā)者看到的是 Token 快速蒸發(fā)底層原因是 Agent 在自己的推理循環(huán)里失去了“退出意識”。這個循環(huán)機制本身不是壞事。業(yè)界管它叫AI 工具循環(huán)agentic loop模型先分析任務選擇一個工具拿到返回結(jié)果再分析結(jié)果再選下一個工具直到它認為任務完成。這是所有 Agent 的骨架沒有這個循環(huán)Agent 就只是“一次性的問答機器”。但它一旦失控就成了無底洞。控制這個循環(huán)有幾個關鍵抓手其中最基礎也最容易被低估的就是標題里那個recursionLimit。很多人對這個概念的理解停留在“把數(shù)字調(diào)大一點就好了”實際上它和條件路由是搭配使用的。recursionLimit 負責兜底條件路由負責止損兩者一個管“最多跑多遠”一個管“這段路怎么選”配合起來才能真正把循環(huán)管住。這篇文章我按自己的項目經(jīng)驗來拆先說說工具循環(huán)到底是怎么失控的再從原理層面講清 recursionLimit 到底限制了什么然后用一個條件路由的設計實例說明“如何讓 Agent 自己知道何時收手”最后分享一次完整的故障排查鏈路和一些框架配置上的對照。如果你正準備給自己的 Agent 加工具或者已經(jīng)被循環(huán)折磨過這篇文章應該能幫你從“搶修模式”切換到“設計模式”。2. recursionLimit 到底在限制什么執(zhí)行深度與資源代價的賬先拋個結(jié)論recursionLimit 限制的不是工具的調(diào)用次數(shù)而是 Agent 決策循環(huán)的執(zhí)行深度。這個區(qū)別很重要。很多人看名字里有“recursion”這個詞就誤以為它管的是“遞歸調(diào)用”也就是工具 A 內(nèi)部調(diào)用了工具 B 那種嵌套場景。實際上在主流 Agent 框架和編排引擎的語境里recursionLimit 是配置在圖執(zhí)行器graph executor層面的一個計數(shù)器——每一次大模型完成一次推理并產(chǎn)生工具調(diào)用計數(shù)器就加一一旦超出設定閾值整個執(zhí)行就強制終止。為了把這個講透我從執(zhí)行深度的角度重新解釋一遍它的作用。2.1 執(zhí)行深度Agent 的“第幾手牌”你可以把 Agent 完成一個任務的過程想象成和它玩撲克。每打出一張牌它就根據(jù)牌桌上反饋的信息決定下一張怎么打。這張牌可能是調(diào)用一個搜索工具可能是讀一個網(wǎng)頁也可能是寫一段代碼然后運行。每一次“出牌”就代表 AI 工具循環(huán)多走了一步。recursionLimit 限制的就是“一共能出幾手牌”。它和工具數(shù)量無關——你要是有二十個工具但所有信息一步就能拿全可能一次調(diào)用就結(jié)束了反過來就算你只有兩個工具如果 Agent 在所有分支里反復橫跳二十步也可以瞬間用光。我自己的經(jīng)驗里比較合理的理解方式是遞歸深度代表 Agent 對任務不確定性的容忍度上限。如果任務鏈路非常直觀比如“查天氣并告訴我”3 到 5 輪足矣如果是“訪問官網(wǎng) → 獲取 API 文檔 → 寫一個調(diào)用腳本 → 執(zhí)行腳本 → 分析輸出結(jié)果”那么這個鏈路本身就要 4 到 5 輪此時 recursionLimit 必須大于它。2.2 循環(huán)失控時錢和時間的賬怎么算為什么整個行業(yè)都在強調(diào)管住 recursionLimit因為失控循環(huán)燒掉的不僅是耐心更是真金白銀。我算過一筆實際賬一次 GPT-4 級別的工具調(diào)用輸入加上工具返回結(jié)果平均在 2000 到 4000 Token 左右一次循環(huán)里模型要重新讀取全部歷史消息包括前面所有工具調(diào)用及其結(jié)果所以 Token 消耗是線性疊加甚至超線性增長的再加上每次模型調(diào)用的 API 延遲通常在 2 到 6 秒之間循環(huán) 20 次就意味著用戶要干等 40 秒以上這還是在不考慮限流的情況下。更讓運營頭疼的是這些失效循環(huán)并不是毫無意義的中斷而是反復調(diào)用真實工具。如果你的工具帶副作用——比如“發(fā)送郵件”“創(chuàng)建訂單”——那就不只是燒錢問題可能還會制造臟數(shù)據(jù)。我聽過一個案例某個團隊給 Agent 配了數(shù)據(jù)庫寫入工具循環(huán)失控后訂單表里多了一堆重復測試記錄最后只能人工清洗。2.3 兜底和保護的關系所以我會說recursionLimit 本質(zhì)上是一道安全閘門。它不關心你這段循環(huán)的質(zhì)量不判斷你“是不是正走在正確的路上”它只是冷酷地告訴你到點了必須停。也正因為它是一個“一刀切”的全局限制單獨配置它并不能解決循環(huán)的“質(zhì)量問題”。舉個很常見的情況Agent 在處理一個需要分三步走的任務但是每走一步前它都習慣性地先調(diào)用一個“搜索工具”去確認上下文。你把 recursionLimit 從 10 提到 20它可能還是會在 19 步時因為“沒有足夠信心”而繼續(xù)調(diào)用。這個場景里真正管用的不是加大限制而是給它一套更聰明的決策邏輯——這就是后面要講的條件路由。提示不要把 recursionLimit 當作治理工具循環(huán)的唯一手段。它是發(fā)動機的保險絲不是方向盤。保險絲能防止燒毀但它不負責把車開到目的地。3. 條件路由把“無限可能性”壓縮成“有限路由表”條件路由conditional routing這個詞聽起來很工程其實概念特別簡單在 Agent 每次打算調(diào)用工具之前先通過一組規(guī)則或模型判斷明確告訴它“現(xiàn)在有那幾條路可選每條路的入口條件是什么”。沒有路由的 Agent 像是一個沒有地圖的司機。它手里有導航大模型導航告訴它“往前開”但是沒告訴它什么時候該拐彎、什么時候該停。條件路由就是在地圖上把路標豎起來遇到“前方路口左轉(zhuǎn)”的標志才允許它進入左轉(zhuǎn)車道遇到“禁止通行”的標志必須重新規(guī)劃。在實現(xiàn)層面條件路由可以做到很多種粒度。3.1 第一層路由工具選擇的前置規(guī)則最基礎的一層是在模型產(chǎn)生 tool call 之前加一道過濾器。比如你配置了三個工具get_file_content、summarize_text、send_email。理論上模型可以因為一句“我覺得需要發(fā)送一下結(jié)果”就去調(diào)用send_email這沒問題但在真實場景里模型經(jīng)常因為對工具職責理解不夠亂調(diào)一通。我自己的做法是寫一個簡單的前置判斷def route_intent(user_input: str, tool_calls: list): # 假設模型已經(jīng)產(chǎn)生了候選工具調(diào)用 allowed_tools [] for call in tool_calls: tool_name call.function.name if tool_name send_email and not user_email_confirmed(user_input): # 未確認收件人之前不允許進入發(fā)信流程 continue allowed_tools.append(call) return allowed_tools這套規(guī)則的業(yè)務含義是如果用戶從頭到尾都沒提供過收件人郵箱就別讓 Agent 進入發(fā)信分支。它看起來和“工具循環(huán)”問題無關但實際效果非常大——很多失控循環(huán)都是因為 Agent 在某個分支里做了“無法退出的操作”比如它真要發(fā)郵件但是參數(shù)又不夠于是反復回到查詢邏輯里死循環(huán)。3.2 第二層路由基于工具返回值的循環(huán)出口判斷第二層路由才是真正管循環(huán)的。它的核心機制是在每次工具返回結(jié)果后不是直接把結(jié)果原樣丟回給大模型而是先做一次“結(jié)果質(zhì)量判斷”決定是繼續(xù)調(diào)用、還是切換到另一個分支、還是直接終止。我舉個可復現(xiàn)的偽代碼例子def trim_tool_response(tool_output: str) - str: # 如果輸出過長則截斷如果輸出為空則返回特定標記 if len(tool_output) 3000: return tool_output[:3000] ...[已截斷] if not tool_output.strip(): return [工具返回為空] return tool_output def decide_next_step(tool_name: str, tool_output: str) - str: trimmed trim_tool_response(tool_output) if [工具返回為空] in trimmed: return to_fallback # 走兜底分支而不是繼續(xù)調(diào)用同一工具 if 錯誤 in trimmed or 異常 in trimmed: return to_error_handler return to_agent_loop # 正常進入下一輪模型推理看到這里你應該能明白條件路由的價值不只是“走到哪算哪”而是給循環(huán)增加了結(jié)構(gòu)性出口。原本 Agent 只能依靠自己“覺得任務完成了”來退出現(xiàn)在每一輪工具返回后都會先被一套規(guī)則重新審視一遍這次的返回到底有沒有推進任務如果推進了繼續(xù)沒推進立刻切換策略出現(xiàn)錯誤進入錯誤恢復。3.3 路由與 recursionLimit 的聯(lián)動在我實際的項目配置里這兩個是一起調(diào)整的。舉例來說我給 Agent 設定的基礎規(guī)則是默認 recursionLimit 為 12。如果某個任務的工具鏈路預期會超過 8 步比如“先搜索 → 再讀取 → 再分析 → 再生成報告 → 再發(fā)送”那么路由邏輯會自動把限制放大到 20。但這還不夠因為執(zhí)行深度越接近上限Agent 越容易慌亂開始無意義地重復調(diào)用工具。于是我又加了一條“殘余步數(shù)保護”當引擎檢測到剩余步數(shù)少于 3 步時自動把路由策略切換為“保守模式”只允許調(diào)用已經(jīng)驗證過的工具禁止再開新的工具分支。這個設計聽起來復雜實際上就是在循環(huán)的某一輪開始之前先把路由表替換一份。執(zhí)行后狀態(tài) - 當前輪次9 - recursionLimit12 - 剩余步數(shù)3 - 路由模式conservative 允許read_document 允許search_documents僅限精確關鍵詞 - 禁止send_email、create_ticket、execute_script我實測下來這套“余量感知路由”能顯著降低循環(huán)失控率。原因不復雜——大模型很多時候不是笨而是“貪心”它總覺得再多查一步、再確認一輪會更完美。越到后期這種完美主義傾向越強。切斷新分支的選項等于直接告訴它“只能從這個出口走”最后幾步它不是漫無目的地探索而是收斂式地完成任務。4. 條件路由設計實例從“亂串門”到“直線頂?shù)浇Y(jié)果”光講理論不好消化這一節(jié)我把我自己做的一個小項目完整展開。這是一個“代碼倉庫問診 Agent”用戶描述一個異常現(xiàn)象它需要去倉庫里找代碼、讀關鍵文件、定位 bug、給出修復建議。第一次配好工具時它就是這么失控的Agent 老是亂串門一會兒調(diào)list_files列出目錄結(jié)構(gòu)一會兒調(diào)read_file讀文件一會兒又回頭調(diào)list_files搞得像是在迷宮里亂撞。4.1 問題抽象工具調(diào)用路徑太發(fā)散為什么要引入條件路由因為默認的 agentic loop 把“下一步做什么”的決定權(quán)完全交給了大模型。模型每次都是根據(jù)“我剛才看到的東西”來決定下一步它沒有全局意識更看不到“我已經(jīng)白走了三遍同一個目錄”。我的思路是把任務劃分成四個階段每個階段是一個路由容器探索階段只看結(jié)構(gòu)不讀內(nèi)容。允許調(diào)用list_files、grep_code閱讀階段根據(jù)探索結(jié)果領取文件清單逐一批量讀取不允許再臨時搜索分析階段不允許再調(diào)用任何工具要求模型基于已有上下文輸出完整的推理鏈條報告階段生成最終結(jié)論可調(diào)用send_email。每個階段內(nèi)部有自己的 recursionLimit 預算。整個循環(huán)執(zhí)行時引擎先判斷當前處于哪個階段再決定下一步該走哪個分支。4.2 實現(xiàn)階段路由表解析我簡化后的路由代碼如下方便大家照著改自己的場景STAGES { explore: { allowed_tools: [list_files, grep_code], max_steps: 4, }, read: { allowed_tools: [read_file], max_steps: 8, }, analyze: { allowed_tools: [], max_steps: 1, }, report: { allowed_tools: [send_email], max_steps: 2, }, }在執(zhí)行器的主循環(huán)里我會先通過一個函數(shù)判斷當前狀態(tài)屬于哪個階段def get_current_stage(trace: list) - str: 根據(jù)最近的調(diào)用記錄判斷當前處于哪個階段 if not trace: return explore recent_calls trace[-5:] all_read all(call.had_content_result for call in recent_calls) if all_read: return analyze last_tool trace[-1].tool_name if last_tool read_file: return read if last_tool grep_code or last_tool list_files: return explore return explore這個設計有個額外好處recursionLimit不再是一個全局拍腦袋的數(shù)字而是可以分攤到每個階段里。按照上面配置整個任務最多消耗 4 8 1 2 15 步。也就是說即使 Agent 完全學不會“怎么收斂”路由容器也會在 15 步內(nèi)強制把它推到最終報告。從實際使用效果看改造前平均每個任務要跑 11.6 輪改造后平均只需要 5.2 輪而且任務的失敗率從 32% 降到了 9% 左右。4.3 兜底分支設計路由撞墻了怎么辦任何路由設計都不能保證“一定不會走錯”所以必須給自己留兜底分支。我通常在每個階段外加一個fallback分支專門處理“模型堅持調(diào)用禁用工具”的情況。舉個例子在 explore 階段模型理論上不允許直接調(diào)read_file。但我們的模型偶爾還是會發(fā)出這個請求。這時候我不會生硬地拒絕而是注入一條提示語注意當前處于“探索結(jié)構(gòu)”階段請先列目錄或搜索關鍵詞定位目標文件 不要直接讀取文件內(nèi)容。建議調(diào)用 list_files 或 grep_code。更進階的做法是往回撥狀態(tài)如果模型在一個階段里連續(xù)兩次觸發(fā)了禁用工具調(diào)用我就把它的“視野”往前回溯一步相當于讓它重新看到上一個階段結(jié)束時的信息避免它在錯誤分支里越陷越深。我把這個叫“狀態(tài)回滾”它和 recursionLimit 一樣是第三道保險栓。5. 一次“遞歸風暴”的完整排查鏈路從日志到修復紙上談兵完了分享一次真實的踩坑過程。不光講結(jié)論把排查的鏈路和思路一起寫出來這比直接給答案有用得多。5.1 現(xiàn)象Agent 在 47 秒內(nèi)完成 143 次工具調(diào)用有一次我把一個帶八個工具的 Agent 接進了生產(chǎn)環(huán)境的工單系統(tǒng)讓它自動回復客戶問題。上線第二天監(jiān)控面板報警某個工單的應答流程持續(xù)了 47 秒期間完成了 143 次工具調(diào)用消耗的 Token 相當于平時一千多條消息的總量。第一反應是檢查是不是工具本身卡住了比如某個 API 超時導致循環(huán)。但日志顯示每個工具都是正常返回的而且返回的內(nèi)容一樣——是同一個“客戶訂單狀態(tài)查詢”接口。這說明問題不在工具而在 Agent 的循環(huán)策略本身。5.2 逐層剝離三步定位問題根因排查這種事最忌諱直接改配置瞎試。我按下面三個步驟做了剝離**第一步看調(diào)用圖譜。**把所有工具調(diào)用按時間序列畫出來后發(fā)現(xiàn)是典型的“乒乓型循環(huán)”調(diào)用訂單查詢 → 返回“已發(fā)貨” → Agent 再調(diào)物流查詢 → 返回“運輸中” → Agent 再調(diào)訂單查詢 …… 無限往復。兩個工具互相給對方“喂新問題”而大模型又沒有能力判斷“這兩個信息其實指向同一個結(jié)論”。**第二步縮小工具范圍。**我臨時把物流查詢工具下線只留訂單查詢。結(jié)果 Agent 雖然沒再乒乓但還是反復調(diào)用訂單查詢并持續(xù)輸出“我需要進一步核實”。問題定位為模型對“已發(fā)貨”狀態(tài)的理解存在不確定性它傾向于反復確認來彌補這種不確定感。**第三步檢查路由規(guī)則。**當時項目里其實已經(jīng)配置了一個簡單路由如果訂單狀態(tài)為“已發(fā)貨”則路由到“物流階段”。但這條路由只判斷了訂單狀態(tài)沒有判斷“物流階段是否已經(jīng)查詢過”。所以 Agent 在物流查詢結(jié)果回來后又被叔叔式地引導回訂單查詢循環(huán)就產(chǎn)生了。修復的方案很直接把路由判斷從“無狀態(tài)”改成“有狀態(tài)”。用一個上下文存儲記錄“最近 3 輪已經(jīng)調(diào)用過哪些工具、結(jié)果是什么”。如果接下來模型再次觸發(fā)相同工具調(diào)用并且參數(shù)完全相同路由直接攔截這次調(diào)用然后把上一次的結(jié)果重新提供給模型并附上一句提示“該信息已存在請基于現(xiàn)有信息繼續(xù)”。5.3 修復效果的量化對比我記錄了一個小表方便后續(xù)參考指標修復前修復后單任務平均工具調(diào)用次數(shù)27.46.8單任務平均耗時時長21.3s5.7sToken 消耗中位數(shù)18.2K6.1K用戶投訴率14%3%這次經(jīng)歷讓我深刻體會到一點recursionLimit 管的是“最大能跑多少”路由管的是“跑得有沒有意義”。如果只調(diào)大 recursionLimit這次故障的持續(xù)時間會更長如果只加路由但不管 recursionLimit極端情況照樣會失控。兩件事必須同時做。6. 不同框架里的 recursionLimit 配置對照與選型建議聊到具體配置不同框架對 recursionLimit 的實現(xiàn)不完全一樣。我這里把幾個常見環(huán)境下的配置方式做一下對照方便大家按自己的需求選。6.1 LangGraph顯式的 recursion_limitLangGraph 是我用得最多的 Agent 編排框架。它內(nèi)置了recursion_limit的概念直接傳給編譯后的圖執(zhí)行器即可from langgraph.graph import StateGraph graph StateGraph(...) app graph.compile( checkpointer..., # 限制整個圖執(zhí)行的最大步數(shù) recursion_limit15, ) result await app.ainvoke( initial_state, config{recursion_limit: 15}, )值得注意的是LangGraph 里recursion_limit是按“圖中節(jié)點被執(zhí)行的次數(shù)”計的中間的消息聚合、條件路由本身也會消耗這里的計數(shù)而不僅僅是“工具調(diào)用次數(shù)”。所以設計圖結(jié)構(gòu)時不要以為自己設置 10 就等于模型只能調(diào)用 10 次工具——如果圖里加了一個條件分支節(jié)點每輪循環(huán)要跑兩個節(jié)點那 10 次 recursion limit 其實只相當于 5 輪工具循環(huán)。6.2 OpenAI Assistants APImax_prompt_turns 與 tools 約束OpenAI 的 Assistants API 里沒有叫 recursionLimit 的參數(shù)但有一個更接近的概念max_prompt_turns。它限制的是“一次會話中最多允許多少輪模型推理”。和 LangGraph 不同的是OpenAI 平臺在后臺會自動做一次“任務完成度預判”當你設置得明顯低于任務需要時它會提前終止。另外OpenAI 的 Function Calling 有一個隱性約束同一輪內(nèi)模型可以直接放棄調(diào)用工具并返回最終答案。所以對付簡單失控不一定非要上重型路由先把 system prompt 里的“請僅在確保需要外部數(shù)據(jù)時才調(diào)用工具”寫到位就能避免很多無謂循環(huán)。6.3 自研 Loop自己造輪子時的三個關鍵點如果你像我一樣早期是自己寫簡單的 tool loop讀歷史 → 調(diào)模型 → 調(diào)工具 → 追加歷史 → 再讀歷史那么沒有現(xiàn)成的 recursionLimit 可用必須自己實現(xiàn)。我建議至少封裝三層class ToolLoopController: def __init__(self, max_rounds10, max_consecutive_same_tool2): self.max_rounds max_rounds self.max_consecutive_same_tool max_consecutive_same_tool def before_tool_call(self, trace, next_tool_name, next_tool_args): # 第一層總量限制 if len(trace) self.max_rounds: return stop # 第二層連續(xù)相同工具限制 recent [t.tool_name for t in trace[-3:]] if len(recent) 3 and all(name next_tool_name for name in recent): return force_new_branch # 第三層參數(shù)相似度限制 last_call trace[-1] if trace else None if last_call and last_call.tool_args next_tool_args: return reuse_previous_result return allow這個控制器在每次模型返回 tool call 前執(zhí)行等于是把路由規(guī)則和 recursionLimit 合二為一了。三層判斷分別解決“總步數(shù)超了”“繞圈圈”“原地踏步”三類循環(huán)問題。6.4 選型建議如果你剛開始做 Agent我建議直接用 LangGraph 或同類圖編排框架因為它把 recursionLimit 和條件路由都做成了第一公民不需要你自己維護循環(huán)狀態(tài)。如果只是簡單接一兩個工具的快速驗證場景OpenAI Assistants 的max_prompt_turns也夠用。只有當你對控制粒度有非常規(guī)要求時再考慮自己造輪子。7. 管住循環(huán)之后Agent 才談得上“可靠”工具循環(huán)是 Agent 的核心機制但核心機制恰恰需要被約束才能發(fā)揮價值。recursionLimit 和條件路由這兩者的關系我總結(jié)成一句話限制告訴 Agent 最多能跑多遠路由告訴 Agent 每一步往哪個方向跑。只給限制不給方向Agent 會在撞墻前不斷繞圈只給方向不給限制一個迷路的 Agent 能把整條鏈路拖垮。我個人的實操體會治理工具循環(huán)至少要過三關第一關是基線設計。給每個 Agent 任務估算最合理的步數(shù)范圍然后把 recursionLimit 定在“能完成任務的 1.2 倍”而不是“盡可能大”。比如你推斷任務需要 6 步那就寫 8 或 9不要寫 20。寬松的限制只是延遲了問題爆發(fā)的時間。第二關是運行監(jiān)控。給工具調(diào)用加上日志和可視化追蹤重點觀察那些“逼近上限但還在調(diào)用”的任務。這類任務往往不是缺失步數(shù)而是缺少退出條件——你應該去補路由規(guī)則而不是去調(diào)大限制。第三關是事后復盤。每次循環(huán)失控后把你當時的工具調(diào)用序列保存下來作為構(gòu)造條件路由規(guī)則的數(shù)據(jù)來源。你的路由規(guī)則越貼近真實失敗案例后續(xù)的效果就越好。最后再分享一個小技巧如果你發(fā)現(xiàn) Agent 在某類任務上總是差一兩步才能完成不要只調(diào)高 recursionLimit試著在路由里給它加一個“重試一次并忽略歷史細節(jié)”的分支——讓它忘掉前面幾輪無效信息只基于最近一次工具結(jié)果重新推理。我用了這個方法之后很多“卡在臨界點”的 Agent 一下就順暢了。工具循環(huán)本身不可怕可怕的是對它毫無防備。把遞歸上限和路由規(guī)則一起配齊你的 Agent 才能真正從“會調(diào)用工具”升級成“能靠譜地完成任務”。