Вызов инструментов
В прошлом уроке мы разобрали цикл, а инструменты оставили за скобками. Теперь наоборот: разберём, из чего состоит описание инструмента, как модель решает, когда его вызвать, и соберём ревьюера PR целиком — сначала вручную, потом с помощью tool runner из SDK, который прячет цикл внутрь себя.
Из чего состоит инструмент
Инструмент для модели — это три вещи: имя, описание и JSON-схема параметров.
import Anthropic from "@anthropic-ai/sdk";
const TOOLS: Anthropic.Tool[] = [
{
name: "get_diff",
description:
"Возвращает diff pull request по его номеру. Вызывай в начале ревью, " +
"чтобы увидеть изменения, прежде чем читать отдельные файлы.",
input_schema: {
type: "object",
properties: {
pr: { type: "integer", description: "Номер pull request" },
},
required: ["pr"],
},
},
{
name: "read_file",
description:
"Читает файл из основной ветки репозитория. Вызывай, когда из diff " +
"непонятен контекст: как используется изменённая функция, какие есть тесты.",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "Путь от корня репозитория" },
},
required: ["path"],
},
},
];Модель не видит вашего кода. Всё, что она знает об инструменте, написано в description и в описаниях полей схемы. Поэтому описание — самая важная часть. Хорошее описание говорит не только что делает инструмент, но и когда его вызывать: «вызывай в начале ревью», «вызывай, когда непонятен контекст». Старшие модели вызывают инструменты осторожно, и явное условие заметно повышает вероятность, что инструмент будет использован там, где нужно.
Ещё несколько правил для схемы: для полей с фиксированным набором значений используйте enum; в required включайте только действительно обязательное; каждому полю давайте description. Имена — конкретные глаголы: get_diff лучше, чем diff, а post_review_comment лучше, чем comment.
И правило про количество. Тридцать инструментов с похожими описаниями — верный способ получить вызов не того. Держите набор узким: ревьюеру хватает трёх-четырёх. Если инструментов объективно много (например, обёртка над всем API трекера), у платформы есть отдельный механизм поиска инструментов, который подгружает схемы по мере надобности; о нём — в документации, а в курсе мы обходимся маленькими наборами.
Описания стоит тестировать так же, как промпты: соберите десяток запросов, где инструмент нужен, и десяток, где не нужен, и проверьте, в скольких случаях модель угадала. Изменение одной фразы в описании часто даёт больше, чем любые ухищрения в цикле.
Ручной цикл целиком
Соберём цикл из прошлого урока с настоящими инструментами. Функции fetchDiff и fetchFile ходят в API вашего git-сервера; их реализация не важна для урока.
const client = new Anthropic();
async function runTool(name: string, input: unknown): Promise<string> {
const args = input as Record<string, any>;
switch (name) {
case "get_diff":
return await fetchDiff(args.pr);
case "read_file":
return await fetchFile(args.path);
default:
throw new Error(`Неизвестный инструмент ${name}`);
}
}
async function review(pr: number): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: `Проверь pull request #${pr} и напиши замечания.` },
];
for (let i = 0; i < 20; i++) {
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
system: REVIEW_RULES,
tools: TOOLS,
messages,
});
if (response.stop_reason === "end_turn") {
return response.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("\n");
}
if (response.stop_reason !== "tool_use") {
throw new Error(`stop_reason: ${response.stop_reason}`);
}
messages.push({ role: "assistant", content: response.content });
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
try {
results.push({
type: "tool_result",
tool_use_id: block.id,
content: await runTool(block.name, block.input),
});
} catch (err) {
results.push({
type: "tool_result",
tool_use_id: block.id,
content: err instanceof Error ? err.message : String(err),
is_error: true,
});
}
}
messages.push({ role: "user", content: results });
}
throw new Error("Слишком много итераций");
}block.input в SDK уже разобран в объект, парсить JSON вручную не нужно. Но и полагаться на строгое соответствие схеме по умолчанию нельзя: модель может пропустить необязательное поле или прислать число строкой. Если нужна гарантия, добавьте в описание инструмента strict: true (схема при этом должна содержать additionalProperties: false и полный required). Тогда API проверит параметры до того, как они попадут к вам.
Управление выбором инструмента
Параметр tool_choice говорит модели, обязана ли она вызывать инструменты. По умолчанию { type: "auto" }: модель решает сама. { type: "none" } запрещает вызовы на этом шаге. { type: "any" } требует вызвать хотя бы один, а { type: "tool", name: "get_diff" } — конкретный. Последние два не поддерживаются на Fable 5.1, поэтому для переносимости лучше формулировать требование в промпте: «Начни с вызова get_diff». Любой вариант можно дополнить disable_parallel_tool_use: true, если вам нужно не больше одного вызова за шаг.
Tool runner: цикл внутри SDK
Ручной цикл полезно написать один раз, чтобы понимать, что происходит. Дальше его удобнее не писать. В SDK есть tool runner: вы описываете инструменты вместе с функцией run, а цикл он крутит сам. На момент написания это бета-возможность SDK, работает через client.beta.messages.
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const getDiff = betaZodTool({
name: "get_diff",
description: "Возвращает diff pull request по номеру. Вызывай в начале ревью.",
inputSchema: z.object({
pr: z.number().int().describe("Номер pull request"),
}),
run: async ({ pr }) => fetchDiff(pr),
});
const readFile = betaZodTool({
name: "read_file",
description:
"Читает файл из основной ветки. Вызывай, когда из diff непонятен контекст.",
inputSchema: z.object({
path: z.string().describe("Путь от корня репозитория"),
}),
run: async ({ path }) => fetchFile(path),
});
const finalMessage = await client.beta.messages.toolRunner({
model: "claude-opus-5",
max_tokens: 16000,
system: REVIEW_RULES,
tools: [getDiff, readFile],
messages: [{ role: "user", content: "Проверь pull request #482." }],
});
for (const block of finalMessage.content) {
if (block.type === "text") console.log(block.text);
}Схема генерируется из Zod, типы параметров в run выводятся автоматически, ошибки внутри run превращаются в tool_result с is_error. Если не хотите зависеть от Zod, есть betaTool из @anthropic-ai/sdk/helpers/beta/json-schema, который принимает обычную JSON-схему.
Tool runner — не чёрный ящик. Его можно итерировать (for await (const message of runner)) и между шагами смотреть, какие инструменты модель собирается вызвать. Ограничение на итерации задаётся параметром max_iterations.
Подтверждение человеком
Добавим ревьюеру третий инструмент — post_review_comment, который пишет в PR. Такой инструмент стоит поставить на подтверждение. С tool runner это делается внутри run: спросили человека, если отказал — вернули строку «пользователь отклонил», и модель узнает об этом как о результате.
const postComment = betaZodTool({
name: "post_review_comment",
description: "Оставляет комментарий к pull request. Вызывай только для финальных замечаний.",
inputSchema: z.object({
pr: z.number().int(),
body: z.string().describe("Текст комментария в Markdown"),
}),
run: async ({ pr, body }) => {
const ok = await askHuman(`Опубликовать в PR #${pr}?\n\n${body}`);
if (!ok) return "Пользователь отклонил публикацию комментария.";
await publishComment(pr, body);
return "Комментарий опубликован.";
},
});Правило из прошлого урока никуда не делось: модель решает, что вызвать, а ваш код — можно ли. Tool runner это правило не отменяет, он лишь избавляет от рутины.
Попробуйте сами
10–15 мин на рабочем месте- Напишите описания для двух инструментов из своего проекта, включив в каждое условие «когда вызывать». Дайте прочитать коллеге: понятно ли из текста, чем они отличаются?
- Соберите ревьюера на tool runner. Вместо настоящего git-сервера пусть
fetchDiffвозвращает diff из локального файла. Посмотрите, в каком порядке модель вызывает инструменты. - Добавьте
strict: trueк ручному определению инструмента и намеренно уберитеadditionalProperties: false. Посмотрите, какую ошибку вернёт API.
Коротко
- Инструмент — это имя, описание и JSON-схема; модель видит только их.
- В описании пишите не только что делает инструмент, но и когда его вызывать.
block.inputуже разобран;strict: trueдаёт гарантию соответствия схеме.tool_choiceуправляет обязательностью вызова;anyиtoolесть не на всех моделях.- Tool runner в SDK (
betaZodTool+client.beta.messages.toolRunner) прячет цикл; ошибкиrunстановятсяtool_resultсis_error. - Подтверждение человеком делается внутри
run: отказ возвращается модели как результат.
Видеоверсия
Сценарий озвучки · 526 слов, ≈ 4 мин
В прошлом уроке мы разобрали агентный цикл. Теперь разберём его вторую половину — инструменты: как их описывать, чтобы модель вызывала их вовремя и с правильными параметрами.
Инструмент для модели — это три вещи. Имя. Описание. И схема параметров в формате джейсон-схемы. Больше модель о нём не знает ничего: она не видит вашего кода. Поэтому описание — самая важная часть. Хорошее описание говорит не только, что делает инструмент, но и когда его вызывать. Например: «возвращает изменения по номеру пул-реквеста, вызывай в начале ревью». Или: «читает файл из основной ветки, вызывай, когда из изменений непонятен контекст». Старшие модели вызывают инструменты осторожно, и такое явное условие заметно повышает шанс, что инструмент будет использован там, где нужно.
Ещё несколько правил для схемы. Для полей с фиксированным набором значений используйте перечисление. В список обязательных включайте только то, что действительно обязательно. Каждому полю давайте описание. И называйте инструменты конкретными глаголами: «получить дифф» лучше, чем просто «дифф».
Соберём ревьюера целиком. Список инструментов, функция-диспетчер, которая по имени ходит в API гит-сервера, и цикл из прошлого урока. Одно дополнение: вызов инструмента обёрнут в обработку ошибок. Если гит-сервер ответил ошибкой, мы не роняем цикл, а возвращаем модели результат с флагом «ошибка» и текстом. Она прочитает и решит, что делать.
Параметры инструмента SDK уже разобрал в объект, парсить их вручную не нужно. Но по умолчанию строгого соответствия схеме нет: модель может пропустить необязательное поле. Если нужна гарантия, добавьте к инструменту флаг «строгий», и API проверит параметры до того, как они попадут к вам.
Два слова про количество и проверку. Тридцать инструментов с похожими описаниями — верный способ получить вызов не того. Держите набор узким: ревьюеру хватает трёх-четырёх. И тестируйте описания, как промпты: десяток запросов, где инструмент нужен, десяток, где не нужен, и смотрите, сколько раз модель угадала. Одна фраза в описании часто даёт больше, чем любые ухищрения в цикле.
Есть параметр, который управляет обязательностью вызова. По умолчанию модель решает сама. Можно запретить вызовы на этом шаге, можно потребовать вызвать хотя бы один инструмент или конкретный. Но два последних варианта поддерживаются не на всех моделях, поэтому надёжнее просто написать в промпте: «начни с вызова такого-то инструмента».
Теперь про упрощение. Ручной цикл полезно написать один раз, чтобы понимать, что происходит. Дальше его удобнее не писать вовсе. В SDK есть tool runner: вы описываете инструмент вместе с функцией, которая его выполняет, а цикл он крутит сам. Схему он генерирует из описания на Zod, типы параметров выводит автоматически, ошибки внутри функции превращает в результат с флагом «ошибка». На момент записи это бета-возможность SDK. И это не чёрный ящик: между шагами можно посмотреть, какие инструменты модель собирается вызвать, и ограничить число итераций.
Последнее — подтверждение человеком. Добавим ревьюеру инструмент «оставить комментарий в пул-реквесте». Его стоит поставить на подтверждение. С tool runner это делается прямо внутри функции инструмента: спросили человека, если отказал — вернули модели строку «пользователь отклонил публикацию». Модель узнает об отказе как о результате и учтёт его. Правило из прошлого урока остаётся: модель решает, что вызвать, а ваш код — можно ли.
Подведём итог. Инструмент — это имя, описание и схема, и модель видит только их. Описание говорит, когда вызывать. Цикл можно написать руками, а можно отдать tool runner. Подтверждение человека живёт внутри функции инструмента. В следующем уроке разберём, что происходит у модели в голове перед ответом, — режим рассуждения.
