Tool Evaluation — Đánh giá hiệu quả tools trong agent systems
Điểm nổi bật
Nhấn để đến mục tương ứng
- 1 Công cụ AI sẽ thay đổi cách bạn làm việc: Agent failures thường không rõ ràng như code errors. Điểm mấu chốt là biết cách đặt prompt đúng để nhận kết quả có thể sử dụng ngay.
- 2 Một điều ít người đề cập: from dataclasses import dataclass, field from typing import Optional, Callable import anthropic import json import time. Hiểu rõ bối cảnh áp dụng sẽ quyết định 80% thành công khi triển khai.
- 3 Không thể bỏ qua: class ToolEvaluator: def initself, tools: list, systemprompt: str = "": self.tools = tools self.systemprompt =. Đây là kiến thức nền tảng mà mọi người làm việc với AI đều cần hiểu rõ.
- 4 Công cụ AI sẽ thay đổi cách bạn làm việc: weathertools = { "name": "getweather", "description": "Get current weather for a city", "inputschema": { "type":. Điểm mấu chốt là biết cách đặt prompt đúng để nhận kết quả có thể sử dụng ngay.
- 5 Một điều ít người đề cập: import subprocess import sys def runevalincisuite: ToolTestSuite, minpassrate: float = 0.9 -> bool: """Gate CI/CD. Hiểu rõ bối cảnh áp dụng sẽ quyết định 80% thành công khi triển khai.
Xây dựng agent với tools không khó. Khó là biết agent của bạn đang hoạt động tốt đến đâu. Khi nào Claude chọn đúng tool? Khi nào nó hallucinate tool parameters? Khi nào nó gọi tool không cần thiết? Không có systematic evaluation, bạn chỉ đang đoán mò.
Bài này trình bày framework đầy đủ để đánh giá tool effectiveness — từ định nghĩa metrics đến automated test suites và continuous monitoring.
Tại sao Tool Evaluation quan trọng?
Agent failures thường không rõ ràng như code errors. Agent có thể:
- Gọi đúng tool nhưng với wrong parameters — kết quả sai nhưng không crash
- Không gọi tool khi nên gọi — trả lời từ knowledge cũ, không accurate
- Gọi tool không cần thiết — tốn tokens và latency
- Gọi tool sai — confused giữa search vs calculate vs lookup
Chỉ với systematic evaluation bạn mới phát hiện và fix những vấn đề này.
Framework: 5 Dimensions of Tool Evaluation
from dataclasses import dataclass, field
from typing import Optional, Callable
import anthropic
import json
import time
client = anthropic.Anthropic()
@dataclass
class ToolCallExpectation:
"""Kỳ vọng về một tool call cụ thể"""
tool_name: str # Tool phải được gọi
required_params: dict = field(default_factory=dict) # Params bắt buộc và giá trị mong đợi
forbidden_params: list = field(default_factory=list) # Params không được có
param_validators: dict = field(default_factory=dict) # Custom validators (key: validator_fn)
@dataclass
class TestCase:
"""Một test case cho agent evaluation"""
id: str
user_message: str
expected_tool_calls: list = field(default_factory=list) # List[ToolCallExpectation]
should_not_call_tools: bool = False # Đôi khi agent KHÔNG nên gọi tool
expected_output_contains: list = field(default_factory=list) # Keywords trong final response
max_tool_calls: int = 5 # Không gọi quá nhiều tools
max_latency_ms: int = 10000 # Latency budget
@dataclass
class EvalResult:
"""Kết quả evaluation của một test case"""
test_id: str
passed: bool
score: float # 0.0 - 1.0
tool_precision: float # Correct tools called / Total tools called
tool_recall: float # Correct tools called / Expected tools
param_accuracy: float # Correct params / Total params
latency_ms: int
tool_calls_made: list
issues: list
response: str
Core Evaluation Engine
class ToolEvaluator:
def __init__(self, tools: list, system_prompt: str = ""):
self.tools = tools
self.system_prompt = system_prompt
self.client = anthropic.Anthropic()
def evaluate_test_case(self, test: TestCase) -> EvalResult:
"""Chạy một test case và return detailed results"""
start_time = time.time()
tool_calls_made = []
messages = [{"role": "user", "content": test.user_message}]
final_response = ""
issues = []
# Run agent loop
iteration = 0
while iteration < test.max_tool_calls + 1:
response = self.client.messages.create(
model="claude-haiku-4-5",
max_tokens=2000,
system=self.system_prompt,
tools=self.tools,
messages=messages
)
if response.stop_reason == "end_turn":
final_response = next((b.text for b in response.content if b.type == "text"), "")
break
elif response.stop_reason == "tool_use":
# Record tool calls
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_call = {
"name": block.name,
"input": block.input,
"id": block.id
}
tool_calls_made.append(tool_call)
# Mock tool execution
result = self._mock_tool_execution(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
iteration += 1
else:
break
latency_ms = int((time.time() - start_time) * 1000)
# Check latency
if latency_ms > test.max_latency_ms:
issues.append(f"Latency {latency_ms}ms exceeded budget {test.max_latency_ms}ms")
# Evaluate results
scores = self._score_results(
test, tool_calls_made, final_response, latency_ms, issues
)
return EvalResult(
test_id=test.id,
passed=scores["passed"],
score=scores["overall_score"],
tool_precision=scores["precision"],
tool_recall=scores["recall"],
param_accuracy=scores["param_accuracy"],
latency_ms=latency_ms,
tool_calls_made=tool_calls_made,
issues=issues,
response=final_response
)
def _score_results(self, test: TestCase, tool_calls: list,
response: str, latency_ms: int, issues: list) -> dict:
"""Tính điểm cho test case"""
# Check: should NOT call tools
if test.should_not_call_tools and tool_calls:
issues.append(f"Called tools when shouldn't: {[tc['name'] for tc in tool_calls]}")
return {"passed": False, "overall_score": 0.0, "precision": 0.0, "recall": 0.0, "param_accuracy": 0.0}
if not test.expected_tool_calls:
# No expectations about tools — only check response content
content_ok = all(kw.lower() in response.lower() for kw in test.expected_output_contains)
return {
"passed": content_ok,
"overall_score": 1.0 if content_ok else 0.5,
"precision": 1.0, "recall": 1.0, "param_accuracy": 1.0
}
# Tool precision: out of all tools called, how many were expected?
called_names = [tc["name"] for tc in tool_calls]
expected_names = [e.tool_name for e in test.expected_tool_calls]
correct_calls = [name for name in called_names if name in expected_names]
precision = len(correct_calls) / len(called_names) if called_calls else 1.0
recall = len(correct_calls) / len(expected_names) if expected_names else 1.0
# Unexpected tools
unexpected = [name for name in called_names if name not in expected_names]
if unexpected:
issues.append(f"Unexpected tool calls: {unexpected}")
# Missing tools
missing = [name for name in expected_names if name not in called_calls]
if missing:
issues.append(f"Missing expected tool calls: {missing}")
# Parameter accuracy
param_scores = []
for expected in test.expected_tool_calls:
actual_call = next((tc for tc in tool_calls if tc["name"] == expected.tool_name), None)
if not actual_call:
param_scores.append(0.0)
continue
actual_input = actual_call.get("input", {})
param_score = self._score_params(expected, actual_input, issues)
param_scores.append(param_score)
param_accuracy = sum(param_scores) / len(param_scores) if param_scores else 1.0
# Output content check
content_score = 1.0
if test.expected_output_contains:
matched = sum(1 for kw in test.expected_output_contains if kw.lower() in response.lower())
content_score = matched / len(test.expected_output_contains)
if content_score < 1.0:
missing_kw = [kw for kw in test.expected_output_contains if kw.lower() not in response.lower()]
issues.append(f"Response missing keywords: {missing_kw}")
overall_score = (precision * 0.3 + recall * 0.3 + param_accuracy * 0.3 + content_score * 0.1)
passed = overall_score >= 0.8 and not any("Missing expected" in i for i in issues)
return {
"passed": passed,
"overall_score": round(overall_score, 3),
"precision": round(precision, 3),
"recall": round(recall, 3),
"param_accuracy": round(param_accuracy, 3)
}
def _score_params(self, expected: ToolCallExpectation, actual: dict, issues: list) -> float:
"""Score parameter correctness"""
scores = []
for param, expected_val in expected.required_params.items():
if param not in actual:
issues.append(f"Missing param '{param}' in {expected.tool_name}")
scores.append(0.0)
elif expected_val is not None and actual[param] != expected_val:
issues.append(f"Wrong '{param}': expected '{expected_val}', got '{actual[param]}'")
scores.append(0.5) # Partial credit — param exists but wrong value
else:
scores.append(1.0)
# Check forbidden params
for param in expected.forbidden_params:
if param in actual:
issues.append(f"Forbidden param '{param}' used in {expected.tool_name}")
scores.append(0.0)
# Custom validators
for param, validator in expected.param_validators.items():
if param in actual:
if not validator(actual[param]):
issues.append(f"Param '{param}' failed validation in {expected.tool_name}")
scores.append(0.0)
return sum(scores) / len(scores) if scores else 1.0
def _mock_tool_execution(self, tool_name: str, tool_input: dict) -> dict:
"""Mock tool results cho evaluation purposes"""
return {"status": "success", "result": f"Mock result for {tool_name}", "input_received": tool_input}
Test Suite Builder
class ToolTestSuite:
def __init__(self, name: str, evaluator: ToolEvaluator):
self.name = name
self.evaluator = evaluator
self.test_cases = []
def add_test(self, test: TestCase):
self.test_cases.append(test)
def run(self) -> dict:
"""Chạy toàn bộ test suite"""
results = []
print(f"
Running: {self.name} ({len(self.test_cases)} tests)")
print("=" * 60)
for test in self.test_cases:
result = self.evaluator.evaluate_test_case(test)
results.append(result)
status = "PASS" if result.passed else "FAIL"
print(f" [{status}] {test.id} — score: {result.score:.2f} | latency: {result.latency_ms}ms")
if result.issues:
for issue in result.issues[:2]:
print(f" Issue: {issue}")
return self._aggregate_results(results)
def _aggregate_results(self, results: list) -> dict:
passed = sum(1 for r in results if r.passed)
total = len(results)
report = {
"suite": self.name,
"total": total,
"passed": passed,
"failed": total - passed,
"pass_rate": round(passed / total * 100, 1) if total > 0 else 0,
"avg_score": round(sum(r.score for r in results) / total, 3) if total > 0 else 0,
"avg_precision": round(sum(r.tool_precision for r in results) / total, 3) if total > 0 else 0,
"avg_recall": round(sum(r.tool_recall for r in results) / total, 3) if total > 0 else 0,
"avg_param_accuracy": round(sum(r.param_accuracy for r in results) / total, 3) if total > 0 else 0,
"avg_latency_ms": round(sum(r.latency_ms for r in results) / total) if total > 0 else 0,
"failures": [r for r in results if not r.passed]
}
print(f"
Results: {passed}/{total} passed ({report['pass_rate']}%)")
print(f" Avg precision: {report['avg_precision']:.3f}")
print(f" Avg recall: {report['avg_recall']:.3f}")
print(f" Avg param accuracy: {report['avg_param_accuracy']:.3f}")
print(f" Avg latency: {report['avg_latency_ms']}ms")
return report
Ví dụ: Test Suite cho Weather Agent
weather_tools = [
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"units": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"}
},
"required": ["city"]
}
},
{
"name": "get_forecast",
"description": "Get weather forecast for next N days",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"days": {"type": "integer", "minimum": 1, "maximum": 7}
},
"required": ["city", "days"]
}
}
]
evaluator = ToolEvaluator(tools=weather_tools, system_prompt="You are a weather assistant.")
suite = ToolTestSuite("Weather Agent Tests", evaluator)
# Test 1: Simple current weather
suite.add_test(TestCase(
id="T001_current_weather",
user_message="What's the weather in Hanoi right now?",
expected_tool_calls=[
ToolCallExpectation(
tool_name="get_weather",
required_params={"city": "Hanoi"},
param_validators={"units": lambda v: v in ["celsius", "fahrenheit"]}
)
],
expected_output_contains=["Hanoi", "weather"]
))
# Test 2: Forecast request
suite.add_test(TestCase(
id="T002_5day_forecast",
user_message="Give me the 5-day forecast for Ho Chi Minh City",
expected_tool_calls=[
ToolCallExpectation(
tool_name="get_forecast",
required_params={"city": "Ho Chi Minh City", "days": 5}
)
]
))
# Test 3: Should NOT call tools
suite.add_test(TestCase(
id="T003_no_tool_needed",
user_message="What's the difference between weather and climate?",
should_not_call_tools=True,
expected_output_contains=["weather", "climate"]
))
# Run it
report = suite.run()
print(f"
Final pass rate: {report['pass_rate']}%")
Continuous Evaluation trong CI/CD
import subprocess
import sys
def run_eval_in_ci(suite: ToolTestSuite, min_pass_rate: float = 0.9) -> bool:
"""Gate CI/CD pipeline dựa trên eval results"""
report = suite.run()
pass_rate = report["pass_rate"] / 100
if pass_rate < min_pass_rate:
print(f"
CI GATE FAILED: {pass_rate*100:.1f}% < {min_pass_rate*100:.1f}% required")
print("Failing tests:")
for failure in report["failures"]:
print(f" - {failure.test_id}: {failure.issues}")
return False
print(f"
CI GATE PASSED: {pass_rate*100:.1f}% >= {min_pass_rate*100:.1f}%")
return True
# In CI pipeline
success = run_eval_in_ci(suite, min_pass_rate=0.85)
if not success:
sys.exit(1) # Fail the CI build
Phân tích Failure Patterns
def analyze_failures(results: list[EvalResult]) -> dict:
"""Phân tích patterns trong test failures để tìm root causes"""
failures = [r for r in results if not r.passed]
if not failures:
return {"message": "No failures to analyze"}
# Group by failure type
param_errors = []
wrong_tool = []
missing_tool = []
over_calling = []
for f in failures:
for issue in f.issues:
if "Wrong" in issue or "Missing param" in issue:
param_errors.append(f.test_id)
elif "Unexpected tool" in issue:
wrong_tool.append(f.test_id)
elif "Missing expected" in issue:
missing_tool.append(f.test_id)
print("
Failure Analysis:")
print(f" Parameter errors: {len(param_errors)} cases ({param_errors[:3]})")
print(f" Wrong tool called: {len(wrong_tool)} cases")
print(f" Tool not called: {len(missing_tool)} cases")
recommendations = []
if param_errors:
recommendations.append("Improve tool input_schema descriptions — add examples for each param")
if wrong_tool:
recommendations.append("Clarify tool descriptions — disambiguate similar tools more clearly")
if missing_tool:
recommendations.append("Add more trigger phrases in tool descriptions for missed tools")
return {"recommendations": recommendations}
analyze_failures([])
Tổng kết
Tool Evaluation không phải nice-to-have — đây là requirement cho production agent systems. Framework này cung cấp:
- 5 metrics: precision, recall, param accuracy, latency, content quality
- Automated test suites chạy được trong CI/CD
- Failure analysis để tìm root causes và improve tool descriptions
- Pass/fail gates để prevent regressions khi update tools
Đọc thêm: Extended Thinking + Tool Use — cải thiện tool selection accuracy với reasoning trước khi action.
Bài viết liên quan
Bai viet co huu ich khong?
Bản quyền thuộc về tác giả. Vui lòng dẫn nguồn khi chia sẻ.






