+15

Quy trình xây dựng hệ thống RAG tích hợp Function Calling (with source code)

I. Giới thiệu (Introduction)

Trong thời đại bùng nổ thông tin như hiện nay, việc xây dựng hệ thống có khả năng truy xuất và xử lý thông tin hiệu quả là vô cùng quan trọng. Retrieval Augmented Generation (RAG) đã nổi lên như một kiến trúc đầy hứa hẹn, cho phép kết hợp sức mạnh của mô hình ngôn ngữ (Language Model) với kho dữ liệu khổng lồ.

Tuần trước, chúng ta đã cùng nhau tìm hiểu về Function Calling - một kỹ thuật giúp mở rộng khả năng của RAG, cho phép hệ thống tương tác với thế giới bên ngoài thông qua việc gọi các hàm được định nghĩa sẵn. (Xem lại bài viết)

Hôm nay, chúng ta sẽ đi sâu vào chi tiết quy trình xây dựng một hệ thống RAG tích hợp Function Calling hoàn chỉnh. Bài viết này sẽ hướng dẫn bạn từng bước, từ khâu thu thập và xử lý dữ liệu, xây dựng kho dữ liệu với công nghệ embedding, đến việc tích hợp function calling với các mô hình ngôn ngữ phổ biến như GPT-4o, Gemini 1.5 Flash và Vistral 7B (mô hình mã nguồn mở được fine-tuned từ mô hình Viet-Mistral/Vistral-7B-Chat bởi anh Hiếu Ngô (link huggingface model).

Mục tiêu của chúng ta là trang bị cho bạn kiến thức và kỹ năng để tự tay xây dựng một hệ thống RAG mạnh mẽ, linh hoạt và hiệu quả, đáp ứng được nhu cầu xử lý thông tin ngày càng cao trong thực tế.

II. Thu thập và xử lý dữ liệu (Data Collection and Processing)

2.1. Xác định nguồn dữ liệu (Identify Data Sources)

Trước khi bắt tay vào xây dựng hệ thống RAG, việc đầu tiên là xác định nguồn dữ liệu phù hợp với bài toán cụ thể. Dữ liệu có thể đến từ nhiều nguồn khác nhau, tùy thuộc vào mục đích và yêu cầu của hệ thống. Một số nguồn dữ liệu phổ biến bao gồm:

  • File văn bản (Text files): Chứa thông tin dưới dạng văn bản thô, ví dụ như sách, báo, tài liệu nghiên cứu,...
  • Cơ sở dữ liệu (Databases): Lưu trữ thông tin có cấu trúc, cho phép truy vấn và xử lý dữ liệu hiệu quả.
  • Website (Websites): Chứa lượng thông tin khổng lồ, đa dạng về chủ đề và định dạng.
  • API (Application Programming Interfaces): Cung cấp giao diện để truy cập dữ liệu từ các ứng dụng khác.

Việc lựa chọn nguồn dữ liệu phù hợp phụ thuộc vào nhiều yếu tố, bao gồm:

  • Mục tiêu của hệ thống RAG: Hệ thống cần trả lời loại câu hỏi gì? Cần thông tin về lĩnh vực nào?
  • Khả năng tiếp cận dữ liệu: Có thể dễ dàng thu thập dữ liệu từ nguồn nào?
  • Chất lượng dữ liệu: Dữ liệu có đáng tin cậy, chính xác và đầy đủ hay không?

2.2. Thu thập dữ liệu (Data Acquisition)

Sau khi xác định được nguồn dữ liệu phù hợp, bước tiếp theo là thu thập dữ liệu. Có nhiều phương pháp thu thập dữ liệu khác nhau, phổ biến nhất là:

  • Web Scraping: Kỹ thuật tự động trích xuất dữ liệu từ website.
    • Ưu điểm: Thu thập dữ liệu từ nhiều website khác nhau một cách tự động.
    • Nhược điểm: Cần hiểu biết về HTML, CSS và có thể gặp khó khăn với website có cấu trúc phức tạp.
    • Công cụ hỗ trợ: Scrapy, Beautiful Soup,...
  • API: Sử dụng giao diện lập trình ứng dụng (API) do các website hoặc ứng dụng khác cung cấp để truy cập dữ liệu.
    • Ưu điểm: Dữ liệu có cấu trúc rõ ràng, dễ dàng xử lý.
    • Nhược điểm: Cần tìm hiểu tài liệu API và có thể bị giới hạn bởi nhà cung cấp.
  • Truy vấn cơ sở dữ liệu (Database Queries): Sử dụng ngôn ngữ truy vấn (SQL, NoSQL,...) để lấy dữ liệu từ cơ sở dữ liệu.
    • Ưu điểm: Truy xuất dữ liệu nhanh chóng và hiệu quả.
    • Nhược điểm: Cần có quyền truy cập vào cơ sở dữ liệu.

Ngoài ra, tôi thường sử dụng Scrapegraph-ai, một công cụ mạnh mẽ kết hợp sức mạnh của LLM để crawl web hiệu quả. Scrapegraph-ai giúp đơn giản hóa việc trích xuất dữ liệu từ website, đặc biệt là những website có cấu trúc phức tạp. Tôi sẽ chia sẻ chi tiết hơn về công cụ này trong một bài viết sắp tới. (Xem thêm về Scrapegraph-ai trên Github)

Việc lựa chọn phương pháp thu thập dữ liệu phù hợp phụ thuộc vào đặc thù của từng nguồn dữ liệu và yêu cầu của dự án.

Trong trường hợp của bài viết này, để minh họa cho quy trình xây dựng hệ thống RAG, tôi sẽ tạo một bộ dữ liệu mô phỏng data của một cửa hàng bán hàng hóa. Bộ dữ liệu này bao gồm 3 mục cơ bản:

  • Thông tin nhân viên (employee_info.txt): Chứa thông tin về họ tên, chức vụ, số điện thoại, email của các nhân viên trong cửa hàng.
  • Thông tin sản phẩm (products.txt): Chứa thông tin chi tiết về từng sản phẩm, bao gồm tên sản phẩm, thương hiệu, nhà sản xuất, mô tả, giá bán, tình trạng tồn kho,...
  • Đánh giá của khách hàng (reviews.txt): Chứa các đánh giá của khách hàng về sản phẩm, bao gồm nội dung đánh giá, thời gian đánh giá, thông tin người đánh giá.

Dữ liệu được tổ chức đơn giản, dễ hiểu và lưu trữ trong các file văn bản (.txt).

2.3. Xử lý dữ liệu (Data Preprocessing)

Sau khi thu thập được dữ liệu, bước tiếp theo là xử lý dữ liệu thô thành dạng thức phù hợp để xây dựng kho dữ liệu cho hệ thống RAG. Quá trình xử lý dữ liệu đóng vai trò quan trọng, ảnh hưởng trực tiếp đến hiệu quả tìm kiếm và chất lượng câu trả lời của hệ thống.

Các bước xử lý dữ liệu cơ bản bao gồm:

  • Làm sạch dữ liệu (Data Cleaning):
    • Loại bỏ các ký tự đặc biệt, không cần thiết (ví dụ: HTML tags, punctuation).
    • Chuẩn hóa dữ liệu: chuyển đổi chữ hoa/thường, sửa lỗi chính tả,...
  • Chia văn bản thành các đoạn nhỏ (Chunking):
    • Mô hình ngôn ngữ thường có giới hạn về độ dài văn bản đầu vào.
    • Chia văn bản thành các đoạn nhỏ giúp model xử lý hiệu quả hơn và tăng khả năng tìm kiếm thông tin chính xác.

Có nhiều cách để chia văn bản thành các đoạn nhỏ, phổ biến nhất là:

  • Chia theo số lượng từ (Word Count): Chia văn bản thành các đoạn có độ dài cố định, ví dụ: 100 từ/đoạn.
  • Chia theo câu (Sentence Segmentation): Sử dụng các công cụ Natural Language Processing (NLP) để xác định ranh giới câu và chia văn bản thành các câu riêng biệt.
  • Chia theo dấu ngắt đoạn (Paragraph Segmentation): Chia văn bản theo các dấu xuống dòng hoặc thẻ HTML <p>.
  • Chia theo ý nghĩa (Semantic Chunking): Sử dụng các kỹ thuật NLP phức tạp hơn để xác định các đoạn văn bản có ý nghĩa liên quan.

Trong các ví dụ tiếp theo, tôi sẽ sử dụng cách chia văn bản thành từng đoạn bằng cách tách theo dòng, như đoạn code sau:

from langchain.text_splitter import TextSplitter

# Định nghĩa class kế thừa TextSplitter để chia văn bản theo dòng
class LineTextSplitter(TextSplitter):
    def split_text(self, text):
        return text.split('\n')

# Khởi tạo đối tượng LineTextSplitter
text_splitter = LineTextSplitter()

# Đọc dữ liệu từ file products.txt
with open('products.txt', 'r', encoding='utf-8') as f:
    text = f.read()

# Chia văn bản thành các đoạn theo dòng
documents = text_splitter.split_text(text)

# In ra kết quả để kiểm tra
print(documents)

Giải thích đoạn code:

  1. Import TextSplitter: Import lớp TextSplitter từ thư viện langchain.text_splitter.
  2. Định nghĩa LineTextSplitter: Tạo một lớp mới LineTextSplitter kế thừa từ TextSplitter và ghi đè phương thức split_text để chia văn bản theo dòng (\n).
  3. Khởi tạo text_splitter: Tạo một đối tượng text_splitter từ lớp LineTextSplitter.
  4. Đọc dữ liệu: Đọc nội dung văn bản từ file products.txt và lưu vào biến text.
  5. Chia văn bản: Sử dụng phương thức split_text của đối tượng text_splitter để chia văn bản text thành các đoạn theo dòng và lưu vào biến documents.
  6. In kết quả: In ra biến documents để kiểm tra kết quả chia đoạn.

Kết quả:

Khi chạy đoạn code trên với file products.txt bạn cung cấp, biến documents sẽ chứa một list các đoạn văn bản, mỗi đoạn tương ứng với một dòng trong file. Ví dụ:

>>> documents[:3]
['Áo phông nam - thông tin cơ bản: Tên sản phẩm: Áo phông nam Basic; Thương hiệu: The Casual; Nhà sản xuất: Công ty TNHH May mặc Thời trang; Mã sản phẩm: APN001; Loại sản phẩm: Áo thun, Thời trang nam; Mô tả ngắn: Áo phông nam chất liệu cotton thoáng mát, thoải mái cho mọi hoạt động.',
 'Áo phông nam - thông tin chi tiết: - Chất liệu: 100% cotton co giãn 4 chiều, thấm hút mồ hôi tốt. - Thiết kế: Cổ tròn, tay ngắn, form áo suông nhẹ, dễ phối đồ. - Màu sắc: Đen, trắng, xám, xanh navy. - Size: S, M, L, XL, XXL. - Hướng dẫn sử dụng: Giặt máy ở chế độ nhẹ nhàng, không dùng chất tẩy mạnh.',
 'Áo phông nam - thông tin bổ sung: Giá bán: 150.000 VNĐ; Tình trạng tồn kho: Còn hàng; Khuyến mãi: Giảm 10% cho đơn hàng từ 2 sản phẩm; Đánh giá của khách hàng: 4.5/5 sao - Chất vải mềm mại, thoải mái khi mặc. - Form áo đẹp, dễ phối đồ. - Giá cả hợp lý.',
 ]

Bằng cách chia văn bản thành các đoạn nhỏ, chúng ta đã chuẩn bị dữ liệu sẵn sàng cho bước tiếp theo: xây dựng kho dữ liệu với công nghệ embedding.

III. Xây dựng kho dữ liệu (Building Data Index)

3.1. Giới thiệu về Vector Embedding (Introduction to Vector Embeddings)

Sau khi đã xử lý và chia văn bản thành các đoạn nhỏ, bước tiếp theo là xây dựng kho dữ liệu (index) để có thể tìm kiếm thông tin một cách hiệu quả. Trong các hệ thống tìm kiếm truyền thống, việc tìm kiếm thường dựa trên keyword matching - tức là so khớp các từ khóa trong câu truy vấn với từ khóa có trong văn bản. Tuy nhiên, phương pháp này có nhiều hạn chế:

  • Không nắm bắt được ngữ nghĩa: Keyword matching chỉ so khớp từ khóa mà không hiểu được ý nghĩa của câu truy vấn và văn bản.
  • Khó xử lý ngôn ngữ tự nhiên: Ngôn ngữ tự nhiên rất đa dạng và phức tạp, việc tìm kiếm dựa trên keyword matching dễ dẫn đến kết quả không chính xác.

Để khắc phục những hạn chế trên, Vector Embedding đã ra đời như một giải pháp hiệu quả. Vector embedding là kỹ thuật chuyển đổi văn bản (đoạn văn bản, câu, từ) thành một vector số (numerical vector). Mỗi vector đại diện cho ý nghĩa của văn bản trong không gian vector đa chiều.

Lợi ích của việc sử dụng vector embedding:

  • Nắm bắt được ngữ nghĩa: Các vector embedding được huấn luyện để nắm bắt được ngữ nghĩa của văn bản. Các văn bản có ý nghĩa tương đồng sẽ có vector gần nhau trong không gian vector.
  • Tăng khả năng tìm kiếm: Việc tìm kiếm dựa trên vector embedding cho phép tìm kiếm thông tin dựa trên ý nghĩa, không chỉ dựa trên từ khóa.
  • Hỗ trợ nhiều tác vụ NLP: Vector embedding được ứng dụng rộng rãi trong nhiều tác vụ NLP khác như phân loại văn bản, phân cụm văn bản, dịch máy,...

Trong dự án này, tôi sẽ sử dụng hai công nghệ embedding phổ biến:

  • OpenAI Embeddings: Cung cấp bởi OpenAI, được huấn luyện trên tập dữ liệu văn bản khổng lồ, cho chất lượng embedding rất tốt.
  • Google AI Embeddings: Cung cấp bởi Google, cũng được huấn luyện trên tập dữ liệu lớn, mang lại hiệu quả cao trong nhiều tác vụ NLP.

Việc lựa chọn công nghệ embedding phù hợp phụ thuộc vào yêu cầu cụ thể của dự án, cũng như nguồn lực tính toán và ngân sách.

Trong các phần tiếp theo, chúng ta sẽ cùng tìm hiểu cách xây dựng kho dữ liệu với từng công nghệ embedding cụ thể.

3.2. Xây dựng kho dữ liệu với OpenAI Embeddings

Để xây dựng kho dữ liệu với OpenAI Embeddings, chúng ta sẽ sử dụng thư viện langchain_openai kết hợp với FAISS.

FAISS (Facebook AI Similarity Search) là một thư viện mã nguồn mở được phát triển bởi Meta AI, cho phép tìm kiếm gần nhất (nearest neighbor search) hiệu quả trên tập dữ liệu lớn. FAISS cung cấp nhiều thuật toán và cấu trúc dữ liệu được tối ưu hóa cho việc tìm kiếm vector, giúp tăng tốc độ tìm kiếm đáng kể so với các phương pháp truyền thống.

Các bước thực hiện:

  1. Cài đặt thư viện:
pip install langchain_openai langchain_community faiss-cpu
  1. Tạo vector embedding:
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

# Khởi tạo đối tượng OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# Tạo vector embedding cho từng đoạn văn bản
db = FAISS.from_texts(documents, embeddings)

Trong đoạn code trên:

  • Chúng ta khởi tạo đối tượng OpenAIEmbeddings với model text-embedding-3-large.
  • Hàm FAISS.from_texts() sẽ tự động tạo vector embedding cho từng đoạn văn bản trong documents sử dụng OpenAIEmbeddings và xây dựng index sử dụng FAISS.
  1. Lưu trữ kho dữ liệu:
# Lưu kho dữ liệu vào thư mục "openai_index"
db.save_local("openai_index\\products_index")

Đoạn code này sẽ lưu kho dữ liệu FAISS vào thư mục openai_index với tên file là products_index.

Như vậy, chúng ta đã hoàn thành việc xây dựng và lưu trữ kho dữ liệu với OpenAI Embeddings. Bước tiếp theo, chúng ta sẽ tìm hiểu cách xây dựng kho dữ liệu với Google AI Embeddings.

3.3. Xây dựng kho dữ liệu với Google AI Embeddings

Tương tự như OpenAI Embeddings, chúng ta cũng sử dụng langchain_google_genai kết hợp với FAISS để xây dựng kho dữ liệu với Google AI Embeddings.

Các bước thực hiện:

  1. Cài đặt thư viện:
pip install langchain_google_genai langchain_community faiss-cpu google-generativeai
  1. Cấu hình Google Generative AI:
import google.generativeai as genai
import os

# Thiết lập API key cho Google Generative AI
genai.configure(api_key="YOUR_GOOGLE_API_KEY")
os.environ["GOOGLE_API_KEY"] = "YOUR_GOOGLE_API_KEY"

Hãy chắc chắn bạn đã thay thế YOUR_GOOGLE_API_KEY bằng API key của bạn.

  1. Tạo vector embedding:
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS

# Khởi tạo đối tượng GoogleGenerativeAIEmbeddings
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")

# Tạo vector embedding cho từng đoạn văn bản
db = FAISS.from_texts(documents, embeddings)

Trong đoạn code trên:

  • Chúng ta khởi tạo đối tượng GoogleGenerativeAIEmbeddings với model models/embedding-001.
  • Tương tự như OpenAI, hàm FAISS.from_texts() sẽ tự động tạo vector embedding cho từng đoạn văn bản trong documents sử dụng GoogleGenerativeAIEmbeddings và xây dựng index sử dụng FAISS.
  1. Lưu trữ kho dữ liệu:
# Lưu kho dữ liệu vào thư mục "googleai_index"
db.save_local("googleai_index\\employee_index")

Đoạn code này sẽ lưu kho dữ liệu FAISS vào thư mục googleai_index với tên file là employee_index.

Như vậy, chúng ta đã hoàn thành việc xây dựng và lưu trữ kho dữ liệu với cả OpenAI Embeddings và Google AI Embeddings. Bước tiếp theo, chúng ta sẽ tìm hiểu cách tích hợp function calling vào hệ thống RAG với các mô hình ngôn ngữ phổ biến.

IV. Xây dựng hệ thống RAG tích hợp Function Calling

4.1. Giới thiệu về Function Calling (Introduction to Function Calling)

Như đã đề cập trong bài viết trước, Function Calling là một kỹ thuật mạnh mẽ cho phép mô hình ngôn ngữ tương tác với thế giới bên ngoài bằng cách gọi các hàm được định nghĩa sẵn. Thay vì chỉ đơn thuần là tạo ra văn bản, mô hình ngôn ngữ có thể thực hiện các hành động cụ thể, truy xuất thông tin từ cơ sở dữ liệu, hoặc tương tác với các API khác.

Trong bối cảnh của hệ thống RAG, Function Calling mở ra một hướng đi mới cho phép:

  • Truy xuất thông tin động: Thay vì chỉ tìm kiếm thông tin tĩnh trong kho dữ liệu, hệ thống có thể gọi các hàm để truy xuất thông tin động, cập nhật theo thời gian thực.
  • Thực hiện các tác vụ phức tạp: Hệ thống có thể kết hợp nhiều function calls để thực hiện các tác vụ phức tạp hơn, ví dụ như đặt vé máy bay, đặt món ăn, hoặc quản lý lịch hẹn.
  • Tăng tính linh hoạt và khả năng mở rộng: Việc tích hợp Function Calling giúp hệ thống RAG trở nên linh hoạt và dễ dàng mở rộng hơn trong tương lai.

Trong bài viết này, chúng ta sẽ cùng tìm hiểu cách tích hợp Function Calling vào hệ thống RAG với ba mô hình ngôn ngữ phổ biến:

  • GPT-4o (OpenAI API): Mô hình ngôn ngữ mạnh mẽ được cung cấp bởi OpenAI thông qua API.
  • Gemini 1.5 Flash (Google Generative AI API): Mô hình ngôn ngữ tiên tiến được cung cấp bởi Google thông qua API.
  • Vistral 7B (Fine-tuned): Mô hình ngôn ngữ mã nguồn mở được tinh chỉnh (fine-tuned) để hỗ trợ Function Calling.

Việc lựa chọn mô hình ngôn ngữ phù hợp phụ thuộc vào yêu cầu cụ thể của dự án, cũng như nguồn lực tính toán và ngân sách.

4.2. Xây dựng hệ thống RAG với GPT-4o

Phần này sẽ hướng dẫn chi tiết cách xây dựng hệ thống RAG tích hợp function calling với GPT-4o thông qua OpenAI API.

Bước 1: Chuẩn bị môi trường và thư viện

Đầu tiên, bạn cần cài đặt thư viện openailangchain_openai:

pip install openai langchain_openai

Sau đó, cấu hình API key cho OpenAI:

import os
os.environ['OPENAI_API_KEY'] = "YOUR_API_KEY"

Bước 2: Định nghĩa các hàm (functions)

Mỗi function sẽ đóng vai trò như một công cụ truy xuất thông tin từ nguồn dữ liệu cụ thể. Ví dụ, hàm about_employee sẽ tìm kiếm thông tin về nhân viên trong cơ sở dữ liệu tương ứng:

def about_employee(info):
    """
    Cung cấp thông tin về các thành viên ban quản lý và nhân viên của công ty
    
    Args:
        info (str): Thông tin cần tìm kiếm về nhân viên
    
    Returns:
        str: Chuỗi JSON chứa thông tin về nhân viên
    """
    
    # Load cơ sở dữ liệu thông tin nhân viên
    db = FAISS.load_local("openai_index\employee_index", OpenAIEmbeddings(model="text-embedding-3-large"), allow_dangerous_deserialization = True)
    
    # Tìm kiếm thông tin tương đồng trong cơ sở dữ liệu
    results = db.similarity_search(info, k=1) 
    
    # Chuyển đổi kết quả thành dạng JSON
    docs = [{"content": doc.page_content} for doc in results]
    docs_string = json.dumps(docs, ensure_ascii=False)

    return docs_string

Trong đoạn code trên:

  • Đầu tiên, hàm about_employee nhận đầu vào là info (chuỗi truy vấn).
  • Tiếp theo, nó tải cơ sở dữ liệu employee_index đã được xây dựng trước đó bằng FAISS.
  • Sau đó, hàm sử dụng db.similarity_search để tìm kiếm thông tin tương đồng với info trong cơ sở dữ liệu.
  • Cuối cùng, kết quả tìm kiếm được chuyển đổi sang định dạng JSON và trả về.

Tương tự, bạn có thể định nghĩa các hàm khác như about_productsreviews_search để truy xuất thông tin từ các nguồn dữ liệu khác.

Bước 3: Khai báo các công cụ (tools) cho GPT-4o

GPT-4O cần biết về các hàm mà nó có thể sử dụng. Chúng ta sẽ khai báo các hàm này dưới dạng "tools" với mô tả chi tiết về chức năng và tham số đầu vào:

tools = [
    {
        "type": "function",
        "function": {
            "name": "about_employee",
            "description": "Cung cấp những tài liệu liên quan đến người quản lý/nhân viên của công ty mà bạn cần biết.",
            "parameters": {
                "type": "object",
                "properties": {
                    "info": {
                        "type": "string",
                        "description": "Thông tin mà bạn cần tìm kiếm, e.g. Email của himmeow the coder.",
                    },
                },
                "required": ["info"],
            },
        },
    },
    # ... Khai báo tương tự cho about_products và reviews_search
]

Mỗi "tool" bao gồm:

  • type: Loại công cụ, ở đây là "function".
  • function: Thông tin chi tiết về hàm:
    • name: Tên hàm.
    • description: Mô tả chức năng của hàm.
    • parameters: Định nghĩa các tham số đầu vào của hàm, bao gồm kiểu dữ liệu, mô tả và các trường bắt buộc.

Bước 4: Gửi yêu cầu trò chuyện và xử lý phản hồi

Hàm chat_completion_request sẽ gửi yêu cầu trò chuyện đến OpenAI API và xử lý phản hồi:

def chat_completion_request(messages, functions=None, model="gpt-4o"):
    """
    Gửi yêu cầu trò chuyện đến OpenAI API và xử lý phản hồi.
    
    Args:
        messages (list): Danh sách tin nhắn trong cuộc trò chuyện.
        functions (list): Danh sách các công cụ có sẵn cho trợ lý ảo.
        model (str): Mô hình ngôn ngữ được sử dụng cho chatbot.
    
    Returns:
        str: Phản hồi từ chatbot hoặc thông báo lỗi.
    """
    
    try:
        # Gửi yêu cầu trò chuyện
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=functions,
            tool_choice="auto", 
            temperature=0,
        )

        # Lấy phản hồi từ OpenAI API
        response_message = response.choices[0].message
        tool_calls = response_message.tool_calls

        # Nếu có yêu cầu sử dụng công cụ
        if tool_calls:
            # ... Xử lý yêu cầu sử dụng công cụ
            
        # Nếu không có yêu cầu sử dụng công cụ, trả về phản hồi trực tiếp
        else:
            msg = response_message.content
            return msg
        
    except Exception as e:
        # ... Xử lý lỗi

