·

Khi nói về tối ưu token cho AI agent, phần lớn engineer sẽ nghĩ ngay đến prompt, đến system message, hoặc đến độ dài của conversation history. Rất ít người để ý đến một khoản chi phí âm thầm nhưng cực kỳ tốn kém: tool definitions (định nghĩa công cụ) — hay còn gọi là tool schema. Đây là phần khai báo "agent của bạn có thể gọi những công cụ (tool/function) gì, với tham số ra sao" mà bạn gửi cho model ở MỖI lần gọi API, dù model có dùng tool đó hay không.

Nếu bạn đã từng build agent tích hợp với MCP server (Model Context Protocol — chuẩn giao tiếp giúp agent kết nối với các tool/data source bên ngoài) có vài chục tool, hoặc dùng framework tự động load toàn bộ tool có sẵn, rất có thể bạn đang trả tiền cho hàng nghìn token "vô hình" mỗi request — mà không hề biết. Bài học này sẽ giúp bạn hiểu rõ cơ chế, đo lường được con số thật, và nhận diện khi nào tool schema của mình đang trở thành gánh nặng.

Model Nhận Tool Definition Như Thế Nào

Một hiểu lầm phổ biến: nhiều người nghĩ tool definition chỉ là "metadata" được xử lý riêng, tách biệt với phần prompt chính, giống như một tham số cấu hình (config) không ảnh hưởng đến độ dài input. Thực tế hoàn toàn ngược lại.

Khi bạn gọi API của Anthropic (Claude), OpenAI, hay bất kỳ provider hỗ trợ function calling (cơ chế cho phép model gọi hàm/tool bên ngoài) nào khác, toàn bộ mảng tools mà bạn truyền vào — bao gồm tên tool, phần mô tả (description), và JSON Schema định nghĩa tham số — đều được serialize thành text và chèn thẳng vào phần đầu của context window trước khi model xử lý câu hỏi của người dùng. Nói cách khác, dưới góc nhìn của model, tool definition không khác gì một đoạn văn bản dài được thêm vào system prompt. Model phải "đọc" toàn bộ đoạn đó, tốn token input, trước khi nó bắt đầu suy nghĩ để trả lời.

Điều này có ba hệ quả quan trọng mà engineer cần khắc cốt ghi tâm:

  1. Tool definition tốn token dù model không dùng tool nào cả. Bạn khai báo 30 tool, model chỉ gọi 1 tool trong lượt hội thoại đó — nhưng bạn vẫn trả tiền cho token của cả 30 định nghĩa, ở MỌI turn của conversation (vì lịch sử hội thoại thường được gửi lại kèm tool definition mỗi lần).
  2. Tool definition được gửi lại lặp đi lặp lại. Với các agent loop nhiều bước (multi-turn), tool definition không chỉ tính 1 lần — nó bị gửi kèm ở mỗi request tiếp theo trong cùng session, vì API là stateless (không lưu trạng thái) theo mặc định. Nếu agent thực hiện 10 vòng gọi tool để hoàn thành một task, bạn có thể trả tiền cho tool schema tới 10 lần.
  3. Cách bạn viết description và schema ảnh hưởng trực tiếp đến chất lượng lựa chọn tool của model. Vì model "đọc" mô tả này như một phần ngữ cảnh, một description mơ hồ hoặc thiếu ví dụ không chỉ tốn token mà còn khiến model chọn sai tool hoặc điền sai tham số.

Mẹo

  • Đừng tin vào cảm giác "tool definition chỉ là cấu hình, không tốn gì". Hãy coi mỗi tool bạn thêm vào như một đoạn prompt bổ sung — nếu bạn không viết prompt đó một cách cẩn thận và tiết kiệm, đừng thêm tool đó vào.
  • Với các framework agent (LangChain, LlamaIndex, Vercel AI SDK, Claude Agent SDK...), hãy kiểm tra kỹ xem framework có tự động "bơm" thêm field nào vào schema (ví dụ ví dụ mẫu, ràng buộc bổ sung) mà bạn không chủ động viết — những field ẩn này cũng tính vào token.
  • Nếu dùng nhiều MCP server cùng lúc, nhớ rằng mỗi server thường tự khai báo một bộ tool riêng — tổng số tool gửi cho model là cộng dồn từ tất cả server đang kết nối, không phải chỉ server bạn "đang dùng".

