完整的 LLM 网关架构指南,解决多服务硬编码 API 密钥、成本失控、提供商锁定的痛点。包含速率限制、成本追踪、提供商切换方案。
你的 LLM 账单突然翻了三倍,团队里没人能说清楚为什么。听起来熟悉吗?如果你从堆栈中的每个服务都直接调用 OpenAI 或 Anthropic,你面临的不是集成问题,而是治理问题。这正是 LLM API 网关大展身手的地方,在 Node.js 中搭建一个所需时间不到调试下一张惊人账单的一半。
不用网关实际会发生什么。每个调用 LLM 的服务都硬编码自己的 API key、自己的重试逻辑、自己的模型名称。然后某天你想出于成本原因从 GPT 切换到 Claude,结果得在六个仓库里 grep。或者某个初级开发的脚本对速率限制的端点进入了无限重试循环,你的账单一夜之间飙升。或者,更糟的是,你对哪个团队或功能实际在驱动成本毫无掌握。
网关坐在你的应用代码和模型提供商之间。每个请求都流经一个关键节点,这意味着你得到集中的路由、集中的成本追踪、集中的速率限制,以及一个无须触及应用代码就能交换提供商的地方。企业级模型 API 支出已经超过 84 亿美元并持续攀升,其中大部分支出零集中控制。这不是你以后要应对的规模问题,而是你已经身在其中的规模问题。
没人警告过的陷阱:在项目后期添加网关比从第一天就加要痛苦得多,因为那时每个服务都已经用自己一套独特的调用约定了。如果你是从零开始,在写第一个 prompt 调用前就把这个接好。
你这里有三个不错的选项,为你的情况选错一个很容易踩坑。
如果你的团队想拥有每一个按钮且不介意运行另一个服务,选择 LiteLLM。如果你想要护栏(PII 检测、prompt 注入检查、内容审核)而不想自己写这些逻辑,Portkey 预建的集合很难被超越,特别是现在它完全开源了。如果你的流量已经经过 Cloudflare,AI Gateway 是最轻松的选择,因为没有新的基础设施要搭建。
在这个演练中我选择 LiteLLM,因为它能最清楚地展示底层实际发生的事,而且这种理解即使你最后选择 Portkey 或 Cloudflare 也能迁移。
LiteLLM 作为代理服务器运行,提供 OpenAI 兼容的 API,所以你的 Node 代码基本不变,只需指向一个不同的基础 URL。
# litellm-config.yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-5
api_key: os.environ/ANTHROPIC_API_KEY
router_settings:
routing_strategy: least-busy
fallbacks:
- gpt-4o: [claude-sonnet]
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
# spin up the proxy locally (Docker keeps this reproducible across environments)
docker run -d \
-v $(pwd)/litellm-config.yaml:/app/config.yaml \
-e OPENAI_API_KEY=$OPENAI_API_KEY \
-e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
-e LITELLM_MASTER_KEY=$LITELLM_MASTER_KEY \
-p 4000:4000 \
ghcr.io/berriai/litellm:main-latest \
--config /app/config.yaml
现在你的 Node.js 代码调用代理而不是直接调用提供商。如果你已经在使用 OpenAI SDK,这只是改一行基础 URL 的问题:
// lib/llm-client.ts
import OpenAI from "openai";
const gateway = new OpenAI({
apiKey: process.env.LITELLM_MASTER_KEY,
baseURL: "http://localhost:4000/v1",
});
export async function askModel(prompt: string) {
const response = await gateway.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: prompt }],
});
return response.choices[0].message.content;
}
就这么简单。你的应用代码完全不知道它在和代理而不是直接和 OpenAI 通话,以后交换提供商只需改配置,不需改代码。
这是一个早期咬过我的模式。如果你的应用回答常见问题,"你们的退款政策是什么"、"我怎样重置密码",你在一遍遍为几乎相同的答案付全价。语义缓存通过检查是否已经回答过语义相似的 prompt,而不是要求完全字符串匹配,解决了这个问题。
对于重复性工作负载,语义缓存单独可以削减 30 到 50 的成本,路由策略结合缓存可以削减 40 到 70。堆栈多个优化技术,你看得到生产工作负载上 50 到 80 的成本削减。这不是细微的调整,而是一个可持续的 AI 功能和一个财务部门不断要求你证正当性的功能之间的区别。如果你想要完整的分析,我在我的 LLM 推理成本优化文章中深入讨论了更多的成本数学。
// lib/semantic-cache.ts
import { createClient } from "redis";
import OpenAI from "openai";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const embeddings = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const SIMILARITY_THRESHOLD = 0.95;
async function getEmbedding(text: string): Promise<number[]> {
const res = await embeddings.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
return res.data[0].embedding;
}
function cosineSimilarity(a: number[], b: number[]): number {
const dot = a.reduce((sum, val, i) => sum + val * b[i], 0);
const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
return dot / (magA * magB);
}
export async function cachedAsk(prompt: string, askFn: (p: string) => Promise<string>) {
const promptEmbedding = await getEmbedding(prompt);
const cachedKeys = await redis.keys("semcache:*");
for (const key of cachedKeys) {
const entry = JSON.parse((await redis.get(key)) ?? "{}");
if (!entry.embedding) continue;
if (cosineSimilarity(promptEmbedding, entry.embedding) >= SIMILARITY_THRESHOLD) {
return entry.response; // cache hit, skip the model call entirely
}
}
const response = await askFn(prompt);
await redis.set(
`semcache:${Date.now()}`,
JSON.stringify({ embedding: promptEmbedding, response }),
{ EX: 86400 }
);
return response;
}
用线性相似度检查扫描所有缓存键在小规模时可以,但一旦你有超过几千个缓存条目,就可以考虑向量数据库(Pinecone、Qdrant 或 pgvector)。模式保持一致,只有查找机制改变。
提供商会宕机。速率限制会被击中。延迟尖峰会在最糟糕的时刻发生。如果你的网关不能优雅地处理这些,一个不稳定的提供商会让你整个功能下线。
断路器监视重复的失败,一旦超过阈值,就会"打开",停止向故障提供商发送请求一段冷却期,而是立即故障转移到备份。这正是 LiteLLM 的故障转移配置在代理级别处理的,但值得理解这个模式,这样你能推理它,也能为任何绕过网关的东西在应用层添加相同的保护。
// lib/circuit-breaker.ts
import CircuitBreaker from "opossum";
import { gateway } from "./llm-client";
async function callPrimary(prompt: string) {
const res = await gateway.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: prompt }],
});
return res.choices[0].message.content;
}
const options = {
timeout: 8000, // fail fast instead of hanging
errorThresholdPercentage: 50,
resetTimeout: 30000, // try again after 30 seconds
};
const breaker = new CircuitBreaker(callPrimary, options);
breaker.fallback(async (prompt: string) => {
// circuit is open, reroute to the fallback model instead of failing the request
const res = await gateway.chat.completions.create({
model: "claude-sonnet",
messages: [{ role: "user", content: prompt }],
});
return res.choices[0].message.content;
});
breaker.on("open", () => console.warn("Circuit opened, primary model degraded"));
breaker.on("close", () => console.info("Circuit closed, primary model recovered"));
export async function resilientAsk(prompt: string) {
return breaker.fire(prompt);
}
Bifrost 是较新的网关选项之一,在每秒 5000 个请求时每个请求仅增加 11 微秒开销,如果你在比较原始代理性能这值得知道,因为基于 Python 的网关通常按对比增加数百微秒。基于 Node 和 Go 的代理往往坐得更靠近那个范围的低端。
路由和缓存在模型层面阻止了失控的成本。但你也想要一个硬天花板,阻止单个用户、租户或有问题的脚本在任何人注意到前炸掉你的月度预算。这是按用户配额在中间件层发挥作用的地方,在请求到达网关前。
// middleware/cost-cap.ts
import { Request, Response, NextFunction } from "express";
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const DAILY_TOKEN_BUDGET = 100_000;
export async function costCapMiddleware(req: Request, res: Response, next: NextFunction) {
const userId = req.headers["x-user-id"] as string;
if (!userId) return res.status(401).json({ error: "Missing user identifier" });
const key = `quota:${userId}:${new Date().toISOString().slice(0, 10)}`;
const used = parseInt((await redis.get(key)) ?? "0", 10);
if (used >= DAILY_TOKEN_BUDGET) {
return res.status(429).json({
error: "Daily token budget exceeded",
resetAt: "midnight UTC",
});
}
res.locals.recordUsage = async (tokensUsed: number) => {
await redis.incrBy(key, tokensUsed);
await redis.expire(key, 86400);
};
next();
}
在你的路由处理器前接入这个,让处理器在从模型响应获回令牌计数后调用 res.locals.recordUsage(tokensUsed)。将它和网关自己的治理层(这是 Portkey 的护栏或 LiteLLM 的预算设置发挥双重作用的地方)结合,你在边界和中间件都有保护。如果单靠配额不能覆盖你的合规要求,我在"生产中的 AI 治理"中深入讨论了治理方面。
什么是 LLM API 网关? 它是一个坐在你的应用和 LLM 提供商(OpenAI、Anthropic 和其他)之间的代理层,在一个集中的地方处理路由、缓存、速率限制和成本追踪,而不是将该逻辑分散到每个调用模型的服务。
我如何减少 LLM API 成本? 结合语义缓存(对重复性工作负载削减 30 到 50)、根据任务复杂度在更便宜和更强大的模型间的智能路由,以及硬的按用户配额。堆栈在一起,这些技术通常相比直接调用提供商而无控制交付 50 到 80 的成本削减。
Node.js 的最佳 LLM 网关是什么? 如果你想要一个自托管、完全可控的代理和 OpenAI 兼容的 API,选择 LiteLLM。如果你想要预建的护栏而不想自己构建,选择 Portkey。如果你的流量已经经过 Cloudflare 的边界并想要最低的添加延迟,选择 Cloudflare AI Gateway。
如果你想要更深入地看 LLM 成本优化,我在我的网站上覆盖得更详细。
如果你想要这个在你自己的网站上端到端地接好,那正是我承接的那种工作。
如果你的设置看起来不同,请留言,很想知道人们在生产中实际运行什么路由或缓存策略。