Trong đoạn code trên:

  • Đầu tiên, yêu cầu trò chuyện được gửi đến OpenAI API với model, messages, tools và các tham số khác.
  • Tiếp theo, phản hồi từ API được kiểm tra xem có yêu cầu sử dụng công cụ (tool_calls) hay không.
  • Nếu có, chương trình sẽ xử lý từng yêu cầu bằng cách gọi hàm tương ứng và gửi lại yêu cầu trò chuyện mới với thông tin bổ sung từ công cụ.
  • Nếu không, phản hồi trực tiếp từ GPT-4o sẽ được trả về.

Bước 5: Xử lý yêu cầu sử dụng công cụ

Dưới đay là quá trình xử lý logic khi GPT-4o quyết định sử dụng một trong các hàm đã khai báo:

        # ... (Tiếp tục từ đoạn code trước)
        if tool_calls:
            # Thêm phản hồi của chatbot vào bộ nhớ
            messages.append(response_message)

            # Xử lý từng yêu cầu sử dụng công cụ
            for tool_call in tool_calls:
                function_name = tool_call.function.name
                
                # Gọi hàm tương ứng với tên công cụ được yêu cầu
                if function_name in available_functions:
                    function_to_call = available_functions[function_name]
                    function_args = json.loads(tool_call.function.arguments)
                    function_response = function_to_call(function_args.get("info"))
                    
                    # Thêm kết quả của công cụ vào bộ nhớ
                    messages.append(
                        {
                            "tool_call_id": tool_call.id,
                            "role": "tool",
                            "name": function_name,
                            "content": function_response,
                        }
                    )
                
            # Gửi yêu cầu trò chuyện mới với thông tin bổ sung từ công cụ
            return chat_completion_request(messages=messages, functions=functions)