Đo Lường Chi Phí Token Của Tool Schema

Lý thuyết là một chuyện, nhưng để thuyết phục team (hoặc chính bạn) rằng đây là vấn đề cần giải quyết, bạn cần có số liệu thực tế. Cách chính xác nhất là dùng chính token counter mà provider cung cấp, hoặc gọi thử API hai lần: một lần có tools, một lần không có tools, rồi so sánh input_tokens trả về.

Dưới đây là ví dụ dùng Python với Anthropic SDK để đo token cho một bộ tool set thực tế — mô phỏng một agent hỗ trợ quản lý task trong hệ thống project management, có 4 tool cơ bản:

import anthropic

client = anthropic.Anthropic()

task_management_tools = [
    {
        "name": "create_task",
        "description": (
            "Create a new task in the project tracker. Use this when the user "
            "wants to add a new work item, assign it to someone, or schedule it "
            "for a sprint. Returns the created task's ID and URL."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "Short task title"},
                "description": {"type": "string", "description": "Detailed task description"},
                "assignee_email": {"type": "string", "description": "Email of the assignee"},
                "due_date": {"type": "string", "description": "ISO 8601 date, e.g. 2026-07-15"},
                "priority": {"type": "string", "enum": ["low", "medium", "high", "urgent"]},
            },
            "required": ["title"],
        },
    },
    {
        "name": "search_tasks",
        "description": (
            "Search existing tasks by keyword, assignee, status, or date range. "
            "Use this before creating a duplicate task, or when the user asks "
            "about the status of ongoing work."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string"},
                "status": {"type": "string", "enum": ["open", "in_progress", "done", "blocked"]},
                "assignee_email": {"type": "string"},
            },
            "required": [],
        },
    },
    {
        "name": "update_task_status",
        "description": "Update the status of an existing task by its ID.",
        "input_schema": {
            "type": "object",
            "properties": {
                "task_id": {"type": "string"},
                "status": {"type": "string", "enum": ["open", "in_progress", "done", "blocked"]},
            },
            "required": ["task_id", "status"],
        },
    },
    {
        "name": "add_comment",
        "description": "Add a comment to a task, e.g. a status update or a question.",
        "input_schema": {
            "type": "object",
            "properties": {
                "task_id": {"type": "string"},
                "comment": {"type": "string"},
            },
            "required": ["task_id", "comment"],
        },
    },
]

user_message = [{"role": "user", "content": "What's the status of the login bug task?"}]

baseline = client.messages.count_tokens(
    model="claude-opus-4-8",
    messages=user_message,
)

with_tools = client.messages.count_tokens(
    model="claude-opus-4-8",
    messages=user_message,
    tools=task_management_tools,
)

tool_overhead = with_tools.input_tokens - baseline.input_tokens

print(f"Baseline (no tools):  {baseline.input_tokens} tokens")
print(f"With tools attached:  {with_tools.input_tokens} tokens")
print(f"Tool schema overhead: {tool_overhead} tokens")
print(f"Overhead per tool:    {tool_overhead / len(task_management_tools):.1f} tokens/tool")

Chạy đoạn code trên với endpoint count_tokens (không tốn phí, không sinh ra completion, chỉ đếm token của input) sẽ cho bạn con số cụ thể. Với 4 tool tương đối gọn như trên, overhead thường rơi vào khoảng 350-500 token — nghe thì nhỏ, nhưng hãy nhân với:

  • Số turn trong một conversation (agent loop 8-10 vòng là bình thường).
  • Số conversation mỗi ngày (hàng nghìn nếu là sản phẩm production).
  • Số tool thực tế trong hệ thống (nhiều team có 20-50 tool, không chỉ 4).

