Rust สร้างแพลตฟอร์มพลังประมวลผล AI KeyCompute: เส้นทางอัจฉริยะ คิดค่าบริการแม่นยำ พร้อมจัดสรรพลังประมวลผลจากพีซีส่วนตัว

ในบริบทที่เทคโนโลยีโมเดลขนาดใหญ่กำลังเฟื่องฟู นักพัฒนาและองค์กรต่างเผชิญกับความท้าทายทางเทคนิคร่วมกัน นั่นคือ จะทำอย่างไรให้สามารถเชื่อมต่อ API แบบรวมศูนย์ จัดการทราฟฟิกอย่างชาญฉลาด คิดค่าบริการตามการใช้งานได้อย่างแม่นยำ และกู้คืนจากความผิดพลาดโดยอัตโนมัติ ระหว่างผู้ให้บริการหลายราย เช่น OpenAI, Claude, Gemini, DeepSeek เป็นต้น

โซลูชัน API Gateway ที่มีอยู่ในปัจจุบัน มักเขียนด้วย Python หรือ Node.js ซึ่งมีประสิทธิภาพไม่ดีนัก หรือขาดการจัดการวงจรชีวิตของการประมวลผลแบบสตรีมอย่างมีประสิทธิภาพ ที่สำคัญกว่านั้นคือ แทบไม่มีโซลูชันใดเลยที่นำทรัพยากรการคำนวณของพีซีส่วนบุคคลเข้ามาอยู่ในระบบการจัดสรรแบบรวมศูนย์

  • keycompute/keycompute: KeyCompute คือแพลตฟอร์มบริการรุ่นใหม่ที่มุ่งเน้นไปที่พลังประมวลผล AI Token มีคุณสมบัติเด่นคือประสิทธิภาพสูง ขยายได้ง่าย และพร้อมใช้งานทันที
  • ที่อยู่โปรเจกต์: https://github.com/keycompute/keycompute
  • บทความประมาณ 7,000 คำ คาดว่าใช้เวลาอ่าน 20 นาที พอดแคสต์ประกอบยาว 24 นาที

โค้ดของ KeyCompute 94.7% เป็นภาษา Rust สร้างระบบบริการพลังประมวลผล AI ที่ครอบคลุมกระบวนการทั้งหมดตั้งแต่ “การตัดสินใจเส้นทาง → การดำเนินการแบบสตรีม → การคิดค่า Token → การจัดสรรโหนด” ขึ้นมาจากศูนย์

บทความนี้จะเจาะลึกถึงสถาปัตยกรรมหลักของมัน โดยสำรวจว่าภายใต้ปรัชญาการออกแบบที่เน้นความปลอดภัยของชนิดข้อมูล (Type Safety) และการแยกส่วนแบบไร้ต้นทุน (Zero-Cost Abstraction) ของ Rust นั้น KeyCompute สามารถรับมือกับความท้าทายทางวิศวกรรมแบบ Full-Stack ในการให้บริการ AI Token ได้อย่างไร

KeyCompute รองรับโมเดลหลากหลาย ผู้ใช้สามารถเข้าถึงโมเดลใหญ่หลักๆ ทั้งหมดผ่านรูปแบบ API OpenAI มาตรฐาน ให้ประสบการณ์ที่พร้อมใช้งานทันที

สารบัญ

  • เริ่มต้นใช้งานอย่างรวดเร็ว
  • หนึ่ง ภาพรวมสถาปัตยกรรม
    • 1.1 วงจรชีวิตเจ็ดขั้นตอนของการเริ่มต้นบริการ
  • สอง LLM Gateway: การจัดการวงจรชีวิตแบบสตรีมของเลเยอร์การดำเนินการเพียงหนึ่งเดียว
    • 2.1 ห่วงโซ่ Retry-Fallback ของตัวดำเนินการ
    • 2.2 ท่อส่งการประมวลผลเหตุการณ์แบบสตรีม
    • 2.3 การนับ Token ที่แม่นยำด้วย tiktoken
  • สาม เอ็นจิ้นการกำหนดเส้นทางสองชั้นและการตอบสนองสถานะสุขภาพ
    • 3.1 เลเยอร์ 1: การจัดลำดับผู้ให้บริการอัจฉริยะ
    • 3.2 เลเยอร์ 2: กลุ่มบัญชีผู้เช่าและกลไกการคูลดาวน์
    • 3.3 การกำหนดเส้นทางโหนด: ช่องทางอิสระสำหรับพีซีส่วนบุคคล
    • 3.4 วงจรป้อนกลับสถานะสุขภาพ
  • สี่ การคิดค่าบริการภายหลัง: การออกแบบบัญชีแยกประเภทหลักที่ไม่สามารถเปลี่ยนแปลงได้
  • ห้า Node Gateway: ทำให้พีซีส่วนบุคคลกลายเป็นโหนดประมวลผล
  • หก นามธรรม Trait ของอะแดปเตอร์ผู้ให้บริการ
  • สรุป
    • 1. ความปลอดภัยของชนิดข้อมูลทั่วทั้งสแต็ก
    • 2. โมเดลอะซิงโครนัสแบบแยกส่วนไร้ต้นทุน
    • 3. การสังเกตการณ์ได้ในฐานะพลเมืองชั้นหนึ่ง
    • 4. การจัดเก็บแบบเข้ารหัสและการออกแบบความปลอดภัย

