Fork me on GitHub

分类 大模型 下的文章

使用 Embedding 技术打造本地知识库助手

基于不同的提示语,可以让 ChatGPT 实现各种不同的功能,比如在 基于 ChatGPT 实现一个划词翻译 Chrome 插件 这篇文章中,我基于 ChatGPT 实现了一个翻译助手,OpenAI 官方的 Examples 页面 也列出了提示语的更多示例,展示了 ChatGPT 在问答、翻译、文本总结、代码生成、推理等各方面的能力。

尽管 ChatGPT 的表现非常亮眼,但是它也有其局限性,由于它是基于互联网上公开的资料训练的,所以它只能回答公开领域的知识的问题,比如你问它是谁发明了空调,第一个登月的人是谁,它都能回答得头头是道,但是对于一些私有领域的知识,比如你问它张三家的宠物狗叫什么名字,它就鞭长莫及了。

Fine tuning vs. Embedding

打造私有领域的知识库助手对于企业和个人来说是一个非常重要的应用场景,可以实现个性化定制化的问答效果,要实现这个功能,一般有两种不同的方式:Fine tuningEmbedding。Fine tuning 又被称为微调,它可以在不改动预训练模型的基础上,对特定任务进一步训练,以适应特定数据和要求。OpenAI Cookbook 中有一个 基于 Fine tuning 实现 QA 的例子,不过官方已经不推荐使用这种方式来做知识问答任务,因为 Fine tuning 更适合于学习新任务或新模式,而不是接受新信息,比如我们可以使用 Fine tuning 让模型按特定的语气或风格来回答问题,或者让模型按固定的格式来回答问题。

相对应的,Embedding 更适合知识问答任务,而且 Embedding 技术还顺便解决了大模型的一个问题,那就是上下文限制,比如 OpenAI 的 GPT-3.5 模型,它的限制在 4k - 16k 个 token,就算是 GPT-4 模型,最多也只支持 32k 个 token,所以,如果你的知识库内容长度超出了限制,我们就不能直接让 ChatGPT 对其进行总结并回答问题。通过 Embedding 技术,我们可以使用语义搜索来快速找到相关的文档,然后只将相关的文档内容注入到大模型的上下文窗口中,让模型来生成特定问题的答案,从而解决大模型的限制问题。这种做法比 Fine tuning 速度更快,而且不需要训练,使用上也更灵活。

Embedding 也被称为嵌入,它是一种数据表征的方式,最早可以追溯到 1986 年 Hinton 的论文 《Learning distributed representations of concepts》,他在论文中提出了分布式表示(Distributed Representation)的概念,这个概念后来被人们称为词向量或词嵌入(Word Embedding),使用它可以将单词表示成一个数字向量,同时可以保证相关或相似的词在距离上很接近。Embedding 技术发展到今天,已经可以将任意对象向量化,包括文本、图像甚至音视频,在搜索和推荐等业务中有着广泛的应用。

知道了 Embedding 技术,我们就可以理解如何使用 Embedding 实现本地知识问答了,整个处理流程如下图所示:

qa-with-embedding.png

构建本地知识库

假设我们有一个本地知识库,这可能是某个产品的使用手册,或者某个公司的内部文档,又或者是你自己的一些私人资料,我们希望 ChatGPT 能够回答关于这些本地知识的问题。根据上面的流程图,我们首先需要对我们的知识库进行 Embedding 处理,将知识库中的所有文档向量化,这里其实涉及两个问题:

  1. 如何计算每个文档的向量?
  2. 如何存储每个文档的向量?

如何计算文档的向量?

对此,前辈大佬们提出了很多种不同的解决方案,比如 Word2vec、GloVe、FastText、ELMo、BERT、GPT 等等,不过这些都是干巴巴的论文和算法,对我们这种普通用户来说,可以直接使用一些训练好的模型。开源项目 Sentence-Transformers 是一个很好的选择,它封装了 大量可用的预训练模型;另外开源项目 Towhee 不仅支持大量的 Embedding 模型,而且还提供了其他常用的 AI 流水线的实现,这里是它支持的 Embedding 模型列表;不过在本地跑 Embedding 模型对机器有一定的门槛要求,我们也可以直接使用一些公开的 Embedding 服务,比如 OpenAI 提供的 /v1/embeddings 接口,它使用的 text-embedding-ada-002 模型是 OpenAI 目前提供的效果最好的第二代 Embedding 模型,相比于第一代的 davincicuriebabbage 等模型,效果更好,价格更便宜。我们这里就直接使用该接口生成任意文本的向量。使用 OpenAI 的 Python SDK 调用该接口如下:

import os
import openai

