LlamaIndexを使った要約の内容把握

# 1. はじめに
2023年はChatGPTや生成AI、LLMアプリ開発ライブラリの展開がとても速いですね。  
キャッチアップするのもなかなか大変ですが、業務で関係しそうなところを中心に触って理解を深めていきたいと考えています。  
今回は業務でも使う場面が多そうな「要約」について試しています。  
プロンプトにはトークン数の制限があるため、長いテキストの要約を行うにはいろいろと工夫が必要になります。  
今回はChatGPT＋LlamaIndexを使って長文の要約を試しました。  
LlamaIndexの公式ドキュメントを見るとわかるのですが、あまり内部の仕組みについては記載されていません。  
自分も全容を把握するのに苦労したので、実施結果だけでなく解説も載せることにしました。  
記事を読まれた方の参考になれば幸いです。  

# 2. 要約に使用したテキスト
青空文庫から吉川英治の三国志をチョイスしています。  
01～12まで副題が分かれていますが、その中から「02 桃園の巻」をダウンロードしました。  
参照) [図書カード：三国志](https://www.aozora.gr.jp/cards/001562/card52410.html)  
ココだけでもかなりのボリュームがあるため、さらに「義盟」という大見出しの章だけを抽出しています。  
劉備、関羽、張飛が義兄弟の契りを結ぶシーンが描かれています。  
ちなみに原文は、以下のような内容です。  
```
［＃３字下げ］義盟《ぎめい》［＃「義盟」は大見出し］

［＃７字下げ］一［＃「一」は中見出し］

　桃園へ行ってみると、関羽と張飛のふたりは、近所の男を雇ってきて、園内の中央に、もう祭壇を作っていた。
　壇の四方には、笹竹《ささだけ》を建て、清縄《せいじょう》をめぐらして金紙《きんし》銀箋《ぎんせん》の華《はな》をつらね、土製の白馬を贄《いけにえ》にして天を祭り、烏牛を屠《ほふ》ったことにして、地神を祠《まつ》った。
「やあ、おはよう」
　劉備《りゅうび》が声をかけると、
「おお、お目ざめか」
　張飛、関羽は、振向いた。
：
```
ルビが振られていたり、注釈があったり、なかなか扱いづらい書体となっています。  
そのため、以下のように前処理を施したテキストを使用しています。  
```
02_09_義盟
02_09_01_一
　桃園へ行ってみると、関羽と張飛のふたりは、近所の男を雇ってきて、園内の中央に、もう祭壇を作っていた。
　壇の四方には、笹竹を建て、清縄をめぐらして金紙銀箋の華をつらね、土製の白馬を贄にして天を祭り、烏牛を屠ったことにして、地神を祠った。
「やあ、おはよう」
　劉備が声をかけると、
「おお、お目ざめか」
　張飛、関羽は、振向いた。
：
```
だいぶすっきりした印象です。  
ちなみに大見出しや中見出しについているプレフィックスは、先頭から順に巻番号(02)、大見出しの章番号(09)、中見出しの節番号(01,02,・・・)を表しています。(今回の要約には特に関係ありません)  
<details><summary>なお、今回使用した全文はこちらをクリックすると参照できます。</summary>

```
02_09_義盟
02_09_01_一
　桃園へ行ってみると、関羽と張飛のふたりは、近所の男を雇ってきて、園内の中央に、もう祭壇を作っていた。
　壇の四方には、笹竹を建て、清縄をめぐらして金紙銀箋の華をつらね、土製の白馬を贄にして天を祭り、烏牛を屠ったことにして、地神を祠った。
「やあ、おはよう」
　劉備が声をかけると、
「おお、お目ざめか」
　張飛、関羽は、振向いた。
「見事に祭壇ができましたなあ。寝る間はなかったでしょう」
「いや、張飛が、興奮して、寝てから話しかけるので、ちっとも眠る間はありませんでしたよ」
　と、関羽は笑った。
　張飛は劉備のそばへきて、
「祭壇だけは立派にできたが、酒はあるだろうか」
　心配して訊ねた。
「いや、母が何とかしてくれるそうです。今日は、一生一度の祝いだといっていますから」
「そうか、それで安心した。しかし劉兄、いいおっ母さんだな。ゆうべからそばで見ていても、羨しくてならない」
「そうです。自分で自分の母を褒めるのもへんですが、子に優しく世に強い母です」
「気品がある、どこか」
「失礼だが、劉兄には、まだ夫人はないようだな」
「ありません」
「はやくひとり娶らないと、母上がなんでもやっている様子だが、あのお年で、お気の毒ではないか」
「…………」
　劉備は、そんなことを訊かれたので、またふと、忘れていた鴻芙蓉の佳麗なすがたを思い出してしまった。
　で、つい答えを忘れて、何となく眼をあげると、眼の前へ、白桃の花びらが、霏々と情あるもののように散ってきた。
「劉備や。皆さんも、もうお支度はよろしいのですか」
　厨に見えなかった母が、いつの間にか、三名の後ろにきて告げた。
　三名が、いつでもと答えると、母はまた、いそいそと厨房のほうへ去った。
　近隣の人手を借りてきたのであろう。きのう張飛の姿を見て、きゃっと魂消て逃げた娘も、その娘の恋人の隣家の息子も、ほかの家族も、大勢して手伝いにきた。
　やがて、まず一人では持てないような酒瓶が祭壇の莚へ運ばれてきた。
　それから豚の仔を丸ごと油で煮たのや、山羊の吸物の鍋や、干菜を牛酪で煮つけた物だの、年数のかかった漬物だの――運ばれてくるごとに、三名は、その豪華な珍味の鉢や大皿に眼を奪われた。
　劉備さえ、心のうちで、
「これは一体、どうしたことだろう」と、母の算段を心配していた。
　そのうちにまた、村長の家から、花梨の立派な卓と椅子がかつがれてきた。
「大饗宴だな」
　張飛は、子どものように、歓喜した。
　準備ができると、手伝いの者は皆、母屋へ退がってしまった。
　三名は、
「では」
　と、眼を見合せて、祭壇の前の蓆へ坐った。そして天地の神へ、
「われらの大望を成就させ給え」
　と、祈念しかけると、関羽が、
「ご両所。少し待ってくれ」
　と、なにか改まっていった。

02_09_02_二
「ここの祭壇の前に坐ると同時に、自分はふと、こんな考えを呼び起されたが、両公の所存はどんなものだろうか」
　関羽は、そう云いだして、劉備と張飛へ、こう相談した。
　すべて物事は、体を基とする。体形を整えていないことに成功はあり得ない。
　偶然、自分たち三人は、その精神において、合致を見、きょうを出発として大事をなそうとするものであるが、三つの者が寄り合っただけでは、体をなしていない。
　今は、小なる三人ではあるが、理想は遠大である。三体一心の体を整えおくべきではあるまいか。
　事の中途で、仲間割れなど、よくある例である。そういう結果へ到達させてはならない。神のみ祷り、神のみ祀っても、人事を尽さずして、大望の成就はあり得べくもあるまい。
　関羽の説くところは、道理であったが、さてどういう体を備えるかとなると、張飛にも劉備にもさし当ってなんの考えもなかった。
　関羽は、語をつづけて、
「まだ兵はおろか、兵器も金も一頭の馬すら持たないが、三名でも、ここで義盟を結べば、即座に一つの軍である。軍には将がなければならず、武士には主君がなければならぬ。行動の中心に正義と報国を奉じ、個々の中心に、主君を持たないでは、それは徒党の乱に終り、烏合の衆と化してしまう。――張飛もこの関羽も、今日まで、草田に隠れて時を待っていたのは、実に、その中心たるお人が容易にないためだった。折ふし劉備玄徳という、しかも血統の正しいお方に会ったのが、急速に、今日の義盟の会となったのであるから、今日ただいま、ここで劉備玄徳どのを、自分らの主君と仰ぎたいと思うが、張飛、おまえの考えはどうだ」
　訊くと、張飛も、手を打って、
「いや、それは拙者も考えていたところだ。いかにも、兄のいう通り、きめるならば、今ここで、神に祷るまえに、神へ誓ったほうがよい」
「玄徳様、ふたりの熱望です。ご承知くださるまいか」
　左右から詰めよられて、劉備玄徳は、黙然と考えていたが、
「待って下さい」
　と、二人の意気ごみを抑え、なおややしばらく沈思してから、身を正していった。
「なるほど、自分は漢の宗室のゆかりの者で、そうした系図からいえば、主たる位置に坐るべきでしょうが、生来鈍愚、久しく田舎の裡にひそみ、まだなにも各〻の上に立って主君たるの修養も徳も積んでおりませぬ。どうか今しばらく待って下さい」
「待ってくれと仰っしゃるのは」
「実際に当って、徳を積み、身を修め、果たして主君となるの資才がありや否や、それを自身もあなたたちも見届けてから約束しても、遅くないと思われますから」
「いや。それはもう、われわれが見届けてあるところです」
「左はいえ、私はなお、憚られます。――ではこうしましょう。君臣の誓いは、われわれが一国一城を持った上として、ここでは、三人義兄弟の約束を結んでおくことにして下さい。君臣となって後も、なお三人は、末永く義兄弟であるという約束をむしろ私はしておきたいのですが」
「うむ」
　関羽は、長い髯を持って、自分の顔を引っぱるように大きくうなずいた。
「結構だ。張飛、おぬしは」
「異論はない」
　改めて三名は、祭壇へ向って牛血と酒をそそぎ、ぬかずいて、天地の神祇に黙祷をささげた。

02_09_03_三
　年齢からいえば、関羽がいちばん年上であり、次が劉備、その次が張飛という順になるのであるが、義約のうえの義兄弟だから年順をふむ必要はないとあって、「長兄には、どうか、あなたがなって下さい。それでないと、張飛の我ままにも、おさえが利きませんから」と、関羽がいった。
　張飛も、ともども、
「それは是非、そうありたい。いやだといっても、二人して、長兄長兄と崇めてしまうからいい」
　劉備は強いて拒まなかった。そこで三名は、鼎座して、将来の理想をのべ、刎頸の誓いをかため、やがて壇をさがって桃下の卓を囲んだ。
「では、永く」
「変るまいぞ」
「変らじ」
　と、兄弟の杯を交わし、そして、三人一体、協力して国家に報じ、下万民の塗炭の苦を救うをもって、大丈夫の生涯とせんと申し合った。
　張飛は、すこし酔うてきたとみえて、声を大にし、杯を高く挙げて、
「ああ、こんな吉日はない。実に愉快だ。再び天にいう。われらここにあるの三名。同年同月同日に生まるるを希わず、願わくば同年同月同日に死なん」
　と、呶鳴った。そして、
「飲もう。大いに、きょうは飲もう――ではありませんか」
　などと、劉備の杯へも、やたらに酒をついだ。そうかと思うと、自分の頭を、ひとりで叩きながら、「愉快だ。実に愉快だ」と、子供みたいにさけんだ。
　あまり彼の酒が、上機嫌に発しすぎる傾きが見えたので、関羽は、
「おいおい、張飛。今日のことを、そんなに歓喜してしまっては、先の歓びは、どうするのだ。今日は、われら三名の義盟ができただけで、大事の成功不成功は、これから後のことじゃないか。少し有頂天になるのが早すぎるぞ」と、たしなめた。
　だが、一たん上機嫌に昇ってしまうと、張飛の機嫌は、なかなか水をかけても醒めない。関羽の生真面目を、手を打って笑いながら、
「わはははは、今日かぎり、もう村夫子は廃業したはずじゃないか。お互いに軍人だ。これからは天空海闊に、豪放磊落に、武人らしく交際おうぜ。なあ長兄」
　と、劉備へも、すぐ馴々といって、肩を抱いたりした。
「そうだ。そうだ」と、劉備玄徳は、にこにこ笑って、張飛のなすがままになっていた。
　張飛は、牛の如く飲み、馬のごとく喰ってから、
「そうそう。ここの席に、劉母公がいないという法はない。われわれ三人、兄弟の杯をしたからには、俺にとっても、尊敬すべきおっ母さんだ。――ひとつおっ母さんをこれへ連れてきて、乾杯しなおそう」
　急に、そんなことを云いだすと、張飛はふらふら母屋のほうへ馳けて行った。そしてやがて、劉母公を、無理に、自分の背中へ負って、ひょろひょろ戻ってきた。
「さあ、おっ母さんを、連れてきたぞ。どうだ、俺は親孝行だろう――さあおっ母さん、大いに祝って下さい。われわれ孝行息子が三人も揃いましたからね――いやこれは、独りおっ母さんにとって祝すべき孝行息子であるのみではない。支那の――国家にとってもだ、われわれこう三名は得がたい忠良息子ではあるまいか――そうだ、おっ母さんの孝行息子万歳、国家の忠良息子万歳っ」
　そしてやがて、こう三人の中では、酒に対しても一番の誠実息子たるその張飛が、まっ先に酔いつぶれて、桃花の下に大いびきで寝てしまい、夜露の降りるころまで、眼を醒まさなかった。

02_09_04_四
　大丈夫の誓いは結ばれた。しかし徒手空拳とはまったくこの三人のことだった。しかも志は天下にある。
「さて、どうしたものか」
　翌日はもう酒を飲んでただ快哉をいっている日ではない。理想から実行へ、第一歩を踏みだす日である。
　朝飯を食べると、すぐその卓の上で、いかに実行へかかるかの問題がでた。
「どうかなるよ。男児が、しかも三人一体で、やろうとすれば」
　張飛は、理論家でない。また計画家でもない。遮二無二、実行力に燃える猪突邁進家なのである。
「どうかなるって、ただ貴公のように、力んでばかりいたってどうもならん。まず、一郡の士を持たんとするには、一旗の兵がいる。一旗の兵を持つには、すくなくとも相当の軍費と、兵器と、馬とが必要だな」
　が、関羽は、常識家であった。二人のことばを飽和すると、そこにちょうどよい情熱と常理との推進力が醸されてくる。
　劉備は、そのいずれへも、うなずきを与えて、
「そうです。こう三人の一念をもってすれば、必ず大事を成しうることは目に見えていますが、さし当って、兵隊です。――これをひとつ募りましょう」
「馬も、兵器も、金もなく、募りに応じてくれる者がありましょうか」
　関羽の憂いを、劉備はかろく微笑をもって打消し、
「いささか、自信があります。――というのは、実はこの楼桑村の内にも、平常からそれとなく、私が目にかけた、同憂の志を持っている青年たちが少々あります。――また近郷にわたって、檄を飛ばせば、おそらく今の時勢に、鬱念を感じている者もすくなくはありませんから、きっと、三十人や四十人の兵はすぐできるかと思います」
「なるほど」
「ですから、恐れいるが、関羽どのの筆で、ひとつ檄文を起草して下さい。それを配るのは、私の知っている村の青年にやらせますから」
「いや、手前は、生来悪文の質ですから、ひとつそれは、劉長兄に起草していただこう」
「いいや、あなたは多年塾を持って、子弟を教育していたから、そういう子弟の気持を打つことは、よくお心得のはずだ。どうか書いて下さい」
　すると張飛がそばからいった。
「こら関羽、怪しからんぞ」
「なにが怪しからん」
「長兄劉玄徳のことば、主命の如く反くまいぞ、昨日、約束したばかりじゃないか」
「やあ、これは一本、張飛にやられたな、よし早速書こう」
　飛檄はでき上がった。
　なかなか名文である。荘重なる慷慨の気と、憂国の文字は、読む者を打たずにおかなかった。
　それが近郷へ飛ばされると、やがてのこと、劉玄徳の破れ家の門前には、毎日、七名十名ずつとわれこそ天下の豪傑たらんとする熱血の壮士が集まってきた。
　張飛は、門前へ出て、
「お前達は、われわれの檄を見て、兵隊になろうと望んできたのか」
　と、採用係の試験官になって、いちいち姓名や生国や、また、その志を質問した。
「そうです、大人がたのお名前と、義挙の趣旨に賛同して、旗下に馳せ参じてきた者どもです」
　壮士らは異口同音にいった。
「そうか、どれを見ても、たのもしい面魂、早速、われわれの旗挙げに、加盟をゆるすが、しかしわれらの志は、黄巾賊の輩の如く、野盗掠奪を旨とするのとは違うぞ。天下の塗炭を救い、害賊を討ち、国土に即した公権を確立し、やがては永遠の平和と民福を計るにある。分っておるかそこのところは！」
　張飛は、一場の訓示を垂れて、それからまた、次のように誓わせた。

02_09_05_五
「われわれの旗下に加盟するからには、即ち、われわれの奉じる軍律に服さねばならん。今、それを読み聞かすゆえ、謹んで承れ」
　張飛は、志願してきた壮士たちへいって、うやうやしく、懐中から一通を取出して、声高く読んだ。

　　一　卒たる者は、将たる者に、絶大の服従と礼節を守る。
　　一　目前の利に惑わず、大志を遠大に備う。
　　一　一身を浅く思い、一世を深く思う。
　　一　掠奪断首。
　　一　虐民極刑。
　　一　軍紀を紊る行為一切死罪。

「わかったかっ」
　あまり厳粛なので、壮士たちも、しばらく黙っていたが、やがて、
「分りました」と、異口同音にいった。
「よし、しからば、今よりそれがしの部下として用いてやる。ただし、当分の間は給料もつかわさんぞ。また、食物その他も、お互いにある物を分けて喰い、いっさい不平を申すことならん」
　それでも、募りに応じてきた若者輩は、元気に兵隊となって、劉備、関羽らの命に服した。
　四、五日のうちに、約七、八十人も集まった。望外な成功だと、関羽はいった。
　けれど、すぐ困りだしたのは食糧であった。ゆえに、一刻もはやく、戦争をしなければならない。
　黄匪の害に泣いている地方はたくさんある。まずその地方へ行って、黄巾賊を追っぱらうことだ。その後には、正しい税と食物とが収穫される。それは掠奪でない。天禄だ。
　するとある日。
「張将軍、張将軍。馬がたくさん通りますぞ、馬が」
　と、一人の部下が、ここの本陣へ馳せてきて注進した。
　何者か知らないが、何十頭という馬を数珠つなぎにひいて、この先の峠を越えてくる者があるという報告なのだ。
　馬と聞くと、張飛は、「そいつは何とか欲しいものだなあ」と正直にうなった。
　実際いま、喉から手の出るほど欲しい物は馬と金と兵器だった。だが、義挙の軍律というものを立てて部下にも示してあるので、「掠奪して来い」とは、命じられなかった。
　張飛は、奥へ行って、
「関羽、こういう報告があるが、なんとか、手に入れる工夫はあるまいか。実に天の与えだと思うのだが」と、相談した。
　関羽は聞くと、
「よし、それでは、自分が行って、掛合ってみよう」と、部下数名をつれて、峠へ急いで行った。麓の近くで、その一行とぶつかった。物見の兵の注進に過りなく、成程、四、五十頭もの馬匹をひいて、一隊の者がこっちへ下ってくる。近づいて見ると皆、商人ていの男なので、これならなんとか、話合いがつくと、関羽は得意の雄弁をふるうつもりで待ち構えていた。
　ここへきた馬商人の一隊の頭は、中山の豪商でひとりは蘇双、ひとりは張世平という者だった。
　関羽は、それに着くと、自分ら三人が義軍を興すに至った、愛国の衷情をもって、切々訴えた。今にして、誰か、この覇業を建て、人天の正明をたださなければ、この世は永遠の闇黒であろうといった。支那大陸は、ついに、胡北の武民に征服され終るであろうと嘆いた。
　張世平と蘇双の両人は、なにか小声で相談していたが、やがて、
「よく分りました。この五十頭の馬が、そういうことでお役に立てば満足です。差上げますからどうぞ曳いて行って下さい」と、意外にも、いさぎよく云った。

02_09_06_六
　いずれ易々とは承知しまい。最悪な場合までを関羽は考えていたのである。それが案外な返辞に、
「ほ。……いや忝けない。早速の快諾に、申しては失礼だが、利に敏い商人たるお身らが、どうしてそう一言のもとに、多くの馬匹を無料でそれがしへ引渡すといわれたか」
　掛合いにきた目的は達しているのに、こう先方へ要らざる念を押すのも妙なはなしだと思ったが、あまり不審なので、関羽はこう訊ねてみた。
　すると、張世平はいった。
「はははは。あまりさっぱりお渡しするといったので、かえってお疑いとみえますな。いやごもっともです。けれど手前は、第一にまず大人が悪人でないことを認めました。第二に、ご計画の義兵を挙げることは、すこぶる時宜をえておると存じます。第三は、あなた方のお力をもって、自分らの恨みをはらしていただきたいと思ったからです」
「恨みとは」
「黄巾賊の大将張角一門の暴政に対する恨みでございます。手前も以前は中山で一といって二と下らない豪商といわれた者ですが、かの地方もご承知の通り黄匪の蹂躙にあって秩序は破壊され、財産は掠奪され、町に少女の影を見ず、家苑の小禽すら啼かなくなってしまいました。――手前の店なども一物もなく没収され、あげくの果てに、妻も娘も、暴兵にさらわれてしまったのです」
「むむ。なるほど」
「で、甥の蘇双と二人して、馬商人に身を落し、市から馬匹を購入して、北国へ売りに行こうとしたのですが、途中まで参ると、北辺の山岳にも、黄賊が道をふさいで、旅人の持物を奪い、虐殺をほしいままにしておるとのことに、むなしくまた、この群馬をひいて立ち帰ってきたわけです。南へ行くも賊国、北へおもむくも賊国、こうして馬とともに漂泊しているうちには、ついに賊に生命まで共に奪われてしまうのは知れきっています。恨みのある賊の手に武力となる馬匹を与えるよりも、貴下の如きお志を抱く人に、進上申したほうが、はるかに意味のあることなんです。よろこんで手前がお渡しする気持というのは、そんなわけでございます」
「やあ、そうか」
　関羽の疑問も氷解して、
「では、楼桑村まで、馬をひいて一緒に来てくれないか。われわれの盟主と仰ぐ劉玄徳と仰っしゃる人にひきあわせよう」
「おねがい致します。手前も根からの商人ですから、以上申上げたような理由でもって、無料で馬匹を進上しましても、やはりそこはまだ正直、利益のことを考えておりますからな」
「いや、玄徳様へお目にかかっても、ただ今のところ、代金はお下げになるわけにはゆかぬぞ」
「そんな目先のことではありません。遠い将来でよろしいので。……はい。もしあなたがたが大事を成しとげて、一国を取り、十州二十州を平らげ、あわよくば天下に号令なさろうという筋書きのとおりに行ったらば、私へも充分に、利をつけて、今日の馬代金を払っていただきたいのでございます。私は、あなたの計画を聞いて、これがあなたがたの夢ではなく、わたしども民衆が待っていたものであるという点から、きっと成功するものと信じております。ですから、今日この処分に困っている馬を使っていただくのは、商人として、手前にも遠大な利殖の方法を見つけたわけで、まったくこんなよろこばしいことはありません」
　張世平は、そういって、甥の蘇双と共に、関羽に案内されてついて行ったが、その途中でも、関羽へ対して、こう意見を述べた。
「事を計るうえは、人物はお揃いでございましょうし、馬もこれで整いました。これで一体、あなた方のご計画の内輪には、よく経済を切りまわして糧食兵費の内助の役目をする算数の達識が控えているのでございますか。算盤というものも、充分お考えのうえでこのお仕事にかかっておいででございますか？」

02_09_07_七
　張世平に、そう指摘されてみると、関羽は、自分らの仲間に、大きな欠陥のあるのを見いだした。
　経営ということであった。
　自分はもとより、張飛にも、劉玄徳にも、経済的な観念は至ってない。武人銭を愛さずといったような思想がはなはだ古くから頭の隅にある。経済といえばむしろ卑しみ、銭といえば横を向くをもって清廉の士とする風が高い。一個の人格にはそれも高風と仰ぎうるが、国家の大計となればそれでは不具を意味する。
　一軍を持てばすでに経営を思わねばならぬ。武力ばかりでふくらもうとする軍は暴軍に化しやすい。古来、理想はあっても、そのため、暴軍と堕し、乱賊と終った者、史上決してすくなくない。
「いや、いいことを聞かしてくれた。劉玄徳様にも、大いにそのへんのことをはなして貰いたいものだ」
　関羽は、正直、教えられた気がしたのである。一商人のことばといえども、これは将来の大切な問題だと考えついた。
　やがて、楼桑村に着く。
　関羽はすぐ張世平と蘇双のふたりを、劉玄徳の前につれて来た。もちろん、玄徳も張飛も、張の好意を聞いて非常によろこんだ。
　張は五十頭の馬匹を、無償で提供するばかりでなく、玄徳に会ってから玄徳の人物をさらに見込んで、それに加うるに、駿馬に積んでいた鉄一千斤と、百反の獣皮織物と、金銀五百両を挙げてみな、「どうか、軍用の費に」と、献上した。
　その際も、張はいった。
「最前も、みちみち、申しました通り、手前はどこまでも、利を道とする商人です。武人に武道あり、聖賢に文道あるごとく、商人にも利道があります。ご献納申しても、手前はこれをもって、義心とは誇りません。その代り、今日さし上げた馬匹金銀が、十年後、三十年後には、莫大な利を生むことを望みます。――ただその利は、自分一個で飽慾しようとは致しません。困苦の底にいる万民にお頒ちください。それが私の希望であり、また私の商魂と申すものでございます」
　玄徳や関羽は、彼の言を聞いて大いに感じ、どうかしてこの人物を自分らの仲間へ留めおきたいと考えたが、張は、
「いやどうも私は臆病者で、とても戦争なさるあなた方の中にいる勇気はございません。なにかまた、お役に立つ時には出てきますから」といって、倉皇、何処ともなく立ち去ってしまった。
　千斤の鉄、百反の織皮、五百両の金銀、思いがけない軍費を獲て、玄徳以下三人は、
「これぞ天のご援助」
　と、いやが上にも、心は奮い立った。
　早速、近郷の鍛冶工をよんできて、張飛は、一丈何尺という蛇矛を鍛ってくれと注文し、関羽は重さ何十斤という偃月刀を鍛えさせた。
　雑兵の鉄甲、盔、槍、刀などもあわせて誂え、それも日ならずしてできてきた。

　　日月の旗幟。
　　飛龍の幡。
　　鞍、鏃。

　軍装はまず整った。
　その頃ようやく人数も二百人ばかりになった。
　もとより天下に臨むには足りない急仕立ての一小軍でしかなかったが、張飛の教練と、関羽の軍律と、劉玄徳の徳望とは、一卒にまでよく行きわたって、あたかも一箇の体のように、二百の兵は挙手踏足、一音に動いた。
「では。――おっ母さん。行って参ります」
　劉玄徳は、ある日、武装して母にこう暇を告げた。
　兵馬は、粛々、彼の郷土から立って行った。劉玄徳の母は、それを桑の木の下からいつまでも見送っていた。泣くまいとしている眼が湯の泉のようになっていた。

```
</details>

OpenAIのサイトにあるトークナイザで確認したところ、トークン数は **13,645** 、文字数は **9,625** でした。  
![tokens.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512099/3ae7e237-970d-ab00-66e5-84d2ebc95bd2.png)
参照) [OpenAI API](https://platform.openai.com/tokenizer)  

# 3. LlamaIndexとは
![llamaindex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512099/b6732382-749b-e44d-e815-cb9819f4617b.png)
一言で言うと、「LLMに対して独自または外部のデータを取り込むためのライブラリ」です。  
既にいろいろなサイトで紹介されているため、ココでは概要説明は割愛し、勉強時にお世話になったサイトを列挙します。  
- [LlamaIndexを完全に理解するチュートリアル その１：処理の概念や流れを理解する基礎編（v0…](https://dev.classmethod.jp/articles/llamaindex-tutorial-001-overview/)  
  こちらは中身について網羅的に説明されていて、大変お世話になりました。  
- [LlamaIndex 🦙 0.6.20](https://gpt-index.readthedocs.io/en/latest/)  
  公式ドキュメントです。  
  ぱっと始めるにはちょうどよい分量だと思いますが、内部の仕組みについてもう少し詳しく書いてくれるとうれしいな～という印象です。  
- [LlamaIndexとChatGPT APIを活用して書籍の全文要約を行う｜mah_lab / 西見 公宏](https://note.com/mahlab/n/na949c267951e)  
  LlamaIndexを使った要約が、シンプルに説明されています。  

# 4. LlamaIndexを使った要約の実装
いよいよここからが本題です。  

## 4.1. 実行環境
ローカルPC(Windows11 & Miniconda & Jupyter Notebook)上で実行しています。  
主要ライブラリのバージョンは以下の通りです。  
| Name        | Version |
|:------------|:--------|
| python      | 3.10.9  |
| openai      | 0.27.6  |
| llama_index | 0.6.8   |

※試していたのがおよそ2か月前だったため、LlamaIndexのバージョンはかなり変わっています。(この分野のスピード感はすごいです)

## 4.2. 実行したコード
Jupyter Notebookのコードを簡単な解説付きで載せています。

### (1) ライブラリのインポート
```python
import openai
import os
import logging
import sys
from dotenv import load_dotenv
from llama_index import GPTListIndex, SimpleDirectoryReader, LLMPredictor, ServiceContext
from langchain.chat_models import ChatOpenAI
```
- os, dotenvはOpenAIのAPIキーを環境変数として.envに設定し、そこから読み込むために使用
- sys, loggingはLlamaIndexのデバッグログをJupyter Notebook上で確認するために使用

### (2) OpenAIのAPIキー読込
```python
load_dotenv()
openai.api_key = os.environ.get('OPENAI_API_KEY')
```

### (3) ログ出力設定
```python
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
```

### (4) ServiceContextの設定
```python
llm_predictor = LLMPredictor(
    llm=ChatOpenAI(
        temperature=0,
        model_name='gpt-3.5-turbo',
        streaming=True,
        max_tokens=1024
    )
)
service_context = ServiceContext.from_defaults(
    llm_predictor=llm_predictor,
    chunk_size_limit=1024
)
```
-  **ServiceContext** はLlamaIndexの動作環境や設定を管理するためのクラス
- 今回は言語モデルやチャンクサイズなど、解説に必要な部分のみ設定
-  **temperature(温度)** は、一貫性があり予測可能な結果になりやすい **0** に設定
-  **max_tokens(最大トークン数)** は、デフォルトの **1024** のまま
-  **chunk_size_limit(チャンクサイズ上限)** は、デフォルトの **1024** のまま

### (5) インデックス作成(メモリ上)
```python
documents = SimpleDirectoryReader('input_data').load_data()
index = GPTListIndex.from_documents(documents, service_context=service_context)
```
- input_dataフォルダ配下にあるテキストを全て読込(事前に要約に使用するテキストを格納)
-  **index** は全文を順に要約するのに適した **GPTListIndex(リストインデックス)** を使用  
  - 格納時  
    ![listindex1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512099/c0b18a87-0975-c371-2a14-c933b630d494.png)
  - クエリ時  
    ![listindex2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512099/5c231fad-e643-d0e4-9344-49a3d2ef42f8.png)
  参照) [各インデックスの仕組み - LlamaIndex 🦙 0.6.9](https://gpt-index.readthedocs.io/en/latest/guides/primer/index_guide.html)

デバッグログを確認すると、以下のようにチャンク分割しているのがわかります。  
```
Adding chunk: 02_09_義盟 02_09_01_一 　桃園へ行ってみると、関羽と張飛のふたりは、近所の男を...  
Adding chunk: 「劉備や。皆さんも、もうお支度はよろしいのですか」 　厨に見えなかった母が、いつの間にか、三名...  
:  
Adding chunk: 　雑兵の鉄甲、盔、槍、刀などもあわせて誂え、それも日ならずしてできてきた。  　　日月の旗幟。...  
```

### (6) インデックス作成(ディスク上) ※任意
```python
index.storage_context.persist()
```
- storageフォルダおよび以下の3ファイルを作成
  - vector_store.json
  - docstore.json
  - index_store.json

3ファイルの中身を確認してみます。  
-  **vector_store.json** 
  ```
  {"embedding_dict": {}, "text_id_to_doc_id": {}}
  ```
  今回は **GPTListIndex** であり、ベクトルストアは使用しないため、空っぽとなっていました。  

-  **docstore.json**   
  ```
  {
	"docstore/metadata": {
		"8f3c50b1-fe31-4ef0-94eb-698d4b7f45bc": {
			"doc_hash": "f0101ad844589ac353136bd0ec61b9644b08360876b21faee8cf009103febea6"
		},
		"0817765e-0a8a-47cf-9e00-9b31343233e7": {
			"doc_hash": "5a208156de292f9e1b62d0cafddff8b6e55f237b1ded0fd6239ae80246409ea8"
		},
    	"4c524b7e-47ef-4ca0-a847-3ba201e58827": {
			"doc_hash": "3606b5b462543615831e413c594243633cf7f73182e4aac2628f52e22ec5ccff"
		},
    ：
	},
	"docstore/data": {
		"0817765e-0a8a-47cf-9e00-9b31343233e7": {
			"__data__": {
				"text": "02_09_義盟 02_09_01_一 　桃園へ行ってみると、関羽と張飛のふたりは、近所の男を雇ってきて、園内の中央に、もう祭壇を作っていた。 　・・・(途中省略)・・・ 　三名が、いつでもと答えると、母はまた、いそいそと厨房のほうへ去った。",
				"doc_id": "0817765e-0a8a-47cf-9e00-9b31343233e7",
				"embedding": null,
				"doc_hash": "5a208156de292f9e1b62d0cafddff8b6e55f237b1ded0fd6239ae80246409ea8",
				"extra_info": null,
				"node_info": {
					"start": 0,
					"end": 749
				},
				"relationships": {
					"1": "8f3c50b1-fe31-4ef0-94eb-698d4b7f45bc",
					"3": "4c524b7e-47ef-4ca0-a847-3ba201e58827"
				}
			},
			"__type__": "1"
		},
		"4c524b7e-47ef-4ca0-a847-3ba201e58827": {
			"__data__": {
				"text": "「劉備や。皆さんも、もうお支度はよろしいのですか」 　厨に見えなかった母が、いつの間にか、三名の後ろにきて告げた。 　・・・(途中省略)・・・ 　すべて物事は、体を基とする。体形を整えていないことに成功はあり得ない。",
				"doc_id": "4c524b7e-47ef-4ca0-a847-3ba201e58827",
				"embedding": null,
				"doc_hash": "3606b5b462543615831e413c594243633cf7f73182e4aac2628f52e22ec5ccff",
				"extra_info": null,
				"node_info": {
					"start": 659,
					"end": 1322
				},
				"relationships": {
					"1": "8f3c50b1-fe31-4ef0-94eb-698d4b7f45bc",
					"2": "0817765e-0a8a-47cf-9e00-9b31343233e7",
					"3": "d48c01dc-9061-41d2-9a44-a18e06614c76"
				}
			},
			"__type__": "1"
		},
    ：
  }
  ```
  前半の **"docstore/metadata"** には、分割した各ノードごとにドキュメントIDとハッシュデータが格納されていました。  
  ※先頭のドキュメントIDは先頭を示すだけのダミーデータのようで、ドキュメントIDに該当するノードのデータはありませんでした。  
  後半の **"docstore/data"** には、分割した各ノードごとのテキストのチャンクとその他ノードに関する情報が格納されていました。  
  テキストのチャンクから以下のことが確認できました。  
  1. ちゃんと句点「。」で区切れている  
  1. 各ノードのトークンサイズは **chunk_size_limit** で定義した **1024** より小さくなっている  
    (例えば1つ目のノードは1016トークン、2つ目のノードは945トークン)  
  1. 各ノードで重複部分がある  
    (例えば1つ目のノードは0～749文字目、2つ目のノードは659～1322文字目)  
  
  ここから読み取れることは、 **chunk_size_limit** 以内かつ文章の区切りがよいところで分割し、かつチャンク間で文脈を保持するためにあえて重複をさせている、ということだと思われます。  

-  **index_store.json**   
  ```
  {
	"index_store/data": {
		"db7a929e-74eb-4dcb-a44b-8bfc480c521b": {
			"__type__": "list",
			"__data__": {
				"index_id": "db7a929e-74eb-4dcb-a44b-8bfc480c521b",
				"summary": null,
				"nodes": [
					"0817765e-0a8a-47cf-9e00-9b31343233e7",
					"4c524b7e-47ef-4ca0-a847-3ba201e58827",
					"d48c01dc-9061-41d2-9a44-a18e06614c76",
					"eb85f2ec-8a44-4bfe-8e78-1f06c09a8b88",
					"c68eb697-a6bf-4def-af6f-882ec0029bc5",
					"29862189-3384-44ac-b7f2-a581b0cb8cc2",
					"c4d3ae76-761a-4eff-9148-2ec33d32402d",
					"48084af9-6fd8-4a1f-82da-f5db4b431550",
					"c5a49506-301d-4e47-a4bc-a7455f934765",
					"08f27cb8-30fa-4cda-bc57-b41f76a9c5f7",
					"3b803a24-b550-49b6-b629-07d418ff8727",
					"13930c06-9f5f-418e-9778-680c1742b63c",
					"0d201c52-b9fa-4d21-982b-64118b5a0bc0",
					"8f302eab-feb0-4eff-b0cc-0852955f2ec3",
					"e9fd3863-4f2a-45e0-bd44-75fd23e7f4a1",
					"61058716-72ff-4ac5-87ac-8a90668b4ab3",
					"1e9f248b-ad70-4bd8-ab81-17cb6154f928"
				]
			}
		}
	}
  }
  ```
   **"nodes"** にdocstore.jsonで出てきた各ノードのドキュメントIDが列挙されていました。

### (7) クエリ実行(シンプルに要約させてみる)
```python
query_engine = index.as_query_engine(
    response_mode='tree_summarize'
)
response = query_engine.query('この文書を500文字程度で要約してください。')
```
-  **response_mode** は要約するのに適した **tree_summarize** を使用

デバッグログを確認すると、以下のようにノードごとに要約を繰り返していることがわかります。  
```
DEBUG:llama_index.llm_predictor.base:劉備、関羽、張飛の三人は、桃園で祭壇を作り、神に祈りを捧げた。関羽は、三人が一つの軍として行動するためには、体を整える必要があると説明し、張飛も同意した。そして、劉備を主君と仰ぐことを誓い、義盟を結んだ。周囲の人々も手伝い、豪華な料理や卓が用意された。劉備は、自分の母親に感謝し、鴻芙蓉のことを思い出した。関羽は、三人が正義と報国を奉じ、主君を持つことが重要であると説明した。張飛も同意し、劉備を主君と仰ぐことを誓った。  
DEBUG:llama_index.llm_predictor.base:劉備、関羽、張飛の三人が、兵を募り黄巾賊を追い払い、正しい税と食物を収穫するために戦争をすることを決める。募った若者輩は、給料も食物もなく、お互いにある物を分けて喰い、戦争をすることになる。しかし、食糧が不足しているため、一刻もはやく戦争をしなければならない。ある日、何者かが何十頭もの馬を連れて峠を越えてくるという報告が入る。
:  
DEBUG:llama_index.llm_predictor.base:劉玄徳は、張飛の教練と関羽の軍律によって訓練された小軍を率いて、天下に挑戦する。彼らは一体となって行動し、劉玄徳の徳望も兵士たちに浸透していた。ある日、劉玄徳は武装して母に暇を告げ、兵馬は彼の郷土から立ち去った。劉玄徳の母は、それを見送り、涙を堪えていた。 
```
デバッグログの最後の方を確認すると、全体の要約のようなものができています。
```
DEBUG:llama_index.indices.response.response_builder:> Refined prompt template: Human: この文書を500文字程度で要約してください。
AI: 劉備、関羽、張飛の三人は、義盟を結び、人々を助け、目標を達成するために協力することを誓います。彼らは、忠誠心、兄弟愛、そして目的に対する献身を強調し、酒を飲んで祝います。しかし、張飛は酔って寝てしまいます。翌日、彼らは自分たちの理想を実現する方法について話し合います。この文書は、忠誠心、兄弟愛、そして目的に対する献身の重要性を強調しています。また、馬商人から馬を手に入れることを提案し、商人たちは義兵を支援することを決め、馬を提供することで、劉備の軍隊に資源を提供する方法についても触れています。
```
なお、ChatGPT+LlamaIndexの要約結果を見る前に自分で要約した内容を章ごとにまとめました。（全638文字）  
国語は全く得意ではないですが、この後出てくる要約結果との比較に使います。
```
一
　桃園で関羽、張飛が祭壇の準備を行い、劉備の母が近隣の住人達と共に酒や豪華な食材を運んできた。
　全ての準備が終わると、劉備、関羽、張飛の三名は祭壇の前に座った。
二
　関羽が大事を成すために劉備を主君とすることで軍の体を備えるべきだと提案し、張飛も同意した。
　劉備はしばらく考えた末、三人が義兄弟の契りを結ぶことを提案し、関羽、張飛は同意した。
三
　劉備、関羽、張飛は兄弟の杯を交わし、三人一体で協力して万民の塗炭の苦を救うことを誓いあった。
　張飛は上機嫌で大酒を飲み、劉備の母も連れて調子よく語り、その後真っ先に酔いつぶれた。
四
　翌日、劉備の提案で最初に兵を募ることを決め、関羽が檄文を作成することになった。
　檄文を近郷へ飛ばしたところ、次々と壮士が集まるようになってきた。
五
　四、五日のうちに八十人が集まったが、すぐに食糧の確保に困り出した。
　ある日部下から何十頭もの馬が通るとの報告を受け、関羽が即座に掛け合いに行ったところ、馬を譲り受けることになった。
六
　関羽が馬商人の張世平に快諾した理由を尋ねたところ、黄巾賊に家族や財産を略奪された恨みを晴らしてもらうためという答えだった。
　その後、張世平とその甥の蘇双を劉備に引き合わせることとなった。
七
　張世平は劉備の人物を見込み、鉄一千斤、百反の獣皮織物と、金銀五百両を献上したため、それらを元手に軍装を整えることができた。
　二百人の兵と共に劉備は母に別れを告げ、郷土から旅立っていった。
```

### (8) レスポンス内容出力(要約結果の出力)
```python
print(response)
```
出力結果は、まさかの英語でした・・・(ここに来て) :sob:  
```
This document emphasizes the importance of loyalty, brotherly love, and dedication to a common goal. It also touches on the importance of economic management in running a successful army. Zhang Shiping offers to provide horses and resources to Liu Bei's army in exchange for future profits, which Liu Bei's army uses to quickly equip themselves with weapons and armor. Liu Bei and his companions realize their lack of economic knowledge and seek to improve it. The army is now composed of around 200 soldiers, who are well-trained and united in their actions.
```
念のため、Google翻訳で日本語に翻訳してみます。(見やすくするため、文末に改行を入れています)  
```
この文書は、忠誠心、兄弟愛、共通の目標への献身の重要性を強調しています。
また、軍隊を成功させるための経済管理の重要性についても触れています。
張石平は、将来の利益と引き換えに馬と資源を劉備軍に提供することを提案し、劉備軍はそれを使って武器や鎧を迅速に装備します。
劉備と彼の仲間たちは自分たちの経済知識の欠如を認識し、それを改善しようと努めます。
現在、軍隊は約 200 人の兵士で構成されており、よく訓練され、行動において団結しています。
```
日本語の質問に対して英語で返ってくるのはたまにある現象ですが、デバッグログの途中経過の内容より明らかに劣る印象です。  
最後の最後でChatGPTの判断で英語回答になったことで、要約の質も残念な結果となってしまいました。  
このままではすっきりしないので、日本語で出力する旨を伝えて試してみました。  

### (9) 日本語で出力するよう強制する形で、再実施
```python
query_engine = index.as_query_engine(
    response_mode='tree_summarize'
)
response = query_engine.query('この文書を500文字程度で要約し、必ず日本語で回答してください。')
print(response)
```
出力結果は、以下のようになりました。(見やすくするため、文末に改行を入れています)
```
劉備、関羽、張飛の三人は、義盟を結び、天下の塗炭を救い、永遠の平和と民福を計ることを目指していた。
彼らは兵を募り、黄巾賊を追い払い、正しい税と食物を収穫することで、自分たちの理想を実現するために行動を起こすことを決意した。
募った若者輩は、元気に兵隊となって命に服し、約七、八十人も集まった。
しかし、すぐに食糧不足に陥り、戦争を始めなければならなくなった。
ある日、馬商人の一隊に出会い、馬を無料で提供してくれることになった。
馬商人たちは、義挙の目的に賛同し、馬を提供することに同意した。
関羽は、代金を下げることはできないと伝えた。
また、劉備たちは、鍛冶工を呼んで武器を作り、軍装を整え、劉玄徳の徳望により200人の兵士を集めた。
彼らは一つの体のように動き、劉玄徳の母に暇を告げ、兵馬は彼の郷土から立ち去った。
```
こちらの要約は353文字と指定した長さよりはかなり少なめでしたが、内容についてはなかなかの出来となりました。  
すごいと感じたところは、話の流れや文脈的にそれほど重要でない描写を、ばっさりとカットしているところです。  
自分の要約は各節を平均的に圧縮しただけだったので、その点ではChatGPT&LlamaIndexの方が優れていると感じました。

# 5. LlamaIndexを使った要約の解説
ようやくですが、ココからが要約の解説になります。  
ソースやドキュメントを見たり、ChatGPTプラグインに解説してもらったりと、ココの理解が一番苦労しました。  

要約の流れは以下のようになっていました。  
1. 事前に分割した全てのテキストチャンクを結合し、1つのテキストを作成  
1. このテキストを再度テキストチャンクに分割し、インデックスを作成  
   イメージとしては、以下のようなツリー構造のインデックスになる  
   ![tree_index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512099/258b423e-ce95-f7e7-3b40-72311fcdeebc.png)
   上記のイメージでは **子ノード(Child Node)** は3個になっているが、実際には **10** 個作られる(内部のパラメータで`num_children=10`となっているため)  
   さらに1つのノードのサイズが **1,024** トークンを超える場合、そこから **孫ノード(Grandchild Node)** が複数作られる(`chunk_size_limit=1024`となっているため)  
   今回は13,000以上のトークンのテキストを扱っているため、子ノードが10個、さらに孫ノードも複数個あるような構造になっている想定  
1. ノードのリストをソートし、その純に各ノード毎に要約を生成  
1. 最後に各ノードの要約を組み合わせて全体の要約を生成  

今回は **tree_summarize** というレスポンスモードを試しましたが、 **simple_summarize** でも要約ができます。  
前者はツリー構造でチャンク分割し、ツリーの各レベルで個別に要約を行い、それを上位のレベルでさらに要約する、というものでした。  
後者は **chunk_size_limit** に収まるようにフラットにチャンク分割し、それぞれ簡潔に要約する、というものになるようです。  
こちらは今回は試してはいませんが、用途によって使い分けるのが良さそうです。  

# 6. おわりに
今回はChatGPT＋LlamaIndexを使って長文の要約を試してみました。  
LlamaIndexはかなり簡単に利用できてとっつきやすい印象を受けました。  
一方で、内部でどのような処理が行われているかを理解するのはなかなか大変でした。(と言っても概要を把握できたくらいですが)  
次は同様の組み合わせで長文テキストからの議事録作成を試してみたいと考えています。  
1時間の会議になると、軽くトークン制限を超える量のテキストになるため、実用的になるのではないかと思います。