Привет, я — Евгений Клецов, Go-разработчик в Cloud.ru. Если вы тоже Go-разработчик, то и вам, наверняка, приходила в голову мысль добавить в свой сервис «немного AI», но казалось, что это требует погружения в незнакомый мир Python и машинного обучения. Каждый день появляются новые AI-стартапы, да и существующие сервисы не отстают с внедрением искусственного интеллекта. Еще недавно это и правда было невозможным без глубоких знаний в области ML/AI, но сейчас всё меняется. Большие текстовые модели обзавелись удобным API для работы и фактически превратились в AI as a Service. Давайте на практике убедимся, что Go тоже прекрасно подходит для разработки подобных приложений на примере RAG.

План

Немного теории

Про Retrieval-Augmented Generation (RAG) уже написано много, и подробнее можно почитать в интернете, и даже ChatGPT GigaChat доступно объяснит, поэтому кратко. Эта технология позволяет нам «научить» модель новым знаниям. Модифицировать саму модель мы не можем, но можем добавить в промпт всю новую информацию! Современные модели обладают весьма внушительным контекстным окном в тысячи токенов, что позволяет вместить несколько документов в дополнение к запросу пользователя.

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

Давайте же посмотрим, как собрать такое приложение на Go. Нам понадобится:

  • Большая модель для текста

  • Маленькая для эмбеддинга

  • Векторная база данных

  • Синяя изолента Немного кода

Архитектура RAG-приложения

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

Прыгать с парашютом совсем не страшно. Открываешь дверь самолета — а там Google Maps. Вы ведь не боитесь Google Maps?
Прыгать с парашютом совсем не страшно. Открываешь дверь самолета — а там Google Maps. Вы ведь не боитесь Google Maps?

История диалога хранится в привычной базе данных, можем использовать как SQL, так и NoSQL базы, или комбинировать их для достижения целей по скорости и надежности.

База знаний хранится в векторной базе данных, взаимодействие с ней еще проще: даже не нужно писать запросы. Нужно только преобразовать текст от пользователя в вектор и передать его в метод поиска. Для такого преобразования используется API эмбеддера: текст на вход, набор чисел на выходе.

Собираем воедино все блоки текста и отправляем в API большой языковой модели, получаем ответ, сохраняем в историю и отдаем пользователю.

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

Выбор инструментов: фреймворки

Для написания приложения с RAG воспользуемся готовым фреймворком для AI-приложений. В принципе можно и с нуля написать, воспользоваться низкоуровневыми библиотеками для работы с базами данных, но это будет долго. Готовые фреймворки заметно упрощают взаимодействие с векторными хранилищами и LLM. Я нашел трех кандидатов для этого:

  • Langchain

  • Eino

  • GenKit

Ознакомился со всеми тремя и сделал для себя следующие выводы.

Genkit

Самый новый из троих, стабильная версия 1.0 вышла в сентябре 2025. Разрабатывается компанией Google.

Плюсы:

  • встроен��ые инструменты для отладки и разработки;

  • Google;

  • поддержка строгой типизации входных и выходных данных;

  • есть инструменты для мониторинга.

Минусы:

  • плохая поддержка Go, больше нацелен на Node.js, есть интеграции с фреймворками для фронтенда, часть фич отсутствует для Go;

  • выбор моделей ограничен;

  • тесно связан с облачными сервисами самого Google, нет возможности подключить свой Prometheus для сбора метрик, зато свой облачный мониторинг включается одной строкой;

  • векторные хранилища тоже поддерживаются в ограниченном количестве, часть их них также облачные от Google.

Eino

Весьма функциональный фреймворк от ByteDance — создателя TikTok, одного из крупнейших разработчиков ПО в Китае.

Плюсы:

  • серьезный разработчик;

  • есть свои плагины для IDE для облегчения разработки;

  • поддержка строгой типизации;

  • ориентирован на потоковую генерацию;

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

  • хорошо проработаны хуки жизненного цикла;

  • ориентирован на разработку агентов, оркестрация на основе графов.

