如何在 OpenRouter 上建立可靠的工具呼叫 Agent 迴圈
Build a Reliable Tool-Calling Agent Loop on OpenRouter
本文介紹如何使用 OpenRouter TypeScript SDK 從頭建立具備工具呼叫能力的 Agent 迴圈,並探討控制流與錯誤處理機製。
- 核心架構:重複傳送對話歷史與工具定義給模型,由應用程式解析引數、執行函式並處理結果。
原文提供使用 TypeScript SDK 建立工具呼叫迴圈的完整步驟,讀者可據此評估如何掌握控制流與終止條件。
工具呼叫代理迴圈會重複將對話與可用工具送給模型,執行模型請求的任何工具呼叫,將結果附加,並詢問模型接下來該做什麼。當模型回傳沒有工具呼叫或應用程式定義的停止條件觸發時,迴圈結束。
模型決定請求哪個工具,但由你的應用程式解析引數、執行函式,並決定何時停止迴圈。
本指南展示如何使用 OpenRouter TypeScript SDK 建構該迴圈。範例使用本機天氣資料,讓你無須設定其他服務即可執行。
簡而言之
- 模型請求工具。你的應用程式執行它們並回傳每個工具呼叫 ID 的一個結果。
- 每次請求時都送出工具定義,包括工具結果後的跟進請求。
- 當模型回傳沒有工具呼叫、同一呼叫重複超過限制,或達到迭代上限時停止。
models引數提供有序模型回退,Auto Exacto 在工具呼叫請求時預設會重新排序供應商。
工具呼叫代理迴圈的運作方式
設定任務、工具與訊息歷史,然後重複以下步驟:
- 以完整歷史與工具定義呼叫模型。
- 若回應沒有工具呼叫,回傳助手文字並停止。
- 若這是允許的最後一次迭代,於執行工具前停止。其結果永遠不會送達模型。
- 若呼叫重複次數過多則停止。
- 否則,執行每個工具呼叫,並將助手訊息與每次呼叫的一個結果附加。
迴圈還需要硬性上限。模型可能重複失敗呼叫或持續搜尋更佳答案。若無迭代上限,該行為可能持續到系統其他部分終止為止。
你不必使用 AI 代理框架即可構建或理解此控制流程。即使之後將迴圈移入函式庫,小型實作仍然有用。
步驟一:設定客戶端並定義工具
建立一個 TypeScript 專案,並安裝 OpenRouter TypeScript SDK 與 tsx:
mkdir openrouter-agent-loop
cd openrouter-agent-loop
npm init -y
npm pkg set type=module
npm install @openrouter/sdk
npm install --save-dev tsx建立一個 OpenRouter API 金鑰,然後將其提供給程序:
export OPENROUTER_API_KEY="your-api-key"建立 agent.ts,並加入客戶端、按順序排列的模型清單以及一個本機工具:
import { OpenRouter } from "@openrouter/sdk";
import type { ChatMessages, ChatToolCall } from "@openrouter/sdk/models";
if (!process.env.OPENROUTER_API_KEY) {
throw new Error("Set OPENROUTER_API_KEY before running this example");
}
const openRouter = new OpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
const models = [
"google/gemini-3-flash-preview",
"nvidia/nemotron-3.5-lightning",
];
const tools = [
{
type: "function" as const,
function: {
name: "get_weather",
description: "Get local sample weather data for Lagos or London",
parameters: {
type: "object",
properties: {
city: { type: "string", enum: ["Lagos", "London"] },
},
required: ["city"],
additionalProperties: false,
},
},
},
];
const weatherByCity: Record<
string,
{ temperatureC: number; conditions: string }
> = {
lagos: { temperatureC: 29, conditions: "partly cloudy" },
london: { temperatureC: 18, conditions: "overcast" },
};工具定義告訴模型何時使用該函式以及接受哪些引數。函式本身保留在你的應用程式內,因為只有你的程式碼能執行它。
保持描述具體,並包含模型正確使用工具所需的細節。在此,命名兩個支援的城市有助於模型產生有效引數。
列表中的兩個模型均支援工具呼叫。模型可用性會隨時間變動,請先檢查 模型目錄 以確認目前支援 tools 引數的模型清單,再選擇適合的模型。
步驟二:呼叫模型並閱讀回應
接下來,加入一個協助函式,傳送一次模型請求並回傳助手訊息與任何工具呼叫:
async function sendTurn(
messages: ChatMessages[],
toolChoice: "required" | "auto",
) {
const result = await openRouter.chat.send({
chatRequest: {
models,
messages,
tools,
toolChoice,
maxCompletionTokens: 1024,
stream: false,
},
});
if (!("choices" in result)) {
throw new Error("Expected a non-streaming response");
}
const message = result.choices[0]?.message;
if (!message) throw new Error("The model returned no message");
return {
result,
message,
calls: message.toolCalls ?? [],
};
}SDK 將請求主體巢狀於 chatRequest,並使用 camelCase 欄位如 toolChoice、maxCompletionTokens 與 toolCalls。它會將它們轉換為 API 的 snake_case 介面格式,因此 toolChoice 以 tool_choice 送出。
每次呼叫均保留 tools,包括後續呼叫。我們會根據這些定義驗證回傳的工具呼叫,模型需要它們來決定是否另一工具能協助。
第一輪使用 toolChoice: "required",因此範例始終執行工具路徑。後續輪次使用 "auto",允許模型在收到工具結果後回傳最終答案。若每輪都必須使用工具,模型將無法以文字結束。這是範例的選擇,而非 API 預設。當存在工具時,tool_choice 的 API 預設為 "auto"。
步驟 3:執行工具呼叫並回傳結果
加入一個協助函式,執行一次工具呼叫並產生你將傳回模型的工具訊息:
async function executeToolCall(call: ChatToolCall): Promise<ChatMessages> {
let content: string;
try {
if (call.function.name !== "get_weather") {
throw new Error(`Unknown tool: ${call.function.name}`);
}
const args = JSON.parse(call.function.arguments) as { city?: unknown };
if (typeof args.city !== "string") {
throw new Error("city must be a string");
}
const weather = weatherByCity[args.city.toLowerCase()];
if (!weather) {
throw new Error(`No weather data for ${args.city}`);
}
content = JSON.stringify({ city: args.city, ...weather });
} catch (error) {
content = JSON.stringify({
error: error instanceof Error ? error.message : String(error),
});
}
return {
role: "tool",
toolCallId: call.id,
content,
};
}工具引數以 JSON 字串傳入,因此 JSON.parse() 應放於 try 區塊內。無效 JSON、未知函式名稱或處理失敗會產生工具結果,而非使迴圈崩潰。模型可以改變引數、選擇其他工具或說明失敗。
結果也攜帶原始呼叫 ID 作為 toolCallId。這是模型將每個結果對應到其所請求的呼叫的方法。回應可包含多個呼叫,每個都需要自己的結果。
此範例會將預期的解析與執行失敗回傳給模型。身份驗證、網路及其他請求層級錯誤仍應保持迴圈,讓周邊應用程式處理。
步驟 4:將呼叫包裝於受限迴圈中
步驟 2 與 3 處理一次模型回合。加入 runAgent() 以連線它們:
async function runAgent(task: string, maxIterations = 10) {
if (!Number.isSafeInteger(maxIterations) || maxIterations < 1) {
throw new Error("maxIterations must be a positive safe integer");
}
const messages: ChatMessages[] = [{ role: "user", content: task }];
const callCounts = new Map<string, number>();
for (let iteration = 1; ; iteration++) {
const startedAt = performance.now();
const { result, message, calls } = await sendTurn(
messages,
iteration === 1 ? "required" : "auto",
);
console.info({
iteration,
model: result.model,
tools: calls.map((call) => call.function.name),
latencyMs: Math.round(performance.now() - startedAt),
});
if (calls.length === 0) {
return typeof message.content === "string" ? message.content : null;
}
if (iteration === maxIterations) {
throw new Error(`Stopped after ${maxIterations} iterations`);
}
for (const call of calls) {
const fingerprint = `${call.function.name}:${call.function.arguments}`;
const count = (callCounts.get(fingerprint) ?? 0) + 1;
callCounts.set(fingerprint, count);
if (count >= 3) {
throw new Error(
`Stopped after three identical calls to ${call.function.name}`,
);
}
}
messages.push(message);
messages.push(...(await Promise.all(calls.map(executeToolCall))));
}
}每一次迭代都呼叫模型、檢查是否完成,然後將助手訊息與工具結果附加。下一次迭代將擴充後的歷史透過 sendTurn() 傳回。將助手訊息放在工具結果之前,以保留完整回合。
迭代上限於模型回應後、工具執行前檢查。工具結果僅在下一次模型請求時有用,因此若模型在最後允許的迭代仍請求工具,迴圈會停止而不執行。執行它們會產生模型永遠無法看到或回報的副作用。
上限是迴圈中唯一的硬限制,檢查會將 iteration 與 maxIterations 做相等比較。runAgent() 在首次請求前會拒絕非正整數安全值的上限,因為像 0、1.5 或 NaN 的數值永遠不會相符,迴圈將一直執行直到模型停止請求工具。Number.isSafeInteger() 亦會拒絕大於 Number.MAX_SAFE_INTEGER 的值,因為 iteration++ 會停止產生不同值,永遠無法達到上限。
指紋計數器在相同工具與引數字串出現三次後停止執行。兩次會在模型在空或暫時結果後重試一次時停止。三次允許一次重試,仍能快速停止卡住的模型。閾值與 maxIterations 的預設為應用程式選擇,而非 OpenRouter 預設。
在正式應用中,請在比較前先正規化已解析的引數,這樣重新格式化但相同的呼叫仍算作重複。你亦可為每個工具設定不同的限制。
測試迴圈
使用一個簡短的 main() 函式完成 agent.ts:
async function main() {
const answer = await runAgent(
"Compare the weather in Lagos and London. Which city is warmer?",
);
console.log(answer);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});在終端機執行:
npx tsx agent.ts你應該會看到一個或多個呼叫 get_weather 的迭代,接著是最終比較。這些值來自步驟 1 的本機對映,故可在將處理程式替換為實際服務前測試完整迴圈。以下為一次對 API 執行的輸出:
{
iteration: 1,
model: 'google/gemini-3-flash-preview',
tools: [ 'get_weather', 'get_weather' ],
latencyMs: 3930
}
{
iteration: 2,
model: 'google/gemini-3-flash-preview',
tools: [],
latencyMs: 1417
}
Lagos is currently warmer than London.第一次迭代在一次回應中回傳兩個 get_weather 呼叫,迴圈並行執行。第二次迭代未回傳工具呼叫,因此迴圈回傳文字。你的執行可能在迭代次數、答案措辭與延遲上有所不同。
加入回退與併發控制
這裡的每個控制項處理不同的失敗模式。迭代與重複限制能阻止應用程式內部的無效行為。你的併發選擇可避免平行工具互相破壞。模型回退會處理所選模型的錯誤。Auto Exacto 在工具呼叫請求時會調整供應商排序。
先以迴圈中已存在的停止條件開始。當模型不傳送工具呼叫時返回,當同一呼叫出現三次時失敗,當模型在第maxIterations次迭代仍要求工具時失敗。即使之後加入時間、代幣或工具特定限制,也請保留硬性上限。
併發是下一個選擇。示例使用Promise.all()執行工具呼叫,因為兩個查詢是獨立的。除非已確認工具不共享狀態,否則請按順序執行呼叫。寫入後再讀取同一條記錄時,兩者不應同時並行。
模型回退是下一層。models 引數 接受一個有序列表。第一個模型為首選,當第一個模型回傳錯誤(包括速率限制、供應商停機、內容審核拒絕)時,我們會嘗試列表中的下一個模型。你的迴圈會讀取result.model來檢視是哪個模型回覆。
Auto Exacto 會在每個包含tools的請求中預設執行。若模型由多個供應商提供,我們會根據吞吐量、工具呼叫成功率以及基準測試結果重新排序供應商,而非依賴價格。你的迴圈不需要改動即可取得此功能。若模型僅由單一供應商提供,則無需重新排序,請在模型頁面檢查供應商列表以確認是否適用重新排序。若你的迴圈在每次迭代都傳送大型工具定義,而提示快取命中率對你更重要,則可透過將provider.sort設為"price"或使用:floor模型變體來選擇退出。
限制歷史成長並記錄每一次迭代
每一次迭代都會加入助手訊息以及每個工具呼叫的一個結果。這些訊息會在後續請求中再次傳送,因此大型工具結果可能會使歷史迅速膨脹。
若工具回傳大型回應而模型只需要少數欄位,請先挑選那些欄位再加入結果。亦可截斷內容或將完整載荷存放於其他位置並回傳參考。保持截斷結果有效。例如,回傳一個包含預覽、原始大小以及截斷標誌的 JSON 物件,而非在 JSON 字串中間切割。
迴圈已經記錄了迭代、回傳模型、工具名稱與延遲。在應用程式中,請加入引數與停止原因的安全雜湊。不要記錄機密或原始敏感引數。簡短且一致的追蹤可顯示執行何時開始重複或達到上限。
在迴圈中使用 MCP 工具
Model Context Protocol 伺服器(MCP 伺服器)會將另一個服務或程序的工具公開出來。你的迴圈仍會將工具定義送給模型、接收呼叫、執行並附加結果。不同之處在於 MCP 客戶端會處理工具發現與執行,而非使用本地函式對映。
當你擁有少量函式且想要最小實作時,請使用本地處理器。若工具已存在於遠端伺服器(如 GitHub、Linear 或內部服務)後面,則使用 MCP。
相同的邊界仍然適用。為每個工具呼叫 ID 回傳一個結果,保持工具在後續請求中可用,偵測重複並執行上限。MCP 改變了工具執行的位置,但並未消除對這些控制的需求。
何時轉向 Agent SDK
當您想讓庫管理多輪迴圈、工具執行、對話狀態和停止條件時,請轉向我們的 Agent SDK。它還支援 MCP 工具、串流、工具審批與狀態持久化,以及 惡性迴圈偵測(針對重複工具呼叫),此功能預設為關閉。
若您的工具已經存在於遠端 MCP 伺服器後端,@openrouter/mcp 可以發現它們並將其與本機工具一起暴露給 Agent SDK 的 callModel 函式。
自行構建迴圈在您想直接控制訊息、工具派遣和停止條件時仍然很有用。它也提供了具體方式,讓您瞭解代理框架為您管理的內容。
當您的應用程式需要持久工作流程或對話之外的持久狀態時,較大的協調系統可能更合適。
下一步
我們處理模型請求、按順序的模型回退以及供應商路由。您的應用程式則負責工具執行及其相關限制。
欲瞭解完整的請求與回應結構,請閱讀 tool-calling guide。本指南使用的 SDK 請求欄位,請參閱 TypeScript SDK overview。若要使用第二個模型對迴圈輸出進行評分,請閱讀 LLM-as-a-Judge: Score AI Agent Outputs Automatically。若要在您的派遣器執行前,檢查每個工具呼叫是否符合使用者請求,請參閱 Gate Agent Tool Calls with Jev。
常見問題
如何決定代理迴圈何時應停止?
代理迴圈應在模型回傳沒有工具呼叫、重複不允許的呼叫,或達到固定迭代上限時停止。正式環境的迴圈也可加入耗時、令牌數或成本限制。它們應始終至少有一個硬性界限,並記錄是哪個界限結束了執行。
工具呼叫失敗時應該怎麼處理?
將失敗作為對應工具結果返回,讓模型能夠回應。使用 TypeScript SDK 時,結果應使用 role: "tool"、包含原始 toolCallId,並包含明確的錯誤訊息。於迴圈內捕捉引數解析與工具執行錯誤,而非讓單一失敗工具終止整個流程。
如何阻止代理重複相同動作?
從工具名稱與引數產生指紋,然後統計其在整個執行期間出現的頻率。當計數超過閾值時停止或套用每個工具的重試上限。在正式環境中,先將解析後的引數正規化,再進行比較,確保等價的 JSON 物件產生相同指紋。僅在迴圈有其他變化條件且有硬性上限時,允許對輪詢工具進行有意重複。
我需要使用 LangChain 或其他 AI 代理框架來建立工具呼叫代理嗎?
不需要。小型代理可使用訊息、工具定義、本機處理器與受限迴圈執行。當您需要管理執行狀態、審批、持久執行或更大型工具生態系統時,才使用 AI 代理框架。
來源:openrouter blog · openrouter.ai