Tool Calling và structured outputs: Bản hợp đồng vẫn đứng vững
24 lệnh gọi, JSON không vỡ, chỉ 2 ngày dùng được. Cùng endpoint với mô tả tốt hơn, và điều schema không sửa được.
Trên trang này
Đưa cho model một công cụ tìm chuyến bay và yêu cầu nó tìm chuyến bay từ Madrid đến Berlin. Đây là kết quả trả về:
<tool_call>
{"name": "search_flights",
"arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>JSON hợp lệ. Tên công cụ đúng. Mọi trường bắt buộc đều có. Và lệnh gọi này vô dụng: không API chuyến bay nào chấp nhận "Madrid" ở nơi nó cần mã sân bay, hay "3rd October 2026" ở nơi nó cần ngày.
Khoảng cách đó — cú pháp hoàn hảo, ngữ nghĩa không dùng được — là điều chương này nói đến, và điều đầu tiên cần xác lập là đây không phải vấn đề JSON. Qua hai mươi bốn yêu cầu với công cụ này, model tạo ra 24 lệnh gọi công cụ hợp lệ và 0 JSON bị vỡ. Nó chưa từng thất bại ở phần mà ai cũng debug.
Model không thực thi gì cả
Liên kết đến mục: Model không thực thi gì cảTrước khi đi vào cơ chế, đây là câu giúp tránh nhiều nhầm lẫn nhất: một lệnh gọi công cụ là một yêu cầu, không phải một hành động.
Model phát ra một thông điệp có cấu trúc nói rằng tôi muốn gọi search_flights với các đối số này. Rồi nó dừng. Code của bạn nhận thông điệp đó, quyết định có chấp nhận hay không, gọi bất cứ thứ gì nó cần gọi, rồi gửi kết quả trở lại như một thông điệp khác. Model chưa từng chạm vào cơ sở dữ liệu của bạn, chưa từng tạo HTTP request, chưa từng có credential.
Mọi thứ về bảo mật agent trong Chương 30 đều xuất phát từ sự phân chia đó, và mọi thứ về thiết kế agent trong Chương 23 cũng vậy: model đề xuất, code của bạn định đoạt, và code là nơi mọi bảo đảm tồn tại.
Vì vậy, nếu bỏ lớp thuật ngữ đi, một công cụ gồm hai thứ:
Một schema. Một JSON Schema mô tả một hàm: tên của nó, nó làm gì, và nó nhận những đối số nào cùng kiểu và ràng buộc của chúng. Đây là phần đi vào prompt, và là thứ duy nhất model từng thấy.
Một endpoint. Một hàm trong code của bạn nhận các đối số đó và trả về thứ gì đó. Model không bao giờ thấy nó, không biết nó viết bằng ngôn ngữ nào, và không thể phân biệt một truy vấn cơ sở dữ liệu với một chuỗi hardcode.
Bạn gửi các schema cùng với yêu cầu
Liên kết đến mục: Bạn gửi các schema cùng với yêu cầuCác định nghĩa công cụ đi vào prompt, được tuần tự hóa sang bất kỳ định dạng nào model đã được huấn luyện. Chúng tốn token trên từng lệnh gọi — một sự thật sẽ quay lại với một con số ở phần sau chương này.
Model trả lời bằng một lệnh gọi thay vì văn bản
Liên kết đến mục: Model trả lời bằng một lệnh gọi thay vì văn bảnThay vì văn xuôi, phản hồi chứa một yêu cầu có cấu trúc, và API báo một lý do kết thúc cho biết điều đó. Lý do này quan trọng: đó là cách code của bạn biết phải chạy một công cụ thay vì hiển thị câu trả lời cho người dùng.
Code của bạn chạy nó — hoặc từ chối
Liên kết đến mục: Code của bạn chạy nó — hoặc từ chốiĐây là bước không có model trong đó. Xác thực các đối số theo schema, quyết định người gọi này có được phép làm việc này không, rồi thực thi.
Bạn gửi kết quả trở lại như một thông điệp
Liên kết đến mục: Bạn gửi kết quả trở lại như một thông điệpKết quả trở thành một lượt khác trong cuộc hội thoại, trong một vai trò dành riêng cho nó. Model đọc nó như mọi context khác.
Model trả lời, hoặc yêu cầu một công cụ khác
Liên kết đến mục: Model trả lời, hoặc yêu cầu một công cụ khácĐó là vòng lặp của Chương 23, và là lý do một yêu cầu đơn lẻ có thể biến thành cả chục lượt đi-về.
Không có gì ở đây là tự phát sinh. Như Chương 11 đã xác lập, tool calling là một hành vi được huấn luyện:1 trong giai đoạn post-training, model đã thấy hàng nghìn cuộc hội thoại có đúng hình dạng như thế này. Đó là lý do định dạng phụ thuộc vào model, lý do độ tin cậy khác nhau rất nhiều giữa các model có kích thước tương tự, và lý do một model có thể gọi một công cụ nó chưa từng thấy — hình dạng đã được huấn luyện, công cụ cụ thể đến từ prompt của bạn.
Một schema tệ tốn gì, đo bằng số liệu
Liên kết đến mục: Một schema tệ tốn gì, đo bằng số liệuĐây là công cụ như đa số mọi người viết lần đầu. Lưu ý rằng không có gì về nó là sai; nó chỉ mỏng:
{
name: "search_flights",
description: "Search for flights.",
parameters: {
type: "object",
properties: {
from: { type: "string", description: "Airport." },
to: { type: "string", description: "Airport." },
date: { type: "string", description: "The date." },
},
required: ["from", "to", "date"],
},
}Hai mươi bốn yêu cầu, sáu cặp thành phố kết hợp với bốn cách diễn đạt ngày ("ngày 3 tháng tới", "thứ Sáu tới", "15 tháng 12", "ngày mai"), greedy decoding để kết quả tái lập được:
| đã gọi công cụ | JSON vỡ | ngày ở ISO | sân bay là IATA | mọi thứ đúng | |
|---|---|---|---|---|---|
| schema ở trên | 24/24 | 0 | 2/24 | 4/24 | 1/24 |
Hãy đọc hai cột đầu trước ba cột cuối. Model gọi đúng công cụ mỗi lần và tạo JSON đúng định dạng mỗi lần. Lỗi nằm hoàn toàn ở giá trị, và các giá trị đó không dùng được: "Madrid" thay vì MAD, "3rd October 2026" thay vì 2026-10-03.
Điều này đáng nhấn mạnh vì nó quyết định bạn nhìn vào đâu khi có thứ hỏng. Phản xạ thường là thêm một JSON parser kèm retry, hoặc yêu cầu model mạnh mẽ hơn rằng hãy xuất JSON hợp lệ. Cả hai đều không xử lý bất cứ điều gì đã xảy ra ở đây.
Giờ chỉ thay đổi phần mô tả
Liên kết đến mục: Giờ chỉ thay đổi phần mô tảCùng endpoint. Cùng code phía sau. Cùng model, cùng prompt, cùng decoding. Thứ duy nhất thay đổi là văn bản trong schema:
{
name: "search_flights",
description: "Search scheduled flights between two airports on a given day.",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",
pattern: "^[A-Z]{3}$",
},
to: { /* same */ },
date: {
type: "string",
description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",
format: "date",
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
},
},
required: ["from", "to", "date"],
},
}| ĐỊNH DẠNG ngày | GIÁ TRỊ ngày | ĐỊNH DẠNG sân bay | GIÁ TRỊ sân bay | |
|---|---|---|---|---|
| schema mỏng | 2/24 | 1/24 | 4/24 | 4/24 |
| schema có mô tả | 24/24 | 12/24 | 16/24 | 8/24 |
Định dạng ngày đi từ 2 trên 24 lên 24 trên 24. Hoàn hảo, chỉ nhờ thay đổi văn bản, không đụng code và không cần logic retry. Nếu bạn chỉ lấy một thói quen vận hành từ chương này, thì đó là: khi một công cụ bị gọi sai, cách sửa gần như luôn nằm trong phần mô tả, và đó là cách sửa rẻ nhất trong hệ thống.
Giờ hãy đọc cột thứ hai, phần quan trọng hơn.
Schema ràng buộc hình dạng. Nó không thể cung cấp tri thức.
Liên kết đến mục: Schema ràng buộc hình dạng. Nó không thể cung cấp tri thức.Ngày ở định dạng ISO 24 trên 24 lần. Nó là đúng ngày 12 trên 24 lần.
Vậy một nửa số lệnh gọi giờ mang một ngày được định dạng hoàn hảo nhưng là ngày sai. Phần mô tả đã nói với model cần tạo hình dạng nào, và model tạo ra hình dạng đó không lỗi — nhưng biến "thứ Sáu tới" thành 2026-09-11 đòi hỏi phải biết ngày hôm nay và làm số học lịch, mà không lượng mô tả nào có thể cung cấp điều đó. Với sân bay cũng vậy: định dạng tăng từ 4 lên 16, nhưng giá trị chỉ tăng từ 4 lên 8, vì viết MAD đòi hỏi biết rằng sân bay của Madrid là MAD.
Sự phân biệt đó là ý tưởng chịu lực của chương này:
Schema là một hợp đồng về hình thức. Nó có thể làm cho đầu ra của model parse được, có kiểu và nhất quán. Nó không thể làm đầu ra đó đúng, và mọi chế độ lỗi còn sống sót sau một schema tốt đều là lỗi tri thức, không phải lỗi định dạng.
Hai thứ này cần cách sửa khác nhau, và nhầm lẫn giữa chúng làm lãng phí hàng tuần. Lỗi định dạng được sửa trong phần mô tả hoặc bằng constrained decoding, bên dưới. Lỗi tri thức được sửa bằng cách đưa tri thức vào prompt — ngày hiện tại trong system message, một tra cứu sân bay như một công cụ thứ hai mà model gọi trước, một enum trong schema khi tập giá trị đủ nhỏ để liệt kê. Hãy chú ý điểm chung của cả ba: chúng chuyển vấn đề ra khỏi trí nhớ của model và đưa vào đầu vào của nó, chính là toàn bộ nội dung của Chương 24.
Structured outputs, và "constrained decoding" thực ra là gì
Liên kết đến mục: Structured outputs, và "constrained decoding" thực ra là gìMọi thứ ở trên vẫn dựa vào việc model chọn tạo ra hình dạng đúng. Có một bảo đảm mạnh hơn, và đó là phần đem lại nhiều giá trị nhất của Chương 17.
Hãy nhớ cách generation hoạt động: ở mỗi bước, model tạo một logit cho mọi token trong từ vựng, và sampler chọn một token. Constrained decoding chèn thêm một bước ở giữa. Với một grammar — được suy ra từ JSON Schema của bạn — nó tính xem những token nào có thể xuất hiện hợp lệ tiếp theo, đặt logits của tất cả token còn lại thành âm vô cực, rồi để sampler chọn trong phần còn lại.
Nếu schema nói thứ tiếp theo phải là {, thì mọi token không phải { có xác suất bằng không. Không phải "khó xảy ra": bằng không. Model không thể phát ra JSON không hợp lệ vì các token không hợp lệ đã bị loại khỏi phân phối trước khi sampling.
Đó là bản chất bên dưới của "structured outputs", "JSON mode" và "guided generation", và nó giải thích hai thuộc tính của chúng. Bảo đảm là toàn phần cho bất cứ điều gì grammar có thể biểu đạt — kiểu, trường bắt buộc, enum, lồng nhau — vì nó được cưỡng chế bằng cơ chế thay vì được yêu cầu lịch sự. Và nó không nói gì về nội dung: một grammar có thể buộc "date" là một chuỗi khớp mẫu ngày, nhưng không thể buộc đó là đúng ngày. Chính là bức tường ở phần trước, chỉ đi tới từ phía bên kia.
Hai ghi chú thực tế. Nó không miễn phí: mask phải được tính ở mỗi bước, và grammar phức tạp gây độ trễ đo được. Và nó thay đổi việc model đang làm — một model bị lái khỏi token nó ưa thích có thể tạo nội dung tệ hơn trong khi tạo cấu trúc hoàn hảo, đó là lý do "hỏi tử tế rồi xác thực" vẫn là mặc định hợp lý cho các hình dạng đơn giản, còn constrained decoding đáng với chi phí khi hình dạng phức tạp hoặc bên tiêu thụ nghiêm ngặt.
Side effect, và thuộc tính duy nhất quan trọng
Liên kết đến mục: Side effect, và thuộc tính duy nhất quan trọngChương 14 đã đo một lần timeout rồi retry khiến một câu trả lời bị tính phí thành hai lần generation. Với công cụ, cùng lỗi đó tệ hơn, vì một công cụ có thể làm điều gì đó.
Nếu code của bạn gọi charge_card, bị timeout, rồi retry, bạn có hai khoản thu. Model không hề biết chuyện nào trong đó đã xảy ra; nó thấy một kết quả công cụ. Cách sửa giống mọi hệ phân tán khác và không phải vấn đề của model: làm cho thao tác idempotent bằng cách gán cho lệnh gọi một key, để lần thực thi thứ hai nhận ra lần đầu và trả về kết quả của nó thay vì làm lại công việc.
Quy tắc thiết kế theo sau đáng được nói thẳng. Tách read khỏi write trong catalogue công cụ của bạn. Một read có thể được retry tự do, chạy song song và cache. Một write thì không, và nên mang một key, một kiểm tra quyền, và — với bất cứ thứ gì người dùng muốn biết trước khi nó xảy ra — một bước phê duyệt đặt con người giữa yêu cầu và hành động. Bước phê duyệt đó không phải phép lịch sự: nó là một trong vài thứ đứng giữa prompt injection và hệ quả thật — và, như Chương 30 đo được, cũng là thứ yếu nhất trong số đó.
Bao nhiêu công cụ thì bắt đầu suy giảm?
Liên kết đến mục: Bao nhiêu công cụ thì bắt đầu suy giảm?Giai thoại truyền miệng nói rằng nạp nhiều công cụ làm model chọn tệ. Điều đó đáng được đo thay vì lặp lại, nên: cùng hai mươi bốn yêu cầu, với công cụ chuyến bay cộng thêm một tập công cụ khác tăng dần — bao gồm ba công cụ cố tình dễ nhầm lẫn (lịch tàu, chuyến phà, tuyến xe buýt).
| số công cụ đã nạp | prompt tokens | chọn search_flights | ngày ở ISO |
|---|---|---|---|
| 1 | 353 | 24/24 | 24/24 |
| 5 | 730 | 24/24 | 24/24 |
| 10 | 1,193 | 21/24 | 21/24 |
| 20 | 2,119 | 24/24 | 24/24 |
Việc chọn không suy giảm. Với hai mươi công cụ, ba trong số đó có vẻ dễ nhầm, một model nửa tỷ tham số chọn đúng công cụ hai mươi bốn trên hai mươi bốn lần. Điểm tụt ở mười là ba lệnh gọi đặt tên một công cụ khác, và nó không còn khi tăng lên hai mươi.
Đó là một kết quả âm và nên được báo cáo như vậy: trong tác vụ này, với các công cụ này, "quá nhiều công cụ" không phải vấn đề. Thứ tăng lên, đơn điệu và gấp sáu lần, là prompt: từ 353 token lên 2,119, trả phí trên mọi yêu cầu trong cuộc hội thoại, mãi mãi, dù có công cụ nào được dùng hay không.
Vì vậy, phiên bản trung thực của giai thoại này nói về chi phí và context, không phải độ chính xác. Hai mươi công cụ là một khoản thuế thường trực trên mọi tin nhắn, và Chương 16 đã cho thấy một tiền tố thường trực làm gì với hóa đơn qua bốn mươi lượt. Khi mọi người báo cáo rằng nhiều công cụ làm giảm chất lượng, cơ chế thường là các định nghĩa đã chen mất context quan trọng — một vấn đề của Chương 24 khoác áo Chương 18. Các công cụ thực sự gần trùng lặp với nhau cũng là vấn đề thật, và cách sửa chúng không phải là ít công cụ hơn mà là mô tả và namespace tốt hơn: thêm tiền tố theo hệ thống (crm.search_customer, billing.search_customer) để hai catalogue gộp từ hai team không va nhau, và để model có thứ mà phân biệt.
Ba loại công cụ, và loại mở ra phần tiếp theo
Liên kết đến mục: Ba loại công cụ, và loại mở ra phần tiếp theoViệc phân loại công cụ theo những gì chúng làm với thế giới sẽ hữu ích, vì kỹ thuật cho mỗi loại khác nhau.
Công cụ dữ liệu đọc: search, fetch, query. Có thể retry, có thể chạy song song, có thể cache. Chúng thất bại bằng cách không trả về gì hữu ích, và rủi ro chính của chúng là đưa văn bản không đáng tin vào context — toàn bộ bề mặt tấn công của Chương 30.
Công cụ hành động ghi: gửi, tạo, tính phí, xóa. Không thể retry nếu không có key, không thể chạy song song một cách an toàn, và là lý do tồn tại các luồng phê duyệt.
Công cụ điều phối gọi các model khác. Một công cụ có phần triển khai là một agent khác, với prompt riêng, công cụ riêng và vòng lặp riêng — và với model đang gọi, nó trông y hệt hai loại kia, vì một schema và một endpoint là tất cả những gì nó từng thấy.
Loại thứ ba không phải chuyện lạ. Nó là cơ chế đằng sau nửa agent-as-a-tool của Chương 25 — topology còn lại, handoff, trao cuộc hội thoại đi và không bao giờ lấy lại — và nó hoạt động chính xác vì interface trong chương này đủ hẹp để cả một agent nằm phía sau nó.
Tiếp theo là gì
Liên kết đến mục: Tiếp theo là gìGiờ bạn có một model có thể yêu cầu mọi thứ, và một hợp đồng làm cho việc yêu cầu đó parse được. Thứ bạn chưa có là bất cứ gì để nó hỏi về, ngoài những gì vừa trong prompt của nó.
Công cụ phổ biến nhất trong production, với khoảng cách rất xa, là tìm kiếm trên một khối văn bản mà model chưa từng thấy trong training: tài liệu của bạn, ticket của bạn, hợp đồng của bạn. Điều đó nghe như một vấn đề đã giải xong — embed nó, tìm các hàng xóm gần nhất, dán chúng vào — và những phần chưa được giải lại là những phần quyết định câu trả lời có đáng tin không: văn bản được cắt ra như thế nào trước khi được embedded, ngưỡng tương đồng nào đủ thấp để có nghĩa là tôi không biết, và một trích dẫn được gắn vào một claim ra sao để người đọc có thể kiểm tra.
Chương 19 là retrieval, và đó là chương nơi một câu trả lời sai thôi không còn là điều gây tò mò nữa mà bắt đầu trở thành trách nhiệm pháp lý.
Nguồn và phương pháp
Liên kết đến mục: Nguồn và phương phápCác phép đo trong chương này đến từ Qwen/Qwen2.5-0.5B-Instruct với greedy decoding, trên 24 yêu cầu được tạo ra bằng cách kết hợp sáu cặp thành phố với bốn cách diễn đạt ngày, dùng chat template riêng của model cho định nghĩa công cụ. Chúng tái lập chính xác, và đó là một model nhỏ: hãy đọc phần tách định dạng/giá trị như một minh họa cơ chế hơn là một benchmark về những gì các model hiện tại làm. Một frontier model xử lý "thứ Sáu tới" đúng thường xuyên hơn nhiều — và vẫn không thể bị một schema buộc làm như vậy, đó là phần có tính khái quát.
Bộ từ vựng JSON Schema dùng ở trên (type, properties, required, pattern, format, enum) được quy định trong bản draft JSON Schema mà tài liệu của provider của bạn nêu tên; tập con hữu ích thì nhỏ và giống nhau giữa các provider, còn những khác biệt có tồn tại — keyword nào được constrained decoding thực thi thay vì chỉ truyền cho model — đáng để đọc trong hướng dẫn structured-output của provider hơn là giả định.
Với constrained decoding như một kỹ thuật, các thư viện kiểu guidance và dự án outlines mô tả cách xây grammar-to-logit-mask theo cách ánh xạ trực tiếp lên sampler của Chương 17. Và với chính lượt đi-về, đặc tả rõ nhất không phải một tutorial mà là một protocol: Chương 26 đọc nó từng dòng.
Tài liệu tham khảo
Liên kết đến mục: Tài liệu tham khảo-
Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). Bài báo đã khiến công thức post-training trở thành chuẩn; hình dạng của một lệnh gọi công cụ được học ở đó, từ các minh họa, y hệt hình dạng của một câu trả lời. ↩