Chào mừng bạn trở lại với chuỗi bài viết “AI Engineer Roadmap”! Sau khi khám phá đặc vụ AI là gì, cách tạo đặc vụ thông minh hơn với Function Calling, và hiểu về RAG cũng như sự khác biệt giữa Assistant API và RAG thủ công, đã đến lúc chúng ta đi sâu vào thực hành.
Nếu bạn là một kỹ sư AI đang tìm cách xây dựng các ứng dụng phức tạp, có khả năng duy trì hội thoại, sử dụng công cụ và truy xuất thông tin, thì OpenAI Assistant API chính là một công cụ mạnh mẽ mà bạn cần nắm vững. Nó cung cấp một tầng trừu tượng (abstraction layer) đáng kể, giúp đơn giản hóa quá trình phát triển các đặc vụ AI, cho phép bạn tập trung vào logic nghiệp vụ thay vì quản lý trạng thái (state), xử lý các bước của RAG hay điều phối việc gọi hàm (function calling).
Bài viết này sẽ là hướng dẫn chi tiết, từng bước về cách xây dựng một đặc vụ tùy chỉnh (custom agent) sử dụng OpenAI Assistant API. Chúng ta sẽ đi từ những khái niệm cơ bản nhất đến việc tích hợp các công cụ mạnh mẽ như Code Interpreter, Retrieval và Function Calling.
Mục lục
Tại Sao Lại Chọn OpenAI Assistant API?
Trước khi bắt tay vào code, hãy cùng nhắc lại một chút lý do tại sao Assistant API lại hữu ích, đặc biệt khi bạn đã quen thuộc với Chat Completions API truyền thống:
- Quản lý Trạng thái Hội thoại (State Management): Assistant API tự động quản lý lịch sử hội thoại trong các Thread, giúp bạn không cần phải tự mình truyền toàn bộ context qua mỗi lần gọi API.
- Hỗ trợ Công cụ Tích hợp (Built-in Tools): Nó cung cấp sẵn Code Interpreter và Retrieval (dựa trên RAG), chỉ cần bật lên là dùng.
- Điều phối Function Calling: Assistant API xử lý các bước gọi hàm phức tạp, bao gồm việc quyết định khi nào cần gọi hàm, yêu cầu các tham số cần thiết và chờ đợi kết quả trả về từ ứng dụng của bạn. Điều này giúp triển khai các luồng ReAct hiệu quả hơn.
- Quản lý Vòng đời Đặc Vụ (Agent Lifecycle): Bạn có thể định nghĩa đặc vụ (Assistant) một lần với các hướng dẫn (instructions) và công cụ cụ thể, sau đó sử dụng lại trong nhiều hội thoại (Threads).
Nói tóm lại, Assistant API giúp giảm đáng kể lượng boilerplate code mà bạn phải viết khi xây dựng các đặc vụ phức tạp, đặc biệt là những đặc vụ cần bộ nhớ (memory – dưới dạng lịch sử hội thoại) và khả năng tương tác với thế giới bên ngoài qua công cụ.
Các Khái Niệm Cốt Lõi Trong Assistant API
Để làm việc hiệu quả với Assistant API, bạn cần hiểu rõ các đối tượng chính cấu thành nên nó:
- Assistant (Đặc Vụ): Đây là “người” mà bạn định nghĩa. Một Assistant có tên (name), hướng dẫn (instructions) về cách nó nên hành động và vai trò, một mô hình ngôn ngữ (model) cụ thể (ví dụ: `gpt-4o`, `gpt-3.5-turbo`), và một tập hợp các công cụ (tools) mà nó có thể sử dụng (Code Interpreter, Retrieval, Function Calling).
- Thread (Luồng Hội thoại): Một Thread đại diện cho một phiên hội thoại duy nhất giữa người dùng và một hoặc nhiều Assistant. Nó lưu trữ lịch sử các tin nhắn (Messages).
- Message (Tin nhắn): Là một đơn vị hội thoại trong Thread. Tin nhắn có thể đến từ người dùng (`role=”user”`) hoặc Assistant (`role=”assistant”`). Nó chứa nội dung văn bản và có thể đính kèm tệp.
- Run (Phiên chạy): Khi bạn muốn Assistant xử lý các tin nhắn mới trong một Thread, bạn tạo một Run. Run là nơi Assistant đọc các tin nhắn trong Thread, quyết định hành động dựa trên hướng dẫn và công cụ được cung cấp, và tạo ra tin nhắn phản hồi. Run có các trạng thái (status) khác nhau như `queued`, `in_progress`, `requires_action`, `completd`, `failed`, v.v.
- Run Step (Bước chạy): Một Run được chia thành các bước (Steps). Mỗi Step ghi lại một hành động mà Assistant thực hiện trong quá trình Run, ví dụ: gọi mô hình ngôn ngữ, sử dụng một công cụ.
- File (Tệp): Các tệp có thể được đính kèm vào Messages (ví dụ: người dùng tải lên tài liệu) hoặc được liên kết với Assistant (cho công cụ Retrieval).
- Tools (Công cụ): Các khả năng mà Assistant có thể sử dụng. Hiện tại bao gồm Code Interpreter, Retrieval, và Function Calling.
Mối quan hệ giữa các đối tượng này có thể được tóm tắt như sau: Một Assistant được định nghĩa sẵn. Khi người dùng bắt đầu tương tác, một Thread được tạo. Messages từ người dùng được thêm vào Thread. Để Assistant phản hồi, bạn tạo một Run trên Thread đó. Trong quá trình Run, Assistant có thể sử dụng Tools và các bước này được ghi lại trong Run Steps. Files có thể được dùng cho Messages hoặc Assistant (Retrieval).
Thiết Lập Môi Trường
Để bắt đầu, bạn cần có:
- Một tài khoản OpenAI và khóa API (API Key).
- Cài đặt thư viện OpenAI cho ngôn ngữ lập trình của bạn (ví dụ: Python, Node.js).
Cài đặt thư viện OpenAI cho Python:
pip install openai
Thiết lập Client và khóa API (thay YOUR_API_KEY bằng khóa API thực tế của bạn):
import openai
import os
# Thay thế bằng khóa API của bạn hoặc đặt trong biến môi trường
# CÁCH TỐT NHẤT: ĐẶT KHÓA API TRONG BIẾN MÔI TRƯỜNG
# os.environ["OPENAI_API_KEY"] = "YOUR_API_KEY"
client = openai.OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
Luôn khuyến khích sử dụng biến môi trường để quản lý khóa API thay vì mã hóa cứng trong code.
Xây Dựng Đặc Vụ Tùy Chỉnh: Từng Bước Chi Tiết
Bây giờ, chúng ta sẽ đi qua các bước để xây dựng một đặc vụ đơn giản có khả năng trả lời các câu hỏi và sử dụng công cụ.
Bước 1: Tạo Assistant (Đặc Vụ)
Bạn định nghĩa Assistant bằng cách chỉ định tên, hướng dẫn, mô hình và các công cụ cần thiết.
# Bước 1: Tạo Assistant
assistant = client.beta.assistants.create(
name="Trợ Lý Kiến Thức",
instructions="Bạn là một trợ lý hữu ích. Bạn sẽ trả lời các câu hỏi về lập trình và công nghệ. Nếu cần tính toán hoặc xử lý dữ liệu, hãy sử dụng Code Interpreter. Nếu câu hỏi liên quan đến tài liệu đã cung cấp, hãy sử dụng Retrieval.",
model="gpt-4o", # Hoặc model phù hợp khác
tools=[{"type": "code_interpreter"}, {"type": "retrieval"}]
)
print(f"Đã tạo Assistant với ID: {assistant.id}")
# Lưu lại assistant.id để sử dụng sau này
assistant_id = assistant.id
Ở đây, chúng ta tạo một Assistant có tên “Trợ Lý Kiến Thức”, cung cấp hướng dẫn rõ ràng về vai trò và khi nào sử dụng các công cụ Code Interpreter và Retrieval. Chúng ta chọn model `gpt-4o` và bật cả hai công cụ này. `assistant.id` là định danh duy nhất của Assistant này, bạn sẽ cần lưu nó lại.
Bước 2: Tạo Thread (Luồng Hội thoại)
Mỗi phiên hội thoại mới giữa người dùng và Assistant sẽ diễn ra trong một Thread mới.
# Bước 2: Tạo Thread mới cho mỗi phiên hội thoại
thread = client.beta.threads.create()
print(f"Đã tạo Thread với ID: {thread.id}")
# Lưu lại thread.id cho phiên hội thoại hiện tại
thread_id = thread.id
`thread.id` là định danh duy nhất cho phiên hội thoại này. Nếu người dùng quay lại hoặc tiếp tục hội thoại sau này, bạn cần lưu lại `thread_id` để thêm tin nhắn mới vào đúng Thread.
Bước 3: Thêm Tin nhắn của Người dùng vào Thread
Khi người dùng gửi một câu hỏi hoặc yêu cầu, bạn thêm tin nhắn của họ vào Thread hiện tại.
# Bước 3: Thêm tin nhắn của người dùng vào Thread
message = client.beta.threads.messages.create(
thread_id=thread_id,
role="user",
content="Giải thích về cơ chế RAG và khi nào nên dùng nó thay vì fine-tuning."
)
print(f"Đã thêm tin nhắn của người dùng vào Thread {thread_id}")
Bạn chỉ định `thread_id`, vai trò (`role=”user”`) và nội dung (`content`). Bạn có thể thêm nhiều tin nhắn của người dùng vào Thread trước khi yêu cầu Assistant phản hồi.
Bước 4: Chạy Assistant để Xử lý Thread
Để Assistant đọc các tin nhắn trong Thread và tạo phản hồi, bạn tạo một Run.
# Bước 4: Chạy Assistant trên Thread để nhận phản hồi
run = client.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=assistant_id,
# Có thể ghi đè hướng dẫn hoặc model nếu cần cho Run cụ thể
# instructions="Hãy trả lời câu hỏi này thật chi tiết."
)
print(f"Đã tạo Run với ID: {run.id}")
print(f"Trạng thái Run ban đầu: {run.status}")
Bạn cần cung cấp cả `thread_id` và `assistant_id`. Lúc này, Assistant bắt đầu làm việc. Quá trình xử lý có thể mất một chút thời gian.
Bước 5: Theo Dõi Trạng thái Run và Xử lý Yêu cầu Công cụ
Run có thể đi qua nhiều trạng thái. Bạn cần theo dõi trạng thái cho đến khi nó hoàn thành (hoặc yêu cầu hành động).
- `queued`: Run đang chờ được xử lý.
- `in_progress`: Run đang chạy.
- `requires_action`: Run cần bạn cung cấp kết quả từ một Function Call.
- `cancelling`: Run đang trong quá trình hủy.
- `cancelled`: Run đã bị hủy.
- `failed`: Run gặp lỗi.
- `completed`: Run đã hoàn thành và Assistant đã tạo phản hồi.
- `expired`: Run đã hết hạn.
Cách đơn giản nhất là lặp lại và kiểm tra trạng thái cho đến khi nó không còn là `queued` hoặc `in_progress`. Đối với ứng dụng thực tế, bạn nên sử dụng webhook hoặc kiểm tra định kỳ (polling) một cách hiệu quả hơn.
import time
# Bước 5: Theo dõi trạng thái Run
while run.status in ['queued', 'in_progress']:
time.sleep(1) # Chờ 1 giây trước khi kiểm tra lại
run = client.beta.threads.runs.retrieve(
thread_id=thread_id,
run_id=run.id
)
print(f"Trạng thái Run: {run.status}")
# Xử lý trạng thái 'requires_action' (ví dụ về Function Calling)
if run.status == 'requires_action':
print("Run yêu cầu thực hiện hành động (Function Calling).")
# Logic để gọi hàm và gửi kết quả sẽ ở đây (xem phần Sử dụng Công cụ)
# Sau khi gửi kết quả, bạn cần gửi lại Run object với kết quả
# client.beta.threads.runs.submit_tool_outputs(...)
# Và sau đó tiếp tục theo dõi trạng thái Run
elif run.status == 'completed':
print("Run đã hoàn thành.")
# Đến bước 6 để hiển thị tin nhắn
elif run.status == 'failed':
print(f"Run thất bại: {run.last_error}")
# Xử lý các trạng thái khác nếu cần
Phần phức tạp nhất ở đây là xử lý trạng thái `requires_action`, đặc biệt với Function Calling. Assistant sẽ cung cấp thông tin về hàm cần gọi và các tham số. Ứng dụng của bạn cần thực thi hàm đó và gửi kết quả trở lại cho Assistant bằng cách sử dụng `submit_tool_outputs`. Assistant sau đó sẽ tiếp tục Run.
Bước 6: Hiển thị Phản hồi từ Assistant
Khi Run hoàn thành (`completed`), Assistant đã thêm các tin nhắn phản hồi vào Thread. Bạn có thể lấy tất cả tin nhắn trong Thread (hoặc chỉ những tin nhắn mới nhất) để hiển thị cho người dùng.
# Bước 6: Lấy các tin nhắn sau khi Run hoàn thành
if run.status == 'completed':
messages = client.beta.threads.messages.list(
thread_id=thread_id,
order="asc", # Sắp xếp theo thời gian tăng dần
after=message.id # Lấy tin nhắn sau tin nhắn cuối cùng của người dùng
)
# Lọc chỉ tin nhắn của Assistant
assistant_messages = [
msg for msg in messages.data
if msg.run_id == run.id and msg.role == "assistant"
]
# Hiển thị nội dung tin nhắn của Assistant
for msg in assistant_messages:
for content_block in msg.content:
if content_block.type == 'text':
print(f"Assistant: {content_block.text.value}")
# Có thể xử lý các loại nội dung khác như 'image_file' nếu có
Ở đây, chúng ta lấy danh sách tin nhắn trong Thread, sắp xếp theo thứ tự tăng dần và lọc ra những tin nhắn mới được tạo bởi Run vừa rồi và có vai trò là “assistant”. Nội dung tin nhắn thường nằm trong `content` là một danh sách các block (ví dụ: văn bản, tệp hình ảnh).
Sử Dụng Các Công Cụ Với Assistant API
Sức mạnh thực sự của Assistant API nằm ở khả năng tích hợp liền mạch các công cụ.
Code Interpreter
Bật Code Interpreter cho phép Assistant thực thi code (chủ yếu là Python) trong môi trường sandbox để giải quyết các bài toán toán học, phân tích dữ liệu, hoặc xử lý các định dạng tệp được hỗ trợ. Bạn chỉ cần bật nó lên khi tạo Assistant:
tools=[{"type": "code_interpreter"}]
Khi Assistant quyết định cần dùng Code Interpreter, Run status sẽ chuyển sang `in_progress` và Run Step sẽ hiển thị hành động `tool_code`. Kết quả thực thi code sẽ được Assistant sử dụng để tạo phản hồi.
Retrieval (RAG)
Công cụ Retrieval cho phép Assistant truy xuất thông tin từ các tệp mà bạn cung cấp và sử dụng nó để trả lời câu hỏi. Điều này tương tự như kỹ thuật RAG mà chúng ta đã thảo luận trước đó, nhưng được tích hợp sẵn.
Để sử dụng Retrieval, bạn cần:
- Tải tệp lên OpenAI.
- Gắn tệp đó vào Assistant.
# Bước 1a: Tải tệp lên
file = client.files.create(
file=open("path/to/your/document.pdf", "rb"),
purpose='assistants' # Mục đích sử dụng cho Assistant
)
print(f"Đã tải tệp {file.filename} với ID: {file.id}")
file_id = file.id
# Bước 1b: Tạo Assistant với công cụ Retrieval và gắn tệp
assistant_with_retrieval = client.beta.assistants.create(
name="Trợ Lý Tài Liệu",
instructions="Bạn là trợ lý trả lời câu hỏi dựa trên các tài liệu được cung cấp.",
model="gpt-4o",
tools=[{"type": "retrieval"}],
file_ids=[file_id] # Gắn tệp vào Assistant
)
print(f"Đã tạo Assistant với Retrieval và gắn tệp {file.filename}")
assistant_retrieval_id = assistant_with_retrieval.id
Khi người dùng đặt câu hỏi liên quan đến nội dung trong tệp, Assistant sẽ tự động sử dụng công cụ Retrieval để tìm kiếm và trích xuất thông tin phù hợp trước khi tạo phản hồi. Run Step sẽ hiển thị hành động `tool_code` (bởi vì Retrieval được triển khai bằng cách sử dụng Code Interpreter nội bộ để tìm kiếm trong tệp).
Function Calling
Đây là công cụ mạnh mẽ nhất, cho phép Assistant tương tác với các API hoặc dịch vụ bên ngoài mà bạn cung cấp. Bạn định nghĩa các hàm có sẵn (ví dụ: tra cứu thông tin sản phẩm, gửi email, lấy dữ liệu thời tiết) bằng schema JSON, và Assistant sẽ quyết định khi nào cần gọi hàm nào, với tham số gì.
Quá trình Function Calling với Assistant API phức tạp hơn một chút so với Chat Completions API vì nó là một phần của vòng lặp Run:
- Bạn định nghĩa hàm bằng JSON schema và gắn vào Assistant.
- Trong một Run, Assistant quyết định cần gọi hàm nào, và Run status chuyển sang `requires_action`.
- API trả về danh sách các yêu cầu gọi hàm trong `run.required_action.submit_tool_outputs.tool_calls`.
- Ứng dụng của bạn cần phân tích các yêu cầu này, thực thi các hàm tương ứng.
- Gửi kết quả thực thi hàm trở lại cho Assistant bằng `client.beta.threads.runs.submit_tool_outputs`.
- Assistant tiếp tục Run với thông tin mới nhận được.
Định nghĩa hàm và gắn vào Assistant:
# Ví dụ về định nghĩa hàm (JSON schema)
function_schema = {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}
# Tạo Assistant với công cụ Function Calling
assistant_with_functions = client.beta.assistants.create(
name="Trợ Lý Thời Tiết",
instructions="Bạn là trợ lý thời tiết. Sử dụng các hàm được cung cấp để trả lời câu hỏi về thời tiết hiện tại.",
model="gpt-4o",
tools=[{"type": "function", "function": function_schema}]
)
print(f"Đã tạo Assistant với Function Calling, ID: {assistant_with_functions.id}")
assistant_function_id = assistant_with_functions.id
Xử lý `requires_action` và gửi kết quả:
# ... (Code từ Bước 1-4) ...
# Bước 5 (tiếp theo): Xử lý requires_action và gửi kết quả hàm
while run.status in ['queued', 'in_progress', 'requires_action']:
time.sleep(1)
run = client.beta.threads.runs.retrieve(
thread_id=thread_id,
run_id=run.id
)
print(f"Trạng thái Run: {run.status}")
if run.status == 'requires_action':
print("Run yêu cầu thực hiện hành động (Function Calling).")
tool_outputs = []
for tool_call in run.required_action.submit_tool_outputs.tool_calls:
if tool_call.function.name == "get_current_weather":
# Thực thi hàm của bạn ở đây (ví dụ: gọi API thời tiết)
location = json.loads(tool_call.function.arguments).get("location")
unit = json.loads(tool_call.function.arguments).get("unit", "fahrenheit")
# Đây là nơi bạn gọi hàm thực tế và lấy kết quả
# weather_data = your_weather_api_call(location, unit)
weather_data = "25 độ C, trời nắng" # Ví dụ kết quả giả
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": weather_data # Kết quả từ việc gọi hàm của bạn
})
print(f"Đã thực thi hàm {tool_call.function.name} và chuẩn bị gửi kết quả.")
# Gửi kết quả của hàm trở lại Assistant
if tool_outputs:
run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run.id,
tool_outputs=tool_outputs
)
print("Đã gửi kết quả hàm. Assistant sẽ tiếp tục Run.")
elif run.status == 'completed':
print("Run đã hoàn thành.")
# Đến bước 6 để hiển thị tin nhắn
elif run.status == 'failed':
print(f"Run thất bại: {run.last_error}")
# ... (Code Bước 6) ...
Ví dụ trên minh họa cách xử lý một Function Call duy nhất. Trong thực tế, một Run có thể yêu cầu nhiều Function Call cùng lúc, và bạn cần xử lý tất cả chúng trước khi gửi kết quả. Điều này tạo ra một vòng lặp tương tác giữa Assistant và ứng dụng của bạn cho đến khi Assistant tự tin rằng nó đã thu thập đủ thông tin để trả lời yêu cầu ban đầu của người dùng.
Tóm Tắt Các Đối Tượng Chính
Dưới đây là bảng tóm tắt các đối tượng cốt lõi của Assistant API để bạn dễ hình dung:
Đối Tượng | Mô Tả | Vai Trò | Vòng Đời |
---|---|---|---|
Assistant | Định nghĩa đặc vụ với hướng dẫn, mô hình và công cụ. | Bản thiết kế của đặc vụ. | Tạo một lần, sử dụng lại nhiều lần trên các Thread khác nhau. |
Thread | Lưu trữ lịch sử hội thoại (Messages). | Đại diện cho một phiên hội thoại duy nhất. | Tạo cho mỗi phiên hội thoại, tồn tại cho đến khi bị xóa. |
Message | Một đơn vị hội thoại (người dùng hoặc Assistant), chứa nội dung. | Thành phần của lịch sử hội thoại. | Được tạo trong Thread, tồn tại cùng với Thread. |
Run | Phiên xử lý nơi Assistant đọc Messages trong Thread và tạo phản hồi. | Thực thi logic của Assistant trên Thread. | Tạo cho mỗi lần Assistant cần xử lý Thread (thường là sau khi có tin nhắn mới của người dùng). |
Run Step | Chi tiết các hành động của Assistant trong một Run (gọi mô hình, sử dụng công cụ). | Ghi lại quá trình làm việc của Assistant. | Được tạo trong quá trình Run, tồn tại cùng với Run. |
Kết Luận
OpenAI Assistant API cung cấp một framework mạnh mẽ và linh hoạt để xây dựng các đặc vụ AI phức tạp, có khả năng duy trì trạng thái, sử dụng công cụ và truy xuất thông tin từ các nguồn bên ngoài. Bằng cách trừu tượng hóa nhiều chi tiết quản lý backend, nó cho phép các kỹ sư AI tập trung vào việc định nghĩa hành vi của đặc vụ và tích hợp nó vào ứng dụng của họ.
Việc nắm vững các khái niệm cốt lõi như Assistant, Thread, Message và Run, cùng với việc hiểu cách tích hợp các công cụ như Code Interpreter, Retrieval và Function Calling, là bước tiến quan trọng trên lộ trình trở thành một kỹ sư AI chuyên nghiệp.
Hãy thử nghiệm với Assistant API, xây dựng những đặc vụ tùy chỉnh của riêng bạn và khám phá tiềm năng to lớn của nó trong việc tạo ra các ứng dụng AI thông minh và tương tác hơn. Trong các bài viết tiếp theo, chúng ta sẽ tiếp tục khám phá sâu hơn các khía cạnh khác của AI Engineering.