Cụ thể:

  • Đầu tiên, phản hồi của GPT-4o được thêm vào bộ nhớ (messages).
  • Sau đó, chương trình duyệt qua từng yêu cầu sử dụng công cụ (tool_call).
  • Tên hàm được trích xuất từ tool_call và kiểm tra xem có tồn tại trong available_functions hay không.
  • Nếu có, hàm tương ứng được gọi với tham số được trích xuất từ tool_call.
  • Kết quả của hàm được thêm vào bộ nhớ (messages) với vai trò là "tool".
  • Cuối cùng, một yêu cầu trò chuyện mới được gửi đi với thông tin bổ sung từ công cụ.

Kết quả:

Ví dụ 1:

User: Xin chào!

Chatbot: Xin chào! Tôi có thể giúp gì cho bạn hôm nay?

Ví dụ 2:

User: Ở bạn có bán tai nghe không?

Chatbot: Chúng tôi có bán tai nghe Bluetooth True Wireless với các thông tin sau:

### Thông tin cơ bản:
- **Tên sản phẩm:** Tai nghe Bluetooth True Wireless
- **Thương hiệu:** SoundWave
- **Nhà sản xuất:** Công ty sản xuất thiết bị âm thanh
- **Mã sản phẩm:** TNB008
- **Loại sản phẩm:** Tai nghe Bluetooth, Thiết bị âm thanh
- **Mô tả ngắn:** Tai nghe Bluetooth true wireless kết nối ổn định, âm thanh chất lượng cao.

