news 2026/9/7 5:23:24

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用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 统一鉴权层

第一层是鉴权。网关对外只暴露一个主 Key(Master 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-chatopenrouter/deepseek-chat这样。但在大多数个人场景里,直接用模型名作为全局 Key 已经足够了,不必过度设计。

2.3 协议兼容层(OpenAI 兼容格式)

第三层是协议。为什么客户端只需要写一套代码就能调用所有模型?因为绝大多数 LLM API 服务商都提供了 OpenAI 兼容的 HTTP 接口,即请求路径通常是POST /v1/chat/completions,请求体包含modelmessagestemperature等字段,鉴权通常是Authorization: Bearer <key>,返回结构也是统一的choices+usage格式。

网关要做的事情,就是把自己收到的请求尽量原样转发给上游。它本身不负责理解聊天内容,只负责协议搬运和模型路由。这也是为什么网关代码可以做到很短,以后回过来维护也不会觉得吃力。只要协议兼容,网关就很容易替换、增加或下线某个模型服务。

3. 环境准备与项目结构

写代码之前,先把环境准备好。本文的示例以 Python 3.10+ 为例,使用的核心依赖是 FastAPI、Uvicorn、httpx、pydantic、PyYAML 和 python-dotenv。版本不需要和我完全一样,以你本机能够正常安装为准,关键接口在常见版本里是兼容的。

3.1 安装依赖

建议先创建一个虚拟环境,避免依赖污染系统 Python:

python3 -m venv .venv source .venv/bin/activate # Windows 下为 .venv\Scripts\activate pip install -U pip

然后安装依赖。为了方便复制,我直接给一份 requirements.txt:

fastapi>=0.110.0 uvicorn[standard]>=0.29.0 httpx>=0.27.0 pydantic>=2.6.0 pyyaml>=6.0.1 python-dotenv>=1.0.1

3.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_KEY=sk-your-deepseek-key OPENROUTER_API_KEY=sk-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", encoding="utf-8") as f: cfg = yaml.safe_load(f) MASTER_KEY = cfg["gateway"]["master_key"] PROVIDERS = cfg["providers"] app = FastAPI(title="Free LLM Gateway") class ChatRequest(BaseModel): """OpenAI 兼容请求体。 除了 model/messages/stream 三个字段外,客户端还可能传 temperature、 top_p、max_tokens 等参数,所以开启 extra="allow" 并原样转发。 """ model: str messages: list stream: bool = False model_config = ConfigDict(extra="allow") def check_master_key(authorization: str): """统一鉴权:所有客户端请求只检查主 Key。""" if not authorization: raise HTTPException(status_code=401, detail="Missing Authorization header") token = authorization.removeprefix("Bearer ").strip() if token != MASTER_KEY: raise HTTPException(status_code=401, detail="Invalid API key") def find_provider(model: str): """模型路由:根据 model 字段找到对应的上游服务商配置。""" for provider in PROVIDERS: if model in provider["models"]: return provider raise HTTPException(status_code=404, detail=f"Model '{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_code=502, detail=f"Env {provider['api_key_env']} is not set", ) return { "Authorization": f"Bearer {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_code=400, detail="model is required") provider = find_provider(req.model) url = provider["base_url"].rstrip("/") + "/chat/completions" payload = req.model_dump(exclude_none=True) headers = build_upstream_headers(provider) if not req.stream: # 非流式:把上游返回的 JSON 原样透传给客户端 async with httpx.AsyncClient(timeout=provider["timeout"]) as client: resp = await client.post(url, json=payload, headers=headers) if resp.status_code != 200: raise HTTPException(status_code=resp.status_code, detail=resp.text) return resp.json() # 流式:直接转发上游的 SSE 事件流 async def event_stream(): async with httpx.AsyncClient(timeout=provider["timeout"]) as client: async with client.stream( "POST", url, json=payload, headers=headers ) as upstream: async for line in upstream.aiter_lines(): yield line + "\n" return StreamingResponse(event_stream(), media_type="text/event-stream")

核心逻辑其实非常短:

  1. check_master_key完成统一鉴权。
  2. find_provider根据model字段定位上游服务商。
  3. build_upstream_headers从环境变量读取真实服务商 Key。
  4. 最后使用httpx.AsyncClient异步转发请求。

代码里最容易被忽略但又很重要的是ChatRequestextra="allow"配置。如果不允许额外字段,客户端传进来的temperaturemax_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 官方接口非常相似,包含idchoicesusage等字段。再测试流式接口:

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换成网关主 Key:

pip install openai
from openai import OpenAI client = OpenAI( api_key="sk-gateway-2024", base_url="http://localhost:8000/v1", ) resp = client.chat.completions.create( model="deepseek-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( model="deepseek-chat", api_key="sk-gateway-2024", base_url="http://localhost:8000/v1", ) resp = llm.invoke("用一句话解释什么是大语言模型") print(resp.content)

在 RAG 场景里,同样可以通过LLMChainRetrievalQA等组件把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.yamlmodels列表里。客户端只会原样把字符串传给网关,真正判断模型是否存在的是网关本身。

还有一类较新的 CLI 编码工具,可能会优先调用/v1/responses端点而不是/v1/chat/completions。如果工具连接网关后提示某个 endpoint 不支持,可以在网关里额外增加一个/v1/responses转发接口,整体思路和chat/completions基本一致,只是请求路径和字段略有不同。遇到这类问题时,不要先怀疑 Key 配错了,先确认工具请求的到底是哪个端点。

6. 常见问题与排查思路

统一网关本身不复杂,但接入不同服务商时,报错类型会五花八门。下面整理了一份高频问题对照表,帮助你在第一时间定位方向。

问题现象常见原因解决思路
401 Invalid API key网关主 Key 不正确检查 Authorization 请求头是否带了正确的 Bearer Token
404 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 model's maximum context length is 1048576 tokens, however your prompt has ... tokens。这说明输入长度超过了模型的上下文窗口。解决办法不外乎三个方向:精简历史消息、启用文档分块后再检索、或者把模型切换成支持更长上下文的版本。网关在这个环节能做的,只是把上游错误原样暴露出来,方便客户端定位,不要在网关层静默吞掉错误。

免费模型繁忙问题。免费档模型的并发能力通常很有限,高峰期很容易返回selected model is at capacity之类的提示。个人项目的处理思路是增加一层异常重试:检测到容量错误时,等待几秒后换一个备用模型重试;也可以把同一个模型名映射到两个不同服务商,用轮询策略分发。需要提醒的是,免费额度都有服务商的限流规则,重试时要遵守退避策略,不要写成无限快速循环。

推理模型的 thinking 字段回传问题。这是一个比较隐蔽的坑。当上游是带思考模式的推理模型时,多轮对话可能会返回额外的reasoning_content字段,表示模型思考过程的内容。部分服务商要求你把这个字段原样保存,并在下一轮请求时一起回传,否则会直接拒绝请求。网关如果自作聪明地过滤掉未知字段,反而会导致上游报错。所以前面代码里才特意给ChatRequest开了extra="allow",并用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开启extra="allow"是保证协议兼容性的关键。大模型 API 的参数一直在演进,比如新增的工具调用、结构化输出字段,网关如果定义了一长串固定参数,很快就会过时;让未知字段原样透传,反而能减少维护成本。每次新增模型时,先在测试环境验证一次非流式和流式调用,再更新生产配置,不要直接在线上改配置实验。

7.5 合规与数据安全

调用免费 LLM API 之前,要确认服务商的开发者协议,特别是免费额度的使用条件和商用限制。公司项目如果涉及敏感数据,还要评估模型服务商的数据留存策略,判断是否允许把业务数据发送到对应的模型服务。必要时在网关层增加内容脱敏、敏感词过滤或审批流程,避免数据违规。这里的基本原则是:先看条款,再上生产。

8. 总结与学习路线

看到这里,你已经完成了一个最小可用的 LLM API 统一网关:它有一个统一鉴权层、一张模型路由表、一层 OpenAI 兼容协议转发,能够把多个免费模型服务收敛到同一把 Key 后面。这个架构虽然很小,但它把“多模型接入”“统一鉴权”“协议兼容”三个关键问题都覆盖到了。

接下来如果你想继续深入,建议按下面顺序去研究:

  1. OpenAI 官方 API 文档里的参数细节,例如temperaturetop_ptool callresponse_format是如何参与请求转发的。
  2. SSE 协议,以及如何把流式响应包装成客户端更容易消费的事件流格式。
  3. 给网关增加 Redis 缓存、语义缓存,让相同问题不再重复消费 token。
  4. 把模型路由从静态配置升级成动态策略,例如按成本、按延迟、按成功率自动选择上游。

如果这篇文章对你有帮助,可以收藏备用,下次接入新模型或排查 API 报错时,直接对照配置和清单来检查,能省不少时间。有问题也可以在评论区交流,我看到后会继续补充完善。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:23:22

AI简历网站开发实战:从FastAPI架构到安全防护与获客变现

最近关于 AI 简历网站的讨论热度很高&#xff0c;但很多人对它存在一个严重误判&#xff1a;既然 ChatGPT 能写简历&#xff0c;为什么还要单独做一个 AI 简历网站&#xff1f;这里必须说清楚——模型能写简历&#xff0c;和产品能交付一份合格的简历&#xff0c;是两个层面的事…

作者头像 李华
网站建设 2026/9/7 5:23:18

Buzz 离线转写:从音频文件到 SRT 字幕的 3 步路径

Buzz 离线转写&#xff1a;从音频文件到 SRT 字幕的 3 步路径 【免费下载链接】buzz Buzz transcribes and translates audio offline on your personal computer. Powered by OpenAIs Whisper. 项目地址: https://gitcode.com/GitHub_Trending/buz/buzz 手上有会议录音…

作者头像 李华
网站建设 2026/9/7 5:21:47

Qwen3-VL视觉语言模型落地实战:数据处理、微调与部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:21:39

薪酬设计实战:3P模型破解定薪、调薪与绩效分配难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华