ในบริบทที่เทคโนโลยีโมเดลขนาดใหญ่กำลังเฟื่องฟู นักพัฒนาและองค์กรต่างเผชิญกับความท้าทายทางเทคนิคร่วมกัน นั่นคือ จะทำอย่างไรให้สามารถเชื่อมต่อ 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-typescrate เพื่อรวมชนิดข้อมูลการไหลของทั้งระบบ (เช่น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
本文来自网络搜集,不代表คลื่นสร้างอนาคต立场,如有侵权,联系删除。转载请注明出处:https://www.itsolotime.com/th/archives/34715
