검색 보강 (RAG)

Firebase Genkit은 검색 보강(RAG) 흐름을 빌드하는 데 도움이 되는 추상화와 관련 도구와의 통합을 제공하는 플러그인을 제공합니다.

RAG란 무엇인가요?

검색 보강 생성은 외부 정보 소스를 LLM의 응답에 통합하는 데 사용되는 기법입니다. LLM은 일반적으로 광범위한 소재를 기반으로 학습되지만 LLM을 실제로 사용하려면 특정 도메인 지식이 필요한 경우가 많으므로 이를 수행할 수 있어야 합니다. 예를 들어 회사 제품에 대한 고객의 질문에 답할 때 LLM을 사용하고자 할 수 있습니다.

한 가지 해결책은 보다 구체적인 데이터를 사용하여 모델을 미세 조정하는 것입니다. 하지만 이 방법은 컴퓨팅 비용과 적절한 학습 데이터를 준비하는 데 필요한 작업 모두 많은 비용이 들 수 있습니다.

반면 RAG는 모델에 전달될 때 외부 데이터 소스를 프롬프트에 통합하는 방식으로 작동합니다. 예를 들어 '리사와 바트의 관계는 무엇인가요?'라는 프롬프트를 관련 정보를 추가하여 확장 ('보강')할 수 있으며, 그러면 '호머와 마지의 아이들의 이름은 바트, 리사, 매기입니다. 바트와 리사의 관계는 어떤가요?"

이 방식의 장점은 다음과 같습니다.

  • 모델을 다시 학습시킬 필요가 없으므로 더 경제적일 수 있습니다.
  • 데이터 소스를 지속적으로 업데이트할 수 있으며 LLM은 업데이트된 정보를 즉시 활용할 수 있습니다.
  • 이제 LLM의 응답에서 참조를 인용할 수 있습니다.

반면 RAG를 사용하면 당연히 더 긴 프롬프트를 사용하게 되며 일부 LLM API 서비스는 전송하는 각 입력 토큰에 대해 요금을 부과합니다. 결국에는 애플리케이션의 비용 절충점을 평가해야 합니다.

RAG는 매우 광범위한 영역이며 최고 품질의 RAG를 얻기 위해 사용되는 다양한 기법이 있습니다. 핵심 Genkit 프레임워크는 RAG 작업에 도움이 되는 두 가지 기본 추상화를 제공합니다.

  • 색인 생성기: '색인'에 문서를 추가합니다.
  • 임베딩: 문서를 벡터 표현으로 변환합니다.
  • 검색기: 쿼리가 지정된 경우 '색인'에서 문서를 검색합니다.

Genkit에서는 '색인'의 정의나 '색인'에서 문서가 정확히 어떻게 검색되는지에 대한 의견이 많지 않기 때문에 이러한 정의는 목적이 광범위합니다. Genkit는 Document 형식만 제공하며 그 밖의 모든 것은 검색기 또는 색인 생성기 구현 제공자가 정의합니다.

색인 생성기

색인은 특정 쿼리를 고려하여 관련 문서를 빠르게 검색할 수 있는 방식으로 문서를 추적하는 역할을 합니다. 이 작업은 대부분 임베딩이라는 다차원 벡터를 사용하여 문서의 색인을 생성하는 벡터 데이터베이스를 사용하여 수행됩니다. 텍스트 임베딩 (불투명)은 텍스트 구절로 표현된 개념을 나타내며, 특수 목적의 ML 모델을 사용하여 생성됩니다. 벡터 데이터베이스는 임베딩을 사용하여 텍스트의 색인을 생성함으로써 개념적으로 관련된 텍스트를 클러스터링하고 새로운 텍스트 문자열 (쿼리)과 관련된 문서를 검색할 수 있습니다.

생성 목적으로 문서를 검색하려면 먼저 문서 색인으로 문서를 수집해야 합니다. 일반적인 수집 흐름은 다음을 수행합니다.

  1. 프롬프트를 보강하기 위해 관련된 부분만 사용하도록 '청킹'처럼 큰 문서를 작은 문서로 분할합니다. 많은 LLM은 컨텍스트 기간이 제한되어 있어 프롬프트에 전체 문서를 포함하는 것이 비실용적이기 때문입니다.

    Genkit는 기본 제공 청킹 라이브러리를 제공하지 않습니다. 하지만 Genkit와 호환되는 오픈소스 라이브러리가 있습니다.

  2. 각 청크에 대한 임베딩을 생성합니다. 사용 중인 데이터베이스에 따라 임베딩 생성 모델을 사용해 명시적으로 수행하거나 데이터베이스에서 제공하는 임베딩 생성기를 사용할 수 있습니다.

  3. 텍스트 청크와 색인을 데이터베이스에 추가합니다.

