Xin chào tất cả mọi người! Gần đây, tôi đã áp dụng một giải pháp thú vị trong quá trình thực hành mà tôi đã muốn thử từ lâu và bây giờ tôi sẵn sàng giải thích cách bạn có thể tạo một giải pháp tương tự cho bất kỳ nhiệm vụ nào khác. Chúng ta sẽ nói về việc tạo một phiên bản ChatGPT tùy chỉnh để trả lời các câu hỏi, có tính đến cơ sở kiến thức lớn không bị giới hạn về độ dài bởi kích thước của lời nhắc (nghĩa là bạn không thể chỉ cần thêm tất cả thông tin trước mỗi câu hỏi vào ChatGPT).
Để đạt được điều này, chúng tôi sẽ sử dụng các nhúng theo ngữ cảnh từ OpenAI (để tìm kiếm các câu hỏi có liên quan thực sự chất lượng cao từ cơ sở tri thức) và chính API ChatGPT (để định dạng câu trả lời bằng ngôn ngữ tự nhiên của con người).
Ngoài ra, người ta cho rằng trợ lý không chỉ có thể trả lời các câu hỏi Hỏi & Đáp được nêu rõ ràng mà còn cả những câu hỏi mà một người quen thuộc với Hỏi & Đáp có thể trả lời. Nếu bạn quan tâm đến việc tìm hiểu cách tạo các bot đơn giản phản hồi bằng cách sử dụng cơ sở kiến thức lớn, vui lòng đến với thông tin chi tiết.
Tôi muốn chỉ ra rằng có một số dự án thư viện cố gắng giải quyết nhiệm vụ này dưới dạng một khung, chẳng hạn như LangChain và tôi cũng đã thử sử dụng nó. Tuy nhiên, giống như bất kỳ framework nào đang ở giai đoạn phát triển ban đầu, trong một số trường hợp, nó có xu hướng hạn chế hơn là đơn giản hóa mọi thứ. Đặc biệt, ngay từ khi bắt đầu giải quyết công việc này, tôi đã hiểu mình muốn làm gì với dữ liệu và biết cách tự mình thực hiện (bao gồm tìm kiếm theo ngữ cảnh, đặt đúng ngữ cảnh trong lời nhắc, kết hợp các nguồn thông tin).
Nhưng tôi không thể định cấu hình khung để thực hiện chính xác điều đó với mức chất lượng có thể chấp nhận được và việc gỡ lỗi khung có vẻ như quá mức cần thiết cho nhiệm vụ này. Cuối cùng, tôi đã tạo mã soạn sẵn của riêng mình và hài lòng với cách tiếp cận này.
Hãy để tôi mô tả ngắn gọn nhiệm vụ mà tôi đang thực hiện và bạn có thể sử dụng cùng một mã trong các nhiệm vụ của riêng mình, thay thế các nguồn dữ liệu và lời nhắc bằng những nguồn phù hợp với bạn . Bạn vẫn sẽ có toàn quyền kiểm soát logic của bot.
Khi viết mã, tôi thường sử dụng ChatGPT (và tôi không xấu hổ về điều đó🙂). Tuy nhiên, do thiếu dữ liệu cho năm 2022 trở lên, đôi khi có vấn đề với các công nghệ tương đối mới.
Đặc biệt, khi phát triển các đồ thị con cho giao thức Đồ thị (cách phổ biến nhất để xây dựng ETL để truy xuất dữ liệu được lập chỉ mục từ các chuỗi khối tương thích với EVM, bạn có thể đọc thêm về nó trong các bài viết trước của tôi [ 1 ] và [ 2 ]), bản thân các thư viện đã trải qua một số thay đổi đột phá về tính tương thích. Các câu trả lời "cũ" từ ChatGPT không còn hữu ích nữa và tôi phải tìm kiếm câu trả lời chính xác trong tài liệu khan hiếm hoặc trường hợp xấu nhất là trong Discord của nhà phát triển, điều này không thuận tiện lắm (nó không giống như StackOverflow).
Phần thứ hai của vấn đề là mỗi khi bạn cần cung cấp ngữ cảnh hội thoại một cách chính xác vì ChatGPT thường chuyển sang chủ đề về đồ thị con, chuyển sang GraphQL, SQL hoặc toán học cao hơn (“Đồ thị”, “đồ thị con”, v.v. không phải là thuật ngữ duy nhất và có nhiều cách hiểu và chủ đề khác nhau).
Do đó, sau một thời gian ngắn vật lộn với ChatGPT để sửa lỗi trong mã biểu đồ con, tôi quyết định tạo bot SubgraphGPT của riêng mình, bot này sẽ luôn ở trong ngữ cảnh phù hợp và cố gắng trả lời, có tính đến cơ sở kiến thức và thông báo từ các nhà phát triển bất hòa.
tái bút Tôi làm việc với tư cách là người quản lý sản phẩm chính tại chainstack.com , nhà cung cấp cơ sở hạ tầng Web3 và tôi chịu trách nhiệm phát triển dịch vụ lưu trữ sơ đồ con . Vì vậy, tôi phải làm việc với các sơ đồ con khá nhiều, giúp người dùng hiểu được công nghệ tương đối mới này.
Cuối cùng, để giải quyết vấn đề này, tôi quyết định sử dụng hai nguồn:
Cơ sở kiến thức gồm các câu hỏi và câu trả lời được biên soạn thủ công, được chọn ở chế độ nửa mù (tôi thường lấy tiêu đề chủ đề từ tài liệu làm câu hỏi và toàn bộ đoạn thông tin làm câu trả lời).
Tin nhắn đã xuất từ các nhà phát triển giao thức Discord trong 2 năm qua (để bù cho khoảng thời gian còn thiếu từ cuối năm 2021).
Tiếp theo, các cách tiếp cận khác nhau đã được sử dụng cho từng nguồn để soạn yêu cầu tới API ChatGPT, cụ thể:
Đối với phần Hỏi & Đáp được biên soạn thủ công,
đối với mỗi câu hỏi, một nhúng theo ngữ cảnh được tạo (một vectơ mô tả câu hỏi này ở trạng thái đa chiều), thu được thông qua mô hình nhúng văn bản-ada-002 ,
sau đó, bằng cách sử dụng chức năng tìm kiếm khoảng cách cosine, 3 câu hỏi tương tự hàng đầu từ cơ sở kiến thức sẽ được tìm thấy (thay vì 3, bạn có thể sử dụng số phù hợp nhất cho tập dữ liệu của mình),
câu trả lời cho 3 câu hỏi này được thêm vào lời nhắc cuối cùng với mô tả gần đúng là " Chỉ sử dụng đoạn Hỏi & Đáp này nếu nó liên quan đến câu hỏi đã cho ."
Đối với các tin nhắn được xuất từ Discord, thuật toán sau đã được sử dụng:
đối với mỗi thông báo có chứa dấu chấm hỏi, nhúng theo ngữ cảnh cũng được tạo (sử dụng cùng một mô hình),
sau đó, theo cách tương tự, 5 câu hỏi giống nhau nhất được chọn,
và dưới dạng ngữ cảnh cho câu trả lời, 20 thông báo sau câu hỏi đó được thêm vào, được cho là có xác suất nhất định chứa câu trả lời cho câu hỏi,
và thông tin này đã được thêm vào lời nhắc cuối cùng đại khái như thế này: " Nếu bạn không tìm thấy câu trả lời rõ ràng cho câu hỏi trong đoạn Hỏi & Đáp đính kèm, các đoạn trò chuyện sau của nhà phát triển có thể hữu ích cho bạn để trả lời câu hỏi ban đầu..."
Ngoài ra, nếu chủ đề không được đưa ra rõ ràng, thì sự hiện diện của các đoạn Hỏi & Đáp và các cuộc trò chuyện có thể dẫn đến sự mơ hồ trong các câu trả lời, chẳng hạn như có thể trông như sau:
Vì vậy, nó hiểu rằng câu hỏi đã được tách ra khỏi ngữ cảnh và câu trả lời cũng được chấp nhận tách khỏi ngữ cảnh. Sau đó, người ta nói rằng dữ liệu đó có thể được sử dụng và tóm tắt nó như sau:
Để tránh điều này, chúng tôi giới thiệu khái niệm chủ đề, được định nghĩa rõ ràng và chèn vào đầu lời nhắc như sau:
"Tôi cần có câu trả lời cho câu hỏi liên quan đến chủ đề 'Phát triển đồ thị con của Đồ thị': {{{đồ thị con là gì?}}}"
Hơn nữa, trong câu cuối cùng, tôi cũng thêm điều này:
Cuối cùng, chỉ khi thông tin trên là không đủ, bạn có thể sử dụng kiến thức của mình trong chủ đề 'Phát triển đồ thị con' để trả lời câu hỏi.
Cuối cùng, lời nhắc hoàn chỉnh (không bao gồm phần thu được từ các cuộc trò chuyện) trông như sau:
==I need to get an answer to the question related to the topic of "The Graph subgraph development": {{{what is a subgraph?}}}.== ==Possibly, you might find an answer in these Q&As \[use the information only if it is actually relevant and useful for the question answering\]:== ==Q: <What is a subgraph?>== ==A: <A subgraph is a custom API built on blockchain data. Subgraphs are queried using the GraphQL query language and are deployed to a Graph Node using the Graph CLI. Once deployed and published to The Graph's decentralized network, Indexers process subgraphs and make them available to be queried by subgraph consumers.>== ==Q: <Am I still able to create a subgraph if my smart contracts don't have events?>== ==A: <It is highly recommended that you structure your smart contracts to have events associated with data you are interested in querying. Event handlers in the subgraph are triggered by contract events and are by far the fastest way to retrieve useful data. If the contracts you are working with do not contain events, your subgraph can use call and block handlers to trigger indexing. Although this is not recommended, as performance will be significantly slower.>== ==Q: <How do I call a contract function or access a public state variable from my subgraph mappings?>== ==A: <Take a look at Access to smart contract state inside the section AssemblyScript API. https://thegraph.com/docs/en/developing/assemblyscript-api/>== ==Finally, only if the information above was not enough you can use your knowledge in the topic of "The Graph subgraph development" to answer the question.==
Phản hồi cho yêu cầu trên với lời nhắc được tạo bán tự động này ở đầu vào có vẻ chính xác ngay từ đầu:
Trong trường hợp này, bot ngay lập tức trả lời bằng khóa chính xác và thêm thông tin liên quan hơn, vì vậy câu trả lời trông không đơn giản như trong phần Hỏi & Đáp (tôi nhắc bạn rằng câu hỏi này chính xác nằm trong danh sách câu hỏi và câu trả lời), nhưng với những lời giải thích hợp lý giúp giải quyết một phần các câu hỏi sau.
Tôi cần lưu ý ngay rằng sẽ có một liên kết đến kho lưu trữ ở cuối , vì vậy bạn có thể chạy bot như hiện tại, thay thế "chủ đề" bằng tệp của riêng bạn, tệp cơ sở kiến thức Hỏi & Đáp bằng tệp của riêng bạn và cung cấp các khóa API của riêng bạn cho OpenAI và bot Telegram. Vì vậy, mô tả ở đây không nhằm hoàn toàn tương ứng với mã nguồn trên GitHub, mà là để làm nổi bật các khía cạnh chính của mã.
Hãy tạo một môi trường ảo mới và cài đặt các phụ thuộc từ tests.txt:
virtualenv -p python3.8 .venv source .venv/bin/activate pip install -r requirements.txt
Như đã đề cập ở trên, giả định rằng có một danh sách các câu hỏi và câu trả lời, trong trường hợp này ở định dạng tệp Excel thuộc loại sau:
Để tìm câu hỏi tương tự nhất với câu hỏi đã cho, chúng ta cần thêm phần nhúng của câu hỏi (một vectơ đa chiều trong không gian trạng thái) vào mỗi dòng của tệp này. Chúng tôi sẽ sử dụng tệp add_embeddings.py cho việc này. Kịch bản bao gồm một số phần đơn giản.
Nhập thư viện và đọc đối số dòng lệnh:
import pandas as pd import openai import argparse # Create an Argument Parser object parser = argparse.ArgumentParser(description='Adding embeddings for each line of csv file') # Add the arguments parser.add_argument('--openai_api_key', type=str, help='API KEY of OpenAI API to create contextual embeddings for each line') parser.add_argument('--file', type=str, help='A source CSV file with the text data') parser.add_argument('--colname', type=str, help='Column name with the texts') # Parse the command-line arguments args = parser.parse_args() # Access the argument values openai.api_key = args.openai_api_key file = args.file colname = args.colname
Tiếp theo, đọc tệp vào khung dữ liệu gấu trúc và lọc các câu hỏi dựa trên sự hiện diện của dấu chấm hỏi. Đoạn mã này dùng phổ biến để xử lý cơ sở kiến thức cũng như các luồng thông báo thô từ Discord, vì vậy, giả sử các câu hỏi thường bị trùng lặp, tôi quyết định giữ lại một phương pháp đơn giản như vậy để lọc thô không phải câu hỏi.
if file[-4:] == '.csv': df = pd.read_csv(file) else: df = pd.read_excel(file) # filter NAs df = df[~df[colname].isna()] # Keep only questions df = df[df[colname].str.contains('\?')]
Và cuối cùng - một chức năng để tạo nhúng bằng cách gọi API của mô hình text-embedding-ada-002 , một vài yêu cầu lặp lại do API đôi khi có thể bị quá tải và có thể phản hồi với lỗi và áp dụng chức năng này cho từng hàng của khung dữ liệu.
def get_embedding(text, model="text-embedding-ada-002"): i = 0 max_try = 3 # to avoid random OpenAI API fails: while i < max_try: try: text = text.replace("\n", " ") result = openai.Embedding.create(input=[text], model=model)['data'][0]['embedding'] return result except: i += 1 def process_row(x): return get_embedding(x, model='text-embedding-ada-002') df['ada_embedding'] = df[colname].apply(process_row) df.to_csv(file[:-4]+'_question_embed.csv', index=False)
Cuối cùng, tập lệnh này có thể được gọi bằng lệnh sau:
python add_embeddings.py \ --openai_api_key="xxx" \ --file="./subgraphs_faq.xlsx" \ --colname="Question"
thiết lập khóa API OpenAI, tệp có cơ sở kiến thức và tên của cột chứa văn bản câu hỏi. Tệp được tạo cuối cùng, subgraphs_faq._question_embed.csv, chứa các cột "Câu hỏi", "Trả lời" và "ada_embedding ".
Nếu bạn quan tâm đến một bot đơn giản chỉ phản hồi dựa trên cơ sở kiến thức được thu thập thủ công, bạn có thể bỏ qua phần này và phần tiếp theo. Tuy nhiên, tôi sẽ cung cấp ngắn gọn các ví dụ về mã ở đây để thu thập dữ liệu từ cả kênh Discord và nhóm Telegram. Tệp discord-channel-data-collection.py bao gồm hai phần. Phần đầu tiên bao gồm nhập thư viện và khởi tạo đối số dòng lệnh:
import requests import json import pandas as pd import argparse # Create an Argument Parser object parser = argparse.ArgumentParser(description='Discord Channel Data Collection Script') # Add the arguments parser.add_argument('--channel_id', type=str, help='Channel ID from the URL of a channel in browser https://discord.com/channels/xxx/{CHANNEL_ID}') parser.add_argument('--authorization_key', type=str, help='Authorization Key. Being on the discord channel page, start typing anything, then open developer tools -> Network -> Find "typing" -> Headers -> Authorization.') # Parse the command-line arguments args = parser.parse_args() # Access the argument values channel_id = args.channel_id authorization_key = args.authorization_key
Thứ hai là chức năng truy xuất dữ liệu từ kênh và lưu nó vào khung dữ liệu gấu trúc, cũng như lệnh gọi của nó với các tham số được chỉ định.
def retrieve_messages(channel_id, authorization_key): num = 0 limit = 100 headers = { 'authorization': authorization_key } last_message_id = None # Create a pandas DataFrame df = pd.DataFrame(columns=['id', 'dt', 'text', 'author_id', 'author_username', 'is_bot', 'is_reply', 'id_reply']) while True: query_parameters = f'limit={limit}' if last_message_id is not None: query_parameters += f'&before={last_message_id}' r = requests.get( f'https://discord.com/api/v9/channels/{channel_id}/messages?{query_parameters}', headers=headers ) jsonn = json.loads(r.text) if len(jsonn) == 0: break for value in jsonn: is_reply = False id_reply = '0' if 'message_reference' in value and value['message_reference'] is not None: if 'message_id' in value['message_reference'].keys(): is_reply = True id_reply = value['message_reference']['message_id'] text = value['content'] if 'embeds' in value.keys(): if len(value['embeds'])>0: for x in value['embeds']: if 'description' in x.keys(): if text != '': text += ' ' + x['description'] else: text = x['description'] df_t = pd.DataFrame({ 'id': value['id'], 'dt': value['timestamp'], 'text': text, 'author_id': value['author']['id'], 'author_username': value['author']['username'], 'is_bot': value['author']['bot'] if 'bot' in value['author'].keys() else False, 'is_reply': is_reply, 'id_reply': id_reply, }, index=[0]) if len(df) == 0: df = df_t.copy() else: df = pd.concat([df, df_t], ignore_index=True) last_message_id = value['id'] num = num + 1 print('number of messages we collected is', num) # Save DataFrame to a CSV file df.to_csv(f'../discord_messages_{channel_id}.csv', index=False) if __name__ == '__main__': retrieve_messages(channel_id, authorization_key)
Từ thông tin hữu ích ở đây, có một chi tiết mà tôi không thể tìm thấy mỗi khi tôi cần - lấy khóa ủy quyền. Xem xét channel_id có thể được lấy từ URL của kênh Discord được mở trong trình duyệt (số dài cuối cùng trong liên kết), thì bạn chỉ có thể tìm thấyủy quyền_key bằng cách bắt đầu nhập thông báo vào kênh, sau đó sử dụng các công cụ dành cho nhà phát triển để tìm sự kiện có tên " typing " trong phần Mạng và trích xuất tham số từ tiêu đề.
Sau khi nhận được các tham số này, bạn có thể chạy lệnh sau để thu thập tất cả các tin nhắn từ kênh (thay thế các giá trị của riêng bạn):
python discord-channel-data-collection.py \ --channel_id=123456 \ --authorization_key="123456qwerty"
Vì tôi thường tải xuống nhiều dữ liệu khác nhau từ các cuộc trò chuyện/kênh trong Telegram, nên tôi cũng quyết định cung cấp mã cho việc này. Mã này tạo ra tệp CSV có định dạng tương tự (tương thích với tập lệnh add_embeddings.py ). Vì vậy, tập lệnh telegram-group-data-collection.py trông như sau. Nhập thư viện và khởi tạo đối số từ dòng lệnh:
import pandas as pd import argparse from telethon import TelegramClient # Create an Argument Parser object parser = argparse.ArgumentParser(description='Telegram Group Data Collection Script') # Add the arguments parser.add_argument('--app_id', type=int, help='Telegram APP id from https://my.telegram.org/apps') parser.add_argument('--app_hash', type=str, help='Telegram APP hash from https://my.telegram.org/apps') parser.add_argument('--phone_number', type=str, help='Telegram user phone number with the leading "+"') parser.add_argument('--password', type=str, help='Telegram user password') parser.add_argument('--group_name', type=str, help='Telegram group public name without "@"') parser.add_argument('--limit_messages', type=int, help='Number of last messages to download') # Parse the command-line arguments args = parser.parse_args() # Access the argument values app_id = args.app_id app_hash = args.app_hash phone_number = args.phone_number password = args.password group_name = args.group_name limit_messages = args.limit_messages
Như bạn có thể thấy, bạn không thể chỉ tải xuống tất cả các tin nhắn từ cuộc trò chuyện mà không cho phép mình là người đầu tiên. Nói cách khác, bên cạnh việc tạo ứng dụng thông qua https://my.telegram.org/apps (lấy APP_ID và APP_HASH), bạn cũng sẽ cần sử dụng số điện thoại và mật khẩu của mình để tạo một phiên bản của lớp TelegramClient từ thư viện Telethon.
Ngoài ra, bạn sẽ cần group_name công khai của cuộc trò chuyện Telegram và chỉ định rõ ràng số lượng tin nhắn mới nhất sẽ được truy xuất. Nhìn chung, tôi đã thực hiện quy trình này nhiều lần với bất kỳ số lượng tin nhắn đã xuất nào mà không nhận được bất kỳ lệnh cấm tạm thời hoặc vĩnh viễn nào từ API Telegram, không giống như khi một người gửi tin nhắn quá thường xuyên từ một tài khoản.
Phần thứ hai của tập lệnh chứa chức năng thực tế để xuất thông báo và quá trình thực thi của nó (với bộ lọc cần thiết để tránh các lỗi nghiêm trọng có thể dừng quá trình thu thập giữa chừng):
async def main(): messages = await client.get_messages(group_name, limit=limit_messages) df = pd.DataFrame(columns=['date', 'user_id', 'raw_text', 'views', 'forwards', 'text', 'chan', 'id']) for m in messages: if m is not None: if 'from_id' in m.__dict__.keys(): if m.from_id is not None: if 'user_id' in m.from_id.__dict__.keys(): df = pd.concat([df, pd.DataFrame([{'date': m.date, 'user_id': m.from_id.user_id, 'raw_text': m.raw_text, 'views': m.views, 'forwards': m.forwards, 'text': m.text, 'chan': group_name, 'id': m.id}])], ignore_index=True) df = df[~df['user_id'].isna()] df = df[~df['text'].isna()] df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date').reset_index(drop=True) df.to_csv(f'../telegram_messages_{group_name}.csv', index=False) client = TelegramClient('session', app_id, app_hash) client.start(phone=phone_number, password=password) with client: client.loop.run_until_complete(main())
Cuối cùng, tập lệnh này có thể được thực thi bằng lệnh sau (thay thế các giá trị bằng giá trị của riêng bạn):
python telegram-group-data-collection.py \ --app_id=123456 --app_hash="123456qwerty" \ --phone_number="+xxxxxx" --password="qwerty123" \ --group_name="xxx" --limit_messages=10000
Hầu hết thời gian, tôi đưa các dự án yêu thích của mình vào các bot Telegram vì nó đòi hỏi nỗ lực tối thiểu để khởi chạy và ngay lập tức cho thấy tiềm năng. Trong trường hợp này, tôi cũng làm như vậy. Tôi phải nói rằng mã bot không chứa tất cả các trường hợp góc mà tôi sử dụng trong phiên bản sản xuất của bot SubgraphGPT , vì nó có khá nhiều logic kế thừa từ một dự án thú cưng khác của tôi. Thay vào đó, tôi đã để lại số lượng mã cơ bản tối thiểu để dễ dàng sửa đổi theo nhu cầu của bạn.
Tập lệnh telegram-bot.py bao gồm một số phần. Đầu tiên, như trước đây, các thư viện được nhập và các đối số dòng lệnh được khởi tạo.
import threading import telegram from telegram.ext import Updater, CommandHandler, MessageHandler, Filters import openai from openai.embeddings_utils import cosine_similarity import numpy as np import pandas as pd import argparse import functools # Create an Argument Parser object parser = argparse.ArgumentParser(description='Run the bot which uses prepared knowledge base enriched with contextual embeddings') # Add the arguments parser.add_argument('--openai_api_key', type=str, help='API KEY of OpenAI API to create contextual embeddings for each line') parser.add_argument('--telegram_bot_token', type=str, help='A telegram bot token obtained via @BotFather') parser.add_argument('--file', type=str, help='A source CSV file with the questions, answers and embeddings') parser.add_argument('--topic', type=str, help='Write the topic to add a default context for the bot') parser.add_argument('--start_message', type=str, help="The text that will be shown to the users after they click /start button/command", default="Hello, World!") parser.add_argument('--model', type=str, help='A model of ChatGPT which will be used', default='gpt-3.5-turbo-16k') parser.add_argument('--num_top_qa', type=str, help="The number of top similar questions' answers as a context", default=3) # Parse the command-line arguments args = parser.parse_args() # Access the argument values openai.api_key = args.openai_api_key token = args.telegram_bot_token file = args.file topic = args.topic model = args.model num_top_qa = args.num_top_qa start_message = args.start_message
Xin lưu ý rằng trong trường hợp này, bạn cũng sẽ cần khóa API OpenAI, vì để tìm câu hỏi gần giống nhất với câu hỏi mà người dùng vừa nhập từ cơ sở tri thức, trước tiên bạn cần lấy bản nhúng của câu hỏi đó bằng cách gọi API như chúng tôi đã làm cho chính cơ sở tri thức.
Ngoài ra, bạn sẽ cần:
Sau đó là quá trình tải tệp cơ sở kiến thức và khởi tạo nhúng câu hỏi.
# reading QA file with embeddings df_qa = pd.read_csv(file) df_qa['ada_embedding'] = df_qa.ada_embedding.apply(eval).apply(np.array)
Để đưa ra yêu cầu đối với API ChatGPT, biết rằng đôi khi nó phản hồi lỗi do quá tải, tôi sử dụng chức năng thử lại yêu cầu tự động trong trường hợp có lỗi.
def retry_on_error(func): @functools.wraps(func) def wrapper(*args, **kwargs): max_retries = 3 for i in range(max_retries): try: return func(*args, **kwargs) except Exception as e: print(f"Error occurred, retrying ({i+1}/{max_retries} attempts)...") # If all retries failed, raise the last exception raise e return wrapper @retry_on_error def call_chatgpt(*args, **kwargs): return openai.ChatCompletion.create(*args, **kwargs)
Theo khuyến nghị của OpenAI, trước khi chuyển đổi văn bản thành nhúng, các dòng mới nên được thay thế bằng khoảng trắng.
def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ") return openai.Embedding.create(input=[text], model=model)['data'][0]['embedding']
Để tìm kiếm các câu hỏi giống nhau nhất, chúng tôi tính toán khoảng cách cosine giữa các phần nhúng của hai câu hỏi, được lấy trực tiếp từ thư viện openai.
def search_similar(df, question, n=3, pprint=True): embedding = get_embedding(question, model='text-embedding-ada-002') df['similarities'] = df.ada_embedding.apply(lambda x: cosine_similarity(x, embedding)) res = df.sort_values('similarities', ascending=False).head(n) return res
Sau khi nhận được danh sách các cặp câu hỏi-câu trả lời giống nhất với câu hỏi đã cho, bạn có thể biên dịch chúng thành một văn bản, đánh dấu nó theo cách mà ChatGPT có thể xác định rõ ràng đó là gì.
def collect_text_qa(df): text = '' for i, row in df.iterrows(): text += f'Q: <'+row['Question'] + '>\nA: <'+ row['Answer'] +'>\n\n' print('len qa', len(text.split(' '))) return text
Sau đó, cần phải tập hợp các "mảnh" của lời nhắc được mô tả ở đầu bài viết thành một tổng thể.
def collect_full_prompt(question, qa_prompt, chat_prompt=None): prompt = f'I need to get an answer to the question related to the topic of "{topic}": ' + "{{{"+ question +"}}}. " prompt += '\n\nPossibly, you might find an answer in these Q&As [use the information only if it is actually relevant and useful for the question answering]: \n\n' + qa_prompt # edit if you need to use this also if chat_prompt is not None: prompt += "---------\nIf you didn't find a clear answer in the Q&As, possibly, these talks from chats might be helpful to answer properly [use the information only if it is actually relevant and useful for the question answering]: \n\n" + chat_prompt prompt += f'\nFinally, only if the information above was not enough you can use your knowledge in the topic of "{topic}" to answer the question.' return prompt
Trong trường hợp này, tôi đã xóa phần đó bằng tin nhắn từ Discord, nhưng bạn vẫn có thể tuân theo logic nếu chat_prompt != Không có.
Ngoài ra, chúng tôi sẽ cần một chức năng phân tách phản hồi nhận được từ API ChatGPT thành các tin nhắn Telegram (không quá 4096 ký tự):
def telegram_message_format(text): max_message_length = 4096 if len(text) > max_message_length: parts = [] while len(text) > max_message_length: parts.append(text[:max_message_length]) text = text[max_message_length:] parts.append(text) return parts else: return [text]
Bot bắt đầu với một chuỗi các bước điển hình, chỉ định hai chức năng được kích hoạt bằng lệnh /start và nhận thông báo cá nhân từ người dùng:
bot = telegram.Bot(token=token) updater = Updater(token=token, use_context=True) dispatcher = updater.dispatcher dispatcher.add_handler(CommandHandler("start", start, filters=Filters.chat_type.private)) dispatcher.add_handler(MessageHandler(~Filters.command & Filters.text, message_handler)) updater.start_polling()
Mã để trả lời /start rất đơn giản:
def start(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text=start_message)
Và để trả lời một tin nhắn dạng tự do, nó không hoàn toàn rõ ràng.
Đầu tiên , để tránh chặn luồng từ những người dùng khác nhau, hãy ngay lập tức "tách" chúng thành các quy trình độc lập bằng thư viện luồng .
def message_handler(update, context): thread = threading.Thread(target=long_running_task, args=(update, context)) thread.start()
Thứ hai , tất cả logic sẽ xảy ra bên trong hàm long_running_task . Tôi cố ý bọc các đoạn chính trong try/ngoại trừ để dễ dàng khoanh vùng các lỗi khi sửa đổi mã của bot.
def long_running_task(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text='🕰️⏰🕙⏱️⏳...') try: question = update.message.text.strip() except Exception as e: context.bot.send_message(chat_id=user.id, text=f"🤔It seems like you're sending not text to the bot. Currently, the bot can only work with text requests.") return try: qa_found = search_similar(df_qa, question, n=num_top_qa) qa_prompt = collect_text_qa(qa_found) full_prompt = collect_full_prompt(question, qa_prompt) except Exception as e: context.bot.send_message(chat_id=user.id, text=f"Search failed. Debug needed.") return
Vì có thể có lỗi khi thay thế cơ sở kiến thức và chủ đề bằng chủ đề của riêng bạn, chẳng hạn như do định dạng, lỗi mà con người có thể đọc được sẽ hiển thị.
Tiếp theo, yêu cầu được gửi đến API ChatGPT với một thông báo hệ thống hàng đầu đã tự chứng minh: " Bạn là một trợ lý hữu ích. " Kết quả đầu ra được chia thành nhiều thông báo nếu cần và gửi lại cho người dùng.
try: print(full_prompt) completion = call_chatgpt( model=model, n=1, messages=[{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": full_prompt}] ) result = completion['choices'][0]['message']['content'] except Exception as e: context.bot.send_message(chat_id=user.id, text=f'It seems like the OpenAI service is responding with errors. Try sending the request again.') return parts = telegram_message_format(result) for part in parts: update.message.reply_text(part, reply_to_message_id=update.message.message_id)
Điều đó kết thúc phần với mã.
Hiện tại, nguyên mẫu của một bot như vậy có sẵn ở định dạng giới hạn tại liên kết sau . Khi API được trả phí, bạn có thể thực hiện tối đa 3 yêu cầu mỗi ngày, nhưng tôi không nghĩ nó sẽ giới hạn bất kỳ ai, vì điều thú vị nhất không phải là một bot chuyên biệt tập trung vào một chủ đề hẹp, mà là mã của dự án AnythingGPT, có sẵn trên GitHub với một hướng dẫn ngắn về cách tạo bot của riêng bạn để giải quyết nhiệm vụ cụ thể của bạn với cơ sở kiến thức của bạn dựa trên ví dụ này. Nếu bạn đã đọc đến cuối, tôi hy vọng bài viết này hữu ích cho bạn.