生成AIと乱読してみた｜LangGraph＋GPT-4.1で10冊一気読み

# はじめに

乱読とは、ジャンルに関わらずあらゆる本を好奇心のままに読むことです。

外山滋比古さんの『乱読のセレンディピティ』では、科学・哲学・宗教などジャンルを超えて色んな本を読むことで教養が身につくとともに、分野を横断することでまだ誰も気づいていない発見(セレンディピティ)があると主張されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/45b04c27-f54b-4a86-b884-f99c263ce016.png)


本が大量に出版されている現代、どんどん乱読することはどのような仕事をしていても価値があることだと思います。

しかし、いざ本を手にとってみても、最初の数ページしか読み進められず、積読となってしまうことが多々あります。特に、タイトルで興味がそそられたものの、文章が難しかったり、自分の仕事と関係ないような本は積読になりがちです。

生成AIに助けてもらうことで、今までは全体の1%くらい読んで挫折していた本を10%程度だけでも理解できるようになれば、かなり革新的だと思います。
こういったモチベーションで、自分にとって難しい本でも生成AIに分かりやすく説明してもらうことで、乱読する方法を考えました。

# ステップ1: 古本を10冊買う

古本屋の100円コーナーで10冊の新書を買いました。**内容は一切見ずに、タイトルだけで興味を持ったものを選びました。**

ざっくりとした内容は以下です。ジャンル・出版された年はバラバラです。まさしく乱読です。

|出版年代 | 内容 | ジャンル |
|----|----|----|
|1990年代前半|水と環境|自然科学|
|1990年代前半|摩擦について|自然科学|
|1990年代前半|夏目漱石の批評|文学|
|1990年代後半|名前と社会|人文科学|
|1990年代後半|胃について|医学|
|2000年代前半|水について|自然科学|
|2000年代後半|Web2.0|IT|
|2010年代前半|多数決について|社会科学|
|2010年代後半|アジアの市場|経済|
|2010年代後半|研究者の海外での就職|キャリア|

# ステップ2: スキャン+OCR

生成AIにinputできるように、10冊の本をスキャンし、PDFに変換します。OCR処理も施しておきます。(※私的利用目的のみ)
**この時点でも、一切本の中には目を通していません。**

# ステップ3: 生成AIと乱読

本題です。生成AIと本を読んでいきます。
ただ要約してもらうだけだと、頭に入ってきにくいと思ったので、「博士と学生の対話」形式で説明してもらうことにしました。

今回の取り組みでは、Open AIのAPI GPT4.1 + LangGraphを使います。
pdf全体をGPTに渡すとトークン数が多すぎるので、LangGraphを使ってpdfを章ごとに分割して渡します。分割=>対話の作成まで一気にできるように実装しました。
※もちろん、ChatGPTのWeb版や、他のLLMでも同様に読めると思います。

## LangGraphの実装

### ポイント1: tool_callingで元のpdfを分割する

split_pdf_by_pagesという関数をgptがtool_callingで呼び出せるようにします。
"filepath: ./スキャンしたpdf1.pdf　, 1章: 33-, 2章: 77-150"のようなメモ書きをプロンプトとして渡すと、split_pdf_by_pagesの引数をgptが決めてくれます。こちらでは、提案された引数を実行する処理を書いておきます。

```python
@tool
def split_pdf_by_pages(input_pdf_path: str, page_ranges: list[list[int]], output_dir: str):
    """
    Splits a PDF file into multiple PDFs based on the specified page ranges.

    Args:
        input_pdf_path (str): Path to the input PDF file.
        page_ranges (list of tuple[int, int]): A list of (start_page, end_page) tuples.
            Page numbers are 1-based and inclusive.
        output_dir (str): Directory to save the output PDF files.

    Raises:
        ValueError: If a page range is invalid.
    """
    # Ensure output directory exists
    os.makedirs(output_dir, exist_ok=True)

    # Load the input PDF
    reader = PdfReader(input_pdf_path)
    total_pages = len(reader.pages)

    for idx, (start, end) in enumerate(page_ranges):
        # Validate page range
        if not (1 <= start <= end <= total_pages):
            raise ValueError(f"Invalid page range: ({start}, {end}). Total pages: {total_pages}")

        # Create a new PDF writer
        writer = PdfWriter()

        # Add pages to the writer
        for page_num in range(start - 1, end):  # 0-based index
            writer.add_page(reader.pages[page_num])

        # Write the split PDF to a file
        output_path = os.path.join(output_dir, f"split_{idx+1}.pdf")
        with open(output_path, "wb") as output_file:
            writer.write(output_file)

        print(f"Saved: {output_path}")
        pdf_file_paths.append(output_path)
    
    return 
```