수집 흐름을 자주 실행하지 않거나 안정적인 데이터 소스로 작업하는 경우 한 번만 실행할 수 있습니다. 반면 자주 변경되는 데이터로 작업하는 경우 수집 흐름을 지속적으로 실행할 수 있습니다 (예: 문서가 업데이트될 때마다 Cloud Firestore 트리거에서).

임베딩

삽입기는 콘텐츠 (텍스트, 이미지, 오디오 등)를 가져와서 원본 콘텐츠의 시맨틱 의미를 인코딩하는 숫자 벡터를 만드는 함수입니다. 위에서 언급했듯이 임베딩은 색인 생성 프로세스의 일부로 활용되지만 색인 없이 임베딩을 만드는 데 독립적으로 사용할 수도 있습니다.

리트리버

리트리버는 모든 종류의 문서 검색과 관련된 로직을 캡슐화하는 개념입니다. 가장 많이 사용되는 검색 사례에는 일반적으로 벡터 저장소 검색이 포함되지만 Genkit에서는 데이터를 반환하는 모든 함수가 검색 대상이 될 수 있습니다.

검색기를 만들려면 제공된 구현 중 하나를 사용하거나 직접 만들면 됩니다.

지원되는 색인 생성기, 검색기, 임베딩

Genkit은 플러그인 시스템을 통해 색인 생성기 및 리트리버를 지원합니다. 공식적으로 지원되는 플러그인은 다음과 같습니다.

또한 Genkit는 데이터베이스 구성 및 스키마에 맞게 맞춤설정할 수 있는 사전 정의된 코드 템플릿을 통해 다음과 같은 벡터 저장소를 지원합니다.

모델 임베딩은 다음 플러그인을 통해 제공됩니다.

플러그인 모델
Google 생성형 AI 도마뱀 텍스트 임베딩
Google Vertex AI 도마뱀 텍스트 임베딩

RAG 흐름 정의

다음 예는 레스토랑 메뉴 PDF 문서 모음을 벡터 데이터베이스로 수집하여 사용할 수 있는 음식을 결정하는 흐름에서 사용하기 위해 검색하는 방법을 보여줍니다.

PDF 처리를 위한 종속 항목 설치

npm install llm-chunk pdf-parse
npm i -D --save @types/pdf-parse

구성에 로컬 벡터 저장소 추가

import {
  devLocalIndexerRef,
  devLocalVectorstore,
} from '@genkit-ai/dev-local-vectorstore';
import { textEmbeddingGecko, vertexAI } from '@genkit-ai/vertexai';

configureGenkit({
  plugins: [
    // vertexAI provides the textEmbeddingGecko embedder
    vertexAI(),

    // the local vector store requires an embedder to translate from text to vector
    devLocalVectorstore([
      {
        indexName: 'menuQA',
        embedder: textEmbeddingGecko,
      },
    ]),
  ],
});

색인 생성기 정의

다음 예에서는 색인 생성기를 만들어 PDF 문서 컬렉션을 수집하고 이를 로컬 벡터 데이터베이스에 저장하는 방법을 보여줍니다.

간단한 테스트 및 프로토타입 제작을 위해 Genkit에서 즉시 제공하는 로컬 파일 기반 벡터 유사성 검색기를 사용합니다 (프로덕션에서는 사용하지 마세요).

색인 생성기 만들기

import { devLocalIndexerRef } from '@genkit-ai/dev-local-vectorstore';

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

단위 분할 구성 만들기

이 예에서는 간단한 텍스트 분할기를 제공하는 llm-chunk 라이브러리를 사용하여 문서를 벡터화할 수 있는 세그먼트로 나눕니다.

다음 정의는 1, 000~2, 000자(영문 기준)의 문서 세그먼트를 취하기 위해 청크화 함수를 구성합니다. 이러한 문서 세그먼트는 문장의 끝에서 나뉘며 100자 청크 간에 중첩됩니다.

const chunkingConfig = {
  minLength: 1000,
  maxLength: 2000,
  splitter: 'sentence',
  overlap: 100,
  delimiters: '',
} as any;

llm-chunk 문서에서 이 라이브러리의 더 많은 청크 옵션을 확인할 수 있습니다.

색인 생성기 흐름 정의

import { index } from '@genkit-ai/ai';
import { Document } from '@genkit-ai/ai/retriever';
import { defineFlow, run } from '@genkit-ai/flow';
import { readFile } from 'fs/promises';
import { chunk } from 'llm-chunk';
import path from 'path';
import pdf from 'pdf-parse';
import * as z from 'zod';

