Хотя экосистема LLM в основном ориентирована на Python, мы нашли Go исключительно подходящим для производственных развертываний. Наша инфраструктура на базе Go Хотя экосистема LLM в основном ориентирована на Python, мы нашли Go исключительно подходящим для производственных развертываний. Наша инфраструктура на базе Go

[Перевод] Масштабирование LLM с помощью Golang: как мы обслуживаем миллионы запросов LLM

Хотя экосистема LLM в основном ориентирована на Python, мы нашли Go исключительно подходящим для производственных развертываний. Наша инфраструктура на базе Go обрабатывает миллионы ежемесячных запросов LLM с минимальной настройкой производительности. Помимо хорошо документированных преимуществ Go (см. отличное изложение Роба Пайка о преимуществах Go), три возможности оказались особенно ценными для нагрузок LLM: статическая проверка типов для обработки выходных данных модели, горутины для управления параллельными вызовами API и интерфейсы для построения составных конвейеров ответов. Вот как мы реализовали каждую из них в нашем производственном стеке.

Типобезопасность и структурированные выходы

Одной из основных проблем с LLM является обработка их неструктурированных выходных данных. Поддержка структурированных выходных данных OpenAI стала значительным продвижением для нас, и система типов Go делает её особенно элегантной для реализации. Вместо написания отдельных определений схемы, мы можем использовать теги структур Go и рефлексию для генерации четко определенных схем. Вот пример, где мы автоматически конвертируем SupportResponse в формат JSON-схемы OpenAI с использованием библиотеки go-openai:

import ( "github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai/jsonschema" ) type SupportResponse struct { Answer string `json:"answer"` RelatedDocs []string `json:"related_docs"` } func GetSupportResponse(messages []openai.ChatCompletionMessage) (*SupportResponse, error) { var supportResponse SupportResponse schema, err := jsonschema.GenerateSchemaForType(supportResponse) if err != nil { return nil, err } resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Messages: messages, ResponseFormat: &openai.ChatCompletionResponseFormat{ Type: openai.ChatCompletionResponseFormatTypeJSONSchema, JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{ Name: "support_response", Schema: schema, Strict: true, }, }, }) if err != nil { return nil, err } err = schema.Unmarshal(resp.Choices[0].Message.Content, &supportResponse) if err != nil { return nil, err } return &supportResponse, nil }

Вышеприведенный код предоставит нам Answer и RelatedDocs, заполненные непосредственно из вызова LLM. Теперь SupportResponse можно легко передать на наш фронтенд или сохранить в нашей базе данных.

Обратите внимание, что поскольку у Golang есть встроенная система типов, вам не нужно тратить дополнительное время на определение структуры объекта (как вы бы сделали в Python) - она уже доступна через рефлексию, и вы можете потратить больше времени на промптинг, входы и выходы LLM.

Параллельная обработка и задержка

Приложения LLM часто требуют параллельных вызовов API и сложной оркестрации. Горутины и каналы Go делают это удивительно просто.

Например, предположим, мы запускаем конвейер Retrieval Augmented Generation (RAG) и хотим выполнить гибридный поиск по трем различным поисковым бэкендам (см. нашу статью о Лучших результатах RAG с помощью Reciprocal Rank Fusion и гибридного поиска). Запуск этих поисков последовательно добавил бы их индивидуальные задержки, что привело бы к более медленным ответам. С Go мы можем относительно легко распараллелить поиски по нескольким бэкендам:

func ParallelSearch(query string) []SearchResult { ctx, cancel := context.WithTimeout(context.Background(), 750*time.Millisecond) defer cancel() resultsChan := make(chan []SearchResult, len(backends)) var wg sync.WaitGroup for _, backend := range backends { wg.Add(1) go func(backend func(string) ([]SearchResult, error)) { defer wg.Done() results, err := backend(query) if err != nil { return } select { case resultsChan <- results: case <-ctx.Done(): } }(backend) } wg.Wait() close(resultsChan) var combined []SearchResult for res := range resultsChan { combined = append(combined, res...) } return combined }

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

