Function calling: Lời giải cho hệ thống RAG linh hoạt và hiệu quả
1. Giới thiệu
Vào ngày 8/6/2024 vừa qua, sự kiện TechUp #2 do Sun* tổ chức đã diễn ra thành công tốt đẹp, thu hút sự quan tâm của đông đảo cộng đồng yêu công nghệ. Sự kiện tập trung khai thác hai chủ đề nóng hổi là ứng dụng trí tuệ nhân tạo và xây dựng hệ thống RAG - Retrieval Augmented Generation. Tại đây, bên cạnh việc được lắng nghe những chia sẻ giá trị từ các chuyên gia đầu ngành, tôi còn có cơ hội giao lưu và học hỏi từ kinh nghiệm thực tế của những người tham gia khác.
Một trong những chia sẻ khiến tôi đặc biệt chú ý đến từ anh Dũng. Anh ấy đã gặp phải một vấn đề khá phổ biến khi xây dựng hệ thống RAG, đó là chatbot của anh ấy chỉ trả lời tốt một số câu hỏi với số lượng tài liệu truy xuất được cài đặt trước. Ví dụ, với câu lệnh results = db.similarity_search(info, k=5)
, chatbot hoạt động hiệu quả với một số dạng câu hỏi nhưng lại đòi hỏi số lượng tài liệu k
khác nhau đối với các trường hợp còn lại.
Theo chia sẻ từ các chuyên gia tham gia sự kiện, có rất nhiều phương pháp để giải quyết vấn đề này, bao gồm:
- Điều chỉnh động số lượng tài liệu truy xuất (k): Thay vì sử dụng một giá trị k cố định, ta có thể thiết lập một cơ chế để tự động điều chỉnh k dựa trên độ phức tạp của câu hỏi hoặc độ tự tin của mô hình.
- Sử dụng kỹ thuật Reranking: Áp dụng các mô hình học máy bổ sung để đánh giá và xếp hạng lại kết quả tìm kiếm ban đầu, từ đó lựa chọn ra những tài liệu phù hợp nhất.
- Phân nhóm câu hỏi: Phân loại các câu hỏi thành các nhóm khác nhau dựa trên đặc điểm và mục đích của chúng. Từ đó, ta có thể thiết lập các tham số truy vấn tối ưu cho từng nhóm.
Bản thân tôi cũng đã từng đối mặt với vấn đề tương tự trong quá trình phát triển các hệ thống RAG, đặc biệt là trong các dự án sử dụng lượng dữ liệu lớn và đa đối tượng, điển hình là dự án xây dựng trợ lý ảo thông minh cho trường Đại học Công nghệ UET. Phương pháp mà tôi thấy hiệu quả và ứng dụng trong dự án này là tích hợp phương thức function calling.
Trong bài viết này, tôi sẽ chia sẻ kinh nghiệm của mình về cách thức ứng dụng phương pháp function calling để tối ưu hóa hệ thống RAG, giúp chatbot có khả năng tự động lựa chọn nguồn dữ liệu phù hợp và 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ơn.
2. Vấn đề thường gặp khi xây dựng hệ thống RAG
Hệ thống Retrieval Augmented Generation (RAG) đang ngày càng trở nên phổ biến trong lĩnh vực xử lý ngôn ngữ tự nhiên nhờ khả năng kết hợp giữa kiến thức nền tảng rộng lớn của mô hình ngôn ngữ với thông tin cụ thể được truy xuất từ tập dữ liệu. Tuy nhiên, việc xây dựng một hệ thống RAG hiệu quả không phải là điều dễ dàng.
Như anh Phạm Văn Toàn đã chia sẻ trong bài viết Bí kíp võ công thượng thừa giúp cải thiện ứng dụng Retrieval Augmented Generation (RAG) trên Viblo, có rất nhiều yếu tố cần được xem xét và tối ưu hóa để đảm bảo hệ thống RAG hoạt động như mong đợi.
Một trong những vấn đề phổ biến mà các nhà phát triển thường gặp phải là việc chatbot hoạt động không nhất quán với các truy vấn khác nhau. Cụ thể, chatbot có thể đưa ra câu trả lời chính xác cho một số câu hỏi với số lượng tài liệu truy xuất được cài đặt trước, nhưng lại yêu cầu số lượng tài liệu khác nhau đối với các trường hợp còn lại.
Ví dụ, với câu lệnh results = db.similarity_search(info, k=5)
, chatbot có thể xử lý tốt các câu hỏi đơn giản hoặc yêu cầu thông tin chung. Tuy nhiên, khi người dùng đặt câu hỏi phức tạp hơn, yêu cầu phân tích sâu hoặc tổng hợp thông tin từ nhiều nguồn khác nhau, việc giới hạn số lượng tài liệu truy xuất ở mức 5 có thể dẫn đến việc chatbot không thu thập đủ thông tin để đưa ra câu trả lời chính xác và đầy đủ.
Vấn đề này phát sinh do hệ thống RAG truyền thống thường sử dụng một giá trị cố định cho số lượng tài liệu cần truy xuất, mà chưa tính đến sự khác biệt về độ phức tạp và yêu cầu thông tin của từng truy vấn. Điều này dẫn đến việc chatbot hoạt động không hiệu quả và ảnh hưởng đến trải nghiệm của người dùng.
3. Giải pháp: Tích hợp phương thức function calling
Để giải quyết vấn đề về số lượng tài liệu truy xuất cố định trong hệ thống RAG truyền thống, tôi xin đề xuất giải pháp tích hợp phương thức function calling. Phương pháp này mang lại khả năng linh hoạt và hiệu quả hơn trong việc lựa chọn nguồn dữ liệu phù hợp với từng truy vấn cụ thể.
Function calling là một tính năng nổi bật của các mô hình ngôn ngữ lớn hiện đại, cho phép chúng tương tác với các hàm và API bên ngoài. Thay vì chỉ dựa vào kiến thức được huấn luyện trước, mô hình có thể gọi các hàm chuyên dụng để truy xuất thông tin, thực hiện tính toán hoặc thực thi các hành động cụ thể.
Một số mô hình ngôn ngữ hỗ trợ function calling bao gồm:
- GPT-3.5 Turbo, GPT-4 (OpenAI)
- Gemini 1.0 và 1.5 Pro (Google AI)
- Llama 3 (Meta) - có thể được tích hợp thông qua kỹ thuật fine-tuning.
Lý do tôi lựa chọn phương pháp này cho dự án xây dựng trợ lý ảo thông minh cho trường Đại học Công nghệ UET là do tính linh hoạt và khả năng mở rộng của nó.
Lợi ích của việc tích hợp function calling:
- Tối ưu hóa quá trình truy xuất: Mô hình có thể tự động xác định xem có cần truy xuất dữ liệu hay không dựa trên yêu cầu của truy vấn.
- Linh hoạt trong việc lựa chọn nguồn dữ liệu: Thay vì sử dụng một nguồn dữ liệu duy nhất, mô hình có thể gọi các hàm truy xuất khác nhau, tương ứng với các nguồn dữ liệu và định dạng dữ liệu khác nhau.
- Nâng cao khả năng xử lý câu hỏi phức tạp: Mô hình có thể kết hợp thông tin từ nhiều nguồn dữ liệu khác nhau để trả lời các câu hỏi yêu cầu phân tích sâu hoặc tổng hợp kiến thức.
- Kiểm soát tốt hơn luồng thông tin: Việc sử dụng function calling cho phép kiểm soát chặt chẽ hơn cách thức mô hình truy cập và sử dụng thông tin.
Với function calling, hệ thống RAG trở nên linh hoạt và thông minh hơn, có khả năng thích ứng với nhiều loại truy vấn và tình huống khác nhau.
4. Các bước triển khai
Để triển khai hệ thống RAG tích hợp phương thức function calling cho dự án UET AI, tôi đã thực hiện theo các bước sau:
4.1. Thu thập và xử lý dữ liệu
Bước đầu tiên là thu thập dữ liệu từ các nguồn khác nhau của trường Đại học Công nghệ, bao gồm website, tài liệu nội bộ, cơ sở dữ liệu, ... Sau đó, dữ liệu được xử lý và làm sạch để đảm bảo tính nhất quán và loại bỏ thông tin nhiễu.
(Chi tiết về quá trình thu thập và xử lý dữ liệu sẽ được tôi trình bày rõ hơn trong một bài viết khác.)
4.2. Chia dữ liệu và lưu trữ index
Tiếp theo, tôi tiến hành phân loại và chia dữ liệu đã thu thập thành các thư mục khác nhau dựa trên chủ đề và mục đích sử dụng. Ví dụ:
- uet_info: Chứa thông tin cơ bản về trường Đại học Công nghệ, như lịch sử hình thành, cơ cấu tổ chức, thông tin liên lạc, ...
- programs: Chứa thông tin chi tiết về các chương trình đào tạo của trường, bao gồm chương trình đại học, sau đại học, chương trình quốc tế, ...
Mỗi thư mục dữ liệu sau đó được chuyển đổi thành một index riêng biệt sử dụng thư viện FAISS (Facebook AI Similarity Search) của Meta và model embedding "text-embedding-3-large" của OpenAI.
Ví dụ, đoạn mã sau đây minh họa cách tạo index cho thư mục "uet_info":
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
db = FAISS.from_documents(documents, OpenAIEmbeddings(model="text-embedding-3-large"))
db.save_local("uet_index")
Việc tạo index riêng biệt cho từng chủ đề giúp tối ưu hóa quá trình tìm kiếm và truy xuất thông tin, đồng thời cho phép mô hình dễ dàng lựa chọn nguồn dữ liệu phù hợp với từng truy vấn.
4.3. Tạo phương thức truy xuất dữ liệu
Với mỗi index đã tạo, tôi xây dựng một phương thức truy xuất dữ liệu cơ bản. Phương thức này có thể được tùy chỉnh để đáp ứng các yêu cầu cụ thể của từng ứng dụng.
Ví dụ, với index "uet_info", tôi tạo phương thức about_uet()
như sau:
def about_uet(info):
db = FAISS.load_local("uet_index",...)
results = db.similarity_search(info, k=5)
return results
Phương thức này nhận đầu vào là một chuỗi văn bản info
đại diện cho thông tin mà người dùng muốn tìm kiếm về trường Đại học Công nghệ. Sau đó, nó sử dụng phương thức similarity_search()
của FAISS để tìm kiếm 5 tài liệu liên quan nhất trong index "uet_info" và trả về kết quả.
4.4. Mô tả công cụ và tích hợp function calling
Để mô hình ngôn ngữ có thể sử dụng các phương thức truy xuất dữ liệu đã tạo, tôi tiến hành mô tả chức năng và cách sử dụng của chúng theo định dạng mà mô hình có thể hiểu được.
Ví dụ, dưới đây là mô tả cho phương thức about_uet()
:
{
"type": "function",
"function": {
"name": "about_uet",
"description": "Cung cấp những tài liệu/trích dẫn liên quan đến thông tin mà bạn tìm kiếm về trường Đại học Công nghệ",
"parameters": {
"type": "object",
"properties": {
"info": {
"type": "string",
"description": "Thông tin/lĩnh vực của trường mà bạn cần tìm kiếm, ví dụ: 'Ngày thành lập trường Đại học Công nghệ'",
},
},
"required": ["info"],
},
},
}
Mô tả này cho mô hình biết rằng about_uet()
là một hàm nhận đầu vào là một chuỗi văn bản info
và trả về các tài liệu liên quan đến thông tin được yêu cầu về trường Đại học Công nghệ.
Thông tin mô tả này sau đó được tích hợp vào quá trình giao tiếp với mô hình ngôn ngữ thông qua function calling. Khi nhận được yêu cầu từ người dùng, mô hình sẽ dựa vào mô tả này để quyết định có nên gọi hàm about_uet()
hay không và truyền tham số đầu vào phù hợp.
4.5. Cơ chế hoạt động của function calling
Sau khi đã định nghĩa các phương thức truy xuất và mô tả chúng cho mô hình, bước tiếp theo là tích hợp chúng vào quy trình xử lý yêu cầu của chatbot.
Dưới đây là đoạn mã minh họa cách thức hoạt động của function calling trong hệ thống UET AI:
import json
from openai import OpenAI
client = OpenAI() # Khởi tạo OpenAI client
def chat_completion_request(messages, functions=None, model="gpt-3.5-turbo-0125"):
try:
response = client.chat.completions.create(
model=model,
messages=messages,
tools=functions,
tool_choice="auto",
temperature=0,
)
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
if tool_calls:
available_functions = {
"about_uet": about_uet,... # Danh sách các function đã định nghĩa
}
messages.append(response_message) # Thêm message của mô hình vào history
for tool_call in tool_calls:
function_name = tool_call.function.name
if function_name == "about_uet":
print("call uet")
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.function.arguments)
function_response = function_to_call(function_args.get("info"))
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
}
)
# Gọi đệ quy để xử lý kết quả trả về từ function
return chat_completion_request(messages = messages, functions = functions)
else:
msg = response_message.content
return msg
Giải thích đoạn mã:
- Khởi tạo OpenAI client: Đầu tiên, ta cần khởi tạo một client để tương tác với API của OpenAI.
- Hàm
chat_completion_request
: Hàm này nhận đầu vào làmessages
(lịch sử cuộc trò chuyện),functions
(danh sách các hàm khả dụng) vàmodel
(mô hình ngôn ngữ sử dụng). - Gọi API
chat.completions.create
: Hàm này gửi yêu cầu đến API của OpenAI để tạo phản hồi cho người dùng, đồng thời cho phép chỉ định các function có thể được sử dụng. - Xử lý kết quả trả về:
- Nếu
tool_calls
khácNone
, tức là mô hình đã quyết định gọi một function:- Xác định function cần gọi dựa trên
function_name
. - Lấy tham số từ
function_args
. - Gọi function và nhận kết quả trả về.
- Thêm kết quả trả về vào
messages
dưới vai trò "tool". - Gọi đệ quy hàm
chat_completion_request
để xử lý tiếp kết quả từ function.
- Xác định function cần gọi dựa trên
- Nếu
tool_calls
làNone
, tức là mô hình không cần gọi function, ta trả về nội dung phản hồi cho người dùng.
- Nếu
Lưu ý: Đoạn mã trên sử dụng đệ quy để xử lý trường hợp mô hình có thể gọi nhiều function liên tiếp.
Việc tích hợp function calling trực tiếp vào quy trình xử lý yêu cầu giúp chatbot trở nên linh hoạt và thông minh hơn, có khả năng tự động lựa chọn và sử dụng các công cụ phù hợp để giải quyết các yêu cầu phức tạp từ phía người dùng.
4.6. Hạn chế sự "ảo giác" của mô hình
Mặc dù mô hình ngôn ngữ lớn có khả năng tạo văn bản ấn tượng, chúng vẫn có thể "ảo giác" và đưa ra thông tin không chính xác hoặc không liên quan. Để hạn chế điều này, tôi đã thêm vào input của mô hình một system_message
như sau:
"IMPORTANT: LUÔN LUÔN PHẢI tìm thông tin trong các tài liệu bằng tools (about_uet,...) được cung cấp trước khi trả lời câu hỏi của người dùng!"
Thông điệp này nhắc nhở mô hình ưu tiên sử dụng thông tin từ các phương thức truy xuất dữ liệu (như about_uet()
) trước khi dựa vào kiến thức nền tảng của nó để trả lời. Điều này giúp đảm bảo chatbot cung cấp thông tin chính xác và phù hợp với ngữ cảnh của trường Đại học Công nghệ.
Đối với những trường hợp phức tạp hơn, ví dụ như câu hỏi yêu cầu xử lý đa truy xuất (kết hợp thông tin từ nhiều nguồn dữ liệu), tôi đã sử dụng kỹ thuật fine-tuning để tinh chỉnh mô hình ngôn ngữ.
(Chi tiết về quá trình fine-tuning sẽ được tôi trình bày rõ hơn trong một bài viết khác.)
4.7. Triển khai thử nghiệm và hiệu chỉnh
Sau khi hoàn thành các bước xây dựng, tôi tiến hành triển khai thử nghiệm hệ thống và đánh giá hiệu suất hoạt động. Quá trình này bao gồm:
- Kiểm thử với tập dữ liệu đa dạng: Sử dụng tập dữ liệu phong phú bao gồm nhiều loại câu hỏi khác nhau để đánh giá khả năng đáp ứng của chatbot. Tập dữ liệu này nên bao gồm cả câu hỏi đơn giản, câu hỏi phức tạp, câu hỏi yêu cầu thông tin chính xác và câu hỏi mở để đánh giá toàn diện khả năng của hệ thống.
- Điều chỉnh thông số: Tối ưu hóa các thông số của hệ thống để cải thiện độ chính xác và hiệu quả của chatbot. Việc này bao gồm:
- Điều chỉnh phương thức tìm kiếm của FAISS: Thử nghiệm với các phương thức tìm kiếm khác nhau của FAISS như similarity_search, range_search, hoặc knn_search để tìm ra phương thức phù hợp nhất với đặc thù dữ liệu và yêu cầu của ứng dụng.
- Điều chỉnh số lượng tài liệu k được trả về: Tìm kiếm giá trị k tối ưu để cân bằng giữa độ chính xác và hiệu suất của hệ thống.
- Tinh chỉnh các tham số của quá trình fine-tuning: Bao gồm learning rate, batch size, số epochs,... để mô hình hội tụ tốt hơn và đạt hiệu suất cao hơn.
- Thu thập phản hồi từ người dùng: Thu thập ý kiến phản hồi từ người dùng thực tế để đánh giá trải nghiệm và điều chỉnh hệ thống cho phù hợp. Phản hồi từ người dùng là vô cùng quý giá, giúp chúng ta nhận ra những điểm mạnh, điểm yếu của hệ thống và có hướng cải thiện phù hợp. Quá trình triển khai và hiệu chỉnh là một vòng lặp liên tục, nhằm mục đích tối ưu hóa hệ thống RAG và mang đến trải nghiệm tốt nhất cho người dùng.
5. Ưu điểm và nhược điểm của hệ thống kết hợp RAG với function calling
Việc tích hợp phương thức function calling vào hệ thống RAG mang lại nhiều lợi ích đáng kể, nhưng cũng đi kèm với một số hạn chế nhất định. Dưới đây là phân tích chi tiết về ưu điểm và nhược điểm của phương pháp này:
5.1. Ưu điểm
- Tối ưu hóa quá trình truy xuất: Thay vì luôn luôn truy xuất một số lượng tài liệu cố định, mô hình có thể tự động quyết định có nên truy xuất dữ liệu hay không, và truy xuất từ nguồn nào dựa trên yêu cầu của từng truy vấn. Điều này giúp tiết kiệm thời gian xử lý và nâng cao hiệu suất của hệ thống.
- Linh hoạt và khả năng mở rộng: Hệ thống có thể dễ dàng tích hợp với nhiều nguồn dữ liệu và định dạng dữ liệu khác nhau thông qua việc xây dựng và kết nối các hàm truy xuất phù hợp. Việc cập nhật và mở rộng hệ thống cũng trở nên dễ dàng hơn.
- Nâng cao khả năng xử lý câu hỏi phức tạp: Việc kết hợp thông tin từ nhiều nguồn dữ liệu giúp chatbot có khả năng xử lý các câu hỏi yêu cầu phân tích sâu, tổng hợp kiến thức hoặc kết nối thông tin từ nhiều lĩnh vực khác nhau.
- Kiểm soát luồng thông tin hiệu quả: Việc sử dụng function calling cho phép kiểm soát chặt chẽ hơn cách thức mô hình truy cập và sử dụng thông tin, từ đó giảm thiểu nguy cơ rò rỉ thông tin nhạy cảm hoặc đưa ra thông tin không chính xác.
- Phù hợp cho việc xây dựng chatbot: Tính linh hoạt và khả năng tùy biến cao của phương pháp này giúp chatbot có thể tương tác với người dùng một cách tự nhiên và hiệu quả hơn, đáp ứng được nhiều mục đích sử dụng khác nhau.
5.2. Nhược điểm
- Đòi hỏi lượng token lớn hơn: Việc sử dụng function calling đòi hỏi phải cung cấp cho mô hình mô tả về các hàm truy xuất, điều này làm tăng lượng token cần xử lý và có thể dẫn đến chi phí tính toán cao hơn.
- Khó khăn trong việc debug và kiểm tra lỗi: Do luồng thông tin phức tạp hơn, việc gỡ lỗi và kiểm tra lỗi trong hệ thống kết hợp RAG với function calling có thể gặp nhiều khó khăn hơn so với hệ thống RAG truyền thống.
- Yêu cầu kiến thức kỹ thuật cao: Việc triển khai phương pháp này đòi hỏi người phát triển có kiến thức chuyên sâu về mô hình ngôn ngữ, function calling và kỹ thuật xử lý ngôn ngữ tự nhiên.
Tóm lại, việc tích hợp phương thức function calling vào hệ thống RAG mang lại nhiều lợi ích vượt trội, giúp chatbot hoạt động thông minh và hiệu quả hơn. Tuy nhiên, phương pháp này cũng có những hạn chế nhất định, đặc biệt là về mặt kỹ thuật và chi phí tính toán. Do đó, tùy thuộc vào yêu cầu cụ thể của từng ứng dụng và nguồn lực sẵn có, bạn có thể cân nhắc lựa chọn phương pháp phù hợp nhất để xây dựng hệ thống RAG hiệu quả.
6. Kết luận
Bài viết đã giới thiệu về phương pháp tích hợp function calling vào hệ thống RAG như một giải pháp hiệu quả để nâng cao khả năng truy xuất thông tin và xử lý ngôn ngữ tự nhiên của chatbot. Thông qua việc sử dụng các hàm truy xuất dữ liệu được định nghĩa trước và khả năng gọi hàm linh hoạt của các mô hình ngôn ngữ lớn, hệ thống RAG có thể vượt qua được hạn chế của việc sử dụng số lượng tài liệu truy xuất cố định, từ đó mang lại trải nghiệm tốt hơn cho người dùng.
Mặc dù còn tồn tại một số hạn chế nhất định, những lợi ích mà phương pháp này mang lại là không thể phủ nhận. Bằng việc tối ưu hóa quá trình truy xuất, tăng cường khả năng xử lý câu hỏi phức tạp và kiểm soát luồng thông tin hiệu quả, hệ thống RAG tích hợp function calling mở ra nhiều tiềm năng cho việc phát triển các ứng dụng chatbot thông minh và hữu ích hơn trong tương lai.
Hy vọng rằng bài viết đã cung cấp cho bạn cái nhìn tổng quan về phương pháp kết hợp RAG với function calling và những lợi ích mà nó mang lại. Khuyến khích bạn đọc hãy tự mình thử nghiệm và khám phá thêm về phương pháp này để ứng dụng vào các dự án thực tế của mình.
All rights reserved