DEV Community

Cover image for วิธีทดสอบ AI Agent ที่ไม่เป็นเชิงกำหนด (เมื่อ temperature=0 ไม่เพียงพอ)
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

วิธีทดสอบ AI Agent ที่ไม่เป็นเชิงกำหนด (เมื่อ temperature=0 ไม่เพียงพอ)

การทดสอบของคุณผ่านเมื่อวันจันทร์ ด้วยอินพุตเดิม โค้ดเดิม และ temperature=0 แต่เมื่อวันอังคารมันกลับล้มเหลว ทั้งที่คุณไม่ได้เปลี่ยนอะไรเลย assertion ตรวจสอบสตริงที่ตรงกันเป๊ะ แต่โมเดลตอบด้วยถ้อยคำที่ต่างออกไปเล็กน้อย การทดสอบขึ้นสีแดง แม้เอเจนต์ยังทำงานได้ดี และคุณต้องดีบั๊กชุดทดสอบแทนผลิตภัณฑ์

ลองใช้ Apidog วันนี้

นี่คือภาระของการทดสอบระบบที่เรียกใช้โมเดลภาษา: ผลลัพธ์เปลี่ยนแปลงได้ แม้ตั้งค่าให้สุ่มน้อยที่สุดแล้วก็ตาม temperature=0 ไม่ได้รับประกันว่าทุกการรันจะได้คำตอบเหมือนกันทุกไบต์ บทความนี้อธิบายวิธีเขียน assertion ที่ทนต่อความเปลี่ยนแปลงของข้อความ โดยเน้นการตรวจสอบสัญญา (contract) ของผลลัพธ์แทนการตรวจถ้อยคำตรงตัว นี่คือการเจาะลึกโหมดความล้มเหลวที่สามจากคู่มือ เหตุใด AI agents จึงพังในการใช้งานจริง

ทำไม temperature=0 จึงไม่ได้หมายถึง deterministic

Temperature ควบคุมวิธีที่โมเดลสุ่มเลือกโทเค็นถัดไป ที่ค่า 0 โมเดลจะเลือกโทเค็นที่มีความน่าจะเป็นสูงสุด แต่ไม่ได้หมายความว่าผลลัพธ์จะทำซ้ำได้แบบบิตต่อบิต

สาเหตุสำคัญอยู่ในสแตกการให้บริการโมเดล:

  • Floating-point math บน GPU ไม่เป็น associative: การบวกตัวเลขชุดเดิมในลำดับต่างกันอาจให้ผลต่างกันเล็กน้อย
  • ความต่างระดับทศนิยมอาจทำให้อันดับของโทเค็นเปลี่ยน
  • ผู้ให้บริการอาจ batch คำขอของคุณร่วมกับทราฟฟิกอื่น ใช้ฮาร์ดแวร์คนละชุด หรืออัปเดต kernel และ inference library
  • โมเดลหรือ weights อาจถูก re-quantize หรือ route ไปยัง region อื่น

การสนทนา vLLM นี้ อธิบายว่าการใช้ seed คงที่ร่วมกับ temperature=0 ยังไม่เพียงพอสำหรับ bitwise reproducibility

สรุปคือ determinism เป็นคุณสมบัติของสแตกทั้งหมด ไม่ใช่แค่พารามิเตอร์ใน request ของคุณ ดังนั้นอย่าตั้งสมมติฐานว่าคำตอบต้องเหมือนเดิมทุกตัวอักษร ให้ทดสอบสิ่งที่ต้องคงเดิมแทน

ทำไมการ assert สตริงตรงตัวจึงทำให้เทสต์ flaky

กับดักที่พบบ่อยคือ:

expect(response).toBe("Your order total is $42.00.");
Enter fullscreen mode Exit fullscreen mode

การรันแรกผ่าน แต่โมเดลอาจตอบว่า:

Your total comes to $42.00.
Enter fullscreen mode Exit fullscreen mode

ความหมายยังถูกต้อง แต่เทสต์ล้มเหลว

เทสต์ที่ล้มเหลวทั้งที่ระบบถูกต้องอันตรายกว่าไม่มีเทสต์ เพราะทีมจะเริ่มไม่เชื่อผลการทดสอบ รันซ้ำจนกว่าจะผ่าน และสุดท้ายอาจมองข้าม regression จริงไปท่ามกลาง false positive

