검색 증강 생성(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는 플러그인 시스템을 통해 색인 생성기와 검색기 지원을 제공합니다. 공식적으로 지원되는 플러그인은 다음과 같습니다.

  • Pinecone 클라우드 벡터 데이터베이스

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

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

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

RAG 흐름 정의

다음 예시는 식당 메뉴 PDF 문서 컬렉션을 벡터 데이터베이스에 수집하고 어떤 음식을 주문할 수 있는지 결정하는 흐름에서 사용할 수 있도록 검색하는 방법을 보여줍니다.

종속 항목 설치

이 예시에서는 langchaingotextsplitter 라이브러리와 ledongthuc/pdf 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 라이브러리를 사용합니다.

다음 정의는 20자(영문 기준)의 청크 간에 중복되는 200자(영문 기준)의 문서 세그먼트를 반환하도록 청킹 함수를 구성합니다.

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
	},
)