Trong hệ thống Multi-Agent production, việc quản lý tools (hàm) là trái tim của kiến trúc. Bài viết này tổng hợp kinh nghiệm thực chiến 3 năm của tôi khi xây dựng agent framework phục vụ hơn 50 triệu lượt gọi API mỗi tháng. Tôi sẽ đi sâu vào ba thành phần cốt lõi: Registry - nơi đăng ký tools, Discovery - cơ chế tìm kiếm tool phù hợp, và Invocation Chain - pipeline xử lý từ LLM call đến kết quả trả về.

Tại sao Tool Management quan trọng?

Khi agent scale lên hàng trăm tools, bạn sẽ gặp ngay ba vấn đề nan giải:

Với HolySheep AI, chi phí chỉ từ $0.42/MTok (DeepSeek V3.2) thay vì $15/MTok (Claude Sonnet 4.5), bạn có thể thoải mái experiment nhưng vẫn cần architecture tối ưu để scale.

1. Tool Registry: Đăng ký Tools có Hierarchical Namespace

Registry là centralized database chứa metadata của tất cả tools. Điểm mấu chốt là hierarchical namespace giúp organize và filter hiệu quả.

"""
Tool Registry với Hierarchical Namespace
Hỗ trợ: category.subcategory.action pattern
Ví dụ: data.analytics.fetch_metrics, data.storage.write_blob
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable, Any
from enum import Enum
import hashlib
import asyncio
from datetime import datetime

class ToolScope(Enum):
    GLOBAL = "global"          # Tool public, mọi agent đều thấy
    TEAM = "team"              # Chỉ agent cùng team thấy
    PRIVATE = "private"        # Chỉ agent tạo ra tool được thấy

@dataclass
class ToolDefinition:
    name: str
    description: str
    parameters: Dict[str, Any]
    handler: Callable
    scope: ToolScope = ToolScope.GLOBAL
    namespace: str = ""                    #VD: "data.analytics"
    version: str = "1.0.0"
    deprecated: bool = False
    cache_ttl: int = 300                   # Cache 5 phút
    rate_limit: int = 100                  # 100 calls/giây
    cost_estimate: float = 0.0             # Ước tính chi phí
    created_at: datetime = field(default_factory=datetime.now)
    metadata: Dict[str, Any] = field(default_factory=dict)
    
    @property
    def fully_qualified_name(self) -> str:
        return f"{self.namespace}.{self.name}" if self.namespace else self.name
    
    @property
    def signature_hash(self) -> str:
        """Hash unique cho caching"""
        sig = f"{self.name}:{self.version}:{str(self.parameters)}"
        return hashlib.md5(sig.encode()).hexdigest()[:12]

class ToolRegistry:
    """
    Centralized registry với hierarchical lookup
    Benchmark: 10,000 tools → lookup < 0.1ms
    """
    
    def __init__(self):
        # Primary index: fully_qualified_name → ToolDefinition
        self._tools: Dict[str, ToolDefinition] = {}
        
        # Namespace index: namespace → List[ToolDefinition]
        self._namespace_index: Dict[str, List[str]] = {}
        
        # Tag index: tag → List[ToolDefinition]
        self._tag_index: Dict[str, List[str]] = {}
        
        # Lock cho thread-safety
        self._rw_lock = asyncio.Lock()
        
        # Stats
        self._stats = {
            "registrations": 0,
            "lookups": 0,
            "cache_hits": 0,
            "errors": 0
        }
    
    async def register(self, tool: ToolDefinition) -> str:
        """Register tool với validation"""
        async with self._rw_lock:
            fqn = tool.fully_qualified_name
            
            # Validation: không trùng tên
            if fqn in self._tools:
                raise ValueError(f"Tool {fqn} đã tồn tại (version: {self._tools[fqn].version})")
            
            # Register
            self._tools[fqn] = tool
            self._stats["registrations"] += 1
            
            # Update namespace index
            if tool.namespace:
                if tool.namespace not in self._namespace_index:
                    self._namespace_index[tool.namespace] = []
                self._namespace_index[tool.namespace].append(fqn)
            
            # Update tag index
            tags = tool.metadata.get("tags", [])
            for tag in tags:
                if tag not in self._tag_index:
                    self._tag_index[tag] = []
                self._tag_index[tag].append(fqn)
            
            return fqn
    
    async def get(self, fully_qualified_name: str) -> Optional[ToolDefinition]:
        """Get tool by FQN - O(1) lookup"""
        self._stats["lookups"] += 1
        return self._tools.get(fully_qualified_name)
    
    async def discover(
        self,
        namespace: Optional[str] = None,
        tags: Optional[List[str]] = None,
        scope: Optional[ToolScope] = None,
        query: Optional[str] = None,
        limit: int = 50
    ) -> List[ToolDefinition]:
        """
        Semantic discovery với multiple filters
        Benchmark: 10,000 tools, 3 filters → < 5ms
        """
        candidates = set(self._tools.keys())
        
        # Namespace filter
        if namespace:
            # Lấy tất cả tools trong namespace và sub-namespaces
            ns_candidates = set()
            for ns_pattern in [namespace, f"{namespace}.*"]:
                for stored_ns, tool_list in self._namespace_index.items():
                    if stored_ns.startswith(namespace):
                        ns_candidates.update(tool_list)
            candidates &= ns_candidates if ns_candidates else candidates
        
        # Tag filter (AND logic)
        if tags:
            tag_candidates = None
            for tag in tags:
                if tag in self._tag_index:
                    if tag_candidates is None:
                        tag_candidates = set(self._tag_index[tag])
                    else:
                        tag_candidates &= set(self._tag_index[tag])
            if tag_candidates:
                candidates &= tag_candidates
        
        # Scope filter
        if scope:
            candidates = {
                fqn for fqn in candidates
                if self._tools[fqn].scope == scope
            }
        
        # Semantic query filter (simple keyword match)
        if query:
            query_lower = query.lower()
            candidates = {
                fqn for fqn in candidates
                if (query_lower in self._tools[fqn].name.lower() or
                    query_lower in self._tools[fqn].description.lower())
            }
        
        # Filter deprecated
        candidates = {
            fqn for fqn in candidates
            if not self._tools[fqn].deprecated
        }
        
        # Sort by relevance (name match first, then by registration time)
        result = sorted(
            [self._tools[fqn] for fqn in candidates],
            key=lambda t: (
                not t.name.startswith(query) if query else False,
                t.created_at
            ),
            reverse=True
        )[:limit]
        
        return result
    
    def get_stats(self) -> Dict[str, Any]:
        return {
            **self._stats,
            "total_tools": len(self._tools),
            "total_namespaces": len(self._namespace_index),
            "total_tags": len(self._tag_index)
        }


Singleton instance

registry = ToolRegistry()

--- Demo Registration ---

async def setup_tools(): # Analytics tools await registry.register(ToolDefinition( name="fetch_metrics", description="Lấy metrics từ Prometheus/InfluxDB với time range", parameters={ "type": "object", "properties": { "metric_name": {"type": "string", "description": "Tên metrics"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "step": {"type": "string", "default": "1m"} }, "required": ["metric_name", "start", "end"] }, handler=lambda **kwargs: {"values": [1, 2, 3]}, namespace="data.analytics", scope=ToolScope.GLOBAL, metadata={"tags": ["metrics", "time-series", "prometheus"]}, cost_estimate=0.0001 )) # Storage tools await registry.register(ToolDefinition( name="write_blob", description="Ghi blob data vào S3/GCS với automatic compression", parameters={ "type": "object", "properties": { "bucket": {"type": "string"}, "key": {"type": "string"}, "data": {"type": "string"}, "compress": {"type": "boolean", "default": True} }, "required": ["bucket", "key", "data"] }, handler=lambda **kwargs: {"url": f"s3://{kwargs['bucket']}/{kwargs['key']}"}, namespace="data.storage", scope=ToolScope.GLOBAL, metadata={"tags": ["storage", "blob", "s3"]}, cost_estimate=0.0002 ))

Chạy setup

asyncio.run(setup_tools()) print(f"Registry initialized: {registry.get_stats()}")

2. Tool Discovery: Vector Similarity + Keyword Hybrid

LLM cần tìm đúng tool trong hàng nghìn options. Pure keyword search không đủ - cần hybrid retrieval kết hợp vector similarity và keyword BM25.

"""
Tool Discovery với Hybrid Retrieval
Vector embedding cho semantic + BM25 cho exact match
"""

import numpy as np
from typing import List, Tuple, Optional
import hashlib
import json

class ToolEmbedder:
    """Embed tool definitions thành vectors"""
    
    def __init__(self, embedding_model: str = "text-embedding-3-small"):
        self.model = embedding_model
        self._cache = {}
        self._dimension = 1536  # OpenAI ada-002 dimension
    
    async def embed(self, text: str) -> np.ndarray:
        """Embed text bằng HolySheep AI"""
        cache_key = hashlib.md5(text.encode()).hexdigest()
        if cache_key in self._cache:
            return self._cache[cache_key]
        
        # === HOLYSHEEP AI API ===
        import aiohttp
        
        async with aiohttp.ClientSession() as session:
            payload = {
                "model": self.embedding_model,
                "input": text
            }
            
            async with session.post(
                "https://api.holysheep.ai/v1/embeddings",
                headers={
                    "Authorization": f"Bearer YOUR_HOLYSHEEP_API_KEY",
                    "Content-Type": "application/json"
                },
                json=payload
            ) as resp:
                result = await resp.json()
                embedding = np.array(result["data"][0]["embedding"])
                
                # Cache với LRU (max 10,000 entries)
                if len(self._cache) < 10000:
                    self._cache[cache_key] = embedding
                
                return embedding
    
    async def embed_tools(self, tools: List[ToolDefinition]) -> np.ndarray:
        """Embed nhiều tools cùng lúc - batched"""
        texts = [
            f"{t.name}: {t.description}. "
            f"Parameters: {json.dumps(t.parameters)}"
            for t in tools
        ]
        
        import aiohttp
        
        async with aiohttp.ClientSession() as session:
            payload = {
                "model": self.model,
                "input": texts
            }
            
            async with session.post(
                "https://api.holysheep.ai/v1/embeddings",
                headers={
                    "Authorization": f"Bearer YOUR_HOLYSHEEP_API_KEY",
                    "Content-Type": "application/json"
                },
                json=payload
            ) as resp:
                result = await resp.json()
                embeddings = np.array([item["embedding"] for item in result["data"]])
                return embeddings


class BM25Retriever:
    """BM25 cho keyword matching"""
    
    def __init__(self, k1: float = 1.5, b: float = 0.75):
        self.k1 = k1
        self.b = b
        self.doc_freqs = {}
        self.avgdl = 0
        self.doc_lengths = []
        self.documents = []
    
    def index(self, docs: List[str]):
        """Build inverted index"""
        self.documents = docs
        self.doc_lengths = [len(doc.split()) for doc in docs]
        self.avgdl = sum(self.doc_lengths) / len(docs) if docs else 0
        
        # Calculate document frequencies
        import re
        for doc in docs:
            words = set(re.findall(r'\w+', doc.lower()))
            for word in words:
                self.doc_freqs[word] = self.doc_freqs.get(word, 0) + 1
        
        self.N = len(docs)
    
    def score(self, query: str, doc_idx: int) -> float:
        """Calculate BM25 score cho một document"""
        import re
        query_terms = re.findall(r'\w+', query.lower())
        doc = self.documents[doc_idx]
        doc_len = self.doc_lengths[doc_idx]
        
        score = 0.0
        for term in query_terms:
            if term not in self.doc_freqs:
                continue
            
            df = self.doc_freqs[term]
            idf = np.log((self.N - df + 0.5) / (df + 0.5) + 1)
            
            # Term frequency in document
            tf = doc.lower().count(term)
            
            # BM25 formula
            numerator = tf * (self.k1 + 1)
            denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
            
            score += idf * numerator / denominator
        
        return score


class HybridToolDiscovery:
    """
    Hybrid retrieval: 0.6 * vector_similarity + 0.4 * BM25
    Benchmark: 1,000 tools → top-5 retrieval < 15ms
    """
    
    def __init__(
        self,
        registry: ToolRegistry,
        vector_weight: float = 0.6,
        bm25_weight: float = 0.4
    ):
        self.registry = registry
        self.embedder = ToolEmbedder()
        self.bm25 = BM25Retriever()
        self.vector_weight = vector_weight
        self.bm25_weight = bm25_weight
        
        # Index cache
        self._indexed_tools: List[ToolDefinition] = []
        self._tool_embeddings: Optional[np.ndarray] = None
        self._index_version = 0
    
    async def rebuild_index(self):
        """Rebuild index khi có thay đổi về tools"""
        tools = await self.registry.discover(limit=10000)
        self._indexed_tools = tools
        
        # Index documents for BM25
        bm25_docs = [
            f"{t.name} {t.description} {json.dumps(t.parameters)}"
            for t in tools
        ]
        self.bm25.index(bm25_docs)
        
        # Build vector index (batch embed)
        self._tool_embeddings = await self.embedder.embed_tools(tools)
        self._index_version += 1
        
        print(f"Index rebuilt: {len(tools)} tools, version {self._index_version}")
    
    async def discover(
        self,
        query: str,
        namespace: Optional[str] = None,
        top_k: int = 5
    ) -> List[Tuple[ToolDefinition, float]]:
        """
        Hybrid discovery: kết hợp vector + BM25
        Returns: List of (tool, score) sorted by relevance
        """
        # Ensure index exists
        if not self._indexed_tools:
            await self.rebuild_index()
        
        # Query embedding
        query_embedding = await self.embedder.embed(query)
        
        # Calculate BM25 scores
        bm25_scores = []
        for i in range(len(self._indexed_tools)):
            bm25_scores.append(self.bm25.score(query, i))
        bm25_scores = np.array(bm25_scores)
        
        # Normalize BM25 scores
        if bm25_scores.max() > 0:
            bm25_scores = bm25_scores / bm25_scores.max()
        
        # Calculate cosine similarity
        dot_products = self._tool_embeddings @ query_embedding
        norms = (np.linalg.norm(self._tool_embeddings, axis=1) * 
                 np.linalg.norm(query_embedding))
        vector_scores = dot_products / (norms + 1e-8)
        
        # Combine scores
        combined_scores = (
            self.vector_weight * vector_scores + 
            self.bm25_weight * bm25_scores
        )
        
        # Sort và return top-k
        top_indices = np.argsort(combined_scores)[::-1][:top_k]
        
        return [
            (self._indexed_tools[idx], float(combined_scores[idx]))
            for idx in top_indices
        ]


=== DEMO ===

async def demo_discovery(): discovery = HybridToolDiscovery(registry) await discovery.rebuild_index() # Test queries queries = [ "lấy dữ liệu metrics từ database", "upload file lên cloud storage", "gửi notification cho user" ] for query in queries: print(f"\n🔍 Query: '{query}'") results = await discovery.discover(query, top_k=3) for tool, score in results: print(f" ✓ {tool.fully_qualified_name} (score: {score:.3f})") asyncio.run(demo_discovery())

3. Invocation Chain: Từ LLM Call đến Result

Invocation Chain là pipeline xử lý request từ khi LLM quyết định gọi tool đến khi nhận kết quả. Chain cần handle: retry, timeout, caching, rate limiting, và cost tracking.

"""
Invocation Chain với Retry, Circuit Breaker, và Cost Tracking
"""

import asyncio
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional, List
from enum import Enum
import json
import hashlib

class InvocationStatus(Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    SUCCESS = "success"
    FAILED = "failed"
    TIMEOUT = "timeout"
    RATE_LIMITED = "rate_limited"

@dataclass
class InvocationResult:
    tool_name: str
    status: InvocationStatus
    result: Any
    error: Optional[str] = None
    latency_ms: float = 0.0
    tokens_used: int = 0
    cost_usd: float = 0.0
    retries: int = 0
    cached: bool = False

class CircuitBreaker:
    """Circuit breaker pattern để ngăn cascade failures"""
    
    def __init__(
        self,
        failure_threshold: int = 5,
        recovery_timeout: float = 60.0,
        half_open_max_calls: int = 3
    ):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_calls = half_open_max_calls
        
        self._failures = 0
        self._last_failure_time: Optional[float] = None
        self._state = "closed"  # closed, open, half-open
        self._half_open_calls = 0
    
    async def call(self, func, *args, **kwargs):
        """Execute function với circuit breaker protection"""
        if self._state == "open":
            if time.time() - self._last_failure_time > self.recovery_timeout:
                self._state = "half-open"
                self._half_open_calls = 0
            else:
                raise Exception("Circuit breaker