เราเคยอธิบาย สาเหตุของการทดสอบที่เปราะบาง ไว้แล้ว และ non-deterministic output คือหนึ่งในสาเหตุที่สร้าง flaky test ได้เร็วที่สุด

แนวทางที่ถูกต้องไม่ใช่การล็อก output ให้แน่นขึ้นด้วย snapshot ของข้อความทั้งหมด แต่คือการลดการพึ่งพาถ้อยคำ แล้วตรวจสอบโครงสร้าง ความหมาย และข้อจำกัดที่วัดได้

ยืนยันตามโครงสร้างและความหมาย ไม่ใช่ข้อความที่ตรงกันเป๊ะ

เอเจนต์สนับสนุนลูกค้าอาจยืนยันการคืนเงินได้หลายร้อยรูปแบบ แต่คำตอบที่ถูกต้องควรมีข้อเท็จจริงเดียวกัน เช่น:

  • จำนวนเงินคืน
  • รหัสคำสั่งซื้อ
  • สถานะการคืนเงิน

ให้ทดสอบข้อเท็จจริงเหล่านี้ ไม่ใช่ประโยคที่โมเดลเลือกใช้

เปลี่ยนคำถามจาก:

โมเดลพูดประโยคนี้ตรงตัวหรือไม่?

เป็น:

คำตอบมีรูปแบบ ฟิลด์ ประเภทข้อมูล และค่าที่ถูกต้องหรือไม่?

ด้านล่างคือ assertion ที่นำไปใช้ได้จริง

ตรวจสอบคำตอบด้วย JSON Schema

หากเอเจนต์คืนข้อมูลแบบมีโครงสร้าง ให้กำหนด JSON Schema และตรวจสอบทุก response เทียบกับ schema

ตัวอย่าง schema สำหรับผลลัพธ์การคืนเงิน:

