Генерация с расширенным поиском (RAG)

Firebase Genkit предоставляет абстракции, которые помогают создавать потоки генерации с расширенным поиском (RAG), а также плагины, обеспечивающие интеграцию со связанными инструментами.

Что такое РАГ?

Генерация с расширенным поиском — это метод, используемый для включения внешних источников информации в ответы LLM. Важно уметь это делать, поскольку, хотя LLM обычно обучают широкому кругу материалов, практическое использование LLM часто требует знаний в конкретной предметной области (например, вы можете использовать LLM, чтобы отвечать на вопросы клиентов о деятельности вашей компании). продукты).

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

Напротив, RAG работает путем включения внешних источников данных в подсказку во время ее передачи в модель. Например, вы можете представить себе подсказку: «Какие отношения у Барта с Лизой?» может быть расширено («дополнено») путем добавления некоторой соответствующей информации, в результате чего появится подсказка: «Детей Гомера и Мардж зовут Барт, Лиза и Мэгги. Каковы отношения Барта с Лизой?»

Этот подход имеет ряд преимуществ:

  • Это может быть более экономически эффективным, поскольку вам не придется переобучать модель.
  • Вы можете постоянно обновлять свой источник данных, и LLM сможет немедленно использовать обновленную информацию.
  • Теперь у вас есть возможность цитировать ссылки в ответах вашего LLM.

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

RAG — это очень обширная область, и существует множество различных методов, используемых для достижения RAG наилучшего качества. Базовая структура Genkit предлагает две основные абстракции, которые помогут вам реализовать RAG:

  • Индексаторы: добавляйте документы в «индекс».
  • Embedders: преобразует документы в векторное представление.
  • Retrievers: извлекают документы из «индекса» по запросу.

Эти определения намеренно являются широкими, поскольку Genkit не имеет мнения о том, что такое «индекс» или как именно из него извлекаются документы. Genkit предоставляет только формат Document , а все остальное определяется поставщиком реализации средства извлечения или индексатора.

Индексаторы

Индекс отвечает за отслеживание ваших документов таким образом, чтобы вы могли быстро получить соответствующие документы по конкретному запросу. Чаще всего это достигается с помощью базы данных векторов, которая индексирует ваши документы с использованием многомерных векторов, называемых встраиваниями. Встраивание текста (непрозрачно) представляет концепции, выраженные отрывком текста; они генерируются с использованием моделей машинного обучения специального назначения. Индексируя текст с помощью его внедрения, векторная база данных способна группировать концептуально связанный текст и извлекать документы, связанные с новой текстовой строкой (запросом).

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

  1. Разделите большие документы на более мелкие, чтобы для дополнения подсказок использовались только соответствующие части – «разбиение на части». Это необходимо, поскольку многие LLM имеют ограниченное контекстное окно, что делает непрактичным включение целых документов в подсказку.

    Genkit не предоставляет встроенных библиотек фрагментирования; однако существуют библиотеки с открытым исходным кодом, совместимые с Genkit.

  2. Сгенерируйте вложения для каждого фрагмента. В зависимости от используемой вами базы данных вы можете сделать это явно с помощью модели генерации внедрения или использовать генератор внедрения, предоставляемый базой данных.

  3. Добавьте текстовый фрагмент и его индекс в базу данных.

Вы можете запускать поток приема нечасто или только один раз, если работаете со стабильным источником данных. С другой стороны, если вы работаете с данными, которые часто меняются, вы можете постоянно запускать поток приема (например, в триггере Cloud Firestore при каждом обновлении документа).

Встраивающие устройства

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

Ретриверы

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

Чтобы создать ретривер, вы можете использовать одну из предоставленных реализаций или создать свою собственную.

Поддерживаемые индексаторы, ретриверы и средства внедрения

Genkit обеспечивает поддержку индексаторов и ретриверов через свою систему плагинов. Официально поддерживаются следующие плагины:

Кроме того, Genkit поддерживает следующие векторные хранилища посредством предопределенных шаблонов кода, которые вы можете настроить в соответствии с конфигурацией и схемой вашей базы данных:

Поддержка модели внедрения обеспечивается посредством следующих плагинов:

Плагин Модели
Генеративный искусственный интеллект Google Встраивание текста Gecko
Google Вертекс ИИ Встраивание текста Gecko

Определение потока RAG

В следующих примерах показано, как можно вставить коллекцию PDF-документов меню ресторана в векторную базу данных и извлечь их для использования в потоке, который определяет, какие продукты питания доступны.

Установить зависимости

В этом примере мы будем использовать библиотеку textsplitter от langchaingo и библиотеку анализа PDF ledongthuc/pdf :

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

Определить индексатор

В следующем примере показано, как создать индексатор для приема коллекции PDF-документов и сохранения ее в локальной базе данных векторов.

Он использует локальный инструмент поиска сходства векторов на основе файлов, который Genkit предоставляет «из коробки» для простого тестирования и прототипирования ( не используйте в производстве ).

Создайте индексатор

// Import Genkit's file-based vector retriever, (Don't use in production.)
import "github.com/firebase/genkit/go/plugins/localvec"

// Vertex AI provides the text-embedding-004 embedder model.
import "github.com/firebase/genkit/go/plugins/vertexai"
ctx := context.Background()

