ในบทความนี้
In this post
ตอนที่ 5 จากสิบตอน: เขียนนโยบาย abstention และ fallback ที่ทดสอบได้ แยกความมั่นใจออกจากความถูกต้อง และไม่มองข้ามคำขอใช้มนุษย์
ฉบับร่างสำหรับตรวจในเครื่อง • โค้ดเริ่มต้นใช้ 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/pipeline.mjs และใช้ผลจาก fixture เพื่อทดสอบการประกอบระบบเท่านั้น ยังไม่ได้ติดตั้ง SDK หรือใช้ key ในการจัดทำบทเรียน เส้นทาง live ที่แยกไว้ในตอนท้ายยังไม่ได้ทดสอบผ่าน API จริง
ทำความเข้าใจขอบเขตนี้
นโยบาย abstention คือพฤติกรรมผลิตภัณฑ์ที่รันได้ ในแล็บนี้ Choice confidence ต่ำอาจเรียก fixture ของ generative baseline แต่กรณีอาจขอคุยกับคนจะไป review โดยตรง สองแขนงนี้มีผลต่างกัน fallback เป็นอีกความเห็นหนึ่งที่ผิดได้และเป็นอีกการเรียกที่มีต้นทุนเมื่อใช้งานจริง ไม่ใช่ข้อยกเว้นจาก effect guard
Choice confidence สรุปความกระจุกตัวของการแจกแจง ไม่ใช่สัดส่วนการตอบถูกที่วัดจาก ticket ของคุณ ค่า 0.9 จึงไม่รับประกันความแม่น 90 เปอร์เซ็นต์เมื่อนำไปใช้ เราวัด calibration โดยเทียบ probability ของตัวเลือกที่เลือกกับเฉลย และยังถือผลนั้นว่าเป็นคุณสมบัติของชุดข้อมูลกับรุ่นโมเดล ไม่ใช่ค่าคงที่สากล
threshold route ที่ 0.7 และขอบเขต human-no ที่ 0.3 เป็นพารามิเตอร์สอน ยังไม่ได้ยืนยันกับธุรกิจหรือประชากรภาษาใด อย่านำไปใช้จริงเพียงเพราะเห็นในบทเรียน ให้ประกาศคุณภาพและ coverage ที่รับได้ล่วงหน้า sweep เฉพาะ validation แล้วเก็บ test ที่แยกไว้จนกว่าจะตรึง policy
ลงมือทีละเจ็ดขั้น
ขั้นที่ 1
อ่านไฟล์ policy ในฐานะ release artifact จดรุ่นก่อนเปลี่ยน threshold routeConfidence ใช้กับสถิติของ Choice ส่วน humanNoMax ใช้กับความน่าจะเป็นของ Noul การแยกชื่อฟิลด์ช่วยไม่ให้ helper ชื่อ confidence แบบครอบจักรวาลตีความสองอย่างนี้ปนกัน
ขั้นที่ 2
รัน T-002 fixture ของ route กำกวมจนต่ำกว่า threshold ทำให้ hybrid เรียก baseline หนึ่งครั้ง ดู attempts ทั้งสองและลำดับของมัน อย่าทิ้งการเรียกครั้งแรกเมื่อครั้งที่สองเป็นผู้ให้ route สุดท้าย
ขั้นที่ 3
รัน T-004 คำขอคุยกับคนชัดเจนทำให้เข้า review ก่อน fallback จะเขียนคำตัดสินใหม่ เงื่อนไขนี้เป็นภาระของผลิตภัณฑ์ ไม่ใช่เพียงความชอบของ classifier โปรแกรมต้องรักษาไว้แม้ provider อีกตัวเสนอ label อื่นอย่างมั่นใจ
ขั้นที่ 4
รัน T-006 ซึ่ง Noul อยู่ใกล้ช่วงกลางที่ไม่แน่ใจ เราไม่แปลงมันเป็น no ที่แน่นอนแล้วส่งคิวอัตโนมัติ เส้นทางของตัวอย่างนี้คือ needs_review ให้บันทึกว่าการงดตัดสินลักษณะนี้สร้างภาระเท่าไรต่อกระบวนงาน แทนการซ่อนเป็น model error หรือนับเป็นความสำเร็จฟรี
ขั้นที่ 5
ดู T-011 เทียบกับเฉลย เราตั้งใจใส่คำทำนายรูปแบบ Jev ที่มั่นใจสูงแต่ผิด fixture นี้สอนข้อจำกัด ไม่ใช่อัตราความผิดพลาดของ Jev ที่วัดจริง เมื่อประเมินจริงควรมีตัวอย่างโต้แย้งเช่นนี้ เพื่อไม่ให้คะแนนสูงถูกใช้แทนความถูกต้อง
ขั้นที่ 6
รัน sweep ที่ใช้ validation เท่านั้น อ่าน selective accuracy, coverage และ fallback rate คู่กันในแต่ละ threshold ถ้าทุกค่าผิดเงื่อนไขที่ประกาศไว้ คำตอบไม่ใช่เลือกตัวที่แพ้แต่ถูกที่สุด ให้แก้คำถาม ชุดข้อมูล หรือสถาปัตยกรรมแล้วเริ่มรอบ validation ใหม่
ขั้นที่ 7
ตรึง policy หนึ่งชุดก่อนตอนถัดไป เก็บสำเนาไว้คู่กับรายงานและรุ่นคำถาม ใช้ test split ประเมิน candidate ที่ตรึงแล้ว ไม่ใช่ใช้เลือก threshold หลังเห็นว่าค่าไหนทำตารางออกมาสวยที่สุด
ไฟล์และคำสั่งที่ใช้
โค้ดประกอบบน GitHub: ai-core-jev-tutorial · README. ใช้ repo เป็นทางหลักในการทำแบบฝึก ไฟล์ต่อไปนี้ตรึงกับรุ่นที่ทดสอบแล้ว src/pipeline.mjs · config/policy.json
เปิดไฟล์นี้เทียบกับ source ใน ZIP ก่อนแก้ ข้อความต่อไปนี้คือโค้ดที่ใช้ในโครงการจริง ไม่ใช่ pseudocode ผลโมเดลเป็น input ให้โค้ดตรวจและวางเส้นทางต่อ ไม่ใช่สิทธิ์ให้ข้ามด่านอนุมัติ
import {validateTicket,parseJev,parseBaseline,probability} from './contracts.mjs';
export function validatePolicy(policy) {
probability(policy.routeConfidence,'policy routeConfidence');probability(policy.humanNoMax,'policy humanNoMax');
if(policy.humanNoMax>=0.5||typeof policy.fallbackOnLowConfidence!=='boolean')throw new Error('invalid abstention policy');return policy;
}
export async function decide(input,{mode='hybrid',providers,policy}) {
const ticket=validateTicket(input);validatePolicy(policy);
if(!['baseline','jev','hybrid'].includes(mode))throw new Error('invalid mode');
const attempts=[];let decision=null,source=null,reason='not_evaluated';
async function obtain(kind,parse) {
try {const result=await providers[kind](ticket);attempts.push(result.attempt);source=kind;return parse(result.value);}
catch(error) {
if(attempts.at(-1)?.provider!==kind)attempts.push({provider:kind,mode:'error',usage:null,wallMs:null,scenarioLatencyMs:null,httpAttempts:null,costComplete:false});
reason=`${kind}_failed_validation_or_call`;return null;
}
}
if(mode==='baseline')decision=await obtain('baseline',parseBaseline);
else {
decision=await obtain('jev',parseJev);
if(decision&&decision.wantsHuman>policy.humanNoMax)reason='human_request_or_uncertainty';
else if(!decision||decision.confidence<policy.routeConfidence) {
const humanProbability=decision?.wantsHuman;
if(mode==='hybrid'&&policy.fallbackOnLowConfidence) {
const fallback=await obtain('baseline',parseBaseline);
decision=fallback?{...fallback,wantsHuman:Math.max(fallback.wantsHuman,humanProbability??0)}:null;
} else reason='low_route_confidence';
}
}
const auto=!!decision&&decision.wantsHuman<=policy.humanNoMax&&(source==='baseline'||decision.confidence>=policy.routeConfidence);
if(auto)reason=source==='baseline'?'validated_baseline_proposal':'validated_jev_proposal';
else if(decision?.wantsHuman>policy.humanNoMax)reason='human_request_or_uncertainty';
return {ticketId:ticket.id,status:auto?'proposed':'needs_review',reason,source,decision,attempts,action:auto?{kind:'route_ticket',ticketId:ticket.id,queue:decision.route}:null};
}
คำสั่งออฟไลน์ต่อไปนี้รันได้โดยไม่ใช้ SDK หรือ API key:
node src/cli.mjs demo --mode hybrid --ticket T-002
node src/cli.mjs demo --mode hybrid --ticket T-004
node src/cli.mjs demo --mode hybrid --ticket T-006
node src/cli.mjs demo --mode jev --ticket T-011
node src/cli.mjs sweep
ตรวจผลที่ควรได้
T-002 ใช้สอง attempts ส่วน T-004 และ T-006 ต้อง review และ T-011 อาจมั่นใจแต่ผิดได้ ข้อสังเกตสามแบบนี้สอนได้มากกว่าค่าเฉลี่ยเดียว เพราะแยกความมั่นใจในการส่งคิว ภาระของผลิตภัณฑ์ และความถูกต้องเชิงความหมายออกจากกัน
ตรวจชื่อ test และเส้นทางปฏิเสธด้วย อย่าดูเพียง exit code หรือ label สุดท้าย ข้อมูลที่ไม่ทราบต้องยังระบุว่าไม่ทราบ และรายงานทุกชิ้นต้องแยกผลจำลองออกจากข้อมูลที่วัดจริง
แก้ปัญหาเมื่อผลไม่ตรง
ถ้าเพิ่ม threshold แล้วเหมือนทุกอย่างดีขึ้น ให้ตรวจ attempts ที่หายและงาน review ที่ยังไม่จบ ถ้าเคสขอคุยกับคนกลายเป็นข้อเสนอหลัง fallback แปลว่า fallback ลบข้อจำกัดทิ้ง ถ้าใช้ผล test เลือก threshold ไปแล้ว ให้ถือชุดนั้นเป็น validation และเตรียม held-out ใหม่
หากเปิดบทเรียนผ่าน Python http.server ให้ใช้ลิงก์ .html ที่ท้ายตอนเมื่อจำเป็น ลิงก์ chip แบบไม่มีนามสกุลในแถบซีรีส์อาศัยการ resolve ของ GitHub Pages และเป็นข้อจำกัด preview ที่มีอยู่เดิม ไม่ใช่ปัญหาในโค้ด Node
เปลี่ยนหนึ่งอย่างแล้วทดสอบ
เลือกขั้นต่ำ coverage และ selective accuracy สมมติไว้ก่อนดู sweep แล้วอธิบายว่า candidate ใดไม่ผ่านเพราะอะไร ถ้าไม่มีตัวไหนผ่านให้รายงานตรงนั้น แบบฝึกนี้ฝึกกระบวนการตัดสินใจ ไม่ได้ทำให้ fixture กลุ่มเล็กกลายเป็นหลักฐานสำหรับ threshold ใช้งานจริง
ทำการแก้ไขในสำเนาทดลอง เก็บไฟล์ต้นฉบับไว้เทียบ แล้วคืนค่าก่อนเริ่มตอนถัดไป หากทดลองกับ provider จริงภายหลัง ให้สร้าง experiment record ใหม่แทนเขียนทับผลเดิม
จุดตรวจถัดไป
ก่อนขยับต่อ ให้บอกได้ว่าไฟล์ใดเป็น contract ไฟล์ใดเป็น policy หลักฐานใดได้จากการรัน และข้อใดยังไม่ได้ทดสอบกับโมเดลจริง การแยกสี่อย่างนี้เป็นงานวิศวกรรมที่สำคัญกว่าการได้เดโมที่ดูไหลลื่น
แหล่งอ้างอิงและหลักฐาน
เอกสารและ source ที่ใช้
ตรวจเอกสาร Jev วันที่ 23 ตุลาคม 2026 ข้อเท็จจริง API และราคาอาจเปลี่ยน ให้ตรวจใหม่ก่อนใช้ live ส่วนผลของ fixture เป็นข้อมูลสอนที่ระบุไว้ในโครงการ
สรุปที่นำไปใช้ต่อ
- contract จำกัดโครงสร้าง ส่วน evaluation ตรวจพฤติกรรม
- อ่าน coverage คุณภาพ เวลา และต้นทุนรวมควบคู่กัน
- ข้อเสนอไม่ใช่ผลที่อนุมัติแล้ว และหลักฐาน mock ไม่ใช่หลักฐานโมเดล live
Part 5 of ten: Implement abstention and fallback without treating concentration as correctness or ignoring a human request.
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/pipeline.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
An abstention policy is executable product behavior. In this lab, a low Choice confidence can trigger a generative-baseline fixture, while a possible human request goes directly to review. These are different branches with different consequences. A fallback is another fallible opinion and another billable operation in a real deployment, not an exemption from the effect guard.
Choice confidence summarizes the concentration of an option distribution; it is not a measured fraction of correct decisions on your tickets. A value of 0.9 is not a warranty of 90 percent deployment accuracy. We measure calibration using the selected option probability against labels, and still treat that result as a property of a dataset and model version, not a universal constant.
Our 0.7 route threshold and 0.3 human-no boundary are teaching parameters. They have not been validated for a business or language population. Do not deploy them because a tutorial uses them. Predeclare acceptable quality and coverage, sweep only validation data, and keep the held-out test untouched until the policy is frozen.
Seven practical steps
Step 1 — Read the policy file as a release artifact
Read the policy file as a release artifact. Record its version before changing a threshold. routeConfidence applies to the Choice statistic; humanNoMax applies to a Noul probability. Keeping the two field names distinct prevents a generic confidence helper from applying the wrong meaning to both.
Step 2 — Run T-002
Run T-002. Its route fixture is ambiguous enough to fall below the route threshold, so hybrid calls the baseline once. Inspect the two attempts and their ordering. Do not discard the first call when the second call supplies the final route.
Step 3 — Run T-004
Run T-004. An explicit request for a human causes review before a fallback can rewrite the decision. This is a product obligation, not merely a classifier preference. The program must retain this condition even when another provider offers a confident alternative label.
Step 4 — Run T-006
Run T-006. Its Noul probability is near the uncertain middle. We do not convert that into a firm no and auto-route. The safe path here is needs_review. Write down how much such abstention costs your support workflow instead of hiding it as a model error or counting it as a free success.
Step 5 — Inspect T-011 against its label
Inspect T-011 against its label. It deliberately contains a high-confidence wrong Jev-shaped prediction. The fixture teaches a limitation, not a measured Jev failure rate. Keep such counterexamples in your real evaluation set so a high score cannot become a substitute for correctness.
Step 6 — Run the validation-only sweep
Run the validation-only sweep. For each threshold read selective accuracy, coverage and fallback rate together. If every candidate fails your predeclared constraints, the answer is not to choose the cheapest loser; revise the questions, dataset or architecture and start a new validation cycle.
Step 7 — Freeze one policy before the next chapter
Freeze one policy before the next chapter. Keep a copy with the report and the question version. The test split is for evaluating that frozen candidate once, not for selecting the threshold after seeing which number gives the nicest table.
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/pipeline.mjs · config/policy.json
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.
import {validateTicket,parseJev,parseBaseline,probability} from './contracts.mjs';
export function validatePolicy(policy) {
probability(policy.routeConfidence,'policy routeConfidence');probability(policy.humanNoMax,'policy humanNoMax');
if(policy.humanNoMax>=0.5||typeof policy.fallbackOnLowConfidence!=='boolean')throw new Error('invalid abstention policy');return policy;
}
export async function decide(input,{mode='hybrid',providers,policy}) {
const ticket=validateTicket(input);validatePolicy(policy);
if(!['baseline','jev','hybrid'].includes(mode))throw new Error('invalid mode');
const attempts=[];let decision=null,source=null,reason='not_evaluated';
async function obtain(kind,parse) {
try {const result=await providers[kind](ticket);attempts.push(result.attempt);source=kind;return parse(result.value);}
catch(error) {
if(attempts.at(-1)?.provider!==kind)attempts.push({provider:kind,mode:'error',usage:null,wallMs:null,scenarioLatencyMs:null,httpAttempts:null,costComplete:false});
reason=`${kind}_failed_validation_or_call`;return null;
}
}
if(mode==='baseline')decision=await obtain('baseline',parseBaseline);
else {
decision=await obtain('jev',parseJev);
if(decision&&decision.wantsHuman>policy.humanNoMax)reason='human_request_or_uncertainty';
else if(!decision||decision.confidence<policy.routeConfidence) {
const humanProbability=decision?.wantsHuman;
if(mode==='hybrid'&&policy.fallbackOnLowConfidence) {
const fallback=await obtain('baseline',parseBaseline);
decision=fallback?{...fallback,wantsHuman:Math.max(fallback.wantsHuman,humanProbability??0)}:null;
} else reason='low_route_confidence';
}
}
const auto=!!decision&&decision.wantsHuman<=policy.humanNoMax&&(source==='baseline'||decision.confidence>=policy.routeConfidence);
if(auto)reason=source==='baseline'?'validated_baseline_proposal':'validated_jev_proposal';
else if(decision?.wantsHuman>policy.humanNoMax)reason='human_request_or_uncertainty';
return {ticketId:ticket.id,status:auto?'proposed':'needs_review',reason,source,decision,attempts,action:auto?{kind:'route_ticket',ticketId:ticket.id,queue:decision.route}:null};
}
Run these offline commands without an SDK installation or API key:
node src/cli.mjs demo --mode hybrid --ticket T-002
node src/cli.mjs demo --mode hybrid --ticket T-004
node src/cli.mjs demo --mode hybrid --ticket T-006
node src/cli.mjs demo --mode jev --ticket T-011
node src/cli.mjs sweep
Verify the expected behavior
T-002 uses two attempts, T-004 and T-006 need review, and T-011 can be confidently wrong. Those three observations are more instructive than a single average. They distinguish routing confidence, product obligations and semantic correctness.
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.
Troubleshooting
If increasing the threshold seems to improve everything, inspect missing attempts and unresolved reviews. If a human-request case reaches a proposal after fallback, the fallback has erased a constraint. If test-set results guided the chosen threshold, treat that test set as validation and obtain a new held-out set.
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
Choose a hypothetical coverage floor and a selective-accuracy floor before examining the sweep. Explain which candidates fail and why. If none qualify, report that outcome. This exercise practices a decision process; it does not turn the tiny fixture population into evidence for a production threshold.
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.