### Thông tin bổ sung:
- **Giá bán:** 700.000 VNĐ
- **Tình trạng tồn kho:** Còn hàng
- **Khuyến mãi:** Giảm 10% khi mua online
- **Đánh giá của khách hàng:** 4.8/5 sao
  - Chất lượng âm thanh tốt.
  - Kết nối ổn định.
  - Thời lượng pin lâu.

### Thông tin chi tiết:
- **Công nghệ Bluetooth:** Bluetooth 5.0, kết nối ổn định trong phạm vi 10m.
- **Dung lượng pin:** 5 giờ nghe nhạc liên tục, hộp sạc cung cấp thêm 20 giờ sử dụng.
- **Chức năng:** Chống ồn, chống nước IPX4, điều khiển cảm ứng.
- **Màu sắc:** Đen, trắng, xanh.
- **Hướng dẫn sử dụng:** Sạc đầy pin trước khi sử dụng lần đầu.

Nếu bạn cần thêm thông tin hoặc muốn đặt hàng, hãy cho tôi biết nhé!

Bằng cách kết hợp các bước trên, ta đã xây dựng thành công một hệ thống RAG tích hợp function calling với GPT-4o. Hệ thống này cho phép GPT-4o truy xuất thông tin từ các nguồn dữ liệu bên ngoài thông qua các hàm được định nghĩa trước, từ đó cung cấp câu trả lời chính xác và đầy đủ hơn cho người dùng.

