ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Rust写1k行LLM Proxy:轻量API网关的模型路由与转发实战

用Rust写1k行LLM Proxy:轻量API网关的模型路由与转发实战 一个用 Rust 写的 LLM Proxy代码量压到 1k 行左右第一眼看上去像玩具但实际拆下来会发现它把 LLM 网关里最常用的部分压缩得很干净。这里说的 Proxy 不是网络代理而是指 HTTP 层的 API 转发器客户端按 OpenAI 风格发送/v1/chat/completions请求它负责模型名映射、选择上游 Provider、补鉴权头、转发请求再把上游结果原样返回。适合谁看正在自建 LLM 应用、Agent 服务或者想在团队内部做一个统一 LLM API 入口又不想上重型网关的人。最值得关注的不是它能实现多少功能而是它用很少的代码把问题选得足够准。下面按从零到能上线的顺序把这个 1k 行项目拆开讲一遍。1. 先别急着写功能想清楚这个 LLM Proxy 到底解决什么问题很多人看到“LLM Proxy”会下意识以为要做一个全能网关。实际不是。它更像一个翻译层和路由层解决的是 API 散乱问题不是网络问题。1.1 它解决的是 API 入口散乱不是网络问题现在做 LLM 应用最难受的不是模型能力不够而是每个服务都要自己处理一堆重复逻辑。每个项目都要配一遍 API Key。模型地址散落在不同配置文件里。换模型时代码里到处要改。不同 Provider 的请求格式虽然接近但细节总有差异。日志里只有“调用失败”不知道是 Model 不存在、Key 过期还是上游限流。一个本地 LLM Proxy 可以把这些收口。客户端不用关心真正调用哪家 API只向网关发一个标准请求网关注入 Provider 选择、Key 注入、错误状态透传和日志记录。这样做之后业务代码里只需要一个稳定的base_url。1.2 1k 行能做到什么做不到什么1k 行听起来少但做转发层足够。关键是别想着把企业网关的功能都塞进去。能做的不能做的统一 OpenAI 风格 API 入口复杂多租户计费模型名到实际模型名的映射可视化控制台多 Provider 路由多区域智能调度隐藏上游 API Key大规模数据缓存基础鉴权和日志自动化审计平台错误状态透传通用可观测系统单机并发转发高可用集群如果只是个人开发、小团队内部工具或者给 ComfyUI、Agent 套件提供一个统一入口1k 行完全够用。如果要做面向外部客户的平台级网关那就不是这个项目该承担的事。1.3 为什么偏偏用 Rust 写做 API 转发关键不是计算而是并发连接管理、I/O 等待和配置处理。Python、Go、Node 都能做但 Rust 有几个实际优势单个二进制文件部署到服务器、NAS、树莓派都方便。没有运行时依赖不用先装 Python 或 Node。内存占用低长驻服务很合适。并发模型清晰处理大量等待上游响应的请求时不会明显吃资源。不是说 Rust 一定比其他语言快多少而是“小体积、低依赖、方便部署”这个特性正好和 1k 行 LLM Proxy 的定位匹配。如果你以后打算把网关放到边缘设备或者嵌入式环境Rust 这条路的优势会更明显。2. 环境准备和依赖选型1k 行怎么写才不臃肿代码量控制在 1k 行不代表不准备环境。先把工具链和依赖方向定好否则后面改起来会很难受。2.1 先装好 Rust 工具链顺便解决下载慢的问题不管在 Windows、macOS 还是 Linux先用 rustup 安装稳定版工具链这个没有争议。装好后执行rustup update stable如果你在 Windows 上会面临stable-msvc和stable-gnu的选择。大部分情况下用默认的stable-msvc就行Rust 官方工具链和 C 依赖链在 MSVC 环境下兼容性更好。只有很清楚自己不能用 MSVC 的时候再换stable-gnu。依赖下载慢是国内环境常见痛点。Cargo 依赖源和 rustup 发行源是两个概念经常有人搞混。解决 Cargo 依赖下载可以在~/.cargo/config.toml里配置国内镜像[source.crates-io] replace-with rsproxy-sparse [source.rsproxy-sparse] registry sparsehttps://rsproxy.cn/index/配置完后再执行cargo build绝大多数常用 crate 会快很多。如果公司内部有私服也可以把地址换成自己的 sparse registry。2.2 HTTP 框架怎么选转发型服务不需要花哨的 Web 框架但需要一个稳定的 HTTP 路由层。目前 Rust 生态里常用的是这几套框架适合场景使用体验axum小型 API、网关、中间件组合路由清晰和 tower 配合好actix-web功能完整的高性能 Web 服务能写大型服务但心智成本略高hyper底层 HTTP 实现扩展自由但代码量会明显增加如果只是复刻这个 1k 行 LLM Proxy我建议选 axum。理由不是 axum 比 actix-web 强而是 axum 的Router和State模型对“把请求转发到上游”这种场景很直接。你在网上搜“actix web 构建高性能 Rust API”能拿到很多工程化写法但一个 1k 行项目没必要把工程框架拉那么满。常见依赖可以控制在这些axum 0.8 tokio { version 1, features [full] } reqwest { version 0.12, features [json, stream] } serde { version 1, features [derive] } serde_json 1 tracing 0.1 tracing-subscriber 0.32.3 项目目录按职责拆别把 1k 行堆在一个 main.rs1k 行不意味着一个文件写到黑。提前拆成几个小文件后面加功能、排查问题都轻松得多。src/ main.rs # 启动和装配 config.rs # 读取配置和模型映射 route.rs # 路由定义 upstream.rs # 上游转发逻辑 auth.rs # 客户端鉴权文件少而清晰每个文件只做一件事。这个结构不复杂但比全堆在main.rs里容易维护。很多 1k 行项目最后失控不是因为代码多而是因为职责混在一起。3. 最小实现路径先跑通 /v1/chat/completions再谈兼容不要一开始就想着兼容所有 Provider。先把一条链路跑通收到请求、读取 model、映射到上游、转发、返回结果。3.1 最小转发函数长什么样下面是一个简化示例重点看转发思路不要当成完整工程代码use std::sync::Arc; use axum::{Json, Router, routing::post}; use axum::extract::State; use axum::http::StatusCode; use serde_json::Value; use reqwest::Client; #[derive(Clone)] struct AppState { client: Client, config: ArcConfig, } async fn forward_chat( State(state): StateAppState, Json(body): JsonValue, ) - ResultJsonValue, (StatusCode, String) { let model body.get(model).and_then(Value::as_str); let upstream_url state .config .find_upstream(model) .ok_or((StatusCode::NOT_FOUND, model not mapped.to_string()))?; let resp state .client .post(upstream_url) .json(body) .send() .await .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?; let status resp.status(); let text resp.text().await.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?; if !status.is_success() { return Err((status, text)); } serde_json::from_str(text) .map(Json) .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string())) }这里最关键的一步是使用serde_json::Value而不是严格的ChatRequest结构体来接收请求体。原因后面会细说LLM API 的字段经常变化严格反序列化很可能把某些 Provider 新增的字段丢掉。3.2 先跑单条请求不追求完整效果启动服务后先用 curl 打一条最小请求curl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: default, messages: [ {role: user, content: hello} ] }成功结果一般长这样返回 200。响应里有id、choices、usage或者至少是合法的 OpenAI 风格 JSON。控制台日志能看到请求模型、上游地址、响应状态和耗时。这里先不要开并发。单条请求跑通至少能确认路由、上游地址、鉴权和返回格式都正常。很多人一上来就写批量并发结果连“模型没有映射”“上游地址错误”都压在日志里排查成本翻倍。3.3 流式接口要单独处理别和普通请求混在一起如果客户端传了stream: true上游会返回text/event-stream。这个场景和普通 JSON 请求完全不同不能等上游完全返回再解析也不能把 SSE 事件当成一个 JSON 整体。伪代码如下// 伪代码流式转发时不要先收集完整响应再转发 let mut stream resp.bytes_stream(); while let Some(chunk) stream.next().await { let chunk chunk.map_err(...)?; tx.send(Ok::_, Infallible(chunk)).await?; }流式转发主要有几个坑SSE 事件边界不能被拆坏。不能对中间内容做 JSON 解析否则会卡在未完成的 JSON 片段上。超时时间不能设置成固定总超时否则长对话容易断流。建议第一次实现时先回归普通请求再单独加stream分支。能把流式做好这个 Proxy 的基本功就算过关了。4. 从单路由到多 Provider模型映射、鉴权和重试一条链路通到任意一家 API只是开始。真正让 Proxy 有价值的是多 Provider 路由。4.1 模型名映射的三种做法模型映射的目的一般有三个隐藏真实模型名、统一客户端配置、按前缀分流到不同 Provider。方式说明适合场景直接透传客户端传什么模型名就调用什么模型个人本地调试别名映射把default映射到某个具体模型客户端不关心上游型号前缀路由根据deepseek/xxx、openai/xxx决定 Provider一个网关接多家服务配置示例{ listen: 127.0.0.1:8080, providers: { provider-a: { base_url: https://api.example.com/v1, api_key_env: PROVIDER_A_API_KEY } }, model_map: { default: { provider: provider-a, upstream_model: example-chat } } }这里有两个细节值得注意。第一Provider 的base_url要能区分/chat/completions和/responses这类不同接口。有些 Provider 用chat/completions有些用responses如果配置死一个路径很容易出现 404。第二上游 API Key 不要写死在 JSON 文件里用api_key_env指向环境变量否则配置一提交就泄露了。4.2 鉴权给内部工具和 CI 用很多本地场景确实不需要鉴权但只要网关上放到服务器就至少加一层静态 Token。客户端调用时带Authorization: Bearer your-gateway-token网关检查这个 Token不通过就返回 401。这样做至少有两点好处避免内网里的任何服务都能乱调。以后接管 CI、Agent、ComfyUI 时可以在网关层控制调用方。要注意别把网关 Token 和上游 API Key 搞混。网关 Token 是验证调用者的上游 API Key 是网关拿去调用 Provider 的。日志里两个都不能打全尤其是 Authorization 和 API Key。4.3 超时、重试和限流请求转发出去之后最常见的问题不是代码跑不起来而是上游慢、上游限流、上游偶发失败。超时方面普通 JSON 请求可以设置总超时比如 120 秒。流式请求不能设置总超时应该设置空闲超时只要持续有新数据就不算超时。重试方面记住一个原则不是所有错误都适合重试。400 不重试因为请求体本身有问题。401、403 不重试先查 Key 和权限。404 不重试先查路径和模型映射。429 可以重试但要退避等待。5xx 可以试一次但不能无限试。还有一个容易踩的坑流式请求发出后如果已经给客户端返回了部分 SSE 数据这时不能做自动重试否则客户端会收到重复片断比直接断流更难定位。并发方面不要一上来就拉满。先设一个很低的并发上限观察上游响应速度和错误率再逐步往上加。很多 Provider 有每分钟请求限制代理层堆高并发只是把错误从调用方挪到了上游。5. 上线前必须处理的错误码和排查链路LLM Proxy 最容易出问题的不是正常转发而是异常状态没处理干净。很多本地 API 切换类工具会直接抛出unexpected status 401 unauthorized、unexpected status 404 not found、unexpected status 502 bad gateway这类错误本质上都是上游状态没有被处理好原样透出导致客户端看不懂。5.1 常见状态码和排查方向状态码常见原因先查什么400请求体不符合上游协议推理模型的reasoning_content没回传请求体里未知字段有没有被丢掉401上游 Key 缺失、过期、格式不对api_key_env是否配置环境变量是否加载402上游账户额度不足Provider 控制台403权限不足、IP 不在白名单、区域受限Provider 账户权限和网络策略404路径拼错、模型名没有映射、接口不支持日志里的上游完整 URL429上游限流、并发过高重试策略和请求频率502上游服务异常或上游之后的网关错误Provider 状态页面和历史成功率其中 400 是最值得展开的。有人会遇到类似upstream_status: http 400; cause: the reasoning_content in the thinking mode must be passed back to the api的报错。这不是代理本身坏了而是某些推理模型在thinking模式下会返回reasoning_content字段后续请求必须把相关推理内容回传给上游。如果代理用了严格类型结构体把未知字段过滤掉了上游就会报 400。所以前面强调的“用serde_json::Value原样转发”不是偷懒是避免这一类协议兼容问题。代理的职责是转发不是替业务方解析推理协议。5.2 排查顺序先看日志再改配置遇到请求失败不要急着改代码。按下面顺序来先用 curl 复现拿到完整请求和响应。看网关日志里的入站信息方法、路径、请求 ID、模型名。看日志里的上游地址确认路径拼接和 Provider 选择是不是符合配置。直接用 curl 请求上游地址绕过网关判断问题在代理还是在 Provider。检查环境变量和配置文件Key 有没有加载模型映射有没有匹配。最后再去看代码逻辑多半是路径、字段或异常分支问题。如果日志里出现connection timed out: getsockopt这类网络错误通常也不是代理代码问题。先确认运行环境能不能正常访问上游 API是不是网络策略、防火墙或 DNS 问题。代理能做的只是把网络错误稳定记录成可读日志而不是替你把网络打通。5.3 日志要能回答“这一个请求发生了什么”LLM Proxy 的日志不用特别大但要结构化。建议至少记录这些字段request_id用来关联同一个请求。method和path知道是哪个接口。model客户端传的模型名。upstream实际请求的上游地址。status上游返回状态。latency_ms转发耗时。error如果有错误给简短错误描述。注意不要记录请求里的敏感消息内容除非你明确知道这是内部调试环境。匿名日志已经足够排查大部分问题。6. 1k 行不是越少越好边界要自己定代码量少是一个卖点但也是一个约束。项目真正落地时最重要的问题是哪些功能不塞进去。6.1 这些功能别硬塞进 1k 行可视化配置面板。插件系统。多语言 DSL。复杂计费逻辑。全链路 trace 平台。不是这些功能没用而是每一类功能都会显著增加代码量和维护成本。如果需求真的到了那一步更好的是接一个成熟的企业级网关而不是把一个 1k 行项目硬撑成四不像。把 Scope 守住这个项目才能真正发挥价值。6.2 轻量网关和企业网关怎么选维度1k 行轻量网关企业级网关部署单个二进制放到任意机器多节点、数据库、配置中心功能路由、鉴权、转发、日志配额、审计、多租户、监控告警适用个人、小团队、内部工具对外产品或大规模治理场景维护成本很低需要专门团队很多人一开始就按企业网关的标准来做结果项目卡在中间配置写得很复杂转发链路却不稳定。我更建议先用轻量版本把链路跑通再按真实需求逐步外扩。6.3 和 ComfyUI 这类本地工具的配合方式有读者会问ComfyUI 和 LLM 必须在同一台电脑上吗答案是否定的。它们之间走的是 HTTP API本来就允许部署在不同机器上。你只需要在 ComfyUI 侧把 LLM 的base_url指向代理服务代理再去调用真正的模型服务。这样做好处很明显即使 ComfyUI 和模型服务不在同一台电脑上也能统一管理。以后换模型不用改 ComfyUI 配置只改网关的model_map就行。如果你的网关跑在一台常开的小主机或 NAS 上Rust 单二进制部署非常合适不用装 Python 环境也不会因为系统依赖升级导致服务突然不可用。6.4 关于 LLM 精度问题代理层别多管闲事网上经常能看到“LLM 大模型精度问题 fp16、fp32、bf16 详解与实践”这类内容。这些是模型推理和训练阶段要考虑的事不是代理层该操心的。代理转发的是 JSON不负责计算权重精度。代理层真正要注意的是不要随便把 JSON 里的数字转成f64或f32再重新序列化。这样可能造成精度丢失尤其是一些 Provider 返回的 token id、logprobs、时间戳等字段。最稳妥的做法就是保持原始 JSON 结构原样转发。7. 落地顺序先稳再全最后给一个可执行的落地顺序避免一上来就把项目做偏。7.1 先完成这三件事第一件事单条请求能通。不管接的是哪家 Provider先确保/v1/chat/completions能返回正常结果。第二件事模型映射能改。客户端传default代理能正确映射到指定上游模型不需要业务方知道真实模型名。第三件事日志能查到上游状态。每次请求失败都能从日志里找到请求 ID、模型、上游地址、状态码和耗时。这三件事做完这个 1k 行 LLM Proxy 就已经能当内部工具用了。7.2 再考虑扩展和运维等这三件事稳定后再考虑是否要支持/responses接口。是否要做流式转发。是否要加静态 Token。是否需要 batch 请求的并发控制。是否需要把错误日志汇总到统一平台。这些功能是一层一层加上去的不是第一次就全部做完。代码量保持 1k 行附近重点在于每次新增都仍然围绕“转发链路”这个核心而不是东加一块西加一块。踩过几轮之后你会发现很多问题不是工具能力不够而是输入格式、路径拼接和异常处理没有提前想清楚。一个 1k 行的 LLM Proxy真正难的不是把代码写出来而是知道哪些逻辑该做哪些
返回列表