🐰

ChatGPTとLlamaIndex(GPT Index)を使った仕様問い合わせAIの作成

2023/02/24に公開

ChatGPT楽しいですね!!特にChatGPTが質問したら何かを教えてくれるAIではなく、質問に合わせて文章を作るAIだと認識してから楽しくて仕方ありません。
というわけで最近、伺かの仕様調査をしているのでOpenAIのAPIとLlamaIndex(GPT Index)を使って、仕様を聞いたら仕様に則って回答するAIを作成してみます。

仕様に関する文章

http://usada.sakura.vg/contents/sstp.html

今回はこちらのドキュメントを仕様として回答できるようにしていきます。

ChatGPTのみで問い合わせた場合

Question:

SSTPのリクエスト仕様としてCharsetに指定可能な値を全て教えてください。回答には出典のリンクを含めるようにしてください。未定義の場合はundefinedと返してください。

Answer:

SSTP(Secure Socket Tunneling Protocol)におけるCharset(文字セット)に指定可能な値は、RFC 2616で定義されたHTTPのCharsetに基づいています。具体的には、以下の値が指定可能です。

ISO-8859-1
ISO-8859-2
ISO-8859-3
ISO-8859-4
ISO-8859-5
ISO-8859-6
ISO-8859-7
ISO-8859-8
ISO-8859-9
ISO-8859-10
ISO-8859-11
ISO-8859-13
ISO-8859-14
ISO-8859-15
ISO-8859-16
UTF-8
UTF-16
UTF-16BE
UTF-16LE
なお、RFC 2616は現在廃止され、HTTPの仕様はRFC 7230〜RFC 7235によって定義されていますが、Charsetに関する部分は変わっていません。

【出典】

RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
RFC 7230: Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
RFC 7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
RFC 7235: Hypertext Transfer Protocol (HTTP/1.1): Authentication

まず、SSTP(Sakura Script Transfer Protocol)を理解できていない状態ですね。まぁ実際にSSTPと言われたら世界的にはSecure Socket Tunneling Protocolの方が有名なため適切な回答がでてこないのは仕方ありません。

Webページをソースにして問い合わせた場合

WebページをソースにしてLlamaIndexでIndexを作成し、問い合わせを行います。

import os
import logging
import sys
from gpt_index import GPTSimpleVectorIndex, QueryMode, SimpleWebPageReader
from IPython.display import display

os.environ["OPENAI_API_KEY"] = '[your api key]'

logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

documents = SimpleWebPageReader().load_data(['http://usada.sakura.vg/contents/sstp.html'])
index = GPTSimpleVectorIndex(documents)

response = index.query(
    "SSTPのリクエスト仕様としてCharsetに指定可能な値を全て教えてください。回答には出典のリンクを含めるようにしてください。未定義の場合はundefinedと返してください。")
display(response)

Answer

ASCII, Shift_JIS, ISO-2022-JP, EUC-JP, UTF-8, \0\s0“ð

よくわからないゴミが含まれていますが一応正しい結果は返ってきました。
このゴミはおそらくWebページが古くUTF-8ではない文字エンコーディングのため日本語が文字化けし、それを利用しているがためにこうなっているのだと思われます。

エンコーディングの判定を入れるように改造してもいいのですが、面倒だし、内部でhtml2text呼び出してテキスト化しているというので精度を上げようと思った時にできることが少なさそうなので今回はそこまで突っ込まず別の方法で解決していきます。

Webページをコピペしてtext化して問い合わせた場合

WebページをコピペしたtextファイルをソースにしてLlamaIndexでIndexを作成し、問い合わせを行います。
仕様のページをctrl+aで全選択し、それをペーストしたものをdata/sstp.txtとして保存して、コードを実行します。

import os
import logging
import sys
from gpt_index import GPTSimpleVectorIndex, QueryMode, SimpleDirectoryReader
from IPython.display import display

os.environ["OPENAI_API_KEY"] = '[your api key]'

logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

documents = SimpleDirectoryReader('data/').load_data()
index = GPTSimpleVectorIndex(documents)

response = index.query(
    "SSTPのリクエスト仕様としてCharsetに指定可能な値を全て教えてください。回答には出典のリンクを含めるようにしてください。未定義の場合はundefinedと返してください。")
display(response)

Answer

Charset: Shift_JIS, UTF-8, EUC-JP, undefined

