Khóa học
Vào tháng 8/2024, OpenAI đã công bố một tính năng mạnh mẽ mới trong API của họ — Structured Outputs. Đúng như tên gọi, với tính năng này, bạn có thể đảm bảo LLM chỉ tạo phản hồi theo đúng định dạng bạn chỉ định. Khả năng này sẽ giúp việc xây dựng các ứng dụng yêu cầu định dạng dữ liệu chính xác trở nên dễ dàng hơn rất nhiều.
Trong hướng dẫn này, bạn sẽ học cách bắt đầu với Structured Outputs của OpenAI, hiểu cú pháp mới và khám phá các ứng dụng chủ chốt của nó.
Tầm quan trọng của Structured Outputs trong các ứng dụng AI
Các phản hồi quyết định (deterministic), hay nói cách khác, phản hồi có định dạng nhất quán, rất quan trọng cho nhiều tác vụ như nhập dữ liệu, truy xuất thông tin, hỏi đáp, quy trình nhiều bước, v.v. Bạn có thể đã từng thấy LLM tạo đầu ra với các định dạng khác nhau hoàn toàn, dù prompt giống hệt nhau.
Ví dụ, hãy xét hàm classify_sentiment đơn giản sử dụng GPT-4o:
# List of hotel reviews
reviews = [
"The room was clean and the staff was friendly.",
"The location was terrible and the service was slow.",
"The food was amazing but the room was too small.",
]
# Classify sentiment for each review and print the results
for review in reviews:
sentiment = classify_sentiment(review)
print(f"Review: {review}\nSentiment: {sentiment}\n")
Đầu ra:
Review: The room was clean and the staff was friendly.
Sentiment: Positive
Review: The location was terrible and the service was slow.
Sentiment: Negative
Review: The food was amazing but the room was too small.
Sentiment: The sentiment of the review is neutral.
Mặc dù hai phản hồi đầu có cùng định dạng một từ, phản hồi cuối lại là cả một câu. Nếu một ứng dụng hạ nguồn khác phụ thuộc vào đầu ra của đoạn mã trên, nó có thể bị lỗi do kỳ vọng một phản hồi một từ.
Chúng ta có thể khắc phục bằng prompt engineering, nhưng đó là một quá trình lặp lại tốn thời gian. Ngay cả với một prompt hoàn hảo, ta cũng không thể chắc 100% rằng các phản hồi tương lai sẽ tuân theo định dạng. Trừ khi, tất nhiên, chúng ta dùng Structured Outputs:
def classify_sentiment_with_structured_outputs(review):
"""Sentiment classifier with Structured Outputs"""
...
# Classify sentiment for each review with Structured Outputs
for review in reviews:
sentiment = classify_sentiment_with_structured_outputs(review)
print(f"Review: {review}\nSentiment: {sentiment}\n")
Đầu ra:
Review: The room was clean and the staff was friendly.
Sentiment: {"sentiment":"positive"}
Review: The location was terrible and the service was slow.
Sentiment: {"sentiment":"negative"}
Review: The food was amazing but the room was too small.
Sentiment: {"sentiment":"neutral"}
Với hàm mới classify_sentiment_with_structured_outputs, các phản hồi đều có cùng định dạng.
Khả năng buộc mô hình ngôn ngữ tuân theo định dạng chặt chẽ này rất quan trọng, giúp bạn tiết kiệm vô số giờ làm prompt engineering hoặc phụ thuộc vào các công cụ mã nguồn mở khác.
Bắt đầu với Structured Outputs của OpenAI
Trong phần này, chúng ta sẽ phân tích structured outputs qua ví dụ hàm phân tích cảm xúc.
Thiết lập môi trường
Điều kiện tiên quyết
Trước khi bắt đầu, hãy đảm bảo bạn có:
- Python 3.7 trở lên cài đặt trên hệ thống.
- Khoá API OpenAI. Bạn có thể lấy bằng cách đăng ký trên trang web OpenAI.
Thiết lập OpenAI API
1. Cài đặt gói OpenAI cho Python: Mở terminal và chạy lệnh sau để cài đặt hoặc cập nhật gói OpenAI Python lên phiên bản mới nhất:
$ pip install -U openai
2. Thiết lập khoá API: Bạn có thể đặt khoá API dưới dạng biến môi trường hoặc trực tiếp trong mã. Để đặt làm biến môi trường, chạy:
$ export OPENAI_API_KEY='your-api-key'
3. Xác minh cài đặt: Tạo một script Python đơn giản để kiểm tra:
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Say hello!"}
],
max_tokens=5
)
>>> print(response.choices[0].message.content.strip())
Hello! How can I
Chạy script để đảm bảo mọi thứ đã thiết lập đúng. Bạn sẽ thấy phản hồi của mô hình in ra trong terminal.
Ngoài gói OpenAI, bạn sẽ cần thư viện Pydantic để định nghĩa và xác thực JSON schema cho Structured Outputs. Cài đặt bằng pip:
$ pip install pydantic
Với các bước này, môi trường của bạn đã sẵn sàng để sử dụng tính năng Structured Outputs của OpenAI.
Định nghĩa schema đầu ra bằng Pydantic
Để dùng Structured Outputs, bạn cần định nghĩa cấu trúc đầu ra mong đợi bằng các model Pydantic. Pydantic là thư viện xác thực dữ liệu và quản lý thiết lập cho Python, cho phép bạn định nghĩa model dữ liệu bằng chú thích kiểu (type annotations). Các model này có thể được dùng để ép buộc cấu trúc của đầu ra do các mô hình của OpenAI sinh ra.
Dưới đây là ví dụ model Pydantic để chỉ định định dạng cho bộ phân loại cảm xúc đánh giá của chúng ta:
from pydantic import BaseModel
from typing import Literal
class SentimentResponse(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
Trong ví dụ này:
SentimentResponselà một model Pydantic định nghĩa cấu trúc đầu ra mong đợi.- Model có một trường
sentimentduy nhất, chỉ nhận một trong ba giá trị literal: "positive", "negative" hoặc "neutral".
Khi truyền model này trong yêu cầu API OpenAI, đầu ra sẽ chỉ là một trong các từ chúng ta đã cung cấp.
Hãy xem cách làm.
Sử dụng trợ giúp parse
Để áp dụng schema Pydantic của chúng ta trong các yêu cầu OpenAI, tất cả những gì cần làm là truyền nó vào tham số response_format của Chat Completions API. Đại khái sẽ như sau:
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[...],
response_format=SentimentResponse
)
Bạn sẽ thấy, thay vì dùng client.chat.completions.create, chúng ta dùng phương thức client.beta.chat.completions.parse. .parse() là phương thức mới trong Chat Completions API dành riêng cho Structured Outputs.
Giờ hãy ghép mọi thứ lại bằng cách viết lại bộ phân loại cảm xúc đánh giá với Structured Outputs. Trước tiên, thực hiện import cần thiết, định nghĩa model Pydantic, system prompt và mẫu prompt:
from openai import OpenAI
from pydantic import BaseModel
from typing import Literal
class SentimentResponse(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
client = OpenAI()
MODEL = "gpt-4o-mini"
SYSTEM_PROMPT = "You are a sentiment classifier assistant."
PROMPT_TEMPLATE = """
Classify the sentiment of the following hotel review as positive, negative, or neutral:\n\n{review}
"""
Sau đó, viết hàm mới dùng phương thức trợ giúp .parse():
# Function to classify sentiment using OpenAI's chat completions API with structured outputs
def classify_sentiment_with_structured_outputs(review):
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": PROMPT_TEMPLATE.format(review=review)},
],
response_format=SentimentResponse
)
return response.choices[0].message
Dòng quan trọng trong hàm là response_format=SentimentResponse, chính là thứ kích hoạt Structured Outputs.
Hãy thử với một đánh giá:
# List of hotel reviews
reviews = [
"The room was clean and the staff was friendly.",
"The location was terrible and the service was slow.",
"The food was amazing but the room was too small.",
]
result = classify_sentiment_with_structured_outputs(reviews[0])
>>> print(result.content)
{"sentiment":"positive"}
Ở đây, result là một đối tượng message:
>>> type(result)
openai.types.chat.parsed_chat_completion.PparsedChatCompletionMessage[SentimentResponse]
Ngoài thuộc tính .content dùng để lấy phản hồi, nó còn có thuộc tính .parsed trả về thông tin đã parse dưới dạng lớp:
>>> result.parsed
SentimentResponse(sentiment='positive')
Như bạn thấy, chúng ta nhận được một instance của lớp SentimentResponse. Điều này có nghĩa là ta có thể truy cập cảm xúc dưới dạng chuỗi thay vì dictionary bằng thuộc tính .sentiment:
>>> result.parsed.sentiment
'positive'
Lồng các model Pydantic để định nghĩa schema phức tạp
Trong một số trường hợp, bạn có thể cần định nghĩa cấu trúc đầu ra phức tạp hơn với dữ liệu lồng nhau. Pydantic cho phép bạn lồng các model vào nhau, giúp tạo các schema tinh vi có thể xử lý nhiều tình huống sử dụng. Điều này đặc biệt hữu ích khi làm việc với dữ liệu phân cấp hoặc khi cần áp đặt cấu trúc cụ thể cho các đầu ra phức tạp.
Hãy xét ví dụ cần trích xuất thông tin người dùng chi tiết, gồm tên, thông tin liên hệ và danh sách địa chỉ. Mỗi địa chỉ cần có các trường đường phố, thành phố, bang và mã bưu chính. Điều này đòi hỏi nhiều hơn một model Pydantic để xây dựng schema đúng.
Bước 1: Định nghĩa các model Pydantic
Trước tiên, định nghĩa các model Pydantic cho địa chỉ và thông tin người dùng:
from pydantic import BaseModel
from typing import List
# Define the Pydantic model for an address
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str
# Define the Pydantic model for user information
class UserInfo(BaseModel):
name: str
email: str
phone: str
addresses: List[Address]
Trong ví dụ này:
Addresslà model Pydantic định nghĩa cấu trúc của một địa chỉ.UserInfolà model Pydantic bao gồm danh sách các đối tượngAddress, cùng các trường tên, email và số điện thoại của người dùng.
Bước 2: Dùng các model Pydantic lồng nhau trong lời gọi API
Tiếp theo, dùng các model Pydantic lồng nhau này để áp đặt cấu trúc đầu ra trong một lời gọi OpenAI API:
SYSTEM_PROMPT = "You are a user information extraction assistant."
PROMPT_TEMPLATE = """ Extract the user information from the following text:\n\n{text}"""
# Function to extract user information using OpenAI's chat completions API with structured outputs
def extract_user_info(text):
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": PROMPT_TEMPLATE.format(text=text)},
],
response_format=UserInfo
)
return response.choices[0].message
# Example text containing user information
text = """John DoeEmail: john.doe@example.comPhone: 123-456-7890Addresses:- 123 Main St, Springfield, IL, 62701- 456 Elm St, Shelbyville, IL, 62702"""
# Extract user information and print the results
user_info = extract_user_info(text)
Đoạn văn mẫu hoàn toàn khó đọc và thiếu khoảng trắng giữa các phần thông tin chính. Hãy xem mô hình có xử lý được không. Chúng ta sẽ dùng thư viện json để định dạng đẹp phản hồi:
import json
data = json.loads(user_info.content)
pretty_response = json.dumps(data, indent=2)
print(pretty_response)
{
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "123-456-7890",
"addresses": [
{
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip_code": "62701"
},
{
"street": "456 Elm St",
"city": "Shelbyville",
"state": "IL",
"zip_code": "62702"
}
]
}
Như bạn thấy, mô hình đã trích xuất chính xác thông tin của một người dùng cùng hai địa chỉ tách biệt theo schema đã cung cấp.
Tóm lại, bằng cách lồng các model Pydantic, bạn có thể định nghĩa các schema phức tạp xử lý dữ liệu phân cấp và áp đặt cấu trúc cụ thể cho các đầu ra tinh vi.
Gọi hàm (Function Calling) với Structured Outputs
Một trong những tính năng phổ biến của các mô hình ngôn ngữ mới là gọi hàm (còn gọi là tool calling). Khả năng này cho phép bạn kết nối mô hình ngôn ngữ với các hàm do người dùng định nghĩa, về bản chất là đem đến cho mô hình khả năng truy cập thế giới bên ngoài.
Một số ví dụ phổ biến:
- Truy xuất dữ liệu thời gian thực (ví dụ: thời tiết, giá cổ phiếu, tỉ số thể thao)
- Thực hiện tính toán hoặc phân tích dữ liệu
- Truy vấn cơ sở dữ liệu hoặc API
- Tạo hình ảnh hoặc media khác
- Dịch văn bản giữa các ngôn ngữ
- Điều khiển thiết bị nhà thông minh hoặc hệ thống IoT
- Thực thi logic nghiệp vụ hoặc quy trình tuỳ chỉnh
Chúng tôi sẽ không đi sâu vào chi tiết cách hoạt động của function calling ở đây, nhưng bạn có thể đọc hướng dẫn OpenAI Function Calling của chúng tôi.
Điều quan trọng cần biết là với Structured Outputs, việc dùng function calling với các mô hình OpenAI trở nên dễ dàng hơn nhiều. Trước đây, các hàm bạn truyền vào mô hình OpenAI yêu cầu viết các JSON schema phức tạp, liệt kê mọi tham số hàm với gợi ý kiểu. Ví dụ như sau:
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
}
}
Dù hàm get_current_weather chỉ có hai tham số, JSON schema của nó trở nên rất dài và dễ sai sót nếu viết thủ công.
Structured Outputs giải quyết điều này bằng cách tiếp tục dùng các model Pydantic:
from pydantic import BaseModel
from typing import Literal
def get_weather(location: str, unit: str, condition: str):
# Implementation details...
pass
class WeatherData(BaseModel):
location: str
unit: Literal["celsius", "fahrenheit"]
condition: Literal["sunny", "cloudy", "rainy", "snowy"]
Trước tiên, bạn viết chính hàm và logic của nó. Sau đó, bạn định nghĩa lại bằng một model Pydantic nêu rõ các tham số đầu vào mong đợi.
Tiếp theo, để chuyển model Pydantic thành JSON schema tương thích, bạn gọi pydantic_function_tool:
>>> import openai
>>> openai.pydantic_function_tool(WeatherData)
{'type': 'function',
'function': {'name': 'WeatherData',
'strict': True,
'parameters': {'properties': {'location': {'title': 'Location',
'type': 'string'},
'unit': {'enum': ['celsius', 'fahrenheit'],
'title': 'Unit',
'type': 'string'},
'condition': {'enum': ['sunny', 'cloudy', 'rainy', 'snowy'],
'title': 'Condition',
'type': 'string'}},
'required': ['location', 'unit', 'condition'],
'title': 'WeatherData',
'type': 'object',
'additionalProperties': False}}}
Đây là cách dùng công cụ này trong một yêu cầu:
import openai
client = OpenAI()
tools = [openai.pydantic_function_tool(WeatherData)]
messages = [
{
"role": "system",
"content": "You are a helpful customer support assistant. Use the supplied tools to assist the user.",
},
{
"role": "user",
"content": "What is the weather in Tokyo?",
}
]
response = client.chat.completions.create(
model=MODEL, messages=messages, tools=tools
)
tool_call = response.choices[0].message.tool_calls[0]
>>> tool_call
ChatCompletionMessageToolCall(id='call_QnZZ0DmNN2cxw3bN433JQNIC', function=Function(arguments='{"location":"Tokyo","unit":"celsius","condition":"sunny"}', name='WeatherData'), type='function')
Chúng ta truyền model Pydantic dưới dạng JSON tương thích vào tham số tools của Chat Completions API. Sau đó, tùy theo truy vấn, mô hình sẽ quyết định có gọi công cụ hay không.
Vì truy vấn của chúng ta ở ví dụ trên là “Thời tiết ở Tokyo thế nào?”, ta thấy một lệnh gọi trong tool_calls của đối tượng message trả về.
Hãy nhớ, mô hình không gọi hàm get_weather mà chỉ tạo các tham số cho hàm dựa trên schema Pydantic mà ta cung cấp:
arguments = json.loads(tool_call.function.arguments)
>>> arguments
{'location': 'Tokyo', 'unit': 'celsius', 'condition': 'sunny'}
Phần còn lại là tùy chúng ta gọi hàm với các tham số đã cung cấp:
some_result = get_weather(**arguments)
Nếu bạn muốn mô hình vừa tạo tham số vừa gọi hàm cùng lúc, bạn đang cần một AI agent.
Chúng tôi có một hướng dẫn LangChain Agents riêng nếu bạn quan tâm.
Thực tiễn tốt nhất khi dùng Structured Outputs của OpenAI
Khi dùng Structured Outputs, có một số thực tiễn tốt và khuyến nghị cần ghi nhớ. Phần này sẽ nêu một vài điểm.
- Dùng các model Pydantic để định nghĩa schema đầu ra, vì chúng mang lại cách định nghĩa cấu trúc mong đợi rõ ràng và an toàn kiểu dữ liệu.
- Giữ schema đơn giản và cụ thể để có kết quả chính xác nhất.
- Dùng kiểu dữ liệu phù hợp (
str,int,float,bool,List,Dict) để biểu diễn dữ liệu chính xác. - Dùng kiểu
Literalcho enum để định nghĩa các giá trị được phép cho trường. - Xử lý trường hợp mô hình từ chối. Khi dùng phương thức mới
.parse(), các đối tượng message có thuộc tính mới.refusalđể báo hiệu sự từ chối:
text = """John DoeEmail: john.doe@example.comPhone: 123-456-7890Addresses:- 123 Main St, Springfield, IL, 62701- 456 Elm St, Shelbyville, IL, 62702"""
user_info = extract_user_info(text)
if user_info.refusal:
print(user_info.refusal)
else:
print(user_info.content)
Đầu ra:
{"name":"John Doe","email":"john.doe@example.com","phone":"123-456-7890","addresses":[{"street":"123 Main St","city":"Springfield","state":"IL","zip_code":"62701"},{"street":"456 Elm St","city":"Shelbyville","state":"IL","zip_code":"62702"}]}
6. Cung cấp mô tả rõ ràng, ngắn gọn cho từng trường trong model Pydantic để nâng cao độ chính xác của đầu ra mô hình:
from pydantic import BaseModel, Field
class Person(BaseModel):
name: str = Field(..., description="The person's full name")
age: int = Field(..., description="The person's age in years")
occupation: str = Field(..., description="The person's current job or profession")
Những thực tiễn này sẽ giúp bạn tận dụng Structured Outputs hiệu quả nhất trong ứng dụng của mình.
Kết luận
Trong hướng dẫn này, chúng ta đã học cách bắt đầu với một tính năng mới của OpenAI API: Structured Outputs. Chúng ta đã thấy tính năng này buộc mô hình ngôn ngữ tạo đầu ra theo định dạng do ta chỉ định như thế nào. Chúng ta cũng đã học cách dùng nó cùng với function calling và xem qua một số thực tiễn tốt để tận dụng tối đa tính năng.
Dưới đây là một số tài nguyên liên quan để nâng cao hiểu biết của bạn:
Structured Outputs FAQs
Pydantic hoạt động như thế nào với Structured Outputs?
Các model Pydantic được dùng để định nghĩa schema cho cấu trúc đầu ra mong muốn, sau đó được truyền vào OpenAI API để ép buộc định dạng phản hồi.
Structured Outputs có thể dùng với function calling không?
Có. Structured Outputs có thể được dùng với function calling để đơn giản hoá việc định nghĩa tham số hàm và đầu ra mong đợi.
Lợi ích của việc sử dụng Structured Outputs là gì?
Lợi ích bao gồm định dạng phản hồi nhất quán, giảm nhu cầu hậu xử lý, cải thiện độ tin cậy trong ứng dụng AI và dễ tích hợp với hệ thống hiện có.
Có hạn chế nào khi sử dụng Structured Outputs không?
Mặc dù mạnh mẽ, Structured Outputs có thể hạn chế sự linh hoạt của AI trong phản hồi và đòi hỏi thiết kế schema cẩn thận để cân bằng giữa cấu trúc và mức độ chi tiết mong muốn của đầu ra.
Tôi là người sáng tạo nội dung về khoa học dữ liệu với hơn 2 năm kinh nghiệm và là một trong những tài khoản có lượng người theo dõi lớn nhất trên Medium. Tôi thích viết các bài chuyên sâu về AI và ML với chút giọng điệu mỉa mai, vì bạn cũng phải làm gì đó để chúng bớt khô khan. Tôi đã xuất bản hơn 130 bài viết và một khóa học trên DataCamp, cùng với một khóa nữa đang được thực hiện. Nội dung của tôi đã được hơn 5 triệu lượt xem, trong đó 20 nghìn người đã trở thành người theo dõi trên cả Medium và LinkedIn.
