balkhaev.com Блог · AI-агенты и автоматизации

Корпоративный AI-агентчасть 2 из 9

Первый разговор с моделью

Берём коммерческую модель по API и за 40 строк пишем чат в терминале со стримингом и историей.

обновлено 4 мин чтенияagentspracticeclaude

Корпоративный AI-агентчасть 2 из 9
  1. 01Концепция: агент, который знает компанию
  2. 02Первый разговор с моделью
  3. 03Агентный цикл и инструменты
  4. 04MCP: подключаем внутренние системы
  5. 05Знания компании: поиск по документам
  6. 06Права доступа: SSO, OAuth и агент от имени сотрудника
  7. 07Контекст и деньги: кэширование, сжатие истории и лимиты
  8. 08Интерфейс: веб-чат и бот в Slack
  9. 09В прод: периметр, аудит и проверки

Это вторая часть цикла про корпоративного AI-агента. В первой мы разобрали, что строим и из каких частей. Теперь начинаем писать код: без фреймворков, на TypeScript и Bun, каждый шаг можно запустить.

Что такое агент

Если убрать маркетинг, агент состоит из трёх частей:

  • модель, которая по тексту решает, что делать дальше;
  • инструменты, функции в вашем коде, которые модель может попросить вызвать: прочитать файл, выполнить команду, сходить в API;
  • цикл (harness), который передаёт модели историю, выполняет запрошенные инструменты, возвращает результаты и повторяет, пока задача не решена.

Модель здесь самая дорогая и самая заменяемая часть. Всё интересное происходит в цикле, и его мы будем писать сами. Но сначала нужно научиться просто разговаривать с моделью.

Шаг 1. Ключ

Я буду использовать Claude от Anthropic, но устройство у всех коммерческих API похожее, и переписать код под другого провайдера несложно.

  1. Зарегистрируйтесь в консоли Anthropic и пополните баланс: оплата за токены, на эксперименты из этой серии уйдёт несколько долларов.
  2. В разделе API Keys создайте ключ.
  3. Положите его в переменную окружения:
export ANTHROPIC_API_KEY="sk-ant-..."

Шаг 2. Проект

mkdir my-agent && cd my-agent
bun init -y
bun add @anthropic-ai/sdk

Шаг 3. Первый запрос

hello.tsts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const reply = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 16000,
  messages: [{ role: "user", content: "Объясни, что такое агент, в одном предложении." }],
});

for (const block of reply.content) {
  if (block.type === "text") console.log(block.text);
}
console.log(reply.stop_reason, reply.usage);
bun hello.ts

Обратите внимание на три вещи в ответе.

  • content это массив блоков, а не строка. Кроме text там бывают блоки размышлений (thinking) и вызовы инструментов (tool_use). В следующей части именно на них построен агент.
  • stop_reason объясняет, почему модель остановилась: end_turn (договорила), max_tokens (упёрлась в лимит), tool_use (хочет вызвать инструмент), refusal (отказалась отвечать).
  • usage показывает, сколько токенов ушло на вход и выход. По нему считаются деньги: у Opus 5.5 это 4 доллара за миллион входных токенов и 20 за миллион выходных.

Шаг 4. История

API не хранит разговор. Каждый запрос отправляет всю историю целиком, а «память» модели это просто массив messages, который вы растите сами:

const messages: Anthropic.MessageParam[] = [
  { role: "user", content: "Меня зовут Миша." },
  { role: "assistant", content: "Привет, Миша!" },
  { role: "user", content: "Как меня зовут?" },
];

Из этого следуют две вещи, которые понадобятся позже. Каждый следующий запрос дороже предыдущего, потому что история растёт. И вы полностью контролируете, что модель «помнит»: можно вырезать, сжать или подменить что угодно. Этим займёмся в части про контекст и деньги.

Шаг 5. Чат со стримингом

Ответ модели может генерироваться десятки секунд. Чтобы не смотреть в пустой экран, используем стриминг: текст печатается по мере генерации, а в конце finalMessage() отдаёт полный ответ.

Весь chat.ts занимает около 40 строк: цикл читает строку из терминала, кладёт её в messages и делает запрос. Главное в нём вот это:

const stream = client.messages.stream({
  model: "claude-opus-5-5",
  max_tokens: 16000,
  output_config: { effort: "low" },
  system,
  messages,
});
stream.on("text", (delta) => process.stdout.write(delta));
const reply = await stream.finalMessage();

// в историю кладём весь content, а не только текст
messages.push({ role: "assistant", content: reply.content });

Остальное обвязка: выйти по /exit, при stop_reason: "refusal" убрать последний вопрос из истории и после ответа напечатать reply.usage.

bun chat.ts

Поговорите с ним пару минут и посмотрите на счётчик токенов: входных становится больше с каждым сообщением, потому что история уходит в каждый запрос заново.

Разбор

system задаёт роль и правила. Это не сообщение пользователя: модель относится к нему как к инструкции от разработчика. В агенте здесь будет описание рабочей папки и правил поведения.

effort управляет тем, насколько глубоко модель думает перед ответом. Для чата хватает low, для агента, который правит код, поставим medium или high. Чем выше, тем медленнее и дороже, но точнее на сложных задачах.

reply.content целиком в историю. Соблазнительно сохранить только текст ответа, но в content лежат и служебные блоки, например размышления модели. API ожидает получить их обратно в том же виде. В агенте это станет критично: там в content будут вызовы инструментов, и без них следующий запрос просто не пройдёт.

max_tokens это потолок длины ответа, а не цель. Модель не пытается его заполнить. Если поставить слишком мало, ответ оборвётся на полуслове с stop_reason: "max_tokens".

Что дальше

У нас есть модель, которая умеет только говорить. В следующей части дадим ей руки: инструменты для чтения файлов и запуска команд, и напишем цикл, который превращает чат в агента.