Chuyển đến nội dung chính

Building Context-Aware Chatbots: Leveraging LangChain Framework for ChatGPT

Explore how to build context-aware chatbots using the ChatGPT and LangChain framework.
· 15 phút đọc

Khám phá cùng AI

Trong bối cảnh số hóa phát triển nhanh chóng ngày nay, cùng với sự trỗi dậy của các Mô hình Ngôn ngữ Lớn (LLM), các ứng dụng hội thoại đã trở nên vô cùng phổ biến. Chatbot đã thay đổi cách chúng ta tương tác với ứng dụng, trang web và thậm chí cả các kênh dịch vụ khách hàng trong đời sống hằng ngày.

Khả năng tự xây dựng chatbot của riêng bạn đã trở thành yếu tố tạo khác biệt, giúp doanh nghiệp và cá nhân tạo ra các giao diện hội thoại cá nhân hóa, hiệu quả và hấp dẫn phù hợp với nhu cầu cụ thể.

Bài viết này nhằm phân tích cách xây dựng một chatbot bằng Python tận dụng sức mạnh của LLM. Chúng ta sẽ tìm hiểu cách tạo chatbot của riêng mình bằng ChatGPT và cách tối ưu hóa thêm với khung phổ biến LangChain.

Nếu bạn mới làm quen với ChatGPT và LangChain, hãy xem phần giới thiệu về Mô hình Ngôn ngữ Lớn với GPT & LangChain kèm code dưới đây: 

Bài viết này bao quát mọi khía cạnh then chốt trong quá trình phát triển một chatbot tối ưu sử dụng ChatGPT làm mô hình nền tảng. Chúng ta sẽ dùng Jupyter Notebook với cả hai thư viện openai và langchain đã được cài đặt. Bạn có thể theo dõi trong DataLab workbook này.

Ở các phần tiếp theo, chúng ta sẽ tìm hiểu cách dùng OpenAI API để thực hiện các lời gọi API cơ bản tới ChatGPT như những viên gạch nền cho cuộc hội thoại. Bên cạnh đó, chúng ta sẽ triển khai khả năng nhận biết ngữ cảnh cho chatbot bằng các cấu trúc Python cơ bản. Cuối cùng, chúng ta sẽ tối ưu cả phần triển khai chatbot lẫn lịch sử hội thoại bằng LangChain và khám phá các tính năng bộ nhớ của nó.

Các lời gọi API ChatGPT cơ bản

Chúng ta sẽ dùng ChatGPT làm mô hình đứng sau chatbot. Vì vậy, tác vụ thực hành đầu tiên là viết một lời gọi API đơn giản nhưng đầy đủ tới ChatGPT để thiết lập tương tác ban đầu.

Hàm Python dưới đây bao bọc lời gọi API mong muốn tới ChatGPT chỉ với thư viện openai.

import os
import openai

openai_api_key = os.environ["OPENAI_API_KEY"]

def chatgpt_call(prompt, model="gpt-3.5-turbo"):
   response = openai.ChatCompletion.create(
       model=model,
       messages=[{"role": "user", "content": prompt}]
   )
   return response.choices[0].message["content"]

Lưu ý rằng trong trường hợp này, chúng ta dùng mô hình gpt-3.5-turbo vì đây là mô hình GPT-3.5 mạnh nhất, được tối ưu cho trò chuyện và cũng là một trong những mô hình có chi phí thấp. Thông tin chi tiết về các mô hình khả dụng có trong tài liệu của OpenAI.

Bạn cần trợ giúp để lấy khóa API OpenAI? Vậy khóa học Bắt đầu với OpenAI API và ChatGPT là dành cho bạn.

Hãy phân tích phần triển khai của chúng ta từng bước một!

Hàm chatgpt_call() là một lớp bao quanh lời gọi hàm gốc của OpenAI ChatCompletion.create để tạo yêu cầu hoàn tất chat.

Với prompt đã cho, hàm sẽ trả về phần hoàn tất tương ứng từ ChatGPT. Để gọi hoàn tất thành công, chỉ cần cung cấp ID của mô hình sẽ dùng (model) và prompt mong muốn (prompt).

Phần hoàn tất thực tế do ChatGPT trả về có kèm siêu dữ liệu cùng với phản hồi. Tuy nhiên, phần hoàn tất luôn nằm trong trường response.choices[0].message[“content”]. Thông tin thêm về nội dung hoàn tất đầy đủ có trong tài liệu của OpenAI.

Với phần triển khai này, chúng ta có thể nhắn ChatGPT chỉ bằng một dòng:

