ChatGPTで開発効率アップ！askiを使ったCLIコードレビュー＆バグ特定

エンジニアとしてChatGPTを利用していると、コードや技術文章を基にした会話をしたいことがあります。

ChatGPTのWeb版では、コードや文章をコピペすることで目的を達成することができますが、複数ファイルに跨ったプロジェクトになるとコピペだけでも結構大変です。レビューをして修正したときも、そのファイルを毎度ChatGPTに教えてあげなければいけません。

これの対策のために、一時私はテンプレ文章と指定したコードをコンソール上に表示するシェルスクリプトを作成してコピペして使っていましたが、Web版左側の履歴が大量になってしまう等の問題が発生していました。

これを解決するために `aski` というクライアントを作成しました。コマンドラインで動くプログラムでGo製です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150188/613b14d7-831f-4b95-ec8b-7a96fa386fc1.png)

## CLIクライアント aski
`aski` は、開発者向けに提供されているOpenAIのAPIを利用することで動作するChatGPT向けのCLIクライアントです。

- 会話の背景について定義し、切り替えることができるプロファイル機能
- ファイルを読み取り、会話に含める機能
- 簡単な操作感 😊

[kznrluk/aski: 📬CLI用ChatGPTクライアント](https://github.com/kznrluk/aski)

若干釣り気味のタイトルですが `aski` の紹介も兼ねてコードレビューと不具合をChatGPTに発見させてみる、というのが今回の記事の内容になります。

## インストール
今のところ `go install` にしか対応していません。安定したらビルドを公開できるようにしようと思っています。

```
go install github.com/kznrluk/aski
```

## CLIクライアント askiの機能紹介
ここでは、簡単に機能について紹介してきます。

### 単純なChatGPTクライアントとしての利用
単に `aski` と入力すると、対話モードで利用できます。phpでのFlattenについて聞いてみました。

```
$ aski
kznrluk@SearchEngine> phpでFlattenする関数について教えて
以下はPHPで多次元配列をフラットにするためのサンプルコードです。

function array_flatten($array) {
    $result = array();
    foreach ($array as $key => $value) {
        if (is_array($value)) {
            $result = array_merge($result, array_flatten($value));
        } else {
            $result[$key] = $value;
        }
    }
    return $result;
}

例えば、以下のような多次元配列があるとします。
(以下略)
```


`-c` オプションで対話モードを利用しないで、単発の返答を生成することもできます。bashライクですね。

```bash
$ aski -c こんにちは！じゃんけんぽん✊
こんにちは！じゃんけんぽん！私はAIなので、じゃんけんをすることはできませんが、何かお力になれることがあればおっしゃってくださいね。
```

### ファイルを読み取ってレビューしてもらう
冒頭で紹介しましたが、ファイルの内容を会話に含めたいこともあります。 `aski` では、`$ aski -f file1 -f file2 ...` でファイルの内容を会話に追加することができます。

例えば、`aski` のリポジトリ全体のGoファイルについて追加する場合は、下のようなコマンドになります。
```
$ aski -f */**/*.go
```

記事執筆時点での `lib/aski.go` についてレビューしてもらいましょう。 `-f` でファイル指定をし、`-p` でCodeReview用のプロファイルを指定します。プロファイルについては後述します。

```
$ aski -f .\lib\aski.go -p CodeReview -c "上記コードの改善点を教えてください。"
1. `isBinary`関数の動きが不正確であるため、バイナリファイルを無視しないようになっています。バイナリファイルを無視して、テキストファイルのみを処理するようにトラップ処理を追加しましょう。

2. `Single`関数の応答がエラー解釈によって異なるため、返り値を明確にしましょう。

3. コードがパッケージ名に関連していないため、パッケージ名を変更しましょう。

4. 変数名をより適切にして、コードの読み易さを向上させましょう。

5. ファイルのコンテキストを追加することで、シンタックスハイライトが備わり、クオートされた文の出力を避けることができます。
```

いい感じに答えを返してくれました。試した感じ、いやそこは違うんだよ！ということもありますが、このあたりはGPT-4のAPIが利用できるようになったら改善されていきそうです。

### プロファイルで会話のコンテキストを切り替える

`aski` にはプロファイル機能があります。Web版で新しく会話を開始するときに、「ChatGPTエージェントであるあなたに〇〇してほしいです。」のような全体の会話の流れを定義する発言をすることがあると思います。`aski` ではこれをプロファイルとして定義しておくことで、コマンド実行時に自動的にChatGPTにそれを送信するようにしています。

プロファイルはユーザーのホームディレクトリ内、`.aski/config.yaml` に格納されています。プロファイルの例を下に示します。

```
OpenAIAPIKey: sk-OOOOOOOOOOOOOO
Profiles:
- ProfileName: Default
  UserName: kznrluk
  Model: gpt-3.5-turbo-0301
  Current: true
  SystemContext: |
    質問に答えてください。
  UserMessages:
  - 今日もよろしく！
- ProfileName: CodeReview
  UserName: kznrluk
  Model: gpt-3.5-turbo-0301
  Current: false
  SystemContext: |
    これからプログラムのコードを渡すので、質問に答えてください。
    回答時に、全体のコードを提示する必要はなく、改変したり、指摘したい部分のみを回答してください。
  UserMessages:
  - 良い点もあれば教えてほめてください。
- ProfileName: Translate
  UserName: kznrluk
  Model: gpt-3.5-turbo-0301
  Current: false
  SystemContext: |
    - 入力が日本語だった場合は、英語に直してください
    - 入力が英語だった場合は、日本語に直してください
  UserMessages: []
```

利用例と共に見てみましょう。まずはDefaultプロファイルで試してみます。

```
$ aski -p Default -c "今日はいい天気ですね"
はい、今日はいい天気ですね！晴れていて、気温も過ごしやすいです。外に出かけるのにぴったりの日ですね。
```

次に、Translateプロファイルを試してみます。

```
$ aski -p Translate -c "今日はいい天気ですね"
Today's weather is nice, isn't it?
```

プロファイルで指定した通り、質問としてとらえずに翻訳して返してくれました。この機能を利用することによって、会話の前提となる情報を定義して即座に切り替えて利用することができるようになります。

デフォルトでは、 `Current: true` なものが利用されます。詳細な解説はREADME.mdに書いてあります。

[kznrluk/aski: 📬CLI用ChatGPTクライアント](https://github.com/kznrluk/aski)

## 使用例

ここからは、プログラムのファイルを引数に渡して実行します。

プロファイルはコードレビュー用のものを作成しました。
```
- ProfileName: CodeReview
  UserName: kznrluk
  Current: true
  SystemContext: |
    これからプログラムのコードを渡すので、質問に答えてください。回答時に、全体のコードを提示する必要はなく、改変したり、指摘したい部分のみを回答してください。
  UserMessages:
  - 良い点もあれば教えてほめてください。
```


### 不具合を見つけてもらう
検証のため、あえて不具合のあるコードを作成します。これを `lib/profile.go` として保存します。

```go
func ChangeProfile(cmd *cobra.Command, args []string) {
	cfg, err := config.Init()
	if err != nil {
		panic(err)
	}

	profiles := cfg.Profiles
	_ = survey.AskOne(prompt, &selected)

	for _, p := range profiles {
		if selected == p.ProfileName {
			p.Current = true
		} else {
			p.Current = false
		}
	}

    // 本来はここでProfilesを代入しなおさないとダメ
    // cfg.Profiles = profiles

	if err := config.Save(cfg); err != nil {
		fmt.Printf("Error: %s", err.Error())
		os.Exit(1)
	}
	return
}
```

ファイルを添付して質問します。解決方法は私のコードと異なりますが、原因についてきちんと帰ってきました。

````
$ aski -f .\lib\profile.go -c "ファイル保存時、なぜかProfilesのCurrentの値がTrueになりません。なぜでしょうか？"
原因は、変数`profiles`が`Init()`関数で新しい`slices`として宣言され、それに変更を加えても、元のコン
フィグ構造体に影響を与えません。そのため、`config.Save(cfg)`で元のコンフィグ構造体に変更を反映する
ことができません。

解決策として、新しいスライスの代わりに、コンフィグ構造体内の`slices`を直接更新する必要があります。
次のコードを試してみてください。

```
for i, p := range cfg.Profiles {
        if selected == p.ProfileName {
            cfg.Profiles[i].Current = true
        } else {
            cfg.Profiles[i].Current = false
        }
    }
```
````

### レビューをしてもらう
では、コードを修正してレビューをしてもらいます。プロファイルにいい点についても触れて、と書いてありますのでまずほめてくれていますね。

```
$ aski -f .\lib\profile.go -p CodeReview -c "上記のファイルについて、レビューしてもらえますか？"
`ChangeProfile` 関数は、config パッケージから profile を選択して変更するコマンドの一部です。
以下は、コードの優れた点と改善が必要な点です。

良い点:

- 関数は明確な役割を持ち、機能単位で切り分けられています。
- スライス `strs` は、選択肢を管理するために使用されます。
- prompt は survey.Select のインスタンスであり、選択可能なオプションを指定できます。
- `AskOne` 関数は、ユーザーから回答を求めることができます。
- 関数の最後で、config を変更したあとに、それを保存する必要があります。

改善が必要な点:

- エラーがあれば panic を発生させています。そうでなくても、エラーを処理する必要があります。
- エラー処理は、関数内で行われるべきです。`fmt.Printf` を使用してエラーメッセージを出力している点についても同様です。
- 現在の選択した profile 以外のすべての profiles に対して、 Current フィールドが false に設定されます。
しかし、現在選択されている profile の Current フィールドを true に設定する必要があります。
```

内容は今回は置いておくとして、うまく動作しています。


### テストコードを書いてもらう

ChatGPTが作ったテストコードの是非については置いておくとして、ファイルを渡してから関数を指定して、テストコードを書くようお願いするとそれの出力もしてくれます。ちゃんとテーブル駆動型でした。Goっぽいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150188/aac2a3d7-cef6-a314-a9e5-58fd96d43522.png)



### ソースコードのコメントを翻訳してもらう
ローカルにテキストファイルとして存在しているものであれば、どんなものでもChatGPTに渡すことができます。

例えばライブラリのソースコード。`//` で区切られたコメントがあったとしても、それをファイルに渡したうえでどこを翻訳してほしいか指示してあげることでそこそこの精度で翻訳してくれます。今までみたいに `//` と改行を削除しなくてもOK！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150188/04375904-9195-2189-898f-348b3f67d28b.png)

