はじめに
最近は ChatGPT や Claude などのクラウド AI が便利ですが、「ノートや社内資料を外部へ送りたくない」「オフラインでも使いたい」という場面も少なくありません。
そこで、RTX 5060 Ti 16GB を搭載した PC 上だけで完結する AI ノートアプリを作ってみました。
LLM には Ollama + qwen3:8b を採用し、Docker 上で推論サーバーを構築。フロントエンドは Electron とし、ブラウザではなくデスクトップアプリとして利用できるようにしています。
単に AI とチャットできるだけではなく、Web ページをローカルへ保存して知識ベース化し、そのノートだけを AI に参照させることで、必要な情報だけを根拠に回答・要約・問題生成を行えるようにしました。また、Mermaid による図解生成や Markdown の安全なレンダリングなど、日常的な学習や情報整理に使いやすい機能も実装しています。
本記事では、開発に当たって拘った内容を中心に紹介します。
「クラウドへデータを送らずに AI を活用したい」「自分専用の AI ナレッジベースを構築したい」という方の参考になれば幸いです。
ソースコード
本記事で紹介するプログラムは GitHub で公開しています。
動作確認やカスタマイズを行いたい方は、以下のリポジトリからダウンロードしてください。
GitHub
https://github.com/cherrymoon1217/open_notebook
システム構成
┌──────────────────────────────────────────────────────────┐
│ ホスト PC │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Electron デスクトップアプリ │ │
│ │ │ │
│ │ ┌──────────────┐ ┌────────────────────────────┐ │ │
│ │ │ ノート管理 │ │ ローカル AI チャット │ │ │
│ │ │ URL取込 │ │ Markdown / Mermaid 図解 │ │ │
│ │ │ グループ管理 │ │ 問題生成・学習FB │ │ │
│ │ └──────────────┘ └────────────────────────────┘ │ │
│ └───────────────────────┬────────────────────────────┘ │
│ │ HTTP / 127.0.0.1:8190 │
│ ┌───────────────────────▼────────────────────────────┐ │
│ │ Docker Compose │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────┐│ │
│ │ │ opennotebook-api ││ │
│ │ │ FastAPI ││ │
│ │ │ ││ │
│ │ │ ・ノート CRUD / 検索 ││ │
│ │ │ ・URL 本文抽出 ││ │
│ │ │ ・ノートグループ ││ │
│ │ │ ・定型プロンプト ││ │
│ │ │ ・Ollama への推論要求 ││ │
│ │ └───────────┬───────────────────┬────────────────┘│ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ ┌───────────▼──────────┐ ┌─────▼───────────────┐ │ │
│ │ │ notebook_data volume │ │ opennotebook-ollama │ │ │
│ │ │ │ │ Ollama │ │ │
│ │ │ SQLite │ │ │ │ │
│ │ │ ・ノート │ │ ・qwen3:8b │ │ │
│ │ │ ・グループ │ │ ・ローカル推論 │ │ │
│ │ └──────────────────────┘ └──────────┬───────────┘ │ │
│ │ │ GPU │ │
│ └─────────────────────────────────────────┼──────────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ RTX 5060 Ti 16GB │ │
│ │ NVIDIA Container │ │
│ │ Toolkit │ │
│ └──────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Electron → 127.0.0.1:8190 → FastAPI → Docker 内 Ollama → qwen3:8b
| コンポーネント | 技術 | 役割 |
|---|---|---|
| デスクトップ UI | Electron / HTML / CSS / JavaScript | ノート編集、AI チャット、グループ選択、図解表示 |
| API | FastAPI | ノート管理、URL 取り込み、定型プロンプト適用、LLM 呼び出し |
| データベース | SQLite | ノート、グループ、グループとノートの対応を保存 |
| LLM 実行基盤 | Ollama | ローカルモデルの管理と推論 API |
| ローカル LLM | qwen3:8b | 質問応答、要約、問題作成、フィードバック、Mermaid 図生成 |
| コンテナ管理 | Docker Compose | API・Ollama・永続データをまとめて起動 |
| GPU 実行環境 | NVIDIA Container Toolkit | Docker コンテナから RTX 5060 Ti を利用 |
URL 取り込み時だけの例外的な外部通信
ユーザーが指定した URL
↓
FastAPI が本文を取得
↓
本文を SQLite にローカル保存
↓
以後の質問・要約・問題生成は完全ローカル
開発でこだわったポイント
完全ローカル完結の構成
本アプリは、ノート保存・AI 推論・問題生成・Mermaid 図解・Markdown 描画までをすべて PC 内で完結する構成です。
- LLM は Docker 上の Ollama(qwen3:8b) を利用
- ノート・ノートグループ は SQLite にローカル保存
-
API は
127.0.0.1のみにバインドし、外部公開しない - Electron はローカル API のみへ接続
- Markdown と Mermaid の描画もローカルライブラリで実行
外部通信を行うのは、次の 2つだけ です。
- URL 取り込み時に対象 Web ページを取得するとき
- 初回起動時に Ollama 公式レジストリから
qwen3:8bモデルをダウンロードするとき
これら以外の ノート保存・質問応答・要約・問題生成・図解生成 は、すべて PC 内だけで処理されます。
ローカル LLM への接続固定
docker-compose.yml で、API コンテナが Docker 内の Ollama サービスだけへ接続するよう設定しています。
OLLAMA_HOST: http://ollama:11434
OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen3:8b}
FastAPI 側では、外部 API ではなく Docker 内の Ollama API /api/chat に推論を要求します。
response = await client.post(f"{OLLAMA_HOST}/api/chat", json=payload)
この構成により、ノート内容や質問文は OpenAI などのクラウド LLM API には送信されず、Docker 内で動作する Ollama だけに渡されます。
AI に渡すノートを明示選択にした
AI へ全ノートを無差別に渡すのではなく、ユーザーが選択したノートやノートグループだけをコンテキストとして渡すようにしました。不要な情報の混入を防ぎ、回答精度の向上につながります。
UI 側で選択中ノートを管理
実装箇所は desktop/renderer.js です。選択中ノートの ID は Set で管理します。
let notes = [], groups = [], selected = null, saveTimer, selectedIds = new Set();
selectedIds は、AI に渡すノート ID を重複なく保存するための集合です。
ノートグループを一括選択
グループを選択すると、グループに保存されているノート ID を参照対象に設定します。
const group = groups.find(g => g.id === +e.dataset.group);
selectedIds = new Set(group.note_ids);
renderList();
toast(`「${group.name}」を AI の参照対象にしました`);
例えば、「ネットワーク基礎」グループに L2 スイッチ、L3 スイッチ、VLAN、IPsec のノートを登録しておけば、グループを1回選ぶだけで必要な資料を AI の参照対象にできます。
選択ノート ID だけを API へ送信する
チャット送信時は、選択中のノート ID だけを API へ送信します。ノートを選択していない場合は、AI の実行を止めます。
async function ask(prompt) {
const ids = [...selectedIds];
if (!ids.length) {
toast('AI の参照ノートを選択してください');
return;
}
const response = await request('/chat', {
method: 'POST',
body: JSON.stringify({
prompt,
note_ids: ids,
template_id: $('promptTemplate').value
})
});
message(response.answer, 'assistant');
}
template_id は通常回答、応用情報技術者試験向け問題、図解付き解説などの定型プロンプトを選択するための値です。
API 側で対象ノートだけを取得する
FastAPI 側では、リクエストで受け取った note_ids に対応するノートだけを SQLite から取得します。
marks = ",".join("?" for _ in payload.note_ids)
rows = conn.execute(
f"SELECT title, content FROM notes WHERE id IN ({marks})",
payload.note_ids
).fetchall()
取得したノートだけを LLM に渡すコンテキストとして組み立てます。
context = "\n\n".join(
f"# {row['title']}\n{row['content']}"
for row in rows
)
ノートグループで再利用性を高めた
学習単元や案件、プロジェクト単位で複数のノートをグループ化できるようにしました。一度作成したグループは何度でも利用でき、毎回ノートを選択し直す必要がありません。
例えば、ネットワーク学習用に次のノートを選択したとします。
ネットワーク基礎
├─ L2 スイッチと L3 スイッチ
├─ VLAN
├─ IPsec
└─ ルーティング
この4件を「ネットワーク基礎」というグループとして保存しておけば、次回以降はグループをクリックするだけで、同じノート群を AI の参照対象として復元できます。
データ構造
グループ本体は note_groups、グループとノートの対応関係は note_group_items という中間テーブルで管理しています。
conn.execute("""CREATE TABLE IF NOT EXISTS note_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
)""")
conn.execute("""CREATE TABLE IF NOT EXISTS note_group_items (
group_id INTEGER NOT NULL,
note_id INTEGER NOT NULL,
PRIMARY KEY (group_id, note_id),
FOREIGN KEY(group_id) REFERENCES note_groups(id) ON DELETE CASCADE,
FOREIGN KEY(note_id) REFERENCES notes(id) ON DELETE CASCADE
)""")
この構造により、1つのグループに複数ノートを登録でき、1つのノートを複数グループで使うこともできます。
グループの作成
選択ノート ID を受け取り、グループ本体を作成した後に対応関係を保存します。
cursor = conn.execute(
"INSERT INTO note_groups(name, created_at) VALUES (?, ?)",
(payload.name.strip(), now)
)
conn.executemany(
"INSERT INTO note_group_items(group_id, note_id) VALUES (?, ?)",
[(cursor.lastrowid, note_id) for note_id in ids]
)
グループによる一括選択
フロントエンドでは、グループ選択時に保存済みの note_ids を selectedIds へ設定します。
const group = groups.find(g => g.id === +e.dataset.group);
selectedIds = new Set(group.note_ids);
renderList();
toast(`「${group.name}」を AI の参照対象にしました`);
Set を使うことで、ノート ID の重複を防ぎながらチェック状態を一括更新できます。
グループ削除とノート削除の扱い
グループを削除しても、ノート本体は削除しません。削除されるのはグループ名と対応関係だけです。
一方、ノートを削除した場合は、存在しないノートをグループが参照し続けないよう、中間テーブルの対応関係も削除します。
conn.execute("DELETE FROM note_group_items WHERE note_id=?", (note_id,))
conn.execute("DELETE FROM notes WHERE id=?", (note_id,))
まとめ
Docker、Ollama、Electron を組み合わせることで、ノート保存から LLM 推論、問題生成、図解までをローカル PC 内で完結する AI ノートアプリを構築しました。
特に、AI に渡すノートを明示選択し、グループとして再利用できる設計にしたことで、ノート数が増えても必要な情報だけを根拠に回答させられます。