最近ChatGPT周りを見ていて、自分のブログをChatGPTに繋いでブログが言いそうな回答を出してもらうという記事に興味を持った。
- 自分のScrapboxをChatGPTにつないだ - 西尾泰和のScrapbox
- 自分のはてなブログをChat GPTにつないだ - hitode909の日記
- ChatWP: WordPressをAI化しておしゃべりする
しかし、その仕組みが分からなかったため、自分で実際に動かしながら内容を理解してみたい。
ブログを読んだときに感じた疑問点
なぜembeddings APIを使って数値ベクトルを使うことで、そのブログが答えそうな回答を得ることができるのか。数値をプロンプトに入れても意味はなさそうだが、どのようにしているのか?
まずは動かしてみる
自分もはてなブログを使っているので、https://blog.sushi.money/entry/2023/03/10/190000を参考に動かしてみた。手順としては
- はてなブログをMTエクスポートする
- https://github.com/hitode909/scrapbox_chatgpt_connector/commit/23f3815fa53cfc9c78a286d61bcc7379bbc0d4b8 のスクリプトを使い、make_index.pyに与えられるjsonファイルを作る
ruby mt2json.rb ~/Downloads/blog.shibayu36.org.export.txt > from_hatenablog/blog.shibayu36.org.json
- https://github.com/nishio/scrapbox_chatgpt_connector/blob/82a96d38613495df3f0125428d291fcb2357a84f/make_index.py#L14-L16 のJSON_FILEやINDEX_FILEを適宜書き換え、
python make_index.py
を実行JSON_FILE = "from_hatenablog/blog.shibayu36.org.json"
INDEX_FILE = "blog.shibayu36.org.pickle"
- https://github.com/nishio/scrapbox_chatgpt_connector/blob/82a96d38613495df3f0125428d291fcb2357a84f/ask.py#L9 あたりのINDEX_FILEを書き換えて
python ask.py
を実行
この状態で実際に「コードレビューにおいて、レビュワーは何に気をつければ良いでしょうか?」という質問を投げかけてみると、次のような回答が返ってきた。確かに自分が言っていそうな雰囲気が出ている。
レビュワーは、コード自体を指摘することに集中し、人を責めるのではなく振る舞いを指摘することが重要である。また、レビューによって自分が関わったコード以外も少しずつ詳しくなっていくことがあるため、自分自身も学びながらレビューを行うことができる。さらに、レビューの目的として誤りの発見だけでなく、技術力の向上や属人性の排除といった教育的側面も重要であるため、レビューを通じてチーム全体の技術力向上にも貢献することができる。
ref. [Team Geek読んだ] [コードコンプリートを再読した] [レビュータイムの導入・消滅・再導入] [コードレビューを段階的に行ってもらう話] [CROSS 2014のぶつかり稽古にパネラーとして参加します] [今のコードレビューのやり方] [いつ突然会社をやめても問題ないという基準でコードやドキュメントを書く]
仕組みを理解する
make_index.pyで何を保存しているか理解する
まずはブログ記事をindex化する部分について調べてみる。これは本文をいくつかのブロックに分けて、それぞれembeddings APIで数値ベクトル変換し、それを参考情報とともにpickleファイルに保存するということをやっている。
保存しているのはこういうデータ。
[ "PrePANでcarton 1.0しようとしていたんだけど、手元ではcarton install出来るのに、サーバ側でcarton install --deploymentしようとしたらうまくいかないという状態になってしまった。いろいろ調べていたら原因が分かったのでメモ。 結果としてcarton側が悪いのではなくて、こちら側の使い方が悪かった。手元ではperl 5.16.3を使っていたが、サーバ側は5.10.1になってしまっていたのが原因だった。 この時、例えばJSON::PPは5.16.3ではcore moduleだが、5.10.1ではcore moduleではなく、cpanfile.snapshotの情報に齟齬が出てしまう。そのため、サーバ側では正しくインストールできなかった。 試しにPrePANのrepositoryで、それぞれのversionでcarton checkしてみると以下のようになる。 $ plenv shell 5.16.3 $ carton check cpanfile's dependencies are satisfied. $ plenv shell 5.10.1 $ carton check Following dependencies are not satisfied. CPAN::Meta is not installed. Needs 2.110420 Perl::OSType is not installed. Needs 1 Parse::CPAN::Meta has version 1.39. Needs 1.4401 version has version 0.77. Needs 0.87 parent has version 0.221. Needs 0.223 Test::More has version 0.92. Needs 0.98 Test::Simple has version 0.92. Needs 0.98 IO::Uncompress::Bunzip2 has version 2.020. Needs 2.021 ExtUtils::CBuilder has version 0.2602. Needs 0.27 CPAN::Meta::YAML is not installed. Needs 0.003 CPAN::Meta::Prereqs is not installed. Needs 0": [ [ -0.008785112760961056, -0.0012413746444508433, 0.01069492008537054, -0.01447286643087864, -0.026278946548700333, 0.013354760594666004, -0.012792236171662807, ... ], "手元とserverでのperlのversion違いで、carton installでハマった" ], ... ]
つまり、ベクトル化した本文と、embeddings APIから返ってきた数値ベクトルと、記事タイトルを保存するようにしている。
ask.pyでどのように自分のブログっぽさを出すのか
ask.pyでブログっぽさを出すためには、次のような流れで実装している。
- 質問文をembeddings APIに投げて、数値ベクトルをもらう
- インデックスファイル内の全データに対し内積を取ることで、類似する本文データをいくつか抽出する
- 類似する本文データをChatGPTのプロンプトの先頭につけ、それを考慮して回答するように命令する
実際のプロンプトはこういう感じ。このtextの部分にトークン数が許す限り類似する本文データをいくつか与え、それを読んだ上で回答するように命令している。
Read the following text and answer the question. Your reply should be shorter than 250 characters. ## Text {text} ## Question {input}
例えば最初に試してみた質問だと、自分のブログの以下の本文が利用されていた
USE: Team Geek読んだ 昔この人が書いたコードがだめだったし、今回も全然だめのように見えるから指摘しまくろう このコード読みにくくてなんでこんなことやってるかわからないけど、わからないんだからダメだ みた いな感じで潜在的に思ってしまっていて(そのような考えをいくら排除しようと思っても)、人を尊敬・信頼せずに決めつけてレビューした結果、自分もその人も怒ってしまうみたいなことがあったように感じる。この時もし、「何かよくわからないコードはあるけど、この人なりの考えがあるから質問してみよう」みたいに謙虚を持って、人を信頼してコードレビューをすればもう少し良いところに落ち着くかもしれない。 人ではなく振る舞い に指摘する 何か指摘する時はその人を責めるんでなく、振る舞いを指摘しようみたいな話。 コードレビューの時もレビュー自体はその人を非難しているのではなく、コード自体を指摘しているという感覚を持っていないと結構喧嘩になりがちなので、まあこういうことは前提として考えておく必要があるなあと思う。 USE: コードコンプリートを再読した コードレビューの二次的効果 以下の様なことが書いてあってなるほどと思った。 人は自分のコードがレビューされることを知ると、コードを入念にチェックするようになるという二次的な効果もある。 ペアプログラミングの有効利用 21章くらいにペアプログラミングを安易に使用しないって書いてあって面白かった。 最も複雑なコードにペアプログラミングを使っていたグループは、15分間ホワイトボ ードで詳細な設計を確認してから、1人でプログラミングを行う方法が合理的であることに気づいた。 と書かれていたり ペアプログラミングを試してみた組織のほとんどは、最終的に、作業全体ではなく作業の一部にペア プログラミングを使用する方向に落ち着く とか書かれている。 実際最近チームでも同じようなことを話していて、単なる作業だとペアプロはただ速度が落ちるだけだよねとか、逆にちょっとモチベーションが低いタスクのドライブにはなるよねとか、そういう話をしてた。 まとめ なんか印象に残ったところを適当に書きまくっていったら結構分かりにくい感じになった。コードコンプリートだいぶ昔の本なのに、今の開発にもいろんな方向で役立っていてすごいと思った。 コードコンプリートすごい良い本で読むべきと思ってるけど、とにかく分厚いので躊躇されがち。コードレビュー - hitode909の日記でも言及されていたけど、とりあえずリーダブルコード 読んだほうが手っ取り早いとは思う。リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)作者:Dustin Boswell,Trevor FoucherオライリージャパンAmazon USE: レビュータイムの導入・消滅・再導入 レビュワーのコードの詳しさのばらつきによるレビュー精度の低下みたいなのは懸念していたが、特に問題になるほど精度が落ちることはなかった レビューによって自分が関わ ったコード以外も少しずつ詳しくなっていった という状況になった。問題の解決としてはそこそこうまく機能したかなと思う。 レビュータイムの消滅 導入後数ヶ月くらいして面白いことになった。それはレビュータイム 以外の時間で、チームメンバーで少し手が空いた人が積極的にレビューを行うという状態になった。また反対に昼休み終わったあとにレビューをするということをそんなにしなくなっていった。それでもレビューが誰かに偏るということや、レビューが溜まり続けるということは無かった。 僕自身は導入しようと思った時にこういう状況になるとはあまり思ってなかった。けれど実際にはこのような状況になった。 なんとなくこれをみて「仕組み」っていうのはうまく回ると、チームの「文化」になって、「仕組み」自体の役割を終えることもあるのだなと感じた。非常に興味深い。 こうしてレビュータイムは消滅した。 レビュータイムの再導入 その後また数ヶ 月くらいたって、今度は新しい人がチームにジョインした。そうするとまた新しい人がレビューをためらうという問題が発生した。 このためらいをなくすようにするため、もう一度レビュータイム導入することになった。 その時はまだ新しい人はレビューをすること自体に慣れていなかったので、昔のレビュータイムの仕組みにちょっとだけルールを追加してやってみた。 USE: コードレビューを段階的に行ってもらう話 コードレビューの目的としては誤りの発見と同様に、技術力の向上や属人性の排除といった教育的側面も重要である。 コードレビューで課題に思っていたこと 自分のチームでは基本的に一人がコードレビューをして、OKだったらmergeをして良いという運用だった。ただし、チームにずっといる人(ベテラン)とチームに新しく入った人(新人)が混ざっている場合に以下のような課題があった レビューするコードへの知識がないと、誤りを発見することが難しい 誤りの発見が出来ないのでは、と感じることで新人がコードレビューを躊躇する 結果としてベテランがコードレビューをし続け、負担が大きい またコード レビューによる教育的側面があまり満たされないため、その後もベテランがコードレビューし続ける コードレビューを段階的に行ってもらう 上のような課題の解決として、PullRequestを投げた人以外の全メンバーがコー ドレビューをするということも考えられる。ただ、それでは教育的な側面を担保するためにコストが多すぎる。 そこで、教育的側面を満たしながら、誤りの発見をおろそかにしないように、新人にはコードレビューを段階 的に行ってもらうという解決方法を取ることにした。 USE: CROSS 2014のぶつかり稽古にパネラーとして参加します URL はてなでのペアプロの話 はてなでのコードレビューで気をつけていること コミュニケーションしながら開発するための便利ツール USE: 今のコードレビューのやり方 コードレビュー - hitode909の日記 この話。 大体こういう風なやり方をしてる。Diffだけを見るんじゃなくて手元でもコードチェックアウトしてて、それとDiffを行ったり来たりしながらレビューすることが多い。こういう方法のほうがかっこいいよねみたいなのは書くけどその通りにしてもらうことを強制はしないというのも賛同。 逆にcommitの中身*1はあえて見ないようにしてる。僕はレビューする時 に、そのコードについての学習が行われていない状態で見たほうが良いなと思っている。commitの中身を1つずつ見ていくと学習が進んでしまって、少しわかりづらいコードもあっても、学習が進んでいるせいで無意識に目からすり抜けてしまうという経験があったので、あえてcommitみないということをしてる。 例外もあって、少し大きめなリファクタリングが行われた時はcommitを1つずつ読んでいくことが多い。少し大きめだとDiffだけ だとかなり変わるので何が起こっているか分かりにくいけど、そういう場合は1commitがメソッド移動リファクタリングとか、クラスへの抽出リファクタリングとか、そういう1単位になっていることが多くて、こういう場合はcommitで見たほうが見やすい。 リファクタリングの1単位は結構この本に載っている通りの単位になっていることが多い。なので軽く流し読むくらいならおすすめ。詳細まで読み込む必要はない。でも今は売ってなくて悲しい。リファクタリング―プログラムの体質改善テクニック (Object Technology Series)作者:マーチン ファウラーピアソンエデュケーションAmazon コードレビューのやり方はbestなやり方はなくてチームによってケース バイケースだと思うけど、betterな方法が知りたい。けど今のところよく分からない。何か知見があったら共有されたい。 USE: いつ突然会社をやめても問題ないという基準でコードやドキュメントを書く コードやドキュメントを書く時に、どのくらいきれいにしておくかとか、どのくらいわかりやすくしておくかとかを考えることがある。こんなとき僕は、いつ突然自分が会社をやめて連絡がつかなくなったとしても他の人がある程度理解できるか、を基準にしている。そのためにはあまりいい方法が思いつかなくて仕方なく書いている部分にはちゃんと経緯のコメントを書く。他にも例えば作ったサービスであるイベントを開催する方法のドキュメントを書くなら、全く何もやったことがない人がそのドキュメントを読んだらとりあえず開催できるよう、ドキュメントを書く。当然コードもかっこよさよりも、説明しなくても分かりやすくなるようなシンプルさを追求する。 また、このような基準を満たせるように、PullRequestを送るときは必ず一回頭を空にして、自分で自分のPullRequestをレビューし てみて、本当に何も知らない人が読んでも分かるのか、というところを確認するようにしている。ただし、この「一回頭を空にして」というのが難しいというのはある。 もちろん、こういうことをするだけで終わってはい けない。これはつまるところ属人性の問題であり、属人性を排除するためには知識を伝搬させる仕組みを考えるとか、ペアプロをするとか、いろいろな方法が考えられる。しかし、とりあえず個人で属人性に対して対処する方法として、このような気持ちでコードやドキュメントを書いている。
まとめ
今回は「なぜembeddings APIを使って数値ベクトルを使うことで、そのブログが答えそうな回答を得ることができるのか」という疑問が湧いたので、その仕組みについて調べてみた。
最初に出した疑問の「なぜembeddings APIを使って数値ベクトルを使うことで、そのブログが答えそうな回答を得ることができるのか。数値をプロンプトに入れても意味はなさそうだが、どのようにしているのか?」に答える形でまとめると以下の結論になった。
- 数値ベクトルはあくまで、内積を使って質問文から本文の類似度が高いものをピックアップするためだけに利用している
- ピックアップした本文情報をプロンプトに埋め込んでいる。つまりプロンプトに自然言語のみを使っていることは変わらない
おまけ: embeddings APIを使うときの価格をざっくり把握する
ブログ記事が1000記事あったので、非常に高くなったら怖いなということで以下のようなスクリプトで事前にざっくり価格を把握した。
import sys import tiktoken import re import os enc = tiktoken.get_encoding("cl100k_base") tokens_count = 0 for root, dirs, files in os.walk("/path/to/myblogsync/entry"): for file in files: # ファイルのパスを取得 filepath = os.path.join(root, file) # ファイルの内容を取得 with open(filepath, encoding="utf-8") as f: text = f.read() tokens = enc.encode(text) tokens_count += len(tokens) print(tokens_count)
このコードは大体GitHub Copilotに書いてもらえて便利だった。