简

從一段循環偽代碼出發,拆解一次模型推理、Tool Call 與 Tool Execution 的區別、工具結果如何回到下一次輸入,以及 Agent 何時停止。

Agent Loop:模型判斷、工具執行與結果回傳怎樣連成循環
20 分鐘
3955 字

上一篇把 Model、Tools、Instructions 和編排方式放在了一起。這一篇繼續往執行過程裏看:模型提出“讀取一個文件”以後,誰去讀取?文件內容回來以後,又怎樣影響下一步?

OpenAI 的 《Unrolling the Codex agent loop》沿着模型輸入、工具調用和後續請求,展開了 Codex 的運行過程。我更關注其中這條連接:模型產生行動請求,外部程序執行,再把實際結果交回模型。 Agent 的連續行動,正是在這幾步之間形成的。

  • 一次推理: 當前信息怎樣交給 Model,模型這一次輸出了什麼。
  • 一次循環: Tool Call 怎樣變成執行,Tool Result 怎樣進入下一次輸入。
  • 一次結束: 什麼時候繼續調用模型,什麼時候停止並交還控制權。

下面用一個假設的 Java 代碼排查任務貫穿説明:“看看 UserService.java 為什麼會返回空值。”

一、先看循環:一次任務可以包含多次模型調用h2

模型第一次收到請求時,可能還沒有文件內容。它可以先提出讀取文件的請求;文件讀出後,才有條件判斷是實現本身返回了空值,還是需要繼續查看調用的方法。

這裏並不是讓同一次推理一直運行到任務完成,而是由模型外面的程序,把多次調用連接起來。先把這個過程壓成一段 JavaScript 風格的偽代碼:

const input = [userMessage];
while (!shouldStop()) {
const output = await infer({ instructions, tools, input });
// 保留本次模型输出,包括工具调用记录。
input.push(...output.items);
if (output.toolCalls.length === 0) {
return output.finalAnswer ?? "无法继续,需要补充信息";
}
for (const call of output.toolCalls) {
const result = await runTool(call);
input.push({
type: "function_call_output",
call_id: call.call_id,
output: result,
});
}
}
return "达到停止条件,结束当前执行";

這裏的 infer() 是為了説明控制流程而抽象出的接口,不是 SDK 的原始返回結構。它把完整輸出保留在 items 中,再提取出待執行的 toolCalls 和當前輪次的最終答覆;runTool() 返回可傳回模型的結果,也可以是工具執行失敗的信息。

讀這段代碼,可以抓住三個位置:

  1. infer(): 用當前信息調用一次模型。
  2. runTool(): 模型之外的程序真正執行動作。
  3. input.push(): 保留行動請求與實際結果,供下一次調用使用。

循環的外層結構由程序實現,但下一次調用什麼工具、使用什麼參數,可以由模型根據新結果決定。固定的是“判斷—執行—反饋”的機制,不是每項任務具體要走的路線。

圖 1 · Agent Loop:外部結果進入下一次模型判斷
Agent Loop 的一次循環 Harness 準備當前輸入,調用模型推理。模型提出 Tool Call 後,Harness 執行工具,再把調用記錄和 Tool Result 接回輸入,重新調用模型。沒有待執行工具且可結束時,向用户答覆或請求補充。達到停止條件也會結束執行。 Harness 準備當前輸入指令 · 工具定義 · 當前交互 Model inference依據當前信息,產生本次輸出 需要工具 Tool Call提出請求,還沒有執行動作 Harness 執行工具由外部程序讀取文件或運行命令 接回 Tool Result保留調用記錄,加入實際結果 新結果 → 新輸入 → 再次推理 無待執行工具且可結束 答覆 / 請求補充結束當前輪次,交還用户 程序達到停止條件時也會退出循環
圖中展示基本控制流;窄屏可在圖內左右滑動查看。

二、Model inference:模型這一次拿到了什麼?h2

一次模型推理,是根據本次輸入生成本次輸出,不是執行整個任務。 從請求到響應,可以把它看作一次調用邊界:程序準備信息,模型處理這些信息,然後返回輸出。

輸入也不只是用户剛剛打出的一句話。以本文的排查任務為例,模型需要知道:要解決什麼問題,有哪些工具可以調用,以及已經獲得了什麼結果。

