Tự host eval cho AI agent, theo dõi pass rate mỗi commit
Tự xây eval loop bằng trace thực tế, golden case, check deterministic và LLM judge; lưu pass rate theo commit để biết thay đổi nào làm agent lỗi.
Eval self-host cho AI agent là gì
Eval self-host cho AI agent gồm 4 thành phần bạn lưu trong repository của mình: một file chứa các case đã lưu, một script chạy agent trên các case đó, một nhóm kiểm tra để chấm từng câu trả lời và một bảng kết quả có thể truy vấn. Không thành phần nào trong danh sách này cần vendor. Toàn bộ vòng lặp chỉ cần vài trăm dòng Python và một file SQLite.
Agent hoạt động trong bản demo vì bạn tự chọn 5 input. Đến tuần thứ 2, agent bắt đầu lỗi vì một dòng trong prompt thay đổi, model thay đổi hoặc mô tả tool thay đổi, trong khi không có phép đo nào bao quát các thay đổi đó. Vòng lặp eval biến nhận định “bây giờ có vẻ tệ hơn” thành “tỷ lệ pass giảm từ 58 trên 60 xuống 51 trên 60 ở commit 4f1c9ab”.
Vòng lặp này có 4 bước. Guide này dành một section cho mỗi bước: thu thập trace thực tế, chọn những trace đáng chú ý thành case, chấm điểm mọi case sau mỗi thay đổi và lưu tỷ lệ pass cạnh commit tạo ra kết quả đó. Vòng lặp này hoạt động với mọi tác vụ bạn giao cho agent. Các framework agent self-host đáng dùng chủ yếu khác nhau ở lượng trace chúng cung cấp sẵn cho bạn.
Tại sao agent hỏng ở tuần thứ hai
Một agent gồm prompt, model, tập định nghĩa tool và mọi context được truy xuất tại thời điểm chạy. Cả 4 thành phần này đều có thể thay đổi mà không cần sửa code ứng dụng, nên một lần code review thông thường sẽ không phát hiện được vấn đề.
Nguyên nhân phổ biến nhất là chỉnh sửa prompt. Bạn thêm một câu để ngăn câu trả lời thô lỗ. Câu đó thay đổi hành vi với những input chưa được test lại, và trace thể hiện rất rõ: trace của tuần trước cho cùng câu hỏi có một create_refund lần gọi tool, còn tuần này thì không có, và câu trả lời thay bằng một lời xin lỗi lịch sự. Không có lỗi nào được phát sinh nên không có alert nào được kích hoạt.
Nguyên nhân thứ hai là model. Hãy ghi lại chính xác chuỗi model đã gửi trong mỗi lần chạy, claude-haiku-4-5-20251001 thay vì dùng một tên viết tắt mà bạn chỉ nhớ trong đầu, vì chỉ có thể chẩn đoán việc pass rate giảm đúng ngày bạn đổi model khi model được ghi trong từng dòng dữ liệu.
Nguyên nhân thứ ba là tool. Việc viết lại mô tả tool sẽ thay đổi thời điểm model quyết định gọi tool đó. Nếu các tool của bạn được cung cấp qua MCP server chạy trên VPS, schema nằm trong một process khác, nên nó có thể thay đổi mà repository của bạn hoàn toàn không có diff. Nguyên nhân thứ tư là retrieval: cùng một câu hỏi truy vấn một index vừa được rebuild qua đêm, và câu trả lời sử dụng document mới.
Xây dựng golden set từ các trace đã thu thập
Không tự nghĩ ra các eval case. Hãy lấy chúng từ traffic thực tế. Nếu bạn đã chạy Langfuse self-hosted để tracing agent, mọi request đều được lưu cùng input, các tool call và output. Đây chính là dữ liệu thô cần có để tạo một case.
Export một khoảng thời gian gồm các root observation qua public API. API này dùng basic authentication, trong đó public key là username và secret key là password.
export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
"$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
| jq '.data[0]'Đọc một record trước khi viết bất kỳ logic parsing nào. Các row được trả về trong data, nhưng tên field chứa câu hỏi và câu trả lời phụ thuộc vào cách agent instrument span. Vì vậy, hãy ánh xạ theo dữ liệu thực tế thay vì theo điều bạn dự kiến. Sau đó, tự viết các case, mỗi dòng một JSON object, trong evals/cases.jsonl:
{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}Năm quy tắc sau giúp set này vẫn có giá trị khi chạy:
- Bắt đầu với 40 đến 80 case là đủ. Nếu dưới 20 case, một case flaky có thể làm pass rate thay đổi 5 điểm, khiến người dùng bỏ qua các con số dao động không rõ lý do.
- Mỗi production bug được sửa phải trở thành một case ngay trong ngày sửa lỗi. Thói quen này giúp set phát triển đúng hướng.
- Mỗi case chỉ kiểm tra một hành vi. Một case vừa kiểm tra số tiền hoàn vừa kiểm tra giọng điệu sẽ không cho bạn biết gì khi nó fail.
idkhông bao giờ thay đổi, vì id là cách so sánh run hôm nay với run của tháng trước.- Redact trước khi commit. File này sẽ được đưa vào git, nên hãy xóa tên khách hàng và mọi order number mà bạn không sở hữu.
Chấm điểm bằng các kiểm tra xác định trước, vì chúng không tốn chi phí
Mọi thứ có đáp án đúng đều được kiểm tra bằng assertion đơn giản. Không gọi model, không tốn chi phí, không có sự mơ hồ. Các kiểm tra xác định bắt được những lỗi hồi quy về cấu trúc. Đây là các lỗi làm hỏng những hệ thống xung quanh agent: JSON không parse được, tool chưa từng được gọi, cụm từ bị cấm xuất hiện trở lại hoặc câu trả lời không trích dẫn nguồn nào.
Chỉ một function cần biết về agent của bạn. Mọi thành phần khác trong harness đều dùng chung.
import json, os, urllib.request
def run_agent(case):
req = urllib.request.Request(
os.environ["AGENT_URL"],
data=json.dumps({"input": case["input"]}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as resp:
return json.load(resp)
def deterministic(case, result):
text = result.get("output", "")
called = [c["name"] for c in result.get("tool_calls", [])]
failures = []
for tool in case.get("must_call", []):
if tool not in called:
failures.append(f"tool not called: {tool}")
for phrase in case.get("must_not_include", []):
if phrase.lower() in text.lower():
failures.append(f"forbidden phrase: {phrase}")
if len(called) > case.get("max_tool_calls", 12):
failures.append(f"too many tool calls: {len(called)}")
return failuresGiữ giới hạn tool trong danh sách đó. Một agent hôm nay giải quyết một case bằng 3 lần gọi nhưng ngày mai cần 11 lần gọi đã bị hồi quy, ngay cả khi câu trả lời cuối cùng vẫn đúng, vì bạn phải trả phí cho mọi lần gọi mà nó thực hiện.
LLM làm giám khảo và 4 cách nó cho kết quả sai
Những gì vượt qua các assertion cần một grader có khả năng đọc hiểu. LLM judge là một lần gọi model thứ hai: nó nhận câu hỏi, câu trả lời của agent và một tiêu chí, rồi trả về verdict. Đây là cách thực tế duy nhất để đánh giá “câu trả lời có đáp ứng đúng điều người dùng hỏi hay không”.
4 quy tắc giúp judge có thể sử dụng được:
- Verdict nhị phân, không dùng điểm từ 1 đến 10. Thang điểm thường trả về 7 và 8 cho gần như mọi thứ, nên con số không thay đổi và bạn không học được gì từ đó.
- Mỗi lần gọi chỉ dùng một tiêu chí. Hãy hỏi về số tiền hoàn lại hoặc giọng điệu, không hỏi cả hai cùng lúc.
- Cung cấp câu trả lời kỳ vọng cho judge bất cứ khi nào case có câu trả lời đó. Chấm theo reference dễ hơn nhiều so với chấm một cách trừu tượng.
- Bắt buộc output có đúng format và parse một cách nghiêm ngặt.
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def judge_prompt(case, output):
return (
"You grade one answer against one criterion.\n"
"Reply with JSON only, in this exact shape:\n"
'{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
f"Criterion: {case['rubric']}\n"
f"Question: {case['input']}\n"
f"Answer: {output}\n"
"Length is not a criterion. Judge only the criterion above."
)
def judge(case, output, model):
msg = client.messages.create(
model=model,
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt(case, output)}],
)
return json.loads(msg.content[0].text)Sau đây là các failure mode. Mỗi failure mode đều có một test bạn có thể chạy ngay trong hôm nay. Việc chạy các test này rất quan trọng, vì một judge không được kiểm tra sẽ tạo ra những con số trông có vẻ chính xác nhưng không có ý nghĩa.
Thiên lệch theo độ dài. Câu trả lời dài hơn thường pass nhiều hơn. Cách test: lấy 10 câu trả lời bị judge đánh fail, thêm vào mỗi câu 2 đoạn văn filler đầy tự tin nhưng không bổ sung thông tin mới, rồi chấm lại. Nếu verdict nào chuyển sang pass, đó là thiên lệch theo độ dài và rubric là thứ cần sửa.
Thiên lệch ưu ái chính mình. Judge thường chấm output từ cùng model family dễ hơn output từ model family khác. Cách test: dùng judge thuộc 2 family khác nhau để chấm cùng 30 câu trả lời, rồi so sánh verdict theo từng case. Với các case có kết quả khác nhau, hãy tự đọc case đó.
Thiên lệch theo vị trí. Nếu dùng judge để so sánh 2 câu trả lời A và B, hãy đổi thứ tự rồi chạy lại. Nếu verdict thay đổi sau khi đổi thứ tự, pairwise comparison chưa an toàn cho rubric đó.
Rubric bị lệch. Tiêu chí mơ hồ sẽ tạo ra các judge dễ đồng ý. “Câu trả lời có hữu ích không” sẽ pass gần như mọi thứ. “Câu trả lời có nêu số tiền hoàn lại bằng đô la không” chỉ pass những gì bạn thực sự muốn kiểm tra. Viết lại từng tiêu chí cho đến khi tiêu chí nêu rõ fact cần kiểm tra.
Một biện pháp bảo vệ có thể bao quát cả 4 vấn đề. Giữ lại 30 case do bạn tự gắn nhãn, rồi mỗi lần thay đổi judge model hoặc judge prompt, hãy chấm judge dựa trên các nhãn đó. Nếu judge không đồng ý với bạn ở hơn 1 case trên 10, hãy sửa rubric trước khi tin vào bất kỳ pass rate nào mà nó tạo ra. Judge là code, vì vậy cần version và review giống như code.
Ưu tiên grader rẻ, chỉ chuyển lên frontier model khi cần
Dùng model đắt nhất để đánh giá mọi case trong mỗi commit là cách khiến chi phí eval tăng nhanh hơn agent đang được kiểm thử. Hãy sắp xếp các grader theo giá và dừng ngay khi đã đủ rõ.
The data behind this chart
[
{
"label": "Haiku 4.5, Batch API",
"usd_per_1000_judge_calls": "0.90"
},
{
"label": "Haiku 4.5",
"usd_per_1000_judge_calls": "1.80"
},
{
"label": "Sonnet 5",
"usd_per_1000_judge_calls": "3.60"
},
{
"label": "Opus 5",
"usd_per_1000_judge_calls": "9.00"
}
]Các con số này giả định mỗi lần gọi judge có khoảng 1,200 input token và 120 output token. Đây là kích thước thực tế cho một câu hỏi, một câu trả lời và một tiêu chí. Đánh giá 1,000 case tốn 1.80 US dollar trên Claude Haiku 4.5 và 9.00 trên Claude Opus 5. Chênh lệch này có vẻ nhỏ cho đến khi nhân lên. Một bộ gồm 60 case, được đánh giá trong mỗi commit với 40 commit mỗi tuần, tạo ra 2,400 judge call mỗi tuần trước khi có ai chạy job hằng đêm.
Có 2 mức giảm giá áp dụng trực tiếp cho công việc eval và có thể cộng dồn. Eval run không tương tác, nên Batch API giảm một nửa cả giá input lẫn output để đổi lấy việc trả kết quả bất đồng bộ. Đây là cột đầu tiên trong biểu đồ. Rubric và instruction giống hệt nhau ở mọi lần gọi, đến từng byte, nên prompt caching phù hợp: một cache read có giá bằng một phần mười giá input cơ bản, còn một cache write trong 5 phút có giá bằng 1.25 lần giá input cơ bản. Vì vậy, cache hoàn vốn sau một lần cache hit. Đây là list price của Anthropic tính đến August 2026. Sonnet 5 được áp dụng giá giới thiệu đến 31 August 2026, nên cột thứ ba sẽ tăng sau ngày đó.
Thứ tự xử lý:
- Chạy deterministic check trên mọi case. Hoàn toàn không tốn API cost.
- Dùng small model làm judge cho các case vượt qua những check đó.
- Chỉ dùng frontier judge khi small judge cho kết quả fail hoặc pass với độ tin cậy thấp.
- Mỗi tuần review thủ công một sample nhỏ.
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"
def grade(case, result):
hard = deterministic(case, result)
if hard:
return False, "deterministic", "; ".join(hard)
first = judge(case, result["output"], CHEAP)
if first["verdict"] == "pass" and first["confidence"] == "high":
return True, CHEAP, first["reason"]
second = judge(case, result["output"], STRICT)
return second["verdict"] == "pass", STRICT, second["reason"]Cách này đánh đổi một phần độ chính xác của grading để giảm cost, nên hãy đo lường mức đánh đổi thay vì mặc định chấp nhận. Mỗi tháng một lần, dùng strict judge để grade toàn bộ set rồi so sánh 2 cột kết quả. Nếu 2 cột khác nhau ở nhiều hơn vài case, rubric của bạn quá lỏng đối với small model. Khi đó, thứ cần sửa là rubric. Kiểm soát khoản chi của chính agent là một công việc riêng, được trình bày trong kiểm soát chi phí cho AI agent trên VPS.
Theo dõi tỷ lệ đạt theo thời gian trong hệ thống bạn sở hữu
Một tỷ lệ đạt không thể gắn với một commit chỉ là cảm nhận. Lưu một dòng cho mỗi case trong mỗi lần chạy, với commit và model trong cùng dòng đó.
CREATE TABLE IF NOT EXISTS results (
run_id TEXT NOT NULL,
ran_at TEXT NOT NULL,
git_sha TEXT NOT NULL,
agent_model TEXT NOT NULL,
case_id TEXT NOT NULL,
passed INTEGER NOT NULL,
graded_by TEXT NOT NULL,
reason TEXT
);SELECT run_id, git_sha, agent_model,
count(*) AS cases,
round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;Nạp schema bằng sqlite3 evals/results.db < evals/schema.sql, sau đó đọc xu hướng bằng sqlite3 -box evals/results.db < evals/passrate.sql. Một năm chạy hằng ngày trên 60 case tạo ra khoảng 22,000 dòng, vì vậy kho dữ liệu không bao giờ trở thành một dự án riêng. Chạy SQLite trong production trên VPS trình bày các thiết lập bắt đầu cần thiết nếu file này được chia sẻ giữa nhiều máy.
Runner in cùng thông tin cho một người:
run 2026-08-05T09:14:22Z sha 4f1c9ab model claude-sonnet-5 58/60 pass (96.7%)
FAIL refund-double-charge deterministic: tool not called: create_refund
FAIL pto-policy-question judge(opus): reply gives no dollar amountChạy test suite trên những thay đổi có thể làm agent hỏng. Cụ thể là các thay đổi về prompt, model và tool, thay vì mọi commit ở bất kỳ đâu trong repository. Một hook pre-push xử lý tập kiểm tra nhanh:
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-pushCác lần chạy đầy đủ chậm hơn và nên chạy theo lịch. Một systemd service và timer chạy trên VPS hằng đêm sẽ chạy toàn bộ tập kiểm tra với prompt đã deploy. Cách này phát hiện các thay đổi đến từ bên ngoài repository, chẳng hạn tool được host đã thay đổi hành vi.
Con người đánh giá theo mẫu, không kiểm tra toàn bộ
Judge được hiệu chỉnh dựa trên nhãn do con người gán, nên phải có người tạo các nhãn đó. Mỗi tuần, hãy đọc một mẫu gồm tất cả case mà judge đánh trượt, cộng với 10 case đạt được chọn ngẫu nhiên. Các case đạt được chọn ngẫu nhiên mới là phần quan trọng, vì judge có thể âm thầm bắt đầu cho qua các câu trả lời sai nhưng vẫn trông hoàn hảo trên mọi dashboard được xây dựng từ chính các verdict của nó.
15 case, mỗi case 3 phút, tổng cộng là 45 phút mỗi tuần. Việc này giúp cập nhật rubric ở những điểm bạn và judge đánh giá khác nhau, đồng thời bổ sung các case cho những dạng lỗi trước đó chưa ai nghĩ đến. Ghi verdict của con người vào cùng bảng, đặt graded_by thành human, để mức độ nhất trí giữa judge và con người trở thành một query thay vì chỉ nằm trong trí nhớ.
Điều gì có thể làm hỏng chính eval harness
anthropic.RateLimitError trong lần chạy đầy đủ đầu tiên. Chạy đồng thời 60 case vượt quá giới hạn request hoặc token của tier bạn đang dùng. Giới hạn concurrency ở 4 worker và chuyển lần chạy hằng đêm sang Batch API.
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) từ judge. Model trả lời bằng văn xuôi hoặc đặt JSON trong code fence. Hãy retry 1 lần, sau đó ghi nhận case là lỗi. Không bao giờ tính lỗi parse là pass, vì một suite biến lỗi thành pass sẽ tiến gần 100% trong khi agent ngày càng kém.
Case không ổn định. Cùng một input pass ở lần chạy này nhưng fail ở lần chạy sau vì agent sampling output. Chạy case không ổn định 3 lần và ghi nhận tỷ lệ thay vì xóa case. Case pass 2/3 lần là một lỗi robustness thực sự, và khách hàng sẽ phát hiện ra.
Golden set bị xuống cấp. Có người chỉnh expected answer để suite chuyển sang màu xanh. Hãy review diff tới evals/cases.jsonl cẩn thận như diff của agent, vì file đó là định nghĩa bằng văn bản của bạn về kết quả đúng.
Suite không bao giờ fail. Tỷ lệ pass giữ ở mức 100% trong 1 tháng có nghĩa là set đã ngừng phản ánh product. Lấy 10 trace gần đây, tìm những trace agent xử lý kém và thêm chúng vào. Sau đó cố ý làm hỏng một phần rồi xác nhận lần chạy chuyển sang màu đỏ. Đây là kiểm tra mutation testing áp dụng cho một test suite và là cách duy nhất để biết set của bạn vẫn đủ khả năng phát hiện lỗi.
FAQ
Một bộ eval cho AI agent cần bao nhiêu case?
Hãy bắt đầu với 40 đến 80 case, rồi mở rộng bộ này từ các lỗi thực tế. Nếu có dưới khoảng 20 case, chỉ một kết quả không ổn định cũng làm tỷ lệ đạt thay đổi 5 điểm phần trăm, nên con số này không còn nhiều ý nghĩa. Khi vượt quá vài trăm case, mỗi lần chạy đều tốn tiền và thời gian thực, trong khi mỗi case bổ sung chỉ tăng độ bao phủ rất ít. Chỉ số quan trọng không phải là số lượng case. Đó là tỷ lệ các loại lỗi production đã biết xuất hiện ít nhất một lần trong bộ eval.
Tôi có thể tin LLM judge chấm điểm agent của mình không?
Chỉ sau khi đo được độ chính xác của nó bằng chính các nhãn của bạn. Hãy giữ lại 30 case do bạn chấm thủ công, rồi đối chiếu judge với các case đó mỗi khi thay đổi model hoặc prompt của judge. Judge thường có thiên lệch theo độ dài, trong đó câu trả lời được kéo dài sẽ dễ đạt hơn, và thiên lệch ưu tiên chính mình, trong đó output từ cùng một họ model được chấm dễ hơn. Cả hai đều có thể kiểm tra: kéo dài một câu trả lời đã trượt rồi chấm lại, hoặc dùng judge thuộc họ model khác để chấm cùng các câu trả lời. Nếu judge không đồng nhất với nhãn của bạn ở hơn 1 trên 10 case, rubric quá mơ hồ để sử dụng.
Nên dùng model nào để chấm eval?
Hãy chấm bằng model rẻ trước, rồi mới nâng cấp khi cần. Deterministic assertion không tốn chi phí, nên chạy đầu tiên trên mọi case. Model nhỏ xử lý các case đạt rõ ràng. Chỉ các case trượt và verdict có độ tin cậy thấp mới chuyển cho frontier model. Theo bảng giá niêm yết vào tháng 8 năm 2026, chấm 1,000 case tốn khoảng 1.80 đô la Mỹ với Claude Haiku 4.5 và khoảng 9.00 với Claude Opus 5. Vì các lần chạy eval là asynchronous, Batch API giảm một nửa cả hai mức chi phí.
Evals có thay thế được monitoring production không?
Không, vì chúng trả lời các câu hỏi khác nhau. Một eval suite cho biết thay đổi bạn sắp release làm một tập case cố định tốt hơn hay kém hơn. Tracing và monitoring cho biết người dùng thực tế đang gặp gì ngay lúc này, bao gồm cả những input chưa có case tương ứng. Chúng bổ trợ cho nhau: trace cung cấp case mới, còn eval suite xác định bản sửa của bạn có thực sự hiệu quả hay không.