2022 年 11 月 ChatGPT 像一股风暴席卷全球。时隔数月,OpenAI 终于在 3 月 1 日正式推出了 ChatGPT 的开放 API。这意味着,我们通过简单的 API 调用,就可以与 ChatGPT 进行对话。可以预见的是像自来水一样使用 AI 的时代已经到来,我们可以随时随地使用它,而不需要关心算法实现细节。
值得注意的是在此之前有大量的第三方平台号称调用的是 ChatGPT 的 API,实际多数为基于 GPT-3 的“自动补齐” API,其能力远不可与 ChatGPT 相媲美,而这一次提供的则是官方的基于聊天(Chat)消息的 API。
本文将通过一个简单的命令行翻译程序,来展示如何使用 ChatGPT API。
你以为 API 调用工程就是本文的全部内容吗?不,更重要的是教会大家如何通过“Prompt Engineering”(即所谓“提示工程”学)将聊天型 AIGC 转换为特定领域的生产力。
本文的源代码已上传至 Github (https://github.com/MagicCube/cli-gpt)。
废话不多说,先上 DEMO 为敬:
你是否经常会经常忘记某个命令行工具的参数该怎么写?正确的顺序是什么?或者是 git、docker 命令的新手?通过我们接下来要开发的这个程序,你只需要用中英文(或任何语言)描述你想要完成的任务,AI 会将它转换为对应的命令行文本,再也不用记忆复杂的命令参数啦~
请注意保管好你的秘钥,因为只有在创建时才会有唯一的机会看见完整的秘钥。
接下来让我们认识一下 ChatGPT API。通过阅读官方说明 (https://platform.openai.com/docs/guides/chat),我们可以得知几点:
虽然 API 才上线几天时间,国内外已经有大量的开源项目基于它开发了各种有趣的应用,这里隆重推荐:
接下来,我们将进入本文的重点——Prompt Engineering。ChatGPT 是一个通用型聊天机器,它可以回答任何问题,但是它的回答并不是总是符合我们的预期。因此,我们需要通过 Prompt Engineering 来提示 AI 如何进行输出。
在 ChatGPT 中,提示是由一组聊天消息组成的,每个消息都是由一个特定角色说的话,这些角色包括:
为了更好的解释三者的关系以及如何进行 Prompt,我们将引入一个十分好用的官方工具——Playground (https://platform.openai.com/playground?mode=chat)。
如上图所示,Playground 由三个部分组成:
截止至本文发稿前,Playground 还处于 Beta 阶段,包括右上角的 View Code、Save 和 Share 等功能还未上线,但是它已足够满足我们的需求了。
在下文中为了更好的表示我们录入的提示和输出,我们将使用如下的记号文本:
# SYSTEM
在这里添加系统的行为和限定规则。
支持多行文本
# USER
在这里添加用户的输入。
# ASSISTANT
在这里添加机器的输出。
回到本文示例,我们将实现一个简单的命令行翻译程序,它可以将人类的自然语言描述的指令翻译成对应的命令行。
Prompt Engineering 很像是用自然语言编程,我们需要用到逻辑条件甚至是循环(通过停止条件 + 类似 GOTO 语句组合实现)。
让我们先从最基本的开始,首先我们需要为 ChatGPT 制定扮演的角色。
# SYSTEM
你是一个命令行翻译程序,你可以将人类自然语言描述的指令翻译成对应的命令行语句。
# USER
列出所有 TS 文件
这时,我们可以看到 ChatGPT 已经给出了一个结果,但是存在一些问题:
根据第一步的经验,我们重新设计了 Prompt:
# SYSTEM
你是一个命令行翻译程序,你可以将人类自然语言描述的指令翻译成对应的命令行语句。
1. 你只需要将翻译好的指令直接输出,而不需要对其进行任何的解释。
# USER
显示所有 MD 文件
这时,我们会发现,无论我们尝试 Submit 多少次,ChatGPT 都会返回相同的结果,并且不再会有注解说明。
注意,Submit 前需要手工删除此前 ASSISTANT 输出的内容,否则 ChatGPT 会将其作为输入的一部分。
但是如果我们问机器一些与命令无关的问题时,他会出现下面的情况:
因此,接下来我们需要告诉机器,如果用户输入的内容不是命令,需要如何回答,这相当于编程语言中的 throw 语句。
这里我们使用“如果 / 或者 / 那么”等逻辑单元进行描述,我们修改一下 Prompt:
# SYSTEM
你是一个命令行翻译程序,你可以将人类自然语言描述的指令翻译成对应的命令行语句。
1. 你只需要将翻译好的指令直接输出,而不需要对其进行任何的解释。
2. 如果你不明白我说的话,或不确定如何将我所说的指令转换为计算机命令行,请直接输出 7 个字母,“UNKNOWN” ,无需其他解释。
# USER
你的外婆是谁?
这样当我们问起与命令无关的问题时,ChatGPT 就会直接输出 UNKNOWN 了。
现在让我们再来试一个复杂的命令:
这里我们假设需要命令行翻译器永远只输出一个单行命令,在下一步中我们将添加更多的限定。
在这一步中,我们继续添加规则,以进一步“限定”输出的格式,修改后的 Prompt 如下:
# SYSTEM
你是一个命令行翻译程序,你可以将人类自然语言描述的指令翻译成对应的命令行语句。
1. 你只需要将翻译好的指令直接输出,而不需要对其进行任何的解释。在输出的最前面加上“>”符号。
2. 如果你不明白我说的话,或不确定如何将我所说的指令转换为计算机命令行,请直接输出 7 个字母,“UNKNOWN” ,无需其他解释和“>”符号。
3. 如果翻译后的结果不止一行命令,则请务必将它们通过 & 或 && 合并为单行命令。
众所周知,一些命令对系统来说是十分危险的,比如著名的 rm -rf /,因此我们需要在命令行翻译器中添加一个“危险提示”功能,当用户输入的命令包含危险关键字时,需要提示用户是否继续执行。
# SYSTEM
你是一个命令行翻译程序,你可以将人类自然语言描述的指令翻译成对应的命令行语句。
1. 你只需要将翻译好的指令直接输出,而不需要对其进行任何的解释。在输出的最前面加上“>”符号。
2. 如果你不明白我说的话,或不确定如何将我所说的指令转换为计算机命令行,请直接输出 7 个字母,“UNKNOWN” ,无需其他解释和“>”符号。
3. 如果翻译后的结果不止一行命令,则请务必将它们通过 & 或 && 合并为单行命令。
4. 如果该命令会存在可能的风险或危害,请在输出的末尾另一行,并添加 “DANGEROUS”,无需其他的警告或提示。
这里我们会发现,无论如何修改 SYSTEM 的提示,ASSISTANT 的输出总是不尽人意,似乎它总是不能完全理解我们的意图,要不就是一堆注意事项,要不就是不知道 DANGEROUS 应该放在哪里。这时,我们必须用到一个新技能这就是“榜样”。
前文说过,ChatGPT 会将整个聊天记录作为输入,因此我们可以通过提供一些“榜样”来让 ChatGPT 更好地理解我们的意图。这意味着我们可以在界面上将 ASSISTANT 原先错误的回答修改为正确的,也就是给出了正确回答的“好榜样”。
如上图所示,经过修改后,ChatGPT 就能够正确理解我们的意图,输出正确格式的结果。
因为 ChatGPT 的训练数据以英文为主,如果你想达到最佳的效果,那么你需要将你的 Prompt 翻译为英文。这里我们当然可以使用 ChatGPT 充当翻译,翻译后的 Prompt 如下:
# SYSTEM
You are a command line translation program. You can translate natural language instructions from human language into corresponding command line statements.
1. Simply output the translated instruction without any explanation. Add the ">" symbol at the beginning of the output.
2. If you don't understand what I'm saying or are unsure how to convert my instructions into a computer command line, simply output the 7 letters "UNKNOWN" without any other explanation or ">" symbol.
3. If the translated result consists of more than one line of commands, please use '&' or '&&' to combine them into a single line of command.
4. If this is a dangerous command, please start a new line at the end of the output and output "DANGEROUS" without any other warnings or prompts.
# USER
今天天气如何
# ASSISTANT
UNKNOWN
# USER
从 Github 上克隆 React 库,并且在本地创建新的分支,取名为“feat-gpt”
# ASSISTANT
>git clone https://github.com/facebook/react.git && cd react && git checkout -b feat-gpt
# USER
删除所有的文件或文件夹
# ASSISTANT
>rm -rf *
DANGERS
现在 ChatGPT 最终通过了所有测试,我们可以进入到编码阶段了。
相信这会儿你肯定已经摩拳擦掌了,那么让我们开始动手吧!
我们将使用到的技术栈是 Node.js + TypeScript + OpenAI SDK,尽管如此 Python 的程序员也可以参考本文自行基于官方 SDK (https://github.com/openai/openai-python) 实现。
我们可以将整个项目的架构分为 4 个小模块,他们分别是:
将 OpenAI SDK 封装成一个更加易用的 API,输入为多个 Messages,输出则直接是纯文本结果(包含 UNKNOWN 及 DANGEROUS 也在结果内)。
详见 https://github.com/MagicCube/cli-gpt/blob/main/src/open-ai/index.ts
在上文中,我们自创了一种 Prompt 的表达格式,用“:”加角色名称表示消息的发送者,并且支持多行文本,这非常适合描述多角色 Prompt。因此,我们首先需要一个 Prompt 模板加载器,它可以将模板文件加载为 Prompt 模板,然后再将本次用户从命令行真正输入的内容转换为最后一条 message,这就是 Prompt 生成器的作用。同时模板中可能还包含着一些可替换的变量,例如命令行可能根据操作系统的不同而有差异性,因此在模板中我们可以用 ${OS} 表示指定的操作系统。
详见
https://github.com/MagicCube/cli-gpt/blob/main/src/prompt/index.ts
https://github.com/MagicCube/cli-gpt/blob/main/src/prompt/template.ts
模板详见
https://github.com/MagicCube/cli-gpt/blob/main/prompt-template/default.md
翻译器是 Facade 层,它将上述两个模块的 API 组合起来,将用户的输入通过生成器转化为最终的 Prompt Messages,并且将其通过 API 客户端发送给 OpenAI,最后将结果返回。
详见 https://github.com/MagicCube/cli-gpt/blob/main/src/translator/index.ts
本例中文通过 oclif (https://github.com/oclif/oclif) 库来实现 CLI 界面,它提供了一个非常方便的 CLI 框架,我们只需要专注于 Command 业务逻辑的实现。oclif 还可以帮我们自动发布为全局命令,这样我们就可以在任何地方使用我们的命令行工具了。
详见 https://github.com/MagicCube/cli-gpt/blob/main/src/ui/index.ts
此外,我们不希望将 OpenAI 的 Secret Key 提交到 Github 上。为此,你需要在项目的根目录下新建一个 .env 文件,内容如下:
OPENAI_API_KEY="sk-your-secret-key"
受限于篇幅,这里不再展开介绍每个模块的实现细节,代码都非常好理解,你可以通过点击对应的代码链接查看源码。
通过本文,你可能会对 Prompt Engineering 有更深的理解,也许你会想到一些更有趣的 Prompt 应用场景,并且会发现即便不会写代码,你也可以通过 Prompt 来训练 ChatGPT,从而实现你的想法。
这里介绍一个非常有趣的 Prompt 仓库 Awesome Prompt (https://github.com/f/awesome-chatgpt-prompts),里面收集了很多有趣的 Prompt,相信你一定可以在这里找寻到更多灵感。
另外,一个容易被混淆的概念是“Training”和“Prompt”。
让笔者真正惊讶的是在撰写本文(Markdown)及编写代码示例时,使用的是 VSCode + Github Copilot,无论是中文博文还是源代码,Copilot 都贡献了至少 50% 的文本量。
“人机协同”的时代真的已经到来了吗?
“真的已经到”之后的文字为 Copilot 生成,但是笔者仍然保留了最后一句话的权利。
P.S. 好吧,连上面这句话都是 Copilot 生成的……
点击上方关注 · 我们下期再见