借助Cloudflare Workers打造一个专属OpenAI API

目前已上线积薪,未来会支持本博客

开发

-
积薪功能包括根据文章内容生成分类、标签和摘要,曾使用百度API,但准确度较差。建议将AI接口迁移到OpenAI上,通过Cloudflare或Vercel方便调用。演示如何搭建专属OpenAI API,配置路由、保障安全。测试时部署到Cloudflare。这样即可获得稳定的、适用于SEO展示的摘要和其他AI服务。
借助Cloudflare Workers打造一个专属OpenAI API

积薪有一个功能,即根据文章内容生成分类、标签和摘要,此前使用的是百度的API。这也成了反馈最为集中的功能,因为百度的AI实在是太拉垮了。

不仅分类经常出错,摘要也经常是驴唇不对马嘴,甚至就是文章内容的随机拼凑,连语序通顺都做不到。

A screenshot of the HCP website with Chinese text, including the HCP website address and a link to the HCP website.
可以看出分类错得离谱
A white background with black Chinese text on a white background.
摘要也是胡言乱语,连日期都摘上了

当初选百度纯粹是因为用起来方便,但这个准确度实在太差。GPT的准确度比较高,不如把AI接口迁移到OpenAI上。

但使用OpenAI可以说是穿墙领域最难的事了,你要同时对抗两个大国。如果只是单纯地调用API,很容易遭到封禁。

于是利用Cloudflare或Vercel这种具有边缘计算功能的平台来调用API,成了最方便稳妥的选择。

本文将示范如何通过Workers搭建一个你的专属OpenAI API。

前提

首先你需要一个OpenAI的账号,搞定支付相关事项。至于怎么搞定,你自己想办法。

然后你需要一个Cloudflare账号。

安装wrangler,在本地编写Workers代码:

             npm install wrangler
           

然后新建一个Workers项目:

             npm create cloudflare@latest
           

通用AI接口

workers.ts 是主入口,所有请求都会经过这里。

不过先不用管它,我们从最末端的功能写起。

我希望这个接口能更通用,通过不同的路由指向不同的prompt。比如请求 /summary 并发送一段文本,就会返回摘要;请求 /category 会返回这段文本的分类。

这就需要有一个函数来负责这项工作,接收两个参数:prompt和content。

             // 接收一个propmt和内容,请求OpenAI,返回结果
interface OpenAIResponse {
  choices: {
    "finish_reason": string;
    "index": number;
    "message": {
      "content": string;
      "role": string;
    }
  } [];
  created: number;
  id: string;
  model: string;
  object: string;
  usage: {
    "completion_tokens": number;
    "prompt_tokens": number;
    "total_tokens": number;
  }
}

async function openAI(prompt: string, content: string, key: string) {
  const message = [
    { "role": "system", "content": prompt },
    { "role": "user", "content": content }
  ];
  const apiURL = 'https://api.openai.com/v1/chat/completions';
  const model = content.length > 3000 ? 'gpt-3.5-turbo-16k-0613' : 'gpt-3.5-turbo-0613';

  const response = await fetch(apiURL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${key}`
    },
    body: JSON.stringify({
      messages: message,
      model: model,
      max_tokens: 500,
      temperature: 0.5,
      stop: ['\n']
    })
  });

  const data: OpenAIResponse = await response.json();
  const result = data.choices[0].message.content;
  return result;
}

export default openAI;

           

这段应该不难理解。为了节省开销,我做了个简单的判断:文本长度超出一定限制时使用gpt-3.5-turbo-16k-0613模型,未超出默认使用gpt-3.5-turbo-0613。当然文本长度并不等于token,这只是个简单的判断,够用就行。

当你向该函数传入预设条件和待处理文本时,返回的就是一个纯文本。

设定prompt

通用函数写好,接下来就是处理不同接口对应的prompt了。

假设我希望在访问 /seo 时,返回这段文字适用于SEO展示的摘要。

             // 导入刚才的通用函数
import openAI from "./open-ai";

interface Env {
	API_KEY: string;
}

const prompt = "我需要根据下面的这段文字, 针对搜索引擎优化, 写一段摘要, 要求写明文字的主要内容, 字数不得超过120个汉字";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const content = await request.text();

    return new Response(await openAI(prompt, content, env.API_KEY), { headers: { "Content-Type": "text/plain" } });
  }
}
           

这里的prompt很重要,由于我们希望这是一个接口,输出的内容保持稳定,因此你可以在prompt的设置上多花些心思。比如你可以使用更严谨的语言:

根据文本内容,输出1-3个与内容相关的标签。格式如下: “标签1”, “标签2”

这里面可以优化的空间太多了,在此不赘述。

配置路由

接下来需要在 workers.ts 里进行设置,以便在请求 /seo 的时候能访问到相应代码。

             import generateSEO from './seo';

interface Env {
	API_KEY: string;
}

// Export a default object containing event handlers
export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {

		const url = new URL(request.url);

		switch (url.pathname) {
			case '/seo':
				return generateSEO.fetch(request, env, ctx);
		}

		return new Response(
			`This is a private API and cannot be accessed without authorization`,
			{ headers: { 'Content-Type': 'text/html' } }
		);
	},
};
           

这段代码的作用是,读取环境变量中的token,根据请求的endpoint,分发到相应处理逻辑中。你可以照此增加更多prompt和逻辑。

OpenAI的token可以在Workers的Dashboard - 设置中添加。

安全性

为了防止接口被滥用,可以稍稍增加一点安全措施。你可以随便生成一段token,放在请求头里,对于token不正确的请求一律拒绝。

最终 workers.ts 的代码是这样的:

             import generateSEO from './seo';

interface Env {
	API_KEY: string;
	TOKEN: string;
}

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		// verify if the bearer token is valid
		const auth = request.headers.get('Authorization');
		if (!auth || auth !== `Bearer ${env.TOKEN}`) {
			return new Response(
				`This is a private API and cannot be accessed without authorization`,
				{ headers: { 'Content-Type': 'text/html' } }
			);
		}

		const url = new URL(request.url);

		switch (url.pathname) {
			case '/seo':
				return generateSEO.fetch(request, env, ctx);
		}

		return new Response(
			`This is a private API and cannot be accessed without authorization`,
			{ headers: { 'Content-Type': 'text/html' } }
		);
	},
};
           

测试

为了避免封号,建议一律先部署到Cloudflare,然后在云端测试。

执行 npx wrangler deploy 即可上传代码。

然后在该Workers内点击“快速编辑”,就可以在窗口中进行调试了。

A computer screen displays a Microsoft Word document with a blue background and white text. The document contains a list of text fields, each with a different color, and a list of fields with a different color. The fields are organized in a grid-like manner, with the list of text fields at the top and the list of fields at the bottom. The Microsoft Word interface is visible, with a blue window and a yellow "OK" button. The window is located at the top of the image, and the "OK" button is located at the bottom. The overall layout suggests a user-friendly interface for managing and organizing text fields.

这样你就有了一个稳定的专属API,只要向其不同的endpoint发送文本,就会根据预设的prompt返回相应的回复。

目前这个接口已经上线积薪,但我计划把本博客的后端彻底从头开发一遍,到时候也在这里提供相应的AI服务。这是个大工程,不知道什么时候能完成。