去年 11 月,美国的 OpenAI 公司推出了 ChatGPT 产品,它在发布后的 5 天内用户数就突破了 100 万,两个月后月活用户突破了 1 个亿,成为至今为止人类历史上用户数增长最快的消费级应用。ChatGPT 之所以能在全球范围内火出天际,不仅是因为它能以逼近自然语言的能力和人类对话,而且可以根据不同的提示语解决各种不同场景下的问题,它的推理能力、归纳能力、以及多轮对话能力都让世人惊叹不已,让实现通用人工智能(AGI,Artificial General Intelligence)变成为了现实,也意味着一种新型的人机交互接口由此诞生,这为更智能的 AI 产品提供了无限可能。

很快,OpenAI 推出了相应的 API 接口,所有人都可以基于这套 API 快速实现一个类似 ChatGPT 这样的产品,当然,聊天对话只是这套 API 的基本能力,OpenAI 官方网站有一个 Examples 页面,展示了结合不同的提示语 OpenAI API 在更多场景下的应用:

chatgpt-examples.png

OpenAI API 快速入门

OpenAI 提供了很多和 AI 相关的接口,如下:

  • Models - 用于列出所有可用的模型;
  • Completions - 给定一个提示语,让 AI 生成后续内容;
  • Chat - 给定一系列对话内容,让 AI 生成对应的回复,使用这个接口就可以实现类似 ChatGPT 的功能;
  • Edits - 给定一个提示语和一条指令,AI 将对提示语进行相应的修改,比如常见的语法纠错场景;
  • Images - 用于根据提示语生成图片,或对图片进行编辑,可以实现类似于 Stable DiffusionMidjourney 这样的 AI 绘画应用,这个接口使用的是 OpenAI 的图片生成模型 DALL·E
  • Embeddings - 用于获取一个给定文本的向量表示,我们可以将结果保存到一个向量数据库中,一般用于搜索、推荐、分类、聚类等任务;
  • Audio - 提供了语音转文本的功能,使用了 OpenAI 的 Whisper 模型;
  • Files - 文件管理类接口,便于用户上传自己的文件进行 Fine-tuning;
  • Fine-tunes - 用于管理你的 Fine-tuning 任务,详细内容可参考 Fine-tuning 教程
  • Moderations - 用于判断给定的提示语是否违反 OpenAI 的内容政策;

关于 API 的详细内容可以参考官方的 API referenceDocumentation

其中,CompletionsChatEdits 这三个接口都可以用于对话任务,Completions 主要解决的是补全问题,也就是说用户给出一段话,模型可以按照提示语续写后面的内容;Chat 用于处理聊天任务,它显式的定义了 systemuserassistant 三个角色,方便维护对话的语境信息和多轮对话的历史记录;Edit 主要用于对用户的输入进行修改和纠正。

要调用 OpenAI 的 API 接口,必须先创建你的 API Keys,然后请求时像下面这样带上 Authorization 头即可:

Authorization: Bearer OPENAI_API_KEY

下面是直接使用 curl 调用 Chat 接口的示例:

$ curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
     "model": "gpt-3.5-turbo",
     "messages": [{"role": "user", "content": "你好!"}],
     "temperature": 0.7
   }'

我们可以得到类似下面的回复:

{
  "id": "chatcmpl-7LgiOhYPcGGwoBcEPQmQ2LaO2pObn",
  "object": "chat.completion",
  "created": 1685403440,
  "model": "gpt-3.5-turbo-0301",
  "usage": {
    "prompt_tokens": 11,
    "completion_tokens": 18,
    "total_tokens": 29
  },
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "你好!有什么我可以为您效劳的吗?"
      },
      "finish_reason": "stop",
      "index": 0
    }
  ]
}

如果你无法访问 OpenAI 的接口,或者没有 OpenAI 的 API Keys,网上也有很多免费的方法,比如 chatanywhere/GPT_API_free

OpenAI 官方提供了 Python 和 Node.js 的 SDK 方便我们在代码中调用 OpenAI 接口,下面是使用 Node.js 调用 Completions 的示例:

import { Configuration, OpenAIApi } from "openai";