prompt = "My name is Andrea"
response = chatgpt_call(prompt)
print(response)

# Output: Nice to meet you, Andrea! How can I assist you today?

Lời gọi như một tương tác độc lập

Bạn có nhận ra điểm cần lưu ý trong phần triển khai ở trên không?

Gợi ý nhé, hãy thử một tương tác tiếp theo:

prompt = "Do you remember my name?"
response = chatgpt_call(prompt)
print(response)

# Output: I'm sorry, as an AI language model, I don't have the ability to remember specific information about individual users.

Đúng vậy, như ta thấy, mỗi lời gọi API là một tương tác độc lập. Điều này có nghĩa là mô hình không có ký ức về các prompt đầu vào hay phần hoàn tất trước đó. Mỗi lần gọi hàm chatgpt_call() là ta bắt đầu một cuộc hội thoại hoàn toàn mới.

Đó chắc chắn là vấn đề khi xây dựng chatbot, vì không thể đối thoại nếu mô hình không nhớ những gì đã trao đổi trước đó. Do vậy, bước tiếp theo là giúp mô hình nhận biết các tương tác trước.

Nhận biết ngữ cảnh

Nhận biết ngữ cảnh cho phép chatbot hiểu và phản hồi phù hợp dựa trên cuộc hội thoại đang diễn ra.

Bằng cách xây dựng ngữ cảnh, các tương tác sau có thể được cá nhân hóa dựa trên những trao đổi trước đó, duy trì tính liên tục và tránh lặp lại. Hơn nữa, với chatbot nhận biết ngữ cảnh, người dùng có thể chủ động đặt câu hỏi làm rõ hoặc đưa ra gợi ý để chỉnh sửa hiểu nhầm.

Tổng thể, nhận biết ngữ cảnh giúp chatbot tự nhiên hơn, hiệu quả hơn và nâng cao trải nghiệm người dùng. Từ giờ, chúng ta sẽ gọi tập hợp các tương tác trước đó là lịch sử hội thoại hoặc bộ nhớ.

API gốc của OpenAI cho ChatGPT đã cung cấp cách gửi lịch sử hội thoại kèm theo mỗi prompt mới. Ngoài ra, khung LangChain cũng cung cấp cách quản lý bộ nhớ của ChatGPT hiệu quả hơn và tối ưu chi phí.

Hiểu về thông điệp System, Assistant và User

Cấu trúc lịch sử hội thoại của ChatGPT

Lúc này, chúng ta có thể dùng ngay phương thức chatgpt_call() chỉ bằng cách truyền prompt mong muốn. Nếu xem kỹ phần triển khai, prompt đầu vào được định dạng như một phần của danh sách messages. Danh sách này thực chất dùng để lưu vết lịch sử hội thoại, vì vậy nó phải được định dạng theo một cách rất cụ thể.

Hãy xem kỹ hơn!

Đến giờ, chúng ta đã định dạng danh sách messages trong chatgpt_call() như sau:

messages = [{"role": "user", "content": prompt}]

Danh sách messages lưu lịch sử hội thoại dưới dạng một tập hợp các từ điển. Mỗi từ điển trong danh sách đại diện cho một thông điệp trong cuộc trò chuyện với hai cặp khóa-giá trị.

  • Khóa role với giá trị user cho biết thông điệp được gửi bởi người dùng.
  • Khóa content với giá trị prompt chứa prompt thực tế của người dùng.

Đây là ví dụ rất đơn giản chỉ có một tương tác. Mặc dù khóa content sẽ luôn lưu prompt của người dùng hoặc phần hoàn tất của chatbot, khóa role linh hoạt hơn nhiều.

Các vai trò System, Assistant và User

Khóa role cho biết chủ thể của thông điệp trong content. Khi dùng ChatGPT, chúng ta có tối đa ba vai trò:

  • User. Như đã đề cập, vai trò user tương ứng với người dùng thực sự đang tương tác với chatbot.
  • Assistant. Vai trò assistant tương ứng với chatbot.
  • System. Vai trò system dùng để cung cấp hướng dẫn, chỉ dẫn và ngữ cảnh cho chatbot. Thông điệp hệ thống giúp định hướng tương tác với người dùng, thiết lập hành vi của chatbot, cung cấp thông tin ngữ cảnh và xử lý các tương tác cụ thể trong cuộc trò chuyện. Có thể xem thông điệp hệ thống là chỉ dẫn cấp cao cho chatbot, trong suốt với người dùng.

