Рост генеративного ИИ, 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 (используя векторные базы данных)
И многое, многое другое!
Ссылки: