基于最新 ChatGPT API 实现命令行版 ChatGPT
引子
OpenAI 这两天发布了 ChatGPT API,基于 gpt-3.5-turbo
模型,这是一个 GPT-3.5 的优化版本,用于支持开发者把 ChatGPT 集成到自己的产品中,同时把 API 调用价格降到 $0.002 每千 token,意味着处理 100万字符的文本只需要 2 美元,也就是差不多十几块钱人民币,效果更好、价格更低,这让 ChatGPT API 更具性价比,因此这两天基于 ChatGPT API 的各种套壳应用如雨后春笋般大量冒出。
我也来凑个热闹,试一试水,正好在我今天新建的 ChatGPT 互助讨论群里有人问有没有命令行版 ChatGPT,那就拿它来开刀吧:
调用 API 需要传递 API KEY,我们不希望这个 KEY 硬编码在代码中,而是从系统环境变量读取,从而让代码更安全可维护;
调用封装好的 Go OpenAI 库与 API 接口进行交互,避免通过 HTTP 协议与原生 API 交互编写大量重复代码,让代码更简洁优雅;
- 做一个给客户使用的产品,美观会带来更好的用户体验,所以我希望即便是命令行应用,也尽可能让交互和输出更美观一些。
对于第 2 点,可以使用 go-gpt3 库,这是一个通过 Go 封装的 OpenAI API 调用库。
对于第3点,可以使用 glamour 库,这是一个 Go 语言实现的、能够在兼容 ANSI 终端基于样式渲染 Markdown 文本的第三方库,它是让命令行更美观的开源项目 Charm 的一部分。
因为项目很简单,又是在客户端本地使用,所以不需要做什么复杂的架构,下面直接进入编码部分。
代码编写
面向 ChatGPT 编程的核心就是把需求尽可能准确全面地转化为 Prompt 传递给 ChatGPT,这里有产品需求(来自业务和产品),也有代码设计和架构上的需求(来自开发者),然后让它生成代码,这是作为一个合格的 Prompt 工程师自我修养的必要组成部分。
在《ChatGPT 提示的艺术 —— 编写清晰有效提示指南(二)》这篇教程中,我已经给大家介绍了编写清晰有效 Prompt 的原则、做法和技巧,感兴趣的可以去看下,这里我先将我的需求转化为 Prompt 让 ChatGPT 替我编写对应的 Go 代码实现:
代码优化
看起来不错,基本流程没问题,但是代码审核会发现,它现在把输入的 Prompt 写死了,并不能动态接收用输入,而且运行一次后就退出了,这不是 ChatGPT 的问题,而是我们的需求并没有明确这一点,作为一个完整的需求和程序,需要说明是什么,怎么样,什么时候开始,什么时候退出。
不过细心的同学可能还留意到 go-gpt3
包引入的时候没有设置别名,会导致运行时出错,同时调用的 OpenAI API 接口也不对,最新的 ChatGPT API 接口方法应该是 CreateChatCompletion
,可能是太新的缘故,ChatGPT 还没有学习到这里,不过这都属于 ChatGPT 要优化的点了。以及使用了另一个同名的包,这个也需要明确告知它。
细节上还是需要优化,现在我们基于这些要点先来完善我们的 Prompt:
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/charmbracelet/glamour"
"github.com/common-nighthawk/go-figure"
gpt3 "github.com/sashabaranov/go-gpt3"
)
func main() {
// 获取 OpenAI API Key
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
fmt.Println("请设置 OPENAI_API_KEY 环境变量")
return
}
// 初始化 Glamour 渲染器
renderStyle := glamour.WithEnvironmentConfig()
mdRenderer, err := glamour.NewTermRenderer(
renderStyle,
)
if err != nil {
fmt.Println("初始化 Markdown 渲染器失败")
return
}
// 输出欢迎语(命令行应用启动界面)
myFigure := figure.NewFigure("ChatGPT", "", true)
myFigure.Print()
fmt.Println("输入 start 启动应用,输入 quit 退出应用")
// 创建 ChatGPT 客户端
client := gpt3.NewClient(apiKey)
if err != nil {
fmt.Printf("创建客户端失败: %s\n", err.Error())
return
}
messages := []gpt3.ChatCompletionMessage{
{
Role: "system",
Content: "你是ChatGPT, OpenAI训练的大型语言模型, 请尽可能简洁地回答我的问题",
},
}
// 读取用户输入并交互
var userInput string
for {
fmt.Scanln(&userInput)
if strings.ToLower(userInput) == "start" {
fmt.Println("ChatGPT 启动成功,请输入您的问题:")
} else if strings.ToLower(userInput) == "quit" {
fmt.Println("ChatGPT 已退出")
return
} else if userInput != "" {
messages = append(
messages, gpt3.ChatCompletionMessage{
Role: "user",
Content: userInput,
},
)
// 调用 ChatGPT API 接口生成回答
resp, err := client.CreateChatCompletion(
context.Background(),
gpt3.ChatCompletionRequest{
Model: gpt3.GPT3Dot5Turbo,
Messages: messages,
MaxTokens: 1024,
Temperature: 0,
N: 1,
},
)
if err != nil {
fmt.Printf("ChatGPT 接口调用失败: %s\n", err.Error())
continue
}
// 格式化输出结果
output := resp.Choices[0].Message.Content
mdOutput, err := mdRenderer.Render(output)
if err != nil {
fmt.Printf("Markdown 渲染失败: %s\n", err.Error())
continue
}
fmt.Println(mdOutput)
messages = append(
messages, gpt3.ChatCompletionMessage{
Role: "assistant",
Content: output,
},
)
}
}
}
gpt3 别名和 CreateChatCompletion 方法调用相关的代码还是需要手动调整,不过这也是我前面说的面向 ChatGPT 编程的原则之一,最后一定要审核 ChatGPT 的代码,它目前对于最新的知识还是有一定的迟滞性。
代码细节我就不展开解释了,有不明白的地方可以参考我在《面向 ChatGPT 编程实现全栈开发的 18 种方法》这篇教程中代码解释部分提供的方法自行去基于 ChatGPT 查看。
效果展示
最后我们在终端体验一下这个命令行版 ChatGPT,我这里使用的是 Windows WSL 终端,Windows 终端本身体验其实不太好,尤其是中文输入的时候,删除字符特别费劲,且很容易造成消息变形,如果是 Mac 或者 Ubuntu 终端可能效果会更好一些。
首先我们启动这个应用,如果没有设置 OPENAI_API_KEY
这个系统环境变量(运行 export OPENAI_API_KEY={你的 OpenAI SECRET KEY}
命令设置即可),会提示你设置:
终端需要能够访问 OpenAI API 才能调用成功,这意味着命令行也要支持科学上网。
好了,这就是我们基于最新 ChatGPT API 实现的命令行版 ChatGPT 应用,因为 ChatGPT API 是前两天才发布的,所以看起来 ChatGPT 并没有学习到这个最新的 API 如何调用,存在迟滞性,进而导致编写的代码并不能直接满足需求,需要人为介入去修改,希望未来 ChatGPT 能够在这一块上有所进化。
欢迎点赞、关注、分享,更多关于 ChatGPT 的学习实践探讨,请关注这个订阅号或者点击阅读原文了解极客书房最新动态。