Press "Enter" to skip to content

基于最新的 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,那就拿它来开刀吧:

image-20230303144753053

老规矩,还是面向 ChatGPT 编程来实现这个命令行版 ChatGPT 应用。

在《面向 ChatGPT 编程实现全栈开发的 18 种方法》这篇教程中,我在最后说到在面向 ChatGPT 编程的时候,需要牢记两个原则:第一,知道你在做什么,第二,不要相信 ChatGPT 的代码。

落地到实践的时候,就是在与 ChatGPT 合作形成的结对编程联盟中,作为开发者我们需要承担代码设计、架构、编排与审核(CodeReview)的职责,对方案和结果负责,而具体的代码片段编写这种纯体力活则交由 ChatGPT 完成。

接下来,我们将遵循这个思路实现命令行版 ChatGPT 应用。

代码设计

如我在微信群里所说,这个需求确实很简单 —— 通过编程在控制台应用代码中调用 ChatGPT API 接口,实现一个命令行版的 ChatGPT,几十行代码就能搞定。不过这里仍然需要做一些简单的设计:

  1. 调用 API 需要传递 API KEY,我们不希望这个 KEY 硬编码在代码中,而是从系统环境变量读取,从而让代码更安全可维护;
  2. 调用封装好的 Go OpenAI 库与 API 接口进行交互,避免通过 HTTP 协议与原生 API 交互编写大量重复代码,让代码更简洁优雅;
  3. 做一个给客户使用的产品,美观会带来更好的用户体验,所以我希望即便是命令行应用,也尽可能让交互和输出更美观一些。

对于第 2 点,可以使用 go-openai 库,这是一个通过 Go 封装的 OpenAI API 调用库。

对于第3点,可以使用 glamour 库,这是一个 Go 语言实现的、能够在兼容 ANSI 终端基于样式渲染 Markdown 文本的第三方库,它是让命令行更美观的开源项目 Charm 的一部分。

因为项目很简单,又是在客户端本地使用,所以不需要做什么复杂的架构,下面直接进入编码部分。

代码编写

面向 ChatGPT 编程的核心就是把需求尽可能准确全面地转化为 Prompt 传递给 ChatGPT,这里有产品需求(来自业务和产品),也有代码设计和架构上的需求(来自开发者),然后让它生成代码,这是作为一个合格的 Prompt 工程师自我修养的必要组成部分。

在《ChatGPT 提示的艺术 —— 编写清晰有效提示指南(二)》这篇教程中,我已经给大家介绍了编写清晰有效 Prompt 的原则、做法和技巧,感兴趣的可以去看下,这里我先将我的需求转化为 Prompt 让 ChatGPT 替我编写对应的 Go 代码实现:

image-20230303152914976
image-20230303152941205
image-20230303153048367

注意:go-gpt3 这个扩展包现在已经更名为 go-openai

代码优化

看起来不错,基本流程没问题,但是代码审核会发现,它现在把输入的 Prompt 写死了,并不能动态接收用输入,而且运行一次后就退出了,这不是 ChatGPT 的问题,而是我们的需求并没有明确这一点,作为一个完整的需求和程序,需要说明是什么,怎么样,什么时候开始,什么时候退出。

不过细心的同学可能还留意到 go-gpt3 包引入的时候没有设置别名,会导致运行时出错,同时调用的 OpenAI API 接口也不对,最新的 ChatGPT API 接口方法应该是 CreateChatCompletion,可能是太新的缘故,ChatGPT 还没有学习到这里,翻车了翻车了,不过这都属于 ChatGPT 后面要优化的点了。以及使用了另一个同名的包,这个也需要明确告知它。

细节上还是需要优化,现在我们基于这些要点先来完善我们的 Prompt:

image-20230303161318737

这一次,我们更加细化了程序的行为,用户输入 start 启动,输入 quit 退出,在此期间则不断读取用户输入,返回 ChatGPT 处理结果并渲染到控制台输出,另外,我还在读取系统环境变量时嘱咐通过 os.Getenv 获取,因为有时候发现 ChatGPT 输出不稳定,会尝试从 .env 文件读取 OPENAI_API_KEY,当然这不是 ChatGPT 的问题,是我们没有给出明确清晰的 Prompt。

现在再来看 ChatGPT 输出的代码,就更完善了:

package main
​
import (
    "context"
    "fmt"
    "os"
    "strings"
​
    "github.com/charmbracelet/glamour"
    "github.com/common-nighthawk/go-figure"
    openai "github.com/sashabaranov/go-openai"
)
​
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 := openai.NewClient(apiKey)
    if err != nil {
        fmt.Printf("创建客户端失败: %s\n", err.Error())
        return
    }
​
    messages := []openai.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, openai.ChatCompletionMessage{
                    Role:    "user",
                    Content: userInput,
                },
            )
            // 调用 ChatGPT API 接口生成回答
            resp, err := client.CreateChatCompletion(
                context.Background(),
                openai.ChatCompletionRequest{
                    Model:       openai.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, openai.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} 命令设置即可),会提示你设置:

image-20230303164037797

如果已经设置,会进入下面这样的启动欢迎界面:

image-20230303164028606

输入 start 即可启动应用,然后我们在命令行输入问题,回车,就会将问题提交给 ChatGPT,ChatGPT 处理的结果会返回并输出到控制台,这里的格式经过了 Glamour 库的美化:

image-20230303172815890

因为是 for 循环,所以你可以持续提问、得到答案,直到输入 quit 退出应用。

终端需要能够访问 OpenAI API 才能调用成功,这意味着命令行也要支持科学上网。

好了,这就是我们基于最新 ChatGPT API 实现的命令行版 ChatGPT 应用,因为 ChatGPT API 是前两天才发布的,所以看起来 ChatGPT 并没有学习到这个最新的 API 如何调用,存在迟滞性,进而导致编写的代码并不能直接满足需求,需要人为介入去修改,希望未来 ChatGPT 能够在这一块上有所进化。

本项目源码已经提交到 Github,有需要的自提:geekr-dev/chatgpt-client: 命令行版 ChatGPT 应用

如果你在本地测试过程中遇到 OpenAI 调用超时问题,这多半是因为国内已经墙掉了 OpenAI 的 API 域名,对应的解决办法可以看这里:国内无法调用 OpenAI 接口的解决办法

2 Comments

  1. franktrue
    franktrue 2023年3月16日

    大佬,现在关于gpt3的库都不能用了吗,go-gpt3库github上已经没了,找了一些其他的好像也没有,还有什么推荐的嘛

发表回复