< 返回版块

aiqubit 发表于 2026-06-04 11:30

Tags:全模态协议兼容接口,智能路由引擎,闲置算力网关,实时计费快照,Rust 高性能服务,分布式节点调度,主流大模型SDK集成

在 AI API 消费爆炸式增长的今天,中小企业研发团队和个人开发者面临一个共同的技术痛点:如何在多模型混用、账号池管理、精确计量计费、下游分销这四大复杂链路上,构建一套可维护、可扩展的基础设施,而不是在业务代码里堆出一团乱麻?

现有方案(LiteLLM、OpenRouter 等)要么是 Python/Node.js 实现导致运行时开销偏高,要么是 SaaS 化的黑盒,无法满足私有部署和企业定制逻辑的需求。

KeyCompute 以 90%+ 的 Rust 代码、28 个 workspace crate,从零打造了一套覆盖"请求接入 → 双层路由 → 算力接入执行 → 实时计费 → 二级分销"全链路的 AI Token 算力服务平台。


目录

一、架构总览:28 个 crate 组成的 Workspace Monorepo 二、双层路由引擎:模型级路由 + 账号池路由的分层设计 三、LLM 执行网关:llm-gateway 与 Provider trait 的协作边界 四、实时计费引擎:请求级价格快照与后置精确结算 五、闲置算力网关:拉取式边缘接入的 node-gateway 设计 六、认证与限流:JWT + Redis 的双轨限流架构 七、可观测性:Prometheus + 结构化日志的零侵入集成 八、总结:5 个关键工程亮点


快速上手

# 克隆项目
git clone https://github.com/keycompute/keycompute.git
cd keycompute

# 复制环境变量模板
cp .env.example .env
# 编辑 .env,填入数据库密码、JWT 密钥、加密密钥等

# 一键启动(Docker Compose,推荐)
docker compose up -d

# 验证服务状态
docker compose ps
# 访问 http://localhost:8080,默认账号 admin@keycompute.local

# 最简调用示例(OpenAI 兼容格式)
curl http://localhost:3000/v1/chat/completions \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4","messages":[{"role":"user","content":"Hello!"}]}'

环境要求: Rust ≥ 1.92、PostgreSQL ≥ 16、Redis ≥ 7、Docker(容器部署)。


一、架构总览:28 个 crate 组成的 Workspace Monorepo

1.1 项目目录结构

根目录下的 workspace 声明是整个项目的架构地图。28 个 crate 被划分为四个明确的层次:前端展示层、后端业务层、LLM 执行层、Provider 适配层。每一层之间的依赖关系是严格单向的——上层依赖下层,下层对上层一无所知。

[profile.release] 里的五行配置值得仔细看。lto = true 开启了链接时优化(Link-Time Optimization),这意味着编译器可以在所有 28 个 crate 的边界上做死代码消除和内联——对于一个分层紧密的系统,这个优化尤其有价值。panic = "abort" 则意味着一旦出现 panic,进程直接终止而不做栈展开,这减少了二进制体积,也符合"宁可挂掉重启,不留半死不活"的服务哲学。

1.2 核心设计原则

原则一:依赖方向单向性。 keycompute-types 是最底层的 crate,它不依赖任何其他 workspace crate。所有类型定义(结构体、枚举、错误类型)集中在这里,确保上层 crate 之间不会因为类型共享而产生循环依赖。这是大型 Rust workspace 的标准实践。

原则二:feature flag 驱动可选依赖。 Redis 并非强制依赖——keycompute-server 通过 --features redis 来启用分布式限流。这让单机部署无需引入 Redis,降低了运维复杂度。keycompute-ratelimit crate 内部维护两套后端实现,通过 feature 编译期选择,零运行时判断开销。

原则三:Provider 层完全隔离。 llm-gateway 不直接依赖任何具体的 Provider 实现(如 keycompute-openai),而是通过 keycompute-provider-trait 中定义的 trait 来交互。具体 Provider 在运行时通过注入的方式接入。这意味着添加一个新的 Provider 不需要修改 llm-gateway 的任何代码。

原则四:Release 配置面向容器优化。 opt-level = "z" 优先体积而非速度,这是容器化部署场景的正确取舍——AI 网关的瓶颈在网络 I/O 和上游 Provider 的响应延迟,而非 CPU 计算,因此牺牲少量计算性能换取镜像体积减小是划算的。

1.3 服务启动流程