Hình tự làm. Sự tương tác giữa các vai trò khác nhau trong chuỗi hội thoại khi xây dựng chatbot với chatGPT.

Hình tự làm. Sự tương tác giữa các vai trò khác nhau trong chuỗi hội thoại khi xây dựng chatbot với chatGPT.

Xây dựng lịch sử hội thoại

Chúng ta có thể tiến hành xây dựng lịch sử hội thoại bằng cách sử dụng ba vai trò user, assistant và system trong danh sách messages.

Dựa trên các tương tác thử nghiệm, một lịch sử hội thoại được định dạng đúng, sử dụng cả ba vai trò nói trên có thể như sau:

messages =  [ 
{'role':'system', 'content':'You are friendly chatbot.'},
{'role':'user', 'content':'Hi, my name is Andrea'},
{'role':'assistant', 'content': "Nice to meet you, Andrea! How can I assist you today?"},
{'role':'user', 'content':'Do you remember my name?'}  ]

Chúng ta có thể chỉnh sửa nhẹ hàm call_chatgpt() để đưa lịch sử hội thoại vào như một tham số đầu vào:

def call_chatgpt_with_memory(messages, model="gpt-3.5-turbo"):
   response = openai.ChatCompletion.create(
       model=model,
       messages=messages,
   )
   return response.choices[0].message["content"]

Hãy thử phần triển khai mới này!

messages =  [ 
{'role':'system', 'content':'You are friendly chatbot.'},
{'role':'user', 'content':'Hi, my name is Andrea'},
{'role':'assistant', 'content': "Nice to meet you, Andrea! How can I assist you today?"},
{'role':'user', 'content':'Do you remember my name?'}  ]

response = call_chatgpt_with_memory(messages)
print(response)

# Output: Yes, your name is Andrea.

Tuyệt! Chúng ta đã giúp mô hình nhận biết các tương tác trước đó để có thể thực sự trò chuyện. Tuy nhiên, chúng ta cần cách tự động cập nhật danh sách messages.

Tự động hóa lịch sử hội thoại

Chúng ta có thể tự động thu thập thông điệp bằng cách mở rộng thêm một chút hàm gọi API:

def chatgpt_conversation(prompt):
   context.append({'role':'user', 'content':f"{prompt}"})
   response = call_chatgpt_with_memory(context)
   context.append({'role':'assistant',
   'content':f"{response}"})
   return response

Giờ hãy thử nhé!

chatgpt_conversation("Hello, my name is Andrea")

# Output: 'Nice to meet you, Andrea! How can I assist you today?'

Và một tương tác tiếp theo:

chatgpt_conversation("Do you remember my name?")

# Output: 'Yes, your name is Andrea.'

Giờ đây lịch sử hội thoại được cập nhật tự động. Cuối cùng, lưu ý rằng bạn có thể xem lịch sử hội thoại — và cấu trúc của nó — bất cứ lúc nào chỉ bằng cách in danh sách messages.

print(messages)

# Output:
[{'role': 'user', 'content': 'Hello, my name is Andrea'},
{'role': 'assistant', 'content': 'Nice to meet you, Andrea! How can I assist you today?'},
{'role': 'user', 'content': 'Do you remember my name?'},
{'role': 'assistant', 'content': 'Yes, your name is Andrea.'}]

Khung LangChain

Đến đây, có thể đồng ý rằng phần triển khai của chúng ta tạo ra một chatbot hữu dụng, nhưng chưa hẳn là phiên bản tối ưu nhất.

Liên tục cung cấp mọi prompt và phần hoàn tất trước đó có thể nhanh chóng dẫn đến một lượng lớn token, và ChatGPT sẽ phải xử lý tất cả trước khi trả về phần hoàn tất mới nhất.

Cho đến nay, chúng ta dùng các thao tác .append để gom các tương tác trước lại nhằm cung cấp một dạng bộ nhớ cho chatbot. Tuy nhiên, việc quản lý bộ nhớ có thể hiệu quả hơn bằng cách dùng các gói chuyên dụng như LangChain.

LangChain là một khung phát triển ứng dụng vận hành bởi các mô hình ngôn ngữ. Vì chatbot là một trong những ứng dụng phổ biến nhất hiện nay, LangChain cung cấp cách triển khai sạch sẽ hơn để xây dựng lịch sử hội thoại của chatbot, cùng các lựa chọn khác để xử lý hiệu quả hơn.

Để biết thêm về chính khung LangChain, tôi rất khuyến nghị bài viết nhập môn Giới thiệu LangChain cho Kỹ thuật Dữ liệu & Ứng dụng Dữ liệu.

