Một trong những nguồn "âm thầm đốt token" lớn nhất trong AI agent không nằm ở độ dài prompt hay kích thước tool definition, mà nằm ở cách agent gọi tool. Rất nhiều agent — dù được thiết kế tốt về prompt và tool schema — vẫn lãng phí hàng chục nghìn token mỗi phiên làm việc chỉ vì gọi tool theo kiểu "một lần một cái", tuần tự, thay vì gộp (batch) hoặc xâu chuỗi (chain) chúng lại một cách thông minh.
Bài này sẽ đi sâu vào hai kỹ thuật cốt lõi: batching tool calls (gộp nhiều lệnh gọi tool vào cùng một turn) và chaining tool calls (định trước một chuỗi bước để agent thực thi mà không cần LLM suy nghĩ lại giữa các bước). Đây là những kỹ thuật có ROI (return on investment — lợi tức đầu tư) cao nhất trong toán tối ưu token, vì chúng không chỉ giảm token mà còn giảm latency và chi phí vận hành thực tế.
Hiểu về Chi Phí của Round-Trip
Mỗi lần agent gọi tool và chờ kết quả trước khi tiếp tục, đó là một round-trip (vòng lặp qua lại) giữa agent loop và LLM. Về bản chất, một round-trip bao gồm:
- LLM sinh ra tool call (input tokens của toàn bộ context + output tokens của lệnh gọi).
- Hệ thống thực thi tool, trả kết quả về.
- Kết quả tool được nhồi lại vào context, gửi toàn bộ context (bao gồm lịch sử cũ) cho LLM lần nữa để nó quyết định bước tiếp theo.
Vấn đề nằm ở bước 3: vì hầu hết kiến trúc LLM hiện tại không có trạng thái (stateless) giữa các lần gọi API, mỗi round-trip mới phải gửi lại toàn bộ context tích lũy — bao gồm system prompt, tool definitions, lịch sử hội thoại, và tất cả tool result trước đó. Nếu agent thực hiện 10 tool call tuần tự, và context trung bình mỗi lần là 15.000 token, tổng input token tiêu thụ có thể lên đến hơn 100.000 token — dù nội dung "hữu ích" thực tế chỉ tăng dần một chút.
Có ba loại chi phí cộng gộp theo số round-trip:
- Chi phí token lặp lại: context cũ được gửi lại nguyên vẹn ở mỗi round-trip (dù có prompt caching, phần cache-miss vẫn tính phí, và caching cũng có giới hạn thời gian sống).
- Chi phí latency: mỗi round-trip là một lần gọi API với thời gian xử lý riêng — network, queue, inference. 10 round-trip tuần tự có thể mất 30-60 giây, so với 1 round-trip song song chỉ mất vài giây.
- Chi phí suy luận (reasoning overhead): mỗi lần LLM "tỉnh lại" giữa các round-trip, nó phải đọc lại toàn bộ context để nhớ lại mình đang làm gì — đây là token dùng để "định hướng lại" (re-orientation), không tạo ra giá trị mới.
Công thức đơn giản để ước lượng: nếu gọi N tool tuần tự, tổng input token xấp xỉ:
Total_input_tokens ≈ N × Context_size_avg
Trong khi nếu batch được K tool call vào 1 round-trip, số round-trip giảm N/K lần, kéo theo giảm tương ứng lượng context bị gửi lại nhiều lần.
Mẹo
Trước khi tối ưu bất cứ gì, hãy đo số round-trip trung bình mỗi task bằng cách log lại số lần gọi LLM API trong một agent session. Nếu con số này cao hơn số bước logic thực tế của workflow (ví dụ workflow chỉ cần 3 bước nhưng agent thực hiện 12 round-trip), đó là dấu hiệu rõ ràng cho thấy bạn đang mất token vào round-trip dư thừa chứ không phải vào công việc thực.
Parallel Tool Calls — Nền Tảng của Batching
Hầu hết các LLM hỗ trợ function calling hiện đại (Claude, GPT, Gemini) đều có khả năng parallel tool calls (gọi nhiều tool cùng lúc trong một response): thay vì trả về một tool call, model trả về một danh sách nhiều tool call trong cùng một lượt sinh (turn). Agent loop thực thi tất cả các tool này (có thể song song hoặc tuần tự về mặt hệ thống), rồi gộp tất cả kết quả vào một tool result message duy nhất, gửi lại cho LLM.
Đây chính là cơ chế batching "miễn phí" nhất — không cần thay đổi kiến trúc, chỉ cần LLM nhận diện được rằng các tool call này độc lập với nhau (không phụ thuộc kết quả của nhau).
Ví dụ thực tế: agent cần kiểm tra trạng thái của 3 file trong một PR trước khi review. Cách tuần tự (không batch):
Turn 1: call read_file("src/auth.py") → round-trip 1
Turn 2: call read_file("src/session.py") → round-trip 2
Turn 3: call read_file("src/utils.py") → round-trip 3
Cách batch (parallel tool calls) — chỉ 1 round-trip:
{
"role": "assistant",
"content": [
{ "type": "tool_use", "id": "call_1", "name": "read_file", "input": { "path": "src/auth.py" } },
{ "type": "tool_use", "id": "call_2", "name": "read_file", "input": { "path": "src/session.py" } },
{ "type": "tool_use", "id": "call_3", "name": "read_file", "input": { "path": "src/utils.py" } }
]
}
Về phía code xử lý agent loop, việc thực thi song song có thể viết như sau (ví dụ Python, dùng asyncio):
import asyncio
async def execute_tool_calls(tool_calls: list[dict]) -> list[dict]:
"""Execute a batch of tool calls concurrently and return their results
in the same order, tagged with tool_use_id for correct mapping."""
async def run_one(call):
tool_fn = TOOL_REGISTRY[call["name"]]
try:
result = await tool_fn(**call["input"])
return {"tool_use_id": call["id"], "content": result, "is_error": False}
except Exception as exc:
return {"tool_use_id": call["id"], "content": str(exc), "is_error": True}
results = await asyncio.gather(*(run_one(c) for c in tool_calls))
return results
Điểm quan trọng: batching chỉ tiết kiệm token round-trip khi các tool call độc lập về dữ liệu — tool call thứ hai không cần kết quả của tool call thứ nhất để xác định input. Nếu có phụ thuộc (ví dụ cần đọc file trước để biết tên biến rồi mới search theo tên biến đó), buộc phải tách round-trip, nhưng đó là lúc dùng đến chaining (mục sau).
Mẹo
Khi thiết kế tool description, hãy nói rõ với model là "các tool này có thể được gọi song song nếu không phụ thuộc nhau" ngay trong system prompt hoặc trong phần mô tả nhóm tool — nhiều model chỉ batch khi được gợi ý (xem mục kế tiếp), không tự động nhận ra cơ hội batch nếu không có gợi ý ngữ cảnh.
Kích Hoạt Batching Qua Prompt Engineering
Khả năng kỹ thuật của model để làm parallel tool calls không đồng nghĩa với việc model sẽ chủ động batch. Theo mặc định, nhiều model có xu hướng gọi tool "an toàn" — một cái một lúc, đợi kết quả rồi mới quyết định bước tiếp theo — vì cách suy luận tuần tự này giống với cách con người debug từng bước.
Muốn model batch chủ động, cần prompt engineering (kỹ thuật thiết kế prompt) rõ ràng, chỉ dẫn model nhận diện độc lập giữa các tác vụ và gộp chúng lại. Dưới đây là một đoạn system prompt mẫu, viết cho agent hỗ trợ code review:
When you need to gather information from multiple independent sources
(e.g. reading several files, running several read-only checks, or
querying several unrelated endpoints), issue ALL of those tool calls
together in a single turn instead of one at a time.
Rules for batching:
- If tool call B does not need the output of tool call A to determine
its input, call A and B together, not sequentially.
- Read-only operations (read_file, search_code, get_lint_report,
get_test_results) are almost always safe to batch.
- Only call tools sequentially when a later call's input parameters
depend on the output of an earlier call.
- Before calling any tool, briefly list which calls are independent
and can be batched, then issue them in the same turn.
Một mẫu khác, dùng khi muốn ép model tự lập "kế hoạch batch" trước khi hành động (kỹ thuật gọi là "batch planning"):
Before executing tools, output a short plan:
1. List all information you need to gather.
2. Group items that can be fetched independently into batches.
3. Execute each batch as parallel tool calls in one turn.
4. Only proceed to the next batch after the current one completes,
and only if the next batch truly depends on prior results.
Kết quả thực tế: với các agent thực thi task như "review 5 file thay đổi trong PR", việc thêm hướng dẫn batching vào system prompt thường giảm số round-trip từ 5-6 xuống 1-2, tương đương giảm 60-80% token dùng để lặp lại context.
Một lưu ý quan trọng: đừng chỉ nói "hãy batch tool call" một cách chung — hãy cho ví dụ cụ thể ("ví dụ: đọc nhiều file cùng lúc") vì model học tốt hơn từ ví dụ cụ thể (few-shot) hơn là chỉ dẫn trừu tượng.
Mẹo
Test hiệu quả của prompt batching bằng cách so sánh số lượng tool_use block trong mỗi assistant message trước và sau khi thêm hướng dẫn. Nếu trung bình mỗi turn chỉ có 1 tool_use block dù task có nhiều tác vụ độc lập, prompt của bạn chưa đủ mạnh để kích hoạt batching — hãy làm rõ hơn và thêm ví dụ minh họa.
Chaining Tool Call — Định Trước Trình Tự Cho Workflow
Batching giải quyết bài toán "tool call độc lập, chạy song song". Nhưng có rất nhiều workflow trong thực tế là tuần tự có thứ tự cố định — bước sau luôn cần chạy sau bước trước, và trình tự này gần như không đổi giữa các lần chạy (ví dụ: lint → test → build → deploy). Với những workflow này, để LLM tự quyết định "bước tiếp theo là gì" ở mỗi round-trip là lãng phí — vì câu trả lời luôn giống nhau.
Tool call chaining (xâu chuỗi lệnh gọi tool) là kỹ thuật định nghĩa trước trình tự các bước thành một pipeline cố định, để hệ thống (không phải LLM) điều khiển việc chuyển từ bước này sang bước kế tiếp. LLM chỉ được gọi khi cần ra quyết định (ví dụ: đọc lint report để quyết định có cần sửa code không), không được gọi để quyết định "giờ nên làm cái gì" khi thứ tự đã biết trước.
Ví dụ thực tế: workflow chuẩn bị Pull Request (PR) trước khi submit, gồm các bước: lint code, chạy test, tạo diff review, và tóm tắt thay đổi. Đây là cách định nghĩa chain đó bằng code (Python), tách rời phần điều phối (orchestration) khỏi phần LLM:
from dataclasses import dataclass
from typing import Callable, Any
@dataclass
class ChainStep:
name: str
tool_fn: Callable[..., Any]
# Decides whether the LLM must be consulted after this step
# (e.g. to interpret a failure) or the chain can proceed automatically.
needs_llm_review: bool = False
PR_PREPARATION_CHAIN: list[ChainStep] = [
ChainStep(name="run_lint", tool_fn=run_lint, needs_llm_review=False),
ChainStep(name="run_tests", tool_fn=run_tests, needs_llm_review=False),
ChainStep(name="generate_diff", tool_fn=generate_diff, needs_llm_review=False),
ChainStep(name="summarize_changes", tool_fn=summarize_changes, needs_llm_review=True),
]
def execute_pr_preparation_chain(chain: list[ChainStep], context: dict) -> dict:
"""Run each step in order. Only escalate to the LLM when a step
fails or when the step is explicitly flagged as needing judgment."""
results = {}
for step in chain:
output = step.tool_fn(**context)
results[step.name] = output
step_failed = output.get("status") == "error"
if step_failed or step.needs_llm_review:
# Only NOW do we spend a round-trip on the LLM — to decide
# how to fix a failure, or to write a human-readable summary.
return {
"status": "needs_llm_input",
"completed_steps": results,
"pending_step": step.name,
}
context[f"{step.name}_output"] = output
return {"status": "completed", "results": results}
Với thiết kế này, một workflow chuẩn bị PR gồm 4 bước chỉ tốn 1 round-trip LLM (để viết summary cuối, hoặc để xử lý khi lint/test fail) — thay vì 4 round-trip nếu để LLM tự quyết định "bước tiếp theo là gì" sau mỗi bước.
Điểm mấu chốt của chaining là: logic điều phối nằm ở code, không nằm ở LLM. LLM chỉ tham gia ở những điểm quyết định thực sự cần suy luận (reasoning) — như đọc lỗi test để đề xuất fix, hoặc viết mô tả PR mạch lạc từ diff. Những bước "máy móc, có thứ tự cố định" nên được code hoá thành pipeline, không nên giao cho LLM điều khiển từng bước.
Mẹo
Một chain tốt nên có khả năng fail-fast và báo cáo rõ bước nào fail để khi cần đưa lại cho LLM xử lý, bạn chỉ cần gửi context của bước fail đó (không phải toàn bộ lịch sử chain) — điều này giữ context nhỏ và tiết kiệm token hơn nữa.
Kết Hợp Batch-Rồi-Chain
Trong thực tế, workflow phức tạp thường là kết hợp của cả hai: bên trong một bước của chain, có thể có nhiều tool call độc lập cần batch; và giữa các bước của chain, thứ tự vẫn cố định.
Ví dụ mở rộng workflow chuẩn bị PR ở trên: bước đầu tiên "kiểm tra chất lượng code" thực chất gồm 3 tác vụ độc lập — lint, kiểm tra type (type check), và security scan — có thể chạy song song (batch), nhưng bước này phải hoàn tất trước khi chạy test (vì test không cần chờ lint, nhưng theo quy ước team, ta chỉ chạy test khi code sạch để tiết kiệm CI resource).
Mô hình batch-then-chain (batch trong từng bước, chain giữa các bước) trông như sau:
CHAIN_WITH_BATCHED_STEPS = [
{
"name": "quality_gate",
"batch": ["run_lint", "run_typecheck", "run_security_scan"], # parallel
},
{
"name": "test_gate",
"batch": ["run_unit_tests", "run_integration_tests"], # parallel
},
{
"name": "review_prep",
"batch": ["generate_diff", "summarize_changes"], # LLM step here
},
]
Về mặt round-trip: nếu mỗi "batch" trong chain là một round-trip LLM (khi bước đó cần LLM tham gia), một workflow 3 bước với 3 batch chỉ tốn tối đa 3 round-trip — thay vì 7 round-trip nếu mọi tool call được gọi tuần tự riêng lẻ (3+2+2). Nếu các bước đầu hoàn toàn không cần LLM (chạy thẳng bằng code như ví dụ chain ở mục trước), số round-trip LLM có thể giảm về 1.
Cách tư duy đúng khi thiết kế workflow agent: hãy tự hỏi "bước nào thực sự cần LLM suy luận, bước nào chỉ là thực thi có thứ tự?" Với bước cần LLM, hỏi tiếp "trong bước này, những tool call nào độc lập với nhau và có thể batch?".
Mẹo
Vẽ sơ đồ workflow của bạn thành dạng DAG (directed acyclic graph — đồ thị có hướng không chu trình) trước khi code: các node độc lập nằm cùng một "lớp" (layer) là ứng viên batch, các lớp nối tiếp nhau là ứng viên chain. Việc này giúp nhìn rõ cấu trúc thay vì đoán mò khi viết code.
Thiết Lập Ngân Sách Round-Trip
Ngay cả khi đã áp dụng batching và chaining, một số agent vẫn có xu hướng "vòng lặp vô tổ chức" — gọi tool nhiều lần hơn cần thiết vì không có giới hạn rõ ràng. Round-trip budget (ngân sách round-trip) là kỹ thuật đặt giới hạn cứng về số lượt gọi LLM/tool tối đa cho một task, buộc agent phải hoàn thành công việc hiệu quả hơn hoặc dừng lại và báo cáo tiến độ.
Cách triển khai đơn giản trong agent loop:
MAX_ROUND_TRIPS = 6
def run_agent_task(task: str) -> dict:
round_trip_count = 0
context = build_initial_context(task)
while round_trip_count < MAX_ROUND_TRIPS:
response = call_llm(context)
round_trip_count += 1
if response.stop_reason == "end_turn":
return {"status": "done", "round_trips_used": round_trip_count}
tool_calls = extract_tool_calls(response)
results = execute_tool_calls(tool_calls) # batched execution
context = append_results(context, response, results)
# Budget exhausted — force the agent to summarize and hand off
# instead of silently continuing to burn tokens.
return {
"status": "budget_exceeded",
"round_trips_used": round_trip_count,
"message": "Round-trip budget exceeded. Returning partial progress.",
}
Ngân sách nên được đặt dựa trên độ phức tạp thực tế của task, không phải một số cố định cho mọi trường hợp. Một tác vụ đơn giản như "đọc file và tóm tắt" nên có budget 1-2; một tác vụ phức tạp như "debug lỗi CI xuyên suốt nhiều service" có thể cần budget 10-15. Đặt budget quá thấp khiến agent bị cắt ngang giữa việc; đặt quá cao khiến ngân sách vô nghĩa.
Một biến thể hữu ích: soft budget với cảnh báo. Khi đạt 70-80% ngân sách, chèn thêm một system message nhắc nhở model: "Bạn đã dùng X trên Y round-trip cho phép — hãy ưu tiên batch các tool call còn lại và tiến tới kết luận." Cách này định hướng model tự điều chỉnh hành vi thay vì bị cắt cứng.
Mẹo
Log lại lý do agent chạm ngân sách (budget_exceeded) kèm theo task gốc — đây là dữ liệu quý để phát hiện những loại task hệ thống của bạn đang thiếu batching/chaining phù hợp, từ đó cải thiện thiết kế workflow chứ không chỉ đơn thuần tăng ngân sách.
Đo Lường Hiệu Quả Batching
Không thể cải thiện thứ không đo được. Để biết batching và chaining có thực sự hiệu quả, cần theo dõi các chỉ số (metrics) cụ thể cho mỗi agent session, tối thiểu gồm:
- Số round-trip LLM trên mỗi task hoàn thành.
- Số tool call trung bình mỗi round-trip (batch size) — con số này tăng nghĩa là batching đang phát huy tác dụng.
- Tổng token input/output cho toàn task.
- Tỷ lệ token "lặp lại" — phần token trong input là context cũ được gửi lại (có thể ước lượng qua cache hit rate nếu dùng prompt caching).
- Latency tổng của task, để đối chiếu với mức giảm round-trip.
Ví dụ đoạn code log metric đơn giản, có thể tích hợp vào agent loop ở trên:
import json
import time
from dataclasses import dataclass, field
@dataclass
class SessionMetrics:
task_id: str
round_trips: int = 0
total_tool_calls: int = 0
input_tokens: int = 0
output_tokens: int = 0
started_at: float = field(default_factory=time.time)
def record_round_trip(self, tool_call_count: int, input_tok: int, output_tok: int):
self.round_trips += 1
self.total_tool_calls += tool_call_count
self.input_tokens += input_tok
self.output_tokens += output_tok
def to_log_entry(self) -> dict:
duration = time.time() - self.started_at
avg_batch_size = (
self.total_tool_calls / self.round_trips if self.round_trips else 0
)
return {
"task_id": self.task_id,
"round_trips": self.round_trips,
"total_tool_calls": self.total_tool_calls,
"avg_batch_size": round(avg_batch_size, 2),
"input_tokens": self.input_tokens,
"output_tokens": self.output_tokens,
"duration_seconds": round(duration, 2),
}
metrics = SessionMetrics(task_id="pr-prep-2026-07-09-001")
metrics.record_round_trip(tool_call_count=3, input_tok=8200, output_tok=650)
print(json.dumps(metrics.to_log_entry(), indent=2))
Với dữ liệu này, bạn có thể lập baseline trước khi tối ưu (ví dụ: trung bình 8 round-trip, avg_batch_size 1.1) và so sánh sau khi áp dụng batching/chaining (ví dụ: trung bình 3 round-trip, avg_batch_size 2.8). Nếu avg_batch_size gần 1.0 trong khi task có nhiều tác vụ độc lập, đó là dấu hiệu cần siết lại prompt batching hoặc thiết kế thêm chain.
Mẹo
Đưa các metric này vào dashboard theo thời gian (không chỉ log rời rạc) — xu hướng avg_batch_size và round_trips theo từng phiên bản prompt hoặc từng phiên bản model sẽ giúp bạn phát hiện regression sớm, ví dụ khi đổi model mới khiến agent "quên" batch dù prompt không đổi.
Tổng Kết
Batching và chaining tool call là hai kỹ thuật bổ trợ cho nhau nhằm giảm số round-trip giữa agent và LLM — nguồn tiêu hao token và latency lớn nhất trong nhiều hệ thống agent hiện nay. Batching khai thác khả năng parallel tool calls của model để gộp nhiều tác vụ độc lập vào một turn, cần được kích hoạt chủ động qua prompt engineering rõ ràng, có ví dụ cụ thể. Chaining đưa logic điều phối các bước có thứ tự cố định ra khỏi LLM, để code kiểm soát pipeline và chỉ gọi LLM tại những điểm thực sự cần suy luận.
Kết hợp cả hai theo mô hình batch-then-chain cho phép thiết kế workflow phức tạp (như chuẩn bị PR: lint, test, diff, summary) chỉ tốn một phần nhỏ round-trip so với cách tiếp cận tuần tự ngây thơ. Thêm vào đó, round-trip budget đóng vai trò như một lưới an toàn, ngăn agent rơi vào vòng lặp gọi tool vô tổ chức, còn việc đo lường liên tục (round-trip count, avg batch size, token usage) giúp bạn xác nhận rằng các tối ưu này thực sự mang lại hiệu quả — không chỉ trên lý thuyết mà cả trong vận hành thực tế.
Về bản chất, tư duy đúng khi tối ưu tool use không phải là "làm sao gọi ít tool hơn", mà là "làm sao mỗi round-trip LLM mang lại giá trị suy luận tối đa, và những gì không cần suy luận thì để code xử lý".