Минусы:

  • Не очень большой выбор вариантов для моделей и векторных хранилищ, в основном китайские. Можно реализовать свои адаптеры, но хотелось бы из коробки.

  • Скромная документация при богатом функционале, мало примеров использования, часть документации в коде на китайском без английского перевода.

Langchain

Если не самый популярный, то точно один из таковых, изначально написан на Python, но имеется и его порт на Go.

Плюсы:

  • Очень известен в среде AI-разработчиков, можно проконсультироваться с ними в случае сложностей и вам будет всё понятно, так как в Go версии все те же концепции, что и для Python.

  • Самое большое количество поддерживаемых из коробки провайдеров LLM среди трех кандидатов. Есть локальный запуск в Ollama и из бинарного файла, OpenAI-совместимые API, Google, Mistral, HuggingFace и т. п.

  • Поддержка известных векторных хранилищ.

  • Механизмы кэширования ответов моделей, поддержания памяти чата, чтения и разбиения документов.

  • Богатая библиотека примеров использования в различных сценариях.

Минусы:

  • Отсутствие возможностей для строгой типизации и структуры входных и выходных данных.

  • Возможность реализовать одно и то же несколькими способами разной степени очевидности и понятности.

Genkit очевидно не подходит, по крайней мере на момент написания статьи, но выглядит перспективно. Eino кажется хорош, но сложноват для входа, и некоторых вещей не хватает. В итоге я остановился на Langchain, он оказался в целом понятным, все что нужно в нем уже есть из коробки, и нагуглить решение сложного вопроса будет гораздо проще (пусть даже и на Python). В дальнейших примерах кода буду использовать упомянутую библиотеку langchaingo.

Собираем приложение

Шаг 1: Общаемся с моделью

Для начала нам нужен минимальный чат с моделью, чтобы можно было ей задавать вопросы и читать ее ответы. Модели умеют отдавать результаты генерации в потоковом режиме по мере появления, во всех современных приложениях с AI мы это можем наблюдать. Выглядит красиво, давайте и мы тоже сделаем. Полный пример кода приложения можно посмотреть по ссылке.
В качестве источника знаний я решил взять текст романа А.С. Пушкина «Евгений Онегин».

Я буду использовать llama3, запущенную локально, поскольку она довольно простенькая, а серьезные модели и без нашего RAG хорошо знакомы с содержанием книги. Так можно будет увидеть разницу с RAG и без него.

package main

func main() {  
    ctx := context.Background()  
    slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))  
  
    llm := initOllamaModel()  
    srv := server.NewServer(chat.NewService(llm))
    if err := srv.Run(); err != nil {  
       slog.Error("failed to run server", "error", err)  
       os.Exit(1)  
    }  
}

func initOllamaModel() *ollama.LLM {  
    modelName := "llama3.2:3b"
  
    llm, err := ollama.New(ollama.WithModel(modelName))  
    if err != nil {  
       slog.Error("failed to create ollama llm", "error", err)  
       os.Exit(1)  
    }  
    llm.CallbacksHandler = metrics.NewCallbackHandler(modelName)  
  
    return llm  
}

Сервер будет обрабатывать сообщения чата отдельной ручкой и отдавать результат с помощью Server-Sent Events. Это проще, чем вебсокеты, особенно в части масштабирования и балансировки нагрузки. Так мы реализуем появление текста по мере генерации.

Также добавим главную страницу с простеньким фронтендом для отображения чата и отправки сообщений.

package server

type ChatService interface {  
    Chat(ctx context.Context, in chat.Message) <-chan string  
}  
  
type Server struct {  
    chat ChatService  
}
  
func (s *Server) Run() error {  
    r := chi.NewRouter()  
    r.Use(middleware.Recoverer)  
  
    r.Get("/", s.handleRoot)
    r.Post("/chat/{chatID}", s.handleChat)  
  
    return http.ListenAndServe(":8080", r)  
}

