機密コードを外に出さずに読みたい。Ollama + FastAPIで「解析 → Q&A → 引き継ぎ資料生成」まで作った
はじめに
上司が言いました
「ソースコードは機密が高いので、githubなどクラウド上に上げていないです」と
上司が言いました
「人のソースコードを読むのは大変なので、Chatgptなどに上げて解説してもらっています」と
私は思いました「.....矛盾してない?」
この矛盾を解決すべく、ローカルLLMでファイルやフォルダをアップロードすることで解析やしてQ&Aや引継ぎ資料の作成ができるシステムを作成しようと。
私の例は極端ですが、実際の開発の現場でも次のような問題があると思います。
- 社外へ出せないソースコードがある
- プロジェクト全体を貼るには量が多い
- 引継ぎが十分ではなくどれがどのファイルなのかの理解に時間がかかる
- LLMが存在しないファイル名や関数名をそれっぽく生成する
- 「このファイルは何をしているか」だけでなく、「どこから読むべきか」「変更すると何に影響するか」まで知りたい
- 最後は引き継ぎ資料まで残したい
そこで、ソースコードをローカル環境から出さずに、
解析 → プロジェクト理解 → Q&A → 引き継ぎ資料生成
までを一本につなぐローカルツールを作りました。
名前はひとまず Local Code Reader としています。
今回の記事では、Pythonプロジェクトを主対象として作った v3.4 時点の構成と、作りながら分かったことをまとめます。
完成したもの
現在は大きく分けて、次の4つができます。
- 単一ファイルの静的解析 + LLMによる説明
- プロジェクト全体の構造解析
- プロジェクト全体に対するQ&A
- README / ARCHITECTURE / HANDOVER の自動生成
最終的には、プロジェクトフォルダを指定すると、
ソースコード
↓
静的解析
↓
ファイルごとの意味解析
↓
プロジェクト構造を集約
↓
Q&A
↓
README.md
ARCHITECTURE.md
HANDOVER.md
という流れになります。
構成
構成はかなり単純です。
Browser
↓
localhost FastAPI
↓
localhost Ollama
↓
Qwen3 8B / 14B
ブラウザ側でフォルダを選択し、FastAPIへファイルを送ります。
FastAPI側では静的解析を行い、必要な情報だけをOllamaへ渡して意味解析させます。
Ollamaもlocalhostで動作させるため、アプリとしては外部APIへソースコードを送信しません。
プライバシー上の方針
このツールでは以下を意識しました。
- アプリ側ではソースコードを永続保存しない
- 保存するJSONには元ソースを含めない
- プロジェクトQ&Aでは、原則として解析済み情報を再利用する
- 元ソースをそのまま引き継ぎ資料へ埋め込まない
- 環境変数は値ではなく名前だけを資料化する
もちろん、OS・ブラウザ・Ollama・プロキシなどのログまで含めて「一切痕跡が残らない」ことを保証するものではありません。
あくまで、アプリ自身が不要にソースを保存・外部送信しない設計です。
一番重要だった設計方針
開発していて一番大きかったのは、
静的解析を「事実」、LLMを「解釈」に分けたこと
でした。
最初はLLMへコードを渡して、
このファイルは何をしている?
と聞けば十分だと思っていました。
ところが、実際には存在しない関数名やファイル名をかなり自然に生成します。
例えば、
ProjectIndex
analyze_files()
LLMConfig
models.py
のような、それっぽい名前です。
文章としては自然なので、コードを知らない人ほど気づきにくいのが厄介でした。
そこで途中から、
静的解析
├ ファイルパス
├ import
├ 関数
├ クラス
├ 行番号
├ 環境変数名
├ APIデコレータ
└ ローカル依存関係
LLM
├ purpose
├ overview
├ main_flow
├ change_risks
└ 自然言語での説明
という役割分担に変えました。
LLMが出した関数名やファイル名は、静的解析側に存在しなければ落とします。
PythonではASTを使う
Pythonについては ast を使って静的情報を取っています。
現在は主に次の情報を抽出しています。
- import
- 関数名
- クラス名
- qualified_name
- 開始行 / 終了行
- 関数内の呼び出し候補
- decorator
- トップレベル定義
- os.getenv() 等で参照される環境変数名
例えばFastAPIなら、
@app.post("/api/project/ask")
def ask_project(...):
...
のようなデコレータを拾うことで、
POST /api/project/ask
というAPIルートも静的な事実として扱えます。
JavaScript / HTML / CSSについても簡易解析は入れていますが、現状はPythonが最も情報量の多い対象です。
プロジェクト全体の依存関係を作る
各ファイルの解析結果を集めた後、プロジェクト全体の project_index を作っています。
例えば今回のLocal Code Reader自身を解析すると、
main.py
├→ analyzer.py
├→ config.py
├→ ollama_client.py
├→ project_analyzer.py
└→ project_docs.py
templates/index.html
├→ static/app.js
└→ static/style.css
のような依存関係が取れます。
import文やHTMLのscript/link参照を使ったbest-effortなので、動的importやDI、設定経由の依存関係まですべて追えるわけではありません。
それでも、「まずどこを見るべきか」を作るにはかなり有効でした。
Q&Aは単純に全情報を投げない
プロジェクトQ&Aで最初にやったのは、
プロジェクト解析結果
+
過去の会話
+
現在の質問
↓
LLM
という単純な方式でした。
これはかなり問題がありました。
前の質問で貼ったテストコードに次の回答が引っ張られたり、本来関係ないファイルを説明し始めたりしました。
そこで現在は、質問する前に Context Router を挟んでいます。
質問
↓
質問意図を判定
↓
関連ファイルを選定
↓
関連する会話履歴だけを選定
↓
必要なFactを抽出
↓
LLM
質問意図としては、例えば次のようなものを見ています。
- 特定ファイルの説明
- 特定言語のファイル群
- 関数 / クラス
- 依存関係
- 変更影響
- 処理フロー
- 設定
- テスト
- 引き継ぎ時の読む順番
- プロジェクト概要
- follow-up
- 一般質問
例えば、
5つのPythonファイルについてそれぞれの役割を教えて
と聞かれた場合は、Pythonファイルだけを先に抽出してからLLMへ渡します。
会話履歴も全部は渡さない
会話履歴も同様です。
以前は直近数メッセージをそのまま再送していましたが、独立した質問では過去会話がノイズになります。
そのため、
「そこをもう少し詳しく」
「前の部分について」
「それはどこから呼ばれている?」
のようなfollow-upと判断した場合だけ、関連する履歴を使います。
逆に、
Ollamaとの通信はどのファイル?
のように質問単体で成立している場合は、過去会話を使いません。
これだけでも回答のブレはかなり減りました。
Answer Grounding
関連ファイルを選ぶだけでは、まだLLMが存在しないシンボルを作ることがありました。
そこで、Q&A用に Fact Catalog を作りました。
イメージとしては、
F001 = main.py は Python ファイル
F002 = main.py に ask_project が存在
F003 = ollama_client.py に ask_project_with_ollama が存在
F004 = main.py → ollama_client.py のimport関係が存在
...
という事実リストをサーバー側で作ります。
LLMには自由に関数名を考えさせず、
どのfact_idを使うか
を選ばせます。
その後、
LLM出力
↓
fact_idが本当に存在するか確認
↓
path / symbol を再検証
↓
最終回答を組み立てる
という流れにしています。
つまり、LLMの役割を
事実を作る
から、
事実を選んで説明する
へ寄せています。
ローカルの比較的小さなモデルを使う場合、この方法はかなり相性が良いと感じました。
意味レベルのGrounding
さらに、シンボル名だけ正しくても、
config.pyは.envを読み込んでいる
のように、意味として一歩飛躍するケースがありました。
そこで現在はAIの補足説明にも support_fact_ids を要求しています。
{
"interpretation": "...",
"support_fact_ids": ["F012", "F018"]
}
根拠Factが確認できない説明は、最終回答から落とします。
完全に意味誤りを防げるわけではありませんが、
「それっぽい説明をそのまま採用する」
状態からはかなり改善しました。
引き継ぎ時の読む順番も自動生成
依存関係と入口候補が取れるようになると、
初めてこのプロジェクトを引き継ぐなら、どの順番で読むべき?
という質問にも静的情報中心で答えられます。
Local Code Reader自身では、次のような順番になりました。
1. main.py
2. config.py
3. analyzer.py
4. ollama_client.py
5. project_analyzer.py
6. project_docs.py
7. templates/index.html
8. static/app.js
9. static/style.css
この順番はLLMに丸投げしているのではなく、
入口候補
+
import依存
+
HTMLからの参照
からbest-effortで作っています。
最後に引き継ぎ資料を生成する
v3.4では、解析結果から次の3つを生成できるようにしました。
README.md
用途は「最初の5分で全体像を見る」です。
主に次を出します。
- プロジェクト概要
- ファイル数 / 行数 / 言語
- ディレクトリ構成
- 各ファイルの役割
- 外部依存
- 主要な入口
ARCHITECTURE.md
こちらは開発者向けです。
- コンポーネント
- ローカル依存関係
- APIルート
- 環境変数名
- トップレベル定義
- 処理 / データフロー
- 外部依存
- 解析上の境界
Local Code Reader自身では例えば、
GET /
GET /api/status
POST /api/analyze
POST /api/project/analyze-file
POST /api/project/summarize
POST /api/project/generate-docs
POST /api/project/ask
POST /api/ask
まで自動で一覧化できています。
HANDOVER.md
これは引き継ぎ用途に寄せています。
- まず読む順番
- 変更前に見る依存関係
- API / 外部入口
- 設定 / 環境変数
- 変更時の注意
- 未確認事項
- 引き継ぎチェックリスト
特に「未確認事項」を残すようにしたのは意外と重要でした。
分からないものまでそれっぽく埋めるより、
ここはまだ分からない
と書いてある方が、引き継ぎ資料としては信用できます。
ドキュメント生成ではLLMを追加で呼ばない
READMEなどの生成時には、追加でOllamaを呼んでいません。
grounding済み解析結果
↓
決定的なMarkdown生成
としています。
理由は単純で、最後にもう一度LLMへ文章を書かせると、せっかく除去した架空の情報が再び混ざる可能性があるからです。
目的や概要の文章には解析時のAI解釈を使いますが、
ファイル名
関数名
クラス名
API
依存関係
環境変数名
のような部分は、できるだけ静的情報から出します。
うまくいかなかった例
試しに、
このプロジェクトに分かっている脆弱性はある?
とも聞いてみました。
結果はほぼ回答できませんでした。
ただ、これは今の構造を考えると妥当でした。
現在のContext Routerにはセキュリティ診断専用の意図がなく、Fact Catalogにも、
危険なsubprocess
SQL Injection候補
ハードコードされた秘密情報
危険なデシリアライズ
のようなセキュリティ用Factがありません。
そのため、LLMの推測をGrounding側がほぼ全部落としました。
これは「脆弱性を見つけられなかった」という意味では未完成ですが、
根拠がないのに脆弱性を捏造しなかった
という点では、今の設計らしい結果でもあります。
本格的にやるなら、通常のコード読解とは分離して、
Bandit / Semgrep等の静的ルール
+
LLMによる説明
のような構成にした方が良さそうです。
小さいモデルでもGroundingが効く
OllamaではQwen3の8Bと14Bを試しました。
当然14Bの方が意味解釈は安定しやすいですが、8Bでも、
静的解析
+
Context Router
+
Fact Catalog
+
Answer Grounding
を入れることで、かなり実用的な回答まで持っていけました。
ここは今回かなり面白かったところです。
モデルを巨大化する以外にも、
モデルに何を任せるかを減らす
ことで精度を上げられます。
現状の限界
現時点では、まだ次のような制約があります。
- Python以外は簡易解析が中心
- 動的importやDI経由の依存関係は完全には追えない
- JavaScriptの関数解析はPython ASTほど強くない
- プロジェクトQ&Aでは、解析済み情報にない細かな実装までは答えられない場合がある
- セキュリティ診断機能は持っていない
- 静的解析自体が誤ればGroundingの土台も誤る
- 目的・概要・変更リスクなどの自然言語部分にはLLM解釈が残る
なので、これは
「コードを完全自動で理解するAI」
というより、
「人間がコードを読む前の地図を作るツール」
として考えています。
作ってみて分かったこと
今回、一番大きかった学びは、
LLMの精度を上げることより、LLMが間違えても壊れにくい構造を作る方が重要
ということでした。
最初はプロンプトを調整して、
存在しない関数を作らないでください
と書けば改善すると思っていました。
もちろん多少は効きます。
ただ、最終的には
静的解析で事実を作る
↓
LLMに意味を解釈させる
↓
事実と照合する
↓
確認できる情報だけ残す
という構造の方が、はるかに安定しました。
特にローカルLLMのようにモデルサイズに制約がある環境では、こういう設計はかなり重要だと思います。
今後やりたいこと
いったん今回で、
解析
↓
Q&A
↓
引き継ぎ資料
まで一本つながったので、機能追加よりも別プロジェクトで試す段階に入ろうと思っています。
今後やるなら候補はこのあたりです。
- Python以外の静的解析強化
- 必要なファイルだけ元ソースを再参照するProject Q&A
- reverse dependencyを使った変更影響分析
- HTTPルートや設定ファイルのさらに詳細な解析
- Bandit / Semgrep等と組み合わせたセキュリティ分析
- 大規模プロジェクト向けのチャンク / インデックス戦略
まずは、自分の別プロジェクトへ食わせて「どこで崩れるか」を確認したいと思います。
まとめ
Local Code Readerでは、最終的に次の構成になりました。
Browser
↓
FastAPI
↓
静的解析
↓
Ollama
↓
Grounding
↓
Project Index
↓
Context Router
↓
Q&A
↓
README / ARCHITECTURE / HANDOVER
ソースコードをただLLMに読ませるだけなら簡単ですが、
「存在しない情報をできるだけ混ぜず、プロジェクト理解に使える形へする」
ところが一番難しく、同時に面白い部分でした。
まだPython中心の試作ではありますが、
機密コードをローカルで読ませたい
プロジェクトを短時間で把握したい
引き継ぎ資料を自動で作りたい
という用途には、かなり形になってきたと思います。
同じように「ローカルLLMでコードを読みたい」と考えている方の参考になれば幸いです。