เริ่มต้นใช้งานอย่างรวดเร็ว

สำหรับผู้อ่านที่ต้องการทดลองใช้ KeyCompute อย่างรวดเร็ว เพียงสามขั้นตอนก็สามารถเริ่มต้นบริการพลังประมวลผล AI ที่สมบูรณ์ในเครื่องของคุณได้ ซึ่งรวมถึงฐานข้อมูล PostgreSQL, แคช Redis, บริการ API ฝั่งแบ็กเอนด์ และแผงจัดการฝั่งฟรอนต์เอนด์ กระบวนการทั้งหมดอาศัย Docker Compose สำหรับการจัดเตรียมแบบคลิกเดียว โดยไม่จำเป็นต้องติดตั้ง Rust toolchain ด้วยตนเอง:

โคลนโปรเจกต์

git clone https://github.com/keycompute/keycompute.git
cd keycompute

กำหนดค่าตัวแปรสภาพแวดล้อม

cp .env.example .env

แก้ไข .env กรอกรหัสผ่านฐานข้อมูล, คีย์ JWT ฯลฯ (ดูรายละเอียดในความคิดเห็นภายในไฟล์)

เริ่มบริการทั้งหมดด้วยคลิกเดียว (PostgreSQL + Redis + Server + Web)

docker compose up -d

ตรวจสอบสถานะบริการ

docker compose ps

หลังจากเริ่มต้น ให้เข้าไปที่ http://localhost:8080 บัญชีผู้ดูแลระบบเริ่มต้นคือ admin@keycompute.local รหัสผ่านถูกกำหนดไว้ในไฟล์ .env สำหรับขั้นตอนเพิ่มเติมเกี่ยวกับการตั้งค่าสภาพแวดล้อมการพัฒนา การเริ่มต้นฟรอนต์เอนด์แบบอิสระ ฯลฯ โปรดดูที่ README.md

เมื่อบริการพร้อมแล้ว การเรียกใช้โมเดล AI ก็เพียงใช้รูปแบบ OpenAI มาตรฐาน ซึ่งหมายความว่าโค้ดไคลเอ็นต์ OpenAI SDK ที่มีอยู่ทั้งหมดของคุณไม่จำเป็นต้องแก้ไขใดๆ เพียงแค่ชี้ base_url ไปที่ KeyCompute เท่านั้น:

curl http://localhost:3000/v1/chat/completions
-H “Authorization: Bearer sk-your-key”
-H “Content-Type: application/json”
-d ‘{“model”:”gpt-4″,”messages”:[{“role”:”user”,”content”:”Hello!”}],”stream”:true}’

หากคุณต้องการกำหนดเส้นทางคำขอไปยังโมเดลท้องถิ่น Ollama บนพีซีส่วนตัว เพียงเติมคำนำหน้า node: หน้าชื่อโมเดล:

curl http://localhost:3000/v1/chat/completions
-H “Authorization: Bearer sk-your-key”
-H “Content-Type: application/json”
-d ‘{“model”:”node:deepseek-chat”,”messages”:[{“role”:”user”,”content”:”Hello!”}]}’

หนึ่ง ภาพรวมสถาปัตยกรรม

KeyCompute ใช้โครงสร้างโปรเจกต์แบบ Rust Workspace monorepo โดยแบ่งระบบทั้งหมดออกเป็น 18 crate อิสระ หลักการออกแบบหลักประกอบด้วย “ข้อจำกัดของเลเยอร์การดำเนินการเพียงหนึ่งเดียว” “การคิดค่าบริการภายหลัง” และ “การขับเคลื่อนด้วยชนิดข้อมูล” กระบวนการเริ่มต้นบริการเป็นไปตามขั้นตอนการเริ่มต้นเจ็ดขั้นตอนอย่างเคร่งครัด

