ARTICLE DETAIL

资讯详情

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

用FastAPI搭建统一LLM网关:一个Key接入所有免费模型

用FastAPI搭建统一LLM网关:一个Key接入所有免费模型 先说一个挺常见的场景你同时想用多个平台的免费模型做应用于是注册账号、申请 API Key、看文档、配 SDK、写代理代码……一个接一个折腾下来代码里躺着十几把 Key每把 Key 对应的请求地址还不一样。更难受的是等模型版本更新或者某个平台临时调整模型名你还要逐个修改调用代码。这篇文章要解决的就是“免费模型很多、Key 也一堆”的碎片化问题。我会从统一 API 网关的原理讲起用 FastAPI 写一个最小可用的网关把多个免费模型服务统一到一个 Key 后面再给出 OpenAI SDK、LangChain、ChatBox 等常见客户端的接入方法最后整理一份高频报错排查清单。整套代码都是直白可复制的适合学生、个人开发者和正在做 AI 应用原型的团队参考。1. 为什么需要“一个 Key 接入所有免费模型”先解释标题里的“Free LLM API”。它并不是指某一个具体的模型而是指一种能力通过一个统一的 API 入口访问多个提供免费额度或免费档模型的服务商。目前各家大模型服务商几乎都会提供 OpenAI 兼容接口但它们各自有独立的 Base URL、独立的鉴权方式、独立的免费额度和模型列表。你在项目里每接入一家就要管理一整套配置想换一个模型又得重新适配一次。统一网关解决的就是这个多对多问题。从工程角度看它至少带来四个明确收益。第一接入成本下降。客户端只需要实现一套 OpenAI 兼容调用以后新增模型只是在网管配置里加一行模型名和对应服务商业务代码完全不用改。第二Key 不再混乱。无论客户端跑在本地、测试服务器还是 CI 环境都只需要配置同一个网关主 Key省去多个环境维护多套密钥的烦恼。第三可以做统一容灾。免费模型经常遇到“暂时繁忙”或服务不稳定网关可以把请求切换到备用模型避免用户直接看到报错。第四方便做统一监控。不管底层调用了几家服务商日志、请求量、token 消耗都能汇总到同一个服务里统计。市面上确实有不少“聚合全部免费模型”的第三方服务但与其完全依赖第三方聚合服务不如先掌握实现原理再决定是否需要使用现成网关。自己搭一个轻量网关模型列表、密钥、日志都完全可控后面接新模型也只是改配置的事。2. 统一 LLM API 网关的核心原理严格来说模型聚合并不需要写一堆复杂的 AI 代码它的本质是一个 HTTP 服务负责“把客户端的请求转给合适的模型服务商再把结果转回来”。要做到一个 Key 管所有模型需要拆成三个层次来看。2.1 统一鉴权层第一层是鉴权。网关对外只暴露一个主 KeyMaster Key所有客户端请求都带这个 Key网关校验通过后再用自己的服务商密钥去调用上游。这里的关键点是主 Key 和上游密钥完全隔离。客户端永远不应该看到 DeepSeek、OpenRouter 或者其他服务商的实际密钥否则就等于把这把钥匙交出去了。在 FastAPI 里实现这个层非常简单只需要写一个依赖函数读请求头Authorization里的 Bearer Token和配置里的master_key对比。如果校验失败直接返回 401。框架代码后面会给出。2.2 模型路由与别名映射第二层是路由。客户端传过来的model字段决定请求最终发给哪家服务商。最朴素的做法是“模型名映射”网关维护一张表每个模型名对应一个 provider 和它自己的上游模型名。比如内部配置了deepseek-chat就转发到 DeepSeek配置了llama-3.3-70b-instruct:free就转发到 OpenRouter。这里有一个容易踩坑的点不同服务商可能存在同名模型或者同一个模型在各家的名字不一样。为了避免路由错乱网关里的模型名必须全局唯一。如果出现重名建议加前缀命名比如deepseek/deepseek-chat、openrouter/deepseek-chat这样。但在大多数个人场景里直接用模型名作为全局 Key 已经足够了不必过度设计。2.3 协议兼容层OpenAI 兼容格式第三层是协议。为什么客户端只需要写一套代码就能调用所有模型因为绝大多数 LLM API 服务商都提供了 OpenAI 兼容的 HTTP 接口即请求路径通常是POST /v1/chat/completions请求体包含model、messages、temperature等字段鉴权通常是Authorization: Bearer key返回结构也是统一的choicesusage格式。网关要做的事情就是把自己收到的请求尽量原样转发给上游。它本身不负责理解聊天内容只负责协议搬运和模型路由。这也是为什么网关代码可以做到很短以后回过来维护也不会觉得吃力。只要协议兼容网关就很容易替换、增加或下线某个模型服务。3. 环境准备与项目结构写代码之前先把环境准备好。本文的示例以 Python 3.10 为例使用的核心依赖是 FastAPI、Uvicorn、httpx、pydantic、PyYAML 和 python-dotenv。版本不需要和我完全一样以你本机能够正常安装为准关键接口在常见版本里是兼容的。3.1 安装依赖建议先创建一个虚拟环境避免依赖污染系统 Pythonpython3 -m venv .venv source .venv/bin/activate # Windows 下为 .venv\Scripts\activate pip install -U pip然后安装依赖。为了方便复制我直接给一份 requirements.txtfastapi0.110.0 uvicorn[standard]0.29.0 httpx0.27.0 pydantic2.6.0 pyyaml6.0.1 python-dotenv1.0.13.2 项目目录结构为了让教程容易上手我把核心逻辑放在单文件app.py里配置放在config.yaml密钥放在.env。实际项目如果变得复杂可以进一步拆分成多个模块但单文件版本更适合理解核心逻辑。llm-gateway/ ├── requirements.txt ├── .env # 保存各家真实 API Key不要提交到 Git ├── config.yaml # 网关主 Key 和上游模型路由配置 └── app.py # 统一网关主程序4. 完整实战用 FastAPI 实现一个免费 LLM 统一网关从这一节开始我们进入到可以运行的代码阶段。目标很简单本地启动一个 HTTP 服务监听 8000 端口对外提供/v1/chat/completions接口客户端无论请求哪个免费模型都只带同一把主 Key。4.1 编写配置文件首先创建配置文件config.yaml。它主要描述两件事网关自己的主 Key以及上游服务商的连接信息。注意上游服务商的实际 API Key不要写在这个文件里而是通过环境变量注入后面会用.env管理。# config.yaml gateway: master_key: sk-gateway-2024 host: 0.0.0.0 port: 8000 providers: - name: deepseek base_url: https://api.deepseek.com/v1 api_key_env: DEEPSEEK_API_KEY timeout: 120 models: - deepseek-chat - deepseek-reasoner - name: openrouter base_url: https://openrouter.ai/api/v1 api_key_env: OPENROUTER_API_KEY timeout: 120 models: - meta-llama/llama-3.3-70b-instruct:free - mistralai/mistral-7b-instruct:free这里几个字段的含义分别是gateway.master_key客户端访问网关时使用的唯一主 Key。实际项目中应该用足够长的随机字符串。providers[].name服务商别名只用于日志和排查内部不做逻辑判断。providers[].base_url上游服务商的 OpenAI 兼容地址必须是服务商文档里明确提供的地址。providers[].api_key_env上游 API Key 对应的环境变量名避免把真实 Key 写进配置文件。providers[].timeout等待上游响应的时间单位是秒免费模型响应慢时建议设置大一点。providers[].models该服务商下面可以被客户端调用的模型列表。再创建一个.env文件用来保存上游的真实密钥# .env DEEPSEEK_API_KEYsk-your-deepseek-key OPENROUTER_API_KEYsk-your-openrouter-key记得把.env加入.gitignore避免 Key 被提交到 Git 仓库。4.2 编写统一网关主程序 app.py接下来是这篇文章的核心代码。我会把鉴权、路由、转发逻辑全部放在app.py中注释对应关键步骤。# app.py import os import yaml from dotenv import load_dotenv from fastapi import FastAPI, Header, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel, ConfigDict import httpx load_dotenv() with open(config.yaml, r, encodingutf-8) as f: cfg yaml.safe_load(f) MASTER_KEY cfg[gateway][master_key] PROVIDERS cfg[providers] app FastAPI(titleFree LLM Gateway) class ChatRequest(BaseModel): OpenAI 兼容请求体。 除了 model/messages/stream 三个字段外客户端还可能传 temperature、 top_p、max_tokens 等参数所以开启 extraallow 并原样转发。 model: str messages: list stream: bool False model_config ConfigDict(extraallow) def check_master_key(authorization: str): 统一鉴权所有客户端请求只检查主 Key。 if not authorization: raise HTTPException(status_code401, detailMissing Authorization header) token authorization.removeprefix(Bearer ).strip() if token ! MASTER_KEY: raise HTTPException(status_code401, detailInvalid API key) def find_provider(model: str): 模型路由根据 model 字段找到对应的上游服务商配置。 for provider in PROVIDERS: if model in provider[models]: return provider raise HTTPException(status_code404, detailfModel {model} not found) def build_upstream_headers(provider: dict) - dict: 从环境变量读取上游真实 Key构造上游请求头。 api_key os.getenv(provider[api_key_env]) if not api_key: raise HTTPException( status_code502, detailfEnv {provider[api_key_env]} is not set, ) return { Authorization: fBearer {api_key}, Content-Type: application/json, } app.post(/v1/chat/completions) async def chat_completions( req: ChatRequest, authorization: str Header(default), ): check_master_key(authorization) if not req.model.strip(): raise HTTPException(status_code400, detailmodel is required) provider find_provider(req.model) url provider[base_url].rstrip(/) /chat/completions payload req.model_dump(exclude_noneTrue) headers build_upstream_headers(provider) if not req.stream: # 非流式把上游返回的 JSON 原样透传给客户端 async with httpx.AsyncClient(timeoutprovider[timeout]) as client: resp await client.post(url, jsonpayload, headersheaders) if resp.status_code ! 200: raise HTTPException(status_coderesp.status_code, detailresp.text) return resp.json() # 流式直接转发上游的 SSE 事件流 async def event_stream(): async with httpx.AsyncClient(timeoutprovider[timeout]) as client: async with client.stream( POST, url, jsonpayload, headersheaders ) as upstream: async for line in upstream.aiter_lines(): yield line \n return StreamingResponse(event_stream(), media_typetext/event-stream)核心逻辑其实非常短check_master_key完成统一鉴权。find_provider根据model字段定位上游服务商。build_upstream_headers从环境变量读取真实服务商 Key。最后使用httpx.AsyncClient异步转发请求。代码里最容易被忽略但又很重要的是ChatRequest的extraallow配置。如果不允许额外字段客户端传进来的temperature、max_tokens等参数就会被 Pydantic 丢弃上游拿不到这些参数行为就会和直接调用官方 API 不一致。打开这个配置并用model_dump()转发相当于把协议兼容性交给上游判断网关不做多余加工。另外流式和非流式走了两条分支。非流式直接返回 JSON流式则用StreamingResponse把上游 SSE 事件逐行转发。这样做的好处是客户端可以一边接收一边渲染体验更接近官方 API。4.3 运行网关启动前先确认.env里的环境变量已经加载。如果你的终端支持set -a source .env set a可以这样加载常见 IDE 的 run 配置也支持设置环境变量文件。pip install -r requirements.txt set -a source .env set a # Linux/macOS 加载 .env uvicorn app:app --host 0.0.0.0 --port 8000运行成功后终端会输出类似下面的日志INFO: Uvicorn running on http://0.0.0.0:8000 INFO: Application startup complete.4.4 用 curl 验证网关打开一个新终端用 curl 直接验证非流式接口curl http://localhost:8000/v1/chat/completions \ -H Authorization: Bearer sk-gateway-2024 \ -H Content-Type: application/json \ -d {model: deepseek-chat, messages: [{role: user, content: 你好请用一句话介绍自己}]}如果配置和密钥都正确返回结果会和 OpenAI 官方接口非常相似包含id、choices、usage等字段。再测试流式接口curl -N http://localhost:8000/v1/chat/completions \ -H Authorization: Bearer sk-gateway-2024 \ -H Content-Type: application/json \ -d {model: deepseek-chat, messages: [{role: user, content: 给我讲一个冷笑话}], stream: true}如果看到一行行data: {...}增量输出说明流式转发已经正常工作。到这一步网关本身已经跑通了剩下的问题就是如何让各种客户端工具接入。5. 使用统一网关接入各类 LLM 客户端网关对外暴露的是 OpenAI 兼容接口所以凡是支持自定义 API 地址的客户端都可以通过修改 Base URL 和 API Key 接入。下面列举三种最常见的接入方式。5.1 使用 OpenAI SDK 直连安装官方 OpenAI SDK 后只需要把base_url改成网关地址把api_key换成网关主 Keypip install openaifrom openai import OpenAI client OpenAI( api_keysk-gateway-2024, base_urlhttp://localhost:8000/v1, ) resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: 介绍一下你自己}], ) print(resp.choices[0].message.content)这里最关键的认知是SDK 本身并不知道你连的是哪家服务商它只负责按照 OpenAI 协议发送请求。网关收到请求后会根据model字段找到真实的上游模型再把结果返回给 SDK。所以后续不管你切换成哪个免费模型客户端代码都不需要改。5.2 在 LangChain 中接入如果你在用 LangChain 做 Agent 或者 RAG接入方式也很直接。以下是ChatOpenAI的配置示例from langchain_openai import ChatOpenAI llm ChatOpenAI( modeldeepseek-chat, api_keysk-gateway-2024, base_urlhttp://localhost:8000/v1, ) resp llm.invoke(用一句话解释什么是大语言模型) print(resp.content)在 RAG 场景里同样可以通过LLMChain、RetrievalQA等组件把llm实例传进去。只要统一网关里的模型路由表配置到位业务流程完全不需要关心底层到底用的是哪个服务商。5.3 在 ChatBox 等桌面客户端中配置很多非开发者也想用图形界面体验多模型切换。以常见 AI 桌面客户端为例设置页里都会有“API 地址 / Base URL”和“API Key”两个输入框你只需要把地址填成http://localhost:8000/v1把 Key 填成网关主 Key 即可。也有一类工具使用config.toml保存模型服务配置。举个例子[model_provider] name llm-gateway base_url http://localhost:8000/v1 api_key sk-gateway-2024 model deepseek-chat如果你的客户端提示类似“无法加载 config.toml”或者“model 字段不合法”优先检查model的值是否在网关的config.yaml的models列表里。客户端只会原样把字符串传给网关真正判断模型是否存在的是网关本身。还有一类较新的 CLI 编码工具可能会优先调用/v1/responses端点而不是/v1/chat/completions。如果工具连接网关后提示某个 endpoint 不支持可以在网关里额外增加一个/v1/responses转发接口整体思路和chat/completions基本一致只是请求路径和字段略有不同。遇到这类问题时不要先怀疑 Key 配错了先确认工具请求的到底是哪个端点。6. 常见问题与排查思路统一网关本身不复杂但接入不同服务商时报错类型会五花八门。下面整理了一份高频问题对照表帮助你在第一时间定位方向。问题现象常见原因解决思路401 Invalid API key网关主 Key 不正确检查 Authorization 请求头是否带了正确的 Bearer Token404 Model not found模型名不在网关配置里检查 config.yaml 的 models 列表并确认客户端传入的 model 值502 Env xxx is not set上游服务商 Key 未加载检查 .env 文件和进程环境变量400 maximum context length上下文 token 超长精简 messages、分块、或切换更大上下文模型selected model is at capacity免费模型暂时繁忙等待重试或切换到备用模型上游 400 reasoning_content must be passed back推理模型多轮要求回传思考字段客户端完整保留上一轮返回并原样回传流式接口无输出客户端没有处理 SSE 增量数据检查是否使用 -N / 流式解析逻辑下面挑几个最典型的报错展开说明。上下文超长问题。当你把整份长文档直接塞进 messages 时上游会提示类似this models maximum context length is 1048576 tokens, however your prompt has ... tokens。这说明输入长度超过了模型的上下文窗口。解决办法不外乎三个方向精简历史消息、启用文档分块后再检索、或者把模型切换成支持更长上下文的版本。网关在这个环节能做的只是把上游错误原样暴露出来方便客户端定位不要在网关层静默吞掉错误。免费模型繁忙问题。免费档模型的并发能力通常很有限高峰期很容易返回selected model is at capacity之类的提示。个人项目的处理思路是增加一层异常重试检测到容量错误时等待几秒后换一个备用模型重试也可以把同一个模型名映射到两个不同服务商用轮询策略分发。需要提醒的是免费额度都有服务商的限流规则重试时要遵守退避策略不要写成无限快速循环。推理模型的 thinking 字段回传问题。这是一个比较隐蔽的坑。当上游是带思考模式的推理模型时多轮对话可能会返回额外的reasoning_content字段表示模型思考过程的内容。部分服务商要求你把这个字段原样保存并在下一轮请求时一起回传否则会直接拒绝请求。网关如果自作聪明地过滤掉未知字段反而会导致上游报错。所以前面代码里才特意给ChatRequest开了extraallow并用model_dump()把全部字段都转发出去。遇到类似 400 报错时不要急着改网关先在客户端检查上一轮返回内容是否被完整保留。模型名不一致问题。有些客户端的模型列表是定时拉取网关目录的如果它请求时传入的模型名不在config.yaml的 models 列表里网关会返回 404 Model not found。这类报错通常不是网络问题而是配置不一致问题。先检查客户端那边配置的 model 值再看网关配置文件里的 models 列表。需要提醒的是不同服务商对模型名的大小写、连字符、版本后缀都很敏感不要凭印象写。7. 最佳实践与工程建议跑通 demo 之后如果要把这套网关用于真实项目还需要在密钥管理、限流、日志和数据安全几个方向做完善。7.1 密钥管理与安全边界网关的主 Key 和上游服务商 Key 必须分离。客户端只应该拿到网关主 Key上游 Key 通过环境变量或专门的密钥管理服务注入不要写进配置文件也不要通过任何接口返回给客户端。主 Key 建议使用较长的随机字符串例如sk-gw-前缀加 32 位以上随机内容万一泄露直接在 config.yaml 中替换并重启服务即可不需要通知客户端修改。因为所有客户端都只认这一个 Key更新成本非常低。另一个安全边界是不要从客户端请求中动态拼接base_url。所有上游地址只能来自 config.yaml 白名单否则网关会被滥用成任意 HTTP 转发代理带来不可控的安全风险。代码里传model字段去做路由映射而不是传 URL 参数。7.2 限流与配额控制免费模型的免费额度是稀缺资源网关最好在入口层加上限流防止某个调用方把额度全部打满。最简单的做法是维护一个内存计数器限制每个 Api Key 每分钟的最大请求数团队使用场景建议引入 Redis 做滑动窗口限流再把限流规则做成可配置项。注意不要只做网关入口的限流还要观察上游返回的 429 状态码把上游限流信息记录到日志里。7.3 日志、追踪与成本统计每一条请求都建议记录以下信息请求时间、模型名、路由到的服务商、耗时、token 用量、返回状态码。特别是 token 用量免费额度是有上限的统计后你才能知道哪个模型消耗最多、哪个模型总是失败。日志格式建议直接用 JSON方便后续接入日志平台做检索和分析。如果同时跑多个实例还要为每个请求生成一个trace_id这样从客户端到网关再到上游的完整链路才能串起来。7.4 兼容性与维护策略ChatRequest开启extraallow是保证协议兼容性的关键。大模型 API 的参数一直在演进比如新增的工具调用、结构化输出字段网关如果定义了一长串固定参数很快就会过时让未知字段原样透传反而能减少维护成本。每次新增模型时先在测试环境验证一次非流式和流式调用再更新生产配置不要直接在线上改配置实验。7.5 合规与数据安全调用免费 LLM API 之前要确认服务商的开发者协议特别是免费额度的使用条件和商用限制。公司项目如果涉及敏感数据还要评估模型服务商的数据留存策略判断是否允许把业务数据发送到对应的模型服务。必要时在网关层增加内容脱敏、敏感词过滤或审批流程避免数据违规。这里的基本原则是先看条款再上生产。8. 总结与学习路线看到这里你已经完成了一个最小可用的 LLM API 统一网关它有一个统一鉴权层、一张模型路由表、一层 OpenAI 兼容协议转发能够把多个免费模型服务收敛到同一把 Key 后面。这个架构虽然很小但它把“多模型接入”“统一鉴权”“协议兼容”三个关键问题都覆盖到了。接下来如果你想继续深入建议按下面顺序去研究OpenAI 官方 API 文档里的参数细节例如temperature、top_p、tool call、response_format是如何参与请求转发的。SSE 协议以及如何把流式响应包装成客户端更容易消费的事件流格式。给网关增加 Redis 缓存、语义缓存让相同问题不再重复消费 token。把模型路由从静态配置升级成动态策略例如按成本、按延迟、按成功率自动选择上游。如果这篇文章对你有帮助可以收藏备用下次接入新模型或排查 API 报错时直接对照配置和清单来检查能省不少时间。有问题也可以在评论区交流我看到后会继续补充完善。
返回列表