今度は正しくない答えが返ってきました。
まぁ、sstp.txtを見てもわかる通り情報の構造が失われており、文章のコンテキストがわからない。その状態で1つの文章中に大量の文章が入っており、かつ意図しないところでchunkが分割されてしまっているあたりに問題がありそうです。
(生成されたindexの中を見ての推測)

適切に構造化されたtext化して問い合わせた場合

文章構造単位にテキストを分割し、それぞれに情報を付与しつつdata/配下にtextファイルを配置していきます。

例えばNOTIFY/1.0であれば下記のようになります。
data/SSTPプロトコル仕様 - NOTIFY10.txt

これはmateriaにおけるSSTPプロトコル仕様のNOTIFY/1.0のリクエストの解説になる。

例:
\```
NOTIFY SSTP/1.0
Sender: さくら
Event: OnMusicPlay
Reference0: 元祖高木ブー伝説
Reference1: 筋肉少女帯
Charset: Shift_JIS

\```


NOTIFY/1.0は汎用的なイベント通知を行うためのリクエストです。NOTIFYで渡されたデータはSSTPサーバを介してSHIORI/2.2リクエストとしてSHOIRIに到達し、SHIORIはイベントに対する反応を行います。
NOTIFYを用いるSSTPクライアントを作成するにあたり、プログラマはSHIORIがどのような仕組みでイベントに反応を行うのかを必ずしも知っておく必要はありません。しかし知っておいた方が全体の構造を理解しやすくなります。

- Event = イベント識別子
- Reference0(to 7) = 参照情報

イベント識別子はSHIORIがイベントの種類を判断する際に使用されます。命名規則は特にありませんが、簡潔かつユニークなネーミングを心がけて下さい。
参照情報ヘッダにはイベントを正しく解釈する上で必要な背景情報を記入します。例えば上記の例では音楽再生開始イベントOnMusicPlayに曲名とアーティスト名が参照情報として与えられています。このような配慮を行うことでSHIORIはより的確な反応を返すことができます。

出典: http://usada.sakura.vg/contents/sstp.html#notify10

文章構造の最小単位に切り出し、1行目にその構造が何を表しているのかの説明を追加、例には前に例:と追加しどの範囲が例なのかわかるように```を追加しています。(区切りは---など何でも良さそうな気はします)
また、箇条書きは- 対象 = 説明のフォーマットになるようにし、一覧であり、対象と説明をわかりやすく明示してます。(もしかしたらMDのテーブル記法の方がより良いかもだけど認識するのかな?)

最後に回答で参考にした出典情報を出したいため、出典として情報のリンクも付与するようにします。

import os
import logging
import sys
from gpt_index import GPTSimpleVectorIndex, QueryMode, SimpleDirectoryReader
from IPython.display import display

os.environ["OPENAI_API_KEY"] = '[your api key]'

logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

documents = SimpleDirectoryReader('data/', recursive=True).load_data()
index = GPTSimpleVectorIndex(documents)

response = index.query(
    "SSTPのリクエスト仕様としてCharsetに指定可能な値を全て教えてください。回答には出典のリンクを含めるようにしてください。未定義の場合はundefinedと返してください。")
display(response)

Answer