สถาปัตยกรรมระบบที่ออกแบบมาอย่างดี มักจะทำให้นักพัฒนามองเห็นปรัชญาการออกแบบเบื้องหลังได้เพียงแค่ดูจากโครงสร้างไดเรกทอรี KeyCompute แบ่งระบบทั้งหมดออกเป็น 18 crate ที่ยึดหลัก “การรวมกันสูง การเชื่อมต่อกันต่ำ” แต่ละ crate มุ่งเน้นไปที่ความรับผิดชอบเดียว และสื่อสารกันผ่าน trait และสัญญาชนิดข้อมูลที่กำหนดไว้อย่างชัดเจน:

keycompute/
├── crates/
│ ├── keycompute-server/ # จุดเข้า HTTP ของ Axum
│ ├── llm-gateway/ # เลเยอร์การดำเนินการเพียงหนึ่งเดียว: retry/fallback/streaming
│ ├── keycompute-routing/ # เอ็นจิ้นการกำหนดเส้นทางอัจฉริยะสองชั้น
│ ├── keycompute-billing/ # การคิดค่าบริการภายหลังที่แม่นยำ
│ ├── keycompute-pricing/ # เอ็นจิ้นการกำหนดราคา
│ ├── node-gateway/ # เกตเวย์โหนดพีซีส่วนบุคคล
│ ├── keycompute-auth/ # การรับรองความถูกต้อง (JWT + Argon2 password hashing)
│ ├── keycompute-ratelimit/ # การจำกัดอัตราแบบกระจาย (Redis backend เป็นตัวเลือก)
│ ├── keycompute-runtime/ # สถานะรันไทม์ + การเข้ารหัส API Key
│ ├── keycompute-distribution/ # เอ็นจิ้นการจัดจำหน่ายแบบสองระดับ
│ ├── keycompute-observability/# การสังเกตการณ์ได้ (Prometheus + tracing)
│ ├── keycompute-config/ # การโหลดการกำหนดค่า (TOML + ตัวแปรสภาพแวดล้อม)
│ ├── keycompute-emailserver/ # บริการอีเมล (รหัสยืนยัน + รีเซ็ตรหัสผ่าน)
│ ├── keycompute-payment/ # การชำระเงิน (Alipay + WeChat)
│ ├── llm-provider/ # ชุดอะแดปเตอร์ผู้ให้บริการ
│ │ ├── keycompute-openai/ # อินเทอร์เฟซ OpenAI/ที่เข้ากันได้
│ │ ├── keycompute-claude/ # Anthropic Claude
│ │ ├── keycompute-gemini/ # Google Gemini
│ │ ├── keycompute-deepseek/ # DeepSeek
│ │ ├── keycompute-ollama/ # โมเดลท้องถิ่น Ollama
│ │ └── keycompute-vllm/ # vLLM โฮสต์เอง
│ └── …
└── packages/ # ฟรอนต์เอนด์ Dioxus 0.7 (Web + Desktop + Mobile)

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

หลักการออกแบบหลักประกอบด้วยสามประการ:

  • ข้อจำกัดทางสถาปัตยกรรม (Architectural Constraint) — มีเพียง llm-gateway เท่านั้นที่ได้รับอนุญาตให้เริ่มต้นคำขอต้นทาง เอ็นจิ้นการกำหนดเส้นทางเป็นแบบอ่านอย่างเดียว และโมดูลการคิดค่าบริการเป็นแบบภายหลัง ขอบเขตของ “ใครทำอะไรได้” นี้ถูกบังคับใช้ในเวลาคอมไพล์ผ่านการควบคุมการมองเห็นของ crate
  • การคิดค่าบริการภายหลัง (Post-billing) — โมดูลการคิดค่าบริการไม่มีส่วนร่วมในการตัดสินใจเส้นทาง ไม่หักยอดคงเหลือล่วงหน้า ไม่บล็อกกระบวนการจัดการคำขอ และจะดำเนินการชำระเงินที่แม่นยำเพียงครั้งเดียวหลังจากที่การตอบสนองแบบสตรีมสิ้นสุดลงอย่างสมบูรณ์
  • การขับเคลื่อนด้วยชนิดข้อมูล (Type-driven) — ผ่าน keycompute-types crate เพื่อรวมชนิดข้อมูลการไหลของทั้งระบบ (เช่น RequestContext, ExecutionPlan, StreamEvent) เพื่อให้แน่ใจว่าสัญญาระหว่างโมดูลสามารถตรวจสอบได้ในเวลาคอมไพล์

1.1 วงจรชีวิตเจ็ดขั้นตอนของการเริ่มต้นบริการ

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

// ที่มา: crates/keycompute-server/src/main.rs

[tokio::main]

