上一篇把 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() 返回可傳回模型的結果,也可以是工具執行失敗的信息。
讀這段代碼,可以抓住三個位置:
infer(): 用當前信息調用一次模型。runTool(): 模型之外的程序真正執行動作。input.push(): 保留行動請求與實際結果,供下一次調用使用。
循環的外層結構由程序實現,但下一次調用什麼工具、使用什麼參數,可以由模型根據新結果決定。固定的是“判斷—執行—反饋”的機制,不是每項任務具體要走的路線。
二、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 一定要再試一次嗎?
不一定。失敗信息可以進入下一次判斷,但重試不是唯一行動。路徑明確錯誤時,可以查找位置或請求補充;無法繼續時可以結束;達到程序停止條件時則不應繼續。反饋的價值在於改變下一步,而不是強制重複上一步。