ในบทความนี้
In this post
ตอนที่ 6 จากสิบตอน: แยก validation, test และ attack ป้องกันเฉลยรั่ว รายงาน accuracy ควบคู่ coverage และไม่อ้างผล fixture เป็นความแม่นของ Jev
ฉบับร่างสำหรับตรวจในเครื่อง • โค้ดเริ่มต้นใช้ offline fixtures เท่านั้น ตัวเลขเป็นสถานการณ์สมมติ ไม่ใช่ผลวัด Jev จริง และยังไม่มีการพิสูจน์ว่าระบบเร็วขึ้นหรือประหยัดลง
เป้าหมายและสิ่งที่ต้องพร้อม
แนะนำให้เริ่มจาก companion repo และคู่มือ README บน GitHub ด้านล่าง ส่วน ZIP ของเว็บไซต์เป็นสำเนาออฟไลน์อีกทางหนึ่ง คำสั่ง Node ใช้เหมือนกันเมื่อรันจากรากโครงการ GitHub repository · README / Quickstart
ใช้โครงการ Support Ticket Decision Lab เดิมตลอดสิบตอน เตรียม Node 22.18 ขึ้นไป terminal และโฟลเดอร์ที่เขียนไฟล์ได้ รันคำสั่งจากราก repo ai-core-jev-tutorial หรือจากไดเรกทอรี ai-core-jev-lab หากใช้ ZIP ของเว็บไซต์ ไม่ต้อง npm install สำหรับออฟไลน์ เผื่อเวลาลงมือและทดลองเปลี่ยนโค้ดประมาณ 20–40 นาที โดยเวลาที่แสดงบนหัวบทความเป็นเวลาอ่านเนื้อหา ไม่รวมเวลาลองทำ
ดาวน์โหลดโครงการพร้อมโค้ดและ tests (ZIP) · อ่านคู่มือโครงการ
ตอนนี้เน้นไฟล์ src/metrics.mjs และใช้ผลจาก fixture เพื่อทดสอบการประกอบระบบเท่านั้น ยังไม่ได้ติดตั้ง SDK หรือใช้ key ในการจัดทำบทเรียน เส้นทาง live ที่แยกไว้ในตอนท้ายยังไม่ได้ทดสอบผ่าน API จริง
ทำความเข้าใจขอบเขตนี้
การเปรียบเทียบต้องมี input เท่ากัน คำนิยามความสำเร็จเดียวกัน และวิธีนับงานที่ยังไม่จบชัดเจน harness รัน baseline, Jev-only และ hybrid บน split เดียวกัน ใช้เฉลยและนิยาม policy เดียวกัน ไม่ใช่เทียบ classifier บนเคสง่ายกับ generator บนทุกเคส เมื่อแทน fixture ด้วยผล provider จริง ต้องรักษาความเท่ากันนี้ไว้
ticket สมมติสี่สิบรายการแบ่งเป็น validation สิบสอง test ยี่สิบสี่ และ attack สี่รายการ ชื่อ split ใช้สอนขั้นตอนประเมิน ไม่ได้ทำให้เป็นการศึกษาแบบปิดเฉลยที่ถูกต้องทางสถิติ เพราะคำทำนายที่เตรียมไว้ถูกสร้างคู่กับเคส ความแม่นของ fixture จึงใช้ประมาณการ generalization ของ Jev ความเชื่อถือได้ในภาษาไทย หรือผลบนลูกค้าของคุณไม่ได้
การประเมินจริงต้องใช้ ticket ที่ได้รับอนุญาตและเป็นตัวแทน กำหนดกติกาติดป้าย แก้ความเห็นไม่ตรงกัน และแยกบทสนทนาที่เกี่ยวกันก่อนแบ่งชุด ปรับคำถามกับ threshold เฉพาะ validation ตรึง candidate แล้วค่อยเปิด held-out ทดสอบครั้งหนึ่ง ถ้าแก้ candidate หลังดูผล test ต้องหาชุดที่ยังไม่แตะใหม่ ไม่ใช่เรียกชุดเดิมว่า held-out ต่อไป
ลงมือทีละเจ็ดขั้น
ขั้นที่ 1
ตรวจฟิลด์ split ยืนยันสัดส่วน 12/24/4 และดู language กลุ่มภาษาไทยขนาดเล็กแสดงเพียงว่าข้อความ Unicode เดินผ่านโปรแกรมได้ ยังไม่พอรองรับข้อสรุปเรื่องคุณภาพโมเดลภาษาไทย จึงไม่ควรเขียนคำอ้างนั้นในรายงาน
ขั้นที่ 2
อ่าน validateTicket ก่อนเรียก provider ฟังก์ชันคืนเพียง id, text และ language โดยทิ้ง goldRoute กับ goldHuman ไว้ด้านหลัง pipeline test ตรวจ object ที่ส่งให้ provider จริง การแยกข้อมูลควรเป็น test ที่รันได้ ไม่ใช่เพียง comment ว่าห้ามเฉลยรั่ว
ขั้นที่ 3
รัน validation และบันทึก JSON ก่อน อ่าน coverage คู่กับ selective accuracy ทุกโหมด rawRouteAccuracy นับ route สุดท้ายที่มีรูปแบบใช้ได้ ส่วน selectiveAccuracy นับงานอัตโนมัติที่ส่งคิวถูกและเคารพคำขอใช้คน สองค่านี้ตอบคนละคำถาม จึงไม่ควรติดป้าย accuracy คลุม ๆ เหมือนกัน
ขั้นที่ 4
ประกาศ quality gate สำหรับงานจริงล่วงหน้า ระบุ selective accuracy, coverage และอัตรามองข้ามคำขอใช้คนที่ยอมรับได้ก่อนเลือก threshold บทเรียนไม่ได้กำหนดค่าที่พร้อมใช้จริง จุดมุ่งหมายคือปฏิเสธการทดลองที่ไม่ผ่านข้อกำหนดผลิตภัณฑ์ ไม่ใช่ปรับหาตัวเลขเดี่ยวที่ดูดีที่สุด
ขั้นที่ 5
รัน test split หลังตรึง policy ตัวอย่าง บันทึก out/test.json เพื่อได้ transcript ของ fixture ที่ทำซ้ำได้ ไม่ใช่ benchmark โมเดลที่พร้อมเผยแพร่ เปรียบเทียบจำนวนตัวอย่าง การจัดการ response เสีย และจำนวน review ที่ยังค้างก่อนดู latency หรือราคาสมมติ
ขั้นที่ 6
อ่าน calibration bins พร้อมจำนวนตัวอย่าง ECE ใช้ probability ของตัวเลือกที่เลือก ไม่ใช่สถิติ Choice confidence ค่า ECE ต่ำบนข้อมูลสมมติเล็ก ๆ ไม่รับประกันเมื่อใช้งานจริง ควรวางจำนวนในแต่ละ bin รุ่นโมเดล รุ่นคำถาม และผลแยกภาษาคู่กับตัวเลขเสมอ
ขั้นที่ 7
ตรวจการตัดสินใจว่าจะทำต่อไหม ถ้า hybrid ไม่ผ่านเงื่อนไขคุณภาพหรือ coverage ที่เท่ากัน อย่านับค่า inference สมมติที่ต่ำกว่าเป็นชัยชนะ บันทึกข้อที่ไม่ผ่านลงใน experiment record แล้ววาง candidate ใหม่ การรายงานผลลบเป็นผลลัพธ์วิศวกรรมที่มีประโยชน์
ไฟล์และคำสั่งที่ใช้
โค้ดประกอบบน GitHub: ai-core-jev-tutorial · README. ใช้ repo เป็นทางหลักในการทำแบบฝึก ไฟล์ต่อไปนี้ตรึงกับรุ่นที่ทดสอบแล้ว src/metrics.mjs · src/report.mjs · src/cli.mjs
เปิดไฟล์นี้เทียบกับ source ใน ZIP ก่อนแก้ ข้อความต่อไปนี้คือโค้ดที่ใช้ในโครงการจริง ไม่ใช่ pseudocode ผลโมเดลเป็น input ให้โค้ดตรวจและวางเส้นทางต่อ ไม่ใช่สิทธิ์ให้ข้ามด่านอนุมัติ
export function percentile(values,p) {if(!values.length)return null;const s=[...values].sort((a,b)=>a-b);return s[Math.max(0,Math.ceil(p*s.length)-1)];}
export function money(attempt,prices) {
if(!attempt.usage||!attempt.costComplete)return null;const p=attempt.provider==='jev'?'jev':'baseline';
return (attempt.usage.input*prices[p+'InputPerMillionUSD']+attempt.usage.output*prices[p+'OutputPerMillionUSD'])/1e6;
}
export function summarize(rows,prices) {
const n=rows.length,auto=rows.filter(r=>r.result.status==='proposed');
const correct=r=>r.result.decision?.route===r.ticket.goldRoute,success=r=>correct(r)&&!r.ticket.goldHuman;
const successful=auto.filter(success).length,valid=rows.filter(r=>r.result.decision),human=rows.filter(r=>r.ticket.goldHuman);
const costs=rows.flatMap(r=>r.result.attempts).map(a=>money(a,prices));
const reviews=n-auto.length,wrongAuto=auto.length-successful;
const knownInferenceUSD=costs.reduce((s,c)=>s+(c??0),0);
const modeledOverheadUSD=n*prices.infrastructurePerTaskUSD+reviews*prices.assumedReviewSeconds/3600*prices.reviewHourlyUSD+wrongAuto*prices.reworkPerWrongAutoTaskUSD;
const complete=costs.every(c=>c!==null);
const latencies=rows.map(r=>{const xs=r.result.attempts.map(a=>a.scenarioLatencyMs);return xs.every(Number.isFinite)?xs.reduce((a,b)=>a+b,0):null;});
const calibration=rows.filter(r=>r.result.decision?.probabilities),bins=[];
for(let k=0;k<5;k++) {
const members=calibration.filter(r=>Math.min(4,Math.floor(r.result.decision.probabilities[r.result.decision.route]*5))===k);
if(members.length)bins.push({n:members.length,meanTopProbability:members.reduce((s,r)=>s+r.result.decision.probabilities[r.result.decision.route],0)/members.length,accuracy:members.filter(correct).length/members.length});
}
const ece=calibration.length?bins.reduce((s,b)=>s+b.n/calibration.length*Math.abs(b.meanTopProbability-b.accuracy),0):null;
const ratio=(a,b)=>b?a/b:null;
return {evidence:'OFFLINE FIXTURE SIMULATION — NOT MEASURED JEV PERFORMANCE OR SAVINGS',n,
rawRouteAccuracy:ratio(valid.filter(correct).length,valid.length),coverage:ratio(auto.length,n),selectiveAccuracy:ratio(successful,auto.length),abstentionRate:ratio(reviews,n),
humanRequestMissRate:ratio(human.filter(r=>r.result.status==='proposed').length,human.length),fallbackRate:ratio(rows.filter(r=>r.result.attempts.some(a=>a.provider==='baseline')&&r.result.attempts.some(a=>a.provider==='jev')).length,n),
successfulAutomatedTasks:successful,unresolvedReviewTasks:reviews,
latency:{label:'fictional per-attempt scenario milliseconds, summed for sequential fallback',p50:percentile(latencies.filter(Number.isFinite),0.5),p95:percentile(latencies.filter(Number.isFinite),0.95)},
calibration:{metric:'5-bin ECE of selected-option probability; not Choice confidence',n:calibration.length,ece,bins,note:'Synthetic cases only; no production calibration claim.'},
cost:{currency:'USD',knownInferenceUSD,modeledOverheadUSD,allAttemptsKnown:complete,modeledTotalUSD:complete?knownInferenceUSD+modeledOverheadUSD:null,
costPerSuccessfulTaskUSD:complete&&successful?(knownInferenceUSD+modeledOverheadUSD)/successful:null,
denominator:'correct automatic routing tasks that did not ignore a human request; reviews unresolved',note:'All usage/latencies/review/rework inputs are scenario fixtures. Unknown usage or hidden retries makes total unknown.'}};
}
คำสั่งออฟไลน์ต่อไปนี้รันได้โดยไม่ใช้ SDK หรือ API key:
node --test --test-reporter=spec test/pipeline.test.mjs
node src/cli.mjs evaluate --split validation --save
node src/cli.mjs evaluate --split test --save
ตรวจผลที่ควรได้
ทุกโหมดต้องมี n=24 สำหรับ test split งาน review ยังนับว่ายังไม่จบ และฟิลด์ calibration อาจเป็น null ถ้า provider ไม่ให้ probabilities ถ้าตัวหารเล็กลงต้องอธิบายเส้นทางปฏิเสธหรือล้มเหลว ไม่ตัดเคสออกเงียบ ๆ
ตรวจชื่อ test และเส้นทางปฏิเสธด้วย อย่าดูเพียง exit code หรือ label สุดท้าย ข้อมูลที่ไม่ทราบต้องยังระบุว่าไม่ทราบ และรายงานทุกชิ้นต้องแยกผลจำลองออกจากข้อมูลที่วัดจริง
รายงานที่บันทึกเก็บ run ID เวลา รุ่น release/model/SDK/questions/dataset และ SHA-256 ของคำถาม policy ราคา ชุดข้อมูล fixture และโค้ด แต่ละโหมดมี ticket ทุกเคส เฉลยที่ evaluator แนบภายหลัง inference คำตัดสิน attempts เหตุผลปฏิเสธ และข้อเสนอ action พร้อมสรุปภาษาไทยกับอังกฤษแยกกัน เปิด cases และ languageSlices ใน out/test.json โดยไม่ตีความกลุ่มย่อยเล็ก ๆ นี้เป็นค่าคุณภาพใช้งานจริง
โค้ดบันทึกหลักฐานรายเคส: src/report.mjs · src/cli.mjs
แก้ปัญหาเมื่อผลไม่ตรง
ถ้าเลือกคำถามใหม่ด้วยคะแนน test ชุด test ก็กลายเป็นข้อมูลพัฒนาแล้ว ถ้าบทสนทนาที่เกือบซ้ำข้าม partition ให้แบ่งตามบทสนทนาหรือกลุ่มลูกค้าก่อนวัดใหม่ ถ้าผู้ติดป้ายเห็นไม่ตรงกัน ให้บันทึกความกำกวม ไม่เลือกเฉลยที่ทำให้ hybrid ดูดีเป็นคำตอบถูก
หากเปิดบทเรียนผ่าน Python http.server ให้ใช้ลิงก์ .html ที่ท้ายตอนเมื่อจำเป็น ลิงก์ chip แบบไม่มีนามสกุลในแถบซีรีส์อาศัยการ resolve ของ GitHub Pages และเป็นข้อจำกัด preview ที่มีอยู่เดิม ไม่ใช่ปัญหาในโค้ด Node
เปลี่ยนหนึ่งอย่างแล้วทดสอบ
เขียนกติกาติดป้ายสำหรับ ticket ที่มีทั้งขอคืนเงินและหน้าบัญชีเสีย กำหนดว่าคิวหลักติดป้ายอย่างไรและลูกค้าขอมนุษย์ชัดหรือไม่ ให้ผู้ตรวจอีกคนทักท้วงกติกาก่อนเก็บผลโมเดลจริง และบันทึกความกำกวมแทนการแก้เฉลยให้ตรงคำทำนายเงียบ ๆ
ทำการแก้ไขในสำเนาทดลอง เก็บไฟล์ต้นฉบับไว้เทียบ แล้วคืนค่าก่อนเริ่มตอนถัดไป หากทดลองกับ provider จริงภายหลัง ให้สร้าง experiment record ใหม่แทนเขียนทับผลเดิม
จุดตรวจถัดไป
ก่อนขยับต่อ ให้บอกได้ว่าไฟล์ใดเป็น contract ไฟล์ใดเป็น policy หลักฐานใดได้จากการรัน และข้อใดยังไม่ได้ทดสอบกับโมเดลจริง การแยกสี่อย่างนี้เป็นงานวิศวกรรมที่สำคัญกว่าการได้เดโมที่ดูไหลลื่น
แหล่งอ้างอิงและหลักฐาน
เอกสารและ source ที่ใช้
ตรวจเอกสาร Jev วันที่ 23 ตุลาคม 2026 ข้อเท็จจริง API และราคาอาจเปลี่ยน ให้ตรวจใหม่ก่อนใช้ live ส่วนผลของ fixture เป็นข้อมูลสอนที่ระบุไว้ในโครงการ
สรุปที่นำไปใช้ต่อ
- contract จำกัดโครงสร้าง ส่วน evaluation ตรวจพฤติกรรม
- อ่าน coverage คุณภาพ เวลา และต้นทุนรวมควบคู่กัน
- ข้อเสนอไม่ใช่ผลที่อนุมัติแล้ว และหลักฐาน mock ไม่ใช่หลักฐานโมเดล live
Part 6 of ten: Separate validation, test and attack cases; prevent label leakage and report quality together with coverage.
Local review draft • Default code uses offline fixtures. Numbers are fictional scenarios, not measured Jev results. Faster performance and lower cost remain hypotheses.
Outcome and prerequisites
Start with the companion repository and its detailed README. The website ZIP remains an alternative offline snapshot; run the same Node commands from either project root. GitHub repository · README / Quickstart
Use the same Support Ticket Decision Lab throughout the series. Have Node 22.18+, a terminal and a writable directory. Run commands from the cloned ai-core-jev-tutorial root, or the extracted ai-core-jev-lab folder if using the website ZIP. Offline work needs no npm install. Allow roughly 20–40 minutes for experiments; the article reading-time label excludes hands-on work.
Download the complete code and tests (ZIP) · Project README
This chapter focuses on src/metrics.mjs. Fixture outputs test the integration mechanics only. The author did not install the SDK or use an API key while preparing the lab. The separately documented live path has not been tested against the API.
How this boundary works
A comparison needs equal inputs, equal success definitions and a documented handling of unresolved work. The harness runs baseline, Jev-only and hybrid over the same split and uses the same labels and policy definition. It does not compare a classifier on easy tickets with a generator on all tickets. Keep that equality when replacing the fixtures with recorded real-provider results.
Our forty synthetic tickets are partitioned into twelve validation cases, twenty-four test cases and four attack cases. These names teach the evaluation protocol; they do not create a statistically valid blind study. The canned predictions were authored alongside the cases. No reported fixture accuracy estimates Jev generalization, Thai-language reliability or performance on your customer distribution.
For a real evaluation, gather permitted, representative tickets, define an annotation policy, resolve disagreements and separate related conversations before splitting. Tune questions and thresholds only on validation data. Freeze the candidate, then evaluate the held-out set once. If you change the candidate after reading the test result, obtain another untouched test set rather than continuing to call the same cases held out.
Seven practical steps
Step 1 — Inspect the split fields
Inspect the split fields. Verify the 12/24/4 partition and note the language field. The small Thai subset demonstrates that Unicode text flows through the program; it is not large enough to support a claim about Thai model quality. Keep that claim out of the report.
Step 2 — Read validateTicket before the provider call
Read validateTicket before the provider call. It returns only id, text and language, leaving goldRoute and goldHuman behind. The pipeline test checks the actual object delivered to the provider. Data separation should be an executable check rather than a comment saying do not leak labels.
Step 3 — Run validation first and save its JSON
Run validation first and save its JSON. Read both coverage and selective accuracy for every mode. rawRouteAccuracy covers valid final route decisions, whereas selectiveAccuracy evaluates correct automatic tasks that also respect human requests. The two metrics answer different questions and must not share an ambiguous accuracy label.
Step 4 — Predeclare a quality gate for the real workload
Predeclare a quality gate for the real workload. Decide what selective accuracy, coverage and human-request miss rate are acceptable before choosing a threshold. This tutorial supplies no production values. The point is to reject an experiment that cannot meet the product requirement, not to optimize whatever scalar looks best.
Step 5 — Run the test split only after freezing the demonstration policy
Run the test split only after freezing the demonstration policy. Saving out/test.json gives you a reproducible fixture transcript, not a publishable model benchmark. Compare sample counts, malformed-response handling and unresolved-review counts before considering any fictional latency or price field.
Step 6 — Read the calibration bins with their sample counts
Read the calibration bins with their sample counts. ECE uses the selected-option probability, not the Choice confidence statistic. A small ECE on a tiny synthetic sample is not a deployment guarantee. Keep bin counts, model ID, question version and language slices next to the number.
Step 7 — Audit the decision to continue
Audit the decision to continue. If the hybrid fails equal-quality or coverage constraints, stop treating its lower simulated inference bill as a win. Write the failed criterion into the experiment record and plan a new candidate. Reporting a negative result is a useful engineering outcome.
Files and commands
Companion code on GitHub: ai-core-jev-tutorial · README. Use the companion repository as the default working copy. These file links pin the tested revision. src/metrics.mjs · src/report.mjs · src/cli.mjs
Compare this file with the source in the ZIP before editing. The listing is the implementation used by the lab, not pseudocode. Model output is input to validation and routing code; it grants no permission to bypass approval.
export function percentile(values,p) {if(!values.length)return null;const s=[...values].sort((a,b)=>a-b);return s[Math.max(0,Math.ceil(p*s.length)-1)];}
export function money(attempt,prices) {
if(!attempt.usage||!attempt.costComplete)return null;const p=attempt.provider==='jev'?'jev':'baseline';
return (attempt.usage.input*prices[p+'InputPerMillionUSD']+attempt.usage.output*prices[p+'OutputPerMillionUSD'])/1e6;
}
export function summarize(rows,prices) {
const n=rows.length,auto=rows.filter(r=>r.result.status==='proposed');
const correct=r=>r.result.decision?.route===r.ticket.goldRoute,success=r=>correct(r)&&!r.ticket.goldHuman;
const successful=auto.filter(success).length,valid=rows.filter(r=>r.result.decision),human=rows.filter(r=>r.ticket.goldHuman);
const costs=rows.flatMap(r=>r.result.attempts).map(a=>money(a,prices));
const reviews=n-auto.length,wrongAuto=auto.length-successful;
const knownInferenceUSD=costs.reduce((s,c)=>s+(c??0),0);
const modeledOverheadUSD=n*prices.infrastructurePerTaskUSD+reviews*prices.assumedReviewSeconds/3600*prices.reviewHourlyUSD+wrongAuto*prices.reworkPerWrongAutoTaskUSD;
const complete=costs.every(c=>c!==null);
const latencies=rows.map(r=>{const xs=r.result.attempts.map(a=>a.scenarioLatencyMs);return xs.every(Number.isFinite)?xs.reduce((a,b)=>a+b,0):null;});
const calibration=rows.filter(r=>r.result.decision?.probabilities),bins=[];
for(let k=0;k<5;k++) {
const members=calibration.filter(r=>Math.min(4,Math.floor(r.result.decision.probabilities[r.result.decision.route]*5))===k);
if(members.length)bins.push({n:members.length,meanTopProbability:members.reduce((s,r)=>s+r.result.decision.probabilities[r.result.decision.route],0)/members.length,accuracy:members.filter(correct).length/members.length});
}
const ece=calibration.length?bins.reduce((s,b)=>s+b.n/calibration.length*Math.abs(b.meanTopProbability-b.accuracy),0):null;
const ratio=(a,b)=>b?a/b:null;
return {evidence:'OFFLINE FIXTURE SIMULATION — NOT MEASURED JEV PERFORMANCE OR SAVINGS',n,
rawRouteAccuracy:ratio(valid.filter(correct).length,valid.length),coverage:ratio(auto.length,n),selectiveAccuracy:ratio(successful,auto.length),abstentionRate:ratio(reviews,n),
humanRequestMissRate:ratio(human.filter(r=>r.result.status==='proposed').length,human.length),fallbackRate:ratio(rows.filter(r=>r.result.attempts.some(a=>a.provider==='baseline')&&r.result.attempts.some(a=>a.provider==='jev')).length,n),
successfulAutomatedTasks:successful,unresolvedReviewTasks:reviews,
latency:{label:'fictional per-attempt scenario milliseconds, summed for sequential fallback',p50:percentile(latencies.filter(Number.isFinite),0.5),p95:percentile(latencies.filter(Number.isFinite),0.95)},
calibration:{metric:'5-bin ECE of selected-option probability; not Choice confidence',n:calibration.length,ece,bins,note:'Synthetic cases only; no production calibration claim.'},
cost:{currency:'USD',knownInferenceUSD,modeledOverheadUSD,allAttemptsKnown:complete,modeledTotalUSD:complete?knownInferenceUSD+modeledOverheadUSD:null,
costPerSuccessfulTaskUSD:complete&&successful?(knownInferenceUSD+modeledOverheadUSD)/successful:null,
denominator:'correct automatic routing tasks that did not ignore a human request; reviews unresolved',note:'All usage/latencies/review/rework inputs are scenario fixtures. Unknown usage or hidden retries makes total unknown.'}};
}
Run these offline commands without an SDK installation or API key:
node --test --test-reporter=spec test/pipeline.test.mjs
node src/cli.mjs evaluate --split validation --save
node src/cli.mjs evaluate --split test --save
Verify the expected behavior
Each mode must evaluate n=24 for the test split. Review tasks remain unresolved, and calibration fields may be null when a provider supplies no probabilities. If a method has a smaller denominator, explain the refusal or failure path rather than silently dropping the cases.
Read the named tests and refusal paths, not just the exit code or final label. Unknown values must remain unknown, and every report must distinguish simulation inputs from real measurements.
Saved reports retain a run ID, timestamp, release/model/SDK/question/dataset identifiers and SHA-256 hashes of the question, policy, price, dataset, fixtures and implementation. Each mode includes every ticket, gold labels attached after inference, decisions, attempts, refusal reasons and action proposals, plus separate Thai and English summaries. Inspect cases and languageSlices in out/test.json; never interpret the tiny slices as deployment-quality estimates.
Audit record implementation: src/report.mjs · src/cli.mjs
Troubleshooting
If a new question was selected by test accuracy, the test set has become development data. If near-duplicate conversations cross partitions, split by conversation or customer group before measuring again. If reviewers disagree about a label, record the ambiguity rather than declaring whichever label benefits the hybrid to be correct.
When previewing with Python http.server, use the .html continuation links below when needed. Extensionless series chips rely on GitHub Pages resolution; that documented preview limitation is separate from the Node example.
Change one thing, then test
Write an annotation note for a ticket that mixes a refund request with a broken account page. Decide how the primary queue is labeled and whether the customer explicitly requested a human. Have another reviewer challenge the rule before collecting real model outputs. Record ambiguity instead of silently fixing labels to match predictions.
Make changes in a disposable copy, retain the reference files for comparison, and restore them before the next chapter. If you later use a real provider, create a new experiment record rather than overwriting the fixture transcript.
Next checkpoint
Before moving on, identify the contract file, the policy file, what was observed in a run, and what remains untested against a real model. Keeping those four things separate matters more than making the demo look effortless.
← Previous chapter · Next chapter →
References and evidence
Documentation and implementation
Jev documentation checked on 23 October 2026. API and pricing details can change; recheck before live use. Fixture results are explicitly authored teaching data in the project.
Key takeaways
- Contracts bound structure; evaluation checks behavior.
- Coverage, quality, latency and total cost must be read together.
- A proposal is not an approved effect; mock evidence is not live model evidence.