### ポイント2: 分割したpdfの内容をそれぞれ"博士と学生の対話"にしてもらう

対話への変換用システムプロンプトとして、以下を作りました。博士と学生のそれぞれのセリフに絵文字をつけさせることで、読みやすくします。

```
あなたは作家です。オリジナル作品をもとに、話のスタイルを変換することができます。
あなたはPDFで渡された本の内容を、博士と学生の対話形式でストーリー仕立てにして、詳しく説明する役割を担います。

ルール
    * 説明はただ文章を羅列するだけではなく、会話として流れが滑らかに以降するように説明してください。
    * 元の作品を全く読まなくても理解できるようにしてください。
    * 出力は博士と学生の目印として絵文字を仕様してください。
    * どのページを参考にしたかが明らかな場合、セクションのタイトルに参考ページ数を書いてください。
    * 学生は入学したばかりで、この分野に関して全くの無知です。
    * 博士は説明するのが上手く、相手のレベルに合わせた言葉選びができます。難しい言葉に適宜説明を加えながら解説できます。
    * 詳細に記載してください。目安は2000語です。

例:
    # {タイトル}

    ## {セクションのタイトル}{参考にしたページが分かる場合は参考ページを記載}

    🧑‍🏫博士: {博士のセリフ}
    👦学生: {学生のセリフ}
```

### 完成品

<details><summary>コード</summary>

