在当前大模型技术蓬勃发展的背景下,企业和开发者普遍面临一个共同的技术难题:如何在 OpenAI、Claude、Gemini、DeepSeek 等多个服务提供商之间,实现统一的接口接入、智能化的流量调度、精确的用量计费以及自动化的故障恢复?
现有的 API 网关方案,要么采用 Python 或 Node.js 编写,性能表现欠佳;要么缺乏对流式处理生命周期的有效管理。更关键的是,几乎没有任何方案将个人 PC 的计算资源纳入统一的调度体系。
- keycompute/keycompute: KeyCompute 是一款面向 AI Token 算力的新一代服务平台,具备高性能、易于扩展和开箱即用的特点。
- 项目地址:https://github.com/keycompute/keycompute
- 全文约 7000 字,预计阅读时间 20 分钟,配套播客时长 24 分钟。
KeyCompute 的代码中 Rust 语言占比高达 94.7%,从零开始构建了一套覆盖“路由决策 → 流式执行 → Token 计费 → 节点调度”全流程的 AI 算力服务平台。
本文将深入剖析其核心架构,探讨它如何在 Rust 强调类型安全与零成本抽象的设计哲学下,成功应对 AI Token 服务中的全栈工程挑战。
KeyCompute 支持多种模型,用户可通过标准的 OpenAI API 格式访问所有主流大模型,实现开箱即用的体验。
本文目录
- 快速上手
- 一、架构总览
- 1.1 服务启动的七阶段生命周期
- 二、LLM Gateway:唯一执行层的流式生命周期管理
- 2.1 执行器的 Retry-Fallback 链
- 2.2 流式事件处理管道
- 2.3 tiktoken 精确 Token 计数
- 三、两层路由引擎与健康状态反馈
- 3.1 Layer1:Provider 智能排序
- 3.2 Layer2:租户账号池与冷却机制
- 3.3 Node 路由:个人 PC 的独立通道
- 3.4 健康状态的反馈闭环
- 四、后置计费:不可变主账本设计
- 五、Node Gateway:让个人 PC 成为算力节点
- 六、Provider 适配器的 Trait 抽象
- 总结
- 1. 类型安全贯穿全栈
- 2. 零成本抽象的异步模型
- 3. 可观测性作为一等公民
- 4. 加密存储与安全设计
快速上手
对于希望快速体验 KeyCompute 的读者,只需三个步骤即可在本地启动一套完整的 AI 算力服务——其中包括 PostgreSQL 数据库、Redis 缓存、后端 API 服务以及前端管理面板。整个过程依赖 Docker Compose 进行一键编排,无需手动安装 Rust 工具链:
克隆项目
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}’
如果您希望将请求路由到个人 PC 上的 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/ # Axum HTTP 服务入口
│ ├── llm-gateway/ # 唯一执行层:retry/fallback/streaming
│ ├── keycompute-routing/ # 两层智能路由引擎
│ ├── keycompute-billing/ # 后置精确计费
│ ├── keycompute-pricing/ # 定价引擎
│ ├── node-gateway/ # 个人 PC 节点网关
│ ├── keycompute-auth/ # 认证鉴权(JWT + Argon2 密码哈希)
│ ├── keycompute-ratelimit/ # 分布式限流(Redis 后端可选)
│ ├── keycompute-runtime/ # 运行时状态 + API Key 加密
│ ├── keycompute-distribution/ # 二级分销引擎
│ ├── keycompute-observability/# 可观测性(Prometheus + tracing)
│ ├── keycompute-config/ # 配置加载(TOML + 环境变量)
│ ├── keycompute-emailserver/ # 邮件服务(验证码 + 密码重置)
│ ├── keycompute-payment/ # 支付(支付宝 + 微信)
│ ├── llm-provider/ # 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 结构化日志)
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 算法对上游 Provider 的 API Key 进行加密存储。这意味着即便数据库被拖走,攻击者也无法直接获取明文的 Key。
KC__CRYPTO__SECRET_KEY一旦在写入数据后便不可更换,否则历史数据将无法解密——这是一个在安全性与可维护性之间经过深思熟虑的权衡。 - 阶段 7 的优雅关闭处理同样值得注意:系统会同时监听
SIGINT(Ctrl+C)和SIGTERM(容器编排器发送)信号。收到信号后,系统不会粗暴地杀死进程,而是等待正在处理的流式请求自然结束后才退出。
二、LLM Gateway:唯一执行层的流式生命周期管理
好的,作为一名资深技术文章主编和高级“文章改写”专家,我将严格遵守您设定的所有规则,对提供的文章片段进行深度重写与降重。
以下是重写后的 Markdown 文本:
GatewayExecutor 是系统中唯一能够发起上游 HTTP 请求的模块。它利用 tokio::spawn 结合有界 mpsc::channel 机制,实现了流式背压控制。此外,借助 tiktoken-rs 库中的 o200k_base 分词器,它能够提供与 OpenAI 标准完全一致的 Token 精确计数功能。
llm-gateway 是整个架构中最为关键的 crate。 如果将 KeyCompute 比作一个操作系统,那么 GatewayExecutor 就扮演着“系统调用层”的角色—— 所有与外部世界(上游 LLM Provider)的交互都必须通过这一层。 这种“单一出口”的设计模式看似增加了限制,但实际上极大地简化了系统的安全审计、故障排查以及性能监控工作。
Gateway 的核心职责可以凝练为四个词:执行、重试、降级、计量。
2.1 执行器的 Retry-Fallback 链
当路由引擎生成一个 ExecutionPlan(包含一个 primary target 和多个 fallback targets)后,执行器会负责按顺序依次尝试,直到某个请求成功或所有尝试均告失败:
// 来源: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
// 避免有界 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,上游 Provider 正在高速推送 chunk 事件。如果 HTTP handler 尚未开始向客户端写入 SSE 数据(例如,它正在等待 execute 返回),那么 channel 会迅速被填满,导致生产者 tx.send() 在 .await 时挂起——从而形成死锁。
通过 tokio::spawn 将生产者放入一个独立的 task 中,execute 得以立即返回 rx。handler 拿到 rx 后可以立刻开始消费,使得生产与消费并行运转,死锁问题便迎刃而解。
在 run_plan 内部,Fallback 的逻辑设计得简洁且高效:
// 来源:crates/llm-gateway/src/executor.rs(简化)
async fn run_plan(&self, ctx: Arc<RequestContext>, plan: ExecutionPlan, ...) -> Result<()> {
// 构建 target 链: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) => {
// 失败:记录失败事件,尝试下一个 target
health_store.record_failure(provider);
last_error = Some(e);
}
}
is_primary = false; // 之后的都是 fallback
}
Err(last_error.unwrap_or_else(|| KeyComputeError::RoutingFailed(ctx.model.clone())))
}
这里的核心思想是:失败并非终点,而是切换通道的信号。只有当所有 target(包括 primary 和所有 fallback)都失败时,请求才算真正失败。这赋予了系统“N+M 冗余”的高可用特性。
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. 获取 Provider 适配器(通过 trait object 实现多态)
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 } => {
// Provider 报告的精确用量——覆盖本地估算值
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 数量。然而,如果上游的 Provider(例如 OpenAI 和 DeepSeek 都会这样做)在流结束时返回了精确的用量数据,系统会以 Provider 的数据为准。这种“乐观估算 + 权威修正”的策略,构成了一个双重保险机制。
2.3 tiktoken 精确 Token 计数
Token 计数的精准度直接决定了计费的精确性。在这方面,KeyCompute 没有任何妥协:
// 来源:crates/llm-gateway/src/executor.rs
fn estimate_tokens(content: &str) -> u32 {
if content.is_empty() { return 0; }
// 使用 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 直接集成了 tiktoken-rs 库中的 o200k_base 分词器——这正是 GPT-4o、o1、o3 等模型所使用的官方词表。这种做法确保了计费结果能与 OpenAI 官方 API 的返回值完全对齐。singleton 模式保证了词表仅在初次使用时加载一次,后续每次调用仅执行 BPE 编码运算,性能开销极低。
三、两层路由引擎与健康状态反馈
路由引擎采用 Layer1(Provider 排序)+ Layer2(账号选择)的两层架构;通过成本×0.3 + 延迟×0.25 + 成功率×0.25 + 健康度×0.2 的加权评分公式进行 Provider 排序;账号选择支持优先级排序与冷却机制;Node 路由通过
node:前缀触发独立路径。
如果说 Gateway 是执行任务的“手”,那么 RoutingEngine 就是负责决策的“大脑”。KeyCompute 的路由系统采用了模型级路由 + 账号池路由 的双层架构,并遵循一个核心设计约束:路由引擎是只读且无副作用的。它不会修改任何状态,仅基于当前的快照数据做出最优决策:
// 来源:crates/keycompute-routing/src/lib.rs
//! 路由引擎,双层路由,只读无副作用。
//! 架构约束:只读 Pricing 和状态快照,不写任何状态。
3.1 Layer1:Provider 智能排序
好的,作为资深主编和高级改写专家,我已严格按照您的要求,对原文进行了深度重写与降重处理。
以下是重写后的内容:
第一层路由的核心职责是:针对给定的模型名称(例如 gpt-4o),依据“综合性价比”对所有能够提供该模型的 Provider 进行排序。此排序过程采用了一个四维加权评分体系:
// 来源: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
}
这个评分公式背后蕴含着一个关键的工程理念:所有指标都统一为“数值越高,表现越差”,因此最终得分最低的 Provider 将被优先选择。这种统一的方向性设计,有效规避了混合正向与负向指标时可能引发的逻辑混乱。权重之和被严格约束为 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 Layer2:租户账号池与冷却机制
第二层路由的任务是,在已排序的 Provider 列表中,为每个 Provider 挑选出最优的上游账号。选择逻辑主要考量三个因素:租户隔离(确保不同租户使用独立的账号池)、模型匹配(账号必须支持所请求的模型)、冷却状态(近期发生失败的账号将被临时搁置)。具体实现如下:
// 来源:crates/keycompute-routing/src/lib.rs(简化)
async fn select_best_account(&self, provider: &str, accounts: Vec<Account>) -> Result<Option<ExecutionTarget>> {
// 按优先级降序排列
sorted_accounts.sort_by_key(|account| std::cmp::Reverse(account.priority));
for account in sorted_accounts {
// 跳过正在冷却中的账号(最近失败过,需要等待恢复)
if self.account_states.is_cooling_down(&account.id) {
continue;
}
// 解密上游 API Key(AES 加密存储)
let upstream_api_key = Self::decrypt_upstream_api_key(&account.upstream_api_key_encrypted)?;
return Ok(Some(ExecutionTarget::new_provider(
provider.to_string(),
account.id,
account.endpoint,
upstream_api_key,
)));
}
Ok(None) // 所有账号都在冷却中
}
此处的“冷却”机制,本质上是熔断(Circuit Breaker) 模式的一种轻量化实现。当某个上游账号连续出现故障(例如,触发了 OpenAI 的速率限制)时,系统会将其标记为冷却状态,并在一定时间内不再向其分配新的请求,给予其恢复的机会。这有效避免了“明知会失败却反复重试”所带来的无效资源消耗。
3.3 Node 路由:个人 PC 的独立通道
Node路由:独立路径与无回退机制
当请求中的模型名称以 node: 前缀开头时,路由引擎会切换至一条完全独立的处理路径。与标准的Provider排序流程不同,该路径直接查询在线节点是否能处理指定模型,而不会执行Provider评分与排序。
// 来源:crates/keycompute-routing/src/lib.rs
pub async fn route(&self, ctx: &RequestContext) -> Result<ExecutionPlan> {
// 检测 Node 路由前缀
if let Some(actual_model) = ctx.model.strip_prefix("node:") {
let node_index = self.node_index.as_ref().ok_or_else(|| {
KeyComputeError::Internal("Node routing not configured".to_string())
})?;
// 查询数据库:是否存在 ready 状态的节点支持该模型
if node_index.has_ready_node(actual_model).await {
return Ok(ExecutionPlan {
primary: ExecutionTarget::Node { model: actual_model.to_string() },
fallback_chain: Vec::new(), // Node 路由不支持 fallback
});
} else {
return Err(KeyComputeError::NoReadyNode(actual_model.to_string()));
}
}
// 无前缀:走标准的两层 Provider 路由
// ...
}
Node路由的一个关键设计原则是不支持回退机制——如果没有可用节点,系统会直接返回错误,而不会自动切换到云端Provider。这一选择是有意为之的:用户显式使用node:前缀,表明其明确希望使用本地算力(可能出于隐私、成本或离线等考量),自动回退到云端可能违背用户意图。
健康状态的反馈闭环
路由引擎虽然不直接写入状态,但它读取的健康数据来自执行层的反馈。这形成了一个优雅的闭环:
请求 → 路由决策(读取健康快照)→ 执行(成功/失败)→ 更新健康状态 → 影响下一次路由决策
// 来源:crates/llm-gateway/src/executor.rs(成功路径)
let latency_ms = target_start.elapsed().as_millis() as u64;
if let Some(ref health_store) = provider_health {
health_store.record_success(provider, latency_ms); // 记录成功 + 延迟
if !is_primary {
health_store.record_fallback(); // 统计 fallback 发生频率
}
}
// 失败路径
health_store.record_failure(provider); // 累积失败次数
通过 ProviderHealthStore 和 AccountStateStore 的被动记录机制,路由引擎能够实时感知各个Provider的可用性和延迟水平,实现“越健康权重越高、越不稳定越少调度”的自适应效果。当一个Provider连续失败达到阈值时,其健康评分会急剧下降(叠加100分的 UNHEALTHY_PENALTY),被自然排到队列末尾,但不会被完全剔除——这保留了“自我恢复”的可能性。
四、后置计费:不可变主账本设计
计费模块遵循“不参与路由、不预扣余额、不影响执行结果”的三不原则;使用 rust_decimal::Decimal 确保金额计算的绝对精度;流结束后一次性写入不可变的 usage_logs 主账本。
在许多算力平台的设计中,余额检查被放在请求入口处——“余额不足则拒绝服务”。这种看似合理的设计在高并发场景下会成为严重瓶颈:每个请求都需要一次数据库读取来查询余额,而且还需要处理并发扣款的竞态条件。
KeyCompute 选择了一条不同的路:
// 来源:crates/keycompute-billing/src/lib.rs
//! 计费模块,仅在 stream 结束后执行。
//! 架构约束:不参与路由,不预扣余额,不反向影响执行结果。
这三条约束的含义是:
- 不参与路由 ——路由引擎不知道计费模块的存在,不会因为“这个Provider更便宜”而改变路由决策(成本已经是路由评分的一个维度,但那是在Provider定价层面,不是用户余额层面);
- 不预扣余额 ——请求发起时不检查也不冻结用户余额,避免了高并发下的锁竞争;
- 不反向影响执行结果 ——即使计费失败(比如数据库短暂不可用),已经返回给用户的结果不会被撤回。
计费的精确度通过 rust_decimal::Decimal 类型保证:
// 来源:crates/keycompute-billing/src/lib.rs
/// 计费公式:
/// user_amount = (input_tokens/1000) × input_price + (output_tokens/1000) × output_price
pub fn compute_user_amount(
input_tokens: u32, output_tokens: u32,
input_price_per_1k: Decimal, output_price_per_1k: Decimal,
) -> Decimal {
let input_cost = Decimal::from(input_tokens) / Decimal::from(1000) * input_price_per_1k;
let output_cost = Decimal::from(output_tokens) / Decimal::from(1000) * output_price_per_1k;
input_cost + output_cost
}
为什么不用 f64?因为浮点数的精度问题在金融场景中是不可接受的。举一个典型的例子:0.1 + 0.2 在 f64 中并不等于 0.3,而是 0.30000000000000004。当这种微小误差在百万级请求中累积,最终账单可能与实际消耗产生显著偏差。rust_decimal::Decimal 使用128位定点表示,确保每一笔计费精确到小数点后28位。
BillingTrigger 是计费体系的启动开关,其调用时机极为精准——只有当流式响应完全终止后才会被触发:
// 来源:crates/keycompute-billing/src/lib.rs
pub struct BillingTrigger;
impl BillingTrigger {
/// 触发计费结算
/// 输入: usage + pricing_snapshot + request metadata
/// 输出: 同步写入不可变 usage_logs 主账本
pub async fn trigger(
&self,
ctx: &RequestContext,
provider_name: &str,
account_id: uuid::Uuid,
status: &str, // "success" | "partial" | "upstream_error"
billing: &BillingService,
) -> Result<NewUsageLog> {
billing.finalize(ctx, provider_name, account_id, status).await
}
}
status 参数可接受三种状态值:完全成功、部分成功(流中断但已产生内容)以及上游错误。即便请求仅执行了一半,已生成的 token 依然会被精确计费——这一点在长对话场景中至关重要。
unsetunset五、Node Gateway:让个人 PC 成为算力节点unsetunset
node-gateway借助基于 pull 模式的长轮询机制实现 NAT 穿透;节点注册过程需通过共享 token 进行身份验证;后台 Sweeper 负责清理过期节点并补推滞留任务;PostgresNodeIndex实现了路由引擎所需的NodeCapabilityIndextrait。
node-gateway 是 KeyCompute 最具突破性的组件之一。传统 AI 算力平台通常要求计算资源部署在具备公网 IP 的服务器上,而 KeyCompute 则给出了截然不同的方案——任何安装了 Ollama 的个人 PC,只需通过 pull-based 长轮询 即可接入系统,既不需要公网 IP,也无需配置端口映射或 VPN,只要能访问 KeyCompute 服务端即可:
// 来源:crates/node-gateway/src/lib.rs
pub mod config; // 节点配置(心跳间隔、任务超时等)
pub mod node_index; // 节点能力索引(实现 NodeCapabilityIndex trait)
pub mod redis; // Redis 任务队列(BRPOP 实现长轮询)
pub mod service; // 业务逻辑(注册、心跳、任务分发、结果提交)
pub mod store; // 数据库持久化(节点状态、任务记录)
pub mod sweeper; // 后台维护(清理过期会话、补推滞留任务)
我们可以用一个生活中的场景来理解它的工作流程:想象一个快递代收点(KeyCompute 服务端),快递员(用户请求)将包裹(任务)放入储物柜中,然后通知收件人(PC 节点)。收件人会定期检查柜子(长轮询),发现有自己的包裹就取走处理,完成后将结果放回柜子。如果收件人长时间未出现(心跳超时),系统就会将他的包裹重新分配给其他人。
具体流程如下:
- 节点注册:PC 节点使用共享的
registration_token向服务端注册,并声明自己支持的模型列表 - 心跳保活:每 30 秒发送一次心跳,更新
accepted_models(用户可能在 Ollama 中新拉取了模型) - 长轮询领取任务:节点向 Redis 队列发起 BRPOP,最多等待 30 秒
- 本地执行:节点调用本地的 Ollama API 执行推理计算
- 提交结果:执行完成后,通过 POST 请求将结果返回服务端
从配置文件中可以观察到其完善的容错设计——每个参数都经过了生产环境的反复打磨:
# 来源:config.example.toml
[node_gateway]
registration_token = "change-me-in-production" # 节点注册凭证
session_ttl_secs = 300 # 会话 5 分钟有效期
heartbeat_interval_secs = 30 # 30 秒心跳间隔
poll_timeout_secs = 30 # 长轮询最大等待时间
task_deadline_secs = 120 # 任务必须在 2 分钟内完成
complete_grace_secs = 60 # 超时后仍允许 1 分钟内提交结果
node_failure_threshold = 3 # 连续 3 次失败则排除节点
task_failure_threshold = 3 # 单个任务最多重试 3 次
sweeper_heartbeat_ttl_secs = 600 # 10 分钟无心跳则标记离线
sweeper_repush_interval_secs = 10 # 每 10 秒补推滞留在队列中的任务
complete_grace_secs 是一个非常贴心的设计:假设某个任务的 deadline 是 2 分钟,但节点在 1 分 59 秒时刚好完成计算,还没来得及提交结果——如果此时直接判定超时并重新分发,就会导致重复计算和结果冲突。60 秒的宽限期给了”慢半拍”的节点一个补交作业的机会。
NodeCapabilityIndex trait 则是连接路由引擎与节点网关的关键桥梁:
// 来源:crates/keycompute-routing/src/lib.rs
#[async_trait::async_trait]
pub trait NodeCapabilityIndex: Send + Sync {
/// 检查是否存在 ready 节点可以处理指定模型
/// ready predicate:
/// - nodes.status = 'online'
/// - node_sessions.expires_at > NOW()
/// - node_sessions.revoked_at IS NULL
/// - nodes.capabilities_json->>'runtime' = 'ollama'
/// - node_sessions.accepted_models_json 包含目标模型
async fn has_ready_node(&self, model: &str) -> bool;
}
### 路由决策:基于数据库的严格验证
`PostgresNodeIndex`(定义于 `node-gateway/src/node_index.rs`)通过单一 SQL 查询一次性完成对所有前置条件的校验,确保只有真正处于健康状态且具备处理能力的节点才会被纳入路由范围。需要特别指出的是,该组件**不会读取 Redis 缓存,也不信任客户端自行上报的负载数据**——所有判断依据均源自服务端可验证的数据库记录,这是一种将安全性置于首位的设计理念。
---
## 六、Provider 适配器的 Trait 抽象
> 借助 `ProviderAdapter` trait 为所有 LLM Provider 定义统一接口;`HttpTransport` trait 的依赖注入机制使得测试阶段可以完全模拟网络层;新增一个 Provider 只需要实现一个 trait 并创建一个独立的 crate。
在面向对象的编程语言中,我们通常会采用“接口”或“抽象类”来定义 Provider 的统一契约。而在 Rust 里,这个角色由 trait 来承担,并且表现更为出色——因为 Rust 的 trait 不仅定义了方法签名,还通过 `Send + Sync` 约束确保了并发安全性:
```rust
// 来源:crates/llm-gateway/src/executor.rs(从测试代码中可见 trait 完整用法)
#[async_trait]
impl ProviderAdapter for ManyChunksProvider {
fn name(&self) -> &'static str { "many-chunks" }
fn supported_models(&self) -> Vec<&'static str> { vec!["gpt-4o"] }
async fn stream_chat(
&self,
_transport: &dyn HttpTransport, // 注入的传输层——可 mock
_request: UpstreamRequest,
) -> Result<StreamBox> {
// 返回 Pin<Box<dyn Stream<Item = Result<StreamEvent>>>>
Ok(Box::pin(futures::stream::iter(events)))
}
}
这种设计带来了三个至关重要的优势:
1. 零修改扩展:新增一个 Provider(例如未来的 Meta Llama API)只需创建一个新的 crate(如 keycompute-llama),实现 ProviderAdapter trait,然后在 Gateway 构建时进行注册即可。核心代码无需任何改动。
2. 完全可测试:请注意 stream_chat 的第一个参数是 &dyn HttpTransport 而非具体的 HTTP 客户端。这意味着在单元测试中,你可以注入一个返回预设响应的 mock transport,完全脱离网络依赖:
// 测试中的 Provider:生成 150 个 chunk + Usage + Done
struct ManyChunksProvider { chunks: usize }
// 测试验证:execute 必须在 50ms 内返回 receiver(不被背压阻塞)
let mut rx = tokio::time::timeout(
Duration::from_millis(50),
executor.execute(ctx, plan, account_states, Some(provider_health)),
).await.expect("execute should return receiver immediately");
3. 内部 HTTP Proxy 支持:通过 HttpProxy 层统一管理上游连接,可以实现多代理出口(按 Provider 路由到不同的 HTTP 代理)、连接池复用、请求追踪等高级功能,而 Provider 适配器对这些基础设施层的存在完全无感知。
目前 KeyCompute 已实现的 Provider 适配器包括:
| 适配器 crate | 支持的 Provider | 备注 |
|---|---|---|
keycompute-openai |
OpenAI (GPT-4/4o/5) | 同时兼容所有 OpenAI 格式的第三方 API |
keycompute-claude |
Anthropic Claude | Claude 4/3.7/3.5 系列 |
keycompute-gemini |
Google Gemini | Gemini 2.5/2.0/3 系列 |
keycompute-deepseek |
DeepSeek | V3/V4/R1 系列 |
keycompute-ollama |
Ollama | 本地模型(Llama/Qwen 等) |
keycompute-vllm |
vLLM | 自托管高性能推理引擎 |
总结
类型安全贯穿全栈、异步并发模型精心设计、可观测性作为一等公民内建、加密存储保护敏感数据。
回顾整个 KeyCompute 的代码库,有几个工程亮点值得特别提及:
1. 类型安全贯穿全栈
从 SensitiveString(防止 API Key 意外打印到日志——Debug trait 实现会输出 *** 而非明文)到 ExecutionPlan(编译期保证路由结果的完整性——primary 是必须的,fallback_chain 是可选的),Rust 的类型系统被充分利用来在编译期消灭一整类运行时 bug。
2. 零成本抽象的异步模型
基于 tokio + axum 的全异步架构,mpsc::channel(100) 的有界背压控制、tokio::spawn 的任务隔离、tokio::select! 的优雅关闭——每一个并发原语的选择都不是随意的,而是针对具体场景(流式传输、信号处理、后台维护)精心匹配的。
3. 可观测性作为一等公民
Prometheus 指标导出、JSON 结构化日志(通过 tracing + tracing-subscriber 实现)、/health 健康端点——这些不是“上线后再补”的运维工具,而是第一天就内置的核心能力。从代码中随处可见的 tracing::info! 和 tracing::warn! 可以看出,可观测性是与业务逻辑同时编写的。
4. 加密存储与安全设计
上游 Provider 的 API Key 使用 AES 加密存储,用户密码使用 Argon2 哈希(当前最安全的密码哈希算法),JWT token 有明确的过期时间。KC__CRYPTO__SECRET_KEY 的“一旦使用不可更换”策略虽然在运维上增加了约束,但从安全角度看是正确的——它迫使运维人员在首次部署时就认真对待密钥管理。
KeyCompute 不仅是一个 API 网关,更是一个完整的“AI 算力操作系统”——从路由决策到流式传输,从精确计费到分布式节点调度,每一层都体现了对生产环境严苛要求的深刻理解。94.7% 的 Rust 代码比例不是对“新潮语言”的追捧,而是对性能、安全性和正确性的工程承诺。
对于正在构建 AI 基础设施的团队而言,KeyCompute 的架构设计思路——特别是其“唯一执行层”约束、“后置计费”哲学和“两层路由”引擎——值得深入借鉴与学习。
📌 项目仓库地址:https://github.com/keycompute/keycompute
📖 采用开源许可协议:MIT License
🦀 最低 Rust 编译器版本要求:1.92+
推荐文章
- Agent Skill 框架释放小语言模型潜能,12B 模型技能选择准确率逼近 90%,算力成本降低 50%!
- 构建面向 LLM 的下一代记忆管理系统,开源项目 Hypatia:为大模型装上“结构化外脑”
- 暴涨 45 个 Star!NVIDIA CUDA C++ 核心库 Lead 开源 AutoCUDA:永不停机的 GPU 内核优化智能体框架
如需交流入群,请在 NeuralTalk 公众号后台发送关键词:加群
关注“鲸栖”小程序,掌握最新AI资讯
本文来自网络搜集,不代表鲸林向海立场,如有侵权,联系删除。转载请注明出处:https://www.itsolotime.com/archives/34714

