Как создать агента

Как создать агента

@ai_longreads

Создание полноценного агента для редактирования кода оказывается неожиданно простым. В этой статье мы напишем агента на Go менее чем в 400 строках кода, способного читать файлы, просматривать директории и редактировать код.

Это AI-перевод статьи, сделанный каналом Про AI: Лучшие Статьи и Исследования.


Как создать агента

How to Build an Agent Автор: Thorsten Ball Оригинальный текст

или: Король-то голый

Создать полнофункционального агента для редактирования кода не так уж сложно.

Кажется, что должно быть. Когда смотришь, как агент редактирует файлы, выполняет команды, выкручивается из ошибок, пробует разные стратегии — кажется, что за этим стоит какой-то секрет.

Его нет. Это LLM (большая языковая модель), цикл и достаточное количество токенов. Это то, о чём мы говорим в подкасте с самого начала. Всё остальное, что делает Amp таким захватывающим и впечатляющим? Кропотливая работа.

Но для создания маленького и при этом очень впечатляющего агента даже это не нужно. Можно уложиться в менее чем 400 строк кода, большая часть которых — boilerplate (шаблонный код).

Я покажу вам как прямо сейчас. Мы вместе напишем код и пройдём путь от нуля до "вау, это… переворот в сознании".

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

Вот что нам понадобится:

Приготовьте карандаши!

Давайте сразу перейдём к делу и настроим новый Go-проект четырьмя простыми командами:

mkdir cd touch

Теперь откроем main.go и для начала добавим каркас того, что нам нужно:

package import("bufio" "context" "fmt" "os""github.com/anthropics/anthropic-sdk-go") func main(){:=. NewClient():=. NewScanner(.):= func()(string, bool){if!. Scan(){return "", false} return. Text(), true}:= NewAgent(&,):=. Run(. TODO()) if!= nil{. Printf("Error: %s\n",. Error())}} func NewAgent(*., func()(string, bool))*{return &{:,:,}} type struct{*. func()(string, bool)}

Да, это пока не компилируется. Но здесь у нас Agent, который имеет доступ к anthropic.Client (который по умолчанию ищет ANTHROPIC_API_KEY) и может получить сообщение пользователя, читая из stdin в терминале.

Теперь добавим недостающий метод Run():

// main.go func(*) Run(.) error{:=[].{}. Println("Chat with Claude (use 'ctrl-c' to quit)") for{. Print("\u001b[94mYou\u001b[0m: "),:=. getUserMessage() if!{break}:=. NewUserMessage(. NewTextBlock()) = append(,),:=. runInference(,) if!= nil{return} = append(,. ToParam()) for _,:= range.{switch.{case "text":. Printf("\u001b[93mClaude\u001b[0m: %s\n",.)}}} return nil} func(*) runInference(.,[].)(*., error){,:=... New(,.{:.,: int64(1024),:,}) return,}

Это не так уж много, правда? 90 строк, и самое важное в них — цикл в Run(), который позволяет нам общаться с Claude. Но это уже сердцебиение программы.

И для сердцебиения всё довольно прямолинейно: сначала выводим приглашение, просим пользователя что-то ввести, добавляем в диалог, отправляем Claude, добавляем ответ Claude в диалог, выводим ответ и по кругу.

Это каждое AI-чат-приложение, которым вы когда-либо пользовались, только в терминале.

Запустим:

export ANTHROPIC_API_KEY = "это последний раз, когда я напоминаю установить это" # Скачиваем зависимости # Запускаем

Затем можно просто общаться с Claude:

$ go run main.go

Chat with Claude (use 'ctrl-c' to quit)

You: Hey! I'm Thorsten! How are you?

Claude: Hi Thorsten! I'm doing well, thanks for asking...

You: Can you come up with any horse-related nicknames that make fun of my first name?

Claude: I can try to come up with some playful horse-related nicknames based on "Thorsten": * Thorough-bred Thorsten * Trotsten * Neighsten...

Заметьте, как мы вели один диалог на протяжении нескольких ходов. Он помнит моё имя из первого сообщения. Массив conversation растёт с каждым ходом, и мы отправляем весь диалог каждый раз. Сервер — сервер Anthropic — stateless (без сохранения состояния). Он видит только то, что находится в срезе conversation. Поддерживать это — наша задача.

Ладно, двигаемся дальше, потому что прозвища так себе и это ещё не агент. Что такое агент? Вот моё определение: LLM с доступом к инструментам, дающим возможность изменять что-то за пределами контекстного окна.

Первый инструмент

LLM с доступом к инструментам? Что такое инструмент? Базовая идея такова: вы отправляете модели промпт, говорящий, что она должна ответить определённым образом, если хочет использовать "инструмент". Затем вы, получатель этого сообщения, "используете инструмент", выполняя его и отвечая результатом. Вот и всё. Всё остальное, что мы увидим — это просто абстракции поверх этого.

Представьте, что вы говорите другу: "в следующем разговоре моргни, если хочешь, чтобы я поднял руку". Странная фраза, но концепция понятна.

Мы можем попробовать это прямо сейчас, не меняя наш код.

