Next.js и OpenAI: проектируем безопасного weather agent

#nextjs#openai#agentic#safety

За последний год я насмотрелся на кучу demo с API вызовов инструментов (tool calling) в OpenAI, где модель получает слишком много свободы. Особенно тревожно, когда это касается factual claims — утверждений о реальном мире. Вот типичный сценарий:

  1. Пользователь спрашивает: “Нужен ли зонт в Берлине завтра?”
  2. Модель генерит JSON с вызовом “get_weather”
  3. Бекенд без проверок прокидывает запрос в WeatherAPI
  4. Ответ с температурой и осадками возвращается модели
  5. 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 упускают четыре критических аспекта:

  1. Recursive limit — без ограничения глубины вызовов агент может зациклиться
  2. Argument validation — модель может подменить параметры вызова
  3. Output verification — внешний API может вернуть мусор
  4. 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("не удалось проверить");
  });
});

Когда такой подход избыточен

Архитектура с тройной проверкой оправдана, когда:

Для внутренних или демо-проектов можно упростить схему, но я бы все равно добавил:

Что попробовать дальше

  1. Поэкспериментируйте с multi-step verification — например, сравните данные из двух источников
  2. Добавьте user confirmation для чувствительных запросов (“Вы действительно хотите узнать погоду в этом месте?”)
  3. Реализуйте runtime policy checks через Zod схемы
  4. Протестируйте adversarial prompts — попробуйте заставить модель вызвать не тот инструмент

Главный урок: агенты должны быть не просто цепочкой вызовов LLM, а предсказуемыми системами с четкими границами доверия. Next.js отлично подходит для такого дизайна благодаря изоляции server-side логики.


Источник: https://dev.to/gateofai/nextjs-openai-weather-agent-safety-guide-52ii