Paxa Labs
บทความทั้งหมด

25 สิงหาคม 2569 · อ่าน 2 นาที

ภาษาไทยไม่เว้นวรรคระหว่างคำ และมันเปลี่ยนวิธีที่ API แปลภาษาต้องทำงาน

ทำไมการที่ภาษาไทยไม่มีช่องว่างระหว่างคำจึงกำหนดวิธีรักษาโครงสร้างมาร์กอัป วิธีจับคู่อภิธานศัพท์ และจุดที่การตัดข้อความแบบง่าย ๆ ผิดพลาด

ประโยคภาษาไทยไม่เว้นวรรคระหว่างคำ ผมกินข้าวที่ร้านอาหารไทย คือหกคำสำหรับคนอ่าน และเป็นโทเคนเดียวสำหรับ split(' ') เรื่องนี้ไม่ได้แปลกประหลาด ภาษาจีนและภาษาญี่ปุ่นก็เขียนแบบเดียวกัน และประชากรราวหนึ่งในห้าของโลกเขียนโดยไม่มีตัวคั่นที่ซอฟต์แวร์ภาษาอังกฤษสันนิษฐานไว้เงียบ ๆ ตั้งแต่โปรแกรมแก้ไขข้อความตัวแรก

สิ่งที่ควรบันทึกไว้คือการที่ตัวคั่นหายไปเพียงอย่างเดียวส่งผลลึกเข้าไปใน API แค่ไหน มันไม่ได้จำกัดอยู่แค่ตัวตัดคำ แต่เป็นตัวกำหนดว่าจะรักษามาร์กอัปไว้อย่างไร จะจับคู่อภิธานศัพท์อย่างไร ตัวแบ่งข้อความทำอะไรได้บ้าง และต้นทุนของคำขอถูกตัดสินตรงจุดไหน การตัดสินใจสี่ข้อใน /v1/translate มาจากเรื่องนี้ และทุกข้อเคยถูกทำผิดทางมาก่อน

เส้นแบ่งคำอยู่ตรงไหนกันแน่#

การตัดคำภาษาไทยเป็นโจทย์เชิงโมเดล คลังคำอย่างเดียวตัดสินไม่ได้ ตัวอย่างที่ยกกันบ่อยคือ ตากลม ซึ่งอ่านได้ทั้ง ตา กลม และ ตาก ลม ขึ้นอยู่กับประโยครอบข้าง ไม่มีตัวคั่นให้ดู และไม่มีการเปิดพจนานุกรมแบบใดที่ตัดสินได้ เพราะทั้งสองการอ่านเป็นคำจริงทั้งคู่

หนึ่งสายอักขระ สองการตัดคำ
ตากลม

  ตา  กลม     ดวงตาที่กลม
  ตาก ลม      ผึ่งให้แห้ง

ไม่มีช่องว่าง ไม่มีเครื่องหมาย
พจนานุกรมตอบไม่ได้ ประโยครอบข้างเป็นผู้ตัดสิน

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

มาร์กอัป และเหตุผลที่ `format` เป็นฟิลด์หนึ่ง#

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

ในภาษาไทยวิธีนี้พังตั้งแต่แท็กในบรรทัดแรก ตำแหน่งที่ขยับไปหนึ่งตัวอักษรขยับเข้าไปกลางคำ และคำที่ถูกผ่าก็กลายเป็นสายอักขระสองชุดที่ไม่ใช่คำ ความล้มเหลวนี้เงียบ ผลลัพธ์เป็น HTML ที่ถูกต้องตามรูปแบบแต่มีเนื้อความที่ไม่มีความหมาย และไม่มีใครสังเกตจนกว่าคนอ่านภาษาไทยจะเห็น

ตำแหน่งที่คลาดไปหนึ่งตัวอักษร
ต้นฉบับ    ราคา <b>พิเศษ</b> วันนี้
ถอดแท็ก    ราคา พิเศษ วันนี้

ใส่กลับที่ตำแหน่ง 5 คลาดไปหนึ่ง:
          ราคา <b>พ</b>ิเศษ วันนี้

แท็กเปิดคั่นระหว่างพยัญชนะกับสระของมัน
คำถูกทำลาย แต่มาร์กอัปยังถูกต้องอยู่

/v1/translate จึงมีฟิลด์ format เป็น text markdown หรือ html และโมเดลได้รับตัวมาร์กอัปนั้นไปเลย การรักษาโครงสร้างไว้ต้องอาศัยการรู้ว่าคำอยู่ตรงไหน และโมเดลคือฝ่ายเดียวในไปป์ไลน์ที่รู้