4.3. Xây dựng hệ thống RAG với Gemini 1.5 Flash

Gemini 1.5 Flash sở hữu khả năng tích hợp function calling ngay từ khâu khai báo mô hình, giúp đơn giản hóa việc xây dựng hệ thống RAG, tương tự GPT-4o, quy trình xây dựng hệ thống RAG với Gemini 1.5 Flash bao gồm các bước sau:

Bước 1: Chuẩn bị môi trường và thư viện

Cài đặt thư viện google-generativeailangchain_google_genai:

pip install google-generativeai langchain_google_genai

Cấu hình API key cho Google Generative AI:

import os
import google.generativeai as genai

genai.configure(api_key="YOUR_API_KEY")
os.environ["GOOGLE_API_KEY"] = "YOUR_API_KEY"

Bước 2: Định nghĩa các hàm (functions)

Tương tự như GPT-4o, mỗi function đóng vai trò như một công cụ truy xuất thông tin. Điểm khác biệt ở đây là Gemini có khả năng tự động hiểu được chức năng và cách sử dụng của hàm thông qua docstring. Ví dụ, hàm about_employee được khai báo như sau:

def about_employee(info: str) -> str:
    """
    Cung cấp thông tin về các thành viên ban quản lý và nhân viên của công ty.

    Hàm này tìm kiếm thông tin về nhân viên trong cơ sở dữ liệu dựa trên thông tin đầu vào.

    Args:
        info: Thông tin cần tìm kiếm về nhân viên. 
             Ví dụ: 
                - "Email của himmeow the coder"

    Returns:
        Chuỗi JSON chứa thông tin về nhân viên tìm thấy, hoặc chuỗi rỗng nếu không tìm thấy.
    """
    # ... (Phần code xử lý tương tự như GPT-4o)

