Корпоративный AI-агентчасть 2 из 9
- 01Концепция: агент, который знает компанию
- 02Первый разговор с моделью
- 03Агентный цикл и инструменты
- 04MCP: подключаем внутренние системы
- 05Знания компании: поиск по документам
- 06Права доступа: SSO, OAuth и агент от имени сотрудника
- 07Контекст и деньги: кэширование, сжатие истории и лимиты
- 08Интерфейс: веб-чат и бот в Slack
- 09В прод: периметр, аудит и проверки
Это вторая часть цикла про корпоративного AI-агента. В первой мы разобрали, что строим и из каких частей. Теперь начинаем писать код: без фреймворков, на TypeScript и Bun, каждый шаг можно запустить.
Что такое агент
Если убрать маркетинг, агент состоит из трёх частей:
- модель, которая по тексту решает, что делать дальше;
- инструменты, функции в вашем коде, которые модель может попросить вызвать: прочитать файл, выполнить команду, сходить в API;
- цикл (harness), который передаёт модели историю, выполняет запрошенные инструменты, возвращает результаты и повторяет, пока задача не решена.
Модель здесь самая дорогая и самая заменяемая часть. Всё интересное происходит в цикле, и его мы будем писать сами. Но сначала нужно научиться просто разговаривать с моделью.
Шаг 1. Ключ
Я буду использовать Claude от Anthropic, но устройство у всех коммерческих API похожее, и переписать код под другого провайдера несложно.
- Зарегистрируйтесь в консоли Anthropic и пополните баланс: оплата за токены, на эксперименты из этой серии уйдёт несколько долларов.
- В разделе API Keys создайте ключ.
- Положите его в переменную окружения:
export ANTHROPIC_API_KEY="sk-ant-..."Шаг 2. Проект
mkdir my-agent && cd my-agent
bun init -y
bun add @anthropic-ai/sdkШаг 3. Первый запрос
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".
Что дальше
У нас есть модель, которая умеет только говорить. В следующей части дадим ей руки: инструменты для чтения файлов и запуска команд, и напишем цикл, который превращает чат в агента.