Ollama + Qwen3でローカル要約アプリを作ってみた
はじめに
せっかく少し奮発して性能の良いPCを買ったので、以前から気になっていたローカルLLMを試してみることにしました。
ChatGPTやClaudeなど高性能なクラウド型LLMがある中で、ローカルLLMを使うメリットは主に次の2つだと考えています。
-
秘匿性
入力した文章を外部の生成AIサービスへ送信せず、ローカル環境内で処理できる -
外部サービスに依存しないこと
インターネット接続や外部サービスの障害などに左右されにくい
特に業務文書などでは、
内容を短時間で把握したいが、そのまま外部の生成AIへ送るのは難しい
という場面があります。
そこで今回は、ローカル環境でLLMを動かせる Ollama と Qwen3 を使い、文章を要約するWebアプリを作ってみました。
最初は、
ローカルLLMは、自分で追加学習や細かなチューニングをしないと実用にならないのでは
と思っていました。
しかし実際に試してみると、特別なファインチューニングを行わなくても、一般的な文章や技術資料の要約であれば思った以上に使えることが分かりました。
そこから少しずつ機能を追加し、現在は以下まで対応しています。
- テキスト要約
- PDF読み取り
- スキャンPDF・画像のOCR
- 長文の分割要約
- Qwen3 8B / 14Bの切り替え
- 要約形式や要約粒度の切り替え
この記事では、アプリを作った流れと、実際に使ってみて分かったこと、現在見えている課題についてまとめます。
使用環境
今回使用したPCは以下です。
| 項目 | 内容 |
|---|---|
| OS | Ubuntu 24.04.4 LTS |
| GPU | NVIDIA GeForce RTX 5070 |
| VRAM | 約12GB |
| RAM | 62GB |
| CPU | Intel Core Ultra 9 285 |
| ストレージ | 約774GB空き |
使用した主な技術は以下です。
| 用途 | 技術 |
|---|---|
| ローカルLLM | Ollama |
| 使用モデル | Qwen3 8B / 14B |
| バックエンド | Python / FastAPI |
| HTML生成 | Jinja2 |
| Ollama API通信 | httpx |
| PDFテキスト抽出 | PyMuPDF |
| OCR | NDLOCR-Lite |
| フロントエンド | HTML / CSS / JavaScript |
まずOllamaでQwen3を動かしてみる
まずはOllamaを導入し、Qwen3 8Bを動かしてみました。
ollama run qwen3:8b
最初は日本語で普通に会話できるか、簡単なコードを説明できるか、といったところから試しました。
例えば、
def add(a, b):
return a + b
の意味と改善点を質問すると、
- コードの説明
- 型ヒント
- docstring
- 入力チェック
などを日本語でかなり自然に返してきました。
細かい部分では、入力に存在しない内容を補ってしまうこともありましたが、
これだけ簡単にローカルでここまで動くのか
というのが最初の感想でした。
自分の中では、
ローカルLLM = 自分でかなり教育しないと使えない
というイメージがあったのですが、少なくとも簡単な技術相談や文章処理であれば、既存モデルをそのまま利用するだけでもかなり使えそうでした。
Qwen3 14Bも試す
次に14Bモデルも試しました。
ollama run qwen3:14b
VRAMが約12GBなので重すぎるのではと思っていましたが、要約用途では十分現実的に動作しました。
自分の環境では、おおまかに
- 8B:高速で普段使いしやすい
- 14B:少し重いが、文章整理などでは品質を重視できる
という印象です。
そのため、アプリ上でも8Bと14Bを切り替えられるようにしました。
FastAPIでWebアプリ化
Ollama単体でも利用できますが、毎回ターミナルから文章を入力するより、ブラウザから使える方が便利です。
そこでFastAPIを使って簡単なWebアプリにしました。
最初の構成は非常に単純です。
文章入力
↓
FastAPI
↓
Ollama API
↓
Qwen3
↓
要約結果
OllamaはローカルAPIを提供しているため、FastAPIからHTTPで呼び出せます。
実際のコードでは、概ね以下のような形で利用しています。
payload = {
"model": model,
"messages": [
{
"role": "system",
"content": "あなたは日本語文書の要約担当です。",
},
{
"role": "user",
"content": prompt,
},
],
"stream": False,
"think": False,
"options": {
"temperature": 0.2,
},
}
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
"http://127.0.0.1:11434/api/chat",
json=payload,
)
最初は、
- 文章入力
- 要約ボタン
- 結果表示
だけでした。
そこから、
- 8B / 14B切り替え
- 要約形式
- 原文忠実 / 通常 / 補足あり
- 結果コピー
- Markdown保存
- 推論時間・生成速度(tokens/s)の表示
などを追加していきました。
PDFも直接読めるようにする
文章を毎回コピーして貼り付けるのは面倒なので、次にPDFを直接読み込めるようにしました。
通常のPDFであれば、PyMuPDFを使って内部のテキストを抽出します。
PDF
↓
PyMuPDF
↓
テキスト
↓
Qwen3
↓