## Dockerでビルド
ローカル環境を汚したくない人はDockerを使えば下のようなコマンドでもビルドできると思います。

```
git clone https://github.com/kznrluk/aski.git
cd aski

# Dockerコンテナを実行し、リポジトリをマウントします
docker run --rm -it -v ${PWD}:/usr/src/app -w /usr/src/app golang:latest bash

# Windows向けにクロスプラットフォームビルドします
GOOS=windows GOARCH=amd64 go build -o aski.exe

# 成果物を抽出します
docker container cp $(docker ps -lq):/usr/src/app/aski.exe .
```

最初の利用時にOpenAIのAPIKeyの入力を求められるので入力をお願いします。

## 現状の制限
OpenAIが提供しているAPIでは、トークンという単位で一度に処理できる内容の大きさが決まっています。

現状、このプログラムが利用しているGPT-3.5では4kまでのトークンとなっています。大体ですが、 `aski` のリポジトリ全体が4200トークンくらいですので、すべてのコードを同時にレビューしてもらうことはできません。必要に応じて、添付するファイルを選ぶ必要があります。

また、これはWeb版でも遭遇した方も多いと思いますが、会話が長くなってくるとストリーミングでの利用が躓くようになります。 `-r` オプションを利用することで回避できますが、すべて生成されてから表示されるのでユーザービリティは悪くなります。

## まとめ(GPT-4)
`aski` は、エンジニアがChatGPTを効率的に利用できるように開発されたCLIクライアントです。プロジェクトに対してコードや技術文章を簡単に取り込む機能、プロファイルを使って会話のコンテキストを切り替える機能、そして簡単な使用感が特徴です。この記事では、`aski` の主な機能の紹介や使用例、インストール方法、そして現状の制限について解説しています。 `aski` を活用することで、ChatGPTを使ったコードレビューやバグの特定、その他の開発タスクを効率化することができます。

## まとめ(人間)
この記事もところどころGPTにレビューしてもらいましたが、割と「たしかに、気がつかなかったなぁ」と思うことも多く、AI技術の進化にはかなり驚かされます。タイトルもGPT-4につけてもらいました。Googleなどの検索エンジンとは違い、コンソール上でも情報の検索がしやすく、GPT-3.5でも用途を絞れば割と扱いやすいように感じました。

`aski` をよろしくお願いします！気に入ったらGitHubのStarをぜひ押してください🙏