Featured image of post LangChainの可能性2:RetrievalQAを使ったPDF文献について質疑応答するChat Promptの試作

LangChainの可能性2:RetrievalQAを使ったPDF文献について質疑応答するChat Promptの試作

前回はPDFドキュメントをvector index化して自然言語で検索するアプリを検討してみました。

最近、ChatGPTを含むLLM(Large Language Model)が注目を集めていますね。 これらのモデルは、人間に近い精度を実現し、自然言語によるコ
LangChainの可能性:LLMを特定の情報ソースと組み合わせて利用するアプリの模索

今回は単にドキュメントを検索するだけでなく、ChatGPTのように自然言語による会話で回答してくれるアプリを作ってみたいと思います。

PDF文献について自然言語で質疑応答させる

LangChainにはまさにこのようなユースケースに利用できるChainが例として掲載されています。

Retrieval Question/Answering — 🦜🔗 LangChain 0.0.123

基本はこちらの例を利用してやってみます。

環境の準備

Google Colabで必要なパッケージをインストールします。

1
2
3
4
!pip install langchain
!pip install openai
!pip install faiss-gpu
!pip install pypdf

続いてOpenAIのAPI keyをセットします。

1
2
import os
os.environ["OPENAI_API_KEY"] = "xxx"

ドキュメントの準備

今回もPDFドキュメントとして、azu (azu) · GitHubさんがCC BY NNで公開されているJavascript Promiseの薄い本を利用させていただきます。

JavaScript Promiseの本. Contribute to azu/promises-book development by creating an account on GitHub.
GitHub - azu/promises-book: JavaScript Promiseの本

ここまではほぼ前回と同じです。

1
2
3
4
5
6
7
8
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.document_loaders import PyPDFLoader
from langchain.vectorstores.faiss import FAISS

loader = PyPDFLoader("/content/javascript-promise-book.pdf")
pages = loader.load_and_split()

docsearch = FAISS.from_documents(pages, OpenAIEmbeddings())

アプリの準備とテスト

まず必要なものをインポートします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from langchain.chains import RetrievalQA

from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    AIMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage
)

あとは例に従ってプロンプトを構築していきます。 公式サイトの例では Chroma を利用していましたが、ここでは前回と同じくFAISSをRetrieverとして利用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
system_template="""
以下のコンテキストを使って質問に回答してください。
もし答えがわからない場合は、答えを作らずにただわからないと回答してください。
----------------
{context}"""
messages = [
    SystemMessagePromptTemplate.from_template(system_template),
    HumanMessagePromptTemplate.from_template("{question}")
]
prompt = ChatPromptTemplate.from_messages(messages)

chain_type_kwargs = {"prompt": prompt}
qa = RetrievalQA.from_chain_type(llm=ChatOpenAI(), chain_type="stuff", retriever=docsearch.as_retriever(), chain_type_kwargs=chain_type_kwargs)

構築したChainに質問してみます。

1
2
query = "Promiseとは何ですか?"
qa.run(query)

Promiseとは、非同期処理を抽象化したオブジェクトとそれを操作する仕組みのことを指します。 JavaScriptにおけるPromiseは、ES6の仕様で定義されたAPIであり、Promiseコンストラクタ関数であるPromiseからインスタンスとなるpromiseオブジェクトを作成して利用します。 Promiseは、コールバックや即時関数を置き換え、コードの可読性や再利用性を向上させることができます。 また、Promiseを利用することで、非同期処理を簡単にパターン化することができます。

内容はよさそうです。ソースのドキュメントに照らしてみます。

Chapter.1に以下のような記述がありますが、他の説明についてはそれっぽい記述はないためOpenAIのAPIによって生成されていそうです。

Promiseは非同期処理を抽象化したオブジェクトとそれを操作する仕組みの事をいいます。

様々な質疑を試してみる

ドキュメントに書かれている内容について聞いてみる

1
qa.run("Promise.all()を使うと何ができますか?")

Promise.all()を使用すると、渡された複数のPromiseオブジェクトを並列に実行し、すべてのPromiseがFulfilledまたはRejectedになるまで待機し、その後次の処理を実行します。 次の処理には、Promise.all()に渡した順序でresolveされた値の配列が渡されます。 これにより、複数の非同期処理を並行実行し、そのすべての結果を同時に受信できます。

内容は正しそうですが、これといった記述は見つけられませんでした。

1
qa.run("Promise/A+とは何ですか?")

Promises/A+とは、Promiseのthenについて取り決めたコミュニティベースの仕様で、ES6 Promisesの前身となったものです。 Promiseのライブラリを探す際には、Promises/A+互換という言葉がよく使われます。 Promises/A+互換と書かれている場合は、多くの場合Promise.allやcatchなどと同様の機能が実装されていることが期待されます。 しかし、Promises/A+はPromise#thenについてのみの仕様となっているため、他の機能は実装されていても名前が異なる場合があります。

こちらについては以下の内容がありました。

p.116の記述。

ES6 Promisesの前身となったコミュニティベースの仕様で あり、ES6 Promisesとは多くの部分が共通している。

p.60の記述。

Promises/A+互換と書かれていた場合はについての動作は互換性があり、多くの場合はそれに加えてPromise.allや等と同様の機能が実装されています。 しかし、Promises/A+はPromise#thenについてのみの仕様となっているため、他の機能は実装されていても名前が異なる場合があります。

かなり内容的な一致が見られました。

書かれていない内容を聞いてみる

1
qa.run("hogeは何を返しますか?")

コード中に"hoge"という変数や関数などの定義はありませんので、何を返すかはわかりません。ただ存在しないものを返すわけではありません。