```python
from typing import Literal
from langchain_core.messages import ToolMessage, SystemMessage, HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.types import Command
import base64
from langchain_core.callbacks import UsageMetadataCallbackHandler
import json
from openai import RateLimitError
import time

import os
from PyPDF2 import PdfReader, PdfWriter

model = ChatOpenAI(model="gpt-4.1", max_tokens=3000)

pdf_file_paths = []

@tool
def split_pdf_by_pages(input_pdf_path: str, page_ranges: list[list[int]], output_dir: str):
    """
    Splits a PDF file into multiple PDFs based on the specified page ranges.

    Args:
        input_pdf_path (str): Path to the input PDF file.
        page_ranges (list of tuple[int, int]): A list of (start_page, end_page) tuples.
            Page numbers are 1-based and inclusive.
        output_dir (str): Directory to save the output PDF files.

    Raises:
        ValueError: If a page range is invalid.
    """
    # Ensure output directory exists
    os.makedirs(output_dir, exist_ok=True)

    # Load the input PDF
    reader = PdfReader(input_pdf_path)
    total_pages = len(reader.pages)

    for idx, (start, end) in enumerate(page_ranges):
        # Validate page range
        if not (1 <= start <= end <= total_pages):
            raise ValueError(f"Invalid page range: ({start}, {end}). Total pages: {total_pages}")

        # Create a new PDF writer
        writer = PdfWriter()

        # Add pages to the writer
        for page_num in range(start - 1, end):  # 0-based index
            writer.add_page(reader.pages[page_num])

        # Write the split PDF to a file
        output_path = os.path.join(output_dir, f"split_{idx+1}.pdf")
        with open(output_path, "wb") as output_file:
            writer.write(output_file)

        print(f"Saved: {output_path}")
        pdf_file_paths.append(output_path)
    
    return 

def convert_pdf_to_base64(pdf_path):
    """
    Convert a PDF file to a Base64 encoded string.

    :param pdf_path: path to the pdf file
    :return: Base64 string
    """
    with open(pdf_path, "rb") as f:
        pdf_bytes = f.read()
    pdf_str = base64.b64encode(pdf_bytes).decode("shift-jis")
    return pdf_str

def pdf_cut_agent(
    state: MessagesState,
) -> Command[Literal["summary_pdfs_agent", "__end__"]]:
    system_prompt = (
        "Your role is splitting a pdf into each section by message contents."
        "The saved directory name should be based on the original pdf name."
    )
    messages = [SystemMessage(system_prompt)] + state["messages"]
    ai_msg = model.bind_tools([split_pdf_by_pages]).invoke(messages)
    split_pdf_by_pages.invoke(ai_msg.tool_calls[0])
    print(ai_msg)

    return {"messages": ""}

def summary_pdfs_agent(
    state: MessagesState,
) -> Command[Literal["__end__"]]:
    system_prompt = (
        """
        あなたは作家です。オリジナル作品をもとに、話のスタイルを変換することができます。
        あなたはPDFで渡された本の内容を、博士と学生の対話形式でストーリー仕立てにして、詳しく説明する役割を担います。
        
        ルール
            * 説明はただ文章を羅列するだけではなく、会話として流れが滑らかに移行するように説明してください。
            * 元の作品を全く読まなくても理解できるようにしてください。
            * 出力は博士と学生の目印として絵文字を仕様してください。
            * どのページを参考にしたかが明らかな場合、セクションのタイトルに参考ページ数を書いてください。
            * 学生は入学したばかりで、この分野に関して全くの無知です。
            * 博士は説明するのが上手く、相手のレベルに合わせた言葉選びができます。難しい言葉に適宜説明を加えながら解説できます。
            * 詳細に記載してください。目安は2000語です。

        例:
            # {タイトル}

            ## {セクションのタイトル}{参考にしたページが分かる場合は参考ページを記載}

            🧑‍🏫博士: {博士のセリフ}
            👦学生: {学生のセリフ}
        """
    )

    for pdf_file in pdf_file_paths:
        messages = [SystemMessage(system_prompt),HumanMessage(
                content=[
                    {
                        "type": "file",
                        "file": {
                            "filename": pdf_file,
                            "file_data": f"data:application/pdf;base64,{convert_pdf_to_base64(pdf_file)}"
                        }
                    },
                ]
            )]
        callback = UsageMetadataCallbackHandler()

        # 1minあたりの上限を超えた場合、1min待ってから再挑戦。2回失敗したら終了
        retry_count = 0
        while retry_count < 3:
            try:
                ai_msg = model.invoke(messages, config={"callbacks": [callback]})
                break
            except RateLimitError:
                retry_count += 1
                time.sleep(60)
        # 出力結果はmarkdownで保存
        with open(pdf_file + "_summary.md", mode="w") as f:
            f.write(ai_msg.content)
        # トークン数などのメタ情報はjsonで保存
        with open(pdf_file + "_result.json", mode="w") as f:
            json.dump(callback.usage_metadata, f, indent=4, ensure_ascii=False)

    return {"messages": [ai_msg]}



workflow = StateGraph(MessagesState)

workflow.add_node("pdf_cut_agent", pdf_cut_agent)
workflow.add_node("summary_pdfs_agent", summary_pdfs_agent)

workflow.add_edge(START, "pdf_cut_agent")
workflow.add_edge("pdf_cut_agent", "summary_pdfs_agent")
workflow.add_edge("summary_pdfs_agent", END)


app = workflow.compile()

res = app.invoke(
    {"messages":"""
    filepath: ./水に関する本.pdf
    0章: 15-
    1章: 33-
    2章: 77-
    3章: 119-
    4章: 155-
    5章: 196-223
    """})
```
</details>

グラフはシンプルです。app.invoke内にpdfのファイルパスと各章のページ情報を記載しておくだけで、対話の生成まで完了します。
```mermaid
---
config:
  flowchart:
    curve: linear
---
graph TD;
        __start__([<p>__start__</p>]):::first
        pdf_cut_agent(pdf_cut_agent)
        summary_pdfs_agent(summary_pdfs_agent)
        __end__([<p>__end__</p>]):::last
        __start__ --> pdf_cut_agent;
        pdf_cut_agent -.-> summary_pdfs_agent;
        summary_pdfs_agent --> __end__;
        classDef default fill:#f2f0ff,line-height:1.2
        classDef first fill-opacity:0
        classDef last fill:#bfb6fc
```

## 使用例

著作権の問題で、実際の本を対話形式に変換したものは掲載できないため、代わりにデジタル庁の資料で実行した結果を載せます。

