LangChainの使い方 LlamaIndex編

# 1. はじめに

注: 初稿を書いたあとでLlamaIndexのAPI仕様が大きく変更されました。そのため、記載のソースコードや準備するデータの仕様に関する記述を`llama-index==0.6.8`に準拠したものに変更いたしました。

本記事は、下記の続編です。
前回紹介しきれなかった**外部データを利用した回答精度の向上など、さらなるLLM(大規模言語モデル)の利活用をLangChainなどで実現する手段を解説**していきます。

https://qiita.com/kzkymn/private/a72796c89ffc696034c8

# 2. LlamaIndexの使い方

LangChainを使って外部データをLLMに受け渡す方法のひとつとして、LlamaIndex(旧名称: GPT Index)を使う方法を紹介します。

**LlamaIndexとは、主に以下2点の機能を担うライブラリ**です。

1. **LLMに外部情報を受け渡すための構造化データを作成する**
1. **作成した構造化データを踏まえて質問に回答するようLLMに要求する処理を実現する**

https://github.com/jerryjliu/gpt_index

従ってLangChainを介さずにLlamaIndex単品を使うだけでも簡単な質問応答はできますので、まずはLlamaIndexの使い方から見ていくことにしましょう。

LlamaIndexはpipでインストール可能です。冒頭で述べた通り、今回は`llama-index==0.6.8`を利用します。

```sh
pip install llama-index==0.6.8
```

## 2.1. LlamaIndexで加工するデータの作り方

LlamaIndexで**構造化データに加工できるデータは基本、プレーンテキスト**です。テキストファイルを用意して所定のフォルダに置けばよいだけです。
以前は日本語テキストの書き方に癖がありましたが、`llama-index==0.6.8`の時点ではUTF-8エンコードにすること以外に特段気にするべきことはありません。