Một phép tính nhanh: 50 tool với overhead trung bình 120 token/tool ≈ 6.000 token chỉ để "khai báo khả năng", trước khi model đọc một chữ nào từ người dùng. Nếu context window của bạn là 200K token, 6.000 token có vẻ nhỏ; nhưng nếu bạn đang chạy trên model có context 32K-64K (phổ biến với các self-hosted hoặc model rẻ hơn để tối ưu chi phí), đó là 10-18% context window bị "ăn" bởi tool schema trước khi agent làm gì cả.

Mẹo

  • Luôn đo bằng endpoint đếm token chính thức của provider (count_tokens với Anthropic, hoặc tiktoken với OpenAI) — đừng ước lượng bằng cách chia số ký tự cho 4, vì JSON Schema có mật độ token khác văn bản tự nhiên (dấu ngoặc, dấu hai chấm, enum lặp lại... đều tốn token).
  • Đo overhead theo từng tool riêng lẻ (thêm tool A, đo lại; thêm tool B, đo lại) để biết chính xác "con nào" đang tốn nhất, thay vào đo tổng rồi chia đều — vì các tool có schema phức tạp khác nhau rất nhiều.
  • Lưu log token overhead theo thời gian như một metric giám sát (observability) — khi có người thêm tool mới vào hệ thống, con số này sẽ tăng dần một cách âm thầm nếu không ai theo dõi.

"Giải Phẫu" Một Tool Schema Bị Phình To

Không phải tool nào cũng tốn token như nhau. Qua kinh nghiệm review nhiều codebase agent, có một vài "triệu chứng" lặp lại khiến tool schema phình to một cách không cần thiết:

1. Description dài dòng, kể chuyện thay vì mô tả. Nhiều người viết description như đang viết tài liệu hướng dẫn sử dụng đầy đủ (full user guide), kèm cả lịch sử phát triển tool, các trường hợp edge case hiếm gặp, hoặc ví dụ lặp đi lặp lại 3-4 lần. Model chỉ cần biết: tool này làm gì, khi nào nên dùng, tham số nào là bắt buộc. Phần còn lại là token lãng phí.

2. Schema tham số quá chi tiết hoặc trùng lặp. Ví dụ: khai báo cả description cho object cha VÀ description lặp lại tương tự cho từng field con, hoặc dùng enum với danh sách giá trị quá dài khi thực ra chỉ cần vài giá trị phổ biến (phần còn lại có thể xử lý qua validation ở tầng code, không cần model biết hết).

3. Tool trùng chức năng (overlapping tools). Đây là lỗi nghiêm trọng nhất về mặt token: có get_user, fetch_user_info, retrieve_user_details — ba tool làm gần như cùng một việc, do được nhiều người/nhiều team thêm vào theo thời gian mà không rà soát lại. Mỗi tool trùng lặp là token nhân đôi, nhân ba cho cùng một khả năng.

4. Field ví dụ (examples) hoặc default không cần thiết cho mọi field. JSON Schema hỗ trợ nhiều field mở rộng như examples, default, minLength, pattern... Những field này có giá trị thật khi giúp model hiểu đúng format, nhưng nếu áp dụng tràn lan cho cả field đơn giản (như id: string), bạn đang trả token cho thông tin dư thừa.

5. Toàn bộ tool set của một MCP server được load "trọn gói". Đây là lỗi kiến trúc phổ biến nhất trong thực tế: kết nối một MCP server (ví dụ server quản lý file, server CRM, server database) và mặc định load hết TẤT CẢ tool mà server đó cung cấp — dù task hiện tại chỉ cần 2-3 tool trong số 30 tool đó.

Mẹo

  • Áp dụng nguyên tắc "một câu, một mục đích" cho description: câu đầu tiên nói tool làm gì, câu thứ hai (nếu cần) nói khi nào dùng/không nên dùng. Đừng viết quá 2-3 câu trừ khi tool có logic nghiệp vụ thực sự phức tạp.
  • Định kỳ (mỗi sprint hoặc mỗi quý) làm một buổi "tool audit": liệt kê toàn bộ tool đang active, đánh dấu tool nào trùng chức năng, tool nào ít được model gọi tới (dựa trên log), rồi gộp hoặc xóa.
  • Với enum, chỉ liệt kê giá trị mà model THỰC SỰ cần biết để chọn đúng — giá trị hiếm gặp hoặc nội bộ có thể xử lý bằng validation phía server sau khi nhận tham số dạng string tự do.

