18
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python+ローカルLLM】AIコーディングエージェントをRAGに組み合わせてみた

18
Posted at

はじめに

「社内ドキュメントに自然言語で質問できたら便利じゃないか?」
「しかも、コードの書き方まで聞けたら最高じゃないか?」

そんな欲張りな発想から生まれたのが Django-RAG (Ver.2) です。

以前投稿した Agentic Coder の記事では、ローカルLLMで動くPySide6アプリをご紹介しました。

そして Ollama と Django を組み合わせたプロジェクトも紹介しました。

今回はその兄弟プロジェクトとして、Djangoで構築した本格的なWebアプリケーションをご紹介します。

このプロジェクトの最大の特徴は 「ローカルでもWebでも動く」、そして 「質問した言語で自動回答できる」 こと。

運用モード 特徴
ローカル運用 APIキー不要・インターネット不要・コードは外に出ない
Web運用 チームメンバーにブラウザからアクセスさせて社内ナレッジ基盤として利用
多言語運用 英語・日本語のUI切り替え(EN/JA)と、日本語・中国語・アラビア語・ロシア語など100言語以上の質問に自動回答

どちらのシナリオにも対応できる、チーム利用を見据えたプロダクション品質の実装です。ナイーブすぎるのはわかっています。(笑)
ご了承ください。


このアプリでできること

Django-RAG は2つのアプリケーションが統合されています。

1. Knowledge Hub(app_core)

  • PDF / Word / Excel / TXT をアップロードするだけで自動インデックス化
  • FAISSベクトル検索+Ollamaで自然言語Q&Aが可能
  • ユーザーロール(Admin / Manager / Employee)による文書アクセス制御
  • クエリ履歴・システムステータスのダッシュボード

2. Coding IDE(coding_ide)

  • コードファイルをアップロードしてRAGコンテキストとして活用
  • Qwen 2.5 Coder 14Bによるコード生成・デバッグ・説明(RTX 5050 + 32GB RAMに最適化)
  • Monaco Editor(VS Code同等)をブラウザ内に搭載
  • スニペット保存・AIへの一発送信機能
  • Git操作(init / add / commit / push / pull)をUIから実行

システム全体像

ユーザー(ブラウザ)
     ↓
[Django 5.x]
├── app_core / Knowledge Hub
│   ├── Document Upload & Processing
│   ├── FAISS CPU Index(文書ベクトル)
│   └── Ollama Client → llama3.2:3b
│
└── coding_ide / Coding IDE
    ├── Code Upload & Chunking
    ├── FAISS CPU Index(コードベクトル)
    ├── Ollama Coder Client → qwen2.5-coder:14b-instruct-q4_K_M
    ├── Monaco Editor(ブラウザ内IDE)
    └── Git Manager
         ↓
[共有埋め込みモデル]
BAAI/bge-m3(CPU、1度だけロード・100言語以上)
         ↓
[Ollama] → GPU で LLM 推論(CUDA自動認識)

2つのアプリが1つの埋め込みモデルを共有するため、メモリ効率が非常に高く、8GBのVRAMでも余裕を持って動作します。


なぜローカル+Web両対応なのか

クラウドAIには優れたツールが多くありますが、以下の問題があります。

プライバシー
業務コード・社内ドキュメントを外部APIに送信することに抵抗を感じる企業は少なくありません。特にセキュリティポリシーの厳しい環境では、外部送信が禁止されているケースもあります。

コスト
GPT-4やClaude OpusのAPIコストは、エージェント的に繰り返し呼び出す用途では予想以上に膨らみます。

オフライン対応
VPN接続必須の環境、出張先、低速回線でも同じように使えるツールが理想です。

Django-RAGはこれらをすべて解決します。

運用モード 説明
ローカル(個人) runserver で起動し、localhostでブラウザアクセス
ローカルネットワーク(チーム) 0.0.0.0 でバインドし、社内LANのメンバーが共有
サーバー(本番) Nginx + Gunicorn + PostgreSQLに切り替えてクラウド/オンプレ運用