$ go run main.go

Chat with Claude (use 'ctrl-c' to quit)

You: You are a weather expert. When I ask you about the weather in a given location, I want you to reply with `get_weather()`. I will then tell you what the weather in that location is. Understood?

Claude: I understand your instructions...

You: Hey, what's the weather in Munich?

Claude: get_weather(Munich)

Мы сказали Claude моргнуть с get_weather, когда он хочет узнать погоду. Следующий шаг — поднять руку и ответить "результатом инструмента":

You: hot and humid, 28 degrees celcius

Claude: Thank you for providing that information. The current weather in Munich is hot and humid at 28 degrees Celsius...

Сработало очень хорошо с первого раза, да?

Эти модели обучены и дообучены использовать "инструменты", и они очень хотят это делать. К 2025 году они как бы "знают", что не знают всего и могут использовать инструменты для получения дополнительной информации.

Подводя итог, всё, что есть в инструментах и их использовании — это две вещи:

  1. Вы сообщаете модели, какие инструменты доступны
  2. Когда модель хочет выполнить инструмент, она вам сообщает, вы выполняете инструмент и отправляете ответ

Чтобы упростить (1), крупные провайдеры моделей встроили API для отправки определений инструментов.

Окей, теперь давайте создадим наш первый инструмент: read_file

Инструмент read_file

Чтобы определить инструмент read_file, мы будем использовать типы, которые предлагает Anthropic SDK, но имейте в виду: под капотом всё это превратится в строки, отправляемые модели. Всё это "моргни, если хочешь, чтобы я использовал read_file".

Каждый добавляемый инструмент требует следующего:

  • Имя
  • Описание, объясняющее модели, что делает инструмент, когда его использовать, когда не использовать, что он возвращает и т.д.
  • Схема входных данных в формате JSON-схемы
  • Функция, которая фактически выполняет инструмент и возвращает результат

Добавим это в код:

// main.go type struct{string`json:"name"` string`json:"description"`.`json:"input_schema"` func(.)(string, error)}

Теперь дадим нашему Agent определения инструментов и отправим их модели в runInference.

Давайте определим read_file:

// main.go var ={: "read_file",:"Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.",:,:,} type struct{string`json:"path" jsonschema_description:"The relative path of a file in the working directory."`} var =[]() func ReadFile(.)(string, error){:={}:=. Unmarshal(, &) if!= nil{panic()},:=. ReadFile(.) if!= nil{return "",} return string(), nil}

Немного, правда? Это одна функция ReadFile и два описания, которые увидит модель: Description, описывающий сам инструмент, и описание единственного входного параметра.

Время попробовать!

$ go run main.go

Chat with Claude (use 'ctrl-c' to quit)

You: what's in main.go?

Claude: I'll help you check what's in the main.go file. Let me read it for you.

Погодите, что? Он хочет использовать инструмент! Очевидно, Claude знает, что может читать файлы, верно?

Проблема в том, что мы не слушаем! Когда Claude моргает, мы игнорируем. Нужно это исправить.

Добавляем обработку вызова инструментов: когда получаем message от Claude, проверяем, попросил ли Claude выполнить инструмент, ища content.Type == "tool_use". Если да — передаём в executeTool, ищем инструмент по имени в локальном реестре, разбираем входные данные, выполняем, возвращаем результат.

Подготовка — выполним это:

echo 'what animal is the most disagreeable because it always says neigh?'>> secret-file.txt

Это создаёт файл с загадочной загадкой.

В той же директории запустим нашего нового агента:

$ go run main.go

Chat with Claude (use 'ctrl-c' to quit)

You: Claude, buddy, help me solve the riddle in the secret-file.txt file

Claude: I'll help you solve the riddle. Let me first read the contents of this file.

tool: read_file({"path":"secret-file.txt"})

Claude: Great! The answer to the riddle is: **A horse**...

Давайте сделаем глубокий вдох и скажем вместе. Готовы? Поехали: ничего себе. Вы просто даёте ему инструмент, и он… использует его, когда думает, что это поможет решить задачу. Помните: мы ничего не говорили о "если пользователь спрашивает о файле, прочитай файл". Нет, ничего такого. Мы говорим "помоги решить загадку в этом файле", и Claude понимает, что может прочитать файл, чтобы ответить.

Инструмент list_files

Если вы похожи на меня, первое, что вы делаете при входе на новый компьютер — запускаете ls, чтобы осмотреться.

Дадим Claude такую же возможность. Вот полная реализация инструмента list_files:

// main.go var ={: "list_files",:"List files and directories at a given path. If no path is provided, lists files in the current directory.",:,:,}

Ничего сложного: list_files возвращает список файлов и директорий в текущей папке.

Одно замечание: мы возвращаем список строк и обозначаем директории завершающим слэшем. Это не обязательно, я просто так решил. Фиксированного формата нет. Подойдёт всё, что Claude может понять.

Теперь проверим:

$ go run main.go

You: what do you see in this directory?

tool: list_files({})

Claude: I can see several files and directories: .git/, .gitignore, main.go, go.mod, go.sum, blogpost.md, .envrc...