Lịch sử hội thoại đơn giản với LangChain

Trong phần này, chúng ta sẽ tìm hiểu cách dùng Khung LangChain để xây dựng chatbot.

Thư viện langchain có thể thay thế thư viện openai ở trên, vì mô-đun langchain.llms đã cho phép bạn tương tác với ChatGPT chỉ với hai dòng mã:

# OpenAI key from environment
from langchain.llms import OpenAI
llm = OpenAI()

Sau khi nạp mô hình llm, hãy nhập đối tượng bộ nhớ sẽ lưu lịch sử hội thoại. Để lưu từng tương tác trước với chatbot, LangChain cung cấp ConversationBufferMemory:

from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory()

Giờ là lúc nhập ConversationChain, một lớp bao sẽ sử dụng llm và memory để gửi prompt của người dùng tới ChatGPT và trả về phần hoàn tất:

from langchain.chains import ConversationChain

conversation = ConversationChain(
   llm=llm,
   memory=memory
)

Cuối cùng, để gửi prompt tới ChatGPT, ta có thể dùng phương thức .predict như sau:

conversation.predict(input="Hello, my name is Andrea")

# Output: "Hi Andrea, I'm an AI created to help you with whatever you may need. What can I do for you today?"

Hãy đặt verbose=True để quan sát thông tin mà mô hình cân nhắc khi đưa ra phần hoàn tất. Hãy thử trong một tương tác tiếp theo:

Ảnh chụp màn hình ngữ cảnh sẵn có cho ChatGPT khi tạo phần hoàn tất. Đầu ra Verbose màu xanh lá.

Ảnh chụp màn hình ngữ cảnh sẵn có cho ChatGPT khi tạo phần hoàn tất. Đầu ra Verbose màu xanh lá.

Cuối cùng, hãy kiểm tra rằng mô hình ghi nhớ hoàn hảo các tương tác trước:

Ảnh chụp màn hình ngữ cảnh sẵn có cho ChatGPT khi tạo phần hoàn tất. Đầu ra Verbose màu xanh lá.

Ảnh chụp màn hình ngữ cảnh sẵn có cho ChatGPT khi tạo phần hoàn tất. Đầu ra Verbose màu xanh lá.

Bổ sung lịch sử hội thoại

Chúng ta có thể nạp thêm ngữ cảnh cho chatbot bằng phương thức memory.save_context(). Bằng cách này, ta có thể giúp chatbot biết về bất kỳ nội dung mong muốn nào mà không cần cung cấp thông tin đó trong cuộc trò chuyện. Trong LangChain, các từ khóa dùng cho mục đích này là input và output. Ví dụ,

memory.save_context({"input": "Hi"},
                   {"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"},
                   {"output": "Cool"})

Ngoài ra, ta cũng có thể xem nội dung bộ nhớ bất cứ lúc nào bằng cách chạy print(memory.buffer) hoặc memory.load_memory_variables({}).

Các loại bộ nhớ của LangChain

LangChain triển khai các loại bộ nhớ khác nhau cho ChatGPT. ConversationBufferMemory dùng ở trên sẽ theo dõi mọi tương tác với ChatGPT. Tuy nhiên, như đã bàn, việc gửi toàn bộ lịch sử hội thoại cho ChatGPT ở mỗi tương tác mới có thể nhanh chóng làm tăng mạnh số token cần xử lý.

Hãy nhớ rằng ngoài giới hạn token mỗi tương tác của ChatGPT, chi phí sử dụng cũng phụ thuộc vào số lượng token. Xử lý toàn bộ lịch sử trong mỗi tương tác mới có thể sẽ tốn kém theo thời gian.

Để tránh phải xử lý liên tục lịch sử hội thoại, LangChain triển khai những cách tối ưu khác để quản lý bộ nhớ của ChatGPT. Chúng ta sẽ khám phá một trong những loại tiên tiến ở phần tiếp theo.

Lịch sử hội thoại nâng cao với LangChain

Một cách nâng cao để quản lý bộ nhớ mà không cần lưu từng tương tác trước là lưu một bản tóm tắt các tương tác.

Đây là loại bộ nhớ tôi yêu thích vì nó thực sự giúp tối ưu quản lý bộ nhớ mà không đánh mất bất kỳ tương tác nào trong quá khứ.

Hãy bắt tay vào làm!

# OpenAI key from environment
from langchain.llms import OpenAI
llm = OpenAI()

from langchain.memory import ConversationSummaryBufferMemory
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=100)