{
  "type": "object",
  "required": ["order_id", "status", "amount"],
  "properties": {
    "order_id": {
      "type": "string",
      "pattern": "^ORD-[0-9]+$"
    },
    "status": {
      "type": "string",
      "enum": ["refunded", "pending", "denied"]
    },
    "amount": {
      "type": "number",
      "minimum": 0
    }
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

จากนั้น assert กับ response โดยใช้ JSON Schema validator เช่น Ajv:

import Ajv from "ajv";

const ajv = new Ajv();
const validate = ajv.compile(refundSchema);

const valid = validate(response.body);

expect(valid).toBe(true);

if (!valid) {
  console.error(validate.errors);
}
Enter fullscreen mode Exit fullscreen mode

วิธีนี้ตรวจจับความผิดพลาดที่สำคัญได้ เช่น:

  • โมเดลลืมส่งฟิลด์ที่จำเป็น
  • amount กลายเป็น string แทน number
  • status มีค่าที่ระบบไม่รองรับ
  • response กลายเป็นข้อความบรรยายแทน JSON
  • โครงสร้าง object ซ้อนผิดตำแหน่ง

คุณสามารถโหลด schema ของ response ลงใน Apidog และตรวจสอบ response จริงของเอเจนต์เทียบกับสัญญานี้ได้ ความล้มเหลวจะระบุฟิลด์ที่ผิดโดยตรง แทนที่จะรายงาน diff ของสตริงยาวหลายร้อยตัวอักษร

ยืนยัน tool call ตามชื่อเป้าหมายและ schema

เมื่อเอเจนต์เลือกเรียกใช้เครื่องมือ ให้ทดสอบ tool call ไม่ใช่ข้อความที่นำไปสู่การเรียกใช้

ตรวจสอบอย่างน้อย 3 อย่าง:

  1. เลือกเครื่องมือถูกต้อง
  2. เรียก endpoint หรือ action ถูกต้อง
  3. payload ตรงกับ schema ของเครื่องมือ

ตัวอย่างสำหรับเอเจนต์จองที่พัก:

expect(toolCall.name).toBe("create_reservation");
expect(toolCall.method).toBe("POST");
expect(toolCall.path).toBe("/reservations");

expect(toolCall.arguments).toMatchObject({
  guests: expect.any(Number),
  date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/)
});
Enter fullscreen mode Exit fullscreen mode

หาก payload เป็น JSON ให้ใช้ schema ตรวจสอบต่อ:

{
  "type": "object",
  "required": ["guests", "date"],
  "properties": {
    "guests": {
      "type": "integer",
      "minimum": 1
    },
    "date": {
      "type": "string",
      "format": "date"
    }
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

แนวคิดนี้เหมือนกับการตรวจสอบ response contract แต่ใช้กับ request ที่เอเจนต์ส่งออกไป อ่านเพิ่มได้ในบทความ วิธีการทดสอบการเรียกใช้ API ของเอเจนต์แบบ end-to-end

ใช้ช่วงตัวเลขแทนค่าที่ตายตัว

สำหรับตัวเลขที่โมเดลสร้างหรือส่งต่อ ให้ assert ช่วงที่ยอมรับได้ แทนค่าที่ต้องตรงเป๊ะ

ตัวอย่าง: เอเจนต์ตะกร้าสินค้าคำนวณยอดรวม คุณอาจไม่ต้องการผูกเทสต์กับยอดเดียว แต่รู้ว่าผลลัพธ์ต้องอยู่ระหว่าง 0 ถึงยอดสูงสุดที่เป็นไปได้

const maxTotal = cartSubtotal + maxShipping + maxTax;

expect(response.body.total).toBeGreaterThanOrEqual(0);
expect(response.body.total).toBeLessThanOrEqual(maxTotal);
Enter fullscreen mode Exit fullscreen mode

Assertion แบบนี้ยังตรวจจับข้อผิดพลาดสำคัญได้ เช่น:

  • ยอดรวมติดลบ
  • ยอดรวมสูงเกินจริง
  • ตะกร้าที่มีสินค้าแต่คืนยอดรวมเป็นศูนย์

ใช้แนวคิดเดียวกันกับ:

  • confidence score
  • จำนวนรายการ
  • token usage
  • latency budget
  • จำนวน tool calls
  • ค่าที่ได้จากการคำนวณ

เลือกขอบเขตที่กว้างที่สุดซึ่งยังตรวจจับความผิดพลาดจริงได้

ตรวจสอบฟิลด์ที่ต้องมี และห้ามมีฟิลด์ต้องห้าม

มี assertion ง่าย ๆ สองแบบที่สำคัญมาก:

  1. ฟิลด์ที่จำเป็นต้องมีอยู่และไม่เป็น null
  2. ฟิลด์ที่ไม่ควรเปิดเผยต้องไม่มีอยู่

ตัวอย่าง response ของระบบจัดการ ticket:

expect(response.body).toHaveProperty("resolution");
expect(response.body.resolution).not.toBeNull();

expect(response.body).not.toHaveProperty("internal_notes");
expect(response.body).not.toHaveProperty("raw_prompt");
Enter fullscreen mode Exit fullscreen mode

การตรวจสอบนี้ไม่สนใจว่าข้อความใน resolution ถูกเขียนอย่างไร แต่ยังป้องกัน regression ที่สำคัญ รวมถึงการเปิดเผยข้อมูลภายในหรือข้อมูลส่วนบุคคลโดยไม่ตั้งใจ

ตรวจสอบข้อความอิสระด้วยคุณสมบัติและเกณฑ์เชิงความหมาย

บางกรณี response จำเป็นต้องเป็นข้อความอิสระ เช่น คำตอบที่ส่งให้ผู้ใช้โดยตรง การจับคู่สตริงตรงตัวไม่เหมาะกับกรณีนี้

เริ่มจาก assertion ที่วัดได้ก่อน:

expect(response.text).toContain(orderId);
expect(response.text.length).toBeLessThanOrEqual(500);
expect(response.text).not.toMatch(/internal policy|raw prompt/i);
Enter fullscreen mode Exit fullscreen mode

คุณอาจตรวจสอบเพิ่มว่า response:

  • มีหมายเลขคำสั่งซื้อที่ส่งเข้ามา
  • ไม่เกินความยาวที่กำหนด
  • ไม่มีวลีต้องห้าม
  • มีลิงก์หรือข้อมูลติดต่อที่จำเป็น
  • ไม่เปิดเผยข้อมูลภายใน

หากต้องตรวจสอบความหมายจริง ๆ ให้เปรียบเทียบ embedding ของคำตอบกับคำตอบอ้างอิง และ assert ว่าคะแนน similarity ผ่านเกณฑ์:

expect(similarityScore).toBeGreaterThanOrEqual(0.85);
Enter fullscreen mode Exit fullscreen mode

แต่ semantic check ควรเป็นเพียงด่านหยาบ เพราะอาจตรวจจับคำตอบที่หลุดประเด็นได้ แต่ไม่รับประกันความถูกต้องของข้อเท็จจริงที่ละเอียดอ่อน ควรใช้ร่วมกับ schema validation และ assertion เชิงโครงสร้างเสมอ

สแนปช็อตช่วงค่า ไม่ใช่ข้อความทั้งหมด

Snapshot test ยังมีประโยชน์ หากสแนปช็อตเฉพาะส่วนที่เสถียร:

  • ชุดของคีย์
  • ประเภทข้อมูล
  • ค่า enum
  • รูปแบบของ ID
  • ช่วงของตัวเลข
  • ฟิลด์ที่อนุญาตและต้องห้าม

แทนที่จะ snapshot response ทั้งก้อน:

expect(response.body).toMatchSnapshot();
Enter fullscreen mode Exit fullscreen mode

ให้ snapshot โครงสร้างที่ normalize แล้ว:

expect({
  keys: Object.keys(response.body).sort(),
  status: response.body.status,
  hasOrderId: /^ORD-\d+$/.test(response.body.order_id),
  amountInRange:
    response.body.amount >= 0 &&
    response.body.amount <= maxRefundAmount
}).toMatchSnapshot();
Enter fullscreen mode Exit fullscreen mode

เมื่อ snapshot ล้มเหลว คุณจะเห็นการเปลี่ยนแปลงเชิงโครงสร้างที่ควรตรวจสอบจริง ไม่ใช่ความต่างจากคำพ้องความหมาย

สถานะและหน่วยความจำทำให้การทดสอบยากขึ้น

แนวทางข้างต้นเริ่มจากสมมติฐานว่ามี request หนึ่งครั้งและ response หนึ่งครั้ง แต่เอเจนต์จริงมักมีหน่วยความจำและสถานะข้ามหลายรอบสนทนา

ผลลัพธ์ของเอเจนต์แบบมีสถานะอาจขึ้นอยู่กับ:

  • เอกสารที่ retrieval เลือกมา
  • ข้อมูลที่ถูกบันทึกใน memory ก่อนหน้า
  • ลำดับของข้อความในบทสนทนา
  • สรุปที่ถูกสร้างระหว่างรอบ
  • ผลลัพธ์ของ tool call ก่อนหน้า

ดังนั้นการรันบทสนทนาเดียวกันสองครั้งอาจต่างกันได้ทั้งจาก non-determinism ของโมเดลและจาก initial state ที่ไม่เหมือนกัน อ่านภาพรวมได้จาก วิธีการทำงานของหน่วยความจำ AI agent

ใช้ 2 แนวทางต่อไปนี้:

1. รีเซ็ต state ก่อนทุกเทสต์

กำหนด memory และข้อมูลเริ่มต้นให้อยู่ในสถานะที่รู้แน่ชัดก่อนเริ่มแต่ละ test case

beforeEach(async () => {
  await resetAgentMemory({
    conversationId: testConversationId
  });

  await seedTestData();
});
Enter fullscreen mode Exit fullscreen mode

เมื่อ state คงที่ คุณจะกำลังทดสอบการเปลี่ยนแปลงเพียงตัวแปรเดียว ไม่ใช่หลายตัวแปรพร้อมกัน

2. Assert ค่าคงที่ที่ไม่ขึ้นกับเส้นทาง

สำหรับ workflow หลายรอบ ให้ตรวจสอบ invariant ที่ต้องเป็นจริงเสมอ เช่น:

expect(finalBalance).toBeGreaterThanOrEqual(0);
expect(createdReservations).toHaveLength(1);
expect(paymentAttempts).toBeLessThanOrEqual(1);
Enter fullscreen mode Exit fullscreen mode

ไม่ว่าเอเจนต์จะใช้กี่รอบสนทนา เลือกเครื่องมืออะไรระหว่างทาง หรืออธิบายเหตุผลด้วยคำใด ผลลัพธ์สุดท้ายต้องไม่ละเมิดกฎธุรกิจเหล่านี้

จำลอง dependencies เพื่อให้เทสต์ทำซ้ำได้

อย่าพึ่ง API ของบุคคลที่สามแบบ production ใน test suite โดยตรง เพราะ API เหล่านั้นอาจ:

  • ถูก rate limit
  • เปลี่ยนข้อมูล
  • ล่มหรือ timeout
  • ส่งผลต่างกันตามเวลา
  • เพิ่มแหล่งความไม่แน่นอนอีกชั้นหนึ่ง

ให้ตรึงทุกอย่างที่ไม่ใช่พฤติกรรมที่คุณกำลังทดสอบ

ตัวอย่าง:

  • Payment API คืนใบเสร็จเดิมทุกครั้ง
  • Search API คืนผลลัพธ์สามรายการเดิม
  • Inventory API คืนสถานะสินค้าคงที่
  • Weather API คืนข้อมูลที่กำหนดไว้สำหรับ test case
mockPaymentApi.onPost("/charge").reply(200, {
  transaction_id: "txn_test_001",
  status: "approved",
  amount: 42.0
});
Enter fullscreen mode Exit fullscreen mode

เมื่อ dependency ถูกควบคุม สิ่งที่ยังเปลี่ยนได้หลัก ๆ คือการตัดสินใจของเอเจนต์ ซึ่งเป็นสิ่งที่คุณต้องการสังเกตจริง ๆ

Mock ยังช่วยให้คุณบังคับกรณีสุดขีดได้ เช่น:

mockInventoryApi.onGet("/items/sku-123").reply(200, {
  sku: "sku-123",
  available: 0
});
Enter fullscreen mode Exit fullscreen mode

จากนั้นตรวจสอบว่าเอเจนต์ไม่สร้างคำสั่งซื้อสำหรับสินค้าที่หมด:

expect(toolCalls).not.toContainEqual(
  expect.objectContaining({
    name: "create_order"
  })
);
Enter fullscreen mode Exit fullscreen mode

ใช้ Apidog เพื่อจำลอง API dependencies ด้วย response ที่เสถียรและควบคุมได้ แล้วใช้ร่วมกับ schema validation เพื่อยืนยันสัญญาของ request และ response แนวทางนี้เป็นส่วนหนึ่งของ การทดสอบ AI แบบเอเจนต์ ที่ใช้ mock และ assertion ร่วมกัน

Apidog เหมาะกับอะไร และไม่เหมาะกับอะไร

ควรแยกขอบเขตของเครื่องมือให้ชัดเจน

Apidog เป็นแพลตฟอร์มสำหรับออกแบบ ทดสอบ และจำลอง API ไม่ใช่:

  • agent framework
  • model host
  • agent runtime
  • orchestration platform
  • ระบบประเมิน reasoning
  • observability platform สำหรับโมเดล

Apidog ไม่ได้สร้างหรือรันเอเจนต์ของคุณ แต่เหมาะกับเลเยอร์ API ที่เอเจนต์สื่อสารด้วย โดยเฉพาะงานต่อไปนี้:

  1. เขียน assertion สำหรับ API response ของเอเจนต์

    • JSON Schema validation
    • response format
    • numeric range
    • required และ forbidden fields
    • tool-call payload schema
  2. จำลอง API dependencies เพื่อให้ test run ทำซ้ำได้

ช่องว่างที่ Apidog เติมเต็มคือการตรวจสอบสัญญาของ request และ response ไม่ใช่การควบคุมโมเดลที่สร้างข้อมูลเหล่านั้น

ทดสอบสัญญา ไม่ใช่ถ้อยคำ

Non-determinism ไม่ใช่บั๊กที่แก้ได้ด้วยการตั้งค่าเดียว และ temperature=0 ก็ไม่ได้ปิดมัน ทีมที่สร้างเอเจนต์ให้เชื่อถือได้จะไม่พยายามบังคับให้ข้อความเหมือนเดิมทุกครั้ง

ให้ทดสอบสิ่งที่ควรคงที่:

  • schema
  • รูปแบบข้อมูล
  • ช่วงตัวเลข
  • ค่า enum
  • ฟิลด์ที่จำเป็น
  • ฟิลด์ที่ห้ามเปิดเผย
  • tool call และ payload
  • invariant ของกฎธุรกิจ

เมื่อทำเช่นนี้ test suite จะขึ้นสีเขียวแม้โมเดลเปลี่ยนถ้อยคำ และจะขึ้นสีแดงเมื่อเกิดความเสียหายจริง

สัปดาห์นี้ ลองเลือก assertion แบบ brittle หนึ่งรายการจากชุดทดสอบของคุณ แล้วเปลี่ยนให้เป็น schema validation ร่วมกับ range check จากนั้นใช้ Apidog เพื่อตรวจสอบ response contract และจำลอง dependencies ที่ทำให้เทสต์ของคุณทำซ้ำได้

Top comments (0)