Tạo hệ thống tăng cường truy xuất (RAG)

Firebase Genkit cung cấp các thành phần trừu tượng giúp bạn xây dựng 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ụ liên quan.

RAG là gì?

Tạo 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 phản hồi của một LLM. Điều quan trọng là phải làm được điều này vì mặc dù các LLM thường được đào tạo về rất nhiều tài liệu, nhưng việc sử dụng các LLM trong thực tế thường đòi hỏi phải có kiến thức chuyên ngành cụ thể (ví dụ: bạn có thể muốn sử dụng LLM để trả lời thắc mắc của khách hàng về sản phẩm của công ty bạn).

Một giải pháp là tinh chỉnh mô hình bằng cách sử dụ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í điện toán lẫn công sức cần thiết để chuẩn bị đầy đủ 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 lời nhắc tại thời điểm truyền dữ liệu đến mô hình. Ví dụ: bạn có thể hình dung câu lệnh "Mối quan hệ của Bart với Lisa là gì?" có thể được mở rộng ("tăng cường") bằng cách thêm một số thông tin có liên quan, dẫn đến câu lệnh "Con của Homer và Marge tên là Bart, Lisa và Maggie. Mối quan hệ của Bart với Lisa là gì?"

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 của mình và LLM có thể ngay lập tức sử dụng thông tin cập nhật.
  • Giờ đây, bạn có thể trích dẫn nội dung tham khảo trong câu trả lời của LLM.

Mặt khác, việc sử dụng RAG đương nhiên có nghĩa là các lời nhắc sẽ dài hơn và một số dịch vụ LLM API 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á sự đánh đổi chi phí 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 được dùng để đạt được RAG chất lượng tốt nhất. Khung Genkit chính cung cấp hai bản tóm tắt chính để giúp bạn thực hiện RAG:

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

Những định nghĩa này mang ý nghĩa rộng vì Genkit không định nghĩa "chỉ mục" là gì hoặc cách chính xác các tài liệu được truy xuất từ đó. Genkit chỉ cung cấp định dạng Document và mọi nội dung khác được xác định bởi nhà cung cấp dịch vụ triển khai trình truy xuất hoặc lập chỉ mục.

Bộ lập chỉ mục

Chỉ mục chịu trách nhiệm theo dõi tài liệu của bạn sao cho bạn có thể nhanh chóng truy xuất các tài liệu liên quan khi có một truy vấn cụ thể. Việc này thường được thực hiện nhất bằng cách sử dụng cơ sở dữ liệu vectơ, giúp lập chỉ mục tài liệu của bạn bằng cách sử dụng vectơ đa chiều được gọi là nhúng. Nhúng văn bản (mờ) thể hiện các khái niệm được biểu thị bằng một đoạn văn bản; chúng được tạo bằng các mô hình học máy chuyên dụng. 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ể phân nhóm 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 (truy vấn).

Trước khi có thể truy xuất tài liệu để tạo, bạn cần nhập các tài liệu đó vào chỉ mục tài liệu của mình. Quy trình truyền dẫn thông thường có những chức năng 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 phù hợp nhằm tăng cường hiệu quả cho lời nhắc của bạn – "chuyển hướng". Điều này là cần thiết vì nhiều LLM có khung thời gian ngữ cảnh bị giới hạn, khiến việc đưa toàn bộ tài liệu kèm câu lệnh vào là không thực tế.

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

  2. Tạo nội dung nhúng cho từng phần. Tuỳ thuộc vào cơ sở dữ liệu mà bạn đang dùng, bạn có thể làm việc này một cách rõ ràng bằng một mô hình tạo nhúng hoặc bạn có thể sử dụng trình tạo 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 nhập 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 những dữ liệu thường xuyên thay đổi, thì 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, mỗi khi tài liệu được cập nhật).

Trình nhúng

Trình nhúng là một hàm nhận 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 gốc. Như đã đề cập ở trên, 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 chúng một cách độc lập để tạo các tệp nhúng mà không cần chỉ mục.

Chó tha mồi

Trình truy xuất là một khái niệm đóng gói logic liên quan đến mọi loại yêu cầu 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 việc truy xuất từ các cửa hàng 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 một 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 trình truy xuất của riêng mình.

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

Genkit hỗ trợ trình lập chỉ mục và trình truy xuất dữ liệu 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:

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

Bạn có thể hỗ trợ mô hình nhúng 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 tắc kè
Vertex AI của Google Nhúng văn bản tắc kè

Xác định luồng RAG

Các ví dụ sau 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 những món ăn có sẵn.

Cài đặt các phần phụ thuộc để xử lý tệp PDF

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

Thêm kho vectơ cục bộ vào cấu hình của bạn

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

Định nghĩa Trình lập chỉ mục

Ví dụ sau cho thấy 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 công cụ truy xuất độ tương tự vectơ dựa trên tệp cục bộ mà Genkit cung cấp ngay từ đầu để kiểm thử và tạo nguyên mẫu đơn giản (không sử dụng trong phiên bản chính thức)

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

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

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

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

Ví dụ này sử dụng thư viện llm-chunk, 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 đoạn có thể được vectơ hoá.

Định nghĩa sau đây định cấu hình hàm phân đoạn để đảm bảo một phân đoạn tài liệu từ 1000 đến 2000 ký tự, được chia nhỏ ở cuối câu, với sự chồng chéo giữa các đoạn 100 ký tự.

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

Bạn có thể xem thêm các lựa chọn phân đoạn khác cho thư viện này trong tài liệu về llm-chunk.

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

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;
}

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

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

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

Xác định luồng bằng hoạt động truy xuất

Ví dụ sau đây cho thấy cách bạn có thể sử dụng chú chó Retriever trong quy trình 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 mà bạn không nên sử dụng trong phiên bản chính thức.

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

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 dữ liệu của riêng mình. Cách này sẽ hữu ích nếu tài liệu đượ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 được tạo 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 tiện ích lời nhắc) ở trên cùng.

Chó tha mồi đơn giản

Các trình truy xuất đơn giản cho phép bạn dễ dàng chuyển đổi mã hiện có thành các trình truy xuất:

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

Chó truy xuất tuỳ chỉnh

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 là thứ bạn phải tự triển khai, không do khung cung cấp)

Sau đó, bạn chỉ cần hoán đổi trình truy xuất:

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