func (s *Server) handleChat(w http.ResponseWriter, req *http.Request) {  
    chatID, err := uuid.Parse(chi.URLParam(req, "chatID"))  
    if err != nil || chatID == uuid.Nil {  
       slog.Error("invalid chat ChatID", "error", err, "chatID", chi.URLParam(req, "chatID"))  
       http.Error(w, "Invalid chat ChatID", http.StatusBadRequest)  
       return  
    }  
  
    w.Header().Set("Content-Type", "text/event-stream")  
    w.Header().Set("Cache-Control", "no-cache")  
    w.Header().Set("Connection", "keep-alive")  
  
    flusher, ok := w.(http.Flusher)  
    if !ok {  
       http.Error(w, "Streaming unsupported", http.StatusInternalServerError)  
       return  
    }  
  
    input, err := io.ReadAll(req.Body)  
    if err != nil {  
       http.Error(w, "Error reading request body", http.StatusBadRequest)  
    }  
    defer req.Body.Close()  
  
    ch := s.chat.Chat(req.Context(), chat.Message{  
       ChatID: chatID,  
       Text:   string(input),  
    })  
    for msg := range ch {  
       _, _ = fmt.Fprintf(w, "data: %s\n\n", msg)  
       flusher.Flush()
    }  
}  
  
func (s *Server) handleRoot(w http.ResponseWriter, _ *http.Request) {  
    w.Header().Set("Content-Type", "text/html; charset=utf-8")  
    data, err := embedFiles.ReadFile("html/index.html")  
    if err != nil {  
       slog.Error("error reading file", "error", err)  
       http.Error(w, "internal server error", http.StatusInternalServerError)  
    } else {  
       _, _ = w.Write(data)  
    }  
}

И непосредственно реализация чата с моделью и потоковым ответом.

type Service struct {  
    llm     llms.Model
}  
  
func NewService(llm llms.Model) *Service {  
    return &Service{llm: llm}  
}  
  
func (s *Service) Chat(ctx context.Context, msg Message) <-chan string {  
    res := make(chan string)  
  
    content := []llms.MessageContent{  
       llms.TextParts(llms.ChatMessageTypeSystem, "Ты специалист по творчеству Александра Сергеевича Пушкина. Тебе нужно ответить на вопрос."),  
    }
    content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, msg.Text))  
  
    go s.generateContent(ctx, msg.ChatID, content, res)  
  
    return res  
}  
  
func (s *Service) generateContent(ctx context.Context, chatID uuid.UUID, content []llms.MessageContent, out chan<- string) {  
    defer close(out)  
  
    completion, err := s.llm.GenerateContent(ctx, content,  
       llms.WithTemperature(0.5),  
       llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {  
          if ctx.Err() != nil {  
             return fmt.Errorf("error: %w", ctx.Err())  
          }  
  
          out <- fmt.Sprintf("%s", chunk)  
          return nil  
       }),  
    )  
  
    if err != nil {  
       slog.Error("failed to generate content", "error", err)  
       out <- "Failed to generate content"  
  
       return  
    }
    slog.Debug("completion result", "result", *completion)
}

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

llama3 не очень хорошо знакома с творчеством Александра Сергеевича, но она старалась
llama3 не очень хорошо знакома с творчеством Александра Сергеевича, но она старалась

Изначально я использовал другую модель, и она знала правильный ответ ещё до внедрения цитат из книги. Перебрал несколько разных, пока не нашёл ту, которая не знает, как же звали Ленского.

Шаг 2: Добавляем «мозг»

Настала очередь базы знаний. В langchaingo есть несколько адаптеров для популярных векторных баз данных, я буду использовать Qdrant. Почему именно его? Он довольно популярный, часто попадался мне на глаза, я захотел попробовать его. А еще его можно запустить в Docker без параметров, и в комплекте есть вебадминка, для тестовой среды очень удобно. Для эмбеддинга буду использовать модель EmbeddingGemma также локально.

func initOllamaEmbedder() embeddings.Embedder {  
    llm, err := ollama.New(ollama.WithModel("embeddinggemma:300m"))  
    if err != nil {  
       slog.Error("failed to create llm", "error", err)  
       os.Exit(1)  
    }  
    embedder, err := embeddings.NewEmbedder(llm)  
    if err != nil {  
       slog.Error("failed to create embedder", "error", err)  
       os.Exit(1)  
    }  
    return embedder  
}  
  
