ในบทความนี้
In this post
ตอนที่ 8 จากสิบตอน: ลองข้อความโจมตี คำตอบผิดรูป และ provider ที่ล้ม แยก inference retry ออกจากการทำ action ซ้ำ และวางแผนทดสอบข้อจำกัดจริง
ฉบับร่างสำหรับตรวจในเครื่อง • โค้ดเริ่มต้นใช้ 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 จริง
ทำความเข้าใจขอบเขตนี้
attack suite ควรทดสอบขอบเขตที่เราควบคุมได้จริง ชุดออฟไลน์พิสูจน์ได้ว่า route admin ที่ผิด การอ้างอนุมัติปลอม และ provider ที่ล้มไม่สร้างผลจำลองโดยตรง แต่มันบอกไม่ได้ว่า Jev จับ prompt injection ได้หรือยังแม่นเมื่อสลับลำดับตัวเลือก คำถามเหล่านั้นต้องใช้ผลโมเดลจริงบนชุดประเมินที่ตรวจแล้ว
บันทึกข้อจำกัด Jev 1.13 ทางการกล่าวถึงปัญหาความแม่นเชิงตัวเลข state ที่มีข้อมูลไม่เกี่ยว เนื้อหาโจมตี และลำดับตัวเลือก เราจึงเก็บเลขคณิตกับสิทธิ์ไว้ในโค้ด deterministic และถือข้อความ ticket เป็นข้อมูล ไม่ใช่คำสั่ง fixture มีข้อความโจมตีโดยตั้งใจ เพื่อให้เห็นว่า guard ยังทำหน้าที่ได้ตรงไหนแม้คำตอบเชิงความหมายผิด
การ retry inference กับ retry effect แก้ปัญหาคนละอย่าง inference ที่ล้มชั่วคราวอาจต้องเรียก provider อีกครั้งซึ่งมี latency และอาจมีต้นทุน ส่วนการทำ action ที่อนุมัติแล้วซ้ำต้องเคารพ idempotency key SDK อาจ retry ภายใน wrapper live ของเราจึงไม่เพิ่มลูป retry ชั้นนอก และไม่อ้างว่าเห็น attempts ที่ซ่อนอยู่ครบ
ลงมือทีละเจ็ดขั้น
ขั้นที่ 1
รัน attack fixtures สี่รายการโดยคงป้ายข้อมูลสมมติไว้ T-037 กับ T-038 พยายามสั่งคิวหรืออ้างอนุมัติผ่านข้อความ ticket ผลที่เตรียมไว้นี้ไม่ใช่การวัดว่าโจมตี Jev สำเร็จหรือไม่ ให้ตรวจข้อเสนอสุดท้ายแยกจากด่าน approval
ขั้นที่ 2
รัน T-040 ในโหมด Jev-only โดยต้องปฏิเสธ label ที่ไม่รองรับ วิธีนี้แยกการตรวจ response ออกจาก hybrid fallback เพราะ test ที่รันแต่ hybrid อาจซ่อนความล้มเหลวเดิมไว้หลัง provider ตัวที่สองซึ่งบังเอิญตอบ label ถูก schema
ขั้นที่ 3
ฉีด exception ของ provider ผ่าน pipeline test เมื่อทั้งสอง provider ใช้ไม่ได้ ผลสุดท้ายต้องต้องการ review และไม่มี action การจัดการ error ควรเก็บเหตุผลที่ใช้วินิจฉัยได้ โดยไม่พิมพ์ API key และไม่ตีความว่าโมเดลใช้ไม่ได้แปลว่าคำตอบเป็น no
ขั้นที่ 4
ทำแผนที่ HTTP failures ก่อนใช้ live ปัญหา authentication กับ schema ต้องแก้สาเหตุ ไม่ใช่ retry แบบไม่ดู ส่วน rate และ overload อาจถูก SDK retry ให้กำหนดงบการลองใหม่ระดับแอปชัด และตรวจพฤติกรรมตามรุ่นในเอกสารทางการ ไม่เติมไลบรารี retry อีกชั้นเพราะความเคยชิน
ขั้นที่ 5
ระบุ metamorphic tests สำหรับ provider จริง เช่นสลับตัวเลือก เติมย่อหน้าไม่เกี่ยว เปลี่ยนถ้อยคำแต่คงเจตนา และแยกเลขคณิตออกจากโมเดล ตรึงสิ่งที่ควรคงเดิมก่อนเก็บคำตอบ provider fixture ปัจจุบันเลือกข้อมูลตาม ticket ID จึงตั้งใจไม่ใช้วัดผลของการแปลงเหล่านี้
ขั้นที่ 6
แยกผลล้มเหลวภาษาไทยกับอังกฤษ การส่งข้อความไทยไม่กี่ fixture ผ่านได้ยืนยันเพียง encoding และ control flow การประเมินภาษาไทยจริงต้องมีภาษาที่เป็นตัวแทน การสลับภาษา และคำเฉพาะงาน พร้อมผู้ตรวจที่เข้าใจภาษา อย่าอนุมานว่าแม่นเท่าอังกฤษเพราะใช้ JSON schema เดียวกัน
ขั้นที่ 7
รัน effect tests อีกครั้งหลังเปลี่ยน failure handling เส้นทาง exception ที่เริ่ม fallback ไม่ควรทำ action ที่อนุมัติแล้วซ้ำหรือข้ามเงื่อนไขขอใช้คน และต้องเก็บ record ของ attempt เดิมไว้แม้แขนงหลังเป็นผู้สร้างข้อเสนอสุดท้าย
ไฟล์และคำสั่งที่ใช้
โค้ดประกอบบน GitHub: ai-core-jev-tutorial · README. ใช้ repo เป็นทางหลักในการทำแบบฝึก ไฟล์ต่อไปนี้ตรึงกับรุ่นที่ทดสอบแล้ว src/pipeline.mjs · test/pipeline.test.mjs · test/network.test.mjs
เปิดไฟล์นี้เทียบกับ 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 evaluate --split attack --save
node src/cli.mjs demo --mode jev --ticket T-040
node --test --test-reporter=spec test/pipeline.test.mjs test/effects.test.mjs test/network.test.mjs
ตรวจผลที่ควรได้
รายงาน attack ต้องมี n=4 และยังติดป้ายว่าเป็น fixture simulation การสาธิต T-040 ด้วย Jev-only ต้องมี action:null ส่วน test provider ล้มและ approval ปลอมต้องผ่านได้โดยไม่ใช้ credentials หรือเครือข่าย
ตรวจชื่อ test และเส้นทางปฏิเสธด้วย อย่าดูเพียง exit code หรือ label สุดท้าย ข้อมูลที่ไม่ทราบต้องยังระบุว่าไม่ทราบ และรายงานทุกชิ้นต้องแยกผลจำลองออกจากข้อมูลที่วัดจริง
แก้ปัญหาเมื่อผลไม่ตรง
mock attack test ผ่านไม่ได้แปลว่าโมเดลทนการโจมตี ถ้าเห็นการเรียกเสียเงินซ้ำ ให้ตรวจว่า retry ของ SDK กับแอปซ้อนกันหรือไม่ ถ้า error string มีความลับ ให้เอาความลับออกจาก log และจัดการข้อมูลรั่วตามกระบวนการของคุณก่อนทำต่อ
หากเปิดบทเรียนผ่าน Python http.server ให้ใช้ลิงก์ .html ที่ท้ายตอนเมื่อจำเป็น ลิงก์ chip แบบไม่มีนามสกุลในแถบซีรีส์อาศัยการ resolve ของ GitHub Pages และเป็นข้อจำกัด preview ที่มีอยู่เดิม ไม่ใช่ปัญหาในโค้ด Node
เปลี่ยนหนึ่งอย่างแล้วทดสอบ
ออกแบบ attack case สำหรับ provider จริงสามข้อที่ไม่มีใน fixture สี่รายการ ระบุ invariant ที่วัดได้กับวิธีตรวจเฉลยแต่ละข้อ เก็บเลขคณิตไว้ในโค้ด และแยกโมเดลตอบผิดออกจาก guard เสีย เพราะโมเดลอาจผิดแต่ effect boundary ยังทำงานถูก
ทำการแก้ไขในสำเนาทดลอง เก็บไฟล์ต้นฉบับไว้เทียบ แล้วคืนค่าก่อนเริ่มตอนถัดไป หากทดลองกับ provider จริงภายหลัง ให้สร้าง experiment record ใหม่แทนเขียนทับผลเดิม
จุดตรวจถัดไป
ก่อนขยับต่อ ให้บอกได้ว่าไฟล์ใดเป็น contract ไฟล์ใดเป็น policy หลักฐานใดได้จากการรัน และข้อใดยังไม่ได้ทดสอบกับโมเดลจริง การแยกสี่อย่างนี้เป็นงานวิศวกรรมที่สำคัญกว่าการได้เดโมที่ดูไหลลื่น
แหล่งอ้างอิงและหลักฐาน
เอกสารและ source ที่ใช้
ตรวจเอกสาร Jev วันที่ 23 ตุลาคม 2026 ข้อเท็จจริง API และราคาอาจเปลี่ยน ให้ตรวจใหม่ก่อนใช้ live ส่วนผลของ fixture เป็นข้อมูลสอนที่ระบุไว้ในโครงการ
สรุปที่นำไปใช้ต่อ
- contract จำกัดโครงสร้าง ส่วน evaluation ตรวจพฤติกรรม
- อ่าน coverage คุณภาพ เวลา และต้นทุนรวมควบคู่กัน
- ข้อเสนอไม่ใช่ผลที่อนุมัติแล้ว และหลักฐาน mock ไม่ใช่หลักฐานโมเดล live
Part 8 of ten: Exercise hostile inputs, malformed responses and provider failures, keeping inference retries separate from effect retries.
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 attack suite should test the boundary you actually own. Our offline suite can prove that an invalid admin route, a forged approval claim and a failed provider do not directly create a simulated effect. It cannot show that Jev recognizes a prompt injection or remains accurate under reordered options; those questions require real model responses on a reviewed evaluation set.
The official Jev 1.13 notes describe difficulties with numerical precision, irrelevant state, adversarial content and option order. We keep arithmetic and permissions in deterministic code and treat tickets as data rather than instructions. The teaching fixtures deliberately include hostile requests so you can see where the guard works even when a semantic answer is wrong.
Inference retries and effect retries solve different problems. A transient inference failure may require another provider attempt, which has latency and possible cost. Repeating an already approved action must instead respect an idempotency key. The SDK may retry internally; our live wrapper does not add an outer retry loop or claim visibility into those hidden attempts.
Seven practical steps
Step 1 — Run the four attack fixtures
Run the four attack fixtures. Keep their synthetic-evidence label visible. T-037 and T-038 try to dictate routing or approval through ticket text. The canned outputs are not measurements of attack success against Jev; inspect the eventual proposal and approval gate separately.
Step 2 — Run T-040 in Jev-only mode
Run T-040 in Jev-only mode. The unsupported label must be refused. This isolates response-validation behavior from hybrid fallback. A test that only exercises hybrid may hide the original failure behind a second provider that happens to return a valid label.
Step 3 — Inject a provider exception through the pipeline test
Inject a provider exception through the pipeline test. With both providers unavailable the final result must need review and contain no action. Error handling should preserve a useful reason without printing API keys or treating an unavailable model as a negative answer.
Step 4 — Map HTTP failure classes before live use
Map HTTP failure classes before live use. Authentication and schema failures require correction, not blind retries. Rate and overload failures may be retried by the SDK. Keep the application-level retry budget explicit and confirm version-specific behavior in the official documentation instead of layering another retry library by habit.
Step 5 — Specify metamorphic tests for the real provider
Specify metamorphic tests for the real provider. Reorder route options, append irrelevant paragraphs, paraphrase the same intent and keep numerical calculations outside the model. Freeze expected invariants before collecting responses. The current fixture provider is indexed by ticket ID and intentionally cannot measure these transformations.
Step 6 — Keep Thai and English failure slices separate
Keep Thai and English failure slices separate. Passing a few Thai fixture strings proves encoding and control flow only. Real Thai evaluation needs representative language, code-switching and domain vocabulary, annotated by competent reviewers. Do not infer parity with English from a shared JSON schema.
Step 7 — Run the effect tests again after changing failure handling
Run the effect tests again after changing failure handling. An exception path that starts a fallback should not duplicate an approved action or bypass the human request condition. Preserve the original attempt record even when a later branch produces the final proposal.
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 · test/pipeline.test.mjs · test/network.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.
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 evaluate --split attack --save
node src/cli.mjs demo --mode jev --ticket T-040
node --test --test-reporter=spec test/pipeline.test.mjs test/effects.test.mjs test/network.test.mjs
Verify the expected behavior
The attack report must contain n=4 and remain labeled as a fixture simulation. The T-040 Jev-only demonstration must have action:null. Provider-failure and forged-approval tests must pass without credentials or network access.
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
A successful mock attack test is not a successful model robustness test. If you see duplicate paid work, inspect whether SDK and application retries are stacked. If an error string includes a secret, remove the secret from logging and handle credential exposure through your normal process before resuming.
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
Design three real-provider attack cases that are absent from the four canned fixtures. For each, state a measurable invariant and the label-review process. Keep numerical arithmetic in code and distinguish a model mistake from a guard failure. A model may be wrong while the effect boundary still behaves correctly.
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.