หนึ่ง SSE stream กับเจ็ดผู้ให้บริการ LLM

ผมสร้างแอป Next.js ที่รองรับผู้ให้บริการ LLM ที่แตกต่างกันถึงเจ็ดราย

OpenAI, Claude, Gemini, Ollama, Mistral, Groq และ Azure

ผมตั้งกฎเหล็กไว้หนึ่งข้อคือ: เบราว์เซอร์ต้องใช้เส้นทางการทำงานของโค้ด (code path) แบบเดียวกันเป๊ะสำหรับทุกผู้ให้บริการ

เรื่องนี้เป็นเรื่องยากเพราะ API เหล่านี้ไม่เหมือนกัน พวกมันใช้วิธีการรับส่งข้อมูล (transport methods) ที่ต่างกัน ส่งรูปแบบข้อมูล (data shapes) ที่ต่างกัน บางเจ้าใช้ SSE ในขณะที่บางเจ้าใช้ NDJSON

หากคุณปล่อยให้ความแตกต่างเหล่านี้หลุดไปถึง UI โค้ดของคุณจะกลายเป็นกองคำสั่ง "if" ที่ยุ่งเหยิง และทุกครั้งที่คุณเพิ่มผู้ให้บริการใหม่ Frontend ของคุณก็จะซับซ้อนขึ้นเรื่อยๆ

ผมแก้ปัญหานี้ด้วยการสร้าง "สัญญา" (contract) เพียงหนึ่งเดียว โดยผู้ให้บริการทุกรายต้องส่งข้อมูลในรูปแบบนี้ไปยังเบราว์เซอร์:

• data: {"delta":""} • data: {"error":""} • data: [DONE]

เบราว์เซอร์จำเป็นต้องเข้าใจเพียงสามอย่างเท่านั้นคือ: delta, error และ [DONE]

นี่คือวิธีที่ผมสร้างมันขึ้นมา:

  1. ใช้ Async Generators ผมจัดการกับผู้ให้บริการแต่ละรายในฐานะ generator ที่ส่งค่า (yield) เป็นข้อความธรรมดา วิธีนี้จะช่วยซ่อนความซับซ้อนของ API ไว้

  2. รูปแบบ Wrapper (The Wrapper Pattern) ผมสร้างฟังก์ชัน wrapper ที่ชื่อว่า createSSEStream เพื่อจัดการกับรูปแบบการรับส่งข้อมูล (wire format) และยังช่วยให้มั่นใจว่า stream จะจบลงเสมอ แม้ว่าผู้ให้บริการจะเกิดข้อผิดพลาดระหว่างทาง wrapper ก็จะส่ง error และสัญญาณ [DONE] ออกมา เพื่อป้องกันไม่ให้ฝั่ง client ค้าง

  3. การจัดกลุ่ม API ที่คล้ายกัน OpenAI, Mistral, Groq และ Azure ใช้รูปแบบ (dialect) เดียวกัน ผมจึงเขียน implementation เพียงชุดเดียวสำหรับทุกเจ้า การเพิ่มผู้ให้บริการใหม่ที่รองรับรูปแบบนี้จึงใช้โค้ดเพียงบรรทัดเดียวเท่านั้น

  4. การจัดการกับกรณีพิเศษ (Handling Outliers) Anthropic และ Ollama ทำงานต่างออกไป Anthropic ใช้ typed events เฉพาะเจาะจง ส่วน Ollama ใช้ NDJSON ผมจึงเขียน parser แยกสำหรับพวกมัน แต่ทั้งคู่จะส่งค่า (yield) เป็นข้อความเข้าไปยัง wrapper ตัวเดียวกัน ทำให้เบราว์เซอร์ไม่เห็นความแตกต่างเลย

ความเป็นส่วนตัวและความเรียบง่าย แอปนี้ใช้โมเดล "Bring Your Own Key"

• ผู้ใช้ใส่ API key ของตนเอง • คีย์จะถูกเก็บไว้ใน local storage • เซิร์ฟเวอร์ทำหน้าที่เป็นเพียง proxy เท่านั้น • คีย์จะไม่ถูกเก็บไว้ในฐานข้อมูลเลย

แนวทางนี้ช่วยลดความจำเป็นในการจัดการระบบยืนยันตัวตน (auth) หรือการจัดการความลับ (secret management) ที่ซับซ้อน และทำให้แอปสามารถนำไป self-host ได้ง่าย

บทเรียนนี้เรียบง่ายมาก: ให้จำลอง (model) การเชื่อมต่อแต่ละอย่างเป็น generator ของสิ่งที่คุณต้องการจริงๆ จากนั้นจึงทำการ wrap มันไว้เพียงครั้งเดียว ปล่อยให้ความแตกต่างต่างๆ อยู่ในส่วนของ generator เพื่อให้ logic หลักของคุณยังคงสะอาดและเป็นระเบียบ

Source: https://dev.to/ikeli0320/one-sse-stream-seven-llm-providers-giving-a-nextjs-app-a-single-streaming-code-path-1fh2

Optional learning community: https://t.me/GyaanSetuAi