Agency: The Go Way to AI. Часть 1

1285868aba5e515a8be2fe575831c712

Рост генеративного ИИ, API OpenAI и локальные LLM, влияют на то, как мы разрабатываем приложения. У разработчиков на Python и JavaScript есть много инструментов, особо популярен LangChain. Однако, у гошников вариантов меньше. LangChainGo, порт оригинального LangChain, пытается маппить питонячие концепции на го, получается не слишком идеоматично. К тому же, есть ощущение, что LangChain сам по себе переусложнен.

Из-за потребности в простом, но мощном инструменте для Go, мы разработали Agency. Эта простая гошная либа с маленьким ядром, которую мы постарались тщательно спроектировать.

Пример — Продолжение текста

Начнем с самого простого примера — продолжить текст. Для простоты мы разберем его по частям, а затем объединим в единый, целостный кусок.

Для начала создадим «провайдер»:

provider := openai.New(
    openai.Params{Key: "YOUR_OPENAI_API_KEY"},
)

Провайдер представляет собой набор операций, реализованных каким-либо внешним сервисом. В данном случае это OpenAI.

Обратите внимание, мы передаем структуру Params. В случае с локальной LLM (должна иметь совместимый API с OpenAI) мы можем передать openai.Params{BaseURL: "YOUR_SERVICE_URL_HERE"}.

Теперь, когда у нас есть провайдер, пора создать операцию:

operation := provider.
    TextToText(openai.TextToTextParams{Model: "gpt-3.5-turbo"}).
    SetPrompt("You are a helpful assistant that translates English to French")

Метод TextToText, который мы тут вызываем, является конструктором операций — функцией, которая принимает некоторые параметры и возвращает операцию, значение типа agency.Operation.

Этот конструктор операций имеет следующую сигнатуру:

func (p Provider) TextToText(params TextToTextParams) *agency.Operation

Эта структура TextToTextParams специфична для каждого конкретного конструктора операций. Она зависит от того, какой внешний сервис использует провайдер и какую функциональность (модальность) он реализует. Почти любой конструктор позволяет указать Model, но позже мы увидим и различия.

Приведу пример: провайдер Anthropic может иметь другие параметры, нежели OpenAI, а конструктор операций openai.SpeechToText будет иметь параметры отличные от openai.TextToText(из-за использования whisper вместо GPT).

Далее, видите строку SetPrompt("You are a helpful assistant that translates English to French")? Так мы конфигурируем операции. В данном случае мы настраиваем используемый промпт.

Окей, похоже, пришло время поговорить о том, чем являются операции на самом деле. Давайте заглянем в исходный код:

// Operation is basic building block.
type Operation struct {
    handler OperationHandler
    config  *OperationConfig
}

// OperationHandler is a function that implements the actual logic.
// It could be thought of as an interface that providers must implement.
type OperationHandler func(context.Context, Message, *OperationConfig) (Message, error)

// OperationConfig represents abstract operation configuration.
// It contains fields for all possible modalities but nothing specific to concrete model implementations.
type OperationConfig struct {
    Prompt   string
    Messages []Message
}

На момент написания статьи библиотека имеет версию v0.1.0 и находится в активной разработке, поэтому детали реализации могут измениться, но основная идея должна остаться — операция состоит из обработчика и конфига.

Теперь давайте посмотрим, что делает метод SetPrompt("..."):

func (p *Operation) SetPrompt(prompt string, args ...any) *Operation {
    p.config.Prompt = fmt.Sprintf(prompt, args...)
    return p
}

Вот и все. Он просто сетит промпт в конфиге и реализует шаблонизацию через fmt.Sprintf.

Вы еще тут? Самое страшное позади!

На самом деле, наша операция готова к использованию! Но нам нужно что-то в качестве входных данных для нашей операции. Давайте-ка создадим сообщение:

input := agency.UserMessage("I love programming.")

Раз уж мы не боимся заглядывать в исходники, то вот вам реализация UserMessage

func UserMessage(content string, args ...any) Message {
    s := fmt.Sprintf(content, args...)
    return Message{Role: UserRole, Content: []byte(s)}
}

Это просто маленький хелпер для Message. Message это абстрактное сообщение, представляющее любое возможное сообщение, с которым работают операции:

type Message struct {
    Role    Role
    Content []byte
}

Мы уже видели это сообщение ранее, в определении операции:

func(context.Context, Message, *OperationConfig) (Message, error)

То есть операция — это функция, которая принимает сообщение в качестве входных данных и возвращает сообщение в качестве выходных данных.

Наконец, осталось выполнить нашу операцию.

output, err := operation.Execute(context.Background(), input)
if err != nil {
    panic(err)
}

Соберем все вместе и получим вот такой код:

provider := openai.New(
    openai.Params{Key: os.Getenv("OPENAI_API_KEY")},
)

operation := provider.
    TextToText(openai.TextToTextParams{Model: "gpt-3.5-turbo"}).
    SetPrompt("You are a helpful assistant that translates English to French")

input := agency.UserMessage("I love programming.")
output, err := operation.Execute(context.Background(), input)
if err != nil {
    panic(err)
}

fmt.Println(string(output.Content))

Не забудьте вставить свой ключ API OpenAI. Вот мой результат:

J'adore la programmation.

Похоже, что это работает! Вы, конечно, можете переписать это более компактно, если хотите:

openai.New(openai.Params{Key: "YOUR_OPENAI_API_KEY"}).
    TextToText(openai.TextToTextParams{Model: "gpt-3.5-turbo"}).
    SetPrompt("You are a helpful assistant that translates English to French").
    Execute(context.Background(), agency.UserMessage("I love programming."))

Вот и все! Большое спасибо за чтение. Если найдете какие-то ошибки, пожалуйста, оставьте комментарий. Ссылка на этот и многие другие рабочие примеры, а также ссылка на саму библиотеку будут ниже.

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

В следующей серии мы рассмотрим такие темы, как:

  • Как создать чат в 40 строк кода

  • Как комбинировать операции в цепочки для последовательного выполнения

  • Как создавать свои операции

  • Как использовать интерсепторы для наблюдения за выполнением операций

  • Как использовать шаблонизацию запросов

  • Как использовать различные модальности (речь, изображения и т.д.)

  • Как реализовать RAG (используя векторные базы данных)

  • И многое, многое другое!

Ссылки:

© Habrahabr.ru