Docstring của hàm bao gồm:

  • Mô tả ngắn gọn chức năng của hàm.
  • Phần Args: Mô tả chi tiết từng tham số đầu vào, bao gồm kiểu dữ liệu, ý nghĩa và ví dụ cụ thể.
  • Phần Returns: Mô tả kiểu dữ liệu trả về và ý nghĩa của nó.

Gemini sẽ sử dụng thông tin này để tự động hiểu cách sử dụng hàm about_employee mà không cần khai báo thêm "tools" như GPT-4O.

Bước 3: Khởi tạo mô hình Gemini và chatbot

Khởi tạo mô hình Gemini 1.5 Flash với danh sách các hàm đã khai báo:

# Danh sách các hàm (công cụ) có sẵn cho chatbot
tools = [about_employee, about_products, reviews_search]

# Khởi tạo model Google Generative AI với tên model và danh sách công cụ
model = genai.GenerativeModel(model_name="gemini-1.5-flash", tools=tools)

Tạo chatbot với system message để cấu hình chatbot:

history=[
    {
      "role": "user",
      "parts": [
        """Bạn là một trợ lý ảo thông minh, làm việc cho một công ty bán hàng hóa. Bạn có khả năng truy xuất thông tin từ cơ sở dữ liệu để trả lời câu hỏi của người dùng một cách chính xác và hiệu quả.

        Hãy nhớ: 
            - Luôn luôn sử dụng các công cụ được cung cấp để tìm kiếm thông tin trước khi đưa ra câu trả lời.
            - Trả lời một cách ngắn gọn, dễ hiểu.
            - Không tự ý bịa đặt thông tin.
        """,
      ],
    },
]

