使用 Vercel Edge Function 访问 OpenAI API 的注意事项

从某天开始,OpenAI API 无法从国内直接访问。而且,也不是每个人都有自己的云服务器,能够搭建独立服务。那么,还有别的办法能比较容易的访问到 OpenAI 么?当然是有的,比如 Vercel Edge Function,或者 CloudFlare Edge Function。

这里我准备结合前阵子的开发经验,分享一下使用 Vercel Edge Function 访问 OpenAI API 的注意事项,让新来的开发者能少走弯路。

推荐阅读

开始之前,我建议大家先花点时间了解一下 Edge Function,以及如何使用 Vercel Edge Function 开发 OpenAI 应用。因为我后面要分享的主要是踩过的坑,所以先系统性了解会好很多:

Building a GPT-3 app with Next.js and Vercel Edge Functions

官方教程,还有 Demo 网站GitHub 项目,非常友好。虽然是英文写的,不过并不难懂,实在不行就用 Edge 浏览器自带的翻译功能吧,建议大家好好学习英文。

自有域名+CNAME 实现国内访问

Vercel 给免费版用户也提供子域名+ SSL 证书,很多时候都够用,但可惜,vercel.app 大域名被墙了,连带所有子域名都无法访问。好在 Vercel CDN 在国内也还能用,所以我们只需要一个自己的域名即可。

申请域名的选择有很多,国内几大云服务商都能注册,国外的域名供应商也可以放心使用。我比较常用的是 namecheap.com。便宜的比如 .xyz 域名首年只要几块钱,随便注册一个就能用。

注册完域名之后,在 Vercel 后台找到自己的应用,在“Setting > Domains“里添加域名,然后 Vercel 会告诉你怎么配置 DNS。复制解析目标,在域名供应商 DNS 配置页面完成 CNAME 配置,稍等片刻,解析生效后,即可得到一个国内也能正常访问的域名。

使用 Edge Function

Vercel Edge Function 与我们日常开发的 node.js 服务器略有区别。它并非完整的 node.js,而是 Edge 基于 V8 专门打造的袖珍运行时,尽可能轻量化,裁剪掉很多系统 API。功能少,但是速度很快,几乎零启动时间。(我之前将它跟 Supabase 记混了,以为它也是基于 deno 的。)

使用 Edge Function 的好处,简单来说:省运维;详细来说,大概有这么几点:

  1. 性能更好。比随便买个小水管强得多。
  2. 自带弹性伸缩。不管访问量怎么成长,都有 Vercel 集群帮我们自动伸缩。(当然可能需要付钱)
  3. 启动速度比 serverless 快很多,基本没有等待时间。
  4. 免费额度足够初期 MVP 验证。

坏处当然也有。首先,Edge Function 里跑的是 TS,这就意味着很多兼容 JS 的开源仓库都不能用。其次,Edge Function 很多原生 API 都不支持,所以没有特意兼容的仓库也不能用。举个例子,要完成网络请求,大家最熟悉的 Axios 就不能用,只能用系统原生的 fetch

解决超时问题

由于算法原因,OpenAI API 返回数据的总时间可能比较长,而 Edge Function 的等待时间又限制得很严。所以如果等待 OpenAI 返回全部数据再渲染,可能因为等太久,在 Edge Function 这里会超时。

解决方案就是使用流(stream)式传播。在这种情况下,OpenAI 会逐步返回结果(差不多一个单词一个单词这样蹦),只要在客户端进行组合,就能看到类似实时输入的效果。

完整的范例代码上面的官方文章有,我就不复制粘贴了,大家注意就好。

Edge Function 的流不是最初的流

这里有个坑,虽然我们在 Edge Function 里获取了 OpenAI API 的流,然后转发出来,但实际上我们接收到的流并不是最初的流。最初的流里,每次发送的数据都是完整的 JSON 文件,可以直接解析;但是 Edge Function 里转发给我们的却是前后合并后随机切分的结果。

于是我们必须重新整理响应体。大概方案如下:

  1. 在每次返回的响应体里找到两个 json 的连接处
  2. 截断,拿到前面的 json,解析,得到自己想要的数据
  3. 继续查找完整的 json,如果没有,则和下一次响应体连接起来处理

核心代码如下:

class fetchGpt {
  fetch() {
    // 前面的代码参考官方例子
    // 我从循环读取开始
    while (!done) {
      const { value, done: doneReading } = await reader.read();
      done = doneReading;
      if (!value) {
        break;
      }

      // readableStream 读出来的是 Uint8Array,不能直接合并
      if (chunkValue.length > offset) {
        lastValue = concatUint8Array(lastValue, value);
      } else {
        lastValue = value;
        offset = 0;
      }
      chunkValue = decoder.decode(lastValue);
      [finishReason, offset] = this.parseChunk(chunkValue, offset);
    }
  }
  parseChunk(chunk: string, offset = 0): [string, number] {
    let finishReason = '';
    let nextOffset = offset;
    while (nextOffset !== -1) {
      nextOffset = chunk.indexOf(splitter, nextOffset + 1);
      const text = nextOffset !== -1
        ? chunk.substring(offset, nextOffset)
        : chunk.substring(offset);
      try {
        const json = JSON.parse(text);
        const [ choice ] = json.choices;
        const { delta, finish_reason } = choice;
        const chunkContent = delta?.content || '';
        // 这里我把数据交给事件 和 pinia 处理
        this.emit(MessengerEvent.MESSAGE, chunkContent, json.id, json.created);
        this.store.appendTextToLastItem(chunkContent, {
          id: json.id,
          created: json.created,
          system: this.options.system || '',
        });
        finishReason = finish_reason;
        offset = nextOffset !== -1 ? nextOffset : chunk.length;
      } catch (e) {
        //- ignore
      }
    }
    return [finishReason, offset];
  }

常见 API 使用错误

大家都知道,OpenAI 按照请求响应的 token 数算钱。所以我就想精打细算,通过在请求参数里减少 max_tokens,尽量少返回些内容。

事实证明这个做法不成立。首先,OpenAI 对请求的兼容性不高,max_tokens 如果是 NaN 或者带有小数,都会报错。其次,ChatGPT 很啰嗦,内容量少了,它不过瘾,就会反复要求继续(finish_reason: 'length'),尤其在极端条件下,如果我们自动 continue,可能会误入死循环。

所以我建议,max_tokens 最少 128,尽量 256 以上。


前几天,有朋友在我的博客下面留言,说 Whisper 他试过,没啥特别的。我的观点不是这样。所谓纸上得来终觉浅,绝知此事要躬行。很多东西,确实不复杂,照着官方教程弄,三下五除二,在本地跑起来,并不难。但是想弄得干净利索,在生产环境里跑顺,遇到什么问题都能快速解决,也不是随随便便就能搞定的。

希望有类似需求,寻求类似解决方案的同学能少走弯路;也欢迎大家多多分享,有意见建议,欢迎留言讨论。

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

评论

《“使用 Vercel Edge Function 访问 OpenAI API 的注意事项”》 有 1 条评论

  1. […] 也欢迎大家阅读我前一篇分享文:使用 Vercel Edge Function 访问 OpenAI API 的注意事项。 […]

欢迎吐槽,共同进步

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据