アーキテクチャの設計思想

クリーンアーキテクチャ

本プロジェクトは厳格なレイヤー分離を採用しています。

インターフェース層  → Django Views(薄いコントローラー)
サービス層         → RAGPipeline / CodeRAGPipeline
インフラ層         → FAISSManager / OllamaClient / EmbeddingCache
ドメイン層         → Document / CodeKnowledgeBase(モデル・例外)

ビジネスロジックはViewに書かず、すべてPipelineクラスに集約しています。これにより、LLMをモックに差し替えたユニットテストが容易です。

シングルトン埋め込みモデル

# coding_ide/cache_manager.py
# 2つのアプリが同じモデルインスタンスを共有 — 2回ロードしない
from app_core.cache_manager import embedding_cache as code_embedding_cache

app_corecoding_ide は同じ SentenceTransformer インスタンスを参照します。Djangoプロセス内で埋め込みモデルは1度だけメモリにロードされます。

FAISSインデックスの自動リセット(埋め込みモデル変更検知)

埋め込みモデルを切り替えたとき(例:bge-large-en-v1.5bge-m3)に次元数が変わり、古いインデックスと不整合が起きるバグを防ぐ仕組みを実装しました。

# faiss_manager.py / faiss_code_manager.py
def save_index(self):
    with open(self.metadata_path, 'wb') as f:
        pickle.dump({
            'metadata': self.metadata,
            'dimension': self.dimension,
            'embedding_model': settings.EMBEDDING_MODEL,  # モデル名も保存
        }, f)

def load_index(self):
    data = pickle.load(f)
    stored_model = data.get('embedding_model', '')
    if stored_model and stored_model != settings.EMBEDDING_MODEL:
        # モデルが変わっていたらインデックスを自動リセット
        logger.warning(
            f"Embedding model changed ({stored_model}{settings.EMBEDDING_MODEL}). "
            "Auto-resetting FAISS index."
        )
        self.initialize_index()
        return
    self.metadata  = data['metadata']
    self.dimension = data['dimension']

これにより、.env でモデルを変更してサーバーを再起動するだけでインデックスが自動クリアされ、次元不整合による無音障害(silent mismatch)を完全に防止しています。bge-m3 に乗り換えた際もコマンド一発で済みます。

python manage.py cache_models --force  # モデル変更後は必ず実行

テンプレートベース日本語UI(LangLoader + LangMiddleware)

DjangoのデフォルトのLocaleMiddleware(.poファイル + gettext)ではなく、テンプレートの透過的な差し替えでEN/JA切り替えを実装しました。翻訳ファイルが不要で、HTMLを書く感覚そのままで日本語UIを管理できます。

knowledge_manager/lang_middleware.py  ← thread-local で言語を保持
knowledge_manager/lang_loader.py      ← AppDirs サブクラス、ja/ を優先参照
knowledge_manager/urls.py             ← /lang/en/ /lang/ja/ でセッションに保存

app_core/templates/app_core/
├── base.html / dashboard.html / …   ← 英語テンプレート(14ファイル)
└── ja/
    ├── base.html / dashboard.html / …  ← 日本語テンプレート(14ファイル)

coding_ide/templates/coding_ide/
├── base_ide.html / ide_chat.html / … ← 英語テンプレート(10ファイル)
└── ja/
    ├── base_ide.html / ide_chat.html / … ← 日本語テンプレート(10ファイル)

仕組みはシンプルです。

# knowledge_manager/lang_loader.py
class LangLoader(AppDirsLoader):
    def get_template_sources(self, template_name):
        lang = get_current_lang()  # thread-local から取得
        if lang != 'en' and f'/{lang}/' not in template_name:
            # app_core/dashboard.html → app_core/ja/dashboard.html を先に探す
            parts = template_name.rsplit('/', 1)
            lang_template = f'{parts[0]}/{lang}/{parts[1]}'
            yield from super().get_template_sources(lang_template)
        yield from super().get_template_sources(template_name)  # 英語フォールバック