chat = model.start_chat(history=history)

Bước 4: Gửi yêu cầu trò chuyện và xử lý phản hồi

Gửi yêu cầu trò chuyện đến chatbot và nhận phản hồi:

while True:
    user_input = input("User: ")
    # ... (Kiểm tra điều kiện thoát)
    
    response = chat.send_message(user_input)
    # ... (Xử lý phản hồi)

Bước 5: Xử lý kết quả trả về từ function call

Gemini sẽ tự động gọi hàm tương ứng nếu xác định được yêu cầu của người dùng. Kết quả trả về từ hàm sẽ được chứa trong response.parts:

    # ... (Tiếp tục từ đoạn code trước)
    responses = {}

    for part in response.parts:
        if fn := part.function_call:
            # ... (Lấy thông tin về function call)
            function_response = function_to_call(function_args)
            responses[function_name] = function_response

    if responses:
        response_parts = [
            genai.protos.Part(function_response=genai.protos.FunctionResponse(name=fn, response={"result": val}))
            for fn, val in responses.items()
        ]
        response = chat.send_message(response_parts)

    print("Chatbot:", response.text)

Đoạn code trên thực hiện:

  • Duyệt qua từng phần tử trong response.parts.
  • Kiểm tra xem phần tử có chứa function_call hay không.
  • Nếu có, lấy thông tin về function call (tên hàm, tham số) và gọi hàm tương ứng.
  • Lưu kết quả trả về từ hàm vào dictionary responses.
  • Gửi lại responses cho chatbot để chatbot có thể sử dụng kết quả từ function call trong câu trả lời cuối cùng.

Kết quả:

User: người dùng thường nhận xét về Tai nghe Bluetooth True Wireless như thế nào?
Chatbot: Người dùng thường đánh giá tai nghe Bluetooth True Wireless là có chất lượng âm thanh tốt, pin trâu, thiết kế đẹp mắt và dễ sử dụng. 

Tương tự như GPT-4o, Gemini 1.5 Flash cũng cho phép xây dựng hệ thống RAG hiệu quả. Điểm khác biệt là Gemini có khả năng tự động hiểu và sử dụng các hàm được khai báo, giúp đơn giản hóa code và quy trình phát triển.

4.4. Xây dựng hệ thống RAG với ViSTRAL 7B (Fine-tuned)

Khác biệt với GPT-4O và Gemini 1.5 Flash, ViSTRAL 7B là mô hình mã nguồn mở được fine-tuned bởi anh Hiếu Ngô, tích hợp phương thức function calling từ mô hình cơ sở Viet-Mistral/Vistral-7B-Chat. Việc sử dụng Vistral 7B cho phép tùy biến và kiểm soát hệ thống RAG một cách linh hoạt hơn.

Bước 1: Chuẩn bị môi trường và tải model

Cài đặt các thư viện cần thiết:

!pip install transformers torch accelerate
!pip install langchain_openai langchain_community
!pip install faiss-cpu
!pip install gdown -U --no-cache-dir

Tải dữ liệu và model từ Google Drive:

import gdown

url = "https://drive.google.com/drive/folders/1K13Lo8sX5iRXJ-rNsiwB-cuHTTz3NdP5?usp=drive_link"
gdown.download_folder(url)

# ... (Thiết lập API key cho OpenAI)

from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

tokenizer = AutoTokenizer.from_pretrained('hiieu/Vistral-7B-Chat-function-calling')
model = AutoModelForCausalLM.from_pretrained(
    'hiieu/Vistral-7B-Chat-function-calling',
    torch_dtype=torch.bfloat16,
    device_map="auto",
    use_cache=True,
)

Bước 2: Xây dựng và lưu trữ cơ sở dữ liệu vector

Sử dụng OpenAIEmbeddings để tạo vector embedding cho dữ liệu và lưu trữ bằng FAISS:

# ... (Định nghĩa class LineTextSplitter)

# Xử lý dữ liệu thông tin nhân viên
with open('/content/data/employee_info.txt', 'r', encoding='utf-8') as f:
    # ... (Đọc dữ liệu, chia đoạn, tạo cơ sở dữ liệu, lưu trữ)

# Tương tự cho dữ liệu sản phẩm và đánh giá

Bước 3: Định nghĩa các hàm truy vấn thông tin

Tương tự như hai mô hình trước, định nghĩa các hàm about_employee, about_productsreviews_search để truy xuất thông tin từ cơ sở dữ liệu tương ứng.