export const indexMenu = defineFlow(
  {
    name: 'indexMenu',
    inputSchema: z.string().describe('PDF file path'),
    outputSchema: z.void(),
  },
  async (filePath: string) => {
    filePath = path.resolve(filePath);

    // Read the pdf.
    const pdfTxt = await run('extract-text', () =>
      extractTextFromPdf(filePath)
    );

    // Divide the pdf text into segments.
    const chunks = await run('chunk-it', async () =>
      chunk(pdfTxt, chunkingConfig)
    );

    // Convert chunks of text into documents to store in the index.
    const documents = chunks.map((text) => {
      return Document.fromText(text, { filePath });
    });

    // Add documents to the index.
    await index({
      indexer: menuPdfIndexer,
      documents,
    });
  }
);

async function extractTextFromPdf(filePath: string) {
  const pdfFile = path.resolve(filePath);
  const dataBuffer = await readFile(pdfFile);
  const data = await pdf(dataBuffer);
  return data.text;
}

색인 생성기 흐름 실행

genkit flow:run indexMenu "'../pdfs'"

indexMenu 흐름을 실행한 후 벡터 데이터베이스에 문서가 입력되고 검색 단계를 통해 Genkit 흐름에서 사용할 수 있게 됩니다.

검색을 사용하여 흐름 정의

다음 예는 RAG 흐름에서 검색기를 사용하는 방법을 보여줍니다. 색인 생성기 예와 마찬가지로 이 예에서는 프로덕션에서 사용해서는 안 되는 Genkit의 파일 기반 벡터 검색기를 사용합니다.

import { generate } from '@genkit-ai/ai';
import { retrieve } from '@genkit-ai/ai/retriever';
import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';
import { defineFlow } from '@genkit-ai/flow';
import { geminiPro } from '@genkit-ai/vertexai';
import * as z from 'zod';

// Define the retriever reference
export const menuRetriever = devLocalRetrieverRef('menuQA');

export const menuQAFlow = defineFlow(
  { name: 'menuQA', inputSchema: z.string(), outputSchema: z.string() },
  async (input: string) => {
    // retrieve relevant documents
    const docs = await retrieve({
      retriever: menuRetriever,
      query: input,
      options: { k: 3 },
    });

    // generate a response
    const llmResponse = await generate({
      model: geminiPro,
      prompt: `
    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.

    Question: ${input}
    `,
      context: docs,
    });

    const output = llmResponse.text();
    return output;
  }
);

나만의 색인 생성기 및 검색기 작성

자체 리트리버를 만들 수도 있습니다. 이 기능은 Genkit에서 지원되지 않는 문서 저장소 (예: MySQL, Google Drive 등)에서 문서를 관리하는 경우에 유용합니다. Genkit SDK는 문서 가져오기를 위한 커스텀 코드를 제공할 수 있는 유연한 메서드를 제공합니다. 또한 Genkit의 기존 검색기를 기반으로 빌드되고 고급 RG 기법 (예: 순위 재지정 또는 프롬프트 확장)을 가장 위에 적용하는 맞춤 검색기를 정의할 수도 있습니다.

심플 리트리버

간단한 검색기를 사용하면 기존 코드를 검색기로 쉽게 변환할 수 있습니다.

import {
  defineSimpleRetriever,
  retrieve
} from '@genkit-ai/ai/retriever';
import { searchEmails } from './db';
import { z } from 'zod';

defineSimpleRetriever({
  name: 'myDatabase',
  configSchema: z.object({
    limit: z.number().optional()
  }).optional(),
  // we'll extract "message" from the returned email item
  content: 'message',
  // and several keys to use as metadata
  metadata: ['from', 'to', 'subject'],
} async (query, config) => {
  const result = await searchEmails(query.text(), {limit: config.limit});
  return result.data.emails;
});

커스텀 검색기

import {
  CommonRetrieverOptionsSchema,
  defineRetriever,
  retrieve,
} from '@genkit-ai/ai/retriever';
import * as z from 'zod';

export const menuRetriever = devLocalRetrieverRef('menuQA');

const advancedMenuRetrieverOptionsSchema = CommonRetrieverOptionsSchema.extend({
  preRerankK: z.number().max(1000),
});

const advancedMenuRetriever = defineRetriever(
  {
    name: `custom/advancedMenuRetriever`,
    configSchema: advancedMenuRetrieverOptionsSchema,
  },
  async (input, options) => {
    const extendedPrompt = await extendPrompt(input);
    const docs = await retrieve({
      retriever: menuRetriever,
      query: extendedPrompt,
      options: { k: options.preRerankK || 10 },
    });
    const rerankedDocs = await rerank(docs);
    return rerankedDocs.slice(0, options.k || 3);
  }
);

extendPromptrerank은 프레임워크에서 제공하지 않고 직접 구현해야 합니다.

그런 다음 리트리버를 교체하면 됩니다.

const docs = await retrieve({
  retriever: advancedRetriever,
  query: input,
  options: { preRerankK: 7, k: 3 },
});