LangMiddleware はリクエストごとに request.session['ui_lang'] を読み、threading.local() に書き込みます。これによりViewを一切変更せずに、テンプレートローダーが自動で言語別ファイルを選択します。

言語の切り替えはすべてのページの右下(サイドバー内)に配置した EN / JA ボタンから行え、セッションをまたいで記憶されます。

/lang/en/  →  session['ui_lang'] = 'en'  →  元のページにリダイレクト
/lang/ja/  →  session['ui_lang'] = 'ja'  →  元のページにリダイレクト

ゼロ依存の言語自動検出(app_core/language_utils.py)

spaCyやlangdetectを使わず、Unicodeブロック解析だけで言語を判定するモジュールを実装しました。

from app_core.language_utils import detect_language, get_lang_instruction

detect_language("こんにちは世界")   # → 'ja'  (ひらがな/カタカナを検出)
detect_language("你好世界")         # → 'zh'  (漢字のみ → 中国語)
detect_language("مرحبا")           # → 'ar'
detect_language("Привет мир")      # → 'ru'
detect_language("Hello world")     # → 'en'

get_lang_instruction("مرحبا")
# → 'يُرجى الرد دائمًا باللغة العربية.'

日本語と中国語の分離はひらがな・カタカナの有無で判断しています。外部ライブラリゼロで依存関係が増えないのが大きな利点です。

LLMプロンプトへの言語ディレクティブ自動注入

両方のOllamaクライアント(Knowledge Hub・Coding IDE)が、ユーザーの質問言語を検出してシステムプロンプトに指示を追加します。

  • 日本語で質問 → 「必ず日本語で回答してください。」を注入 → 日本語で回答
  • アラビア語で質問 → アラビア語指示を注入 → アラビア語で回答
  • 未検出のラテン系言語 → 「Respond in the same language as the user's question」→ 現代LLMが確実に対応

設定変更不要・ユーザーごとの設定も不要です。質問した言語で答えが返ってくるのが自然な動作です。


セキュリティ設計

  • ドキュメントアクセスは can_access(user) メソッドで必ずチェック
  • パスワードはDjango標準ハッシュ(PBKDF2)
  • CSRFトークンを全POST/DELETE操作に適用
  • Gitコマンドはタイムアウト付き subprocess.run で実行(シェルインジェクション対策)
  • pickleを使わずSQLite + JSONでメタデータを永続化

RAGパイプラインの仕組み

文書アップロード時の処理フロー

[ファイルアップロード]
        ↓
[テキスト抽出] ← PyMuPDF(PDF) / python-docx(Word) / openpyxl(Excel)
        ↓
[チャンク分割] ← 文境界を考慮したスライディングウィンドウ
        ↓
[ベクトル化]  ← sentence-transformers(CPU・バッチ処理)
        ↓
[FAISS登録]  ← IndexIDMap + Flat L2(CPU専用)
        ↓
[DBに記録]   ← DocumentChunk(SQLite)

クエリ時の処理フロー

[ユーザーの質問]
        ↓
[クエリをベクトル化]
        ↓
[FAISS検索] → 上位K件のチャンクを取得
        ↓
[アクセス制御フィルタ] ← ユーザーのロール・部署でフィルタ
        ↓
[LLMプロンプト構築] ← チャンクをコンテキストとして注入
        ↓
[Ollama推論] → テキスト生成
        ↓
[回答 + ソース文書を返却]

Coding IDEのハイライト

コード認識チャンキング

通常のスライディングウィンドウではなく、コードの論理境界でチャンクを分割します。

# coding_ide/code_processor.py 内
_BLOCK_PATTERNS = [
    re.compile(r'^(def |class |async def )', re.MULTILINE),   # Python
    re.compile(r'^(function |const |let |class )', re.MULTILINE),  # JS/TS
    re.compile(r'^(fn |pub fn |impl )', re.MULTILINE),        # Rust
]