Chi Phí Token Khác Nhau Giữa Các Provider

Một điều ít người để ý: cùng một bộ tool definition logic giống nhau, nhưng khi serialize theo format riêng của từng provider, số token tiêu thụ có thể khác nhau đáng kể. Lý do là mỗi provider có cấu trúc JSON khác nhau cho tool/function definition:

  • Anthropic (Claude): dùng field input_schema chứa JSON Schema chuẩn, tool được truyền qua tham số tools cấp cao nhất của request. Cấu trúc khá gọn, ít field bọc lồng nhau.
  • OpenAI: trong Chat Completions API cũ, tool được bọc trong {"type": "function", "function": {...}} — có thêm một lớp bọc (type, function) so với Anthropic, nghĩa là cùng nội dung logic nhưng tốn thêm vài token cho phần "khung" này, nhân với số lượng tool.
  • Google Gemini: dùng cấu trúc function_declarations bên trong tools, với schema dạng OpenAPI-like — có một số khác biệt nhỏ về tên field (parameters thay vì input_schema) nhưng độ dài tổng thể tương đương.
  • Các MCP server: bản thân MCP không quyết định format cuối cùng gửi cho model — MCP client (ví dụ Claude Desktop, hoặc code bạn tự viết) sẽ nhận tool list theo chuẩn MCP JSON-RPC, rồi chuyển đổi (convert) sang format riêng của provider mà bạn đang gọi. Bước convert này đôi khi thêm field metadata phụ (ví dụ namespace tên tool theo server) khiến token tăng thêm so với khai báo tool thủ công.

Khác biệt giữa các provider thường không lớn ở cấp một tool đơn lẻ (vài token), nhưng nhân với hàng chục tool và hàng nghìn request mỗi ngày, đây là một biến số đáng đưa vào bài toán chọn provider — đặc biệt nếu bạn đang so sánh chi phí giữa các model có mức giá token khác nhau đáng kể.

Mẹo

  • Nếu hệ thống của bạn hỗ trợ multi-provider (có thể chuyển đổi giữa Claude, GPT, Gemini), hãy đo token overhead của tool schema riêng cho từng provider bằng chính token counter của provider đó — đừng giả định số liệu của provider này áp dụng được cho provider khác.
  • Khi dùng MCP client tự viết, log lại payload cuối cùng gửi cho model (trước khi gửi) để kiểm tra xem quá trình convert từ chuẩn MCP có "phình" thêm field không cần thiết hay không.
  • Đừng chọn provider chỉ vì tool schema của họ "gọn" hơn vài token — hãy đặt yếu tố này trong bức tranh tổng thể gồm chất lượng model, độ chính xác gọi tool, và giá token per-million, vì phần lớn trường hợp overhead schema là phần nhỏ so với các yếu tố đó.

Tác Động Thực Tế: Token Của Tool Schema So Với Ngân Sách Context Window

Hãy đặt mọi thứ vào một bức tranh cụ thể. Giả sử bạn đang build một customer support agent, dùng model có context window 200K token, với các thành phần input điển hình sau mỗi turn:

Thành phần Token ước tính
System prompt (hướng dẫn vai trò, tone, quy tắc) 800
Tool definitions (25 tool: CRM, ticketing, knowledge base, refund...) 4.500
Conversation history (10 turn trước đó) 3.000
Tài liệu tham khảo được RAG (Retrieval-Augmented Generation) truy xuất 2.000
Câu hỏi hiện tại của người dùng 50
Tổng input mỗi turn ~10.350

Trong ví dụ này, tool definitions chiếm khoảng 43% tổng input token mỗi turn — lớn hơn cả conversation history và tài liệu RAG cộng lại. Đây không phải số liệu hiếm gặp; nhiều team khi lần đầu đo đạc đã "giật mình" vì tool schema âm thầm chiếm phần lớn ngân sách token, trong khi công sức tối ưu của họ trước đó chỉ tập trung vào cắt gọn system prompt hoặc conversation history.