在原文討論的 Responses API 請求中,可以用三個字段理解這幾類信息:

字段作用放到排查任務中
instructions處理任務的指導依據代碼和執行結果説明問題
tools可調用工具的定義提供讀取文件等能力的名稱和參數説明
input當前交互中的輸入項用户任務、已有消息、工具調用及其結果

tools 告訴模型“可以怎樣請求讀取文件”,並不會自動把整個項目的文件內容交給模型。第一次輸入裏沒有 UserService.java,模型就不能把某一段具體實現當作已經讀到的事實。

此時,一次推理可以輸出一個 read_file 請求;拿到內容後的下一次推理,才根據實際代碼決定是否繼續讀取別的文件。

這裏也能分清推理與訓練:循環把新信息送入下一次調用,不是在每次讀取文件後重新訓練模型。模型的判斷髮生變化,是因為這次可用的信息變了。

三、Tool Call:模型提出動作,不是親自執行動作h2

假設模型提出下面的調用。read_file 是本文為説明過程使用的假設工具名,不代表 Codex 必須通過這個名稱讀取文件。

{
"type": "function_call",
"name": "read_file",
"arguments": "{\"path\":\"UserService.java\"}",
"call_id": "call_read_user_service"
}

這條記錄説明了三件事:要調用哪個工具、傳什麼參數,以及用什麼標識關聯後續結果。它本身不包含已經讀出的文件內容。

真正的執行發生在模型之外。Harness 收到請求後,把它交給對應的工具實現;工具讀取文件,並把內容或錯誤信息返回。

因此,這裏有兩個不同的事件:

  • Tool Call: 模型提出“請讀取這個文件”。
  • Tool Execution: 外部程序真的進行讀取,併產生結果。

在自定義工具場景裏,執行程序可以在應用進程中;具體運行在哪台機器,不改變這個區別。模型生成了調用參數,不等於它已經直接訪問了磁盤。

這也解釋了為什麼工具執行失敗是有意義的反饋。路徑不存在、文件無法讀取,都屬於實際執行結果,而不是模型在輸出請求時就能保證的事情。

四、Tool Result:下一次判斷為什麼能接上上一次行動?h2

假設工具成功讀到了文件。返回給模型的不是一句脱離來源的“找到了”,而是與那次請求對應的結果項:

{
"type": "function_call_output",
"call_id": "call_read_user_service",
"output": "public User findById(Long id) { return repository.find(id); }"
}

這裏的代碼內容只是縮小後的示例。關鍵是相同的 call_id:它把“這次讀取請求”與“這次讀取結果”連接起來。Function calling 文檔也以“接收調用 → 應用執行 → 回傳結果 → 再次請求模型”組織這個過程。

回看偽代碼,有兩次追加:先保留 output.items,再加入工具結果。只保留結果,而丟掉模型提出過什麼請求,會缺少這段交互的關聯信息。 使用需要回傳推理項的接口時,也應按接口要求保留對應輸出,而不是把響應隨意壓成一段文本。

下一次調用獲得的是更新後的交互,不是模型憑空知道外部程序剛剛做了什麼。沿着這個例子,執行過程可以是:

模型調用本次已有的信息本次可能產生的輸出
第一次用户要求排查,尚未讀取實現請求讀取 UserService.java
第二次已有讀取請求與文件內容,看到方法委派給 Repository請求讀取 Repository 的實現
第三次兩個文件的內容都已返回説明哪個分支會返回空值,以及判斷依據

如果第一次讀取返回“文件不存在”,第二次判斷就應該圍繞這個新事實展開,例如確認路徑,而不是沿着一段並不存在的代碼繼續分析。

動態決定下一步,具體就體現在這裏:相同的任務入口,因為工具返回的信息不同,後續行動可以不同。

工具結果也不是絕對正確的保證:讀到的內容是否完整、結果是否足以支持結論,仍需要判斷。循環提供的是獲取新證據的機會,不是自動正確的保證。

五、Exit Condition:循環結束,不一定等於任務成功h2

前面的示例可能調用模型三次,也可能第一次就能答覆。調用次數不是目標;需要新信息時繼續,已經能夠結束時退出,才是這個循環的意義。

