はじめに
Claude Codeのセッションが短すぎるので、ローカルLLMで自作しました。
「AIにコードを書いてもらいたいけど、APIにお金を払いたくない」「ソースコードをクラウドに送信したくない」「オフライン環境でも使いたい」——そんな悩みを全部解決するために、LocalForge というツールを自作しました。
LocalForgeは、Ollama をバックエンドとして使うローカルファーストのAIコード生成IDEです。コードは一切インターネットに出ていきません。LLMの推論はすべて自分のマシン上で完結します。
そして実際に Qwen 2.5 Coder 7B(量子化版) で試したところ、複数ファイルにまたがる中規模プロジェクトを、コンテキストを保ちながら自動生成できることを確認できました。
LocalForgeが解決する問題
通常、LLMにプロジェクト全体を生成させようとすると、すぐに壁に当たります。
- コンテキスト枯渇問題:ファイルが増えるにつれ、LLMが最初の設計方針を「忘れる」
- ファイル間の一貫性:後から生成したファイルが、先に生成したファイルと整合しない
- コスト問題:Claude APIやGPT-4 APIを使い続けると請求が膨らむ
- プライバシー問題:業務コードをクラウドに送信したくないケース
LocalForgeはこれらをすべてローカルで解決します。
アーキテクチャの全体像
localforge/
├── domain/ # ドメインモデル・ポート定義・例外
├── application/ # ビジネスロジック(生成・コンテキスト・プロジェクト管理)
├── infrastructure/ # Ollama HTTP クライアント・FS・Git アダプター
└── interface/ # Tkinter UI・ウィジェット群
Clean Architectureを採用しており、ビジネスロジックとインフラ層が明確に分離されています。UIからOllamaを直接呼び出すことはなく、すべてポートインターフェース(typing.Protocol)を通じて抽象化されています。
これにより、たとえば将来的にOllamaをllama.cppに差し替えても、LLMPortを実装する新しいアダプターを書くだけで対応できます。
ローリングサマリーによるコンテキスト管理
LocalForgeの核心は context.md と呼ばれるローリングサマリーの仕組みです。
仕組み
- ユーザーがプロジェクトの要件を自然言語で入力します
- LLMが 構造化されたJSONプラン(ファイル一覧・生成順序・依存関係)を生成します
- ファイルを1つ生成するたびに、そのファイルの役割・インターフェース・設計判断を
context.mdに追記・更新します - 次のファイル生成時は、
context.mdの内容をコンテキストに注入します
# application/context_service.py から抜粋
def build_generation_prompt(
self,
project: Project,
plan: GenerationPlan,
plan_file: PlanFile,
generated_files: dict[str, str],
) -> tuple[str, str]:
# コンテキストドキュメントを読み込む
context_content = self._read_context(project)
# 依存ファイルの内容を収集
dependency_contents = self._collect_dependencies(plan_file, generated_files)
user_prompt = f"""## プロジェクトコンテキスト
{context_content}
## 生成プラン全体
{plan.to_json()}
## 依存ファイルの内容
{dependency_contents}
## 生成対象ファイル
パス: {plan_file.path}
説明: {plan_file.description}
..."""
return system_prompt, user_prompt
ファイル生成後は update_context() を呼び、LLMに context.md を 800トークン以内に要約・更新 させます。これによって、プロジェクトが大規模になってもコンテキストがトークン制限を超えません。
なぜこれが重要か
ローカルの7Bモデルはコンテキストウィンドウが限られています。context.md がないと、10ファイル目を生成するころには、LLMは1ファイル目に何を作ったかを完全に忘れています。ローリングサマリーがあることで、小さなモデルでも大きなプロジェクトを「覚え続けながら」生成できます。
Qwen 2.5 Coder 7B で実際に動きました
重要なのは実際に動作するという事実です。
テスト環境:
- モデル:
qwen2.5-coder:7b-instruct-q4_K_M(量子化版) - バックエンド:Ollama(ローカル)
- OS:Windows11
生成できたもの:
- Pythonの複数モジュール構成プロジェクト
- FastAPI + SQLiteのREST APIスケルトン
- 設定クラス・例外クラス・リポジトリパターンを含む構成
実際の流れ:
1. プロンプト入力
└─ "PythonでFastAPIを使ったTODOアプリを作ってください。
Clean Architectureで、SQLiteを使い、CRUD操作をサポート。"
2. プラン生成(JSONで出力)
└─ requirements.txt
└─ main.py
└─ app/domain/models.py
└─ app/domain/exceptions.py
└─ app/infrastructure/database.py
└─ app/application/todo_service.py
└─ app/interface/routes.py
└─ tests/test_todo_service.py
3. ファイルを順に自動生成(ストリーミング表示)
└─ 各ファイル生成後にcontext.mdを自動更新
└─ 各ファイルをgitコミット
4. 完成
7Bモデルはときどき JSON のフォーマットが崩れることがありますが、LocalForgeは正規表現でマークダウンフェンスや前後のテキストを除去し、JSONオブジェクトを抽出する堅牢なパーサーを内蔵しているため、大抵の場合は自動回復できます。
ストリーミングとスレッド設計
UIがフリーズしないよう、すべてのOllamaコールはワーカースレッドで実行され、結果は queue.Queue 経由でメインスレッドに送られます。
# ワーカースレッドがキューにイベントを送る
for chunk in self._llm.stream_completion(...):
ui_queue.put(GenerationEvent(EVENT_CHUNK, chunk))
# UIスレッドは50msごとにキューをポーリング
def _poll_ui_queue(self) -> None:
try:
while True:
event = self._ui_queue.get_nowait()
self._handle_generation_event(event)
except queue.Empty:
pass
if self._state.is_generating.get():
self._root.after(50, self._poll_ui_queue)
widget.after(50, poll) パターンにより、Tkinterのイベントループをブロックせずにリアルタイムストリーミングを実現しています。
Git統合:自動コミットで進捗を追跡
LocalForgeはファイルを1つ生成するたびに自動的にgitコミットを作成します。
feat: generate app/domain/models.py
feat: generate app/infrastructure/database.py
feat: generate app/application/todo_service.py
...
これにより:
- どのファイルがどの順序で生成されたかが明確になります
- 生成途中でエラーが起きても、最後の正常コミットまで戻れます
- diff ビューアで生成内容を確認できます
GitPythonが利用できない環境では subprocess へのフォールバックが自動的に行われます。
セキュリティとアーキテクチャ上の設計判断
ポートパターンによる依存性逆転
# domain/ports.py
class LLMPort(Protocol):
def stream_completion(self, prompt: str, model: str, ...) -> Generator[str, None, None]: ...
def complete(self, prompt: str, model: str, ...) -> str: ...
def is_available(self) -> bool: ...
OllamaClient は LLMPort を実装していますが、アプリケーション層は LLMPort しか知りません。これはテストでも威力を発揮し、MagicMock(spec=LLMPort) を渡すだけで実際のOllamaなしに全サービスをユニットテストできます。
ハードコードされた秘密情報ゼロ
APIキーもトークンも存在しません。Ollamaのエンドポイント(デフォルト http://localhost:11434)は設定として外部化されており、起動時にバリデーションされます。
エラーハンドリングの階層化
# domain/exceptions.py
class LocalForgeError(Exception): ... # 基底クラス
class OllamaConnectionError(LocalForgeError): ... # 接続エラー
class PlanParseError(LocalForgeError): ... # JSONパースエラー
class GitOperationError(LocalForgeError): ... # Git操作エラー
ベアの except Exception は使わず、すべての失敗モードが型付きで明示的に処理されます。
ファイル再生成機能
生成されたファイルが気に入らなければ、右クリック→「新しい指示で再生成」で追加指示を与えて再生成できます。
既存ファイルの内容 + git diff + 追加指示 → LLM → 新しいファイル内容
この際も git diff が自動的にコンテキストに含まれるため、LLMは「何が変わっているか」を把握した上で改善できます。
インストールと起動
# Ollamaをインストール(https://ollama.com)
ollama serve
ollama pull qwen2.5-coder:7b-instruct-q4_K_M
# LocalForgeをクローン
git clone <repo-url>
cd LocalForge
python -m venv .venv
venv/bin/activate
pip install -r requirements.txt
# 起動
python -m localforge.main
必要なパッケージは3つだけです:
requests==2.31.0
gitpython==3.1.43
pydantic==2.7.1
UIはTkinter(Python標準ライブラリ)なので、追加の依存はありません。
ローカルLLMの実力について
Qwen 2.5 Coder 7B(量子化版)は2024年末時点で最強クラスのコーディング特化小型モデルのひとつです。商用品質には届きませんが、定型的な構造(CRUD API、設定クラス、テストスケルトン)の生成であれば十分に実用的なコードを出力できます。
量子化によってVRAMは4〜5GB程度に収まり、16GBのRAMがあれば一般的なノートPCでも動作します。
そして何より——完全に無料、オフラインで動作、コードはマシンから一切出ていきません。
まとめ
LocalForgeで実現できたこと:
| 機能 | 詳細 |
|---|---|
| ローカルLLM推論 | Ollamaベース、インターネット不要 |
| ローリングサマリー | context.mdで大規模プロジェクトのコンテキストを維持 |
| ストリーミング生成 | トークン単位でリアルタイム表示 |
| Git自動コミット | ファイル生成ごとに自動コミット |
| ファイル再生成 | 追加指示で任意のファイルを再生成 |
| Clean Architecture | テスタブルな設計、依存性逆転 |
完全無料・完全オフライン・完全ローカルで、プロジェクトをゼロから自動生成する——LocalForgeはそのための実用的な土台です。ぜひ試してみてください。
レポジトリのリンクは下記となります:
テスト環境:Qwen 2.5 Coder 7B Instruct Q4_K_M / Ollama 0.20.7 / Python 3.11.9 / Windows11