Hãy tưởng tượng bạn giao cho một nhân viên mới một chiếc túi công cụ chứa 80 cái tuốc-nơ-vít, cưa, máy hàn, đồng hồ đo điện... trước khi giao cho họ nhiệm vụ chỉ là "thay pin đồng hồ". Nhân viên đó sẽ mất khá nhiều thời gian để đọc hết catalogue công cụ trước khi bắt tay vào việc — dù cuối cùng chỉ dùng một cái tuốc-nơ-vít bé xíu.
Đó chính xác là điều đang xảy ra với hầu hết AI agent hiện nay. Mỗi lần gọi model, toàn bộ tool definition (định nghĩa công cụ) — tên, mô tả, JSON schema tham số — đều bị nhồi vào system prompt, bất kể agent có dùng đến hay không. Dynamic tool loading (nạp tool động) là kỹ thuật giải quyết đúng vấn đề này: chỉ đưa vào context những tool thực sự cần cho tác vụ đang xử lý, và loại bỏ phần còn lại.
Trong bài này, chúng ta sẽ đi qua bốn chiến lược triển khai dynamic tool loading từ đơn giản đến phức tạp, cách đo lường tác động thực tế bằng benchmark, và những cạm bẫy cần tránh khi áp dụng vào hệ thống production.
Vấn Đề Của Static Tool Loading (Nạp Tool Tĩnh)
Static tool loading là cách làm mặc định mà hầu hết framework agent (LangChain, OpenAI Agents SDK, Claude Agent SDK...) khuyến khích khi mới bắt đầu: khai báo toàn bộ danh sách tool một lần, gắn cứng vào agent, và gửi kèm với mọi request tới LLM (Large Language Model — mô hình ngôn ngữ lớn), dù prompt của người dùng chỉ cần một hoặc hai tool trong số đó.
Vấn đề nằm ở ba lớp chi phí chồng lên nhau:
Chi phí token trực tiếp. Một tool definition trung bình trong function calling (gọi hàm) tốn từ 150-400 token, bao gồm tên, description, và JSON schema cho từng tham số (bao gồm cả các trường optional, enum, nested object...). Một agent DevOps thực tế với 40-60 tool (Git, Docker, Kubernetes, cloud CLI, database, monitoring...) có thể tốn 8.000-15.000 token chỉ để mô tả "tôi có thể làm gì" — trước khi model đọc một dòng câu hỏi nào của người dùng. Nếu agent này xử lý 10.000 request/ngày, con số này nhân lên thành hàng trăm triệu token lãng phí mỗi tháng.
Chi phí suy luận (reasoning cost). Đây là phần ít người để ý nhưng lại nghiêm trọng hơn chi phí token. Khi model phải quét qua 60 tool để tìm ra tool phù hợp, xác suất chọn sai tool, chọn tool gần giống nhưng không đúng, hoặc bị "tool confusion" (nhầm lẫn công cụ) tăng lên đáng kể. Nhiều báo cáo thực nghiệm từ các team xây dựng agent production cho thấy: khi số tool vượt qua khoảng 20-30, độ chính xác chọn tool (tool selection accuracy) bắt đầu giảm rõ rệt, kể cả với các model mạnh.
Chi phí context window bị "ăn mòn". Tool definition chiếm chỗ trong context window (bộ nhớ ngữ cảnh) mà lẽ ra dành cho lịch sử hội thoại, tool result, hoặc reasoning của model. Với các agent chạy vòng lặp dài (multi-turn), phần "cố định" này lặp lại ở mọi turn, khiến context window co lại nhanh hơn dự kiến và buộc agent phải cắt bớt lịch sử sớm hơn.
Điều đáng nói là: phần lớn các tool này không liên quan gì đến tác vụ hiện tại. Một agent được hỏi "sửa lỗi test đang fail trong file user_service_test.go" không cần biết cách tạo Kubernetes deployment hay gửi email qua SendGrid. Static loading buộc model phải "gánh" toàn bộ catalogue trong mọi tình huống, dù ngữ cảnh đã nói rõ tác vụ chỉ cần một tập con nhỏ.
Mẹo
Trước khi tối ưu bất cứ điều gì, hãy đo baseline: log lại số token của phần tool definition trong mỗi request, và tỷ lệ tool thực sự được gọi so với tổng số tool được nạp. Nếu tỷ lệ "tool được nạp nhưng không bao giờ dùng tới" trong một session vượt 70-80%, đó là dấu hiệu rõ ràng bạn cần dynamic tool loading — không cần đoán, số liệu sẽ tự nói.
Strategy 1: Intent-Based Tool Selection (Chọn Tool Theo Ý Định Người Dùng)
Cách tiếp cận đầu tiên và dễ triển khai nhất: phân loại ý định (intent) của request đầu vào, rồi map ý định đó sang một nhóm tool cố định — tương tự cách một tổng đài phân loại cuộc gọi vào đúng phòng ban trước khi chuyển máy.
Ý tưởng cốt lõi: định nghĩa trước các "tool group" (nhóm công cụ) theo use case thường gặp trong domain của bạn, rồi dùng một bước phân loại nhẹ (có thể là rule-based, có thể là một lần gọi LLM giá rẻ) để xác định request thuộc nhóm nào trước khi build tool list gửi cho model chính.
Ví dụ với một coding agent nội bộ:
TOOL_GROUPS = {
"git_workflow": [
"git_status", "git_diff", "git_commit",
"git_branch_create", "git_push", "git_log",
],
"testing": [
"run_unit_tests", "run_integration_tests",
"generate_test_report", "get_coverage_summary",
],
"filesystem": [
"read_file", "write_file", "list_directory",
"search_in_files", "delete_file",
],
"database": [
"run_sql_query", "inspect_schema",
"create_migration", "rollback_migration",
],
"deployment": [
"build_docker_image", "deploy_to_staging",
"deploy_to_production", "rollback_deployment",
],
}
def classify_intent(user_request: str) -> list[str]:
"""Rule-based classifier — không cần gọi LLM, chạy tức thì."""
request_lower = user_request.lower()
groups = []
git_keywords = ["commit", "branch", "merge", "push", "pull request"]
test_keywords = ["test", "coverage", "unit test", "fail", "assert"]
if any(kw in request_lower for kw in git_keywords):
groups.append("git_workflow")
if any(kw in request_lower for kw in test_keywords):
groups.append("testing")
# Fallback: nếu không khớp keyword nào, dùng nhóm an toàn nhất
if not groups:
groups.append("filesystem")
return groups
def build_tool_list(user_request: str) -> list[str]:
groups = classify_intent(user_request)
tool_names = set()
for g in groups:
tool_names.update(TOOL_GROUPS[g])
return list(tool_names)
Với request "Sửa lỗi test đang fail trong user_service_test.go rồi commit", classifier sẽ trả về ["testing", "git_workflow"] — nghĩa là model chỉ nhận khoảng 10 tool definition thay vì toàn bộ 25 tool trong catalogue. Filesystem, database, deployment tool hoàn toàn bị loại khỏi context vì không liên quan.
Bạn có thể nâng cấp classifier này bằng một model nhỏ, rẻ (ví dụ Claude Haiku hoặc một model open-source local) để phân loại ý định chính xác hơn khi rule-based keyword-matching không đủ tinh vi — đánh đổi một khoản chi phí nhỏ (vài chục token cho lời gọi phân loại) để tiết kiệm hàng nghìn token ở lời gọi chính.
Điểm mạnh của intent-based selection là đơn giản, dễ debug, dễ audit — bạn luôn biết chính xác vì sao một tool được hoặc không được nạp. Điểm yếu là cần duy trì logic phân loại thủ công, và khi request thuộc nhiều domain cùng lúc (ví dụ vừa sửa code vừa deploy vừa cập nhật database), bạn cần load nhiều group, làm giảm phần nào hiệu quả tiết kiệm.
Mẹo
Luôn có một "fallback group" an toàn (ví dụ nhóm filesystem cơ bản) cho trường hợp classifier không chắc chắn. Đừng để agent rơi vào tình trạng "0 tool nào được nạp" — tệ hơn nhiều so với nạp dư vài tool không cần thiết.
Strategy 2: Workflow-Stage Tool Loading (Nạp Tool Theo Giai Đoạn Workflow)
Nếu agent của bạn vận hành theo một workflow có cấu trúc rõ ràng — ví dụ pipeline CI/CD, quy trình xử lý ticket support, hoặc quy trình review code nhiều bước — bạn có thể bỏ hẳn bước classifier và map trực tiếp stage (giai đoạn) sang tool tương ứng. Đây là cách tiếp cận deterministic (xác định, không phụ thuộc vào suy luận của model hay classifier), nên độ tin cậy cực cao và không tốn thêm token/latency cho bước phân loại.
Lấy ví dụ agent xử lý một pull request qua các giai đoạn: phân tích → sửa code → viết test → review → merge.
from enum import Enum
class Stage(Enum):
ANALYZE = "analyze"
IMPLEMENT = "implement"
TEST = "test"
REVIEW = "review"
MERGE = "merge"
STAGE_TOOLS = {
Stage.ANALYZE: [
"read_file", "search_in_files", "get_file_history",
"inspect_dependency_graph",
],
Stage.IMPLEMENT: [
"read_file", "write_file", "search_in_files",
"run_linter",
],
Stage.TEST: [
"run_unit_tests", "run_integration_tests",
"get_coverage_summary",
],
Stage.REVIEW: [
"git_diff", "post_review_comment", "get_pr_metadata",
],
Stage.MERGE: [
"git_merge", "git_push", "close_pr", "notify_slack_channel",
],
}
class WorkflowAgent:
def __init__(self):
self.current_stage = Stage.ANALYZE
def get_tools_for_current_stage(self) -> list[str]:
# Deterministic — không cần model đoán, không cần gọi classifier
return STAGE_TOOLS[self.current_stage]
def advance_stage(self, next_stage: Stage):
self.current_stage = next_stage
Ở stage ANALYZE, agent chỉ có 4 tool đọc và tìm kiếm — không có quyền write_file hay git_merge. Điều này không chỉ tiết kiệm token, mà còn tăng an toàn: agent không thể vô tình merge code khi đang ở giai đoạn phân tích, vì tool đó đơn giản là không tồn tại trong context của nó ở stage này. Đây là một dạng "principle of least privilege" (nguyên tắc đặc quyền tối thiểu) áp dụng vào tool access.
So với Strategy 1, workflow-stage loading phù hợp nhất khi quy trình có tính tuần tự, có thể mô hình hóa thành state machine (máy trạng thái) rõ ràng. Nó không phù hợp với agent xử lý request tự do, không theo khuôn mẫu cố định — trường hợp đó nên quay lại Strategy 1 hoặc kết hợp cả hai (dùng stage để giới hạn phạm vi lớn, dùng intent để tinh chỉnh trong phạm vi đó).
Mẹo
Log lại lịch sử chuyển stage của agent. Nếu bạn thấy agent liên tục phải "quay lại" một stage trước đó vì thiếu tool (ví dụ đang ở REVIEW nhưng cần sửa code), đó là dấu hiệu workflow model của bạn chưa phản ánh đúng thực tế — cần thiết kế lại state machine, không phải thêm tool bừa vào mọi stage.
Strategy 3: Tool Discovery via MCP Dynamic Filtering (Khám Phá Tool Qua MCP Với Bộ Lọc Động)
Model Context Protocol (MCP) — giao thức chuẩn hóa việc kết nối agent với tool và data source — mở ra một cách tiếp cận khác biệt hoàn toàn: thay vì gắn cứng danh sách tool ở phía client, để chính MCP server (server cung cấp tool qua MCP) quyết định tool nào nên được expose (hiển thị) trong từng ngữ cảnh, thông qua cơ chế filtering (lọc) động.
Về bản chất, một MCP server có thể quản lý hàng trăm tool (ví dụ một server tích hợp toàn bộ API của một nền tảng cloud provider), nhưng khi client gọi tools/list, server không cần trả về toàn bộ danh sách — nó có thể trả về một tập con dựa trên:
- Session context: thông tin về project, workspace, hoặc quyền hạn của người dùng hiện tại đã được thiết lập từ đầu session.
- Metadata do client truyền vào: ví dụ client gửi kèm
{"scope": "read-only"}khi khởi tạo kết nối, server sẽ chỉ list các tool read-only. - Resource đang được truy cập: nếu agent đang mở một file
.tf(Terraform), server hạ tầng có thể chỉ expose các tool liên quan đến Terraform, ẩn đi các tool liên quan Kubernetes hay Ansible.
Điểm khác biệt cốt lõi so với Strategy 1 và 2: logic lọc nằm ở phía server cung cấp tool, không nằm ở phía agent code. Điều này rất có giá trị khi bạn không kiểm soát toàn bộ codebase của agent (ví dụ agent chạy trên Claude Desktop, Cursor, hoặc bất kỳ MCP client nào của bên thứ ba) — bạn vẫn có thể kiểm soát được lượng token tool definition tiêu tốn, chỉ bằng cách điều chỉnh server.
Về mặt triển khai, một MCP server tuân theo spec hiện tại thường implement tools/list như một hàm động, không phải một danh sách tĩnh:
// Ví dụ request tools/list có kèm context filter (dạng minh họa ý tưởng,
// cơ chế filter cụ thể phụ thuộc vào từng implementation MCP server)
{
"jsonrpc": "2.0",
"method": "tools/list",
"params": {
"cursor": null,
"_meta": {
"activeFileType": "terraform",
"permissionScope": "infra-readonly"
}
}
}
Server nhận request này, đọc _meta.activeFileType và _meta.permissionScope, rồi chỉ trả về các tool phù hợp — thay vì toàn bộ catalogue của server. Nhiều MCP server production còn kết hợp thêm cơ chế phân trang (cursor) để tránh trả về quá nhiều tool trong một lần, cho phép client chỉ nạp thêm khi cần.
Chiến lược này đặc biệt hiệu quả khi bạn xây dựng MCP server cho một nền tảng lớn (ví dụ tích hợp toàn bộ suite API nội bộ của công ty), nơi số lượng tool tiềm năng có thể lên đến hàng trăm, nhưng mỗi agent/session thực tế chỉ cần một phần nhỏ.
Mẹo
Khi thiết kế MCP server, đừng bắt client phải "biết trước" nó cần filter gì. Cung cấp một tool discovery nhẹ (ví dụ một tool list_available_capabilities trả về mô tả rất ngắn, không kèm full schema) để agent tự query trước, rồi mới request full definition cho đúng tool nó cần — giống mô hình "lazy loading" quen thuộc trong lập trình.
Strategy 4: Capability Negotiation Khi Agent Khởi Động (Thỏa Thuận Năng Lực Lúc Startup)
Capability negotiation (thỏa thuận năng lực) là bước bổ sung diễn ra một lần duy nhất khi agent khởi động session, trước khi bất kỳ tool definition nào được nạp vào context chính. Ý tưởng: agent và host (môi trường chạy agent — IDE, CLI, hay orchestrator) trao đổi với nhau về "năng lực" cần và có, rồi mới quyết định tập tool nào sẽ được kích hoạt cho toàn bộ session.
Khác với ba chiến lược trên (vốn tái đánh giá theo từng request hoặc từng stage), capability negotiation hoạt động ở tầng session: một lần thỏa thuận, áp dụng cho suốt phiên làm việc (hoặc cho đến khi có sự kiện thay đổi rõ ràng, ví dụ người dùng mở một loại project khác).
Quy trình thường gồm các bước:
- Host khai báo ngữ cảnh môi trường: loại project (Node.js, Go, Python...), các service đang kết nối (database nào, cloud provider nào), quyền hạn của người dùng hiện tại.
- Agent/orchestrator đối chiếu với capability registry: một bảng tra cứu nội bộ map từ ngữ cảnh môi trường sang các nhóm tool phù hợp — về bản chất là mở rộng của ý tưởng ở Strategy 1, nhưng chạy một lần ở đầu session.
- Cả hai bên "thỏa thuận": nếu agent yêu cầu một capability mà host không hỗ trợ (ví dụ agent muốn dùng tool
execute_sqlnhưng host không có kết nối database nào được cấu hình), tool đó bị loại khỏi tập kích hoạt ngay từ đầu — tránh việc model "thấy" tool tồn tại nhưng gọi vào sẽ luôn fail.
class CapabilityRegistry:
def __init__(self):
self.registry = {
"has_database_connection": ["run_sql_query", "inspect_schema"],
"has_docker": ["build_docker_image", "run_container"],
"has_kubernetes_context": ["kubectl_apply", "kubectl_get_pods"],
"has_write_permission": ["write_file", "delete_file", "git_push"],
}
def negotiate(self, environment_context: dict) -> list[str]:
"""Chạy một lần duy nhất khi agent session khởi tạo."""
active_tools = []
for capability, tools in self.registry.items():
if environment_context.get(capability, False):
active_tools.extend(tools)
return active_tools
env_context = {
"has_database_connection": False,
"has_docker": True,
"has_kubernetes_context": False,
"has_write_permission": True,
}
registry = CapabilityRegistry()
session_tools = registry.negotiate(env_context)
Lợi ích lớn nhất của cách này là loại bỏ hoàn toàn overhead tính toán ở từng turn — không cần classifier, không cần state machine transition liên tục — vì quyết định đã được đưa ra một lần. Đây cũng là cơ chế tự nhiên để tránh tình huống agent "gọi nhầm" một tool không khả dụng trong môi trường thực tế (ví dụ gọi kubectl_apply khi không có Kubernetes context nào được kết nối), vì tool đó chưa bao giờ xuất hiện trong tool list.
Hạn chế: nếu ngữ cảnh môi trường thay đổi giữa session (ví dụ người dùng vừa kết nối thêm một database mới), bạn cần cơ chế re-negotiate hoặc agent sẽ "không biết" là mình giờ có thêm năng lực mới. Với các agent chạy dài hạn, nên kết hợp capability negotiation làm bước lọc thô ban đầu (loại bỏ hẳn các nhóm tool không khả dụng), rồi dùng Strategy 1 hoặc 2 để tinh chỉnh tiếp trong phạm vi đã được negotiate.
Mẹo
Đừng chỉ negotiate capability kỹ thuật (có Docker hay không, có DB hay không) — hãy negotiate cả capability về quyền hạn (permission). Rất nhiều sự cố agent "làm quá tay" (xóa file, push code sai nhánh...) xuất phát từ việc tool nguy hiểm vẫn nằm trong tool list dù người dùng hiện tại không có quyền thực hiện hành động đó.
Đo Lường Tác Động Của Dynamic Tool Loading
Tối ưu mà không đo là tối ưu "cho có". Trước khi tuyên bố một chiến lược dynamic tool loading nào đó hiệu quả, bạn cần benchmark trên một tập request đại diện cho traffic thực tế — không phải vài ví dụ tự nghĩ ra.
Cách làm chuẩn: chuẩn bị một bộ 50 request mẫu, phản ánh đúng phân phối các loại tác vụ mà agent của bạn xử lý trong thực tế (ví dụ nếu 40% request liên quan git workflow, 30% liên quan testing, thì bộ mẫu cũng nên giữ tỷ lệ tương tự). Sau đó chạy agent với static loading (baseline) và với dynamic loading, đo ba chỉ số: token tiêu thụ cho tool definition, độ chính xác chọn tool, và latency.
import time
import statistics
def run_benchmark(requests: list[dict], agent_static, agent_dynamic):
"""
requests: list of {"prompt": str, "expected_tool": str}
agent_static / agent_dynamic: hai instance agent, khác nhau
ở cách nạp tool, giống nhau về model/config còn lại.
"""
results = {"static": [], "dynamic": []}
for req in requests:
for label, agent in [("static", agent_static), ("dynamic", agent_dynamic)]:
start = time.perf_counter()
response = agent.run(req["prompt"])
elapsed = time.perf_counter() - start
results[label].append({
"tool_definition_tokens": response.tool_definition_token_count,
"correct_tool_selected": response.tool_called == req["expected_tool"],
"latency_seconds": elapsed,
})
return results
def summarize(results: dict, label: str):
data = results[label]
avg_tokens = statistics.mean(r["tool_definition_tokens"] for r in data)
accuracy = sum(r["correct_tool_selected"] for r in data) / len(data)
avg_latency = statistics.mean(r["latency_seconds"] for r in data)
print(f"[{label}] avg tool-def tokens: {avg_tokens:.0f}")
print(f"[{label}] tool selection accuracy: {accuracy:.1%}")
print(f"[{label}] avg latency: {avg_latency:.2f}s")
benchmark_requests = load_representative_requests("benchmark_50_requests.jsonl")
results = run_benchmark(benchmark_requests, static_agent, dynamic_agent)
summarize(results, "static")
summarize(results, "dynamic")
Với thiết lập benchmark như trên, trong nhiều trường hợp thực tế (agent có 40-60 tool, áp dụng Strategy 1 hoặc 2), bạn có thể kỳ vọng thấy: token tiêu thụ cho tool definition giảm 60-85% (do chỉ nạp tập con liên quan), độ chính xác chọn tool giữ nguyên hoặc tăng nhẹ (vì model không còn bị "phân tâm" bởi các tool không liên quan), và latency giảm nhẹ do prompt ngắn hơn, ít token cần xử lý ở đầu vào.
Điều quan trọng cần lưu ý: kết quả cụ thể phụ thuộc rất nhiều vào domain, số lượng tool ban đầu, và độ chính xác của lớp phân loại (classifier hay stage-mapping) bạn dùng. Luôn tự chạy benchmark trên chính hệ thống của mình, đừng tin vào một con số phần trăm chung chung nào đó đọc được đâu đó — hãy để dữ liệu từ 50 request đại diện của chính bạn quyết định.
Mẹo
Đừng chỉ đo trung bình (average) — hãy đo cả worst-case (trường hợp tệ nhất): request nào bị classifier phân loại sai, dẫn đến agent thiếu tool cần thiết và phải retry hoặc trả lỗi. Chi phí của một lần retry (thường gấp đôi số turn) có thể "ăn hết" toàn bộ khoản tiết kiệm token bạn đạt được từ dynamic loading, nếu tỷ lệ misclassification quá cao.
Tổng Kết
Dynamic tool loading giải quyết một vấn đề rất cụ thể: đừng bắt LLM đọc catalogue công cụ khổng lồ khi tác vụ chỉ cần vài cái. Bốn chiến lược trong bài — intent-based selection, workflow-stage loading, MCP dynamic filtering, và capability negotiation — không loại trừ nhau, mà thường được kết hợp theo tầng: capability negotiation lọc thô một lần lúc khởi động, workflow-stage loading thu hẹp theo giai đoạn, và intent-based selection tinh chỉnh trong từng request cụ thể.
Chọn chiến lược nào phụ thuộc vào đặc điểm agent của bạn: nếu workflow có cấu trúc rõ ràng, ưu tiên Strategy 2 vì tính deterministic và không tốn thêm chi phí phân loại. Nếu request đến từ người dùng tự do, không theo khuôn mẫu, Strategy 1 linh hoạt hơn. Nếu bạn xây MCP server cho nhiều client khác nhau không kiểm soát được, Strategy 3 là lựa chọn duy nhất khả thi. Và trong mọi trường hợp, capability negotiation nên là lớp lọc đầu tiên, rẻ nhất, chạy một lần cho cả session.
Điều quan trọng nhất, như đã nhấn mạnh ở phần benchmark: đừng triển khai dynamic tool loading rồi tin bằng cảm giác là nó hiệu quả. Đo bằng số liệu thật, trên request thật, và tiếp tục điều chỉnh classifier hoặc stage-mapping khi phân phối traffic của bạn thay đổi theo thời gian.