Конвейер обработки ответов

Выходные данные LLM часто нуждаются в нескольких преобразованиях перед тем, как они готовы для конечных пользователей. Например, если вы используете провайдера LLM с отличными способностями к рассуждению, но еще не имеющего структурированных выходных данных (например, Claude 3.5 Sonnet), вам, вероятно, захочется структурировать выходные данные в вашем промпте и разобрать выходные данные перед передачей конечному пользователю.

Мы построили составной конвейер, который делает эти преобразования как поддерживаемыми, так и тестируемыми:

type ResponseCleaner interface { Clean(context.Context, string) (string, []ResponseDetails, error) } type ResponseDetails struct { DetailType string `json:"detail_type"` Content interface{} `json:"content"` }

Каждый очиститель является дискретной единицей, которая обрабатывает одно конкретное преобразование. Это разделение ответственности делает тестирование простым и позволяет нам модифицировать индивидуальные преобразования без касания остальной части конвейера. Вот как мы обрабатываем цитирование источников:

type CitedSourceCleaner struct{} func (c CitedSourceCleaner) Clean(ctx context.Context, message string) (string, []ResponseDetails, error) { sourceRegex := regexp.MustCompile(`\[(Source|Ref):\s*([^\]]+)\]`) var citations []ResponseDetails matches := sourceRegex.FindAllStringSubmatch(message, -1) for i, match := range matches { citations = append(citations, ResponseDetails{ DetailType: "citation", Content: map[string]interface{}{ "number": i + 1, "source": match[2], }, }) message = strings.Replace(message, match[0], fmt.Sprintf("[%d]", i+1), 1) } return message, citations, nil }

Используя вышеприведенный очиститель, когда LLM отвечает:

Очиститель проанализирует источники и передаст их на фронтенд как детали ответа. Он также преобразует сырые выходные данные LLM в:

Дополнение Python

В то время как Go питает нашу производственную инфраструктуру, Python остается необходимым для экспериментов с ML и быстрого прототипирования. Экосистема Python превосходна в задачах вроде:

  • Кластеризация тикетов поддержки с scikit-learn (например, с AgglomerativeClustering)

  • Тонкая настройка LLM с transformers (особенно открытыми исходными моделями вроде Llama), особенно для кастомизации моделей на наших данных поддержки

  • Прототипирование RAG с sentence-transformers для тестирования моделей эмбеддингов и стратегий чанкинга

Эти задачи были бы значительно более сложными в Go, где ML-библиотеки либо не существуют, либо гораздо менее зрелые.

Чтобы преодолеть разрыв между Go и Python, мы поддерживаем легковесный Python-сервис, который наша Go-инфраструктура вызывает. Этот сервис обрабатывает вычислительно интенсивные ML-задачи (такие как генерация эмбеддингов или кластеризация), сохраняя нашу основную инфраструктуру на Go. На практике мы часто прототипируем фичи полностью на Python, затем постепенно портируем критически важные для производительности компоненты на Go после их доказательства. Этот подход позволяет нам поставлять улучшения инкрементально без ожидания полной Go-реализации.

Заключение

Сильные стороны Go в типобезопасности, параллелизме и построении интерфейсов сделали его отличным выбором для нашей LLM-инфраструктуры. В то время как Python остается нашим языком выбора для ML-разработки, Go предоставляет производительность и надежность, необходимые нам в продакшене. Комбинация обоих языков позволяет нам двигаться быстро, сохраняя надежную, масштабируемую систему.

Источник

Возможности рынка
Логотип Large Language Model
Large Language Model Курс (LLM)
$0.0003267
$0.0003267$0.0003267
-1.98%
USD
График цены Large Language Model (LLM) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу [email protected] для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.