func initQdrantStore(embedder embeddings.Embedder) vectorstores.VectorStore {  
    url, err := url.Parse("http://localhost:6333/")  
    if err != nil {  
       slog.Error("failed to parse url", "error", err)  
       os.Exit(1)  
    }  
  
    store, err := qdrant.New(  
       qdrant.WithURL(*url),  
       qdrant.WithCollectionName("Onegin"),  
       qdrant.WithEmbedder(embedder),  
    )  
    if err != nil {  
       slog.Error("failed to create qdrant store", "error", err)  
       os.Exit(1)  
    }  
  
    return store  
}

В коде можно заметить параметр CollectionName, здесь необходимо указать имя коллекции, которую мы создадим в Qdrant. Запустим его в Docker и настроим.

docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

Открываем http://localhost:6333/dashboard и сразу попадаем на страницу Коллекций. Нажимаем Create Collection, вводим имя коллекции, которое прописали в коде. Далее отвечаем на пару вопросов, выбирая подходящие опции. Я выбираю:

  • Global search (у нас один набор данных для всех пользователей).

  • Simple single embedding (сложные варианты поиска нам пока не нужны).

Последняя опция очень важная, здесь у нас задается размерность вектора. Он должен совпадать с аналогичным параметром модели, иначе мы не сможем ничего сохранить. Для EmbeddingGemma он равен 768, для вашего варианта модели должен быть указан в документации к ней. Устанавливаем его, метрика по умолчанию косинус, оставляем её. В последнем шаге нам предлагают добавить индексы по полям, но мы не будем добавлять к нашим векторам дополнительные метаданные, так что пропускаем этот шаг и сохраняем коллекцию.

Текст Онегина находим в интернете без регистрации и смс, общественное достояние как-никак. Имеющийся текст необходимо разбить на куски, которые мы векторизируем. Они должны быть не слишком большими, чтобы не перегружать модель, и не слишком маленькими, чтобы в них была полезная информация и потенциальный ответ на вопрос пользователя. Воспользуемся модулем textsplitter нашей библиотеки langchaingo, разбивать будем по 400 токенов с перекрытием в 100 токенов. У нас нет цели точно затюнить поиск, а этого хватит для теста.

func main() {  
    ctx := context.Background()  
  
    slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))  
  
    embedder := initOllamaEmbedder()  
    store := initQdrantStore(embedder)  
  
    splitter := textsplitter.NewTokenSplitter(  
       textsplitter.WithChunkSize(400),  
       textsplitter.WithChunkOverlap(100),  
    )  
  
    slog.Info("splitter created")  
  
    file, err := os.Open("evgenii-onegin.txt")  
    if err != nil {  
       slog.Error("failed to open file", "error", err)  
       os.Exit(1)  
    }  
    defer file.Close()  
  
    loader := documentloaders.NewText(file)  
    docs, err := loader.LoadAndSplit(ctx, splitter)  
    if err != nil {  
       slog.Error("failed to load and split documents", "error", err)  
       os.Exit(1)  
    }  
    slog.Info("documents loaded and split", "count", len(docs))  
  
    ids, err := store.AddDocuments(ctx, docs)  
    if err != nil {  
       slog.Error("failed to add documents to store", "error", err)  
       os.Exit(1)  
    }  
    slog.Info("documents added to store", "count", len(ids))  
}

Запускаем нашу простенькую программу, через несколько минут наблюдаем в админке заполненную коллекцию. Добавляем к нашему приложению поиск в базе.

func (s *Service) Chat(ctx context.Context, msg Message) <-chan string {  
    res := make(chan string)
  
    docs, err := s.store.SimilaritySearch(ctx, msg.Text, maxResults)  
    if err != nil {  
       slog.Error("failed to search docs from vector store", "error", err)  
    }  
    slog.Debug("found docs", "docs", docs)  
  
    content := []llms.MessageContent{  
       llms.TextParts(llms.ChatMessageTypeSystem, "Ты специалист по творчеству Александра Сергеевича Пушкина. Тебе нужно ответить на вопрос."),  
    }
  
    if len(docs) > 0 {  
       content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, "Ты знаешь следующие документы:"))  
    }  
    for _, doc := range docs {  
       content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, doc.PageContent))  
    }  
    content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, msg.Text))  
  
    go s.generateContent(ctx, msg.ChatID, content, res)  
  
    return res  
}

