把 AI 接口搬到 ESA 边缘:一个人的边缘函数实战
前几篇写 ESA,分别聊了上手体验、被攻击后的防护复盘和云栖大会上的产品转向。这篇不聊理念,写一个我这个月真在用的小东西:用 ESA 边缘函数,给个人项目做一层 AI 接口中转,全程没开一台服务器。
为什么需要这一层
起因很简单。我有个自己用的小工具:浏览器插件 + 一个静态页面,要调大模型 API。直接调有三个问题:
- 密钥放前端等于裸奔。哪怕混淆,请求一开 DevTools 就看得见;
- 大模型 API 大多没有免费友好的限流,key 一旦泄露,账单就是别人说了算;
- 跨境延迟玄学。直连时好时坏,流式输出经常卡成 PPT。
传统做法是买台小服务器写个中转:鉴权、转发、计费逻辑都丢上去。但一个人维护项目,多一台服务器就多一份要打的补丁、要续的费、要担惊受怕的凌晨告警。ESA 边缘函数恰好把这件事压扁了:代码部署在边缘节点,离用户近,不用管机器,请求量小的时候费用几乎可以忽略。
架构:三十行代码的中转层
整体结构简单到不好意思画架构图:
浏览器插件 → ESA 边缘节点(鉴权 + 限流 + 缓存 + 转发)→ 大模型 API边缘函数用的是 Web 标准的 Request/Response,写过 Service Worker 或者 Cloudflare Workers 的人零学习成本。核心代码:
export default async function handler(request) { // ① 自己的用户鉴权:校验插件请求头里的短期 token const token = request.headers.get('X-My-Token'); if (!(await verifyToken(token))) { return new Response(JSON.stringify({ error: 'unauthorized' }), { status: 401, headers: { 'content-type': 'application/json' }, }); }
const body = await request.json();
// ② 简单限流:每个用户每天 N 次(用边缘 KV 计数) const used = await myKV.get(`cnt:${token}:${today()}`); if (Number(used || 0) > 200) { return new Response('rate limited', { status: 429 }); } await myKV.put(`cnt:${token}:${today()}`, Number(used || 0) + 1);
// ③ 相同请求命中边缘缓存,省 token 也省等待 const cacheKey = new URL(request.url); cacheKey.search = '?q=' + await hashBody(body); const cached = await caches.default.match(cacheKey); if (cached) return cached;
// ④ 转发到真正的大模型 API,密钥只存在边缘环境变量里 const upstream = await fetch('https://api.example-llm.com/v1/chat/completions', { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${env.LLM_API_KEY}`, }, body: JSON.stringify(body), });
// ⑤ SSE 流式响应原样透传,打字机效果不被缓冲吃掉 const resp = new Response(upstream.body, upstream); resp.headers.set('cache-control', 's-maxage=3600'); ctx.waitUntil(caches.default.put(cacheKey, resp.clone())); return resp;}真正的大模型 key 只存在 ESA 的环境变量里,前端到死都接触不到。就这么个东西,解决了前面说的全部三个问题。
三个值得单独说的细节
流式透传是最容易踩的坑。 第一版代码我习惯性地 await response.json() 再重新组装返回,结果打字机效果直接变成”转圈二十秒后一次性吐字”。大模型对话的体验一半在 SSE 流上,边缘层必须原样转发 response.body 这个可读流,不能读完整段再回。改成透传之后,首 token 延迟从直连时的 1~3 秒玄学区间,稳定在几百毫秒——边缘节点到上游这段网络比家庭宽带靠谱太多。
缓存的不是答案,是确定性请求。 别想着缓存对话,那是给自己找投诉。我只缓存明确可复用的部分:标题生成、固定格式的摘要、分类打标这类 temperature 调到 0 的任务。同一个网页再抓一次摘要,边缘直接命中,零 token 费用。上线三周,约三成请求落在了缓存上。
限流要做在边缘,不要做在前端。 前端的每日次数限制是”防君子”,改个本地存储就绕过了。计数放在边缘 KV 里,用户维度 key 用 token 而不是 IP——公司网络和咖啡馆 Wi-Fi 下大家共用出口 IP,按 IP 限流天天误伤好人。
和 WAF、缓存规则怎么配合
边缘函数不是孤军奋战,它和 ESA 原有的规则引擎在同一套配置里,这是相比”裸 Functions 平台”我最满意的一点:
- WAF 规则拦在函数之前。扫描器、爬虫、明显的恶意外包流量在到函数之前就被托管规则挡掉,不消耗函数调用次数;
- 静态资源走缓存规则,动态请求才进函数。一条路径规则分流,插件页面和静态资产依旧是 CDN 老本行,只有
/api/llm这一个路径路由给函数; - 日志在同一个面板。安全事件和函数
console.log不用在两个平台之间来回横跳,排查”这个请求到底是被 WAF 拦了还是代码报错了”一分钟定位。
成本与适合谁
说了半天好话,也说清楚边界,免得有人照着搞完发现不是自己的菜:
费用上,个人项目这种调用量级(日均几百到两三千次),边缘函数 + KV 的月花费是一杯咖啡以内的量级,大头反而是大模型 API 本身。但注意它是按调用次数计费的模型,被人刷接口没有上限保护依然会心疼,所以前面的限流不是可选项。
它适合:个人/小团队的工具站、需要藏密钥的前端项目、轻量的 A/B 分流和灰度、按地理位置做内容改写。它不适合:重计算(视频处理、长任务编排)、需要持久连接和复杂状态的后端、对冷启动有微秒级洁癖的场景——这些老老实实买服务器或者上容器。
一点感受
去年云栖那篇我写”CDN 正在变成 AI 的运行底座”,当时多少还带着点听发布会的距离感。这个小工具做完,我对这句话有了更具体的体感:对一个人维护的项目来说,边缘函数最大的价值不是”快”,是少——少一台服务器,少一份密钥泄露面,少一类凌晨告警。
一个人做东西,注意力就是最稀缺的资源。能把一层架构从”要维护的东西”变成”配置完就忘掉的东西”,这件事本身就值回票价。
下次打算试试把插件的用量统计也搬进边缘,彻底做到”零自有服务器”。踩了坑再来写。