Thế hệ tăng cường truy xuất (RAG)

Genkit cung cấp các bản tóm tắt giúp bạn tạo quy trình tạo tăng cường truy xuất (RAG), cũng như các trình bổ trợ cung cấp khả năng tích hợp với các công cụ có liên quan.

RAG là gì?

Tạo dữ liệu tăng cường truy xuất là một kỹ thuật dùng để kết hợp các nguồn thông tin bên ngoài vào câu trả lời của LLM. Điều này rất quan trọng vì mặc dù LLM thường được huấn luyện trên một lượng lớn tài liệu, nhưng việc sử dụng LLM trong thực tế thường đòi hỏi kiến thức chuyên môn cụ thể (ví dụ: bạn có thể muốn sử dụng LLM để trả lời câu hỏi của khách hàng về sản phẩm của công ty).

Một giải pháp là tinh chỉnh mô hình bằng dữ liệu cụ thể hơn. Tuy nhiên, việc này có thể tốn kém cả về chi phí tính toán và về nỗ lực cần thiết để chuẩn bị đủ dữ liệu huấn luyện.

Ngược lại, RAG hoạt động bằng cách kết hợp các nguồn dữ liệu bên ngoài vào một câu lệnh tại thời điểm câu lệnh được truyền đến mô hình. Ví dụ: bạn có thể tưởng tượng câu lệnh "Bart có mối quan hệ gì với Lisa?" có thể được mở rộng ("mở rộng") bằng cách thêm vào một số thông tin có liên quan, dẫn đến câu lệnh "Con cái của Homer và Marge tên là Bart, Lisa và Maggie. Bart có mối quan hệ gì với Lisa?"

Phương pháp này có một số ưu điểm:

  • Cách này có thể tiết kiệm chi phí hơn vì bạn không phải huấn luyện lại mô hình.
  • Bạn có thể liên tục cập nhật nguồn dữ liệu và LLM có thể sử dụng ngay thông tin đã cập nhật.
  • Giờ đây, bạn có thể trích dẫn tài liệu tham khảo trong câu trả lời của LLM.

Mặt khác, việc sử dụng RAG sẽ dẫn đến các lời nhắc dài hơn và một số dịch vụ API LLM sẽ tính phí cho mỗi mã thông báo đầu vào mà bạn gửi. Cuối cùng, bạn phải đánh giá các chi phí đánh đổi cho ứng dụng của mình.

RAG là một lĩnh vực rất rộng và có nhiều kỹ thuật khác nhau được dùng để đạt được RAG chất lượng cao nhất. Khung Genkit cốt lõi cung cấp hai thành phần trừu tượng chính để giúp bạn thực hiện RAG:

  • Trình lập chỉ mục: thêm tài liệu vào "chỉ mục".
  • Trình nhúng: chuyển đổi tài liệu thành một vectơ đại diện
  • Trình truy xuất: truy xuất tài liệu từ một "chỉ mục", dựa trên một truy vấn.

Các định nghĩa này được mở rộng một cách có chủ ý vì Genkit không có ý kiến về "chỉ mục" là gì hoặc cách truy xuất chính xác tài liệu từ chỉ mục đó. Genkit chỉ cung cấp định dạng Document và mọi thứ khác do trình truy xuất hoặc nhà cung cấp triển khai chỉ mục xác định.

Trình lập chỉ mục

Chỉ mục có trách nhiệm theo dõi các tài liệu của bạn theo cách mà bạn có thể nhanh chóng truy xuất các tài liệu có liên quan khi có một truy vấn cụ thể. Việc này thường được thực hiện bằng cách sử dụng cơ sở dữ liệu vectơ. Cơ sở dữ liệu này lập chỉ mục tài liệu của bạn bằng các vectơ đa chiều được gọi là nội dung nhúng. Một văn bản nhúng (mờ) đại diện cho các khái niệm được thể hiện bằng một đoạn văn bản; các khái niệm này được tạo bằng các mô hình học máy có mục đích đặc biệt. Bằng cách lập chỉ mục văn bản bằng cách nhúng, cơ sở dữ liệu vectơ có thể nhóm các văn bản có liên quan về mặt khái niệm và truy xuất các tài liệu liên quan đến một chuỗi văn bản mới (cụm từ tìm kiếm).