Hệ quả thực tế của việc này không chỉ là chi phí (cost) tính theo token đầu vào, mà còn:

  • Giảm không gian còn lại cho context thực sự quan trọng — nếu context window có hạn (ví dụ model rẻ hơn với 32K-64K), tool schema chiếm nhiều sẽ đẩy các thông tin cần thiết khác (lịch sử hội thoại dài, tài liệu tham khảo) ra khỏi "vùng nhớ" của model, hoặc buộc bạn phải cắt bớt chúng.
  • Tăng latency (độ trễ) — model cần nhiều thời gian hơn để xử lý (prefill) một input dài hơn trước khi bắt đầu sinh câu trả lời, dù phần tăng thêm đó chỉ là tool schema chưa dùng đến.
  • Giảm hiệu quả prompt caching — một số provider hỗ trợ cache phần đầu context (bao gồm tool definitions) để giảm chi phí ở các lần gọi sau; nhưng nếu tool schema thay đổi thường xuyên (do bạn hay chỉnh sửa description, thêm/bớt tool), cache liên tục bị invalidate (làm mất hiệu lực), khiến bạn mất đi lợi ích tối ưu chi phí này.

Về mặt kinh doanh, nếu bạn đang vận hành agent ở quy mô hàng chục nghìn request/ngày, việc giảm tool schema từ 4.500 token xuống 1.500 token (thông qua các kỹ thuật sẽ học ở các bài sau như dynamic tool loading — chỉ load tool cần thiết theo tình huống) có thể tiết kiệm hàng triệu token mỗi ngày, tương đương một khoản chi phí vận hành đáng kể được cắt giảm mà KHÔNG ảnh hưởng đến chất lượng agent.

Mẹo

  • Tính tỷ lệ phần trăm mà tool schema chiếm trong tổng input mỗi turn — đây là chỉ số (metric) đơn giản nhưng trực quan để thuyết phục product manager hoặc leadership đầu tư thời gian tối ưu.
  • Với hệ thống dùng nhiều model khác nhau cho các tác vụ khác nhau (model lớn cho reasoning phức tạp, model nhỏ cho tác vụ đơn giản), ưu tiên rà soát tool schema kỹ hơn ở các request dùng model có context window nhỏ, vì tác động tỷ lệ phần trăm ở đó lớn hơn nhiều.
  • Nếu dùng prompt caching, hãy giữ tool definitions ổn định (ít thay đổi) và đặt chúng ở vị trí đầu của phần được cache, để tối đa hóa số lần cache hit qua nhiều request.

Tổng Kết

Tool definitions không phải là "chi phí phụ" vô hình — chúng là văn bản thật, được gửi vào context window ở mọi turn, và tiêu tốn token thật. Ba điều cốt lõi cần nhớ từ bài học này:

  1. Model đọc tool schema như một phần của prompt, nghĩa là mỗi tool bạn thêm vào hệ thống đều có chi phí token thực, dù có được gọi tới hay không, và chi phí đó nhân lên theo số turn trong conversation.
  2. Bạn phải đo, không nên đoán. So sánh input token có/không có tools bằng endpoint đếm token chính thức của provider là cách nhanh nhất để có con số thật, làm cơ sở cho quyết định tối ưu.
  3. Bloat (phình to) trong tool schema thường đến từ description dài dòng, schema trùng lặp, và việc load "trọn gói" toàn bộ tool của một MCP server — những vấn đề này hoàn toàn có thể khắc phục bằng cách audit định kỳ và viết schema tối giản, đúng mục đích.

Ở các bài tiếp theo trong module này, bạn sẽ học các kỹ thuật cụ thể để xử lý bài toán này ở tầng kiến trúc: dynamic tool loading (chỉ nạp tool cần thiết theo ngữ cảnh), tối ưu cách xử lý kết quả trả về từ tool, và thiết kế custom tool sao cho vừa hiệu quả về token vừa dễ để model sử dụng đúng. Nhưng trước khi tối ưu, bước đầu tiên luôn là: đo lường để biết chính xác mình đang trả tiền cho cái gì.