const configuration = new Configuration({
    apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

const response = await openai.createCompletion({
    "model": "text-davinci-003",
    "prompt": "你好!",
    "max_tokens": 100,
    "temperature": 0
});
console.log(response.data);

由于 SDK 底层使用了 axios 库发请求,所以我们还可以对 axios 进行配置,比如像下面这样设置代理:

const response = await openai.createCompletion({
    "model": "text-davinci-003",
    "prompt": "你好!",
    "max_tokens": 100,
    "temperature": 0
}, {
    proxy: false,
    httpAgent: new HttpsProxyAgent(process.env.HTTP_PROXY),
    httpsAgent: new HttpsProxyAgent(process.env.HTTP_PROXY)
});

使用 OpenAI API 实现翻译功能

从上面的例子可以看出,OpenAI 提供的 CompletionsChat 只是一套用于对话任务的接口,并没有提供翻译接口,但由于它的对话已经初步具备 AGI 的能力,所以我们可以通过特定的提示语让它实现我们想要的功能。官方的 Examples 页面有一个 English to other languages 的例子,展示了如何通过提示语技术将英语翻译成法语、西班牙语和日语,我们只需要稍微修改下提示语,就可以实现英译中的功能:

async function translate(text) {

    const prompt = `Translate this into Simplified Chinese:\n\n${text}\n\n`
    
    const openai = createOpenAiClient();
    const response = await openai.createCompletion({
        "model": "text-davinci-003",
        "prompt": prompt,
        "max_tokens": 100,
        "temperature": 0
    }, createAxiosOptions());
    return response.data.choices[0].text
}

上面我们使用了 Translate this into Simplified Chinese: 这样的提示语,这个提示语既简单又直白,但是翻译效果却非常的不错,我们随便将一段官方文档丢给它:

console.log(await translate("The OpenAI API can be applied to virtually any task that involves understanding or generating natural language, code, or images."));

OpenAI API 可以应用于几乎任何涉及理解或生成自然语言、代码或图像的任务。

看上去,翻译的效果不亚于 Google 翻译,而且更神奇的是,由于这里的提示语并没有明确输入的文本是什么,也就意味着,我们可以将其他任何语言丢给它:

console.log(await translate("どの部屋が利用可能ですか?"));

这些房间可以用吗?

这样我们就得到了一个通用中文翻译接口。

Chrome 插件快速入门

我在很久以前写过一篇关于 Chrome 插件的博客,我的第一个 Chrome 扩展:Search-faster,不过当时 Chrome 扩展还是 V2 版本,现在 Chrome 扩展已经发展到 V3 版本了,并且 V2 版本不再支持,于是我决定将 Chrome 扩展的开发文档 重温一遍。

一个简单的例子

每个 Chrome 插件都需要有一个 manifest.json 清单文件,我们创建一个空目录,并在该目录下创建一个最简单的 manifest.json 文件:

{
  "name": "Chrome Extension Sample",
  "version": "1.0.0",
  "manifest_version": 3,
  "description": "Chrome Extension Sample"
}

这时,一个最简单的 Chrome 插件其实就已经准备好了。我们打开 Chrome 的 管理扩展程序 页面 chrome://extensions/,启用开发者模式,然后点击 “加载已解压的扩展程序”,选择刚刚创建的那个目录就可以加载我们编写的插件了:

chrome-extension-sample.png

只不过这个插件还没什么用,如果要添加实用的功能,还得添加这些比较重要的字段:

  • background:背景页通常是 Javascript 脚本,在扩展进程中一直保持运行,它有时也被称为 后台脚本,它是一个集中式的事件处理器,用于处理各种扩展事件,它不能访问页面上的 DOM,但是可以和 content_scriptsaction 之间进行通信;在 V2 版本中,background 可以定义为 scriptspage,但是在 V3 版本中已经废弃,V3 版本中统一定义为 service_worker
  • content_scripts:内容脚本可以让我们在 Web 页面上运行我们自定义的 Javascript 脚本,通过它我们可以访问或操作 Web 页面上的 DOM 元素,从而实现和 Web 页面的交互;内容脚本运行在一个独立的上下文环境中,类似于沙盒技术,这样不仅可以确保安全性,而且不会导致页面上的脚本冲突;
  • action:在 V2 版本中,Chrome 扩展有 browser_actionpage_action 两种表现形式,但是在 V3 版本中,它们被统一合并到 action 字段中了;用于当用户点击浏览器右上角的扩展图标时弹出一个 popup 页面或触发某些动作;
  • options_page:当你的扩展参数比较多时,可以制作一个单独的选项页面对你的扩展进行配置;

接下来,我们在 manifest.json 文件中加上 action 字段:

  "action": {
    "default_popup": "popup.html"
  }

然后,编写一个简单的 popup.html 页面,比如直接使用 iframe 嵌入我的博客:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <iframe src="https://www.aneasystone.com" frameborder="0" style="width: 400px;height:580px;"></iframe>
    </body>
</html>

修改完成后,点击扩展上的刷新按钮,将会重新加载扩展:

chrome-extension-sample-reload.png

这样当我们点击扩展图标时,就能弹出我的博客页面了:

chrome-extension-sample-popup.png

如果我们把页面换成 ChatGPT 的页面,那么一个 ChatGPT 的 Chrome 插件就做好了:

chrome-extension-chatgpt.png

在嵌入 ChatGPT 页面时发现,每次打开扩展都会跳转到登录页面,后来参考 kazuki-sf/ChatGPT_Extension 这里的做法解决了:在 manifest.json 中添加 content_scripts 字段,内容脚本非常简单,只需要一句 "use strict"; 即可。

注意并不是所有的页面都可以通过 iframe 嵌入,比如当我们嵌入 Google 时就会报错:www.google.com 拒绝了我们的连接请求,这是因为 Google 在响应头中添加了 X-Frame-Options: SAMEORIGIN 这样的选项,不允许被嵌入在非同源的 iframe 中。

实现划词翻译功能

我们现在已经学习了如何使用 OpenAI 接口实现翻译功能,也学习了 Chrome 扩展的基本知识,接下来就可以实现划词翻译功能了。

首先我们需要监听用户在页面上的划词动作以及所划的词是什么,这可以通过监听鼠标的 onmouseup 事件来实现。根据前面一节的学习我们知道,内容脚本可以让我们在 Web 页面上运行我们自定义的 Javascript 脚本,从而实现和 Web 页面的交互,所以我们在 manifest.json 中添加 content_scripts 字段:

  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["content_script.js"]
    }
  ]