入力したpdf: 「Web3.0 研究会報告書」（D庁）（[リンク](https://www.digital.go.jp/assets/contents/node/basic_page/field_ref_resources/a31d04f1-d74a-45cf-8a4d-5f76e0f1b6eb/a53d5e03/20221227_meeting_web3_report_00.pdf)）

LangGraphで以下を実行: 
```python
res = app.invoke(
    {"messages":"""
    filepath: ./web3_report.pdf
    1章: 6-29
    """})
```

出力結果:
```markdown
# Web3.0の基礎と社会的意義（p6-12）

## 総論：Web3.0とは？そしてなぜ注目されているのか（p6-8）

🧑‍🏫博士: さて、今日は「Web3.0」と呼ばれる新しいテクノロジーの波について一緒に学んでいこう。まず、君は
Web3.0という言葉は聞いたことあるかい？

👦学生: 名前くらいは…でも、何が新しいのか、よく分かってません。普通のインターネットと違うんですか？

🧑‍🏫博士: いい質問だ！Web3.0とは、ざっくり言えば「より分散されたインターネット」のことだ。Web1.0は
ただの静的な情報のやりとり、Web2.0でSNSやクラウドを通じ皆が情報発信できるようになった。Web3.0はさら
に進んで、データやサービスの「主役」が特定の大企業からネットワーク参加者全体に分散し、皆が直接価値を
やり取りできる世界を指す。

👦学生: 分散って、どういうことですか？

🧑‍🏫博士: 「銀行」や「SNS」など今まで一箇所に情報や権限が集中していたものが、ブロックチェーン等の技術
で世界中に分散され、みんなが直接やりとりしたり合意形成したりできる仕組みだと考えてごらん。代表的な
技術・サービスとしては、「暗号資産（仮想通貨）」「NFT」「DAO」「メタバース」などがあるんだよ。

👦学生: たしかに最近耳にします。でも、これが社会にとってどんな意味があるんでしょう？

🧑‍🏫博士: 例えば金融なら、「今までは銀行経由でしか送金できなかった」のが、暗号資産なら世界中どこと
でも、“自分と相手”だけで安全にやりとりできる。資産や権利をデジタル化して、個々人が自由に管理・交換
できると期待されている。さらに、組織もDAOのように「ネットで自律的に意思決定する集団」が生まれている。
今後は、個人同士や地域、クリエイター、企業が自由につながり、新しい共創や経済価値の創出が考えられて
いるのさ（p6-7参照）。

👦学生: すごい！じゃあ、こうした技術ってどんどん便利になるんですか？

🧑‍🏫博士: ただし、どのイノベーションにも「銀の弾丸（＝万能の解決策）」はない。新技術は便利さと同時に
リスクも抱えている。例えば、詐欺やセキュリティのリスク、法律が追いつかない問題、匿名性ゆえの責任の
所在などだね（p7-8より）。だからこそ、夢だけでなく課題にも目を向ける必要がある。

---

## 未来像と課題、制度の方向性（p8-12）

🧑‍🏫博士: それじゃあ、このWeb3.0はどんな未来を目指しているのか見てみよう。

👦学生: 未来像って、例えばどんな世界ですか？

🧑‍🏫博士: ブロックチェーン等の非中央集権型技術で「個人と個人が自由につながる世界」だね。クリエイターと
ファンが直接価値をやりとりしたり、知的財産を安全に二次利用できたりと“新しい共創”が生まれると期待されて
いる（*p8-9*）。さらには、仮想空間（メタバース）で現実世界とネット上の価値が交差する。例えば、アート
のNFTを世界中の誰かが購入し、その収益が即座に作者へ分配されたりね。

👦学生: 面白そうですね。でも、法律とかルールはどうなります？

🧑‍🏫博士: そこが最大の課題！これまで国ごとに決めていた規則が、国境や組織の壁を超えて運用されるので、
「グローバル共通のルール」作りが必要なんだ。NFTやDAOのように、匿名性や自律性が高い世界では、「誰が
責任持つの？」「不正が起きたら？」……といった難しい課題が次々に出てくる（p9-10）。

👦学生: どうやって対策を？

🧑‍🏫博士: 大事なのは、関係者が常に最新の問題意識を共有して、法律だけでなく柔軟なルール（ソフトロー）も
作ること。それに加えてOODAループ（状況を観察→方向づけ→決断→行動→また観察…）という方法で、変化に合わ
せて素早く改善していく（*p10-11*参照）。

👦学生: じゃあ、日本の取り組みは？

🧑‍🏫博士: 日本でも「デジタル庁」の主導で、自治体や事業者からの相談窓口や連絡会議を設けたり、国際的な
カンファレンスを通じて、世界と協力してルール作りや技術開発を進めている。特に、文化・コンテンツ領域で
強みを生かしたNFTやDAOのクリエイター支援も重視しているよ（*p11*より）。さらに、研究開発やセキュリティ
の取り組みとともに、グローバルなネットワーク（BGIN等）への積極的参加にも力を入れている。

---

## まとめ：Web3.0時代の社会

🧑‍🏫博士: 最後にまとめよう。Web3.0は社会の基盤そのもの――金融や組織運営、価値創造――をアップデートしよう
とする壮大な挑戦だ。便利さや新しい価値創造の可能性が広がる一方、リスクやルール作りの課題も山積みだ。
だからこそ、一歩一歩試行錯誤を重ねながら、社会にとって本当に役立つ仕組みかを見極めていく必要があるんだ。

👦学生: なるほど、“話題だから”だけじゃなくて、本当に社会に根ざすための準備と議論が大事なんですね。

🧑‍🏫博士: その通り！君たち新世代にも、自分たちで考え、作り、守る主体となってほしい。Web3.0の本質は
「みんなで作り上げていく、オープンで参加型の未来」なのだから。

---

以上がWeb3.0の基礎と、その社会的意義・課題に関する現時点のまとめだ。この他にも、NFTやDAO、デジタル
資産など個々の具体的な話題も今後解説していくので、楽しみにしていてくれたまえ！
```

このような形で出力されます。スキャンした本のpdfでも同様の形になりました。参考にしたページ数を載せるようにシステムプロンプトを記載したため、本文と行き来しながら読むこともできます。

## (参考)トークン数・api料金

gpt4.1のapiを使って10冊の本に対して実行したときのトークン数は以下でした。

|本の内容|入力トークン数|出力トークン数|api料金($)|
|---|---|---|---|
|水と環境|151052|12310|0.401|
|摩擦について|141490|11300|0.373|
|夏目漱石の批評|138936|8098|0.343|
|名前と社会|136016|9326|0.347|
|胃について|134976|14209|0.384|
|水について|133816|13353|0.374|
|Web2.0|158638|18623|0.466|
|多数決について|112886|10486|0.310|
|アジアの市場|140489|11666|0.374|
|研究者の海外での就職|135755|11933|0.367|

※ gpt4.1の料金はインプット:2.00＄/100万トークン, アウトプット:8.00＄/100万トークン で計算

新書なので、どの本もページ数は200-250ページです。だいたい1冊あたり0.35$(現在のレートだと50円程度)でした。
今回は愚直にpdfをバイナリ化して添付し、通常のapiでリクエストを送っていますが、file apiやbatch apiを活用すればもっと安くなるはずです。
gpt4.1miniでもそれなりの質のアウトプットが得られるかもしれません。


# 10冊乱読してみた感想

gptに変換してみた後に、本文もちらっと読んでみましたが、中には難しくて全然進まない本もありました。こういった本の内容をざっくりとでも理解できるようになったのは、画期的なことだと思います。今まで手に取らなかったような本でも、AIの助けを借りて手に取れるようになると思うと、かなりの収穫です。"セレンディピティ"を生む可能性を高められるかもしれません。

また、10冊のうちの1冊(水についての本)では"半導体製造には超純水が必要。水だからといって人が飲めるわけではなく、全く美味しくないし、場合によっては死に至る"という解説がありました。気になったので、gptの出力に載っている参考ページをもとに、本文に戻って周辺を読みました。本文では中盤にこの話が出ていたのですが、私一人で読んでいたら中盤にたどり着く前に挫折していたと思うので、この内容と巡り合うことはなかったと思います。

皆さんも是非乱読してみてください。