Function calling và thiết kế tool cho agent
Ở các bài trước mình đã mổ xẻ vòng lặp ReAct: Thought → Action → Observation. Bài này đi thẳng vào chữ Action. Agent "gọi tool" thực chất là chuyện gì đang xảy ra, và tại sao phần lớn lỗi của agent lại nằm ở chỗ thiết kế tool chứ không phải ở model.
Sau bài này bạn sẽ:
- Hiểu function calling thực chất là gì, và ai mới là bên thực thi code
- Viết được JSON schema tool chặt chẽ, ít để model đoán mò
- Viết description đủ tốt để model chọn đúng tool
- Thiết kế đường xử lý lỗi để agent không bị kẹt trong vòng lặp
- Nhận diện bẫy khi mở quá nhiều tool cùng lúc
Function calling: model xin gọi, runtime của bạn mới chạy
Điểm dễ hiểu nhầm nhất với người mới: LLM không chạy code. Nó chỉ sinh ra một cấu trúc JSON nói rằng "tôi muốn gọi tool get_order_status với tham số này". Vòng lặp thật sự gồm hai bước do code của bạn ghép lại:
- Bạn gửi kèm danh sách tool (name, description, JSON schema) trong request
- Model trả về
tool_callvới tham số - Runtime của bạn chạy hàm thật, đóng gói kết quả
- Kết quả đó quay lại làm observation cho lượt suy luận tiếp theo
# Phần dispatch nằm ở phía bạn, không phải ở model
try:
result = run_tool(call.name, call.arguments)
except ToolError as e:
result = {"ok": False, "error_type": e.type, "retryable": e.retryable, "message": str(e)}
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
Hiểu chỗ này thì bạn cũng hiểu luôn vì sao "agent gọi sai tool" thường là lỗi của schema và description, không phải model kém.
Schema tool: bản hợp đồng giữa model và code của bạn
Schema là chỗ bạn kiểm soát được nhiều nhất. Xem hai phiên bản của cùng một tool.
Bản dở:
{
"name": "search",
"description": "Tìm kiếm",
"parameters": {
"type": "object",
"properties": { "q": { "type": "string" } }
}
}
Không có required, q là string tự do, tên tool là "search" chung chung. Model sẽ gửi đủ kiểu giá trị, và bạn phải viết validation đỡ ở phía sau.
Bản tốt:
{
"name": "get_order_status",
"description": "Tra cứu trạng thái một đơn hàng theo mã đơn. Dùng khi người dùng đã cung cấp mã đơn cụ thể. Không dùng để tìm đơn theo tên khách hàng.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Mã đơn dạng ORD-xxxx, ví dụ ORD-1042"
},
"include_history": {
"type": "boolean",
"description": "True nếu cần lịch sử thay đổi trạng thái, mặc định false"
}
},
"required": ["order_id"]
}
}
Vài quy tắc rút ra từ bản tốt:
- Enum thay cho string tự do khi tập giá trị hữu hạn (status, loại giao dịch, đơn vị). Model khó sinh sai hơn nhiều.
requiredphải khai báo thật. Đây là hàng rào rẻ nhất chống lỗi thiếu tham số.- Một tool một việc. Tool ôm đồm có schema mơ hồ và khó báo lỗi chính xác.
- Đừng lồng object quá sâu. Model dễ sinh sai cấu trúc, còn bạn thì khó validate và khó đọc log.
- Đặt tên theo động từ và đối tượng:
get_order_status,send_email,create_ticket. Tên chung chung nhưsearchhayprocesslà nguồn gốc của việc chọn nhầm.
Description: viết cho model đọc, không phải cho người đọc
Description là thứ model dùng để quyết định có nên gọi tool hay không. Một description tốt trả lời bốn câu:
- Tool làm gì
- Dùng khi nào
- Không dùng khi nào
- Trả về cái gì
Phần số 3 quan trọng nhất và hay bị bỏ nhất. Nếu bạn có get_order_status và list_customer_orders, hãy viết rõ: cái đầu cần mã đơn, cái sau cần mã khách. Không viết rõ thì model sẽ gọi đại một cái rồi tự sửa sau, tốn một vòng lặp vô ích.
Một mẹo thực tế: mô tả luôn hình dạng dữ liệu trả về trong description. Ví dụ "Trả về object gồm status, updated_at, và lý do nếu đơn bị huỷ". Như vậy bước Thought kế tiếp của agent có cơ sở để lập luận thay vì đoán.
Xử lý lỗi: observation phải nói được cái gì sai
Đây là phần quyết định agent có thoát khỏi vòng lặp hay không. Nguyên tắc: lỗi không được ném ra ngoài vòng lặp, nó phải trở thành một observation có cấu trúc.
Ví dụ một tool trả {"error": "not_found"}. Agent không biết "not found" là sai mã đơn, đơn chưa tồn tại, hay hệ thống đang lỗi. Kết quả điển hình: nó thử lại y hệt ba lần rồi bỏ cuộc.
Bản tốt hơn:
{
"ok": false,
"error_type": "invalid_argument",
"retryable": false,
"message": "order_id phải có dạng ORD-xxxx. Giá trị nhận được: 'đơn 1042'."
}
Cờ retryable là thứ đáng làm nhất trong cả bài này:
- Retryable: timeout, rate limit, lỗi mạng tạm thời. Agent nên thử lại, nhưng có giới hạn (2-3 lần).
- Non-retryable: sai định dạng tham số, không tìm thấy dữ liệu, không có quyền. Thử lại là vô nghĩa, agent phải đổi cách làm hoặc hỏi lại người dùng.
Kinh nghiệm xương máu: luôn đặt trần số vòng lặp và trần số lần gọi tool ở tầng runtime. Đừng tin vào việc model sẽ tự biết dừng.
Cái bẫy: mở quá nhiều tool
Khi agent còn 3 tool thì gần như không bao giờ chọn nhầm. Lên 30 tool thì chuyện đó xảy ra liên tục, kể cả với model mạnh. Lý do:
- Nhiều tool có mô tả na ná nhau, model phải phân biệt bằng vài chữ
- Danh sách tool ăn context, đẩy phần hội thoại thật ra rìa
- Mỗi tool thêm vào là một cơ hội để schema mâu thuẫn nhau
Cách xử lý thực dụng, xếp theo mức tăng dần:
- Chỉ expose tool cần cho bước hiện tại. Đang ở bước tra cứu thì đừng đưa tool gửi email.
- Nhóm theo nghiệp vụ, mỗi nhóm một sub-agent hoặc một lượt gọi riêng.
- Tool meta: một tool
find_toolcho phép agent tìm tool phù hợp trong catalog lớn, chỉ nạp schema khi cần. - Gộp các tool quá vụn thành một tool có tham số
actiondạng enum, khi chúng chia sẻ cùng logic.
Con số tham khảo: giữ khoảng 5-10 tool mỗi lượt là vùng an toàn cho phần lớn tác vụ.
Mấy lỗi mình thấy đi thấy lại khi review tool schema
- Description viết cho người đọc. "Công cụ tra cứu đơn hàng nội bộ" không giúp model quyết định gì cả.
- Không khai báo
requiredrồi đổ lỗi cho model khi thiếu tham số. - Lỗi được ném ra ngoài vòng lặp làm agent chết giữa chừng thay vì đọc lỗi và sửa.
- Retry không có trần. Một lỗi non-retryable mà agent thử 20 lần thì tốn tiền mà không ai được lợi.
- Tool trả về quá nhiều dữ liệu. Trả cả object 50 field thì observation nuốt hết context. Trả đúng cái agent cần.
Chốt lại ba câu để nhớ: schema chặt, description nói rõ khi nào không dùng, và lỗi phải là dữ liệu chứ không phải ngoại lệ.
Chốt lại
- Function calling là model sinh JSON, runtime của bạn mới chạy code. Mọi thứ bạn kiểm soát được đều nằm ở schema và description.
required,enum, và tên tool theo động từ là ba thay đổi rẻ nhất mà hiệu quả nhất.- Lỗi phải quay về dưới dạng observation có cấu trúc, kèm cờ
retryablevà một trần retry ở tầng runtime. - Càng nhiều tool, tỉ lệ chọn nhầm càng cao. Chỉ mở cái cần cho bước hiện tại.
Bài tiếp theo mình sẽ nói về memory và quản lý trạng thái: agent nhớ gì giữa các lượt, nhớ ở đâu, và khi nào thì nên quên. Bạn sẽ thấy tool trả về gì ảnh hưởng trực tiếp đến việc memory phình ra hay gọn lại.
Front: Function calling thực chất là gì? Back: Model sinh JSON arguments cho một tool đã khai báo, runtime của bạn mới là bên chạy hàm thật.
Front: Ba phần cốt lõi của một khai báo tool? Back: name, description, và JSON schema cho parameters.
Front: Vì sao nên dùng enum thay vì string tự do trong schema? Back: Thu hẹp không gian giá trị hợp lệ, model khó sinh ra giá trị sai.
Front: Trường required trong JSON schema có tác dụng gì? Back: Buộc model phải điền tham số đó, giảm lỗi thiếu tham số khi chạy tool.
Front: Description của tool nên trả lời những câu nào? Back: Tool làm gì, dùng khi nào, không dùng khi nào, và trả về cái gì.
Front: Khi tool lỗi, agent nên nhận được gì? Back: Một observation có cấu trúc, không phải exception ném ra ngoài vòng lặp.
Front: Cờ retryable dùng để làm gì? Back: Cho agent biết nên thử lại hay phải đổi cách làm, tránh lặp vô hạn.
Front: Lỗi nào là non-retryable? Back: Sai định dạng tham số, không tìm thấy dữ liệu, không có quyền. Thử lại vô nghĩa.
Front: Bẫy lớn nhất khi mở quá nhiều tool? Back: Model chọn nhầm tool và tốn context, nhất là khi các tool mô tả gần giống nhau.
Front: Một tool nên đảm nhận bao nhiêu việc? Back: Một việc. Tool ôm đồm khiến schema mơ hồ và khó báo lỗi chính xác.
Front: Vì sao tránh lồng object quá sâu trong schema? Back: Model dễ sinh sai cấu trúc, còn bạn thì khó validate và khó đọc log.
Front: Cách giảm số tool expose cho model? Back: Chia theo bước hoặc nhóm nghiệp vụ, chỉ đưa các tool cần cho bước hiện tại.
Front: Tool trả về nên có hình dạng thế nào? Back: Ngắn gọn, có cấu trúc, đủ thông tin để bước Thought tiếp theo ra quyết định.
Điền vào chỗ trống để tự kiểm tra:
- LLM không tự chạy code, nó chỉ sinh ra _____ dạng JSON, còn runtime của bạn mới thực thi hàm.
- Trường _____ trong JSON schema buộc model phải điền tham số trước khi gọi tool.
- Khi tham số chỉ có vài giá trị hợp lệ, nên dùng _____ thay vì string tự do.
- Lỗi nên được trả về dưới dạng một _____ có cấu trúc, kèm cờ _____ để agent biết có nên thử lại.
- Mở quá nhiều tool trong một lượt khiến model dễ chọn nhầm tool và tốn _____.