content_script.js 文件的内容很简单:

window.onmouseup = function (e) {

    // 非左键,不处理
    if (e.button != 0) {
        return;
    }
    
    // 未选中文本,不处理
    let text = window.getSelection().toString().trim()
    if (!text) {
        return;
    }

    // 翻译选中文本
    let translateText = translate(text)

    // 在鼠标位置显示翻译结果
    show(e.pageX, e.pageY, text, translateText)
}

可以看到实现划词翻译的整体脉络已经非常清晰了,后续的工作就是调用 OpenAI 的接口翻译文本,以及在鼠标位置将翻译结果显示出来。先看看如何实现翻译文本:

async function translate(text) {
    const prompt = `Translate this into Simplified Chinese:\n\n${text}\n\n`
    const body = {
        "model": "text-davinci-003",
        "prompt": prompt,
        "max_tokens": 100,
        "temperature": 0
    }
    const options = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + OPENAI_API_KEY,
        },
        body: JSON.stringify(body),
    }
    const response = await fetch('https://api.openai.com/v1/completions', options)
    const json = await response.json()
    return json.choices[0].text
}

这里直接使用我们之前所用的提示语,只不过将发请求的 axios 换成了 fetchfetch 是浏览器自带的发请求的 API,但是不能在 Node.js 环境中使用。

接下来我们需要将翻译后的文本显示出来:

function show(x, y, text, translateText) {
    let container = document.createElement('div')
    container.innerHTML = `
    <header>翻译<span class="close">X</span></header>
    <main>
      <div class="source">
        <div class="title">原文</div>
        <div class="content">${text}</div>
      </div>
      <div class="dest">
        <div class="title">简体中文</div>
        <div class="content">${translateText}</div>
      </div>
    </main>
    `
    container.classList.add('translate-panel')
    container.classList.add('show')
    container.style.left = x + 'px'
    container.style.top = y + 'px'
    document.body.appendChild(container)

    let close = container.querySelector('.close')
    close.onclick = () => {
        container.classList.remove('show')
    }
}

我们先通过 document.createElement() 创建一个 div 元素,然后将其 innerHTML 赋值为提前准备好的一段 HTML 模版,并将原文和翻译后的中文放在里面,接着使用 container.classList.add()container.style 设置它的样式以及显示位置,最后通过 document.body.appendChild() 将这个 div 元素添加到当前页面中。实现之后的效果如下图所示:

chrome-extension-translate.png

至此,一个简单的划词翻译 Chrome 插件就开发好了,开发过程中参考了 CaTmmao/chrome-extension-translate 的部分实现,在此表示感谢。

当然这个扩展还有很多需要优化的地方,比如 OpenAI 的 API Keys 是写死在代码里的,可以做一个选项页对其进行配置;另外在选择文本时要等待一段时间才显示出翻译的文本,中间没有任何提示,这里的交互也可以优化一下;还可以为扩展添加右键菜单,进行一些其他操作;有兴趣的朋友可以自己继续尝试改进。

本文所有代码 在这里

参考

扫描二维码,在手机上阅读!