要約
これだけで、
- 論文
- 技術資料
- 資格試験の資料
- マニュアル
などを直接読み込ませられるようになりました。
スキャンPDFや画像はNDLOCR-LiteでOCR
PDFには、文字情報を持ったPDFだけでなく、紙をスキャンした画像だけのPDFもあります。
この場合PyMuPDFでは本文を取得できません。
そこでOCRには、国立国会図書館が公開している NDLOCR-Lite を使用しました。
現在は以下のような流れです。
PDF
↓
PyMuPDFでテキスト抽出
↓
十分な文字が取れた?
├─ Yes → そのままQwen3
│
└─ No
↓
NDLOCR-Lite
↓
OCRテキスト
↓
Qwen3
画像の場合は直接NDLOCR-Liteへ渡します。
JPG / PNG など
↓
NDLOCR-Lite
↓
テキスト
↓
Qwen3
OCR結果には多少の誤認識があります。
ただし今回の目的は、
一字一句完全に文字起こしすること
ではなく、
文書の内容をLLMに読ませて概要を把握すること
なので、この用途ではかなり実用的でした。
【ここにOCR → 要約結果のスクリーンショット】
長文は分割して要約する
最初は、LLMへ渡す文章を30,000文字程度までに制限していました。
しかし、論文や長い資料を扱うには少し短いです。
単純に上限だけを増やす方法もありますが、文章が長くなるほど重要情報の取りこぼしも起こりやすくなります。
そこで現在は、長文を複数のチャンクへ分割して処理しています。
長文
↓
複数Chunkに分割
↓
Chunkごとに重要情報を抽出
↓
中間結果
↓
最終統合
↓
全体要約
現在の設定では、
- 24,000文字を超えると長文モード
- 1チャンクは約12,000文字
- 約800文字のオーバーラップ
- 段落やPDFページ境界をできるだけ維持
- 文書全体は最大300,000文字
としています。
例えば、約33,000文字のCNN関連論文を入力すると、
33,190文字
3 chunks
階層要約
として処理できました。
単純に巨大なコンテキストへ全文を入れるのではなく、
小さめのローカルLLMでも扱いやすい形へ入力文を分割する
という考え方です。
思ったより「モデルの教育」は必要なかった
今回一番意外だった部分です。
最初は、
- LoRA
- ファインチューニング
- 独自教師データ
といった追加学習の準備や、RAGなどの仕組みも早い段階で必要になると思っていました。
なお、RAGはモデルを追加学習させるものではなく、関連情報を検索して入力に加える仕組みです。
しかし、少なくとも要約用途については、
既存モデル
+
プロンプト
+
PDF / OCRなどの前処理
+
長文分割
だけでもかなり使えました。
今回作っていて感じたのは、
最初からモデルそのものを変更するより、モデルへ何をどう渡すかを整える方が先
ということです。
もちろん用途によってファインチューニングが有効なケースはあると思いますが、今回のような要約アプリでは、まず周辺処理を整えるだけでも、想像以上に使えるものになりました。
実際に使って見えてきた課題
もちろん、何でもきれいに要約できるわけではありませんでした。
技術資料や説明文は比較的安定
論点や構造が明確な文章については、かなり扱いやすい印象です。
特に、
- 技術資料
- 説明文
- 論文
- 業務資料
- 見出しのある文書
などは、内容把握用として十分使えました。
例えばCNNに関する約33,000文字の論文でも、
- R-CNN
- Fast R-CNN
- CNNの可視化
- adversarial example
- 画像生成
- スタイル転移
などの主要テーマを拾って要約できました。
ただし、正式な資料としてそのまま利用するのではなく、
原文を読む前に全体像をつかむ
用途が現実的だと思っています。
小説は意外と難しい
興味本位で小説も入力してみました。
ここでは、技術文書とは違う問題が出ました。
試しに芥川龍之介の『河童』を長文要約させたところ、モデルがなぜか、
太宰治の『河童』
として説明を始めることがありました。
さらに、
- 作者の思想
- 文学的背景
- 哲学的意味
- 象徴の解説
など、本文の要約ではなく文学作品の解説モードへ入る場合もありました。
長文では途中で本文を圧縮しているため、最終統合時にモデル自身の知識で不足部分を補完しようとしている可能性があります。
そこで、
モデル自身の外部知識を使わない
本文に明示されていない作者名を補完しない
不明な情報は不明なまま扱う
といった制約も追加しました。
それでも完全に防げるわけではありません。
この辺りは、単純に
すべての文章を同じ方法で要約する
のではなく、
- 技術文書
- 会議
- 物語・小説
など、文章の種類によって保持すべき情報を変える必要がありそうです。
現在は「物語・小説」モードも追加し、
- 登場人物
- 場所
- 人物関係
- 出来事
- 時系列
- 転換点
- 結末
などを重視する方向で試しています。
「簡潔・標準・詳細」は思ったほど簡単ではない
要約の長さとして、
- 簡潔
- 標準
- 詳細
も選択できるようにしました。
当初は、
「短くしてください」
「詳しくしてください」
程度で十分違いが出ると思っていました。
しかし実際には、簡潔と標準で思ったほど出力量が変わらないことがあります。
例えば「簡潔」に設定していても、モデルが親切に、
- 複数の見出し
- 詳細な技術説明
- 関連キーワード
- 謝辞
まで出してしまうことがあります。
つまり、単に「簡潔」と指示するだけでは弱そうです。
今後は、
簡潔:300〜600文字程度
標準:800〜1,500文字程度
詳細:2,000〜4,000文字程度
のように、具体的な出力量や見出し数まで含めて制御する予定です。
このあたりは、実際に使ってみないと気付きにくい部分でした。
「ご希望があれば~」を付けたがる
もう一つ細かい問題として、
ご希望があれば、さらに詳しく説明できます。
のような文章を付けたがることがあります。
プロンプトでは、
追加提案を書かない
と何度か指定しているのですが、それでも出てくることがあります。
これはプロンプトだけで完全に制御しようとせず、
ご希望があれば~
必要であれば~
さらに詳しく~
のような定型的な末尾をアプリ側で除去する後処理も検討しています。
現時点での感想
今回ローカルLLMを実際に触ってみて、一番感じたのは、
思っていたより簡単に使える。
ただし、実用的なアプリにするにはモデルそのものより周辺処理が重要。
ということです。
今回もモデル自体にはほぼ手を加えず、
テキスト
PDF
画像
↓
テキスト抽出 / OCR
↓
必要なら長文分割
↓
Qwen3
↓
要約
という周辺処理を少しずつ増やしてきました。
これだけでも、最初に想像していたよりかなり実用的なものになりました。
一方で、
- 長文統合時の幻覚
- 小説など特殊な文章
- 要約量の制御
- 不要な付加情報
など、色々と試してみないとわからなかった改善点も見えてきました。
今後
小説の要約や出力量の制御には、まだ課題が残っています。
そのため、音声などの新しい入力形式を増やす前に、まずは、
- 「簡潔 / 標準 / 詳細」の差を明確にする
- 文書タイプごとの要約形式を改善する
- 長文要約の忠実性を高める
- 不要な定型出力を整理する
といった、要約の品質と使いやすさの改善を進める予定です。
当初は音声文字起こしまで対応し、会議録の要約にも使いたいと思っていました。
ただ、音声を文字にできても、長い会話を正しく整理できなければ意味がありません。
そのため、まずは長文要約や文書タイプ別の処理をしっかり作った上で、必要であれば音声入力を追加する方が良さそうだと考えています。
ローカルLLMは、モデルを動かした時点で完成するものではなく、入力の整形や出力の制御まで含めて、初めて使いやすいアプリになると感じました。
GitHub
ソースコードはGitHub(local_summary_app)で公開しています。
現時点では開発途中のため、今後仕様を変更する可能性があります。


