
如果你平时玩本地 AI 工具比较多像 Chatbox、Cherry Studio或者某个开源的终端客户端 Hermes大概率会遇到同一个烦心事一个模型一个 API 地址一个服务一种鉴权方式想同时用两家模型就得在客户端里来回切配置今天改 Base URL明天改 API Key周末想加一个模型直接把配置文件拉出一屏来。我做的这个网关就是专门把这类问题一次性收口的。核心思路很简单用 Cloudflare Worker 搭一个统一的 OpenAI 兼容接口对外只暴露一个地址对内按模型名把请求分发到不同的上游比如免费的 Agnes 聚合入口、DeepSeek 官方 API或者其他你想接的杂七杂八的服务端。客户端只需要认一个网关地址就能在多个模型之间切换甚至可以让 Hermes 这类工具直接“白嫖”多个免费或低成本的模型通道不用再手工搬配置。这篇文章不打算讲太多泛泛的概念直接把我从设计、编码、上线到被各种奇怪错误磨掉耐心的完整过程写出来。你能看到一个 Worker 网关是怎么一步步从开口请求路由、密钥隔离、流式转发到接入端验证、排查问题最终变成一套稳定可用的多模型接入方案的。如果你也想在自己本地的 AI 工具里同时喂多个模型的 API这篇文章可以作为一份“抄作业手册”来用。1. 动手之前先说清楚这个东西解决什么问题1.1 为什么会有“重复搭建网关”的需求先说一个最直观的场景我有一个开源的本地 AI 客户端 Hermes它想接入大模型 API但它只认一套 OpenAI 风格协议。可是 DeepSeek 的接口基本兼容 OpenAI某第三方聚合入口 Agnes 的接口也声称兼容 OpenAI但细看后会发现它们的模型命名、鉴权方式、超时行为、报错格式全都不一样。今天这个服务升级了模型名明天那个服务改了请求头里的一个字段我的客户端配置就得跟着改。更麻烦的是如果我有三个客户端或者给同一台电脑上的多个用户用每个人都要拿一份原始的上游密钥。而大多数密钥都是跟账号、余额绑定的一旦真正泄露出去不仅钱包受损失还得花时间一家家地去轮换密钥。我自己之前就在这个问题上栽过跟头。某个聚合服务的密钥因为存到网盘汇总文档里被同事拿去调模型跑了一周的批量任务才发现账单爆了。从那以后我就决定不能再让客户端直接接触上游密钥必须有一个中间层做隔离和统一。这个中间层我的选择是做成“网关”而不是改客户端代码。原因是客户端改起来成本高而且模型接入的活应该收敛到一处谁需要新模型谁去改网关配置而不是每个人去改自己本地工具的配置文件。1.2 网关到底放哪里本地 vs 云端 vs Worker做网关一般有几种放置方式我分别评估一下本地跑一个 Node 或 Python 代理服务比如 FastAPI 写一个/v1/chat/completions接口再在本地起个服务让 Hermes 指向http://localhost:3000。这个方案很灵活能写任意逻辑也能接很多上游但问题是要么你的电脑得一直开着要么你得部署到一台云服务器上而服务器的费用和维护本身就是一笔开销。云服务器部署通用网关项目比如开源的一众 AI 网关项目。这类方案功能强大有的还自带统计面板、限流、缓存等功能但部署流程相对重至少要有一个 Nginx、一个数据库、一个后台进程对于“只想安静地同时吃两家模型 API”的使用场景来说多少有点杀鸡用牛刀。Cloudflare Worker我最终选定的方案。它跑在全球分布式的边缘节点上不需要自己管理服务器免费档的请求配额对个人使用来说基本够用。更关键的是Worker 用一个原生fetch就能把请求转发到任意上游 API天然适合做“中转站”这类逻辑。我举个例子帮理解这三种方案的区别本地代理相当于你自己开了一个“私人中转站”车请求都得从你门口过云服务器代理相当于租了一间固定办公室来做中转站位置稳定但租金一直掏Worker 则像是把中转站直接塞进了城市交通网络的每个路口车流到哪都能立刻找到入口。对于个人折腾 AI 模型接入Worker 的成本最低、灵活性最高、部署速度最快这是我最终选择它的核心原因。你要问我它的缺点等一下我也会提到比如冷启动和超时限制这类细节但总体来说是利大于弊。1.3 这套网关实际做到的几件事在展开代码之前我先把这个网关的“功能清单”列在这里后面每一节都会围绕这些功能来写统一对外提供/v1/chat/completions和/v1/models两个 OpenAI 风格接口让 Hermes 这类客户端只需要配置一个 Base URL。根据请求体里的模型名自动路由到对应上游比如模型名带deepseek前缀就走 DeepSeek 官方 API模型名带ag前缀就走 Agnes 聚合入口。对客户端只要求一个网关密钥所有上游密钥都被隐藏在工作者环境中不会泄露到客户端。原样兼容流式响应SSE 格式不断流、不串格式。把各上游五花八门的错误码统一翻译成标准的 OpenAI 风格错误方便客户端识别。做一层基本的请求来源校验和日志记录便于事后审计。这套功能如果放在传统开发里可能需要一个 Node 服务加一套数据库而 Worker 只需要一个 JavaScript 文件就能搞定这也是为什么我推荐先从这个方向入手的理由。2. 整体方案设计模型路由、密钥管理与成本逻辑2.1 路由策略统一入口加模型名前缀网关的核心是“路由”。我在最初的设计里考虑过两种路由思路一种是完全透明转发也就是客户端想调什么模型网关只是把请求原样转发到某个默认上游另一种是按模型名动态选择上游也就是我在 Worker 内部维护一张映射表根据请求里的模型字段来决定往哪发。第一种思路实现起来最简单但等于没做网关因为没办法在同一个客户端里同时用两家模型。第二种思路才是真正的价值所在。我的路由规则设计成“模型名前缀匹配”。举个例子在 Hermes 配置模型时我让它显示两个模型名deepseek-chat命中的是 DeepSeek 官方渠道的聊天模型ag-fast命中的是 Agnes 聚合入口里的某个快速模型。在 Worker 里我写一个简单的匹配函数检查model字段有没有特定前缀function routeProvider(modelName, env) { if (modelName.startsWith(deepseek) || modelName env.MODEL_DEEPSEEK) { return deepseek; } if (modelName.startsWith(ag) || modelName env.MODEL_AGNES) { return agnes; } return env.DEFAULT_PROVIDER || deepseek; }如果客户端传进来的模型名无法识别就落到默认上游。这样做的兼容性很好因为任何客户端都只是在“选模型”并不知道背后的路由逻辑。它只把model字段当成一个普通字符串交给网关网关再去决定往哪转客户端的配置成本几乎为零。2.2 密钥分层网关密钥与上游密钥的隔离密钥设计是这个项目里我最在意的一块因为它的直接动机就是避免上游密钥被滥用。整个鉴权体系分两层第一层是客户端到网关。客户端发起请求时需要在Authorization: Bearer里带上一个网关自己的密钥。这个密钥我管它叫GATEWAY_KEY它完全可以是一个自定义的随机字符串跟上游没有任何关系。代码里判断方式是这样的function checkAuth(request, env) { const auth request.headers.get(Authorization) || ; if (auth ! Bearer ${env.GATEWAY_KEY}) { return new Response( JSON.stringify({ error: { message: Invalid API key provided, type: invalid_request_error }, }), { status: 401, headers: { Content-Type: application/json } } ); } return null; }第二层是网关到上游。Worker 会根据路由结果把请求头里的Authorization替换成对应上游的密钥这个密钥存储在工作者的wrangler.toml或 Dashboard 的vars与secrets中客户端永远拿不到。有人可能会问直接把网关密钥分享给多个客户端是不是也等于共享了漏洞理论上确实存在这个问题。所以我在 Worker 里预留了一个按请求来源过滤的逻辑后面会提到。至少相比直接把上游密钥散播出去这种分层设计已经大大缩小了风险面。2.3 为什么不建议直接在 Hermes 里配多个上游有人肯定会想既然 Hermes 本身就支持配置多个 OpenAI 兼容 API为什么还要多此一举加一个 Worker我试过直接在 Hermes 里配置多个上游体验是这样的每加一个新的模型通道我都要重新填一遍 Base URL、API Key、模型列表如果某个上游临时维护客户端的报错信息五花八门有的直接显示超时有的显示 502有的显示 JSON 解析错误不同的上游对max_tokens、temperature的边界要求还不一样导致同一段提示词在模型 A 上是正常输出在模型 B 上直接报错。用网关把这些差异在外面“抹平”之后Hermes 的配置文件就非常干净了它只需要维护一个网关地址和一个网关密钥。后面接入新模型我只需要在 Worker 里加一个路由分支客户端完全不用动。这就是我认为网关的价值所在不是多转发一层请求那么简单而是把复杂度集中到一处处理让终端工具保持干净。3. Worker 核心实现一个 OpenAI 兼容代理的完整骨架3.1 路由与鉴权入口处先做两件事接下来进入实际代码环节。先说 Worker 的基本入口。Cloudflare Worker 采用 ES Modules 格式一个默认导出对象就可以覆盖所有 HTTP 请求。export default { async fetch(request, env, ctx) { // 处理 CORS 预检请求 if (request.method OPTIONS) { return new Response(null, { status: 204, headers: corsHeaders(), }); } // 第一件事鉴权 const authError checkAuth(request, env); if (authError) { return authError; // 返回 401 } const url new URL(request.url); // 第二件事路由 if (url.pathname /v1/models) { return handleModels(env); } if (url.pathname /v1/chat/completions) { return handleChat(request, env, ctx); } return new Response(Not Found, { status: 404 }); }, };这里的env是 Cloudflare Worker 注入的环境变量对象里面放着网关密钥和上游密钥等配置。ctx是 ExecutionContext可以用来做后台任务比如等待日志写入不过大部分时候我们用不到它。CORS 这块很重要因为 Hermes 这类本地客户端本质上是浏览器技术栈发请求时会先发一个OPTIONS预检请求所以我要在入口处统一放行OPTIONS。3.2 请求改写把“别名”翻译成上游真实参数用户通过客户端发一个请求到达网关请求体的结构像这样{ model: deepseek-chat, messages: [{ role: user, content: 你好 }], stream: true }Worker 拿到这个请求体之后要做几件事读取 JSON、读取model字段、根据路由决定上游地址、替换鉴权头、可能还要改写部分参数。具体代码如下async function handleChat(request, env, ctx) { let body; try { body await request.json(); } catch (e) { return jsonResponse({ error: { message: Invalid JSON body, type: invalid_request_error } }, 400); } const model body.model || ; const provider routeProvider(model, env); let upstreamUrl; let upstreamKey; const headers { Content-Type: application/json, Authorization: Bearer ${upstreamKey}, }; if (provider deepseek) { upstreamUrl https://api.deepseek.com/v1/chat/completions; } else if (provider agnes) { upstreamUrl env.AGNES_BASE_URL /v1/chat/completions; // Agnes 的特殊之处是可能还要附加一个渠道 ID 头或不同的认证字段 headers[X-Channel-ID] env.AGNES_CHANNEL_ID || ; } else { upstreamUrl env.FALLBACK_BASE_URL; } // 实际请求上游 const upstreamResponse await fetch(upstreamUrl, { method: POST, headers: headers, body: JSON.stringify(body), }); return handleUpstreamResponse(upstreamResponse, request); }这里有一处容易踩坑的地方直接转发body可能会把客户端传入的temperature、max_tokens这类参数原样送到上游。大部分情况下这没问题但有些上游对参数边界要求严格比如max_tokens超出上限会在 400 错误而不是自动截断。所以我的做法是在转发前做一次参数清洗if (typeof body.max_tokens ! undefined) { const limit provider deepseek ? 8192 : 4096; body.max_tokens Math.min(Number(body.max_tokens), limit); }这样做的好处是客户端即使疯狂调大生成长度也不会被上游拒绝网关相当于做了一个“安全带”般的参数约束。3.3 SSE 流式转发最容易翻车也最关键大模型接口最常用的就是流式响应也就是 SSEServer-Sent Events。客户端在请求里带上stream: true之后上游就会返回一段持续不断的data:文本流。Worker 转发流式响应的方式是把上游的响应体直接“接通”给客户端不做缓存式读取。代码如下function handleUpstreamResponse(upstreamResponse, request) { const responseHeaders { Content-Type: upstreamResponse.headers.get(Content-Type) || text/event-stream, Cache-Control: no-cache, Connection: keep-alive, ...corsHeaders(), }; // 构造返回给客户端的 Response直接透传 body stream return new Response(upstreamResponse.body, { status: upstreamResponse.status, headers: responseHeaders, }); }如果只是转发成功响应这段代码已经够用。但有一个问题如果上游在处理过程中返回了一个非 200 的状态码它的响应体可能不是 SSE 格式而是 JSON。如果我也把它当成 SSE 传回给客户端客户端就会因为解析不到data:行而报错。所以更好的写法是先看状态码如果是非 2xx就转成 JSON 错误格式返回如果是 2xx再透传流。if (!upstreamResponse.ok) { const errorText await upstreamResponse.text(); return jsonResponse({ error: { message: Upstream error: ${errorText}, type: upstream_error } }, upstreamResponse.status); }另外还有一个需要特别注意的点如果把Content-Type改成text/event-stream但上游实际返回的可能是application/json客户端就会卡住。我的建议是优先透传上游自己的Content-Type不要自己强行改。客户端其实能处理text/event-stream和application/json两种类型。3.4 错误码规范化让客户端只认一套规则不同上游的报错格式差异很大。DeepSeek 一般会返回 OpenAI 风格的错误对象结构是{ error: { message: ..., type: ..., code: null } }但 Agnes 聚合入口就不一定了它可能返回一个纯文本的 500或者一套完全不同的 JSON。如果客户端解析不了体验就会很糟糕。我的做法是统一包一个normalizeError函数把各种异常咬成标准的 OpenAI 错误结构function normalizeError(status, rawText) { let message rawText; let type api_error; try { const parsed JSON.parse(rawText); if (parsed.error) { message parsed.error.message || parsed.error; type parsed.error.type || type; } else if (parsed.message) { message parsed.message; } } catch (e) { // 保持原文 } return { error: { message: message.slice(0, 500), // 防止日志被疯狂刷屏 type, }, }; }这样Hermes 拿到的所有错误信息都是可解析的 JSON能直接显示在聊天界面中而不是弹出一个不可读的 HTML 报错页。4. 多个上游的适配细节DeepSeek 与 Agnes 的差异4.1 模型名映射表虽然路由用“模型名前缀”来判断但在实际落地时因为各家模型的上下文窗口、计费方式、擅长领域都不同我更建议维护一张更完整的模型映射表。我这里做了两层设计第一层是“路由层”根据前缀判断往哪发第二层是“展示层”也就是/v1/models接口返回什么模型列表让客户端能自动识别。/v1/models的响应大概是这样的async function handleModels(env) { const models [ { id: deepseek-chat, object: model, owned_by: deepseek, }, { id: ag-fast, object: model, owned_by: agnes, }, { id: ag-reason, object: model, owned_by: agnes, }, ]; return jsonResponse({ object: list, data: models }); }当 Hermes 调用GET /v1/models时它就能自动拉出这两个模型的列表用户在界面里直接选即可非常省事。注意这里我没有返回供应商真实模型名而是用了网关自己的别名。为什么这样做因为上游改名的频率比我们想象中大得多如果让客户端直接看到真实模型名一旦上游改名客户端缓存就会失效还得手动清配置。用别名隔离一层上游的变动只影响 Worker 内部映射表。4.2 上游参数差异温度、max_tokens、system当你真正开始适配多家上游时很快会发现所谓“OpenAI 兼容”更像是“大家长得比较像”但细节总有偏差。DeepSeek 官方 API 对max_tokens的上限一般能到 8192新一代上下文模型可能更高对temperature的范围是 0 到 2 之间。它对system消息是完全支持的。但 Agnes 聚合入口有可能对接的是多家中转模型有的后端模型不支持system消息接受之后直接忽略有的要求temperature必须是整数有的则对max_tokens有严格上限超了直接报invalid request。我此前遇到过一个典型的坑我在一个聚合入口传了temperature: 0.7它要求传整数0或1结果我在毫不知情的情况下总是拿到 400 错误。后来查文档才知道它的参数规则跟标准不一样。因此适配 Agnes 时我在 Worker 里做了一层“规则微调”。比如如果检测到provider agnes且temperature是小数我会选择把它改写成Math.round(temperature)或者在网关环境变量里加一个开关来控制是否向下取整。类似的处理逻辑不复杂但确实能让你少踩很多坑。4.3 上游返回结构差异与补充除了入参出参结构其实也是五花八门。OpenAI 标准的响应里有一个choices数组每个 choice 里有message或delta。DeepSeek 基本遵从这个结构但也会多返回一个usage字段里面包含prompt_cache_hit_tokens这类额外信息。Agnes 作为聚合入口返回的结构可能是直接被它的顶层代码包了一层比如{ data: { choices: [...], usage: {...} } }如果客户端拿到choices在顶层找不到就相当于白请求了一遍。所以我在转发层对非流式响应做了轻量级的“修正”检查响应 JSON 里有没有data.choices如果有就把整个data对象提升到最外层。这个处理其实是自己根据常见聚合服务反推出来的通用做法不是某个官方接口文档写死的规则每家都可能不同所以要留一个条件判断如果提升后会导致重复内容再回滚。async function fixUpstreamJson(response) { const text await response.text(); try { const json JSON.parse(text); if (json.data Array.isArray(json.data.choices) !json.choices) { json.choices json.data.choices; json.usage json.data.usage || {}; } return new Response(JSON.stringify(json), { status: response.status, headers: { Content-Type: application/json }, }); } catch (e) { return new Response(text, { status: response.status }); } }这些怪癖如果不经历一次很可能你调试半天也不知道问题出在哪。5. 让 Hermes 吃上这套接口接入方法与验证5.1 在 Hermes 里填网关地址Hermes 是我这边在用的一个本地 AI 客户端工具它本身支持自定义 OpenAI 兼容 API。接入前我先在wrangler.toml里配置环境变量然后发布 Worker。name ai-gateway main src/index.js compatibility_date 2025-01-01 [vars] GATEWAY_KEY gw_live_xxxxxxxxxxxx DEFAULT_PROVIDER deepseek MODEL_DEEPSEEK deepseek-chat MODEL_AGNES ag-fast AGNES_BASE_URL https://your-agnes-endpoint.example.com AGNES_CHANNEL_ID ch_xxxx # 注意真正的上游密钥不要写在 toml 里用 wrangler secret put发布之后在 Hermes 的 API 设置里这样填API Base URLhttps://ai-gateway.your-subdomain.workers.dev/v1API Keygw_live_xxxxxxxxxxxxModel 列表手动添加deepseek-chat和ag-fast填完之后Hermes 会自动请求/v1/models来验证密钥有效性。如果能看到模型列表说明前面打通了。5.2 用 curl 做三连验证在把 Hermes 接进来之前我建议先用curl做三连验证这样能把问题范围缩小到 Worker 还是客户端。第一遍验证网关本身是否在线curl https://ai-gateway.your-subdomain.workers.dev/v1/models -H Authorization: Bearer gw_live_xxxxxxxxxxxx第二遍验证 DeepSeek 路由发一个非流式请求curl https://ai-gateway.your-subdomain.workers.dev/v1/chat/completions \ -H Authorization: Bearer gw_live_xxxxxxxxxxxx \ -H Content-Type: application/json \ -d { model: deepseek-chat, messages: [{role: user, content: 你好输出一句话}], stream: false }第三遍验证 Agnes 路由和流式输出curl https://ai-gateway.your-subdomain.workers.dev/v1/chat/completions \ -H Authorization: Bearer gw_live_xxxxxxxxxxxx \ -H Content-Type: application/json \ -d { model: ag-fast, messages: [{role: user, content: 你好输出一句话}], stream: true }如果第三遍能看到一行行data:前缀的输出且以data: [DONE]结尾就说明流式链路完全正常。如果在这里失败那问题就出在 Worker 层跟 Hermes 无关。5.3 请求路径选择演示为了演示“同一个网关两个不同上游”的实际效果我在 Worker 里加了一个小日志字段每次请求会在响应头里带上X-Gateway-Provider标注这次实际被路由到哪家。responseHeaders[X-Gateway-Provider] provider;这样一来在 Hermes 的开发者面板或curl -i里就能看到每次请求是走的 DeepSeek 还是 Agnes。有一次我发现明明客户端选的是ag-fast响应头却显示provider: deepseek排查后发现是我在路由函数里把前缀判断写反了ag被识别成deepseek。这个调试头后来救了我不少时间。你也可以在自己网关里加上类似字段排查问题会很直观。6. 我实际踩过的坑与排查速查表6.1 典型坑位清单这里把我的翻车记录整理成一张表方便你直接对照排查。现象根因解决办法客户端提示 Invalid API keyAuthorization头带了空格或大小写差异用严格比较时对字符串做 trim且检查 Bearer 后面只有一个空格上游返回 401但网关密钥正确网关没有替换请求头里的 Auth把网关密钥传给了上游确认转发前重新设置headers.Authorization客户端报 SSE 解析失败上游非 200 时仍透传了 JSON 响应给流式请求在handleUpstreamResponse里先判断upstreamResponse.ok响应内容全部拿到但最后报错上游返回的Content-Type不是 SSE但客户端仍按 SSE 解析直接透传上游原始Content-Type不强制改请求偶尔超时Worker 与上游之间地域链路波动为 fetch 设置超时 AbortSignal并对超时错误做降级处理无法调用/v1/modelsWorker 路由里没加这个路径在入口处增加/v1/models分支同一模型名称有时走 A 有时走 B路由函数匹配逻辑有漏洞开头用startsWith并且先用最长前缀匹配6.2 排查思路从客户端到 Worker 再到上游的三层分段法遇到问题不要慌先确定问题在哪一层实验的方法也很简单。第一层客户端到 Worker。先不调用任何模型直接请求/v1/models看鉴权是否通过。如果返回 401问题在密钥或者请求头。如果返回 200说明网关本身可用。第二层Worker 到上游。我临时在 Worker 里加一个调试端点比如/debug/providers它返回当前环境变量里所有非敏感配置包括上游 URL 和模型映射便于确认配置真的被发布到了线上。第三层直接绕过网关用curl打上游原始地址。如果上游原始地址也失败那就是上游的问题与网关无关。我之前遇到过本地上游账号欠费后网关反复报 401一度怀疑是 Worker 代码问题后来直连上游才发现是对方账户状态不对。这套“三层分段”的思路看起来简单但比瞎猜代码高出很多效率。我建议你在开始改造代码前先用这个方式把故障边界画出来。6.3 防止“白嫖”被反薅配额与日志标题里有个词叫“白嫖”说的是尽量用免费或低成本的模型通道。但白嫖是一回事被白嫖是另一回事。如果你的网关密钥被泄漏出去别人就能借用你的上游额度尤其是聚合入口的免费通道额度往往是共享的很容易被刷爆。所以我做了三个加固动作第一网关密钥尽量长且随机至少 32 位以上不要用123456这种过于友好的值。第二在 Worker 里加一层来源域名限制。如果客户端从浏览器发起可以把允许的域名列表写在环境变量里。但 Hermes 这样的本地客户端会带Origin头所以这里要灵活处理。第三给每个上游请求加上轻量化日志。Cloudflare Worker 有ctx.waitUntil()我可以在返回响应的同时异步把请求的关键信息模型名、路由、状态码、耗时发送到一个日志端点。这样做不会阻塞主响应但留下了审计痕迹。日志发送代码大概是这样的ctx.waitUntil( fetch(env.LOG_WEBHOOK, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ ts: Date.now(), model: body.model, provider, status: upstreamResponse.status, duration: Date.now() - startTime, }), }).catch(() {}) );注意不要记录完整请求体或完整响应体因为里面包含用户敏感输入。只记录元信息即可这是给自己看的审计摘要不是给监控系统堆数据。7. 一些不是官方文档会提醒你的经验7.1 超时与网络稳定性Cloudflare Worker 的免费额度很香但它和上游服务之间的连接不是完全不受限制的。我自己在跑长任务时遇到过 Worker 主动断开连接的情况尤其当上游响应时间特别长时比如复杂的推理任务跑了一两分钟。后来我养成一个习惯对于特别长的生成任务不要把流式响应的超时责任完全丢给 Worker。要么在请求里设置合理的超时信号要么在上游侧选择更快的模型通道。如果你确实需要用慢速推理模型建议用 Workers 的 Queue 或者把任务拆成异步轮询模式不要让客户端一直挂着同一个连接。7.2 别过度设计先跑通再优化最后这一点我特别想强调。我第一版搭这个网关时给自己规划了非常宏大的架构多租户、请求计数、速率限制、缓存。结果呢光调试速率限制就把我卡了两天因为 Cloudflare Worker 的免费环境里没有现成的持久化计数器每次都靠 Durable Objects复杂度和排错成本一下就上去了。后来我把这些功能全部删掉只保留最基本的“路由加鉴权加流式转发”半小时就上线了。一个月用下来发现我真正需要的其实就是基础功能。那些高级功能等出现真实需求时再加也不迟而且到时候你会更清楚需要加在什么位置。7.3 几条低调但好用的配置建议AGNES_BASE_URL别写在代码里放在环境变量中方便在不改代码的情况下切换验证地址。客户端Connection: keep-alive头不必特殊处理Cloudflare 节点会处理。给网关的域名加一个自定义域比如ai.example.com而不是直接用*.workers.dev。自定义域走的线路更稳某些网络环境下的兼容性也更好。如果要支持多个团队成员共用建议每个团队成员的 Hermes 配置里都填同一个网关地址但网关密钥不要写在聊天记录里单独发给个人并定期轮换。我在实际使用中还有一个比较深的体会网关这类东西从一开始就该把“密钥与业务逻辑分离”这个原则贯彻到底。我第一版把所有配置都写在代码里后来有一次临时要换上游密钥还得重新打包发布改成环境变量之后在后台点一下就能更新这才是真正能长期用下去的状态。如果你也正在为多个模型 API 的统一接入头疼按照上面的思路搭一个 Worker 网关大概率一下午就能跑通。等跑通了再回头看你会发现其实最难的不是代码而是想清楚“哪些事情应该由网关负责哪些事情应该交给客户端和上游”。这个边界划清楚了后面的路就顺了。