ในบทความนี้
In this post
ตอนที่ 4 จากสิบตอน: ตรวจ label, distribution, confidence และ Noul ตอน runtime ปฏิเสธคำตอบเสีย และรักษา usage ที่ไม่ทราบให้เป็น null
ฉบับร่างสำหรับตรวจในเครื่อง • โค้ดเริ่มต้นใช้ 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/contracts.mjs และใช้ผลจาก fixture เพื่อทดสอบการประกอบระบบเท่านั้น ยังไม่ได้ติดตั้ง SDK หรือใช้ key ในการจัดทำบทเรียน เส้นทาง live ที่แยกไว้ในตอนท้ายยังไม่ได้ทดสอบผ่าน API จริง
ทำความเข้าใจขอบเขตนี้
ชนิดข้อมูลที่ client boundary ไม่ได้แทน runtime validation fixture อาจเสีย response อาจมาไม่ครบ และ adapter ในอนาคตอาจคืนรุ่นโมเดลหรือชื่อฟิลด์ผิด contract ของเราจึงตรวจโมเดลที่ตรึงไว้ ชนิดคำตอบ probability ของทุก route ผลรวมการแจกแจง ตัวเลือกที่มีค่าสูงสุด และค่า Noul ที่เป็นตัวเลขจำกัด
validation ยืนยันข้อเท็จจริงเชิงโครงสร้างที่มีขอบเขตได้ เช่น admin ไม่ใช่คิวที่อนุญาต และ NaN ไม่ใช่ probability ที่ใช้ได้ แต่มันพิสูจน์ไม่ได้ว่าประโยคหนึ่งควรเป็น billing จริงหรือไม่ ต้องรักษาความต่างนี้ไว้ทั้งใน test และคำอธิบาย ไม่เช่นนั้น parser test สีเขียวจะกลายเป็นคำอ้างเรื่องความปลอดภัยเชิงความหมายที่เกินหลักฐาน
ขอบเขตคิดค่าบริการก็เป็น schema boundary เช่นกัน usage.input_tokens หรือ usage.output_tokens อาจหายหรือเป็น null ใน integration ฟังก์ชัน tokenUsage จึงคืน null เมื่อไม่ทราบ แทนการคำนวณศูนย์ดอลลาร์ที่ดูน่าพอใจ แม้ response สุดท้ายครบก็ยังอาจไม่บอกทุก HTTP attempt ที่ SDK retry ภายใน จึงไม่อ้างว่าทราบบิลครบ
ลงมือทีละเจ็ดขั้น
ขั้นที่ 1
อ่าน probability ก่อน ให้ปฏิเสธ string, null, infinity, NaN และค่าที่อยู่นอกศูนย์ถึงหนึ่ง อย่าแปลงชนิดด้วย Number(value) เพื่อให้ผ่านง่าย เพราะการเปลี่ยนฟิลด์ที่หายเป็นศูนย์อาจทำให้ความไม่ทราบกลายเป็นหลักฐานฝั่งปฏิเสธอย่างดูมั่นใจ
ขั้นที่ 2
ตรวจการแจกแจงของ route ให้ครบ ต้องมี key ทั้งสาม ค่าแต่ละตัวรวมเป็นหนึ่งภายใน tolerance เล็กน้อย และ label ที่เลือกต้องมีค่าสูงสุด ด่านนี้จับข้อมูลส่งไม่ครบหรือ adapter ไม่สอดคล้อง แต่ไม่ใช่การทดสอบความจริงของความหมาย
ขั้นที่ 3
ตรวจความสอดคล้องของ confidence สูตร Choice ตามเอกสารคำนวณจาก probability สูงสุดและจำนวนตัวเลือก validator จึงตรวจความสัมพันธ์นี้ เพื่อไม่ให้ response จับคู่การแจกแจงเกือบแบนกับ confidence หนึ่งแล้วลอดนโยบาย abstention โดยไม่ตั้งใจ
ขั้นที่ 4
ตรวจ Noul ตามรูปแบบของมัน ต้องมีชนิดคำตอบ noul และค่าตัวเลข noul ที่จำกัด อย่าบังคับฟิลด์ confidence ที่ API ไม่ระบุสำหรับ Noul ชุดทดสอบมีกรณีถูกต้องที่ไม่มีฟิลด์นั้นอย่างชัดเจน
ขั้นที่ 5
ทดลอง fixture เสีย T-040 มี label admin ที่ไม่รองรับ โหมด Jev-only ต้องคืน needs_review และไม่มี action ส่วน hybrid อาจถาม baseline แยกเพื่อขอข้อเสนอใหม่ แต่ไม่ได้รับรองคำตอบ Jev ที่ผิดว่าเคยผ่าน validation
ขั้นที่ 6
แยก usage ที่หายออกจากคำตอบผิด T-036 มีคำตัดสินที่ถูกโครงสร้างแต่ usage เป็น null จึงอาจเสนอ route ได้ตาม policy แต่ต้นทุนรวมยังไม่ทราบ การมี route กับการมีบัญชีต้นทุนที่เชื่อถือได้เป็นคุณสมบัติคนละเรื่อง
ขั้นที่ 7
รันและอ่านชื่อ test เพิ่มกรณี response เสียก่อนขยาย adapter test ที่ดีเริ่มจากความล้มเหลวที่อธิบายได้แล้วตรวจเส้นทางปฏิเสธ ไม่ใช่คัดนิพจน์ใน implementation มาคำนวณซ้ำแล้วสรุปว่าถูกเพราะได้ผลเหมือนกัน
ไฟล์และคำสั่งที่ใช้
โค้ดประกอบบน GitHub: ai-core-jev-tutorial · README. ใช้ repo เป็นทางหลักในการทำแบบฝึก ไฟล์ต่อไปนี้ตรึงกับรุ่นที่ทดสอบแล้ว src/contracts.mjs · test/contracts.test.mjs
เปิดไฟล์นี้เทียบกับ source ใน ZIP ก่อนแก้ ข้อความต่อไปนี้คือโค้ดที่ใช้ในโครงการจริง ไม่ใช่ pseudocode ผลโมเดลเป็น input ให้โค้ดตรวจและวางเส้นทางต่อ ไม่ใช่สิทธิ์ให้ข้ามด่านอนุมัติ
export const QUEUES = Object.freeze(['billing', 'technical', 'other']);
export function probability(value, name) {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1)
throw new Error(`invalid ${name}: expected a finite number in [0,1]`);
return value;
}
export function validateTicket(value) {
if (!value || typeof value.id !== 'string' || !/^T-\d{3}$/.test(value.id)) throw new Error('invalid ticket id');
if (typeof value.text !== 'string' || !value.text.trim() || value.text.length > 8000) throw new Error('invalid ticket text');
return Object.freeze({id:value.id, text:value.text, language:value.language ?? 'en'});
}
export function parseJev(response) {
if (!response || response.model !== 'jev-1.13.0') throw new Error('unexpected or missing pinned model');
const route = response.answers?.route, human = response.answers?.wantsHuman;
if (route?.type !== 'choice' || !QUEUES.includes(route.choice)) throw new Error('invalid route label/type');
const confidence = probability(route.confidence, 'choice confidence');
const probabilities = route.probabilities;
if (!probabilities || Object.keys(probabilities).sort().join() !== [...QUEUES].sort().join()) throw new Error('incomplete option distribution');
const values = QUEUES.map(k => probability(probabilities[k], `probability ${k}`));
if (Math.abs(values.reduce((a,b)=>a+b,0)-1) > 1e-5) throw new Error('probabilities do not sum to one');
if (probabilities[route.choice] + 1e-8 < Math.max(...values)) throw new Error('selected choice is not maximal');
const expected=(Math.max(...values)-1/QUEUES.length)/(1-1/QUEUES.length);
if(Math.abs(confidence-expected)>1e-5)throw new Error('inconsistent Choice confidence');
if (human?.type !== 'noul') throw new Error('missing Noul answer');
return Object.freeze({route:route.choice, confidence, probabilities:{...probabilities}, wantsHuman:probability(human.noul,'Noul')});
}
export function parseBaseline(raw) {
const value=JSON.parse(raw);
if (!QUEUES.includes(value.route) || typeof value.wantsHuman !== 'boolean') throw new Error('invalid baseline schema');
return Object.freeze({route:value.route,wantsHuman:value.wantsHuman?1:0,confidence:null,probabilities:null});
}
export function tokenUsage(response) {
const input=response?.usage?.input_tokens, output=response?.usage?.output_tokens;
const valid=x=>Number.isSafeInteger(x)&&x>=0;
return valid(input)&&valid(output) ? {input,output} : null;
}
คำสั่งออฟไลน์ต่อไปนี้รันได้โดยไม่ใช้ SDK หรือ API key:
node --test --test-reporter=spec test/contracts.test.mjs
node src/cli.mjs demo --mode jev --ticket T-040
node src/cli.mjs demo --mode jev --ticket T-036
ตรวจผลที่ควรได้
response admin ที่เสียต้องไม่สร้าง action ส่วน usage ที่ไม่ทราบต้องคงเป็น null ขณะที่ศูนย์ที่ทราบจริงยังต้องเป็นเลขศูนย์ ทดสอบสองกรณีคู่กันเพื่อกัน default ที่ดูสะดวกทำให้รายงานต้นทุนผิดเงียบ ๆ
ตรวจชื่อ test และเส้นทางปฏิเสธด้วย อย่าดูเพียง exit code หรือ label สุดท้าย ข้อมูลที่ไม่ทราบต้องยังระบุว่าไม่ทราบ และรายงานทุกชิ้นต้องแยกผลจำลองออกจากข้อมูลที่วัดจริง
แก้ปัญหาเมื่อผลไม่ตรง
ถ้า validator ปฏิเสธ response จริงทั้งหมด ให้เทียบรูปแบบ SDK กับเอกสารรุ่นที่ตรึงก่อนลดความเข้มของด่าน ถ้า confidence กับ probability ไม่สอดคล้อง ให้ตรวจจำนวนตัวเลือกและการแปลงใน adapter อย่าเพิ่มชื่อฟิลด์สำรองที่เดาขึ้นเพียงเพื่อให้เดโมรันผ่าน
หากเปิดบทเรียนผ่าน Python http.server ให้ใช้ลิงก์ .html ที่ท้ายตอนเมื่อจำเป็น ลิงก์ chip แบบไม่มีนามสกุลในแถบซีรีส์อาศัยการ resolve ของ GitHub Pages และเป็นข้อจำกัด preview ที่มีอยู่เดิม ไม่ใช่ปัญหาในโค้ด Node
เปลี่ยนหนึ่งอย่างแล้วทดสอบ
เพิ่ม contract test ที่ลบ confidence ของ Choice แล้วเพิ่มอีกข้อที่เปลี่ยน Noul เป็น string ศูนย์ ทั้งคู่ควรถูกปฏิเสธ แต่ต้องเก็บกรณี Noul ถูกต้องที่ไม่มี confidence แยกเป็น test ผ่าน เพื่อไม่ให้เพิ่ม validation แล้วปฏิเสธรูปแบบตามเอกสารเสียเอง
ทำการแก้ไขในสำเนาทดลอง เก็บไฟล์ต้นฉบับไว้เทียบ แล้วคืนค่าก่อนเริ่มตอนถัดไป หากทดลองกับ provider จริงภายหลัง ให้สร้าง experiment record ใหม่แทนเขียนทับผลเดิม
จุดตรวจถัดไป
ก่อนขยับต่อ ให้บอกได้ว่าไฟล์ใดเป็น contract ไฟล์ใดเป็น policy หลักฐานใดได้จากการรัน และข้อใดยังไม่ได้ทดสอบกับโมเดลจริง การแยกสี่อย่างนี้เป็นงานวิศวกรรมที่สำคัญกว่าการได้เดโมที่ดูไหลลื่น
แหล่งอ้างอิงและหลักฐาน
เอกสารและ source ที่ใช้
ตรวจเอกสาร Jev วันที่ 23 ตุลาคม 2026 ข้อเท็จจริง API และราคาอาจเปลี่ยน ให้ตรวจใหม่ก่อนใช้ live ส่วนผลของ fixture เป็นข้อมูลสอนที่ระบุไว้ในโครงการ
สรุปที่นำไปใช้ต่อ
- contract จำกัดโครงสร้าง ส่วน evaluation ตรวจพฤติกรรม
- อ่าน coverage คุณภาพ เวลา และต้นทุนรวมควบคู่กัน
- ข้อเสนอไม่ใช่ผลที่อนุมัติแล้ว และหลักฐาน mock ไม่ใช่หลักฐานโมเดล live
Part 4 of ten: Validate labels, distributions, confidence and Noul at runtime; reject malformed responses and preserve unknown usage as null.
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/contracts.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
Types at a client boundary do not replace runtime validation. A fixture can be corrupted, a network response can be incomplete, and a future adapter may return the wrong model or field names. Our contract requires the pinned model, the expected answer types, all route probabilities, a normalized distribution, a maximal selected option and a finite Noul.
Validation establishes bounded structural facts. It can prove that admin is not an allowed queue and that NaN is not a usable probability. It cannot prove that a billing classification is correct for a particular sentence. Keep that distinction visible in both tests and explanations, otherwise green parser tests become a misleading claim about semantic safety.
The live billing boundary is also a schema boundary. usage.input_tokens or usage.output_tokens may be absent or null in an integration. tokenUsage returns null for unknown usage instead of an attractive zero-dollar estimate. A complete final response also does not reveal every internally retried HTTP attempt, which is why the live wrapper does not claim a complete bill.
Seven practical steps
Step 1 — Read the probability function first
Read the probability function first. Reject strings, null, infinities, NaN and values outside zero to one. Avoid coercion such as Number(value): converting a missing field into zero can turn uncertainty into apparently strong evidence for the negative class.
Step 2 — Validate the complete route distribution
Validate the complete route distribution. All three expected keys must be present, values must sum to one within a small numerical tolerance, and the chosen label must be maximal. This catches incomplete transport and inconsistent adapters; it is not a semantic truth test.
Step 3 — Check confidence consistency
Check confidence consistency. The documented Choice formula derives confidence from the top option probability and option count. The validator checks that relationship, so a response cannot pair a nearly flat distribution with confidence one and accidentally bypass the abstention policy.
Step 4 — Validate Noul on its own terms
Validate Noul on its own terms. Require a noul answer type and a finite noul value. Do not require a confidence field that the API does not document for Noul. Our tests explicitly cover the valid case in which Noul has no such field.
Step 5 — Exercise a corrupted fixture
Exercise a corrupted fixture. T-040 contains the unsupported admin label. Jev-only mode must produce needs_review with no action. Hybrid mode may ask the separate baseline provider for a new proposal, but it never blesses the invalid Jev answer as if validation had succeeded.
Step 6 — Exercise missing usage separately from a bad decision
Exercise missing usage separately from a bad decision. T-036 contains a structurally valid decision and null usage. It can still be proposed under the policy, but its full cost remains unknown. Availability of a route and availability of a reliable accounting record are two different properties.
Step 7 — Run and read the named tests
Run and read the named tests. Add a new malformed-response case before extending the adapter. A useful test starts at a failure you can describe and asserts the refusal route; it does not simply copy the implementation expression and congratulate the same expression for evaluating identically.
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/contracts.mjs · test/contracts.test.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 const QUEUES = Object.freeze(['billing', 'technical', 'other']);
export function probability(value, name) {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1)
throw new Error(`invalid ${name}: expected a finite number in [0,1]`);
return value;
}
export function validateTicket(value) {
if (!value || typeof value.id !== 'string' || !/^T-\d{3}$/.test(value.id)) throw new Error('invalid ticket id');
if (typeof value.text !== 'string' || !value.text.trim() || value.text.length > 8000) throw new Error('invalid ticket text');
return Object.freeze({id:value.id, text:value.text, language:value.language ?? 'en'});
}
export function parseJev(response) {
if (!response || response.model !== 'jev-1.13.0') throw new Error('unexpected or missing pinned model');
const route = response.answers?.route, human = response.answers?.wantsHuman;
if (route?.type !== 'choice' || !QUEUES.includes(route.choice)) throw new Error('invalid route label/type');
const confidence = probability(route.confidence, 'choice confidence');
const probabilities = route.probabilities;
if (!probabilities || Object.keys(probabilities).sort().join() !== [...QUEUES].sort().join()) throw new Error('incomplete option distribution');
const values = QUEUES.map(k => probability(probabilities[k], `probability ${k}`));
if (Math.abs(values.reduce((a,b)=>a+b,0)-1) > 1e-5) throw new Error('probabilities do not sum to one');
if (probabilities[route.choice] + 1e-8 < Math.max(...values)) throw new Error('selected choice is not maximal');
const expected=(Math.max(...values)-1/QUEUES.length)/(1-1/QUEUES.length);
if(Math.abs(confidence-expected)>1e-5)throw new Error('inconsistent Choice confidence');
if (human?.type !== 'noul') throw new Error('missing Noul answer');
return Object.freeze({route:route.choice, confidence, probabilities:{...probabilities}, wantsHuman:probability(human.noul,'Noul')});
}
export function parseBaseline(raw) {
const value=JSON.parse(raw);
if (!QUEUES.includes(value.route) || typeof value.wantsHuman !== 'boolean') throw new Error('invalid baseline schema');
return Object.freeze({route:value.route,wantsHuman:value.wantsHuman?1:0,confidence:null,probabilities:null});
}
export function tokenUsage(response) {
const input=response?.usage?.input_tokens, output=response?.usage?.output_tokens;
const valid=x=>Number.isSafeInteger(x)&&x>=0;
return valid(input)&&valid(output) ? {input,output} : null;
}
Run these offline commands without an SDK installation or API key:
node --test --test-reporter=spec test/contracts.test.mjs
node src/cli.mjs demo --mode jev --ticket T-040
node src/cli.mjs demo --mode jev --ticket T-036
Verify the expected behavior
The corrupted admin response must not produce an action. Unknown usage must remain null, while a genuine known zero stays a numeric zero. Test the two cases together to stop a convenience default from quietly breaking cost reports.
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 the validator rejects all real responses, compare the actual SDK response shape with the pinned official documentation before weakening checks. If confidence and probability disagree, inspect option count and adapter transformations. Do not add fallback aliases for invented field names just to make a demo run.
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
Add a contract test that deletes confidence from Choice, then one that replaces a Noul value with the string zero. Both should fail validation. Keep a valid Noul without separate confidence as a passing counterexample, so tightening validation does not accidentally reject the documented response shape.
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.