หมายเหตุ

นี่คือเหตุผลที่ฟิลด์นี้ไม่ได้มีไว้เพื่อความสะดวก ผู้เรียกที่ถอดแท็กแล้วใส่กลับเอง กำลังคำนวณตำแหน่งบนสายอักขระที่เขามองไม่เห็นหน่วยของมัน และ API ก็ตรวจจับไม่ได้ว่าเขาทำแบบนั้น

อภิธานศัพท์ที่ใช้เส้นแบ่งคำไม่ได้#

จุดเชื่อมต่อนี้รับอภิธานศัพท์ คือคู่ต้นทางและปลายทางที่ผู้เรียกอยากให้แปลออกมาแบบเดิมทุกครั้ง การส่งอภิธานทั้งชุดไปในทุกคำขอย่อมสิ้นเปลือง ระบบจึงตัดคำที่ต้นทางไม่มีทางปรากฏในข้อความออกก่อน ตั้งแต่ก่อนคิดเงินและก่อนส่งต่อขึ้นไป

การตัดต้องอาศัยการตัดสินว่าคำหนึ่งปรากฏในข้อความหรือไม่ การทดสอบมาตรฐานคือการจับคู่ที่ขอบคำ \bterm\b และมันใช้ไม่ได้ที่นี่ \b นิยามไว้กับชุดอักขระของคำที่อักษรไทยไม่ได้เข้าร่วมอย่างมีประโยชน์ การยืนยันนี้จึงไม่ทำงานเลยหรือทำงานทุกที่ ขึ้นอยู่กับเอนจิน และภาษาที่การทดสอบนี้พังก็คือภาษาที่ API แปลภาษาไทยให้บริการอยู่พอดี

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

การปรับรูปเหตุผลสิ่งที่ต้องแลก
รวมตัวพิมพ์ใหญ่เล็กอภิธานที่พิมพ์ iPhone ควรจับคู่กับข้อความที่พิมพ์ IPHONEไม่มี
ถอดเครื่องหมายเสริมสัทอักษรอภิธานเป็น café ต้นทางเป็น caferesumé กับ resume ถูกรวมเป็นตัวเดียว
ß เป็น ssต้นฉบับภาษาเยอรมันสะกดทั้งสองแบบในทางปฏิบัติไม่มี
รวมอัญประกาศและขีดแบบสวยงามคำที่คัดมาจากเอกสารออกแบบใช้ ส่วนต้นทางใช้ 'ไม่มี
ตัดปัจจัยละตินหนึ่งชั้นออกจากคำในอภิธานอภิธานเก็บรูปพหูพจน์ licenses ควรจับคู่กับรูปเอกพจน์การผันที่เปลี่ยนรากศัพท์จะพลาด

กฎเรื่องปัจจัยทำงานทางเดียว และความไม่สมมาตรนี้คือส่วนที่น่าสนใจ การตัดปัจจัยออกจากคำในอภิธานทำให้ขอบเขตที่จับคู่ได้กว้างขึ้น ส่วนการเติมปัจจัยเข้าไปจะเป็นโค้ดที่ไม่มีวันทำงาน เพราะ คำ + ปัจจัย ย่อมมีคำนั้นอยู่ข้างในเสมอ การตรวจแบบมีสายอักขระอยู่ข้างในจึงครอบคลุมไปแล้ว การเขียนทิศทางที่สองเป็นความผิดพลาดที่พบบ่อย และได้ฟังก์ชันที่ดูรอบคอบแต่ไม่ทำอะไรเลย

ขีดจำกัดที่เราระบุไว้ตรง ๆ คือการผันรูปที่เปลี่ยนรากศัพท์ อภิธานที่เก็บ run จะไม่จับคู่กับ ran และการคำนวณปัจจัยเท่าใดก็ทำให้จับคู่ได้ ทางออกมีสองทางสำหรับกรณีที่สำคัญจริง คือตั้ง always: true ที่คำนั้นเพื่อส่งไปทุกครั้ง และส่ง glossary_mode: "all" เพื่อส่งทั้งชุด

การตัดคำในอภิธานเป็นตัวกำหนดบิล มันจึงทำงานในตำแหน่งที่ตายตัว#

ข้อมูลอ้างอิงคิดค่าบริการในอัตราของตัวเอง และอภิธานศัพท์ก็เป็นข้อมูลอ้างอิง ระบบคิดเงินเฉพาะคำที่ส่งไปจริง การตัดคำจึงมีสถานะเป็นตัวกำหนดยอดที่คิด ไม่ได้เป็นเพียงการปรับแต่งประสิทธิภาพที่วางอยู่ข้าง ๆ

