Paano Gumawa ng Self-Hosted Evals para sa AI Agents
Gumawa ng eval loop gamit ang real traces, deterministic checks, LLM judge, at pass rate kada commit. Kasama ang konkretong halimbawa: 58/60 naging 51/60.
Ano ang self-hosted evals para sa AI agents
Ang self-hosted evals para sa AI agents ay apat na bagay na itinatago mo sa sarili mong repository: isang file ng mga naka-save na case, isang script na nagpapatakbo sa agent sa mga case na ito, isang set ng checks na nagso-score sa bawat sagot, at isang table ng mga resultang maaari mong i-query. Walang vendor na kailangan para sa alinman sa mga ito. Ang buong loop ay maaaring binuo gamit ang ilang daang linya ng Python at isang SQLite file.
Gumana ang agent sa demo dahil ikaw mismo ang pumili ng limang input. Pumalpak ito sa ikalawang linggo dahil may nagbago sa isang linya ng prompt, sa model, o sa description ng tool, at walang measurement na sumaklaw sa alinman sa mga ito. Ginagawang kongkretong resulta ng eval loop ang “parang mas mahina na ito ngayon”: “bumaba ang pass rate mula 58 sa 60 tungo sa 51 sa 60 sa commit 4f1c9ab.”
May apat na hakbang ang loop, at isang section ng guide na ito para sa bawat hakbang: mangolekta ng mga totoong trace, gawing case ang mga kawili-wiling trace, i-grade ang bawat case sa bawat pagbabago, at i-store ang pass rate kasama ng commit na gumawa nito. Gagana ang parehong loop anuman ang pinapatakbuhan ng agent, at ang mga self-hosted agent framework na sulit patakbuhin ay pangunahing nagkakaiba sa dami ng trace na awtomatiko nilang ibinibigay sa iyo.
Bakit pumapalya ang agent sa ikalawang linggo
Ang agent ay binubuo ng prompt, model, mga depinisyon ng tool, at anumang context na kinukuha sa oras ng pagtakbo. Maaaring magbago ang apat na ito kahit walang binabago sa application code, kaya walang mapapansing problema sa karaniwang code review.
Ang pinakakaraniwang sanhi ay pag-edit sa prompt. Nagdaragdag ka ng isang pangungusap para maiwasan ang bastos na reply. Binabago ng pangungusap na iyon ang behavior sa mga input na hindi muling sinuri, at malinaw itong makikita sa traces: ang trace noong nakaraang linggo para sa parehong tanong ay may create_refund tool call, samantalang wala nito ang trace ngayong linggo, at sa halip ay magalang na paghingi ng paumanhin ang reply. Walang nag-raise ng error, kaya walang alert na nag-trigger.
Ang ikalawang sanhi ay ang model. Itala ang eksaktong model string na ipinadala mo sa bawat run, claude-haiku-4-5-20251001 sa halip na shorthand na nasa isip mo lang, dahil matutukoy lamang ang dahilan ng pagbaba ng pass rate noong araw na lumipat ka ng model kapag kasama ang model sa row.
Ang ikatlong sanhi ay ang mga tool. Kapag binago ang pagkakasulat ng tool description, nagbabago kung kailan nagpapasya ang model na tawagin ito. Kung dumarating ang mga tool mo sa pamamagitan ng MCP servers na tumatakbo sa isang VPS, nasa ibang proseso ang schema, kaya maaari itong magbago nang walang anumang diff sa repository mo. Ang ikaapat na sanhi ay retrieval: tumatama ang parehong tanong sa index na muling binuo magdamag, kaya sinusundan ng sagot ang bagong dokumento.
Buuin ang golden set mula sa mga trace na nakokolekta mo na
Huwag mag-imbento ng mga eval case. Kunin ang mga ito mula sa traffic. Kung nagpapatakbo ka na ng self-hosted Langfuse tracing para sa iyong agent, naka-store ang bawat request kasama ang input, mga tool call, at output nito. Iyan mismo ang raw material na kailangan ng isang case.
Mag-export ng window ng root observations sa public API. Gumagamit ito ng basic authentication. Gamitin ang public key bilang username at ang secret key bilang 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]'Basahin muna ang isang record bago magsulat ng anumang parsing. Lumalabas ang mga row sa ilalim ng data, ngunit nakadepende sa paraan ng pag-instrument ng agent mo sa mga span ang mga field name na naglalaman ng tanong at sagot. I-map ang aktuwal mong nakikita, hindi ang inaasahan mong makikita. Pagkatapos, manu-manong isulat ang mga case bilang tig-iisang JSON object sa bawat linya sa 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."}Nakakatulong ang limang panuntunan para manatiling kapaki-pakinabang ang set:
- Sapat na ang 40 hanggang 80 case para magsimula. Kapag mas mababa sa 20, maaaring baguhin ng isang flaky case ang pass rate nang 5 puntos, kaya hindi pinapansin ang numerong biglang nagbabago nang walang malinaw na dahilan.
- Ang bawat production bug na inaayos mo ay dapat maging case sa araw ding iyon. Ang gawaing ito ang nagpapaunlad sa set sa tamang direksiyon.
- Isang behavior bawat case. Kung sabay na sinusuri ng isang case ang halaga ng refund at tono ng sagot, wala kang matututunan kapag nag-fail ito.
- Hindi nagbabago ang
id, dahil ginagamit ang id para ikumpara ang run ngayong araw sa run noong nakaraang buwan. - Mag-redact bago mag-commit. Mapupunta ang file na ito sa git, kaya alisin ang mga pangalan ng customer at anumang order number na hindi mo pagmamay-ari.
Unahin ang pag-grade gamit ang deterministic checks dahil libre ang mga ito
Ang anumang may tamang sagot ay dapat suriin gamit ang plain assertion. Walang model call, gastos, o kalabuan. Nahuhuli ng deterministic checks ang mga structural regression. Ito ang mga regression na sumisira sa mga system sa paligid ng iyong agent: hindi ma-parse ang JSON, hindi kailanman tinawag ang tool, bumalik ang ipinagbabawal na parirala, o walang binanggit na source ang sagot.
Isang function lang ang kailangang may alam tungkol sa iyong agent. Generic ang lahat ng iba pa sa harness.
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 failuresIsama sa listahang iyon ang tool budget. Nagkaroon ng regression ang isang agent na lumulutas ng case sa 3 calls ngayon pero 11 calls bukas, kahit tama ang final answer, dahil binabayaran mo ang bawat call na ginagawa nito.
LLM bilang tagahatol, at ang apat na paraan kung paano ito nagkakamali
Ang lahat ng nakapasa sa mga assertion ay nangangailangan ng grader na marunong bumasa. Ang LLM judge ay isang ikalawang model call: natatanggap nito ang tanong, sagot ng agent, at isang criterion, pagkatapos ay nagbabalik ng verdict. Ito ang tanging praktikal na paraan upang suriin kung sinasagot ng reply ang mismong hinihingi ng user.
Apat na tuntunin ang nagpapagamit sa isang judge:
- Binary verdict lamang, hindi score na 1 hanggang 10. Karaniwang nagbabalik ang scale ng 7 at 8 sa halos lahat ng kaso, kaya hindi gumagalaw ang numero at wala kang natututunan mula rito.
- Isang criterion bawat call. Magtanong tungkol sa halaga ng refund o tungkol sa tono, hindi tungkol sa pareho nang sabay.
- Ibigay sa judge ang expected answer kapag may ganoon ang kaso. Mas madaling mag-grade laban sa reference kaysa mag-grade nang walang tiyak na batayan.
- Pilitin ang output shape at mahigpit itong i-parse.
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)Ngayon, ang mga failure mode. May test na maaari mong patakbuhin ngayong hapon para sa bawat isa. Mahalaga ang pagpapatakbo ng mga ito dahil ang judge na hindi na-check ay gumagawa ng mga numerong mukhang eksakto pero walang kabuluhan.
Length bias. Mas madalas pumapasa ang mas mahahabang sagot. Subukan ito: kumuha ng sampung sagot na ibinagsak ng judge, dagdagan ang bawat isa ng dalawang talata ng kumpiyansang filler na walang bagong fact, at ipa-judge muli. Anumang verdict na magbago tungo sa pass ay length bias, at ang rubric ang kailangang ayusin.
Self-preference. Madalas mas mabait ang judge sa output mula sa sarili nitong model family kaysa sa output mula sa ibang model family. Subukan ito: i-grade ang parehong 30 sagot gamit ang judges mula sa dalawang magkaibang family at ihambing ang mga verdict case by case. Sa mga hindi pagkakasundo, ikaw mismo ang bumasa sa case.
Position bias. Kung ginagamit mo ang judge upang maghambing ng dalawang sagot, A at B, baligtarin ang pagkakasunod-sunod at patakbuhin itong muli. Ang verdict na nagbabago kapag binaligtad ang pagkakasunod ay nangangahulugang hindi pa ligtas gamitin ang pairwise comparison para sa rubric na iyon.
Rubric drift. Ang malalabong criterion ay nagbubunga ng mga judge na madaling sumang-ayon. Ang "Nakatutulong ba ang sagot" ay pumapasa sa halos lahat. Ang "Sinasabi ba ng sagot ang halaga ng refund sa dollars" ay pumapasa lamang sa talagang tinutukoy mo. Isulat muli ang bawat criterion hanggang malinaw nitong pangalanan ang fact na sinusuri.
Isang guard ang sumasaklaw sa lahat ng apat. Magtabi ng 30 case na mano-mano mong nilagyan ng label, at i-score ang judge laban sa mga label mo sa tuwing babaguhin mo ang judge model o judge prompt. Kung hindi ito sumasang-ayon sa iyo sa higit sa isang case sa bawat sampu, ayusin muna ang rubric bago ka magtiwala sa anumang pass rate na ibinibigay nito. Ang judge ay code, kaya dapat itong ma-version at ma-review gaya ng code.
Magsimula sa murang grader, at mag-escalate sa frontier model
Ang paggamit ng pinakamahal na model sa bawat commit para hatulan ang bawat case ang dahilan kung bakit lumalaki ang eval bill nang higit sa agent na sinusuri nito. Ayusin ang mga grader ayon sa presyo, at huminto sa sandaling malinaw na ang sagot.
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"
}
]Ipinapalagay ng mga numerong ito ang humigit-kumulang 1,200 input token at 120 output token sa bawat tawag sa judge. Makatotohanan itong laki para sa isang tanong, isang sagot, at isang criterion. Ang paghatol sa 1,000 case ay nagkakahalaga ng 1.80 US dollars sa Claude Haiku 4.5 at 9.00 sa Claude Opus 5. Mukhang maliit ang agwat hanggang paramihin mo ito. Ang set na may 60 case, na hinahatulan sa bawat commit at may 40 commit bawat linggo, ay katumbas ng 2,400 judge call bawat linggo bago pa mapatakbo ng sinuman ang nightly job.
Dalawang discount ang direktang naaangkop sa eval work, at pinagsasama ang mga ito. Hindi interactive ang eval run, kaya hinahati ng Batch API ang presyo ng input at output kapalit ng asynchronous delivery. Ito ang unang row ng chart. Eksaktong magkapareho sa bawat call ang rubric at instructions, byte for byte, kaya angkop ang prompt caching: ang cache read ay nagkakahalaga ng isang-sampu ng base input price, at ang limang-minutong cache write ay nagkakahalaga ng 1.25 beses ng base input. Dahil dito, nababawi ng cache ang gastos nito pagkatapos ng isang hit. Ito ang list prices ng Anthropic noong August 2026. Nasa introductory pricing ang Sonnet 5 hanggang 31 August 2026, kaya tataas ang ikatlong bar pagkatapos ng petsang iyon.
Ang ladder, ayon sa pagkakasunod:
- Mga deterministic check sa bawat case. Walang API cost.
- Maliit na model judge para sa mga case na nakalusot sa mga check na iyon.
- Frontier judge lamang kapag sinabi ng maliit na judge na fail, o nagsabing pass pero mababa ang confidence.
- Human review sa maliit na sample, isang beses bawat linggo.
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"]Ipinagpapalit nito ang ilang grading accuracy kapalit ng mas mababang gastos, kaya sukatin ang trade-off sa halip na ipagpalagay ito. Isang beses bawat buwan, i-grade rin ang buong set gamit ang strict judge at paghambingin ang dalawang column. Kung hindi magkasundo ang mga ito sa higit sa ilang case, masyadong maluwag ang rubric para sa maliit na model. Ang rubric ang dapat mong ayusin. Hiwalay na gawain ang pagkontrol sa gastos ng agent mismo. Saklaw ito sa pagkontrol sa gastos ng AI agent sa isang VPS.
Subaybayan ang pass rate sa paglipas ng panahon sa isang system na pagmamay-ari mo
Ang pass rate na hindi mo maiugnay sa isang commit ay palagay lamang. Mag-imbak ng isang row para sa bawat case sa bawat run, kasama ang commit at model sa row.
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;I-load ang schema gamit ang sqlite3 evals/results.db < evals/schema.sql, pagkatapos ay basahin ang trend gamit ang sqlite3 -box evals/results.db < evals/passrate.sql. Ang isang taon ng daily run para sa 60 case ay humigit-kumulang 22,000 row, kaya hindi kailanman magiging hiwalay na proyekto ang store. Tinalakay sa Pagpapatakbo ng SQLite sa production sa isang VPS ang mga setting na nagiging mahalaga kapag ibinabahagi ang file sa pagitan ng mga machine.
Ini-print ng runner ang parehong impormasyon para sa isang tao:
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 amountPatakbuhin ang suite sa mga pagbabagong maaaring makasira sa isang agent. Kabilang dito ang mga pagbabago sa prompt, model, at tool, sa halip na bawat commit saanman sa repository. Sinasaklaw ng pre-push hook ang mabilis na subset:
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-pushMas mabagal ang full run, kaya dapat itong ilagay sa isang schedule. Pinapatakbo ng nightly systemd service at timer sa VPS ang buong set laban sa deployed prompt. Ito ang nakakakita sa mga pagbabagong nagmumula sa labas ng repository, gaya ng hosted tool na nagbago ng behavior.
Pagsusuri ng tao, may sampling at hindi kumpleto
Ang judge ay kino-calibrate gamit ang mga label ng tao, kaya kailangang may gumawa ng mga ito. Suriin ang sample bawat linggo: lahat ng kasong hindi tama ang naging hatol ng judge, kasama ang sampung pumasa na random na pinili. Mahalaga ang random na mga pumasa, dahil ang judge na tahimik nang pumapasa sa mga maling sagot ay magmumukhang perpekto sa dashboard na sarili nitong mga hatol ang ginagamit.
Ang labinlimang kaso na tig-tatlong minuto ay 45 minuto bawat linggo. Kapalit nito, makakakuha ka ng mga pagwawasto sa rubric kapag hindi kayo nagkasundo ng judge, pati ng mga bagong kaso para sa mga uri ng failure na hindi pa naisip ninuman. Isulat ang hatol ng tao sa parehong table, kung saan ang graded_by ay nakatakda sa human, para ang pagkakasundo ng judge at tao ay maging isang query sa halip na umasa sa memorya.
Ano ang mga pumapalya sa mismong eval harness
anthropic.RateLimitError sa unang buong run. Kapag sabay-sabay na ipinadala ang 60 case, lalampas ito sa request o token limit ng iyong tier. Limitahan ang concurrency sa apat na worker, at ilipat ang nightly run sa Batch API.
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) mula sa judge. Maaaring sumagot ang model sa anyong prose o ilagay ang JSON nito sa loob ng code fence. Mag-retry nang isang beses, saka itala ang case bilang error. Huwag kailanman ituring na pass ang parse failure, dahil ang suite na ginagawang pass ang mga error ay aakyat patungong 100% habang lalong sumasama ang agent.
Mga flaky case. Pumapasa ang parehong input sa isang run at pumapalya sa kasunod, dahil nagsa-sample ang agent ng output nito. Patakbuhin ang flaky case nang tatlong beses at itala ang fraction sa halip na tanggalin ang case. Ang case na pumapasa sa dalawa sa tatlong run ay totoong robustness bug, at matutuklasan ito ng customer.
Pagkaluma ng golden set. May nag-e-edit ng expected answer para maging green ang suite. Suriin ang mga diff sa evals/cases.jsonl nang kasing-ingat ng mga diff sa agent, dahil ang file na iyon ang nakasulat na depinisyon mo ng tama.
Isang suite na hindi kailanman pumapalya. Kapag nananatili sa 100% ang pass rate sa loob ng isang buwan, hindi na nasusubaybayan ng set ang produkto. Kumuha ng sampung kamakailang trace, hanapin ang mga case na hindi nahawakan nang maayos ng agent, at idagdag ang mga ito. Pagkatapos, sadyang sirain ang isang bahagi at tiyaking magiging pula ang run. Ito ang pagsusuring naaangkop ang mutation testing sa isang test suite at ang tanging paraan para malaman kung may silbi pa rin ang iyong set.
FAQ
Ilang case ang kailangan ng AI agent eval set?
Magsimula sa 40 hanggang 80, at palawakin ang set gamit ang mga totoong failure. Kapag mas mababa sa humigit-kumulang 20 cases, ang isang hindi stable na resulta ay nakapagpapabago ng pass rate nang 5 puntos, kaya hindi na gaanong makabuluhan ang bilang. Kapag lumampas sa ilang daang case, may tunay na gastos at oras ang bawat run, habang kaunti na lamang ang dagdag na coverage mula sa bawat bagong case. Hindi ang bilang ang mahalagang sukatan. Ang mahalaga ay ang bahagdan ng mga uri ng failure sa production na alam mo at lumilitaw nang kahit isang beses sa set.
Mapagkakatiwalaan ko ba ang isang LLM judge sa pag-grade ng agent ko?
Oo lamang matapos mo itong sukatin gamit ang sarili mong labels. Magtabi ng 30 case na mano-mano mong na-grade, at ikumpara rito ang score ng judge tuwing papalitan mo ang judge model o ang judge prompt. Nagpapakita ang mga judge ng length bias, kung saan mas madalas pumapasa ang mga sagot na dinagdagan ng hindi kailangang teksto, at self-preference, kung saan mas mabait ang pag-grade sa output mula sa sarili nilang model family. Pareho itong maaaring subukan: dagdagan ng padding ang isang bagsak na sagot at ipa-grade itong muli, o ipa-grade ang parehong mga sagot sa judge mula sa ibang family. Kung hindi sumasang-ayon ang judge sa iyong mga label sa mahigit isang case sa bawat sampu, masyadong malabo ang rubric para magamit.
Aling model ang dapat mag-grade ng evals?
Gamitin muna ang mura, saka mag-escalate. Walang gastos ang deterministic assertions, kaya nauuna ang mga ito sa bawat case. Isang maliit na model ang humahawak sa mga malinaw na pass. Tanging ang mga fail at verdict na mababa ang confidence ang ipinapasa sa frontier model. Sa list prices noong August 2026, ang pag-grade ng 1,000 cases ay nagkakahalaga ng humigit-kumulang 1.80 US dollars gamit ang Claude Haiku 4.5 at humigit-kumulang 9.00 gamit ang Claude Opus 5. Dahil asynchronous ang eval runs, hinahati ng Batch API sa kalahati ang alinman sa dalawang halagang ito.
Pinapalitan ba ng evals ang production monitoring?
Hindi, dahil magkaiba ang mga tanong na sinasagot ng mga ito. Sinasabi ng isang eval suite kung ang pagbabagong ilalabas mo ay nagpapabuti o nagpapalala sa isang fixed set ng mga case. Sinasabi naman ng tracing at monitoring kung ano ang aktuwal na nararanasan ng mga user ngayon, kabilang ang mga input na hindi saklaw ng anumang case. Magkaugnay ang mga ito: nagbibigay ang traces ng mga bagong case, at tinutukoy ng eval suite kung talagang gumana ang iyong fix.