Bước 4: Định nghĩa metadata cho các hàm

Khác với Gemini, Vistral 7B (Fine-tuned) vẫn cần metadata để hiểu rõ chức năng của từng hàm:

functions_metadata = [
    # ... (Định nghĩa metadata cho about_employee, about_products, reviews_search)
]

Bước 5: Khởi tạo bộ nhớ và định nghĩa hàm ask_AI

Khởi tạo bộ nhớ cho chatbot và định nghĩa hàm ask_AI để xử lý yêu cầu của người dùng:

messages = [
    {"role": "system", "content": f"""Bạn là một trợ lý hữu ích ..., sử dụng chúng nếu cần -\n{str(functions_metadata)} ...""" },
]

def ask_AI(query):
    # ... (Thêm câu hỏi vào bộ nhớ)

    # Mã hóa văn bản và sinh text từ model
    input_ids = tokenizer.apply_chat_template(
        # ...
    )
    outputs = model.generate(
        # ...
    )
    response = tokenizer.decode(outputs[0][input_ids.shape[-1]:], skip_special_tokens=True)

    # ... (Xử lý function call và gọi lại model nếu cần)

    # ... (Thêm phản hồi vào bộ nhớ)

Hàm ask_AI thực hiện các bước:

  • Thêm câu hỏi của người dùng vào bộ nhớ.
  • Mã hóa văn bản và sinh text từ model Vitral 7B.
  • Tìm kiếm function call trong phản hồi.
  • Nếu tìm thấy, gọi hàm tương ứng, thêm kết quả vào bộ nhớ và gọi lại model để nhận câu trả lời cuối cùng.
  • Nếu không, thêm phản hồi vào bộ nhớ và trả về.

Bước 6: Sử dụng chatbot

Gọi hàm ask_AI để bắt đầu trò chuyện với chatbot:

res = ask_AI("xin chào")
print(res)

Kết quả:

res = ask_AI("Cửa hàng của bạn có sản phẩm nào của thương hiệu SoundWave không?")
res
 Có, cửa hàng của chúng tôi có sản phẩm của thương hiệu SoundWave. Một trong số đó là tai nghe Bluetooth True Wireless. Nó cung cấp kết nối ổn định và âm thanh chất lượng cao. 

Vistral 7B (Fine-tuned) cung cấp cho bạn khả năng kiểm soát và tùy biến cao hơn so với các mô hình đóng như GPT-4O và Gemini. Bạn có thể tinh chỉnh mô hình trên tập dữ liệu riêng, thay đổi kiến trúc hoặc tích hợp các kỹ thuật mới để tối ưu hóa hiệu suất của hệ thống RAG.

V. Kết luận (Conclusion)

Bài viết đã giới thiệu một cách chi tiết quy trình xây dựng hệ thống RAG tích hợp function calling, từ khâu thu thập và xử lý dữ liệu, xây dựng kho dữ liệu với công nghệ embedding, đến việc tích hợp function calling với các mô hình ngôn ngữ phổ biến như GPT-4o, Gemini 1.5 Flash và Vistral 7B.

Qua bài viết, chúng ta đã học được:

  • Cách xác định nguồn dữ liệu phù hợp với nhu cầu của hệ thống RAG.
  • Các phương pháp thu thập và xử lý dữ liệu hiệu quả, bao gồm cả việc chia văn bản thành các đoạn nhỏ.
  • Xây dựng kho dữ liệu vector với OpenAI Embeddings và Google AI Embeddings bằng cách sử dụng thư viện FAISS.
  • Tích hợp function calling vào hệ thống RAG với cả mô hình sử dụng API (GPT-4o và Gemini 1.5 Flash) và mô hình mã nguồn mở (Vistral 7B).

Tuy nhiên, việc xây dựng một hệ thống RAG hoàn chỉnh vẫn còn nhiều thách thức:

  • Khả năng mở rộng: Hệ thống cần được thiết kế để có thể xử lý lượng dữ liệu khổng lồ và mở rộng dễ dàng theo thời gian.
  • Hiệu suất: Cần tối ưu hóa hiệu suất tìm kiếm, xử lý ngôn ngữ và function calling để hệ thống hoạt động hiệu quả.
  • Sự an toàn và bảo mật: Bảo mật dữ liệu và tránh những nguy cơ tiềm ẩn từ việc sử dụng các mô hình ngôn ngữ.

Trong tương lai, các kỹ thuật mới như Semantic Search, Retrieval-Augmented Question Answering (RAQA) và Zero-Shot Learning sẽ được nghiên cứu và ứng dụng vào hệ thống RAG, giúp tăng cường khả năng hiểu biết và xử lý thông tin của hệ thống. Việc cải tiến hệ thống bằng những kỹ thuật nói trên, tích hợp học tăng cường (reinforcement learning) và fine tuning cho các tác vụ phức tạp sẽ được tôi chia sẻ trong các bài viết tới.

Bạn có thể tìm thấy code ví dụ minh họa cho từng phần trong bài viết tại himmeow's GitHub repository.

Nếu bạn có bất kỳ câu hỏi hay thắc mắc nào, hãy tham gia server Discord hoặc liên hệ trực tiếp với tôi qua email: himmeow.thecoder@gmail.com.

Sau cùng, tôi xin cảm ơn sự ủng hộ và giúp đỡ từ anh Văn Toàn Phạm và anh Hiếu Ngô trong suốt quá trình thực hiện bài viết này.

Chúc bạn thành công!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.