// 来源:crates/keycompute-server/src/main.rs(推断自架构文档,简化)

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 阶段 1:配置解析(环境变量 → 强类型结构体)
    let config = keycompute_config::Config::from_env()?;

    // 阶段 2:数据库连接池初始化(SQLx PgPool)
    let db = keycompute_db::create_pool(&config.database.url).await?;

    // 阶段 3:运行时状态构建(热配置、Provider 注册表)
    let runtime = keycompute_runtime::AppRuntime::new(&config, db.clone()).await?;

    // 阶段 4:路由引擎初始化(从数据库加载模型→Provider 映射)
    let router = keycompute_routing::RoutingEngine::from_runtime(&runtime).await?;

    // 阶段 5:LLM 网关组装(注入路由引擎 + Provider 适配器)
    let gateway = llm_gateway::Gateway::new(router, /* providers */ ...);

    // 阶段 6:Axum Router 构建与服务启动
    let app = keycompute_server::build_router(gateway, runtime);
    axum::serve(listener, app).await?;
    Ok(())
}

这个初始化流程体现了一个重要的工程原则:依赖关系在编译期确定,状态在启动期构建,请求期零配置读取。没有全局 lazy_static,没有运行时的 Mutex<Option<T>>,每一个组件在被使用前必须已经完全初始化。Rust 的所有权系统在这里充当了"架构卫士"——如果你忘记初始化某个依赖,代码根本无法编译。


二、双层路由引擎:模型级路由 + 账号池路由的分层设计

//! keycompute-routing,双层路由决策引擎。 //! 架构约束:该模块只负责「选择哪个 Provider 账号来执行请求」;不执行网络请求,不修改请求内容,不记录计费数据。

2.1 两层路由的职责划分

想象一家大型酒店的前台调度系统:前台先判断客人需要哪类房型(模型级路由),再在同类房型的多个房间里按照入住情况选一个(账号池路由)。如果选中的房间恰好出了故障,系统自动切到下一个同类房间,不需要客人重新排队。

在 KeyCompute 中,这个比喻对应得非常精确:

ModelRouter::resolve 返回的是一个有序候选列表而非单一选择。这个设计决策看似微小,实则关键:它将"失败重试"的复杂性从路由层剥离出去,委托给 llm-gateway 处理。路由层只管"提供选项",不管"选项失败了怎么办"——单一职责原则的体现。

2.2 加权随机负载均衡

为什么不用轮询(Round Robin)而用加权随机?

因为不同 Provider 账号的配额上限可能差异悬殊——一个企业账号每分钟 10000 TPM,一个免费账号只有 500 TPM。轮询会均匀分配请求,导致高配额账号被严重低估,免费账号被频繁触发限流。加权随机允许运维人员按实际配额比例设置权重,流量分配与配额比例自然对齐。

举一个典型失败场景:如果 3 个账号权重均为 1 用轮询,但账号 A 的配额是 B、C 的 10 倍,在高并发下 B 和 C 会持续 429 错误,触发大量无效重试,实际吞吐量反而低于只使用 A 的情况。加权随机(A 权重设为 10,B 和 C 各为 1)则能让流量按配额比例自然分布。

2.3 健康检查与故障转移

// 来源:crates/keycompute-routing/src/health.rs(推断)

pub struct HealthChecker {
    // 关键设计:AtomicBool 做可用性标记,无需 Mutex
    // 读操作使用 Ordering::Relaxed,不需要跨线程同步保证
    availability: Arc<AtomicBool>,
    last_check: Arc<AtomicU64>,  // Unix timestamp,无锁更新
}

impl HealthChecker {
    pub fn is_available(&self) -> bool {
        self.availability.load(Ordering::Relaxed)
    }

    /// 后台 tokio task 定期探测 Provider 可用性
    pub async fn run_periodic_check(self: Arc<Self>, interval: Duration) {
        loop {
            tokio::time::sleep(interval).await;
            let result = self.probe().await;
            self.availability.store(result.is_ok(), Ordering::Relaxed);
            /* ... */
        }
    }
}

这段代码体现了一个精妙的工程权衡:使用 AtomicBool + Ordering::Relaxed 而不是 Mutex<bool>。在健康检查这个场景下,我们不需要严格的跨核同步——偶尔读到一个稍早的状态值(某个账号刚刚恢复但路由器还没感知到)是可接受的,而 Mutex 的锁开销在高并发路由决策中是真实的性能损耗。这是"宽松一致性(Relaxed Consistency)换取性能"的教科书案例。


三、LLM 执行网关:llm-gateway 与 Provider trait 的协作边界

//! llm-gateway,LLM 请求的执行引擎。 //! 架构约束:该模块不拥有业务逻辑(计费、鉴权均在外部完成);只负责「拿到路由决策 → 调用对应 Provider → 返回流式响应」。

3.1 Provider 抽象 trait