最基本的退出情況可以分成三類:

情況當前執行怎樣結束
已有足夠信息,可以給出最終答覆返回結果,結束當前輪次
缺少關鍵條件,無法可靠繼續説明阻塞原因,請求用户補充或幫助
達到程序設定的停止條件停止繼續調用,並交代當前狀態

第二類裏,模型可以回覆“請提供正確的項目路徑”。這是一條結束當前輪次的消息,但不是宣稱排查已經完成。用户補充路徑後,可以開始下一輪處理。

第三類對應偽代碼中的 shouldStop(),例如達到最大迭代次數或用户中斷。它提醒我們:循環是否繼續,不能完全依賴模型總會在恰當的時候主動結束。

還需要分清三個尺度:

  • 一次 inference: 一次模型輸入與輸出。
  • 一個 turn: 從用户提出本輪請求,到系統結束本輪處理;中間可以包含多次 inference 和工具執行。
  • 一段 conversation: 可以包含多個 turn,用户還可以繼續補充信息。

所以,某次模型響應生成完畢,只能説明這一次響應結束了。如果其中還有工具調用,外部執行和後續推理仍可能繼續;反過來,系統已經退出本輪循環,也不代表用户的最終目標一定完成了。Running agents中的運行循環同樣區分了工具執行後繼續與最終答覆後返回。

六、回看整段循環:Model、Harness 與 Agent 各負責什麼?h2

把三者放回同一段代碼,職責就比較清楚了:

概念在這裏關注的職責
Model依據當前輸入進行一次推理,產生答覆或行動請求
Harness / Runtime準備輸入、調用模型、對接工具、接回結果並控制循環
Agent由模型與外部執行機制協作,根據反饋持續推進任務的整體系統

這裏是為了分清執行職責而劃分層次,不是要求所有產品都採用同一套命名。重點在於:模型輸出的是下一步,Harness 把下一步變成實際執行,再把結果帶回模型。

這也回答了開頭的問題。Agent 不只是一次 LLM 調用,因為一次輸出之後,還可以接上外部行動和新一輪判斷;但一個具體任務也可能無需工具,一次推理就結束。Agent Loop 描述的是系統如何組織執行,而不是要求每個任務都必須繞很多圈。

循環裏還有一個很具體的邊界:runTool(call)。模型提出讀取文件、修改代碼或執行命令後,這個函數究竟把動作送到了哪裏?哪些文件被修改,命令在哪個進程裏運行,程序又在哪個環境中啓動? 這已經從“下一步怎樣決定”,走到了“動作真正在哪裏發生”。

幾個關鍵問題,串起這段循環h2

一次推理生成了很長的分析,等於執行了很多輪 Agent Loop 嗎?

不等於。輸出長短描述的是一次推理產生了多少內容;循環輪次描述的是調用之間是否接上了外部結果。只有提出請求、執行、接回結果並再次調用,才形成這裏討論的反饋過程。

為什麼不能只把工具結果交回去,省掉工具調用記錄?

結果需要對應它所回答的請求。保留工具名、參數和調用標識,才能知道哪次行動得到了什麼反饋。具體接口還可能要求保留其他輸出項,不能把所有歷史都替換成最後一條結果。

Harness 寫了固定的 while,為什麼 Agent 仍能動態行動?

循環規定的是調用與反饋的連接方式,不是業務步驟表。讀取實現後,是繼續追調用鏈、檢查另一份文件,還是已經能夠説明原因,要依據當時拿到的信息決定。執行框架固定,與具體路線固定,是兩件不同的事。

模型説“已經修好”,能作為退出依據嗎?

它可以是當前輪次的一條結束消息,但不是修復正確的獨立證據。是否真的滿足任務,需要結合實際修改和檢查結果判斷。應當區分“系統不再繼續調用”與“任務結果已經得到驗證”。

工具失敗以後,Loop 一定要再試一次嗎?

不一定。失敗信息可以進入下一次判斷,但重試不是唯一行動。路徑明確錯誤時,可以查找位置或請求補充;無法繼續時可以結束;達到程序停止條件時則不應繼續。反饋的價值在於改變下一步,而不是強制重複上一步。

參考資料h2