上半年我接了好几个LLM相关的项目,几乎每个都绕不开同一个问题:模型推理本身只是一部分,真正让团队头疼的是把模型能力稳定地暴露成API给上层业务调用。试过Flask、想过用Django,最后兜兜转转都回到FastAPI。这篇就来系统讲讲,为什么LLM开发的生产级API层,FastAPI几乎是最优解——从框架选型、工程目录、流式响应、鉴权设计,到线上部署和常见报错排查,一次性讲透。
无论你是刚接触LLM开发、还在纠结用哪个Web框架封装模型接口,还是已经写完推理脚本、卡在“怎么把功能变成别人能调的API”,或者正被线上各种超时、限流、模型名报错折磨,这篇文章都能给你一套可以直接“抄作业”的答案。
1. 先搞清楚:LLM 服务的 API 层到底在解决什么问题
很多刚入门的朋友会把“大模型开发”等同于“写推理代码”,以为用 Transformers 加载模型、跑个 prompt 就算完事了。真放到业务里完全不是这么回事。你辛苦写好一个 model.generate(),第一步就卡在怎么让前端聊天窗口、后端业务系统、内部知识库机器人来调用它。
1.1 从模型到服务,中间差了整整一层网关
你把一个 LLM 能力开放出去,至少要解决几个问题:请求怎么进来、参数怎么校验、并发怎么扛、上游模型怎么切换、结果怎么返回、调用方怎么鉴权、日志怎么留。这些问题全部落在模型推理代码之外,属于典型的 API 网关职责。
我见过不少团队直接把 transformers 的推理脚本用 Flask 包一层就扔上线,刚开始确实快,请求一多就原形毕露。尤其 LLM 服务有个极其特殊的点:单次推理耗时极长。普通接口 50ms 就返回了,LLM 接口动辄 3~10 秒,流式输出甚至能持续 30 秒以上。这意味着同样的并发量下,连接数是传统接口的几十倍,服务器的线程、内存、文件描述符全在硬扛。这时候一个不支持异步的 Web 框架,压测一上就是灾难现场。
1.2 LLM 调用场景独有的三个技术特征
第一个特征是长连接。一次完整的大模型对话包含用户提问、模型逐字返回、前端实时渲染整个过程,HTTP 连接必须全程保持,这对 Web 框架的并发模型要求极高。
第二个特征是流式输出。用户在对话框里看到“一个字一个字蹦出来”的效果,底层是 SSE(Server-Sent Events)或者 WebSocket 在持续推流。而主流 LLM 提供商,比如各种 OpenAI 兼容接口和各大国产模型厂商,返回的流式协议基本都是text/event-stream的 SSE 格式。
第三个特征是模型上游的不可控性。你要对接的可能不是本机部署的 Ollama,而是云上的大模型 API、内部统一网关、或者其他团队部署的推理服务。这些上游本身有各自的鉴权、限流、超时策略,你的 API 层必须做好中转、重试、熔断和超时控制。
换句话说,LLM 开发里真正的技术难点不在“调模型”,而在怎么把模型调用封装成一个稳定、安全、可观测、能扛住真实业务压力的 API 服务。而这一层的技术选型,直接决定了你的项目到底能不能叫“生产级应用”。
2. FastAPI 凭什么能扛住 LLM 的调用场景
选 Web 框架这事儿,放到普通业务后端可能没那么敏感,但放到 LLM 服务场景就完全不一样了。FastAPI 能在一堆框架里冒出来,不是因为它“新”,而是因为它的底层设计恰好长在 LLM 服务的痛点上了。
2.1 async 异步模型:让 10 秒长耗时不再堵死线程
先说并发模型。传统 Flask 是同步 WSGI 模型,一个请求占一个线程,线程池默认就那么大。遇到 LLM 这种动辄十几秒才返回的接口,线程被占住不放,后面的请求全部排队,并发能力直接崩盘。
FastAPI 是基于 Starlette 构建的 ASGI 框架,天生就是异步的。它在处理 LLM 这种 IO 密集型任务时,一个 event loop 可以同时管理成千上万个挂起的连接。你可以把 event loop 理解成一个大堂经理:真正干活的是各个“跑腿”协程,经理只管登记谁在等什么,谁回来了就喊一声,全程不用傻等着。
这意味着同样的单机资源,FastAPI 能撑住的 LLM 并发连接数,是同步框架的好几倍。而且你可以直接在路由函数里写async def,也可以对接异步 HTTP 客户端去调用上游模型 API。整个调用链从“收到用户请求”到“转发给模型服务”,全程无阻塞,这才是 LLM 网关该有的姿态。
2.2 流式响应与 SSE:把“打字机效果”变成标准操作
LLM 应用的体验核心是流式输出。用户按下回车后,希望立刻看到回复,而不是干等 5 秒后一次性刷出整段文字。FastAPI 对这类需求支持得非常顺滑,一个简单的生成器再加一个StreamingResponse,就能把模型流式吐出的 token 实时转发给前端。
from fastapi.responses import StreamingResponse async def token_generator(prompt: str): # 伪代码:异步请求上游模型,逐 token 产出 async for token in llm_client.stream_chat(prompt): yield f"data: {token}\n\n" @app.post("/v1/chat/completions") async def chat_completion(body: ChatRequest): return StreamingResponse( token_generator(body.prompt), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"} )中间那个X-Accel-Buffering: no是我踩过坑后才加上的,背景是后端前面挂了 Nginx 做反向代理时,Nginx 默认会缓冲整个响应体,结果是前端等了半天一次性看到所有文字,流式效果全没了。这个参数就是告诉 Nginx 别缓冲,老老实实往上转发。
2.3 Pydantic 模型校验:杜绝脏数据打到大模型脸上
LLM 的接口参数很敏感,model传错了直接报 400,max_tokens传个负数模型直接拒绝,messages里缺了 role 字段也可能被上游打回。FastAPI 本身就基于 Pydantic 做请求体校验,请求还没进你的业务逻辑,参数已经在入口处被清洗过一遍了。
更贴心的是,FastAPI 会自动根据你的 Pydantic 模型生成 OpenAPI 文档。我接过一个需求,要对接国内某个大模型厂商的新模型,研发团队七嘴八舌争论字段命名,最后我把openapi.json导出来发到群里,所有人瞬间闭嘴。自动生成文档这个能力,在多人协作的 LLM 项目里能省下大量的沟通成本。
2.4 自动 OpenAPI 文档:联调效率的隐形加速器
做 LLM 应用有个高频场景:前端同事跑过来问“你这个接口的 messages 到底怎么传?”“temperature 支持小数吗?”“流式响应的数据结构长什么样?”如果靠口头沟通,一轮又一轮,效率极低。FastAPI 启动后直接给你一个 Swagger 交互式文档地址,前端把参数填进去就能直接调,还能实时看到流式返回的数据结构。
这功能对一个团队的实际价值,怎么强调都不过分。它把“接口文档”从一句句打字沟通,变成了打开浏览器就能直观操作的实时界面。配合自动生成的 OpenAPI 规范,甚至可以进一步连到各种 API 管理平台做接口资产统一管理。
3. 从零搭一个 LLM 网关:目录结构与核心实现
选型归选型,真正动手写的时候考验的是工程结构。我看过不少 FastAPI 项目,代码全塞在两个文件里,路由有几个写几个,配置散落各处,密钥硬编码。这种项目别说是生产级,连给同事 review 都是煎熬。这里分享一下我认为比较适合 LLM 网关的目录和核心模块设计。
3.1 项目目录结构怎么拆,才配叫“生产级”
规则很简单:配置跟代码分离,路由跟业务分离,模型跟工具分离,密钥永远不进代码库。下面这个结构是我在几个项目里反复打磨后沉淀下来的模板:
app/ ├── main.py # 应用入口,创建 FastAPI 实例,注册路由 ├── core/ │ ├── config.py # 读取环境变量,集中管理配置 │ ├── security.py # API Key 校验、签名、密钥管理 │ └── logging.py # 日志配置 ├── api/ │ ├── v1/ │ │ ├── router.py # v1 版本路由聚合 │ │ └── endpoints/ │ │ ├── chat.py # 对话/补全接口 │ │ └── models.py # 模型列表查询接口 ├── schemas/ │ ├── chat.py # Pydantic 请求/响应模型 │ └── common.py # 公共数据结构 ├── services/ │ ├── llm_client.py # 上游模型调用封装 │ ├── stream_handler.py # 流式转发处理 │ └── auth_service.py # 鉴权业务逻辑 └── utils/ ├── retry.py # 重试与熔断工具 └── metrics.py # 调用量、耗时指标每个目录各司其职。core只放跟框架生命周期相关的配置和通用能力,api是路由层,只做参数解析和状态码转换,services才放真正的业务逻辑。一个 LLM 接口调用链大致是这样的:请求进来 → 路由解析 → Pydantic 校验 → 依赖注入鉴权 → 业务校验(比如检查余额、配额) → 调用 upstream 模型 → 返回结果或流式字块。
你去看很多优秀开源项目的源码,结构基本没跑出这个框架。核心原则就是分层清晰,别让路由函数里堆一堆底层逻辑。目录拆得清,后面加新模型、加新接口都是往里加模块,而不是在大一统文件里代码越搅越乱。
3.2 把 LLM 上游调用封装成一个可替换的客户端
LLM 项目经常要应对同一个问题:“今天用 A 模型,明天要切 B 模型”“开发环境用 Ollama 本地模型,生产环境用云上 API”。如果把模型调用逻辑写死在业务代码里,每切一次就改一遍代码,非常愚蠢。正确的姿势是做成策略切换。
我的做法是基于一个统一的异步客户端接口,兼容 OpenAI SDK 协议,各家国产模型也基本都兼容了这套协议,所以一个客户端能统一管理。
class LLMClient: def __init__(self, base_url: str, api_key: str, default_model: str): self._client = AsyncOpenAI(base_url=base_url, api_key=api_key) self._default_model = default_model async def chat_stream(self, messages, model=None, **kwargs): model = model or self._default_model stream = await self._client.chat.completions.create( model=model, messages=messages, stream=True, stream_options={"include_usage": True}, **kwargs ) async for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: yield chunk.choices[0].delta.content async def chat_once(self, messages, model=None, **kwargs): resp = await self._client.chat.completions.create( model=model or self._default_model, messages=messages, **kwargs ) return resp.choices[0].message.content配置通过core/config.py里的base_url和api_key区分环境。比如本地 Ollama 部署时填http://localhost:11434/v1,生产环境换成云厂商或者内部统一网关的地址,代码一行都不用改。我经常在群里看到有人问“DeepSeek API 怎么调用”,其实核心就是用 OpenAI 兼容客户端,把 base_url 和 api_key 配成 DeepSeek 的就行。
有一点要特别强调:模型名参数别写死在代码里,让它跟随请求传入,至少也要做成环境配置。热词里那些像api error: 400 the supported api model names are deepseek-flash, deepseek-v4之类的报错,十有八九就是模型名写错或者用了旧版本的模型名。把模型列表做成一个GET /v1/models接口动态下发,前端一拉就知道有哪些模型可用,能少掉一半这种低级报错。
3.3 流式接口的实现细节:别让 token 死在半路上
流式接口是 LLM 网关最核心的出口,也是最容易出 bug 的地方。简单用StreamingResponse包一层生成器没错,但有几个细节不注意,线上体验会非常糟糕。
第一个细节是心跳保活。如果你调用上游模型后,上游迟迟不出第一个 token(冷启动、排队、load model 都可能导致),前端可能会判定连接超时直接断开。解决方案是生成器里做个循环,如果超过一定时间没有新内容产出,就主动 yield 一个注释行作为心跳。
async def sse_wrapper(): try: empty_rounds = 0 async for chunk in llm_client.chat_stream(messages): if chunk: yield f"data: {json.dumps({'content': chunk})}\n\n" empty_rounds = 0 else: empty_rounds += 1 if empty_rounds > 20: yield ": keep-alive\n\n" empty_rounds = 0 except asyncio.CancelledError: # 客户端断开,及时取消上游请求,避免资源泄漏 await llm_client.abort() raise第二个细节是客户端断开处理。前端页面关了、用户刷新了,HTTP 连接就已经断了,但上游模型可能还在继续生成 token。如果不做取消处理,线程和连接就会被浪费掉。上面代码里的asyncio.CancelledError就是处理这种情况,收不到更多数据时主动取消上游调用。
第三个细节是错误透传。流式过程中,如果上游突然报了个鉴权错误或者限流,你不能把这段堆栈直接吐给前端。正确做法是把错误信息包装成 SSE 的 error 事件,同时在后端日志里记录完整的链路 ID,方便排查“用户报告回复中断”这类问题。
3.4 密钥管理与鉴权:最容易被忽略的安全命门
热词里有个问题格外扎眼:“使用 llm 时如何防止密钥等鉴权信息泄露”。这个坑我在不少公司见过了——有人为了图省事,把云厂商的 API Key 直接写在 FastAPI 的常量配置文件里,甚至打进前端代码里。一旦代码泄露到 GitHub,或者前端被扒,云上的模型额度分分钟被刷爆,账单能几个月不给结。
安全做法就一条原则:上游模型的密钥永远只存在于服务端,永远不下发到浏览器调用链中。
你做的是一个网关层,正确的鉴权设计应该是这样:
- 对外提供 LLM 服务时,自己签发一套 API Key 用于识别调用方客户端;
- 用户请求只携带你自己的 API Key,后端验证通过后,再在服务端替换成上游真实模型密钥去调模型厂商;
- 上游密钥配置在环境变量或密钥管理服务里,代码仓库里绝对不出现明文 Key。
实际开发中,FastAPI 的依赖注入做这个验证非常顺手:
from fastapi import Depends, HTTPException, Security, Header async def verify_api_key(x_api_key: str = Header(...)): if not x_api_key or x_api_key != settings.service_api_key: raise HTTPException(status_code=401, detail="Invalid API Key") @app.post("/v1/chat/completions", dependencies=[Depends(verify_api_key)]) async def chat_completion(body: ChatRequest): ...另外一个我强烈建议的做法:在服务端为每个调用方生成独立的上游 Key,或者使用一个专门的服务账号读取密钥。配合日志审计,哪个调用方跑了多少 token、有没有异常调用行为,全部可以追踪到。密钥这件事,多花一小时设计都不嫌多,等泄露了再去补救就全是被动状态。
4. 生产部署:从开发机到稳定服务
代码写得再好,部署端掉链子照样白给。LLM 服务的部署比普通 Web 服务麻烦不少:依赖多、镜像大(尤其涉及到本地推理)、对 GPU 调度有要求、网络链路长。我这里把部署链路拆开讲,从进程模型到容器化到反向代理,从零讲透。
4.1 Uvicorn vs Gunicorn:异步进程模型到底怎么搭配
很多朋友会用uvicorn app:app --host 0.0.0.0 --port 8000直接启动服务就完事了。开发环境这么干没问题,生产环境单进程也够用,但想用满多核 CPU,就得考虑多进程。
FastAPI 属于 ASGI 应用,生产环境常用 Gunicorn 做进程管理器、Uvicorn Worker 做底层 ASGI 协议处理。这套组合好在哪里?Gunicorn 负责管理主进程、worker 数量、优雅重启,Uvicorn 负责真正的 HTTP 解析和异步调度。
gunicorn app.main:app \ -k uvicorn.workers.UvicornWorker \ -w 4 \ -b 0.0.0.0:8000 \ --timeout 120 \ --graceful-timeout 30-w 4的设置我一般按 CPU 核数来,公式习惯用2 * CPU核心数 + 1,比如 4 核机器开 9 个 worker。这里需要提醒一下:如果你用了内存态存储或者进程中缓存做限流计数,多进程模式下每个进程各自为政,需要一个外部存储(比如 Redis)来做统一计数。限流这事在 LLM 场景特别重要,因为模型调用是个“钱”和“算力”双消耗的行为,不做配额控制分分钟账单爆炸。
设置--timeout 120也很有讲究。Gunicorn 默认的 worker 超时是 30 秒,但 LLM 接口动辄几十秒的请求,超时设置小了,长耗时的流式请求会被 Gunicorn 直接 kill 掉。我建议把同步接口超时和流式接口超时分开设计:流式接口对 Gunicorn 来说,只要连接还活着就不算超时,所以问题不大;但如果你有用到同步阻塞的守护逻辑,就一定要把 timeout 调大,同时配合心跳机制。
4.2 Docker 化部署:镜像体积、健康检查、启动顺序
容器化是生产部署的必经之路。但 LLM 项目的 Docker 镜像有个常见通病:依赖太重,镜像体积动辄几个 G。
FROM python:3.11-slim AS base ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-w", "3", "-b", "0.0.0.0:8000", "--timeout", "120"]这里要提醒一个部署细节:如果目标是只部署 API 网关,不做本地推理,镜像里不需要装任何模型相关的大依赖,基础镜像用python:3.11-slim就够。本地模型服务的容器化是另一套故事,通常需要 GPU 运行时、更复杂的健康检查,两者不要混在一个镜像里。
Docker Compose 里建议加两个关键配置:健康检查和重启策略。
services: llm-gateway: build: . ports: - "8000:8000" environment: - UPSTREAM_BASE_URL=${UPSTREAM_BASE_URL} - SERVICE_API_KEY=${SERVICE_API_KEY} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 10s timeout: 5s retries: 3 start_period: 20s restart: unless-stopped健康检查端点我通常单独写一个/health路由,不跑任何外部依赖检查,只返回 200。更激进的方案是检查上游模型配置是否就绪,但这样容易误报;折中做法是/health/live和/health/ready分开,就绪检查里带上上游连通性。很多人在本地调试时遇到failed to connect to the docker api at npipe:////pipe/dockerdesktoplinuxen类似的报错,几乎都是本地 Docker Desktop 根本没启动,或者是容器内没装 curl。在 3.11-slim 这种精简镜像里默认没有 curl,健康检查建议改用 Python 的方式实现,或者安装 curl 也要显式装。
4.3 反向代理与网关配套:Nginx 或云上负载均衡的关键配置
生产环境一般不会让 FastAPI 直接裸奔对公网,中间会挂一层 Nginx、云负载均衡或者 API 网关。这一层配置不对,流式效果归零甚至连接频繁断开。
Nginx 侧最关键的三个配置项我直接给出来:
location /v1/chat/completions { proxy_pass http://llm-gateway:8000; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; chunked_transfer_encoding on; proxy_read_timeout 300s; proxy_send_timeout 300s; }proxy_buffering off是流式请求的命根子,不关掉的话,Nginx 会攒一大段才发给前端,或者更糟,直到上游结束才一次性吐出来。proxy_read_timeout要调大,LLM 长请求多,默认 60 秒很容易断。proxy_set_header Connection ""是让 Nginx 跟后端之间走 HTTP/1.1 长连接,避免每次请求都重新建连。
云端负载均衡配置的思路同理,重点都是关闭缓冲、调大超时时间。如果这些参数没配好,最典型的症状就是“前端明明收到了 200,但等了半天一个字都不出,最后还断连”。这些问题排查起来费劲,配置的时候一步到位才最省心。
5. 常见报错与排查实录
写到这里,我觉得有必要把实际运维过程中遇到过的高频报错集中整理一下。LLM 服务链路太长,故障定位的复杂度是普通 Web 服务的倍数级,我自己经历过很多次“明明测试环境好好的,上了生产就报错”的情况。
5.1 上游模型报错的典型规律
生产环境最常见的返回错误,一套规律可以提前规避。
| 报错形态 | 典型原因 | 处理建议 |
|---|---|---|
api error: 400 the supported api model names are ... | 模型名参数传了不存在或不支持的名字 | 通过/v1/models动态获取模型列表,别硬编码 |
api error: 400 this model's maximum context length is 1048576 tokens... | 上下文长度超限,通常是用户塞入超长文档或知识库命中片段过多 | 入口做 token 数估算与截断,配置上下文长度限制 |
request rejected (429) ... exceeded the 5-hour usage quota | 上游配额或频率限制,触发限流 | 做重试退避,分布式限流,缓存队列,拆分流 |
llm request failed: provider rejected the request schema or tool payload. | 工具调用参数或消息格式不符合 Provider 要求 | 检查 messages 的 role 合法性、tool schema 的 JSON Schema 规范 |
| 连接建立成功但长时间无响应 | 上游在排队推理,或者反向代理缓冲了响应 | 设置合理超时,流式开启心跳保活 |
其中“context too long”这条要重点说。我在做企业知识库问答时经常遇到一个场景:用户问了一个比较复杂的问题,RAG 流程召回了非常多的相关文档片段,一股脑塞给 LLM,结果 token 数直接打爆模型上限。这其实不是框架问题,而是检索结果侧没有做合理的数量与截断控制。在 API 层对输入做 token 估算并设上限,是最实用的兜底防线。
另一个高频事故是 429 限流。很多人以为网络请求被限流就是上游质量不行,其实多数情况是自己没有控制好调用节奏,或者没有做合理的指数退避。这里给大家一个标准的重试逻辑参考:
async def call_with_retry(coro_fn, max_retries=3): for attempt in range(max_retries): try: return await coro_fn() except RateLimitError as e: wait_time = 2 ** attempt + random.uniform(0, 1) logger.warning(f"Rate limited, retry in {wait_time:.2f}s: {e}") await asyncio.sleep(wait_time) raise UpstreamUnavailableError("upstream rate limit exceeded")重试不是简单重复,必须带退避和抖动,否则一堆请求排着队在 429 的瞬间你重试过去又踩同一个 429,白白浪费资源。
5.2 本地部署环节的踩坑实录
本地部署环节,最常见的就是 Ollama 部署和 Docker 相关的坑。
Ollama 本地部署最常踩的坑是端口和网络模式。如果你在容器里跑 Ollama,需要把11434端口映射出来;如果 FastAPI 网关在宿主机上直接调用,base_url写http://localhost:11434/v1是通的;但如果网关也在容器里,就必须用 Docker 容器名 DNS 解析,写成http://ollama:11434/v1。
还遇到过一种情况,本地 Ollama 模型拉取完成后,第一次推理非常慢,因为模型要从磁盘加载到显存,第二次调用才快起来。所以网关的超时和重试设计要考虑到“首 token 延迟”和“稳态首 token 延迟”的差异,别拿第一脚油门的数据去定超时时间。
另一个高频坑是容器内访问 Docker API,很多人本机正常、进容器就报各种连接失败。关键要理解:容器内的网络命名空间跟宿主机是隔离的,宿主机上能用 localhost,容器里就未必。像 Docker Desktop 的 Windows 环境,宿主机上访问别的服务的地址都特殊,不要想当然直接照搬。
5.3 常见的 “GitLab 连不上、Docker、本地推理”组合问题
这类问题一个共同特征:看起来像 A 组件的错误,实际是 B 组件没配置好。比如拉取私有镜像时报login failed. check api token or gitlab version.,这通常不是 Docker 的问题,而是 GitLab 版本和当前 Docker 客户端不兼容,或者 Access Token 权限范围不对。解决办法就是检查 token 的read_registry权限、确认仓库地址和实际的版本是否匹配。
还有failed to connect to the docker api at npipe:////pipe/dockerdesktoplinuxen这类报错,多半是你命令行客户端连 Docker Desktop 时,后者根本没拉起,或者 Linux 环境下的 docker socket 路径不对。排查思路永远是从“最基础的连接”开始一层层往外试,不要一上来就去怀疑代码。
6. 一点个人体会
写了这么多,最后分享几条我自己反复用到、但很少在文档里看到的心得。
第一个心得是:LLM 项目的 API 层,一定要把“可观测性”当一等公民来做。每次调用至少记录 request_id、上游模型、token 消耗、首 token 延迟、总耗时、状态码这六个字段。不要觉得麻烦,等线上出问题、用户投诉“回复变慢”的时候,你就知道这些日志有多值钱。我现在接手任何 LLM 项目,第一件事不是看代码,是看日志里能不能查清一次完整请求的链路。
第二个心得是:流式接口的调试比普通接口难得多,一定写一套自动化测试脚本覆盖“连接中断”“上游超时”“错误事件流”这三个场景,而不是只测“正常返回”的路径。我见过太多项目上线第一天还好好的,第二天 Nginx 配置被改成默认后,流式效果全部失效,前端干等半天。
第三个心得是:如果你做的是一个多模型聚合网关,记得把“模型路由”也做成可配置的。热词里那些围绕 DeepSeek、Ollama、各种 API 的调用问题,本质都是模型接入的配置和管理问题。与其每接一个模型就写一遍胶水代码,不如在网关层维护一个模型注册表,让配置和策略去解决 80% 的重复工作。
LLM 开发走到最后你会发现,模型能力本身的差距在缩小,真正拉开项目档次的,正是这一层看起来不起眼的 API 工程。把 FastAPI 用好了,部署稳了,坑都踩平了,你的 LLM 应用才算真正意义上从“能跑”进化到了“能用”。如果这篇文章能让你在选型或者排障的时候少走几步弯路,那就值了。