openai.api_key = os.getenv("OPENAI_API_KEY")

text_string = "sample text"
model_id = "text-embedding-ada-002"

embedding = openai.Embedding.create(input=text_string, model=model_id)['data'][0]['embedding']
print(embedding)

输出的是一个长度为 1536 的数组,也可以说是一个 1536 维的向量:

[
    -0.0022714741062372923, 
    0.009765749797224998, 
    -0.018565727397799492,
    ...
     0.0037550802808254957, 
     -0.004177606198936701
]

如何存储文档的向量?

第二个问题是计算出来的向量该如何存储?实际上,自从大模型兴起之后,Embedding 和向量数据库就变成了当前 AI 领域炙手可热的话题,一时间,涌出了很多专注于向量数据库的公司或项目,比如 PineconeWeaviateQdrantChromaMilvus 等,很多老牌数据库厂商也纷纷加入向量数据库的阵营,比如 ElasticSearchCassandraPostgresRedisMongo 等。

我们这里使用 Qdrant 作为示例,首先通过 Docker 在本地启动 Qdrant 服务:

$ docker run -p 6333:6333 -v $(pwd)/data:/qdrant/storage qdrant/qdrant

然后通过下面的代码创建一个名为 kb 的向量库:

from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams

client = QdrantClient("127.0.0.1", port=6333)
client.recreate_collection(
    collection_name='kb',
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)

注意我们指定向量维度为 1536,这是 OpenAI Embedding 输出的维度,另外指定距离为 COSINE,它决定如何度量向量之间的相似度,这个我们后面再讲。

作为示例,我们将一个文件 kb.txt 作为本地知识库,文件中的每一行代表一条知识,比如:

小红的好朋友叫小明,他们是同班同学。
小明家有一条宠物狗,叫毛毛,这是他爸从北京带回来的。
小红家也有一条宠物狗,叫大白,非常听话。
小华是小明的幼儿园同学,从小就欺负他。

然后我们读取该知识库文件,依次计算每一行的向量,并将其保存在 kb 库中:

with open('kb.txt', 'r', encoding='utf-8') as f:
    for index, line in enumerate(tqdm.tqdm(f.readlines())):
        embedding = to_embedding(line)
        client.upsert(
            collection_name='kb',
            wait=True,
            points=[
                PointStruct(id=index+1, vector=embedding, payload={"text": line}),
            ],
        )

在保存向量时,可以通过 payload 带上一些元数据,比如这里将原始的文本内容和向量一起保存,这样可以方便后面检索时知道向量和原始文本的对应关系。

至此我们的本地知识库就初始化好了。

实现本地知识问答助手

构建好知识库之后,我们就可以基于知识库实现本地知识库助手了。接下来要解决的问题也有两个:

  1. 如何从知识库中检索出和问题最相关的文档?
  2. 如何让 ChatGPT 回答关于知识库的问题?

如何检索知识?

传统的检索方式是关键词检索,比如 Elasticsearch 通过分词和倒排索引,让我们可以快速搜出含有关键词的文档,再通过一定的排序规则,比如文档中包含的关键词越多得分越高,就可以检索出和问题最相关的文档。不过这种方式面临非常多的问题,比如一个关键词可能有很多同义词,用户搜 “番茄” 时可能就搜不到包含 “西红柿” 的文档。另外,我们还可能需要对用户的输入进行纠错,对用户进行意图识别,不同语言的处理等等,或者用户希望搜索图片、音频或视频,这时传统的检索方式就捉襟见肘了。

我们上面已经介绍过 Embedding 技术,通过 Embedding 技术可以将任何事物表示成一个向量,而且最牛的地方是,它可以保证相关或相似的事物在距离上很接近,或者说有着相似的语义,这就给我们提供了一种新的检索方式:语义搜索(Semantic Search)。

所以想要搜索出和用户问题最相关的文档,我们可以先将用户问题通过 Embedding 转换为向量,然后在上面构建好的知识库中搜索和问题向量最接近的向量及对应的文档即可。那么怎么计算两个向量之间的距离呢?其实我们在上面创建库时已经使用过 COSINE,除了 COSINE,Qdrant 还支持使用 EUCLIDDOT 等方法:

  • COSINE - 余弦相似度,计算两个向量之间的夹角,夹角越小,向量之间越相似;
  • EUCLID - 欧几里得距离,计算两个向量之间的直线距离,距离越近,向量之间越相似;这种度量方法简单直接,在 2 维或 3 维等低维空间表现得很好,但是在高维空间,每个向量之间的欧几里得距离都很靠近,无法起到度量相似性的作用;
  • DOT - 点积相似度,点积是两个向量的长度与它们夹角余弦的积,点积越大,向量之间就越相似。