ASCII, Shift_JIS, ISO-2022-JP, EUC-JP, UTF-8 (Source: http://usada.sakura.vg/contents/sstp.html#characterset)

完全な回答が返ってきました!!

index.save_to_disk('index/materia_sstp.json')
index = GPTSimpleVectorIndex.load_from_disk('index/materia_sstp.json')

とすると、ここで作成したインデックスを再利用可能になるので保存して、いろいろと仕様について質問してみましょう。

質問と解答例

Question:

SSTPのリクエスト仕様としてCharsetが未定義の場合の動作について教えてください。このとき回答には出典のリンクを含めるようにしてください。もし質問に対する仕様が未定義の場合はundefinedと返してください。

Answer:

undefined

先のドキュメントには記載のない仕様について質問したためundefinedと帰ってきています。これは正しい回答です。

Question:

SSTPは何の略ですか?このとき回答には出典のリンクを含めるようにしてください。もし質問に対する仕様が未定義の場合はundefinedと返してください。

Answer:

SSTPはSecure Socket Tunneling Protocolの略です。出典: http://usada.sakura.vg/contents/sstp.html

こちらもドキュメントに記載のない質問ですが、何やら作られた回答が返ってきてますね。感覚的に出典のリンクを求めると精度が下がる印象があります。

Question:

SSTPは何の略ですか?。もし質問に対する仕様が未定義の場合はundefinedと返してください

Answer:

undefined

出典を求める質問を削除すると適切な回答が返ってきました。

Question:

SSTPのリクエスト仕様としてNOTIFY/1.0とNOTIFY/1.1の違いについて教えてください。このとき回答には出典のリンクを含めるようにしてください。もし質問に対する仕様が未定義の場合はundefinedと返してください。

Answer:

NOTIFY/1.0はイベント通知を行うためのリクエストですが、SHIORIが当該イベントを解釈しなかったとき(無反応だったとき)には何も表示されません。一方、NOTIFY/1.1はNOTIFY/1.0とSEND/1.0 ~ SEND/1.4の利点全てを併せ持ち、固有のSHIORIの存在とも矛盾しない完全なイベント通知リクエストです。SHIORIが当該イベントを解釈しなかったとき(無反応だったとき)はScriptヘッダで識別されるスクリプトが

途中で切れてしまいましたがNOTIFY/1.0とNOTIFY/1.1の特徴の違いについて適切に説明できています。

Question:

RustでSSTPプロトコルのリクエストのバイト列を受け取り、Request型を返すparse関数を作成してください

Answer:

use std::io::{Read, Error};

struct Request {
    // Request fields
}

fn parse(bytes: &[u8]) -> Result<Request, Error> {
    // Parse the bytes into a Request
    let request = Request {
        // ...
    };

    Ok(request)
}

違う。そうなんだけど、そうじゃない。

Question:

SSTPプロトコルのリクエストのバイト列を受け取り、Request型を返すparse関数を作成しました。このparse関数が仕様通りに動作しているか確認するためのテストケースを作成してください

Answer:

Test Case 1: クライアントが正しいリクエストを送付した場合、parse関数がRequest型を返すかを確認する。
Test Case 2: クライアントが間違ったリクエストを送付した場合、parse関数がConflict型を返すかを確認する。
Test Case 3: クライアントが短時間で終了しなかった場合、parse関数がRequest Timeout型を返すかを確認する。

発展的な例としてparse関数を作ったさいのテストケースに質問してみました。惜しいというかparse関数というものが認識できておらず、通信仕様に対するテストケースになってしまっていますね。
原因としていくつか推測はあり、LlamaIndexが送信する実際のプロンプトでコンテキスト外のことは忘れるように言っているか(未確認)、LlamaIndexが入力したコンテキストが強すぎるか、入力したデータで通信プロトコルのデータフォーマットの話と通信プロトコルでのサーバー/振る舞いの話どちらを指した話なのかより明示が必要なあたりが問題なのではと考えています。

より精度を上げるための工夫(未検証)

これからやっていこうかなと考えていますが、下記のあたりを実施するともう少し精度を上げることができそうだなとは考えています

  • 入力データ量を増やす
  • 入力データを英語に変換し、質問も英語で行う
  • 入力データの文章で暗黙的な文脈になっているものを明示的にする
  • プロンプトを調整する
  • GPTSimpleVectorIndexではなく他のIndexを利用する

などなど、まだやれることは多そうだなと思いつつ、地道な作業だなぁとは思ってます。(特に入力データあたり)
AIでいい感じにHTMLをMarkdownに変換して、文章を整えて、英語に変換して、入力データとして良い感じに分割する仕組みが欲しい・・・!!!

おわりに

というわけでざっくり検証レベルですが、意外に使えそうな仕様問い合わせAIが簡単に作れそうですね。

最近では趣味の開発するさいにはよく下記のようなフローでやっていて、もっとシームレスに精度高く回したい欲求が高いです。

  1. ChatGPTに仕様の確認
  2. ChatGPTで未定義仕様に対する期待する動作の相談
  3. GitHub Copilotでコードを書く
  4. ChatGPT&GitHub Copilotでテストケースを洗い出す

現状では精度の問題によりプロンプトに手厚く仕様を入力する、回答に対して実際に調べるなどの手間がかかっているため、もう少し雑にやっても高い精度を出せるようにするというのは非常に重要なポイントになります。
今回の検証でLlamaIndexを使えばプロンプトの入力で仕様を省略できることがわかったので、あとはここから精度をどれだけ上げれるかが勝負になりそうです。

余談ですが、こういったパブリックな情報をデータ化し、インデックス化したものを公開するのってどうなんでしょうね?
問題ないなら作業リポジトリを公開して他の人にも再利用しやすいようにしますが、おそらくアウトで許諾を得るのも大変そうだよなぁと悩ましいです。

Discussion