ちょっと質問がいじわるすぎたかもしれませんが、わからないという回答は出力されました。

1
qa.run("Async Functionを使うと何ができますか?")

Async Functionを使うと、非同期処理をシンプルに扱えるようになります。 Async FunctionはPromiseを返しますが、内部でawaitを使ってプログラムの流れを待機させることができるため、コールバック関数を使う場合よりも可読性が高く、コードの書き方が簡単になります。 また、Async Function自身が非同期処理であるため、thenやcatchを使う必要がなく、より直感的なコードを書くことができます。

自分が利用している版はかなり古いため、Async Functionについての記述はありません。 しかし、ちょっと類似した内容として以下のような説明をしている部分があります。

1
2
3
4
5
6
function asyncFunction() {
     return new Promise(function (resolve, reject) {
         setTimeout(function () {
             resolve('Async Hello world');
         }, 16);
     }); }

asyncFunction という関数 は promiseオブジェクトを返していて、 そのpromise オブジェクトに対して then でresolveした時のコールバックを、 catch でエ ラーとなった場合のコールバックを設定しています。

しかしこれらは参照されず、async/awaitに対する返答が出力されたようです。

サンプルコードを出してもらう

1
qa.run("Promise.raceを使った例を提示して頂けますか?")

Markdown形式でサンプルコードを提示してくれました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
はい、以下にPromise.raceを使用した例を示します。

```javascript
// `delay`ミリ秒後にresolveする
function timerPromisefy(delay) {
    return new Promise(function(resolve) {
        setTimeout(function() {
            resolve(delay);
        }, delay);
    });
}

// 一つでもresolveまたはrejectした時点で終了
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function(value) {
    console.log(value); // => 1
});
```

上記のコードでは、4つのPromiseオブジェクトの中で一番早くFulfilled状態になったものが後続の処理に渡されます。上記の例では、1ミリ秒後にFulfilled状態になるPromiseオブジェクトが一番早くFulfilledになるため、コンソールには`1`と出力されます。

これはp.42に掲載されている「Promise.race」セクションのサンプルコードそのままでした。

ドキュメントのページを聞いてみる

1
qa.run("Promise.raceは何ページに書いてありますか?")

Promise.raceの説明は、JavaScript Promiseの本の43ページに書かれています。

p.42から43にかけて「Promise.race」セクションが記述されているので正しいです。

1
qa.run("Thenableについては何ページに書いてありますか?")

JavaScript Promiseの本のコンテキストによって、Thenableについて言及しているページは複数あります。以下のページでThenableについて解説しています。

  • 第4章のPromise.resolveとThenable
  • 第18ページの「JavaScript Promiseの本」の説明
  • 第70ページのThenableを利用することで、中間的な実装をする方法

以上のページで解説されています。

Chapter.4 Advanced以下に「Promise.resolveとThenable」という節があります。

p.18はPromise.resolveについての説明をしている箇所ですが、以下のような記述がちょうどあるところです。

ThenableについてはPromiseを使ったライブラリを書くとき等には知っておくべきで すが、 通常の利用だとそこまで使う機会がないものかもしれません。

p.70はChapter.4 Advanced以下「Promise.resolveとThenable」セクションの一節の指摘です。

指摘としては全部間違ってはいないようです。「Promise.resolveとThenable」の節を出力できているのは悪くない精度だと感じます。

回答のソースを求めて確認する

回答を見ていると、ChatGPTと会話をしているのと変わらないくらいの自然さで、参照している文献の内容について回答を得られていると感じました。 ただ必ずしも文献を当たって回答しているだけというわけではないようなところもあり、本当に文献から引かれているかわからないケースも見られました。

このようなケースでは、どこを参照して回答を提示したのかを質問することで妥当性の検証もできます。 ChatGPTでは既に存在しないWebページを提示してきたり、関係が薄いものを提示することがありますが、 LangChainを使ったアプリでは参照文献を提示しているため、このような質問で正確性を担保することもできそうです。

例えば、先程のAsync Functionについて確認してみます。

1
qa.run("Async Functionについての説明は何ページに書いてありますか")

このテキスト中には、Async Functionについての説明は見当たりません。

と記載がないことが確認できます。

今回のアプリでは会話のコンテキストを維持していないため、指示語による過去の会話内容を示しての質問はできないですが、 LangChainの機能として会話履歴を維持させることも可能なようです。

まとめ

LangChainを使って、特定のPDF文献をソースとして自然言語による会話で質疑応答ができるアプリを試作してみました。

文献をコンテキストとして共有していることで、文献を参照しての回答を指示するような質問や、ソースを要求することで情報の正当性も確認しやすくなったように感じます。

LangChainを利用することで、当初の目的であった効率的な学習という意味では、ChatGPTのような汎用言語モデルを直接使うよりもかなり使い易いアプリケーションを構築できそうです。

先日Github NextにおいてMDNなどの文献に対する質疑が可能な「Github Copilot for Doc」が発表されましたが、現在は特定のDocに固定されているようです。

GitHub Next Project: How would it feel to have an expert on hand at all times? We built a tool that uses relevant information from a project’s documentation to answer questions or explain concepts.
GitHub Next | Copilot for Docs

LangChainであれば、様々な文献やWebサイトをマッシュアップした自分だけのCopilot for Docを作成することができます。 機能的にも十分な出力が期待できそうです。

これを拡張していけば、例えば会話の開始前にドキュメントを渡してコンテキストを固定する、というようなアプリケーションも作成できそうです。

Built with Hugo
テーマ StackJimmy によって設計されています。