Работает! Но вот что интересно: Claude умеет комбинировать инструменты:

$ go run main.go

You: Tell me about all the Go files in here. Be brief!

tool: list_files({})
tool: read_file({"path":"main.go"})
tool: read_file({"path":"go.mod"})

Claude: Here's a brief overview of the Go files in this project...

Сначала он использовал list_files, затем дважды вызвал read_file для Go-файлов, о которых я спросил.

Просто… как бы мы и сделали, верно? Вот что бы вы сделали, если бы я спросил, какую версию Go мы используем в проекте?

$ go run main.go

You: What go version are we using in this project?

tool: list_files({})
tool: read_file({"path":"go.mod"})

Claude: According to the go.mod file, this project is using Go version 1.24.1.

Claude смотрит на директорию, читает go.mod и даёт ответ.

Мы на ~190 строках кода сейчас. Пусть это осознается. Как осознается — добавим ещё один инструмент.

Пусть редактирует файлы — edit_file

Последний инструмент, который мы добавим — edit_file, позволяющий Claude редактировать файлы.

"Ого," думаете вы, "вот тут-то и начнётся магия, тут он достанет кролика из шляпы." Что ж, посмотрим.

Добавим определение инструмента edit_file:

// main.go var ={: "edit_file",:`Make edits to a text file. Replaces 'old_str' with 'new_str' in the given file. 'old_str' and 'new_str' MUST be different from each other. If the file specified with path doesn't exist, it will be created.`,:,:,}

Да, я знаю, о чём вы думаете: "замена строк для редактирования файлов?" Claude 3.7 любит заменять строки (экспериментирование — способ узнать, что они любят), поэтому мы реализуем edit_file, говоря Claude, что он может редактировать файлы, заменяя существующий текст новым.

Реализация EditFile проверяет входные параметры, читает файл (или создаёт, если его нет) и заменяет OldStr на NewStr. Затем записывает содержимое обратно на диск и возвращает "OK".

И… мы готовы. Готовы ли вы? Готовы запустить?

Скажем Claude создать FizzBuzz на JavaScript:

$ go run main.go

You: hey claude, create fizzbuzz.js that I can run with Nodejs

tool: list_files({})
tool: edit_file({"path":"fizzbuzz.js","old_str":"","new_str":"/**\n * FizzBuzz implementation in JavaScript..."})

Claude: I've created a fizzbuzz.js file. You can run it with: node fizzbuzz.js

Впечатляет, правда? И это самая базовая реализация edit_file — агента в целом — какую можно придумать.

Работает ли? Да:

$ node fizzbuzz.js
Running FizzBuzz:
1
2
Fizz
4
Buzz
...

Потрясающе. Но давайте попросим его отредактировать файл:

$ go run main.go

You: Please edit fizzbuzz.js so that it only prints until 15

tool: read_file({"path":"fizzbuzz.js"})
tool: edit_file({"path":"fizzbuzz.js","old_str":"fizzBuzz(100);","new_str":"fizzBuzz(15);"})
tool: edit_file({"path":"fizzbuzz.js","old_str":"Prints numbers from 1 to 100","new_str":"Prints numbers from 1 to 15"})

Claude: The changes have been made. The program will now print FizzBuzz from 1 to 15...

Он читает файл, редактирует вызов функции, и обновляет комментарий в начале.

Сделаем ещё одно и попросим:

Может, сложная задача. Посмотрим:

$ go run main.go

You: Create a congrats.js script that rot13-decodes the following string...

tool: edit_file({"path":"congrats.js","old_str":"","new_str":"function rot13Decode(encodedStr) {...}"})

Claude: I've created congrats.js. You can run it with: node congrats.js

Работает ли?

$ node congrats.js
Congratulations on building a code-editing agent!

Работает!

Разве это не потрясающе?

Если вы похожи на всех инженеров, с которыми я разговаривал последние несколько месяцев, скорее всего, читая это, вы ждали, когда из шляпы достанут кролика, когда я скажу "ну, на самом деле всё гораздо, гораздо сложнее". Но это не так.

По сути, это всё, что есть во внутреннем цикле агента для редактирования кода. Конечно, интеграция в редактор, настройка системного промпта, правильная обратная связь в нужное время, красивый UI вокруг, улучшенный инструментарий, поддержка нескольких агентов и так далее — мы всё это создали в Amp, но это не потребовало гениальности. Всё, что требовалось — практическая инженерия и кропотливая работа.

Эти модели сейчас невероятно мощные. 300 строк кода и три инструмента — и вот вы уже можете общаться с инопланетным интеллектом, который редактирует ваш код. Если вы думаете "ну, мы на самом деле не…" — попробуйте! Посмотрите, как далеко вы сможете зайти с этим. Держу пари, гораздо дальше, чем вы думаете.

Вот почему мы считаем, что всё меняется.


Подпишитесь на канал и каждый день читайте лучшие материалы про AI переведенные на русский!

Нашли интересную статью для перевода? Пришлите нашему боту: @ailongreadsbot

Report Page