03
Agent Loop — замыкаем петлю
Ключевой момент
Вот это и делает агента агентом. Модель вызывает tool → мы выполняем → результат обратно в LLM → repeat.
Всё что описано выше укладывается в ~80 строк кода. Весь агент — это while loop
+ массив messages.
Цикл крутится, пока модель возвращает tool_calls. Когда она решает, что задача
выполнена — возвращает обычный текст, и цикл завершается.
src/03-agent-loop.ts
// src/03-agent-loop.ts
import OpenAI from "openai";
import dotenv from "dotenv";
dotenv.config();
const client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY ?? "",
});
const tools: OpenAI.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "get_weather",
description: "Получить текущую погоду в городе",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "Название города" },
},
required: ["city"],
},
},
},
{
type: "function",
function: {
name: "get_time",
description: "Получить текущее время в городе",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "Название города" },
},
required: ["city"],
},
},
},
];
// Реестр функций — маппинг имя → реализация
const toolHandlers: Record<string, (args: any) => string> = {
get_weather: ({ city }) => {
const data: Record<string, string> = {
"Москва": "−5°C, снег",
"Анапа": "+8°C, облачно",
};
return data[city] ?? `Нет данных для ${city}`;
},
get_time: ({ city }) => {
const now = new Date();
return `Сейчас в ${city}: ${now.toLocaleTimeString("ru-RU")}`;
},
};
async function agentLoop(userMessage: string) {
// Начальный контекст
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "system", content: "Ты полезный ассистент. Используй tools когда нужно." },
{ role: "user", content: userMessage },
];
let iteration = 0;
const MAX_ITERATIONS = 10; // защита от бесконечного цикла
while (iteration < MAX_ITERATIONS) {
iteration++;
console.log(`\n--- Итерация ${iteration} ---`);
const response = await client.chat.completions.create({
model: "anthropic/claude-sonnet-4.5",
messages,
tools,
});
const message = response.choices[0].message;
// Добавляем ответ модели в контекст
messages.push(message);
// Если нет tool_calls — модель решила остановиться
if (!message.tool_calls || message.tool_calls.length === 0) {
console.log("Агент завершил работу.");
console.log("Ответ:", message.content);
return message.content;
}
// Выполняем каждый tool call
for (const toolCall of message.tool_calls) {
const name = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
console.log(`Tool: ${name}(${JSON.stringify(args)})`);
const handler = toolHandlers[name];
const result = handler
? handler(args)
: `Ошибка: tool "${name}" не найден`;
console.log(`Результат: ${result}`);
// Добавляем результат в контекст
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result,
});
}
// Цикл продолжается — отправляем обратно в LLM
}
console.log("Достигнут лимит итераций!");
}
// Тестируем — запрос, требующий ДВА tool call
agentLoop("Какая погода в Москве и Анапе? И сколько сейчас времени?");
npx tsx src/03-agent-loop.ts
Демо в формате диалога
1/1
Пример вывода. У тебя может быть иначе — это
нормально.
✅ Что должно получиться
На первой итерации есть tool_calls, на следующей — финальный ответ без
tool‑вызовов.
🧯 Если не работает
Проверь парсинг аргументов (JSON.parse) и что handlers всегда возвращают строку.
🔍 Что произошло:
Итерация 1 — модель вернула сразу 3 tool_calls (поняла, что нужны все три). Мы
выполнили, добавили результаты в контекст.
Итерация 2 — модель получила все данные, решила что хватит, и ответила текстом.
Цикл завершился.
💡 Ключевые моменты:
while loop — крутимся пока модель не перестанет возвращать tool_calls.
messages массив растёт — каждый tool result добавляется, модель видит всю историю.
Модель сама решает когда остановиться.
MAX_ITERATIONS — защита, чтобы агент не ушёл в бесконечность.