注意 Send + Sync 这两个 trait bound,它们不是可以省略的细节。在 tokio 的多线程运行时中,async 函数的 Future 可能在不同线程间迁移(work-stealing 调度),如果 LlmProvider 的实现包含 Rc<T>RefCell<T> 这类非 Send 类型,代码将无法编译。这是 Rust 类型系统在"编译期强制并发安全"方面的典型体现——让不安全的并发使用无法被编译出来,而不是依赖运行时的检测或人工审查。

3.2 流式响应的管道设计

为什么不在 Provider 内部实现重试,而是在 Gateway 层实现跨 Provider 切换?

因为 Provider 层的重试只能在同一个账号内部重试,无法感知其他可用的替代 Provider。如果 OpenAI 的账号 A 限流,在 Provider 内重试只会打到账号 A 的其他端点,依然大概率失败。Gateway 层的切换逻辑能够跨越 Provider 边界——从 OpenAI 切换到同等能力的 DeepSeek。这是"关注点分离"在故障处理上的应用:Provider 只知道如何与特定 API 通信,Gateway 知道整个系统的拓扑结构。

3.3 OpenAI 协议适配层

注意 API Key 的处理方式:account.api_key 在每次请求时从 ProviderAccount 里读取,而不是烧录在 Provider 实例里。这意味着同一个 OpenAIProvider 实例可以服务于多个不同的账号,账号切换完全不需要重建 Provider 对象。这是"状态分离"设计的典范——行为(如何构造 HTTP 请求)与状态(用哪个账号的 key)解耦。


四、实时计费引擎:请求级价格快照与后置精确结算

//! keycompute-billing,计费结算引擎。 //! 三不原则:不预扣余额(避免悲观锁)、不在请求链路上写库(避免延迟)、不使用浮点数(避免精度损失)。

4.1 为什么不用 f64 而用精确整数?

为什么不用 f64 来存储 Token 单价和费用?

因为 f64 的浮点精度问题在计费场景下会导致真实的资金损失或用户纠纷。举一个典型的失败场景:0.1 + 0.2 = 0.30000000000000004 在 IEEE 754 浮点数中是精确的,但如果你用 if total >= 0.3 { charge_user() } 判断是否达到计费阈值,这个判断会永远为 false——用户白嫖了。反过来,某些浮点累加可能让计算结果偏大,过度扣费同样是问题。

KeyCompute 使用整数(通常是以"厘"或更小单位为基准的定点数)存储所有金额,避免了这个问题。

checked_mul 的使用值得一提。直接用 * 在 release 模式下不会 panic,溢出会静默地产生错误结果。checked_mul 在溢出时返回 None,这里用 expect 让它 panic——因为费用计算溢出是严重的程序错误,应该立即暴露,而不是产生一个错误的账单数字悄悄继续运行。

4.2 请求级价格快照

为什么不在请求结束后再查当前价格,而是在请求开始时做价格快照?

因为流式 LLM 请求可能持续数十秒甚至数分钟。如果在请求完成后才查价,而管理员恰好在这期间调整了价格,用户看到的"请求时价格"与最终账单价格就会不一致,产生争议。快照机制确保用户在发起请求时就确定了这次请求的结算价格,类似于"锁价"。这个设计体现了"时间点一致性"原则——对同一个经济事件,所有相关数据应来自同一个时间点。

4.3 后置结算的异步架构

注意 UPDATE users SET balance = balance - $1 WHERE ... AND balance >= $1 这个 SQL 语句的结构。AND balance >= $1 既是业务约束(余额不能为负),也是并发安全措施——在高并发下,两个请求同时读到余额 10,同时发起 8 的扣减,如果没有这个条件,可能导致余额变为 -6。这个条件让 UPDATE 在余额不足时静默返回 0 行受影响,上层代码检查受影响行数即可判断是否成功。这是"数据库层乐观并发控制"的轻量实践。


五、闲置算力网关:拉取式边缘接入的 node-gateway 设计

//! node-gateway,闲置算力节点的接入网关。 //! 架构约束:节点无需公网 IP;只允许节点主动拉取任务,服务端不主动推送;单节点故障不影响其他节点。

5.1 拉取式轮询的核心设计

允许个人 PC 通过运行推理引擎接入算力网络,无需公网 IP 或复杂的 NAT 穿透配置。

为什么用拉取式轮询而不是 WebSocket 或 SSE 长连接?