在机器学习和数据挖掘中还有很多度量距离的方法:

  • 闵可夫斯基距离
  • 曼哈顿距离
  • 切比雪夫距离
  • 马氏距离
  • 皮尔逊相关系数
  • 汉明距离
  • 杰卡德相似系数
  • 编辑距离
  • DTW 距离
  • KL 散度

不过这些度量方法在语义搜索中用的不多,感兴趣的同学可以进一步了解之。

在语义搜索中,用的最多的是余弦相似度。当计算出用户问题的向量之后,我们就可以遍历知识库中的所有向量,依次计算每个向量和问题向量之间的距离,然后按距离排序,取距离最近的几条数据,就能得到和用户问题最相似的文档了。

不过很显然这里存在一个问题,如果知识库中的向量比较多,这种暴力检索的方法就会非常耗时。为了加快检索向量库的速度,人们提出了很多种 ANN(Approximate Nearest Neighbor,相似最近邻)算法,算法的基本思路是通过对全局向量空间进行分割,将其分割成很多小的子空间,在搜索的时候,通过某种方式,快速锁定在某一个或某几个子空间,然后在这些子空间里做遍历。可以粗略地将这些子空间认为是向量数据库的索引。常见的 ANN 算法可以分为三大类:基于树的方法、基于哈希的方法、基于矢量量化的方法,比如 Annoy、KD 树、LSH(局部敏感哈希)、PQ(乘积量化)、HNSW 等。

Qdrant 针对负载数据和向量数据使用了不同的 索引策略,其中对向量数据的索引使用的就是 HNSW 算法。通过 client.search() 搜索和问题向量最接近的 N 个向量:

question = '小明家的宠物狗叫什么名字?'
search_results = client.search(
    collection_name='kb',
    query_vector=to_embedding(question),
    limit=3,
    search_params={"exact": False, "hnsw_ef": 128}
)

搜索出来的结果类似于下面这样,不仅包含了和问题最接近的文档,而且还有对应的相似度得分:

[
    ScoredPoint(id=2, version=1, score=0.91996545, payload={'text': '小明家有一条宠物狗,叫毛毛,这是他爸从北京带回来的。\n'}, vector=None), 
    ScoredPoint(id=3, version=2, score=0.8796822, payload={'text': '小红家也有一条宠物狗,叫大白,非常听话。\n'}, vector=None), 
    ScoredPoint(id=1, version=0, score=0.869504, payload={'text': '小红的好朋友叫小明,他们是同班同学。\n'}, vector=None)
]

如何向 ChatGPT 提问?

经过上面的三步,我们的本地知识库助手其实就已经完成 90% 了,下面是最后一步,我们需要将搜出来的文档和用户问题重新组装并丢给 ChatGPT,下面是一个简单的 Prompt 模板:

你是一个知识库助手,你将根据我提供的知识库内容来回答问题
已知有知识库内容如下:
1. 小明家有一条宠物狗,叫毛毛,这是他爸从北京带回来的。
2. 小红家也有一条宠物狗,叫大白,非常听话。
3. 小红的好朋友叫小明,他们是同班同学。
请根据知识库回答以下问题:小明家的宠物狗叫什么名字?

组装好 Prompt 之后,通过 openai.ChatCompletion.create() 调用 ChatGPT 接口:

completion = openai.ChatCompletion.create(
    temperature=0.7,
    model="gpt-3.5-turbo",
    messages=format_prompt(question, search_results),
)
print(completion.choices[0].message.content)

得到 ChatGPT 的回复:

小明家的宠物狗叫毛毛。

总结

本文首先从 Fine tuning 和 Embedding 技术聊起,介绍了如何通过这些技术实现本地知识库助手;然后通过学习 OpenAI 的 Embedding 接口和开源向量数据库 Qdrant,从零开始实现了一个非常简单的知识库助手程序,程序的完整源码 在这里。源码实现得非常粗糙,只是对技术原理的简单验证,如果要实现一个真实可用的知识库助手,还有很多其他问题值得考虑,比如如何支持更多类型的文档(pdf、word、excel),如何支持对数据库中的数据进行问答,如何针对大文档进行拆分,等等。

参考

更多

类似项目

基于 ChatGPT 实现一个划词翻译 Chrome 插件

去年 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 是写死在代码里的,可以做一个选项页对其进行配置;另外在选择文本时要等待一段时间才显示出翻译的文本,中间没有任何提示,这里的交互也可以优化一下;还可以为扩展添加右键菜单,进行一些其他操作;有兴趣的朋友可以自己继续尝试改进。

本文所有代码 在这里

参考