Bài 1: LLM APIs Hands-on¶
Tổng quan¶
Bài này trang bị cho bạn khả năng làm việc trực tiếp với LLM thông qua API: gọi completions, kiểm soát output bằng parameters, và áp dụng các kỹ thuật prompting cơ bản đến nâng cao.
1. Chat Completions¶
Cấu trúc cơ bản¶
Mọi LLM API (OpenAI, Anthropic, Gemini, v.v.) đều xoay quanh một endpoint chính: chat completions - nhận vào một danh sách messages, trả về message của assistant.
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Bạn là trợ lý hữu ích."},
{"role": "user", "content": "Giải thích RAG trong 2 câu."}
]
)
print(response.choices[0].message.content)
Cấu trúc Messages¶
| Role | Mục đích |
|---|---|
system |
Thiết lập persona, quy tắc, ngữ cảnh cho toàn bộ cuộc hội thoại |
user |
Input từ người dùng |
assistant |
Phản hồi của model (dùng để xây dựng few-shot hoặc conversation history) |
2. Parameters Quan Trọng¶
Temperature¶
Kiểm soát mức độ ngẫu nhiên trong output.
| Giá trị | Hành vi | Dùng khi |
|---|---|---|
0.0 |
Deterministic, luôn chọn token có xác suất cao nhất | Code generation, fact extraction, structured output |
0.3–0.7 |
Cân bằng giữa nhất quán và sáng tạo | Chatbot, summarization |
0.8–1.0 |
Sáng tạo, đa dạng | Brainstorming, creative writing |
Lưu ý
Temperature không ảnh hưởng đến "độ thông minh" - chỉ ảnh hưởng đến tính ngẫu nhiên. Model vẫn có thể sai dù temperature = 0.
max_tokens¶
Giới hạn số token trong output. Quan trọng để:
- Kiểm soát chi phí (billing theo token)
- Tránh output bị cắt giữa chừng (nên đặt đủ lớn)
- Buộc model trả lời ngắn gọn
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Tóm tắt RAG."}],
temperature=0.2,
max_tokens=150,
)
top_p (Nucleus Sampling)¶
Thay vì xét toàn bộ vocabulary, model chỉ chọn token từ nhóm nhỏ nhất có tổng xác suất tích lũy ≥ top_p.
Giả sử model dự đoán token tiếp theo với xác suất:
"mèo" : 0.5
"chó" : 0.3 ┐
"chim" : 0.1 ├─ top_p = 0.9 -> chỉ xét {mèo, chó, chim} (tổng 0.9)
"cá" : 0.05 ┘ loại bỏ "cá", "rắn"...
"rắn" : 0.05
| Giá trị | Hành vi |
|---|---|
1.0 |
Xét toàn bộ phân phối (mặc định) |
0.9 |
Cân bằng - loại bỏ các token đuôi xác suất thấp |
0.1–0.3 |
Rất tập trung, gần như deterministic |
Không nên chỉnh đồng thời temperature và top_p
Cả hai đều kiểm soát tính ngẫu nhiên theo cơ chế khác nhau. Khuyến nghị chỉ chỉnh một trong hai, giữ cái còn lại ở giá trị mặc định để tránh tương tác khó dự đoán.
Các parameters khác¶
| Parameter | Mô tả | Giá trị phổ biến |
|---|---|---|
frequency_penalty |
Phạt token xuất hiện nhiều lần → giảm lặp | 0.0–1.0 |
presence_penalty |
Khuyến khích model đề cập chủ đề mới | 0.0–1.0 |
seed |
Cố định kết quả (hỗ trợ reproducibility) | Bất kỳ integer |
3. Streaming¶
Thay vì đợi toàn bộ response, streaming trả về token theo thời gian thực - trải nghiệm người dùng tốt hơn nhiều.
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Kể một câu chuyện ngắn."}],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
Khi nào dùng streaming:
- Chat interfaces (hiển thị text ngay khi có)
- Response dài (> 500 tokens)
- Cần first-token latency thấp
4. Rate Limits & API Key Rotation¶
Rate Limit là gì?¶
Mọi LLM provider đều giới hạn số lượng request để bảo vệ hạ tầng. Khi vượt ngưỡng, API trả về lỗi HTTP 429 (Too Many Requests).
| Đơn vị giới hạn | Viết tắt | Ý nghĩa |
|---|---|---|
| Requests per minute | RPM | Số lời gọi API tối đa mỗi phút |
| Tokens per minute | TPM | Tổng số token (input + output) mỗi phút |
| Requests per day | RPD | Số lời gọi tối đa mỗi ngày (thường ở tier thấp) |
Tier càng cao, limit càng lớn
Provider thường nâng limit tự động theo lịch sử thanh toán (usage tier). Tài khoản mới luôn bắt đầu ở tier thấp với RPM/TPM khiêm tốn.
Xử lý 429: Exponential Backoff¶
Khi gặp 429, không retry ngay lập tức - hãy đợi với thời gian tăng dần (kèm jitter để tránh nhiều client retry cùng lúc).
import time
import random
from openai import OpenAI, RateLimitError
client = OpenAI()
def chat_with_retry(messages, max_retries=5):
for attempt in range(max_retries):
try:
return client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
except RateLimitError:
if attempt == max_retries - 1:
raise
# Exponential backoff: 1s, 2s, 4s, 8s... + jitter
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited. Đợi {wait:.1f}s rồi thử lại...")
time.sleep(wait)
Tôn trọng header Retry-After
Nhiều provider trả về header Retry-After (số giây nên đợi) trong response 429. Ưu tiên dùng giá trị này thay vì tự tính backoff khi có.
API Key Rotation¶
Khi cần throughput vượt limit của một key, có thể luân phiên (rotate) qua nhiều API key - mỗi key có hạn mức riêng.
import itertools
from openai import OpenAI
# Danh sách key (đọc từ env, không hardcode)
API_KEYS = [
"sk-key-1",
"sk-key-2",
"sk-key-3",
]
# Vòng lặp vô hạn luân phiên qua các key
key_cycle = itertools.cycle(API_KEYS)
def get_client():
return OpenAI(api_key=next(key_cycle))
# Mỗi request dùng key kế tiếp
response = get_client().chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Xin chào!"}],
)
Rotation nâng cao - tự động loại key đang bị rate limit:
from collections import deque
import time
class RotatingKeyPool:
def __init__(self, keys):
self.keys = deque(keys)
self.cooldown = {} # key -> thời điểm hết cooldown
def get_key(self):
now = time.time()
for _ in range(len(self.keys)):
key = self.keys[0]
self.keys.rotate(-1)
if self.cooldown.get(key, 0) <= now:
return key
# Tất cả key đang cooldown -> đợi
time.sleep(1)
return self.get_key()
def mark_limited(self, key, cooldown_seconds=60):
self.cooldown[key] = time.time() + cooldown_seconds
Bảo mật API key
- Luôn đọc key từ biến môi trường (
os.environ) hoặc secrets manager, không hardcode trong code. - Không commit key lên git - thêm
.envvào.gitignore. - Rotation chỉ là giải pháp tạm thời. Nếu cần throughput cao ổn định, hãy yêu cầu provider nâng tier hoặc dùng batch API.
5. Prompt Patterns¶
Zero-shot Prompting¶
Không cung cấp ví dụ - chỉ mô tả task.
prompt = """Phân loại sentiment của review sau thành: Tích cực / Tiêu cực / Trung lập.
Review: "Sản phẩm ổn nhưng giao hàng chậm quá."
Sentiment:"""
Phù hợp khi: Task đơn giản, rõ ràng; model đã được train trên task tương tự.
Few-shot Prompting¶
Cung cấp 2–5 ví dụ (input → output) trước khi đưa ra câu hỏi thực.
prompt = """Phân loại sentiment:
Review: "Chất lượng tuyệt vời, sẽ mua lại!" → Tích cực
Review: "Hàng kém chất lượng, thất vọng." → Tiêu cực
Review: "Bình thường, không có gì đặc biệt." → Trung lập
Review: "Thiết kế đẹp nhưng pin yếu." → """
Phù hợp khi: Task có format output cụ thể; cần định hướng tone/style cho model.
Chọn examples tốt
Examples nên đa dạng, bao phủ các edge case. Thứ tự examples ảnh hưởng đến kết quả - ví dụ gần nhất thường có trọng số cao hơn.
Chain-of-Thought (CoT)¶
Yêu cầu model "suy nghĩ từng bước" trước khi đưa ra câu trả lời. Cải thiện đáng kể độ chính xác trên các task đòi hỏi lý luận.
Khi nào dùng CoT:
- Bài toán toán học, logic
- Multi-step reasoning
- Code debugging
- Task phân tích phức tạp
Role Prompting¶
Gán persona cụ thể cho model để định hướng tone, style và domain expertise.
messages = [
{
"role": "system",
"content": """Bạn là chuyên gia pháp lý Việt Nam với 20 năm kinh nghiệm
về luật doanh nghiệp. Trả lời chính xác, trích dẫn điều luật khi có thể,
và luôn khuyến nghị tham khảo luật sư cho các vụ việc cụ thể."""
},
{
"role": "user",
"content": "Điều kiện để thành lập công ty TNHH 2 thành viên là gì?"
}
]
Best practices:
- Cụ thể hoá persona (không chỉ "chuyên gia" mà "chuyên gia X với Y năm kinh nghiệm về Z")
- Định nghĩa rõ format output mong muốn trong system prompt
- Kết hợp role prompting với few-shot để kiểm soát chặt hơn
6. Structured Outputs với Pydantic¶
Vấn đề¶
LLM mặc định trả về text tự do - khó parse, không đáng tin cậy về format. Structured outputs đảm bảo output luôn theo schema định trước.
Cách tiếp cận với Pydantic + OpenAI¶
from pydantic import BaseModel
from openai import OpenAI
class ProductReview(BaseModel):
sentiment: str # "positive" | "negative" | "neutral"
score: int # 1-5
key_points: list[str] # Các điểm chính
summary: str # Tóm tắt ngắn
client = OpenAI()
review_text = "Pin trâu, camera đẹp nhưng giá hơi cao so với phân khúc."
completion = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Phân tích review sản phẩm."},
{"role": "user", "content": review_text}
],
response_format=ProductReview,
)
result = completion.choices[0].message.parsed
print(result.sentiment) # "neutral"
print(result.score) # 3
print(result.key_points) # ["pin trâu", "camera đẹp", "giá cao"]
Pydantic Validators¶
from pydantic import BaseModel, Field, field_validator
class AnalysisResult(BaseModel):
category: str = Field(description="Danh mục sản phẩm")
score: int = Field(ge=1, le=5, description="Điểm đánh giá từ 1-5")
tags: list[str] = Field(max_length=5, description="Tối đa 5 tags")
@field_validator('category')
def validate_category(cls, v):
allowed = ["electronics", "fashion", "food", "other"]
if v not in allowed:
raise ValueError(f"Phải là một trong: {allowed}")
return v
Lợi ích:
- Không cần regex parsing hay try/except JSON
- Schema được enforce ở model level
- Type-safe - IDE hỗ trợ autocomplete
7. Function Calling¶
Khái niệm¶
Function calling cho phép model "quyết định" gọi function nào với arguments nào - model không thực thi function, chỉ trả về JSON mô tả lời gọi. Code của bạn thực thi rồi trả kết quả về.
User → Model → [Quyết định gọi function X với args Y] → Code thực thi → Model nhận kết quả → Response
Ví dụ: Tra thời tiết¶
import json
from openai import OpenAI
client = OpenAI()
# Định nghĩa tools
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Lấy thông tin thời tiết hiện tại của một thành phố",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Tên thành phố, ví dụ: Hà Nội, TP.HCM"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Đơn vị nhiệt độ"
}
},
"required": ["city"]
}
}
}
]
# Bước 1: Gọi model
messages = [{"role": "user", "content": "Thời tiết Hà Nội hôm nay thế nào?"}]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
# Bước 2: Kiểm tra model có muốn gọi function không
if response.choices[0].finish_reason == "tool_calls":
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
# Bước 3: Thực thi function thực tế
weather_result = get_weather(city=args["city"]) # Hàm của bạn
# Bước 4: Trả kết quả về model
messages.append(response.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(weather_result)
})
final_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
print(final_response.choices[0].message.content)
Parallel Function Calling¶
Model có thể gọi nhiều functions cùng lúc:
# User: "So sánh thời tiết Hà Nội và TP.HCM"
# Model có thể gọi get_weather("Hà Nội") VÀ get_weather("TP.HCM") song song
Ứng dụng phổ biến của Function Calling:
- Tìm kiếm web / database
- Đặt lịch, gửi email, tạo ticket
- Tính toán / gọi API bên ngoài
- Xây dựng AI agents
Tóm tắt¶
graph TD
A[LLM API Request] --> B{Loại task?}
B --> |Output tự do| C[Điều chỉnh Temperature]
B --> |Output có cấu trúc| D[Structured Outputs + Pydantic]
B --> |Cần dữ liệu bên ngoài| E[Function Calling]
C --> F{Độ phức tạp?}
F --> |Đơn giản| G[Zero-shot]
F --> |Cần format cụ thể| H[Few-shot]
F --> |Cần lý luận| I[Chain-of-Thought]
F --> |Cần domain expertise| J[Role Prompting]
| Kỹ thuật | Khi nào dùng |
|---|---|
| Zero-shot | Task đơn giản, rõ ràng |
| Few-shot | Cần format / tone cụ thể |
| CoT | Reasoning, toán, logic |
| Role prompting | Domain-specific, kiểm soát persona |
| Structured outputs | Pipeline cần parse output |
| Function calling | Cần tương tác với hệ thống bên ngoài |