因为长连接需要服务端维护每个节点的连接状态,在节点数量增加时服务端资源消耗线性增长。而且家庭宽带的 NAT 设备通常会在 1-5 分钟内断开闲置的 TCP 连接,导致 WebSocket 连接需要复杂的心跳维护逻辑。拉取式轮询让每个节点完全独立——节点挂掉不需要服务端做任何清理,服务端完全无状态。这是一个"以增加延迟换取系统简单性"的工程权衡。对于边缘计算节点这类场景,500ms 的任务获取延迟是可接受的,但节点掉线导致服务端状态泄漏是不可接受的。

5.2 模型前缀路由

node:llama3 这个前缀设计体现了"显式优于隐式"的哲学。用户明确写出 node: 前缀,才会将请求路由到本地 PC 节点;没有前缀则走云端 Provider。这避免了"自动路由到本地导致意外的慢响应"的问题,同时保留了利用本地算力的能力。用户知道自己在做什么,系统不替用户做隐式决策。


六、认证与限流:JWT + Redis 的双轨限流架构

//! keycompute-auth,认证与鉴权。 //! keycompute-ratelimit,分布式限流。 //! 架构约束:auth 层只做身份验证(这个 key 是谁的),不做业务鉴权(这个 key 能调哪些模型)。

6.1 API Key 的加密存储

注意 KC__CRYPTO__SECRET_KEY 在 README 中有一条特别的警告:"一旦数据库写入数据后不可更改(会导致历史数据无法解密)"。这说明系统使用的是对称加密,而不是哈希——API Key 的明文在需要时是可以恢复的(用于某些需要展示完整 key 的管理场景)。这是加密 vs 哈希的设计权衡:哈希不可逆但更安全,加密可逆但需要保护好密钥。

6.2 双轨限流:内存 vs Redis

这个 feature flag 设计让单实例部署和分布式部署的代码路径在编译期就分离:没有 --features redis 时,RedisRateLimiter 的代码根本不参与编译,零运行时判断,零条件分支。这是 Rust feature flag 相比运行时配置 if config.use_redis 的关键优势——编译期的可选依赖,而非运行时的条件判断


七、可观测性:Prometheus + 结构化日志的零侵入集成

结合 /health 健康检查端点和 Prometheus 指标,KeyCompute 给出了一个"可观测性最小可行方案":不依赖任何外部 APM 系统,只需要一个 Prometheus + Grafana 栈即可获得完整的监控视图。


总结:5 个关键工程亮点

1. Workspace Monorepo 的边界清晰

28 个 crate 严格单向依赖,keycompute-types 作为零依赖的类型基座,确保整个系统不会出现循环依赖。opt-level = "z" + lto = true 的 release 配置,让跨 crate 的优化成为可能,最终二进制体积最小化。对于借鉴者的价值:这个目录结构可以直接作为中型 Rust 服务的组织模板。

2. 双层路由引擎分离关注点

模型路由(model → Provider 渠道)与账号路由(渠道 → 具体账号)严格解耦。加权随机负载均衡让流量分配自然匹配各账号的实际配额上限。故障转移逻辑在 Gateway 层而非 Provider 层,允许跨 Provider 的能力兜底。

3. Provider trait 的"让错误无法编译"设计

LlmProvider: Send + Sync 的 trait bound 在编译期强制所有 Provider 实现线程安全,消除了一整类并发 bug。BoxStream<'static, ...> 的返回类型统一了不同 Provider 的异构流类型,使得 Gateway 层可以以完全相同的方式处理 OpenAI、DeepSeek、Ollama 的响应流。这是类型驱动设计(Type-Driven Design)的实践。

4. 请求级价格快照防止计费时间线不一致

在请求开始时锁定价格快照,在请求完成后用快照价格结算,而非结算时查询当前价格。数据库层用 AND balance >= amount 做乐观并发控制替代悲观锁,避免了高并发余额操作的锁争用。整数定点数替代浮点数彻底消除精度问题。

5. 拉取式边缘节点——无需公网 IP 的算力众包

node: 前缀路由 + 轮询拉取任务的组合,让闲置 PC 以极低的运维门槛接入算力网络。显式前缀避免了隐式路由带来的用户困惑,指数退避避免了网络故障时对服务端的冲击。这个设计思路对"边缘计算资源整合"类产品有直接的参考价值。


KeyCompute 不仅是一个 AI Token 代理网关,更是一个完整的"AI 算力基础设施工程实践样本"——从 Workspace Monorepo 的依赖组织,到 trait 抽象的 Provider 扩展,到精确计费的类型安全保证,每一层都体现了对"大规模 AI API 服务"在可靠性、可扩展性和精确性上的深刻理解。


完整原文: https://mp.weixin.qq.com/s/vd1M463sE_D16cEaKZRfuA


Ext Link: https://github.com/keycompute/keycompute

评论区

写评论

还没有评论

1 共 0 条评论, 1 页