err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
	log.Fatal(err)
}
err = localvec.Init()
if err != nil {
	log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
	"menuQA",
	localvec.Config{
		Embedder: vertexai.Embedder("text-embedding-004"),
	},
)
if err != nil {
	log.Fatal(err)
}

Создать конфигурацию фрагментации

В этом примере используется библиотека textsplitter , которая предоставляет простой разделитель текста для разбиения документов на сегменты, которые можно векторизовать.

Следующее определение настраивает функцию фрагментирования для возврата сегментов документа по 200 символов с перекрытием между фрагментами по 20 символов.

splitter := textsplitter.NewRecursiveCharacter(
	textsplitter.WithChunkSize(200),
	textsplitter.WithChunkOverlap(20),
)

Дополнительные параметры фрагментации этой библиотеки можно найти в документации langchaingo .

Определите поток индексатора

genkit.DefineFlow(
	"indexMenu",
	func(ctx context.Context, path string) (any, error) {
		// Extract plain text from the PDF. Wrap the logic in Run so it
		// appears as a step in your traces.
		pdfText, err := genkit.Run(ctx, "extract", func() (string, error) {
			return readPDF(path)
		})
		if err != nil {
			return nil, err
		}

		// Split the text into chunks. Wrap the logic in Run so it
		// appears as a step in your traces.
		docs, err := genkit.Run(ctx, "chunk", func() ([]*ai.Document, error) {
			chunks, err := splitter.SplitText(pdfText)
			if err != nil {
				return nil, err
			}

			var docs []*ai.Document
			for _, chunk := range chunks {
				docs = append(docs, ai.DocumentFromText(chunk, nil))
			}
			return docs, nil
		})
		if err != nil {
			return nil, err
		}

		// Add chunks to the index.
		err = ai.Index(ctx, menuPDFIndexer, ai.WithIndexerDocs(docs...))
		return nil, err
	},
)
// Helper function to extract plain text from a PDF. Excerpted from
// https://github.com/ledongthuc/pdf
func readPDF(path string) (string, error) {
	f, r, err := pdf.Open(path)
	if f != nil {
		defer f.Close()
	}
	if err != nil {
		return "", err
	}

	reader, err := r.GetPlainText()
	if err != nil {
		return "", err
	}

	bytes, err := io.ReadAll(reader)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

Запуск потока индексатора

genkit flow:run indexMenu "'menu.pdf'"

После запуска потока indexMenu база данных векторов будет заполнена документами и готова к использованию в потоках Genkit с этапами извлечения.

Определить поток с извлечением

В следующем примере показано, как можно использовать средство извлечения в потоке RAG. Как и в примере с индексатором, в этом примере используется средство извлечения векторов на основе файлов Genkit, которое не следует использовать в рабочей среде.

	ctx := context.Background()

	err := vertexai.Init(ctx, &vertexai.Config{})
	if err != nil {
		log.Fatal(err)
	}
	err = localvec.Init()
	if err != nil {
		log.Fatal(err)
	}

	model := vertexai.Model("gemini-1.5-flash")

	_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
		"menuQA",
		localvec.Config{
			Embedder: vertexai.Embedder("text-embedding-004"),
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	genkit.DefineFlow(
		"menuQA",
		func(ctx context.Context, question string) (string, error) {
			// Retrieve text relevant to the user's question.
			docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
				Document: ai.DocumentFromText(question, nil),
			})
			if err != nil {
				return "", err
			}

			// Construct a system message containing the menu excerpts you just
			// retrieved.
			menuInfo := ai.NewSystemTextMessage("Here's the menu context:")
			for _, doc := range docs.Documents {
				menuInfo.Content = append(menuInfo.Content, doc.Content...)
			}

			// Call Generate, including the menu information in your prompt.
			return ai.GenerateText(ctx, model,
				ai.WithMessages(
					ai.NewSystemTextMessage(`
You are acting as a helpful AI assistant that can answer questions about the
food available on the menu at Genkit Grub Pub.
Use only the context provided to answer the question. If you don't know, do not
make up an answer. Do not add or change items on the menu.`),
					menuInfo,
					ai.NewUserTextMessage(question)))
		})

Напишите свои собственные индексаторы и ретриверы

Также возможно создать свой собственный ретривер. Это полезно, если ваши документы хранятся в хранилище документов, которое не поддерживается Genkit (например, MySQL, Google Drive и т. д.). Genkit SDK предоставляет гибкие методы, позволяющие предоставлять собственный код для получения документов.

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

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

type CustomMenuRetrieverOptions struct {
	K          int
	PreRerankK int
}
advancedMenuRetriever := ai.DefineRetriever(
	"custom",
	"advancedMenuRetriever",
	func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
		// Handle options passed using our custom type.
		opts, _ := req.Options.(CustomMenuRetrieverOptions)
		// Set fields to default values when either the field was undefined
		// or when req.Options is not a CustomMenuRetrieverOptions.
		if opts.K == 0 {
			opts.K = 3
		}
		if opts.PreRerankK == 0 {
			opts.PreRerankK = 10
		}

		// Call the retriever as in the simple case.
		response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
			Document: req.Document,
			Options:  localvec.RetrieverOptions{K: opts.PreRerankK},
		})
		if err != nil {
			return nil, err
		}

		// Re-rank the returned documents using your custom function.
		rerankedDocs := rerank(response.Documents)
		response.Documents = rerankedDocs[:opts.K]

		return response, nil
	},
)