defclass の開始を検出し、関数・クラス単位でチャンクを形成します。これにより、LLMが受け取るコンテキストの品質が大幅に向上します。

Monaco Editorの統合

// VS Code同等の体験をブラウザ内で実現
editor = monaco.editor.create(document.getElementById('monaco-container'), {
    value: initialContent,
    language: initialLang,
    theme: 'vs-dark',
    fontSize: 14,
    minimap: { enabled: true },
    fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace",
});
// Ctrl+S でスニペット保存
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, saveSnippet);

コードをエディタで書きながら、そのまま「Ask AI」ボタンでQwen 2.5 Coderに投げられます。

Git操作のUI統合

# coding_ide/views.py 内
def _run_git(path: str, *args) -> dict:
    result = subprocess.run(
        ['git', '-C', path] + list(args),
        capture_output=True, text=True, timeout=30
    )
    return {'ok': result.returncode == 0, 'output': (result.stdout + result.stderr).strip()}

init / status / add / commit / branch / checkout / push / pull をブラウザから実行できます。リモートリポジトリのURLも管理できます。


セットアップ

1. 環境準備

git clone https://github.com/Rikiza89/Django-RAG
cd Django-RAG
pip install -r requirements.txt

# GPU使用時(オプション、Ollamaのレポート用)
pip install torch --index-url https://download.pytorch.org/whl/cu124

2. 設定ファイル

cp change.env.txt .env
SECRET_KEY=your-secret-key
DEBUG=True
OLLAMA_HOST=http://localhost:11434
OLLAMA_MODEL=llama3.2:3b

# RTX 5050 / 32 GB RAM / Ryzen 7 推奨設定
CODING_OLLAMA_MODEL=qwen2.5-coder:14b-instruct-q4_K_M
CODING_OLLAMA_TIMEOUT=480
EMBEDDING_MODEL=BAAI/bge-m3
EMBEDDING_BATCH_SIZE=64
CHUNK_SIZE=800
CHUNK_OVERLAP=150
CODE_CHUNK_SIZE=1000
CODE_CHUNK_OVERLAP=200
FAISS_TOP_K=5
CODE_FAISS_TOP_K=8
QUERY_CACHE_SIZE=200
CODE_QUERY_CACHE_SIZE=100
MAX_UPLOAD_SIZE=100

3. 初期セットアップ

python manage.py migrate
python manage.py cache_models        # 埋め込みモデルのダウンロード(初回のみ)
# モデルを変更した場合は --force オプションを使用
# python manage.py cache_models --force
python manage.py createsuperuser

# Ollamaモデルの取得
ollama pull llama3.2:3b
ollama pull qwen2.5-coder:14b-instruct-q4_K_M

python manage.py runserver

ブラウザで http://localhost:8000 を開けばすぐに使えます。

Windowsユーザーへ
「モデルをダウンロード済みなのにダッシュボードで『未キャッシュ』と表示される」場合は、Windowsの開発者モード(設定 → システム → 開発者向け)を有効にしてください。開発者モードが無効だとHugging Face Hubがシンボリックリンクを作成できず、キャッシュ検出が誤動作することがあります。本プロジェクトでは3段階のフォールバック検出(シンボリックリンク解決 → ファイルサイズ確認)を実装していますが、根本的な解決は開発者モードの有効化です。


多言語対応の詳細

UIの言語切り替え(EN/JA)

UIの英語・日本語切り替えは、Djangoのi18n(LocaleMiddleware / gettext / .poファイル)を使わず、カスタムテンプレートローダーで実装しています。

Knowledge Hub・Coding IDEの全ページ(計24テンプレート)に日本語版を用意し、セッションに保存した言語設定に応じてローダーが透過的に差し替えます。

すべてのページ右下(またはサイドバー内)の EN / JA ボタンをクリック
  ↓
