За последний год я насмотрелся на кучу demo с API вызовов инструментов (tool calling) в OpenAI, где модель получает слишком много свободы. Особенно тревожно, когда это касается factual claims — утверждений о реальном мире. Вот типичный сценарий:
- Пользователь спрашивает: “Нужен ли зонт в Берлине завтра?”
- Модель генерит JSON с вызовом “get_weather”
- Бекенд без проверок прокидывает запрос в WeatherAPI
- Ответ с температурой и осадками возвращается модели
- GPT радостно сочиняет текст про дождь, даже если WeatherAPI вернул 404
Проблема: модель делает вид, что у нее есть доступ к реальным данным, даже когда их нет. В production такой агент быстро потеряет доверие.
Принцип solver-grounded дизайна
Вместо свободного доступа к API нам нужен workflow, где:
- Модель предлагает вызов инструмента
- Сервер валидирует параметры
- Только доверенный сервис делает запрос
- Верификационный слой проверяет ответ
- Модель получает только проверенные данные
Пример контракта для погодного инструмента:
interface WeatherToolInput {
location: string; // "Berlin" или "48.137154, 11.576124"
date?: string; // "2024-03-15"
}
interface VerifiedWeatherData {
resolvedLocation: string; // "Berlin, DE"
date: string;
temperature: number;
precipitation: number;
windSpeed: number;
verifiedAt: string;
source: "openweathermap" | "weatherapi";
}
Критично, что модель никогда не видит сырой ответ от API — только нормализованные и проверенные данные.
Три слоя защиты в Next.js
1. Route Handler как контроллер доступа
В Next.js 14+ мы можем вынести всю логику агента в server action или route handler:
// app/api/weather/route.ts
export async function POST(req: Request) {
const { messages } = await req.json();
// 1. Валидация входящего запроса
if (messages.length > 5) {
return Response.json({ error: "Message limit exceeded" }, { status: 429 });
}
// 2. Вызов модели с ограниченным набором tools
const response = await openai.chat.completions.create({
model: "gpt-4-1106-preview",
messages,
tools: [WEATHER_TOOL], // только наш разрешенный инструмент
tool_choice: "auto",
});
// 3. Обработка вызова инструмента
const toolCall = response.choices[0]?.message.tool_calls?.[0];
if (toolCall?.name === "get_weather") {
const args = safeParseWeatherArgs(toolCall.arguments); // валидация!
const weatherData = await fetchVerifiedWeather(args); // доверенный источник
return Response.json(weatherData);
}
// 4. Возврат текстового ответа
return Response.json({ message: response.choices[0].message.content });
}
2. Верификация данных перед показом
Прежде чем передавать данные модели, проверяем:
async function fetchVerifiedWeather(args: WeatherToolInput) {
const rawData = await weatherAPI.fetch(args);
// Проверка основных инвариантов
if (!rawData.resolvedAddress) throw new Error("Location not resolved");
if (rawData.days?.[0]?.datetime !== args.date) throw new Error("Date mismatch");
if (typeof rawData.days?.[0]?.temp !== "number") throw new Error("Invalid data");
return {
resolvedLocation: rawData.resolvedAddress,
date: rawData.days[0].datetime,
temperature: rawData.days[0].temp,
precipitation: rawData.days[0].precip,
windSpeed: rawData.days[0].windspeed,
verifiedAt: new Date().toISOString(),
source: "weatherapi"
} satisfies VerifiedWeatherData;
}
3. Честный UI о статусе данных
Интерфейс должен явно показывать, когда данные live, а когда кэшированные или неудачные:
<WeatherResponse>
{data.verifiedAt && (
<div className="text-sm opacity-75">
Данные актуальны на {new Date(data.verifiedAt).toLocaleTimeString()}
</div>
)}
{error && (
<div className="text-red-500">
Не удалось проверить свежие данные. Попробуйте уточнить запрос.
</div>
)}
</WeatherResponse>
Чего не хватает в стандартных туториалах
Большинство гайдов по tool calling упускают четыре критических аспекта:
- Recursive limit — без ограничения глубины вызовов агент может зациклиться
- Argument validation — модель может подменить параметры вызова
- Output verification — внешний API может вернуть мусор
- Error handling — что показывать пользователю при сбоях
Мой совет: начните с написания тестов на edge cases, прежде чем реализовывать happy path. Вот какие кейсы стоит покрыть:
describe("Weather Agent Safety", () => {
it("should reject tool calls with malformed args", async () => {
const fakeCall = { name: "get_weather", arguments: "{}" };
await expect(handleToolCall(fakeCall)).rejects.toThrow();
});
it("should verify location resolution", async () => {
const response = await handleRequest("Погода в Париже");
expect(response.resolvedLocation).toMatch(/Paris/);
});
it("should fail gracefully on API error", async () => {
mockWeatherAPI.mockReject(new Error("Timeout"));
const response = await handleRequest("Погода в Берлине");
expect(response.message).toContain("не удалось проверить");
});
});
Когда такой подход избыточен
Архитектура с тройной проверкой оправдана, когда:
- Данные влияют на решения (например, отмена мероприятия из-за погоды)
- Источник данных нестабильный
- Есть требования compliance
Для внутренних или демо-проектов можно упростить схему, но я бы все равно добавил:
- Явное разделение trusted/untrusted частей системы
- Логирование всех вызовов инструментов
- Ограничение списка доступных API
Что попробовать дальше
- Поэкспериментируйте с multi-step verification — например, сравните данные из двух источников
- Добавьте user confirmation для чувствительных запросов (“Вы действительно хотите узнать погоду в этом месте?”)
- Реализуйте runtime policy checks через Zod схемы
- Протестируйте adversarial prompts — попробуйте заставить модель вызвать не тот инструмент
Главный урок: агенты должны быть не просто цепочкой вызовов LLM, а предсказуемыми системами с четкими границами доверия. Next.js отлично подходит для такого дизайна благодаря изоляции server-side логики.
Источник: https://dev.to/gateofai/nextjs-openai-weather-agent-safety-guide-52ii