Здесь я добавил также дополнительные инструкции для модели: системный промпт для обозначения задачи и пояснение, что вообще за текст тут добавлен. Перезапускаем приложение и повторяем наш вопрос модели.

Модель поняла, что в её распоряжении несколько отрывков книги, и нашла в них ответ.
Модель поняла, что в её распоряжении несколько отрывков книги, и нашла в них ответ.

Шаг 3: Наводим порядок в разговоре

Теперь давайте добавим сохранение истории сообщений и опробуем на более серьезной модели. Здесь также нет ничего сложного, нам нужно сохранить в базу запрос пользователя и ответ модели, а в следующем обращении загрузить предыдущие вопросы и ответы. Чтобы не перегружать модель и сэкономить платные токены, мы можем ограничить историю по количеству сообщений или токенов, или даже делать суммаризацию переписки с помощью модели. Хранить историю можем в любой базе данных, в своём примере я использую PostgreSQL.

func (h *historyStorage) Save(ctx context.Context, id uuid.UUID, role llms.ChatMessageType, content string) error {  
    ctx, cancel := context.WithTimeout(ctx, pgQueryTimeout)  
    defer cancel()  
  
    query := `INSERT INTO history (chat_id, role, message) VALUES ($1, $2, $3)`  
    if _, err := h.db.Exec(ctx, query, id, role, content); err != nil {  
       return fmt.Errorf("insert into history: %w", err)  
    }  
  
    return nil  
}  
  
func (h *historyStorage) Load(ctx context.Context, id uuid.UUID) ([]llms.MessageContent, error) {  
    ctx, cancel := context.WithTimeout(ctx, pgQueryTimeout)  
    defer cancel()  
  
    query := `SELECT role, message FROM history WHERE chat_id = $1 ORDER BY created_at DESC LIMIT 4`  
  
    rows, err := h.db.Query(ctx, query, id)  
    if err != nil {  
       return nil, fmt.Errorf("query history: %w", err)  
    }  
  
    var res []llms.MessageContent  
    for rows.Next() {  
       if err = rows.Err(); err != nil {  
          return nil, fmt.Errorf("history row error: %w", err)  
       }  
       var (  
          role    string  
          content string  
       )  
  
       if err = rows.Scan(&role, &content); err != nil {  
          return nil, fmt.Errorf("scan history row: %w", err)  
       }  
  
       res = append(res, llms.TextParts(llms.ChatMessageType(role), content))  
    }  
    slices.Reverse(res)  
  
    return res, nil  
}
func (s *Service) Chat(ctx context.Context, msg Message) <-chan string {  
    res := make(chan string)  
  
    history, err := s.history.Load(ctx, msg.ChatID)  
    if err != nil {  
       slog.Error("failed to load history", "error", err)  
    }  
  
    err = s.history.Save(ctx, msg.ChatID, llms.ChatMessageTypeHuman, msg.Text)  
    if err != nil {  
       slog.Error("failed to save user message to history", "error", err)  
    }  
  
    docs, err := s.store.Search(ctx, msg.Text)  
    if err != nil {  
       slog.Error("failed to search docs from vector store", "error", err)  
    }  
    slog.Debug("found docs", "docs", docs)  
  
    content := []llms.MessageContent{  
       llms.TextParts(llms.ChatMessageTypeSystem, "Ты специалист по творчеству Александра Сергеевича Пушкина. Тебе нужно ответить на вопрос."),  
    }  
    if len(history) > 0 {  
       for _, historyMsg := range history {  
          content = append(content, historyMsg)  
       }  
    }  
  
    if len(docs) > 0 {  
       content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, "Ты знаешь следующие документы:"))  
    }  
    for _, doc := range docs {  
       content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, doc.PageContent))  
    }  
    content = append(content, llms.TextParts(llms.ChatMessageTypeHuman, msg.Text))  
  
    go s.generateContent(ctx, msg.ChatID, content, res)  
  
    return res  
}