Trước khi có thể truy xuất tài liệu cho mục đích tạo, bạn cần nhập tài liệu đó vào chỉ mục tài liệu. Quy trình truyền dẫn thông thường thực hiện những việc sau:

  1. Chia các tài liệu lớn thành các tài liệu nhỏ hơn để chỉ sử dụng các phần liên quan để tăng cường câu lệnh của bạn – "chia thành phần". Điều này là cần thiết vì nhiều LLM có cửa sổ ngữ cảnh bị hạn chế, khiến việc đưa toàn bộ tài liệu vào một câu lệnh trở nên không thực tế.

    Genkit không cung cấp thư viện phân đoạn tích hợp sẵn; tuy nhiên, có các thư viện nguồn mở tương thích với Genkit.

  2. Tạo các phần nhúng cho mỗi đoạn. Tuỳ thuộc vào cơ sở dữ liệu bạn đang sử dụng, bạn có thể thực hiện việc này một cách rõ ràng bằng mô hình tạo nội dung nhúng hoặc sử dụng trình tạo nội dung nhúng do cơ sở dữ liệu cung cấp.

  3. Thêm đoạn văn bản và chỉ mục của đoạn văn bản đó vào cơ sở dữ liệu.

Bạn có thể chạy quy trình truyền dẫn không thường xuyên hoặc chỉ một lần nếu đang làm việc với một nguồn dữ liệu ổn định. Mặt khác, nếu đang làm việc với dữ liệu thường xuyên thay đổi, bạn có thể liên tục chạy quy trình nhập (ví dụ: trong điều kiện kích hoạt Cloud Firestore, bất cứ khi nào một tài liệu được cập nhật).

Trình nhúng

Trình nhúng là một hàm lấy nội dung (văn bản, hình ảnh, âm thanh, v.v.) và tạo một vectơ số để mã hoá ý nghĩa ngữ nghĩa của nội dung ban đầu. Như đã đề cập ở trên, các trình nhúng được tận dụng trong quá trình lập chỉ mục. Tuy nhiên, bạn cũng có thể sử dụng các hàm này độc lập để tạo các phần nhúng mà không cần chỉ mục.

Chó săn mồi

Trình truy xuất là một khái niệm bao gồm logic liên quan đến mọi loại truy xuất tài liệu. Các trường hợp truy xuất phổ biến nhất thường bao gồm truy xuất từ các kho vectơ. Tuy nhiên, trong Genkit, trình truy xuất có thể là bất kỳ hàm nào trả về dữ liệu.

Để tạo trình truy xuất, bạn có thể sử dụng một trong các phương thức triển khai được cung cấp hoặc tạo phương thức triển khai của riêng mình.

Các trình lập chỉ mục, trình truy xuất và trình nhúng được hỗ trợ

Genkit cung cấp dịch vụ hỗ trợ trình lập chỉ mục và trình truy xuất thông qua hệ thống trình bổ trợ. Các trình bổ trợ sau đây được hỗ trợ chính thức:

  • Cơ sở dữ liệu vectơ trên đám mây Pinecone

Ngoài ra, Genkit hỗ trợ các kho vectơ sau thông qua các mẫu mã được xác định trước mà bạn có thể tuỳ chỉnh cho cấu hình cơ sở dữ liệu và giản đồ của mình:

Hỗ trợ mô hình nhúng được cung cấp thông qua các trình bổ trợ sau:

Trình bổ trợ Mô hình
AI tạo sinh của Google Nhúng văn bản

Xác định quy trình RAG

Các ví dụ sau đây cho thấy cách bạn có thể nhập một tập hợp tài liệu PDF về thực đơn của nhà hàng vào cơ sở dữ liệu vectơ và truy xuất các tài liệu đó để sử dụng trong một luồng xác định các món ăn có sẵn.

Cài đặt phần phụ thuộc