เรื่องนี้บังคับลำดับการทำงาน การตัดอภิธานทั้งชุดคือ CPU แบบซิงโครนัสก้อนเดียวของคำขอ วัดได้ 20 ถึง 45 มิลลิวินาทีที่เพดานของสัญญา และคำขอที่กำลังจะถูกปฏิเสธต้องไม่เสียเวลานั้น เพดานข้อความ การตรวจความพร้อมให้บริการ และการรับเข้าจึงทำงานก่อน แล้วค่อยจับคู่อภิธาน ส่วนการปฏิเสธด้วย request_too_large ที่มาถึงหลังการตัด จะคืนสิทธิ์การประมวลผลที่มันถือไว้ชั่วครู่

ลำดับในเราต์ และเหตุผล
1  เพดานข้อความ        ปฏิเสธก่อนใช้ CPU
2  ความพร้อมให้บริการ   ปฏิเสธก่อนตัดเครดิต
3  การรับเข้า          จำกัดอัตรา สิทธิ์ ล็อก
4  ตัดคำในอภิธาน       20-45ms งานจริง
5  เพดานขนาด           วัดหลังการตัด
6  ตัดเครดิต           บนยอดหลังการตัด
7  ประมวลผล

การตัดอยู่หลังการรับเข้าเพราะมันแพง
และอยู่ก่อนเพดานเพราะสิ่งที่มันผลิตออกมา
คือสิ่งที่เพดานเอาไปวัด

ผลลัพธ์รายงานส่วนประกอบกลับมาให้ ทั้ง usage.credits พร้อมกับ meta.text_chars meta.glossary_terms (ที่ส่งไป) และ meta.glossary_available (ที่แนบมา) หากไม่มีข้อมูลเหล่านี้ ผู้เรียกก็แยกอภิธานที่ใหญ่ออกจากอภิธานที่แพงไม่ได้ และจะไปปรับตัวเลขคนละตัวกับที่เขากำลังจ่ายอยู่

การแบ่งท่อน และเหตุผลที่โครงสร้างคือเส้นแบ่งเดียวที่ซื่อตรง#

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

ไม่มีทางแก้ราคาถูก เพราะเส้นแบ่งที่ตัวแบ่งต้องการคือสิ่งที่ภาษาไม่ได้ทำเครื่องหมายไว้ สิ่งที่มีอยู่แทนคือโครงสร้างที่ตัวเอกสารมีอยู่แล้ว หัวข้อคือเส้นแบ่ง รายการคือเส้นแบ่ง แถวของตารางคือเส้นแบ่ง และไม่มีอันไหนที่ต้องรู้ว่าคำจบตรงไหน

นี่คือเหตุผลที่จุดเชื่อมต่อ OCR คืนค่าเป็น Markdown ที่ยังมีหัวข้อครบ และเป็นเหตุผลที่จุดเชื่อมต่อแปลภาษารับข้อความเป็นท่อน ทั้งสองอย่างมอบเส้นแบ่งที่เอกสารมีอยู่แล้วให้ผู้เรียก และไม่มีอันไหนขอให้ใครสร้างเส้นแบ่งขึ้นมาเอง

สิ่งนี้หมายความว่าอย่างไรกับผู้เรียกใช้#

  • ส่งฟิลด์ format ทุกครั้งที่ข้อความมีมาร์กอัป และอย่าถอดมาร์กอัปเอง
  • ใส่ชื่อแบรนด์ ชื่อสินค้า และชื่อสถานที่ลงในอภิธานศัพท์ ในรูปเดียวกับที่คุณเผยแพร่จริง
  • ส่งหลายท่อนไปในคำขอเดียว ยอดขั้นต่ำต่อคำขอจะถูกคิดครั้งเดียว และโมเดลมองทั้งชุดเป็นบริบทเดียวกัน
  • แบ่งข้อความยาวตามหัวข้อและรายการที่เอกสารมีอยู่แล้ว
  • อ่าน meta.glossary_terms เทียบกับ meta.glossary_available ก่อนสรุปว่าอภิธานแพงเกินไป

ทั้งหมดนี้ไม่ได้เจาะจงกับ API ของเรา มันคือราคาของการสร้างโครงสร้างพื้นฐานด้านข้อความให้ภาษาที่ไม่เว้นวรรคระหว่างคำ และราคานั้นจ่ายครั้งเดียวตอนออกแบบ จากนั้นผลลัพธ์ก็เลิกจ่ายมัน