Здесь я сделал простейший вариант ограничения истории — добавил лимит в запрос. Обратите внимание, мы не сохраняем в историю результаты поиска по базе знаний — они нам там ни к чему. Модель даст ответ с их учетом, и его мы запишем в историю.

func initOpenAIModel() *openai.LLM {  
    modelName := "GigaChat/GigaChat-2-Max"  
  
    client := &http.Client{  
       Transport: initModelAPITransport(modelName),  
       Timeout:   300 * time.Second,  
    }  
  
    cbHandler := metrics.NewCallbackHandler(modelName)  
  
    // Token set in OPENAI_API_KEY env  
    llm, err := openai.New(  
       openai.WithModel(modelName),  
       openai.WithBaseURL("https://foundation-models.api.cloud.ru/v1"),  
       openai.WithHTTPClient(client),  
       openai.WithCallback(cbHandler),  
    )  
    if err != nil {  
       slog.Error("failed to create openai llm", "error", err)  
       os.Exit(1)  
    }  
    return llm  
}

Спросим что-нибудь посложнее и посмотрим на нашу историю.

Куда двигаться дальше: улучшаем PoC

Приведенный пример приложения является лишь Proof-of-Concept, что подобные приложения можно писать на Go. Для полноценного внедрения в продакшн его ещё потребуется доработать.

Потребуются инструменты наблюдения: логи, метрики, трейсинг. Здесь в целом все как в обычном бэкенде: отслеживаем обращения к базам данных, сторонним сервисам (моделям), но добавляется один нюанс. Будет не лишним добавить метрики расхода токенов, поскольку они тарифицируются в платных моделях. Для этих целей в ответах моделей в langchaingo есть поле GenerationInfo, из которого можно получить информацию о количестве входных и выходных токенов, подсчитанных самой моделью. Также библиотека предоставляет возможность задать обработчики для событий жизненного цикла.

func (c CallbackHandler) HandleLLMGenerateContentEnd(_ context.Context, res *llms.ContentResponse) {  
    if len(res.Choices) == 0 {  
       return  
    }  
  
    if input, ok := res.Choices[0].GenerationInfo["PromptTokens"].(int); ok {  
       IncInputTokens(c.model, input)  
    }  
    if output, ok := res.Choices[0].GenerationInfo["CompletionTokens"].(int); ok {  
       IncOutputTokens(c.model, output)  
    }  
}

Для улучшения поиска по базе данных можно задействовать дополнительные поля с метаданными, совместить векторный и полнотекстовый поиск, а также добавить ранжирование результатов с помощью Reranker-моделей (cross-encoder).

Стоит уделить внимание настройке таймаутов для обращений к моделям. Модель может генерировать ответ очень долго, особенно если используется размышление (thinking) или объяснение (reasoning), да и вопрос может быть нетривиальным, и тогда время до окончания генерации может исчисляться минутами. Можно ограничить количество токенов в генерации, но это не всегда приемлемо. Необходимо настроить их таким образом, чтобы генерация не прерывалась на середине, но также и не блокировать сервис в случае сетевых проблем.

Заключение

Я сам думал, что внедрить AI в приложение на Go будет непросто. На практике же оказалось, что RAG, да и не только он — это всего лишь архитектурные паттерны, а не эксклюзивная технология, и он может быть реализован на любом языке программирования. В свою очередь, Go это отличный выбор для реализации AI-приложений:

  • Библиотеки и фреймворки активно развиваются.

  • Высокая производительность и поддержка многопоточности открывают возможности для создания высоконагруженных приложений.

  • Легкая интеграция в существующие системы на Go.

  • Простота развертывания и эксплуатации (DevOps-friendly).

Ну а если хочется научить свою LLM-ку чему-то новому, но возиться с языками программирования вообще нет желания, у моих коллег из Evolution есть сервис Managed RAG. С документацией и примерами.

А вы уже пробовали писать AI приложения на Go? Расскажите, с какими проблемами столкнулись в процессе и какие инструменты показали себя лучше всего?

Комментарии (0)