/lang/ja/ にアクセス → session['ui_lang'] = 'ja' → 元のページに戻る
  ↓
以降のすべてのリクエストで LangLoader が ja/ テンプレートを優先配信

日本語テンプレートでは日付フォーマット(Y年m月d日)・ファイルサイズ表記(バイト/KB/MB/GB)・JavaScriptのメッセージ文字列もすべて日本語化しています。フォントスタックも Hiragino Sans, Meiryo, Yu Gothic を優先するよう設定しています。

LLM回答の言語自動検出(100言語以上)

UIの言語設定とは独立して、LLMはユーザーの質問文の言語を自動検出して同じ言語で回答します。app_core/language_utils.py のUnicode解析で実現しており、日本語・中国語・アラビア語・ロシア語など100言語以上に対応しています。

埋め込みモデルの選択肢

モデル 次元 言語 サイズ 特徴
BAAI/bge-m3 1024 100+ ~1.5GB 推奨・デフォルト
BAAI/bge-large-en-v1.5 1024 英語特化 ~1.3GB 英語のみの環境向け
intfloat/multilingual-e5-large 1024 100+ ~1.1GB 軽量な多言語代替
sentence-transformers/all-mpnet-base-v2 768 英語 ~420MB 最軽量・低スペック向け

モデルを変更したら python manage.py cache_models --force → サーバー再起動でFAISSインデックスが自動リセットされます。

チームへの共有

オフィス内でチームに共有する場合は以下のように起動するだけです。

python manage.py runserver 0.0.0.0:8000

チームメンバーは http://サーバーのIPアドレス:8000 でアクセスできます。ユーザー管理画面からメンバーのアカウントを作成し、部署・ロールを設定することできめ細かいアクセス制御が働きます。


GPU / VRAMの目安

モデル VRAM 用途
qwen2.5-coder:14b-instruct-q4_K_M ~8.5 GB コーディングIDE(RTX 5050推奨)
qwen2.5-coder:7b-instruct-q4_K_M ~4.5 GB VRAMが少ない環境向け
llama3.2:3b ~2 GB ドキュメントQ&A
埋め込みモデル(BAAI/bge-m3) CPU(VRAM不要) 常時CPU処理・100言語以上・~1.5GB RAM

RTX 5050(8GB VRAM)+ 32GB RAM + Ryzen 7 がこのプロジェクトのリファレンス環境です。
埋め込みはすべてCPUで処理し、バッチサイズ64・Ryzen 7のマルチコアを活かして高速化しています。14BモデルはVRAMを最大限占有しますが、embeddings/FAISSがCPU側で動くため競合しません。


技術スタック

カテゴリ 使用技術
Webフレームワーク Django 5.x
LLM実行基盤 Ollama
ドキュメントLLM llama3.2:3b
コーディングLLM qwen2.5-coder:14b-instruct-q4_K_M
埋め込みモデル BAAI/bge-m3(1024次元・100言語以上・~1.5GB)
ベクトルDB FAISS CPU
コードエディタ Monaco Editor (ブラウザ)
日本語UI カスタム LangLoader + LangMiddleware(24テンプレート)
文書処理 PyMuPDF / python-docx / openpyxl
DB SQLite(開発)/ PostgreSQL(本番推奨)
言語 Python 3.11+

今後の展望

  • 非同期処理対応 — Celeryによるバックグラウンド文書処理
  • マルチモーダル対応 — 画像・図表の認識(llava等)
  • エクスポート機能 — チャット履歴・ソース文書のPDFエクスポート

おわりに

Django-RAGは「社内の知識を全員が使えるようにしたい」という課題に対するシンプルかつ実用的な答えです。

ローカルで個人利用から始めて、チームに展開し、いずれはオンプレサーバーへ。段階的に育てられる設計になっています。そしてすべての処理はあなたの環境の中だけで完結します。

ソースコードはGitHubで公開しています。

ぜひ試してみてください!

この記事が参考になったら、:hearts: をお願いします!

18
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?