async fn main() -> anyhow::Result<()> {
// ขั้นตอนที่ 1: โหลดการกำหนดค่า (ตัวแปรสภาพแวดล้อม + ไฟล์ TOML โดยตัวแปรสภาพแวดล้อมมีลำดับความสำคัญสูงกว่า)
let config = AppConfig::load()?;
config.validate()?;

// ขั้นตอนที่ 2: เริ่มต้นการสังเกตการณ์ได้ (สภาพแวดล้อมการพัฒนาใช้ pretty-print, สภาพแวดล้อมการผลิตใช้ JSON structured logging)
init_observability();

// ขั้นตอนที่ 3: เริ่มต้นการเข้ารหัสทั่วโลก (สำหรับการจัดเก็บ API Key แบบเข้ารหัส AES)
init_global_crypto(&config)?;

// ขั้นตอนที่ 4: การเชื่อมต่อฐานข้อมูล + การย้ายข้อมูลอัตโนมัติ (sqlx จัดการเวอร์ชัน schema)
let db = Database::new(&db_config).await?;
db.migrate().await?;

// ขั้นตอนที่ 5: เริ่มต้นผู้ดูแลระบบเริ่มต้น + การตั้งค่าระบบ
initialize_default_admin(&pool).await?;

// ขั้นตอนที่ 6: สร้างสถานะแอปพลิเคชัน (ฉีดโมดูลการกำหนดเส้นทาง เกตเวย์ การคิดค่าบริการ ฯลฯ เข้าไปใน AppState)
let app_state = AppState::with_pool_and_config(pool, state_config);

// ขั้นตอนที่ 7: เริ่มต้นเซิร์ฟเวอร์ HTTP (รองรับการปิดอย่างนุ่มนวลด้วย SIGINT/SIGTERM)
tokio::select! {
result = run(server_config, app_state) => { // }
_ = shutdown => { info!(“กำลังปิดอย่างนุ่มนวล…”); }
}
}

  • การเริ่มต้นการเข้ารหัสทั่วโลกในขั้นตอนที่ 3 สมควรได้รับความสนใจเป็นพิเศษ — KeyCompute ใช้อัลกอริทึม AES ในการเข้ารหัส API Key ของผู้ให้บริการต้นทางเพื่อจัดเก็บ ซึ่งหมายความว่าถึงแม้ฐานข้อมูลจะถูกขโมย ผู้โจมตีก็ไม่สามารถรับ Key ในรูปแบบข้อความธรรมดาได้โดยตรง KC__CRYPTO__SECRET_KEY เมื่อใช้เขียนข้อมูลแล้วจะไม่สามารถเปลี่ยนได้ มิฉะนั้นข้อมูลในประวัติจะไม่สามารถถอดรหัสได้ — นี่คือการแลกเปลี่ยนที่ผ่านการคิดมาอย่างดีระหว่างความปลอดภัยและความสามารถในการบำรุงรักษา
  • การจัดการการปิดอย่างนุ่มนวลในขั้นตอนที่ 7 ก็ควรค่าแก่การสังเกตเช่นกัน: ระบบจะฟังสัญญาณ SIGINT (Ctrl+C) และ SIGTERM (ส่งโดย container orchestrator) พร้อมกัน เมื่อได้รับสัญญาณ ระบบจะไม่ฆ่าโพรเซสอย่างรุนแรง แต่จะรอให้คำขอแบบสตรีมที่กำลังดำเนินอยู่สิ้นสุดลงตามธรรมชาติก่อนจึงจะออก

สอง LLM Gateway: การจัดการวงจรชีวิตแบบสตรีมของเลเยอร์การดำเนินการเพียงหนึ่งเดียว

GatewayExecutor เป็นโมดูลเดียวในระบบที่สามารถเริ่มต้นคำขอ HTTP ต้นทางได้ มันใช้กลไก tokio::spawn ร่วมกับ mpsc::channel แบบมีขอบเขตเพื่อควบคุม Backpressure แบบสตรีม นอกจากนี้ ด้วยความช่วยเหลือของ tokenizer o200k_base ในไลบรารี tiktoken-rs มันยังให้ความสามารถในการนับ Token ที่แม่นยำซึ่งสอดคล้องกับมาตรฐาน OpenAI อย่างสมบูรณ์

llm-gateway เป็น crate ที่สำคัญที่สุดในสถาปัตยกรรมทั้งหมด หากเปรียบ KeyCompute เป็นระบบปฏิบัติการ GatewayExecutor ก็ทำหน้าที่เป็น “เลเยอร์การเรียกของระบบ” — การโต้ตอบทั้งหมดกับโลกภายนอก (ผู้ให้บริการ LLM ต้นทาง) จะต้องผ่านเลเยอร์นี้ รูปแบบการออกแบบ “ทางออกเดียว” นี้ดูเหมือนจะเพิ่มข้อจำกัด แต่จริงๆ แล้วช่วยลดความซับซ้อนในการตรวจสอบความปลอดภัย การแก้ไขปัญหา และการตรวจสอบประสิทธิภาพของระบบได้อย่างมาก

หน้าที่หลักของ Gateway สามารถสรุปได้เป็นสี่คำ: ดำเนินการ, ลองใหม่, ลดระดับ, วัดปริมาณ

2.1 ห่วงโซ่ Retry-Fallback ของตัวดำเนินการ

เมื่อเอ็นจิ้นการกำหนดเส้นทางสร้าง ExecutionPlan (ซึ่งประกอบด้วยเป้าหมายหลักหนึ่งเป้าหมายและเป้าหมายสำรองหลายเป้าหมาย) ตัวดำเนินการจะรับผิดชอบในการลองตามลำดับจนกว่าคำขอใดคำขอหนึ่งจะสำเร็จหรือความพยายามทั้งหมดล้มเหลว:

// ที่มา: crates/llm-gateway/src/executor.rs  
pub async fn execute(  
&self,  
ctx: Arc<RequestContext>,  
plan: ExecutionPlan,       // ประกอบด้วย primary + fallback_chain  
account_states: Arc<AccountStateStore>,  
provider_health: Option<Arc<ProviderHealthStore>>,  
) -> Result<mpsc::Receiver<StreamEvent>> {  
let (tx, rx) = mpsc::channel(100);  

// การออกแบบที่สำคัญ: ดำเนินการใน tokio::spawn เบื้องหลัง และส่งคืน rx ทันที  
// เพื่อหลีกเลี่ยงการบล็อก Backpressure ของ channel แบบมีขอบเขต — handler ต้องได้รับ rx ก่อนจึงจะบริโภคได้  
tokio::spawn(async move {  
runner.run_plan(ctx, plan, tx, account_states, provider_health).await;  
});  

Ok(rx)  
}  

มีการตัดสินใจทางวิศวกรรมที่ชาญฉลาดที่น่าสมควรแก่การเจาะลึก: ทำไมเมธอด execute ถึง ส่งคืน mpsc::Receiver ทันที และให้การเรียกต้นทางจริงดำเนินการใน task เบื้องหลัง?

คำตอบอยู่ที่กลยุทธ์การจัดการ Backpressure แบบสตรีม

ลองนึกภาพสถานการณ์นี้: ความจุของ channel คือ 100 ผู้ให้บริการต้นทางกำลังส่ง chunk events ด้วยความเร็วสูง หาก HTTP handler ยังไม่ได้เริ่มเขียนข้อมูล SSE ไปยังไคลเอ็นต์ (เช่น มันกำลังรอให้ execute ส่งคืน) channel จะเต็มอย่างรวดเร็ว ทำให้ tx.send() ของผู้ผลิตต้องรอ (hang) ที่ .await — ทำให้เกิด deadlock

โดยการใช้ tokio::spawn วางผู้ผลิตไว้ใน task ที่เป็นอิสระ execute สามารถส่งคืน rx ได้ทันที เมื่อ handler ได้รับ rx แล้วก็สามารถเริ่มบริโภคได้ทันที ทำให้การผลิตและการบริโภคทำงานคู่ขนานกัน ปัญหา deadlock ก็จะหมดไป

ภายใน run_plan ตรรกะของ Fallback ได้รับการออกแบบให้เรียบง่ายและมีประสิทธิภาพ:

// ที่มา: crates/llm-gateway/src/executor.rs (แบบง่าย)  
async fn run_plan(&self, ctx: Arc<RequestContext>, plan: ExecutionPlan, ...) -> Result<()> {  
// สร้างห่วงโซ่เป้าหมาย: primary อยู่ข้างหน้า fallback อยู่ข้างหลัง  
let mut targets = vec![plan.primary];  
targets.extend(plan.fallback_chain);  

let mut last_error = None;  
let mut is_primary = true;  

for target in targets {  
let target_start = Instant::now();  
match self.try_execute(&ctx, &target, tx.clone()).await {  
Ok(()) => {  
// สำเร็จ: บันทึกสถานะสุขภาพ (ความหน่วง + เป็น fallback หรือไม่)  
let latency_ms = target_start.elapsed().as_millis() as u64;  
health_store.record_success(provider, latency_ms);  
if !is_primary { health_store.record_fallback(); }  
return Ok(());  
}  
Err(e) => {  
// ล้มเหลว: บันทึกเหตุการณ์ล้มเหลว ลองเป้าหมายถัดไป  
health_store.record_failure(provider);  
last_error = Some(e);  
}  
}  
is_primary = false; // หลังจากนี้เป็น fallback ทั้งหมด  
}  

Err(last_error.unwrap_or_else(|| KeyComputeError::RoutingFailed(ctx.model.clone())))  
}  

แนวคิดหลักที่นี่คือ: ความล้มเหลวไม่ใช่จุดสิ้นสุด แต่เป็นสัญญาณในการเปลี่ยนช่องทาง เฉพาะเมื่อเป้าหมายทั้งหมด (รวมถึง primary และ fallback ทั้งหมด) ล้มเหลว คำขอถึงจะถือว่าล้มเหลวอย่างแท้จริง สิ่งนี้ทำให้ระบบมีคุณสมบัติความพร้อมใช้งานสูงแบบ “N+M redundancy”

2.2 ท่อส่งการประมวลผลเหตุการณ์แบบสตรีม

ตรรกะการดำเนินการจริงของแต่ละ target ถูกห่อหุ้มไว้ภายในฟังก์ชัน try_execute โค้ดด้านล่างแสดงให้เห็นอย่างชัดเจนว่า KeyCompute จัดการวงจรชีวิตของเหตุการณ์สตรีม SSE อย่างสมบูรณ์ได้อย่างไร:

// ที่มา: crates/llm-gateway/src/executor.rs (แบบง่าย)
async fn try_execute(&self, ctx: &RequestContext, target: &ExecutionTarget, tx: mpsc::Sender<StreamEvent>) -> Result<()> {
// 1. รับอะแดปเตอร์ผู้ให้บริการ (ใช้ trait object เพื่อ实现 polymorphism)
let provider_impl = self.providers.get(provider)?;

// 2. เลือกเลเยอร์การขนส่ง HTTP (優先使用內部 HTTP Proxy 的連接池)
let transport: Arc<dyn HttpTransport> = if let Some(ref proxy) = self.http_proxy {
Arc::clone(proxy.default_client())
} else {
Arc::new(DefaultHttpTransport::new())
};

// 3. เริ่มต้นคำขอแบบสตรีม รับ Stream แบบอะซิงโครนัส
let mut stream = provider_impl.stream_chat(transport.as_ref(), request).await?;

// 4. ประมวลผลทีละเหตุการณ์ — นี่คือลูปหลักของท่อส่งแบบสตรีม
while let Some(event) = stream.next().await {
match event? {
StreamEvent::Delta { content, finish_reason } => {
// คำนวณจำนวน token เอาต์พุตแบบเรียลไทม์
let tokens = Self::estimate_tokens(&content);
ctx.add_output_tokens(tokens);
// ส่งต่อไปยังไคลเอ็นต์
tx.send(event).await?;
}
StreamEvent::Usage { input_tokens, output_tokens } => {
// ปริมาณการใช้งานที่แน่นอนที่รายงานโดยผู้ให้บริการ — แทนที่ค่าประมาณในเครื่อง
ctx.set_input_tokens(input_tokens);
}
StreamEvent::Done => { tx.send(StreamEvent::Done).await?; break; }
StreamEvent::Error { message } => return Err(ProviderError(message)),
_ => {}
}
}
Ok(())
}

โปรดสังเกตวิธีการจัดการกับเหตุการณ์ StreamEvent::Usage โดยเฉพาะ: ในระหว่างการส่งแบบสตรีม KeyCompute จะใช้ tiktoken เพื่อประมาณจำนวน token แบบเรียลไทม์ อย่างไรก็ตาม หากผู้ให้บริการต้นทาง (เช่น OpenAI และ DeepSeek มักทำเช่นนี้) ส่งคืนข้อมูลการใช้งานที่แน่นอนเมื่อสิ้นสุดสตรีม ระบบจะใช้ข้อมูลจากผู้ให้บริการเป็นหลัก กลยุทธ์ “การประมาณในแง่ดี + การแก้ไขโดยผู้มีอำนาจ” นี้ก่อให้เกิดกลไกการประกันภัยสองชั้น

2.3 การนับ Token ที่แม่นยำด้วย tiktoken

ความแม่นยำของการนับ Token เป็นตัวกำหนดความแม่นยำของการคิดค่าบริการโดยตรง ในเรื่องนี้ KeyCompute ไม่มีการประนีประนอมใดๆ:

// ที่มา: crates/llm-gateway/src/executor.rs
fn estimate_tokens(content: &str) -> u32 {
if content.is_empty() { return 0; }
// ใช้ tokenizer o200k_base (tokenizer ทางการของซีรีส์ GPT-4o/o1/o3)
// รูปแบบ singleton: โหลดคำศัพท์ทั่วโลกเพียงครั้งเดียว การเรียกใช้ครั้งต่อๆ ไปไม่มีค่าใช้จ่าย
let bpe = tiktoken_rs::o200k_base_singleton();
bpe.encode_with_special_tokens(content).len() as u32
}

ทำไมไม่ใช้การประมาณแบบง่ายๆ ด้วย “จำนวนอักขระหารด้วย 4”? เพราะเมื่อต้องจัดการกับข้อความที่ไม่ใช่ภาษาละติน เช่น จีน ญี่ปุ่น การประมาณคร่าวๆ นี้อาจมีความคลาดเคลื่อนสูงถึง 50% หรือมากกว่า KeyCompute รวม tokenizer o200k_base จากไลบรารี tiktoken-rs โดยตรง ซึ่งเป็นคำศัพท์ทางการที่ใช้โดยโมเดลต่างๆ เช่น GPT-4o, o1, o3 การทำเช่นนี้ทำให้มั่นใจได้ว่าผลการคิดค่าบริการจะสอดคล้องกับค่าที่ส่งคืนโดย OpenAI API อย่างเป็นทางการ รูปแบบ singleton ช่วยให้มั่นใจว่าคำศัพท์จะถูกโหลดเพียงครั้งเดียวเมื่อใช้งานครั้งแรก และการเรียกใช้ในแต่ละครั้งหลังจากนั้นจะดำเนินการเฉพาะการเข้ารหัส BPE เท่านั้น ซึ่งมีค่าใช้จ่ายด้านประสิทธิภาพต่ำมาก

สาม เอ็นจิ้นการกำหนดเส้นทางสองชั้นและการตอบสนองสถานะสุขภาพ

เอ็นจิ้นการกำหนดเส้นทางใช้สถาปัตยกรรมสองชั้น: เลเยอร์ 1 (การจัดลำดับผู้ให้บริการ) + เลเยอร์ 2 (การเลือกบัญชี) การจัดลำดับผู้ให้บริการใช้สูตรคะแนนถ่วงน้ำหนัก: ต้นทุน×0.3 + ความหน่วง×0.25 + อัตราความสำเร็จ×0.25 + ระดับสุขภาพ×0.2 การเลือกบัญชีรองรับการจัดลำดับความสำคัญและกลไกการคูลดาวน์ การกำหนดเส้นทางโหนดจะทริกเกอร์เส้นทางอิสระผ่านคำนำหน้า node:

หาก Gateway คือ “มือ” ที่ปฏิบัติงาน RoutingEngine ก็คือ “สมอง” ที่ทำหน้าที่ตัดสินใจ ระบบการกำหนดเส้นทางของ KeyCompute ใช้สถาปัตยกรรมสองชั้นคือ การกำหนดเส้นทางระดับโมเดล + การกำหนดเส้นทางกลุ่มบัญชี และเป็นไปตามข้อจำกัดการออกแบบหลัก: เอ็นจิ้นการกำหนดเส้นทางเป็นแบบ อ่านอย่างเดียวและไม่มีผลข้างเคียง มันจะไม่แก้ไขสถานะใดๆ แต่จะตัดสินใจอย่างเหมาะสมที่สุดโดยอิงจากข้อมูล snapshot ปัจจุบันเท่านั้น:

// ที่มา: crates/keycompute-routing/src/lib.rs
//! เอ็นจิ้นการกำหนดเส้นทาง, การกำหนดเส้นทางสองชั้น, อ่านอย่างเดียวไม่มีผลข้างเคียง
//! ข้อจำกัดทางสถาปัตยกรรม: อ่านเฉพาะ Pricing และ snapshot สถานะ ไม่เขียนสถานะใดๆ

3.1 เลเยอร์ 1: การจัดลำดับผู้ให้บริการอัจฉริยะ

หน้าที่หลักของการกำหนดเส้นทางชั้นแรกคือ: สำหรับชื่อโมเดลที่กำหนด (เช่น gpt-4o) ให้จัดลำดับผู้ให้บริการทั้งหมดที่สามารถให้บริการโมเดลนั้นตาม “ความคุ้มค่าโดยรวม” กระบวนการจัดลำดับนี้ใช้ระบบการให้คะแนนแบบถ่วงน้ำหนักสี่มิติ:

// ที่มา: crates/keycompute-routing/src/lib.rs  
/// ค่าคงที่น้ำหนักการกำหนดเส้นทาง (ฮาร์ดโค้ด ไม่สามารถแก้ไขผ่านการกำหนดค่า)  
const COST_WEIGHT: f64 = 0.3;      // น้ำหนักต้นทุน 30%  
const LATENCY_WEIGHT: f64 = 0.25;  // น้ำหนักความหน่วง 25%  
const SUCCESS_WEIGHT: f64 = 0.25;  // น้ำหนักอัตราความสำเร็จ 25%  
const HEALTH_WEIGHT: f64 = 0.2;    // น้ำหนักระดับสุขภาพ 20%  
const UNHEALTHY_PENALTY: f64 = 100.0; // บทลงโทษเพิ่มเติมสำหรับการไม่แข็งแรง  

fn score_provider(&self, provider: &str, pricing: &PricingSnapshot) -> f64 {  
let cost_score = self.calculate_cost_score(pricing);       // ต้นทุนยิ่งสูง คะแนนยิ่งสูง  
let latency_score = /* ... */;  // ความหน่วงยิ่งสูง คะแนนยิ่งสูง  
let failure_score = /* ... */;  // อัตราล้มเหลวยิ่งสูง คะแนนยิ่งสูง  
let unhealthiness_score = /* ... */; // ยิ่งไม่แข็งแรง คะแนนยิ่งสูง  

// ค่าเฉลี่ยถ่วงน้ำหนัก + บทลงโทษการไม่แข็งแรง (คะแนนยิ่งต่ำยิ่ง優先)  
let weighted_score = (COST_WEIGHT * cost_score  
        + LATENCY_WEIGHT * latency_score  
        + SUCCESS_WEIGHT * failure_score  
        + HEALTH_WEIGHT * unhealthiness_score) / 1.0;  

weighted_score + unhealthy_penalty  
}  

เบื้องหลังสูตรการให้คะแนนนี้มีแนวคิดทางวิศวกรรมที่สำคัญ: ตัวชี้วัดทั้งหมดถูกทำให้เป็น “ค่าที่สูงขึ้นหมายถึงประสิทธิภาพที่แย่ลง” ดังนั้นผู้ให้บริการที่มีคะแนนสุดท้ายต่ำที่สุดจะถูกเลือกก่อน การออกแบบทิศทางที่เป็นหนึ่งเดียวกันนี้ช่วยหลีกเลี่ยงความสับสนทางตรรกะที่อาจเกิดขึ้นเมื่อผสมตัวชี้วัดเชิงบวกและเชิงลบ ผลรวมของน้ำหนักถูกจำกัดอย่างเคร่งครัดที่ 1.0 (รับประกันโดยการทดสอบหน่วย) ทำให้มั่นใจได้ว่าผลการให้คะแนนมีความสามารถในการตีความที่ดี

การให้คะแนนความหน่วงใช้กลยุทธ์แบบแบ่งช่วง แทนที่จะเป็นการแมปเชิงเส้นอย่างง่าย ซึ่งใกล้เคียงกับประสบการณ์จริงของผู้ใช้มากกว่า:

// ที่มา: crates/keycompute-routing/src/lib.rs  
fn calculate_latency_score(&self, latency_ms: u64) -> f64 {  
if latency_ms == 0    { 50.0 }   // ไม่มีข้อมูล ให้ค่ากลาง  
else if latency_ms < 100  { 10.0 }   // ดีเยี่ยม (<100ms)  
else if latency_ms < 300  { 30.0 }   // ดี (100-300ms)  
else if latency_ms < 1000 { 60.0 }   // ปานกลาง (300ms-1s)  
else                      { 90.0 }   // แย่ (>1s)  
}  

3.2 เลเยอร์ 2: กลุ่มบัญชีผู้เช่าและกลไกการคูลดาวน์

ภารกิจของการกำหนดเส้นทางชั้นที่สองคือ ในรายการผู้ให้บริการที่จัดลำดับแล้ว ให้เลือกบัญชีต้นทางที่ดีที่สุดสำหรับแต่ละผู้ให้บริการ ตรรกะการเลือกพิจารณาปัจจัยหลักสามประการ: การแยกผู้เช่า (确保不同租户使用独立的账号池), การจับคู่โมเดล (บัญชีต้องรอง


⚠️ หมายเหตุ: เนื้อหาได้รับการแปลโดย AI และตรวจสอบโดยมนุษย์ หากมีข้อผิดพลาดโปรดแจ้ง

☕ สนับสนุนค่ากาแฟทีมงาน

หากคุณชอบบทความนี้ สามารถสนับสนุนเราได้ผ่าน PromptPay

PromptPay QR
SCAN TO PAY WITH ANY BANK

本文来自网络搜集,不代表คลื่นสร้างอนาคต立场,如有侵权,联系删除。转载请注明出处:https://www.itsolotime.com/th/archives/34715

Like (0)
Previous 6 hours ago
Next 6 hours ago

相关推荐