Bài này bạn sẽ dựng một tool registry — nơi tập trung khai báo schema và hàm thực thi — rồi nối nó vào vòng lặp model. Đây là bộ khung tái sử dụng cho mọi agent về sau. Sau khi có registry, thêm một tool mới chỉ là viết một hàm và một schema, đăng ký vào bảng; phần vòng lặp và dispatcher không phải sửa. Đó chính là giá trị của việc tách lớp ngay từ đầu.
Bước 1 — Chuẩn bị môi trường
- Cài SDK của nhà cung cấp (ví dụ Python):
pip install openaihoặcpip install anthropic. - Đặt API key qua biến môi trường, không hardcode:
export LLM_API_KEY="sk-..."
- Tạo file
tools.pycho registry vàagent.pycho vòng lặp.
Bước 2 — Viết hàm thực thi thật
Tách hàm nghiệp vụ khỏi phần LLM. Hàm nhận tham số Python thường, trả về dict:
def get_weather(city: str) -> dict:
# Gọi API thật; ở đây mock cho gọn
data = {"Da Nang": 31, "Ha Noi": 28}
if city not in data:
raise ValueError(f"Không có dữ liệu cho {city}")
return {"city": city, "temp_c": data[city]}
Bước 3 — Đăng ký tool vào registry
Gom schema và hàm vào một chỗ, dùng chung name làm khóa:
TOOLS = {
"get_weather": {
"schema": {
"name": "get_weather",
"description": "Lấy nhiệt độ hiện tại của một thành phố VN.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string",
"description": "Tên thành phố, vd 'Da Nang'"}
},
"required": ["city"]
}
},
"fn": get_weather
}
}
def tool_schemas():
return [t["schema"] for t in TOOLS.values()]
Bước 4 — Viết bộ dispatcher
Dispatcher tra registry theo name và gọi đúng hàm. Không bao giờ dùng eval trên tên model trả về — chỉ tra bảng cho phép:
def dispatch(name: str, args: dict) -> dict:
if name not in TOOLS: # allowlist
return {"error": f"Tool {name} không tồn tại"}
try:
return {"ok": TOOLS[name]["fn"](**args)}
except Exception as e:
return {"error": str(e)} # lỗi -> trả về, không crash
Bước 5 — Nối vào vòng lặp model
def run(user_msg: str):
messages = [{"role": "user", "content": user_msg}]
for _ in range(6): # trần lặp, chống loop vô hạn
resp = model.chat(messages, tools=tool_schemas())
if not resp.tool_calls:
return resp.content
for call in resp.tool_calls:
result = dispatch(call.name, call.args)
messages.append(resp.as_message())
messages.append({"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result)})
return "Đã đạt giới hạn số bước."
Hai chi tiết sống còn: trần lặp (range(6)) chống loop vô hạn, và luôn gắn tool_call_id để model biết kết quả nào ứng với lời gọi nào. Con số 6 chỉ là mặc định an toàn — hãy chọn trần vừa đủ cho tác vụ phức tạp nhất mà agent phải làm, không hơn. Trần quá lớn làm mất tác dụng bảo vệ; trần quá nhỏ cắt ngang tác vụ hợp lệ.
Khung tư duy: tách 3 lớp
Giữ ba lớp này độc lập để dễ test và bảo trì:
- Lớp nghiệp vụ (
get_weather): thuần Python, test được không cần LLM. - Lớp registry/dispatch: ánh xạ name → hàm, validate, bắt lỗi.
- Lớp vòng lặp agent: nói chuyện với model, quản lý messages.
Checklist trước khi chạy
- [ ] API key nằm trong env, không trong code.
- [ ] Dispatcher dùng allowlist, tuyệt đối không
eval/getattrđộng. - [ ] Có trần số bước lặp.
- [ ] Mỗi kết quả tool gắn đúng
tool_call_id. - [ ] Hàm nghiệp vụ test được độc lập.
Sai lầm thường gặp
- Dùng
eval(name)hoặcglobals()[name]để gọi hàm theo tên model đưa. Đây là lỗ hổng thực thi mã tùy ý. Luôn tra allowlist. - Quên trần lặp. Model có thể gọi tool liên tục và bạn cháy hết quota trong vài giây.
- Gộp nghiệp vụ vào vòng lặp. Khi trộn code gọi API thời tiết với code nói chuyện model, bạn không test được gì cả.
- Không serialize kết quả. Kết quả tool phải là chuỗi (thường JSON) khi gửi lại model; quên
json.dumpsgây lỗi. - Bỏ sót
tool_call_id. Với nhiều tool call song song, thiếu id khiến model ghép sai kết quả.