Lưu ý rằng loại bộ nhớ này dùng llm đã định để tạo bản tóm tắt các tương tác trước đó.

Ngoài ra, bộ nhớ này có thể lưu trữ các tương tác đầy đủ cho đến một số lượng token tối đa (max_token_limit). Vượt quá giới hạn này, nó sẽ đưa thông tin của những thông điệp đó vào bản tóm tắt.

Để thử nghiệm, hãy tạo một tương tác có số lượng token lớn. Xét schedule sau:

schedule = "There is a meeting at 8am with your product team. \
You will need your powerpoint presentation prepared. \
9am-12pm have time to work on your LangChain \
project which will go quickly because Langchain is such a powerful tool. \
At Noon, lunch at the italian resturant with a customer who is driving \
from over an hour away to meet you to understand the latest in AI. \
Be sure to bring your laptop to show the latest LLM demo."

memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=100)
memory.save_context({"input": "Hello"}, {"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"}, {"output": "Cool"})
memory.save_context({"input": "What is on the schedule today?"}, {"output": f"{schedule}"})

Giờ, ta chỉ cần khai báo một chuỗi hội thoại mới với llm và memory như các ví dụ trước.

conversation = ConversationChain(
   llm=llm,
   memory=memory,
   verbose=True
)

Chúng ta có thể dùng .predict để gửi prompt cho ChatGPT:

Ảnh chụp màn hình ngữ cảnh sẵn có cho ChatGPT khi tạo phần hoàn tất. Đầu ra Verbose màu xanh lá.

Ảnh chụp màn hình ngữ cảnh sẵn có cho ChatGPT khi tạo phần hoàn tất. Đầu ra Verbose màu xanh lá.

Như ta thấy, chatbot giữ một bản tóm tắt lịch sử hội thoại gốc, giúp giảm số token ChatGPT phải xử lý ở mỗi tương tác tiếp theo.

Vậy là xong! Giờ chúng ta có thể trò chuyện hàng giờ với chatbot đã được tối ưu!

Đặt verbose=False để có trải nghiệm Hỏi & Đáp dễ chịu hơn trong notebook.

Kết luận

Trong bài viết này, chúng ta đã thấy cách xây dựng một chatbot nhận biết ngữ cảnh dùng mô hình ChatGPT nổi tiếng.

Chúng ta đã thấy việc xây dựng chatbot nhận biết ngữ cảnh của riêng mình có thể dẫn đến những tương tác liên tục. Cách tiếp cận lặp này nuôi dưỡng một vòng phản hồi thường xuyên, giúp bạn duy trì nhu cầu và sở thích của người dùng xuyên suốt cuộc trò chuyện.

Chúng ta đã thấy hai cách chính để quản lý lịch sử hội thoại của chatbot. Thứ nhất, dùng API gốc của ChatGPT để có một chatbot bản địa hoạt động được. Thứ hai, dùng khung chuyên dụng LangChain và triển khai loại bộ nhớ tối ưu.

Giờ là lượt của bạn! Hãy dùng những viên gạch xây dựng được trình bày trong bài để tùy chỉnh một chatbot phù hợp với trường hợp sử dụng cụ thể của bạn!

Nếu bạn quan tâm đến việc xây dựng các ứng dụng khác bằng cả LangChain và ChatGPT, hội thảo Xây dựng Ứng dụng AI với LangChain và GPT là dành cho bạn. Muốn bắt kịp tốc độ với NLP? Hãy xem các mô-đun về Xử lý Ngôn ngữ Tự nhiên bằng Python!

Andrea Valenzuela

Tác giả

Andrea Valenzuela

Andrea Valenzuela hiện đang làm việc cho thí nghiệm CMS tại máy gia tốc hạt (CERN) ở Geneva, Thụy Sĩ. Với sáu năm kinh nghiệm về kỹ thuật dữ liệu và phân tích, công việc của cô bao gồm phân tích dữ liệu và phát triển phần mềm. Hiện cô đang nỗ lực dân chủ hóa việc học các công nghệ liên quan đến dữ liệu thông qua ấn phẩm Medium ForCode'Sake.

Cô có bằng Cử nhân Vật lý Kỹ thuật từ Đại học Bách khoa Catalonia, cũng như bằng Thạc sĩ Hệ thống Tương tác Thông minh từ Đại học Pompeu Fabra. Kinh nghiệm nghiên cứu của cô bao gồm công việc chuyên môn với các thuật toán trước đây của OpenAI cho tạo sinh hình ảnh, chẳng hạn như Normalizing Flows.

Chủ đề

Liên quan