Разработка ИИ-агентов · Модуль 1 · Урок 1.5
Капстоун: минимальный рабочий агент на Go
Цель капстоуна
Соберём всё из Модуля 1 в один маленький, но настоящий агент на Go: он принимает вопрос пользователя, при необходимости вызывает инструмент, и возвращает ответ. Никаких фреймворков — только стандартная библиотека и HTTP-вызов модели. Этого достаточно, чтобы своими руками потрогать всю механику: messages, схему инструмента, tool calling, цикл, стоп-сигнал и safety-cap.
Возьмём два простых инструмента без внешних зависимостей: add(a, b) — сложение и now() — текущее время. Они тривиальны намеренно: фокус не на инструментах, а на контуре агента.
Архитектура минимального агента
Структура такая:
- Типы DTO —
Message,Tool, разбор ответа (choices,finish_reason,tool_calls). Можно начать сmap[string]any, но типы читаются яснее. - Клиент модели — функция, делающая HTTP POST к OpenRouter и возвращающая распарсенный
choice. - Реестр инструментов —
dispatch(name, argsJSON) string: по имени вызывает нужную Go-функцию и возвращает результат строкой. - Цикл —
runAgentиз урока 1.4: send → стоп-сигнал → выполнение → дозапись → safety-cap.
Дальше в курсе мы будем наращивать именно этот скелет: лучше описывать инструменты, управлять контекстом и памятью, добавлять надёжность.
Что проверить на работающем агенте
Хороший признак, что капстоун удался: на вопрос «сколько будет 17 плюс 25 и который сейчас час?» агент сделает два оборота цикла — закажет инструменты, получит результаты, и только потом ответит текстом (финальный stop). А на вопрос «привет, кто ты?» — ответит сразу, без вызова инструментов (один оборот). Если оба сценария работают, значит контур замкнут правильно: стоп-сигнал, дозапись и safety-cap на месте.
// dispatch выполняет инструмент по имени и возвращает результат строкой,
// которую мы вернём модели сообщением с ролью "tool".
func dispatch(name, argsJSON string) string {
switch name {
case "add":
var a struct{ A, B float64 }
if err := json.Unmarshal([]byte(argsJSON), &a); err != nil {
return "ошибка аргументов: " + err.Error()
}
return fmt.Sprintf("%g", a.A+a.B)
case "now":
return time.Now().Format(time.RFC3339)
default:
return "неизвестный инструмент: " + name
}
}func main() {
tools := []Tool{
toolDef("add", "Складывает два числа a и b. Используй для арифметики.",
map[string]any{
"type": "object",
"properties": map[string]any{
"a": map[string]any{"type": "number"},
"b": map[string]any{"type": "number"},
},
"required": []string{"a", "b"},
}),
toolDef("now", "Возвращает текущее время в формате RFC3339. Аргументов нет.",
map[string]any{"type": "object", "properties": map[string]any{}}),
}
messages := []Message{
{Role: "system", Content: "Ты агент. Для арифметики и времени используй инструменты, не угадывай."},
{Role: "user", Content: "Сколько будет 17 плюс 25 и который сейчас час?"},
}
answer, err := runAgent(messages, tools) // цикл из урока 1.4
if err != nil {
log.Fatal(err)
}
fmt.Println(answer)
}Anti-patterns
| Анти-паттерн | Почему плохо | Как правильно |
|---|---|---|
| Тянуть фреймворк ради двух инструментов | Лишняя зависимость и магия скрывают механику | Для капстоуна хватает stdlib и одного HTTP-вызова |
| Возвращать из инструмента не-строку без сериализации | tool-сообщению нужен текстовый content | Приводить результат к строке/JSON перед возвратом модели |
| Игнорировать ошибку парсинга аргументов | Паника на кривом JSON от модели | Проверять ошибку и возвращать понятное сообщение модели |
| Забыть системный промпт про инструменты | Модель «угадывает» вместо вызова | Явно инструктировать использовать инструменты для фактов/действий |
Практическое задание (Капстоун)
- Соберите рабочий агент из уроков 1.2–1.4 с двумя инструментами (
add,now). - Проверьте сценарий с инструментами: вопрос, требующий арифметики и времени, → агент делает 2 оборота и отвечает.
- Проверьте сценарий без инструментов: «кто ты?» → один оборот, ответ сразу (
stop). - Добавьте третий собственный инструмент (например,
reverse(text)) и убедитесь, что модель выбирает его по описанию. - Залогируйте номер оборота,
finish_reasonи каждый вызов инструмента — так виден весь контур. - Проверьте срабатывание safety-cap, временно понизив лимит до 1.
Проверка знаний
Агент с инструментами add и now получил вопрос «который час?».
Сколько оборотов цикла, скорее всего, потребуется до финального ответа?
Верный ответ: B
B верно. Один оборот — модель просит now; код выполняет, возвращает результат; второй оборот — модель формулирует ответ и завершает (stop). C — safety-cap лишь верхняя граница, а не норма. D неверно: выход по стоп-сигналу.
Что должна вернуть функция-инструмент, чтобы её результат корректно ушёл обратно модели?
Верный ответ: B
B верно. Результат возвращается модели как текстовый content сообщения роли tool (часто это JSON-строка). Произвольный объект нужно сериализовать; число и HTML — частные/неуместные случаи.