Trong ví dụ này, chúng ta sẽ sử dụng thư viện textsplitter từ langchaingo và Thư viện phân tích cú pháp PDF ledongthuc/pdf:

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

Xác định Trình lập chỉ mục

Ví dụ sau đây cho biết cách tạo một trình lập chỉ mục để nhập một tập hợp tài liệu PDF và lưu trữ các tài liệu đó trong cơ sở dữ liệu vectơ cục bộ.

Công cụ này sử dụng trình truy xuất vectơ tương đồng dựa trên tệp cục bộ mà Genkit cung cấp sẵn để kiểm thử và tạo bản minh hoạ đơn giản. Không sử dụng tính năng này trong phiên bản chính thức.

Tạo trình lập chỉ mục

// 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()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

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

Tạo cấu hình phân đoạn

Ví dụ này sử dụng thư viện textsplitter cung cấp một trình phân tách văn bản đơn giản để chia tài liệu thành các phân đoạn có thể được vectơ hoá.

Định nghĩa sau đây định cấu hình hàm phân đoạn để trả về các phân đoạn tài liệu gồm 200 ký tự, với sự chồng chéo giữa các phân đoạn gồm 20 ký tự.

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

Bạn có thể tìm thấy các tuỳ chọn phân đoạn khác cho thư viện này trong tài liệu về langchaingo.

Xác định luồng trình lập chỉ mục

genkit.DefineFlow(
    g, "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.WithDocs(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
}

Chạy quy trình của trình lập chỉ mục

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

Sau khi chạy luồng indexMenu, cơ sở dữ liệu vectơ sẽ được tạo bằng các tài liệu và sẵn sàng để sử dụng trong các luồng Genkit có các bước truy xuất.

Xác định luồng có chức năng truy xuất

Ví dụ sau đây cho thấy cách bạn có thể sử dụng trình truy xuất trong luồng RAG. Giống như ví dụ về trình lập chỉ mục, ví dụ này sử dụng trình truy xuất vectơ dựa trên tệp của Genkit. Bạn không nên sử dụng trình truy xuất này trong môi trường sản xuất.

ctx := context.Background()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

model := googlegenai.VertexAIModel(g, "gemini-1.5-flash")

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

genkit.DefineFlow(
  g, "menuQA",
  func(ctx context.Context, question string) (string, error) {
    // Retrieve text relevant to the user's question.
    resp, err := ai.Retrieve(ctx, menuPdfRetriever, ai.WithTextDocs(question))


    if err != nil {
        return "", err
    }

    // Call Generate, including the menu information in your prompt.
    return genkit.GenerateText(ctx, g,
        ai.WithModelName("googleai/gemini-2.0-flash"),
        ai.WithDocs(resp.Documents),
        ai.WithSystem(`
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.`)
        ai.WithPrompt(question),
  })

Viết trình lập chỉ mục và trình truy xuất của riêng bạn

Bạn cũng có thể tạo trình truy xuất của riêng mình. Điều này sẽ hữu ích nếu tài liệu của bạn được quản lý trong một kho tài liệu không được hỗ trợ trong Genkit (ví dụ: MySQL, Google Drive, v.v.). SDK Genkit cung cấp các phương thức linh hoạt cho phép bạn cung cấp mã tuỳ chỉnh để tìm nạp tài liệu.

Bạn cũng có thể xác định các trình truy xuất tuỳ chỉnh dựa trên các trình truy xuất hiện có trong Genkit và áp dụng các kỹ thuật RAG nâng cao (chẳng hạn như xếp hạng lại hoặc mở rộng lời nhắc) ở trên cùng.

Ví dụ: giả sử bạn có một hàm xếp hạng lại tuỳ chỉnh mà bạn muốn sử dụng. Ví dụ sau đây xác định một trình truy xuất tuỳ chỉnh áp dụng hàm của bạn cho trình truy xuất trình đơn được xác định trước đó:

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}

advancedMenuRetriever := genkit.DefineRetriever(
    g, "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.
        resp, err := ai.Retrieve(ctx, menuPDFRetriever,
            ai.WithDocs(req.Query),
            ai.WithConfig(ocalvec.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
    },
)