では、実際にLlamaIndexで加工するテキストを用意してみましょう。今回は、日本語版Wikipediaの[秀逸な記事](https://ja.wikipedia.org/wiki/Wikipedia:%E7%A7%80%E9%80%B8%E3%81%AA%E8%A8%98%E4%BA%8B)に含まれている、[「関門トンネル (山陽本線)」](https://ja.wikipedia.org/wiki/%E9%96%A2%E9%96%80%E3%83%88%E3%83%B3%E3%83%8D%E3%83%AB_(%E5%B1%B1%E9%99%BD%E6%9C%AC%E7%B7%9A))の冒頭を一部編集したものとします。

```text
 関門トンネル（かんもんトンネル）は、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。
 後に開通した国道2号の関門トンネル（関門国道トンネル）と区別するため、関門鉄道トンネル（かんもんてつどうトンネル）と呼ばれることもある。

 概要
 関門海峡は本州（山口県下関市）と九州（福岡県北九州市）の間にある海峡で、このうち深さの関係から西側の「大瀬戸」と呼ばれる部分に関門トンネルがある一方、もっとも海峡が狭くなる東側の「早鞆（はやとも）の瀬戸」に、ほかの関門海峡横断交通手段である国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋が通っている。もともとは関門連絡船でこの海峡を横断して結んでいたが、乗換・積替の手間を省き輸送力を増強するために3回にわたって関門海峡にトンネルを建設する計画が持ち上がり、3回目の昭和初期の計画により実際に着工することになった。
 当面は単線の輸送力で十分であったことに加えて、工事の容易さから、単線でトンネルを建設することになり、将来輸送量が増えたときにもう1本の単線トンネルを建設して複線とすることになった。先に建設されたのは下り線のトンネルで、両側の地上と水底部との間において機関車が列車を引っ張る（牽引する）性能を勘案して、20パーミル勾配を採用することにしたが、のちに上り線のトンネルを建設した際には、海底部分での土被りを増すために一部で25パーミル勾配が採用された。
 事前に潜水艇による調査やボーリング調査などを実施して地質を調べたうえで、まず地質の調査や周り込んで本線の掘削箇所を増やすことやセメントの注入による地盤改良を行うため、細い試掘坑道を建設することとなった。これは1937年（昭和12年）に着工し、1939年（昭和14年）4月19日に貫通、8月5日に完成した。まだ試掘坑道を建設中であった1937年（昭和12年）12月から下り線トンネルの掘削にも着手し、門司側からは日本では3番目というシールド工法も使用して建設が進められた。
 1942年（昭和17年）6月11日に最初の試運転列車が下り線トンネルを通過し、7月1日に貨物列車用に開通、11月15日に旅客列車用にも開通し、まずは単線での供用を開始した。上り線トンネルについては、1940年（昭和15年）に着工が決定され、1944年（昭和19年）8月8日に開通し、下り線から上り線に列車を移したうえで下り線トンネルの改修工事を行って、9月9日から複線での運転が開始された。
 第二次世界大戦中は船舶不足に陥る中、九州・本州間の連絡に重要な役割を果たした。1953年（昭和28年）6月28日には昭和28年西日本水害により水没し、復旧には2週間ほどを要した。当初から直流電化で開業した関門トンネルは、1960年代に入ると九州島内を交流電化する方針となったことから直流と交流の接続点ともなり、門司駅構内に交直デッドセクションが設けられ、そのための特徴的な車両が通過するようになった。1958年（昭和33年）から1975年（昭和50年）にかけて、関門海峡を渡る国道や高速道路、新幹線も開通したことで並行路線が実現された。
 1987年（昭和62年）の国鉄分割民営化に際しては、九州旅客鉄道（JR九州）が第一種鉄道事業者として施設を承継した。また、日本貨物鉄道（JR貨物）がトンネルを含む区間の第二種鉄道事業者として、貨物列車の運行を行っている。

 関門海峡地区の鉄道路線図
 関門海峡は、九州の北端の福岡県北九州市と、本州の西端の山口県下関市の間にあり、西の日本海・響灘と東の瀬戸内海・周防灘を結んでいる海峡である。東側の下関市壇ノ浦と北九州市門司区和布刈間が早鞆の瀬戸と呼ばれる幅約600メートル程度の海峡最狭部であり、また西側には彦島があって、彦島と九州の間は大瀬戸、彦島と本州の間は小瀬戸と呼ばれる。小瀬戸は昭和初期に埋立工事が行われ、閘門で締め切られて、彦島と本州はほとんど地続きとなっている。
 関門海峡を横断する橋やトンネルは、山陽本線（在来線）の関門トンネルのほかに、国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋があるが、在来線の関門トンネルのみ大瀬戸を通過しており、ほかの3経路はいずれも海峡がもっとも狭くなる早鞆の瀬戸を通過している。
 在来線の関門トンネルは、高架上の下関駅を出て本州から彦島へ渡ってトンネルに入り、弟子待（でしまつ）から大瀬戸の海底下をくぐって九州側の小森江に渡り、門司駅構内で地上に出る。在来線の関門トンネルが早鞆の瀬戸ではなく大瀬戸を通過することを選んだのは、早鞆の瀬戸の方が水深が深く、急勾配が許されない鉄道のトンネルでは全長が長くなってしまうことや、既存の鉄道との接続の関係からである。
 門司駅5・6番のりばの下関側に設置されている「関門トンネル案内板」（5番のりば側は下り線トンネル内部の写真）
 周辺の鉄道路線網は、本州側を山陽本線が通り、下関駅から関門トンネルをくぐって九州側の門司駅へとつながる。一方、九州側は鹿児島本線が門司港駅を起点とし、門司駅で山陽本線と合流して小倉駅へと通っている。門司港駅は当初門司駅という名前で、門司駅は当初大里駅（だいりえき）という名前であったが、当時、国際貿易港でもあった「門司」が全国的にも著名であったため、1942年4月に改称された。また、関門トンネルの九州側の接続部が大里駅構内となったため、その支障移転により大里駅は600メートルほど小倉方の現在地に移転している。大里駅構内にあった機関庫もこれに合わせて現在の北九州貨物ターミナル駅付近に機関区、客貨車区、操車場とともに整備され、移転した。
```

このテキストを選んだ理由は、**関門トンネルという言葉が非常に曖昧なため、外部データを受け渡さない限り間違いを含む回答をLLMがしてしまうと予想**したためです。

俗に関門トンネルと言われているものには、大きく以下の3種類[^1]があります。今回LLMに受け渡すテキストはこのうち①について説明したものです。
- **①[山陽本線(在来線)が通るトンネル](https://ja.wikipedia.org/wiki/%E9%96%A2%E9%96%80%E3%83%88%E3%83%B3%E3%83%8D%E3%83%AB_(%E5%B1%B1%E9%99%BD%E6%9C%AC%E7%B7%9A))**
- ②[新幹線が通る、新関門トンネル](https://ja.wikipedia.org/wiki/%E6%96%B0%E9%96%A2%E9%96%80%E3%83%88%E3%83%B3%E3%83%8D%E3%83%AB)
- ③[国道２号のトンネル](https://ja.wikipedia.org/wiki/%E9%96%A2%E9%96%80%E3%83%88%E3%83%B3%E3%83%8D%E3%83%AB_(%E5%9B%BD%E9%81%932%E5%8F%B7))

それぞれの入口・出口は全く別の場所にあり、感覚的には郊外の1駅～2駅くらい離れています。

|	    |本州側の入口|九州側の入口|
|-------|-----------|-----------|
|**①山陽本線**|**山口県下関市 JR下関駅付近**|**福岡県北九州市 JR門司駅構内**|
|**②新幹線**|同、**JR新下関駅付近**|同、**JR小倉駅付近**|
|**③国道２号**|同、**椋野町<br>(下関駅と新下関駅の中間くらい)**|福岡県北九州市 **東門司**|

各トンネルの出入り口を地図にしたものは、下記を参照ください。

https://youtu.be/ZxOok-i3p7w?t=255

## 2.2. 曖昧な知識のもと、回答を間違える例

LLMは無差別に大量のコーパスを学習しているので、このような関門トンネルの意味の曖昧さを踏まえた回答をするとは考えづらいです。

実際に関門トンネルに関する下記の質問をGPTモデルにしてみたところ、予想通りすべて間違いを含んだ回答になってしまいました。**間違いの部分は太字[^2]にしています**。

例えば、福岡県にあるのは博多市ではなく福岡市ですし、関門トンネルの九州側の入口は前述の通り北九州市にあります。また、関門トンネルの南口が北九州市の小倉北区にあり、北口が門司区にあるという表記をしていますが、そもそも関門トンネルに南口・北口という概念はありません[^3]。

|質問|回答|
|----|----|
|関門トンネルとはどことどこを結ぶトンネルですか。 |関門トンネルは、日本の九州地方の**福岡県博多市**と山口県下関市を結ぶトンネルです。|
|関門トンネルの周辺にある地名をいくつか教えてください。|関門トンネルの周辺には、以下のような地名があります。<br>①**福岡県博多市**：**海峡を挟んで下関市と向かい合っている都市**で、博多港があります。<br>②山口県下関市：**海峡を挟んで博多市と向かい合っている都市**で、下関港があります。<br>③北九州市小倉北区：**関門トンネルの南口**がある地域で、商業施設やオフィスビルが立ち並んでいます。<br>④北九州市門司区：**関門トンネルの北口**がある地域で、港湾施設があります。<br>⑤下関市豊浦町：関門トンネルの北側に位置し、豊かな自然が残る地域です。|

## 2.3. 前提知識を与えて、正しい回答を導出する

ここでLlamaIndexによって作った前提知識の構造化データをGPTモデルに渡し、正しい回答を導出してみます。

まず適当なフォルダに`with_llama_index.py`を作成します。その中身は下記のとおりとします。

```python
import os

from llama_index import (
    GPTVectorStoreIndex,
    SimpleDirectoryReader,
    StorageContext,
    load_index_from_storage,
)

os.environ["OPENAI_API_KEY"] = "実際に使っているキーをコピペ"
persist_dir = "./storage/"

# save to disk
if not os.path.exists(persist_dir):
    os.mkdir(persist_dir)
documents = SimpleDirectoryReader("data").load_data()
index = GPTVectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir)

# load from disk
storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
# load index
index = load_index_from_storage(storage_context)


def print_response(prompt: str, index):
    query_engine = index.as_query_engine()
    print(query_engine.query(prompt))


print_response("関門トンネルとはどことどこを結ぶトンネルですか。", index)
print_response("関門トンネルの周辺にある地名をいくつか教えてください。", index)
```

ここで`with_llama_index.py`と同じフォルダに`data`フォルダを作成し、先に述べたWikipediaの内容を含む`関門トンネル.txt`をその直下に置きます。エンコーディングははUTF-8にしてください。以前はShift-JISにしないと文字化けしていたのですが、今は逆にUTF-8にしないと文字化けします。
その後、`with_llama_index.py`を実行して質問を発行すると、下表のような回答が返ってきます。

**前提知識となるデータを与えていない場合とは異なり、存在しない地名や固有名詞が回答に出てくることは無くなっています**。地名を聞いているのに、「早鞆の瀬戸」のような関門海峡上の場所の名前を出してしまうことはありますが、「博多と下関が隣り合わせである」というような明らかに間違った知識は出てこなくなりました。

|質問|回答|
|----|----|
|関門トンネルとはどことどこを結ぶトンネルですか。 |関門トンネルは、山口県下関市と福岡県北九州市の間を結ぶトンネルです。|
|関門トンネルの周辺にある地名をいくつか教えてください。|- 下関駅<br> - 弟子待<br> - 門司駅 <br> - 福岡県北九州市 <br> - 山口県下関市 <br> - 壇ノ浦 <br> - 和布刈 <br> - 彦島 <br> - 大瀬戸 <br> - 早鞆の瀬戸 <br> - 小瀬戸 |

## 2.4. LlamaIndexで作れるデータ構造

それでは、外部情報をどのようなデータ構造にしてLLMに渡しているのでしょうか。
一番わかり易いのは本家の資料です。

https://gpt-index.readthedocs.io/en/latest/guides/index_guide.html

資料にはいくつかのデータ構造が書かれていますが、**今回使っているのは[vector-store-index](https://gpt-index.readthedocs.io/en/latest/guides/index_guide.html#vector-store-index)というもの**です。

これは読み込んだテキストいくつかのベクトルを作り、それを単純に列挙しただけのデータ構造になっています。**例えば今回作った関門トンネルの文章は、1536次元のベクトル×2個になっています**。そのうち1つを表示してみると、単なる数値の羅列になっています。

```text
[0.007264599669724703,
-0.005486878100782633,
0.027614835649728775,
0.0038962848484516144,
-0.00790618360042572,
0.017322761937975883,
-0.014823258854448795,
-0.013954447582364082,
-0.006292199250310659,
-0.02300345152616501,
(後略)
```

[vector-store-index](https://gpt-index.readthedocs.io/en/latest/guides/index_guide.html#vector-store-index)のデータ構造から回答を生成する場合、**質問文も下記のように1536次元のベクトルに変換されます**。これは「関門トンネルとはどことどこを結ぶトンネルですか。」の質問を変換した結果です。

```text
[0.012113180942833424,
 -0.021244656294584274,
 0.01625296100974083,
 -0.0008635633275844157,
 -0.00911150872707367,
 0.01657242886722088,
 -0.008266247808933258,
 0.0047654053196311,
 0.003281207988038659,
 0.0017005043337121606,
(後略)
```

そして予め作っておいた関門トンネルのベクトルの中から、質問文のベクトルに近いものをコサイン類似度でマッチングします。そのうち類似度の高いものに対応する関門トンネルの原文箇所をプロンプトに組み込み、それに対する回答をLLMに生成させる仕組みになっています[^4]。
![vector_store_query](https://gpt-index.readthedocs.io/en/latest/_images/vector_store_query.png)
(上図は、https://gpt-index.readthedocs.io/en/latest/guides/index_guide.html#vector-store-index より引用)

原文の組み込まれたプロンプトを確認するには、上述のコード内で`import langchain`をした上で`langchain.verbose = True`を実行するように改変する必要があります。
そうすると、下記のようなプロンプトが発行されていることがわかります。

```text
Context information is below. 
---------------------
関門トンネル（かんもんトンネル）は、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。
 後に開通した国道2号の関門トンネル（関門国道トンネル）と区別するため、関門鉄道トンネル（かんもんてつどうトンネル）と呼ばれることもある。

 概要
 関門海峡は本州（山口県下関市）と九州（福岡県北九州市）の間にある海峡で、このうち深さの関係から西側の「大瀬戸」と呼ばれる部分に関門トンネルがある一方、もっとも海峡が狭くなる東側の「早鞆（はやとも）の瀬戸」に、ほかの関門海峡横断交通手段である国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋が通っている。もともとは関門連絡船でこの海峡を横断して結んでいたが、乗換・積替の手間を省き輸送力を増強するために3回にわたって関門海峡にトンネルを建設する計画が持ち上がり、3回目の昭和初期の計画により実際に着工することになった。

関門海峡を横断する橋やトンネルは、山陽本線（在来線）の関門トンネルのほかに、国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋があるが、在来線の関門トンネルのみ大瀬戸を通過しており、ほかの3経路はいずれも海峡がもっとも狭くなる早鞆の瀬戸を通過している。
 在来線の関門トンネルは、高架上の下関駅を出て本州から彦島へ渡ってトンネルに入り、弟子待（でしまつ）から大瀬戸の海底下をくぐって九州側の小森江に渡り、門司駅構内で地上に出る。在来線の関門トンネルが早鞆の瀬戸ではなく大瀬戸を通過することを選んだのは、早鞆の瀬戸の方が水深が深く、急勾配が許されない鉄道のトンネルでは全長が長くなってしまうことや、既存の鉄道との接続の関係からである。
 門司駅5・6番のりばの下関側に設置されている「関門トンネル案内板」（5番のりば側は下り線トンネル内部の写真）
---------------------
Given the context information and not prior knowledge, answer the question: 
関門トンネルとはどことどこを結ぶトンネルですか。
```

## 2.5. PDFをLlamaIndexで読み込む

ここまで、文章に対する質問にちゃんと回答していっているさまを見ていただきました。
こうなると「論文のPDFを読ませて、その内容について質問してみたい！」と思うのが技術者のサガではないかと思います。

実はそうしたことも可能です。
私自身の実機検証はまだなのですが、下記のようにPDFの内容をベクトル化して質疑に対応させることができます。

https://qiita.com/shimajiroxyz/items/aa1e22e19d690a3944ba

# 3. LangChainとLlamaIndexの連携

2章まではLlamaIndex単体の使い方を説明していきましたが、本章ではLangChainのagentから呼び出せるツールとしてLlamaIndexで作ったデータを利用するやり方を説明します。

agentとは[前回記事](https://qiita.com/kzkymn/private/a72796c89ffc696034c8)で言及した、質問文から回答に必要なAPIをLLMを使って判断し、それらを適宜呼び出した結果を利用して回答を返す処理を指します。

基本的な連携方法は下記記事によくまとまっています。ただ、旧GPT IndexがLlamaIndexに改称した関係で書くべきコードの内容が若干異なっているので、本章ではその点を踏まえたコードを示します。

https://note.com/npaka/n/n8de9c081ac46

この記事を参考に、LlamaIndexを回答に利用するagentを呼び出すプログラムを以下の通り作りました。

```python
# パッケージのインポート
import os

from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader

import langchain
from langchain import OpenAI, SerpAPIWrapper
from langchain.agents import Tool, initialize_agent
from langchain.chains.conversation.memory import ConversationBufferMemory

os.environ["OPENAI_API_KEY"] = "本物のAPIキーを設定のこと"
langchain.verbose = True

documents = SimpleDirectoryReader("data").load_data()
index = GPTVectorStoreIndex.from_documents(documents)

search = SerpAPIWrapper(serpapi_api_key="本物のAPIキーをここに")
tools = [
    Tool(
        name="Kanmon Tunnel",
        func=lambda q: str(index.as_query_engine().query(q)),
        description="Useful for the generating the answers of Kanmon Tunnel",
        return_direct=True,
    ),
    Tool(
        name="Search",
        func=search.run,
        description="useful for when you need to answer questions about current events",
    ),
]

agent = initialize_agent(
    tools,
    llm=OpenAI(temperature=0),
    gent="zero-shot-react-description",
    verbose=True,
)

agent.run(input="関門トンネルとはどことどこを結ぶトンネルですか。")
agent.run(input="青函トンネルとはどことどこを結ぶトンネルですか。")
```

このコードでは、agentが利用する外部ツールとして、LlamaIndexへの質問を投げる処理と、SerpAPIWrapperによる検索機能を設定しています。検索機能の設定は関門トンネルと無関係の質問が来たときに、回答に必要な情報を得るのに使います。

```python
search = SerpAPIWrapper(serpapi_api_key="本物のAPIキーをここに")
tools = [
    Tool(
        name="Kanmon Tunnel",
        func=lambda q: str(index.as_query_engine().query(q)),
        description="Useful for the generating the answers of Kanmon Tunnel",
        return_direct=True,
    ),
    Tool(
        name="Search",
        func=search.run,
        description="useful for when you need to answer questions about current events",
    ),
]
```

agentにこのツールの設定を読み込ませれば準備完了です。

```python
agent = initialize_agent(
    tools,
    llm=OpenAI(temperature=0),
    gent="zero-shot-react-description",
    verbose=True,
)
```

コードでは最後に2点の質問をしています。期待としては前者の回答だけLlamaIndexで作ったデータを使ったものになっており、後者はそれを使わないものになっていることを想定しています。

```python
print(agent.run(input="関門トンネルとはどことどこを結ぶトンネルですか。"))
print(agent.run(input="青函トンネルとはどことどこを結ぶトンネルですか。"))
```

### 関門トンネルについての回答

「関門トンネルとはどことどこを結ぶトンネルですか」に対して**期待する内容の回答は得られましたが、回答文が原文引用**になってしまっています。この回答を綺麗に要約する処理をagentの後続に追加すると、こなれた回答文になるかもしれません。

```text
Answer the following questions as best you can. You have access to the following tools:

Kanmon Tunnel: Useful for the generating the answers of Kanmon Tunnel
Search: useful for when you need to answer questions about current events

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Kanmon Tunnel, Search]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: 関門トンネルとはどことどこを結ぶトンネルですか。
Thought:

> Finished chain.
 関門トンネルはどことどこを結ぶトンネルなのかを確認する必要がある。
Action: Kanmon Tunnel
Action Input: 関門トンネル

> Entering new LLMChain chain...
Prompt after formatting:
Context information is below. 
---------------------
関門トンネル（かんもんトンネル）は、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。
 後に開通した国道2号の関門トンネル（関門国道トンネル）と区別するため、関門鉄道トンネル（かんもんてつどうトンネル）と呼ばれることもある。

 概要
 関門海峡は本州（山口県下関市）と九州（福岡県北九州市）の間にある海峡で、このうち深さの関係から西側の「大瀬戸」と呼ばれる部分に関門トンネルがある一方、もっとも海峡が狭くなる東側の「早鞆（はやとも）の瀬戸」に、ほかの関門海峡横断交通手段である国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋が通っている。もともとは関門連絡船でこの海峡を横断して結んでいたが、乗換・積替の手間を省き輸送力を増強するために3回にわたって関門海峡にトンネルを建設する計画が持ち上がり、3回目の昭和初期の計画により実際に着工することになった。

関門海峡を横断する橋やトンネルは、山陽本線（在来線）の関門トンネルのほかに、国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋があるが、在来線の関門トンネルのみ大瀬戸を通過しており、ほかの3経路はいずれも海峡がもっとも狭くなる早鞆の瀬戸を通過している。
 在来線の関門トンネルは、高架上の下関駅を出て本州から彦島へ渡ってトンネルに入り、弟子待（でしまつ）から大瀬戸の海底下をくぐって九州側の小森江に渡り、門司駅構内で地上に出る。在来線の関門トンネルが早鞆の瀬戸ではなく大瀬戸を通過することを選んだのは、早鞆の瀬戸の方が水深が深く、急勾配が許されない鉄道のトンネルでは全長が長くなってしまうことや、既存の鉄道との接続の関係からである。
 門司駅5・6番のりばの下関側に設置されている「関門トンネル案内板」（5番のりば側は下り線トンネル内部の写真）
---------------------
Given the context information and not prior knowledge, answer the question: 関門トンネル


> Finished chain.

Observation: 
関門トンネルは、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。関門トンネルは、関門海峡を横断する橋やトンネルのうち、在来線の関門トンネルのみ大瀬

> Finished chain.
```

### 青函トンネルについての回答

**関門トンネルに関するデータは参照しつつも、最終的には「本州と北海道を結ぶ、鉄道用の海底トンネル」との結論**を下しています。ここまではよいのですが、「札幌駅 - 函館駅間」のトンネルと思っていたり、青函トンネルの全長を約15kmとしている間違った回答[^5]を返しています。このように外部データをちゃんと検索・参照してくれない場合は、LLMからの答えが怪しくなりがちです。

```text
Answer the following questions as best you can. You have access to the following tools:

Kanmon Tunnel: Useful for the generating the answers of Kanmon Tunnel
Search: useful for when you need to answer questions about current events

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Kanmon Tunnel, Search]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: 関門トンネルとはどことどこを結ぶトンネルですか。
Thought:

> Finished chain.
 関門トンネルはどことどこを結ぶトンネルなのかを確認する必要がある。
Action: Kanmon Tunnel
Action Input: 関門トンネル

> Entering new LLMChain chain...
Prompt after formatting:
Context information is below. 
---------------------
関門トンネル（かんもんトンネル）は、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。
 後に開通した国道2号の関門トンネル（関門国道トンネル）と区別するため、関門鉄道トンネル（かんもんてつどうトンネル）と呼ばれることもある。

 概要
 関門海峡は本州（山口県下関市）と九州（福岡県北九州市）の間にある海峡で、このうち深さの関係から西側の「大瀬戸」と呼ばれる部分に関門トンネルがある一方、もっとも海峡が狭くなる東側の「早鞆（はやとも）の瀬戸」に、ほかの関門海峡横断交通手段である国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋が通っている。もともとは関門連絡船でこの海峡を横断して結んでいたが、乗換・積替の手間を省き輸送力を増強するために3回にわたって関門海峡にトンネルを建設する計画が持ち上がり、3回目の昭和初期の計画により実際に着工することになった。

関門海峡を横断する橋やトンネルは、山陽本線（在来線）の関門トンネルのほかに、国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋があるが、在来線の関門トンネルのみ大瀬戸を通過しており、ほかの3経路はいずれも海峡がもっとも狭くなる早鞆の瀬戸を通過している。
 在来線の関門トンネルは、高架上の下関駅を出て本州から彦島へ渡ってトンネルに入り、弟子待（でしまつ）から大瀬戸の海底下をくぐって九州側の小森江に渡り、門司駅構内で地上に出る。在来線の関門トンネルが早鞆の瀬戸ではなく大瀬戸を通過することを選んだのは、早鞆の瀬戸の方が水深が深く、急勾配が許されない鉄道のトンネルでは全長が長くなってしまうことや、既存の鉄道との接続の関係からである。
 門司駅5・6番のりばの下関側に設置されている「関門トンネル案内板」（5番のりば側は下り線トンネル内部の写真）
---------------------
Given the context information and not prior knowledge, answer the question: 関門トンネル


> Finished chain.

Observation: 
関門トンネルは、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。関門トンネルは、関門海峡を横断する橋やトンネルのうち、在来線の関門トンネルのみ大瀬

> Finished chain.
'\n関門トンネルは、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。関門トンネルは、関門海峡を横断する橋やトンネルのうち、在来線の関門トンネルのみ大瀬'
agent.run(input="青函トンネルとはどことどこを結ぶトンネルですか。")

> Entering new AgentExecutor chain...


> Entering new LLMChain chain...
Prompt after formatting:
Answer the following questions as best you can. You have access to the following tools:

Kanmon Tunnel: Useful for the generating the answers of Kanmon Tunnel
Search: useful for when you need to answer questions about current events

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Kanmon Tunnel, Search]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: 青函トンネルとはどことどこを結ぶトンネルですか。
Thought:

> Finished chain.
 青函トンネルはどことどこを結ぶトンネルなのかを調べる必要がある。
Action: Kanmon Tunnel
Action Input: 青函トンネル

> Entering new LLMChain chain...
Prompt after formatting:
Context information is below. 
---------------------
関門トンネル（かんもんトンネル）は、関門海峡をくぐって本州と九州を結ぶ、鉄道用の海底トンネルである。九州旅客鉄道（JR九州）の山陽本線下関駅 - 門司駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長3,614.04メートル、上り線トンネルは全長3,604.63メートルである。
 後に開通した国道2号の関門トンネル（関門国道トンネル）と区別するため、関門鉄道トンネル（かんもんてつどうトンネル）と呼ばれることもある。

 概要
 関門海峡は本州（山口県下関市）と九州（福岡県北九州市）の間にある海峡で、このうち深さの関係から西側の「大瀬戸」と呼ばれる部分に関門トンネルがある一方、もっとも海峡が狭くなる東側の「早鞆（はやとも）の瀬戸」に、ほかの関門海峡横断交通手段である国道2号の関門トンネル、山陽新幹線の新関門トンネル、高速道路の関門橋が通っている。もともとは関門連絡船でこの海峡を横断して結んでいたが、乗換・積替の手間を省き輸送力を増強するために3回にわたって関門海峡にトンネルを建設する計画が持ち上がり、3回目の昭和初期の計画により実際に着工することになった。

当面は単線の輸送力で十分であったことに加えて、工事の容易さから、単線でトンネルを建設することになり、将来輸送量が増えたときにもう1本の単線トンネルを建設して複線とすることになった。先に建設されたのは下り線のトンネルで、両側の地上と水底部との間において機関車が列車を引っ張る（牽引する）性能を勘案して、20パーミル勾配を採用することにしたが、のちに上り線のトンネルを建設した際には、海底部分での土被りを増すために一部で25パーミル勾配が採用された。
 事前に潜水艇による調査やボーリング調査などを実施して地質を調べたうえで、まず地質の調査や周り込んで本線の掘削箇所を増やすことやセメントの注入による地盤改良を行うため、細い試掘坑道を建設することとなった。これは1937年（昭和12年）に着工し、1939年（昭和14年）4月19日に貫通、8月5日に完成した。まだ試掘坑道を建設中であった1937年（昭和12年）12月から下り線トンネルの掘削にも着手し、門司側からは日本では3番目というシールド工法も使用して建設が進められた。
 1942年（昭和17年）6月11日に最初の試運転列車が下り線トンネルを通過し、7月1日に貨物列車用に開通、11月15日に旅客列車用にも開通し、まずは単線での供用を開始した。上り線トンネルについては、1940年（昭和15年）に着工が決定され、1944年（昭和19年）8月8日に開通し、下り線から上り線に列車を移したうえで下り線トンネルの改修工事を行って、9月9日から複線での運転が開始された。
---------------------
Given the context information and not prior knowledge, answer the question: 青函トンネル


> Finished chain.

Observation: 
青函トンネルは、本州と北海道を結ぶ、鉄道用の海底トンネルである。JR東日本の北海道新幹線の札幌駅 - 函館駅間に所在する。単線トンネル2本で構成され、下り線トンネルは全長13,561メートル、上り線トンネルは全長13,558メートルである。

> Finished chain.
```

# 4. まとめ

今回はLlamaIndexで作成した外部データを使って、LLMの回答内容の確からしさを高められることを紹介できました。

LLMの基礎研究に関して日本は大幅に遅れを取ってしまいましたが、LLMが利用できる外部データを大量に用意したり、その検索エンジンを開発したりという形でLLMの発展に寄与して収益を上げることはまだまだ可能ではないかと考えます。

最近ではAzureで記録を残さずLLMを利用できる流れができつつあるようで、こうしたLLMと自社データを組み合わせるなどの形で独自サービスを提供する企業が登場することも予想されます。

https://twitter.com/imos/status/1627975957591232517

本記事を見るようなIT技術者においては、LLMの[ビッグウェーブには乗るしか無い](https://moto-neta.com/net/rido-on-this-big-wave/)と思います。これからはますますLangChainを中心にLLMの応用技術を追っていき、そのビジネス活用手段を突き詰める必要があるように思います。
![ride on the big wave](https://moto-neta.com/wp/wp-content/uploads/ac022d3b246eee88bad10bebaefdea33-e1371034980228.jpg.webp)
(画像引用元: https://moto-neta.com/wp/wp-content/uploads/ac022d3b246eee88bad10bebaefdea33-e1371034980228.jpg.webp)

# Appendix 1. 紹介しきれなかった事柄

本資料で説明した事柄以外にも、執筆にあたって見つけた有用な情報がありますので、雑多ではありますが以下列挙していきます。

## LangChainを用いた文章要約

LLMによる文章要約をLangChainから実行することもできます。
下記にはLangChainに予め用意されたテキストを要約する例が載っています。

https://langchain.readthedocs.io/en/latest/modules/indexes/chain_examples/summarize.html

## OpenAI以外を使った埋め込みベクトルの作り方

LlamaIndexで外部データの埋め込みベクトルを作る場合、デフォルトではOpenAIのAPIにたくさん課金しないといけません。
その課金額を減らすには、OpenAIの提供する以外のモデルを使う手があります。

私が把握している限りでは下記の手段があります。

https://note.com/npaka/n/nee11eb7e620e

https://note.com/npaka/n/n72f12769acc2

なおOpenAIとの価格比較はしていませんが、以下のサービスで埋め込みベクトルを作ることも可能です。

https://langchain.readthedocs.io/en/latest/ecosystem/cohere.html

以前は、OpenAI以外の埋め込みベクトルを保存して読み出す際に不具合がありましたが、今はその不具合はありません。
たとえば、TensorflowHubが提供するベクトル生成機能を利用することができます。以下はそのサンプルコードです。

```python
import os

from llama_index import (
    GPTVectorStoreIndex,
    LangchainEmbedding,
    SimpleDirectoryReader,
    StorageContext,
    load_index_from_storage,
)

import langchain
from langchain.embeddings import TensorflowHubEmbeddings

langchain.verbose = True
os.environ["OPENAI_API_KEY"] = "実際に使っているキーをコピペ"
persist_dir = "./storage_tfh/"

# save to disk
if not os.path.exists(persist_dir):
    os.mkdir(persist_dir)
documents = SimpleDirectoryReader("data").load_data()
index = GPTVectorStoreIndex.from_documents(
    documents,
    embed_model=LangchainEmbedding(TensorflowHubEmbeddings()),
)
index.storage_context.persist(persist_dir)

# load from disk
storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
# load index
index = load_index_from_storage(storage_context)


def print_response(prompt: str, index):
    query_engine = index.as_query_engine()
    print(query_engine.query(prompt))


print_response("関門トンネルとはどことどこを結ぶトンネルですか。", index)
print_response("関門トンネルの周辺にある地名をいくつか教えてください。", index)
```

HuggingFaceにあるモデルを使った埋め込みベクトルの作成も可能です。具体的には上記のコードで`from langchain.embeddings import HuggingFaceEmbeddings`をインポートした上で、`GPTVectorStoreIndex.from_documents`に渡すembed_modelを下記の通り差し替えます。

```python
embed_model=LangchainEmbedding(
    HuggingFaceEmbeddings(model_name="oshizo/sbert-jsnli-luke-japanese-base-lite")
)
```

## 埋め込みベクトルによる文書検索

文書と質問文がどちらもベクトル化できるなら、コサイン類似度などを頼りに質問文に近い文書を検索することもできそうですし、実際できます。詳しくは以下の記事を参照。

https://note.com/npaka/n/n0be34a51395d

## LangChainを介した倫理チェック

チャットボットを公開すると、「人を殺す方法」みたいな不穏なことを聞いてくる輩が想定されます。そうした回答にNoを突きつけるには質問文の倫理チェックが必要です。

そのためのAPIはOpenAIが用意しており、LangChainから呼び出して使うことができます。

https://note.com/npaka/n/n4fa88cd316c1

## LangChainで使える言語モデル

LangChainでは、OpenAI以外にも使える言語モデルが下記の通りあります。

- AI21 Labs　https://langchain.readthedocs.io/en/latest/ecosystem/ai21.html
- CerebriumAI　https://langchain.readthedocs.io/en/latest/ecosystem/cerebriumai.html
- Cohere　https://langchain.readthedocs.io/en/latest/ecosystem/chroma.html
- ForefrontAI　https://langchain.readthedocs.io/en/latest/ecosystem/cohere.html
- GooseAI https://langchain.readthedocs.io/en/latest/ecosystem/gooseai.html
- Hazy Research https://langchain.readthedocs.io/en/latest/ecosystem/hazy_research.html
- Hugging Face https://langchain.readthedocs.io/en/latest/ecosystem/huggingface.html
- NLPCloud　https://langchain.readthedocs.io/en/latest/ecosystem/nlpcloud.html
- Petals　https://langchain.readthedocs.io/en/latest/ecosystem/petals.html
- PromptLayer　https://langchain.readthedocs.io/en/latest/ecosystem/promptlayer.html

## LangChainから利用できるサービス

- トークナイザ
  - Hugging Face https://langchain.readthedocs.io/en/latest/ecosystem/huggingface.html
  - OpenAI　https://langchain.readthedocs.io/en/latest/ecosystem/openai.html
- データセット取得
  - Hugging Face https://langchain.readthedocs.io/en/latest/ecosystem/huggingface.html
- OpenAI利用状況データ取得
  - Helicone https://langchain.readthedocs.io/en/latest/ecosystem/helicone.html
- 検索
  - Google Search　https://langchain.readthedocs.io/en/latest/ecosystem/google_search.html
  - SerpAPI　https://langchain.readthedocs.io/en/latest/ecosystem/serpapi.html
- 計算
  - Wolfram Alpha Wrapper　https://langchain.readthedocs.io/en/latest/ecosystem/wolfram_alpha.html
- VectorStore(埋め込みベクトル用のデータベース)
  - Pinecone https://langchain.readthedocs.io/en/latest/ecosystem/pinecone.html
  - Weaviate　https://langchain.readthedocs.io/en/latest/ecosystem/weaviate.html
  - Chroma https://langchain.readthedocs.io/en/latest/ecosystem/chroma.html

# Appendix 2. フォローしておくと良い人々

## 布留川英一さん

AIに限らず、3D技術やスマホに至るまでIT系の新技術をとにかくすぐに調べて解説本をリリースされる方。

https://note.com/npaka/

布留川さんのAmazonの著者ページを見たら、最低1冊は読んだ本があるのではないでしょうか。

https://www.amazon.co.jp/%E6%9C%AC-%E5%B8%83%E7%95%99%E5%B7%9D-%E8%8B%B1%E4%B8%80/s?rh=n%3A465392%2Cp_27%3A%E5%B8%83%E7%95%99%E5%B7%9D+%E8%8B%B1%E4%B8%80

## 逆瀬川さん

LLMを応用したアプリをよく試作されている方。
Qiita記事の[「契約書の差分比較をGPT-3を使って自動化する」](https://qiita.com/sakasegawa/items/836a30730a2421774354)、[「GPT-3を用いて1つの単語からプレゼンを自動生成するシステムの作り方」](https://qiita.com/sakasegawa/items/abd0e5efac2969898878)は必見。

https://qiita.com/sakasegawa

[^1]: ③の国道2号にあたる関門トンネルは実は、自動車道と人道が別々の場所に分かれて存在しています。ただし後者は俗に「人道トンネル」と呼びがちなのでここでは説明を省きました。
[^2]: 100%間違いではありませんが、「下関市豊浦町」は関門トンネルの北側というには遠く、トンネルから20km以上離れています。
[^3]: 地理的には小倉のほうが門司より南なので、その点は正しいのですが。
[^4]: このあたりのより詳しい解説は外部記事[「ChatGPTで独自データを扱うためのエンべディング」](https://note.com/ogatahisato/n/n899dcb459f35)をご覧ください。
[^5]: 本当の長さは53.85km。