前回は「1本のソースをどう読み解くか」を扱った。だが現実の は、1本では終わらない。数十万行、ファイル数百、ストアドとバッチが絡み合う塊を前にすると、「 に読ませる」の“読ませる”をどう実装するかが本題になる。コンテキストウィンドウには丸ごと入らないし、雑に切って投げれば要約は断片的で使い物にならない。本稿では、巨大コードベースを LLM で要約するためのチャンク戦略・投入順・階層要約・溢れ対策・検証プロンプト・ナレッジベース化を、現場で踏む順番のまま整理する。
「LLM にコードを貼って要約させる」を、単発の手作業から、数十万行を再現可能に処理できるパイプラインへ引き上げる。鍵は、切り方・順番・束ね方・検証の4点だ。
チャンク戦略 ── 行数で切らず、意味で切る
最初にやりがちな失敗が、「N 行ごとに機械的に分割」だ。関数の途中でぶつ切りにされた断片を渡されても、LLM は文脈を失って推測で埋めるしかなく、幻覚の温床になる。刻む単位は行数ではなく構文構造に合わせる。理想は関数・クラス・ストアドプロシージャといった「意味のまとまり」を1チャンクにすることだ。まずは AST(抽象構文木)や言語のパーサでトップレベル定義を切り出し、大きすぎる関数だけをさらに内部ブロックで分ける、という二段構えにすると破綻しにくい。
- 関数/メソッド単位:最も扱いやすい基本単位。呼び出し元・呼び出し先を要約に添えると精度が上がる
- ファイル単位:定数定義や設定ファイルなど、関数に割れないものはファイルごと
- モジュール/ディレクトリ単位:階層要約の中間ノードとして使う(後述)
- 巨大関数の内部分割:1関数が数百行あるレガシーでは、if/loop の論理ブロックで割り、末尾に「これは巨大関数◯◯の一部」と文脈を明記する
チャンク本文だけでなく、ファイルパス・言語・行範囲・依存先シンボルを構造化して一緒に持たせる。この付帯情報が、後段の投入順の決定と、ナレッジベース化での検索性を大きく左右する。
投入順 ── 依存関係の葉から根へ
チャンクをどの順で LLM に渡すかは、要約品質に直結する。呼び出される側(葉)から先に要約し、その要約を呼び出す側(根)の文脈として添える。こうすると、上位の関数を要約するときに「この関数が呼んでいる下位処理が何をするか」を既に言語化済みの状態で渡せる。逆順だと、上位を読む時点で下位が未知のブラックボックスになり、LLM が中身を勝手に想像し始める。実装上は、依存グラフを組んでトポロジカルソートし、その順に処理していく。
import networkx as nx
def build_call_graph(chunks): g = nx.DiGraph() for c in chunks: g.add_node(c.symbol, chunk=c) for callee in c.calls: # チャンク抽出時に集めた呼び出し先シンボル g.add_edge(c.symbol, callee) return g
def summarize_bottom_up(chunks): g = build_call_graph(chunks) summaries = {} # 葉(呼び出し先)から根(呼び出し元)へ。循環は縮約して近似 for scc in reversed(list(nx.topological_sort(nx.condensation(g)))): members = nx.condensation(g).nodes[scc]["members"] for symbol in members: node = g.nodes.get(symbol) if node is None: continue deps = {s: summaries.get(s, "(未要約)") for s in g.successors(symbol)} summaries[symbol] = call_llm_summary(node["chunk"], deps) # 仮実装 return summaries
def call_llm_summary(chunk, dep_summaries): # 仮実装: チャンク本文 + 依存先の要約 をプロンプトに詰めて LLM を呼ぶ raise NotImplementedErrorレガシーに綺麗な DAG は期待できない。相互呼び出しやグローバル状態経由の依存で循環が残る。トポロジカルソートは強連結成分に縮約してから回し、循環部分は「まとめて1グループとして要約」に割り切ると止まらずに済む。
階層要約 ── 要約の要約で全体像を作る
個々のチャンク要約が揃っても、それはまだ部品リストにすぎない。人間が欲しいのは「このシステムは結局何をしているのか」という全体像だ。そこで要約を階層的に束ねる。関数要約 → 同じファイル/モジュールの要約群をまとめてモジュール要約 → モジュール要約群をまとめてサブシステム要約 → 最後にシステム全体像、と粒度を一段ずつ上げていく。各段で入力が要約済みテキストなので、元コードの総量が数十万行でも上位ノードの入力は十分小さく収まる。
| レベル | 入力 | 問い(プロンプトの主眼) |
|---|---|---|
| 関数 | コードチャンク+依存先要約 | 何を入力に何を出すか/隠れた業務ルールは |
| モジュール | 配下の関数要約群 | このモジュールの責務は一言で何か |
| サブシステム | 配下のモジュール要約群 | どの業務ドメインを担当し、何と連携するか |
| システム全体 | サブシステム要約群 | 全体のデータフローと、刷新の切り出し単位はどこか |
溢れ対策 ── コンテキストウィンドウは有限だと前提に置く
モデルのコンテキストウィンドウが大きくなっても、「全部詰め込めば読める」は幻想だ。入力が長いほど中盤の情報は取りこぼされやすく(いわゆる lost in the middle)、コストとレイテンシも膨らむ。「入り切るか」ではなく「その粒度で最良の要約が出るか」で分割点を決める。溢れそうな中間ノードは、さらに一段のサブグループに割って二段で束ねる。トークン数は事前に見積もり、閾値超過は自動で再分割にフォールバックさせておくと、巨大モジュールでパイプラインが落ちない。
- 1チャンク単位でトークン数を推定し、モデル上限の一定割合(例:入力の6〜7割)を実効上限に設定する
- 2束ねる要約群が実効上限を超えたら、サブグループに分けて中間要約を挟む(束ねの木を深くする)
- 3要約テキスト自体も冗長なら、次段に渡す前に「箇条書き数行」へ圧縮する専用パスを入れる
- 41チャンクが単体で上限を超える巨大関数は、論理ブロックで割ってから部分要約→統合する
幻覚を減らす ── 推測を明示させ、原文を引用させる
LLM 要約の最大のリスクは、自信たっぷりに間違えることだ。特にコード解析では、根拠のない「たぶんこういう処理」が混ざると、下流の判断を丸ごと誤らせる。効くのは2つ。(1)推測と事実を分けて書かせる、(2)主張の根拠となる原文の行を引用させる。引用を強制すると、引用できない=原文に根拠がない主張が炙り出され、LLM 自身も断定を控えるようになる。出力は自由文でなく、根拠フィールド付きの構造化フォーマットで受け取ると検証が機械化できる。
次の関数を要約してください。出力は JSON で、各キーに必ず evidence(根拠となる原文の行を原文ママで引用)を付けてください。
- purpose: この関数の目的
- io: 入力と出力
- business_rules: 読み取れる業務ルール(分岐・閾値・特別扱い)
- assumptions: 原文からは確認できず推測した点(必ず "推測" と明記)
原文に根拠が無い項目は evidence を空にし、確信度を low としてください。憶測で埋めないでください。
```
<対象コード>
```{
"purpose": "受注データに割引を適用して確定金額を計算する",
"io": {"in": "order dict", "out": "確定金額(int)"},
"business_rules": [
{"rule": "合計3万円以上で5%割引", "evidence": "if total >= 30000: total = int(total * 0.95)", "confidence": "high"}
],
"assumptions": [
{"note": "端数は切り捨てと推測(int変換より)。丸め規則の根拠はコードに無い", "evidence": "", "confidence": "low"}
]
}`validateUser()` の中で実は課金処理をしている、`temp_` 接頭辞のテーブルが本番の基幹データ、といった裏切りはレガシーの日常だ。LLM は名前に引きずられて要約する。名前ではなく本体の挙動に基づけと明示し、最終的には で実挙動と突き合わせて確定させる。
要約をナレッジベース化する ── RAG で引けるようにする
せっかく数十万行を読み解いても、成果が担当者の頭の中や散らばったメモに留まれば、次の改修でまた読み直しになる。要約とメタデータ(ファイルパス・シンボル・依存先・確信度)をベクトル化して蓄え、 で「請求の締め処理はどこ?」「この閾値を使っている箇所は?」と自然文で引ける状態にする。回答には必ず出典(元ファイルと行)を添えさせ、原文へ即座に飛べるようにするのが要点だ。これが積み上がると、レガシーは「誰も説明できない塊」から「質問すれば答えが返る資産」へ変わっていく。
要約ナレッジベースは、移行の設計・ による段階置き換え・障害調査のたびに繰り返し使える。読み解きを使い捨てにせず資産として残す発想が、二度目以降のコストを大きく下げる。
落とし穴 ── ここで足をすくわれる
最後に、実装より先に頭に入れておきたい失敗パターンを挙げる。技術より前に、この心構えのほうが効くことが多い。要約パイプラインは強力だが、それ自体が新たな を生む危うさも持っている。
- 要約の鵜呑み:LLM 要約は仮説であって仕様書ではない。検証工程を省くと、間違った地図を握って刷新に突っ込むことになる
- 命名への過信:関数名・テーブル名は挙動を保証しない。名前で判断させず本体を根拠にさせる
- チャンク境界の欠落:文脈を切られたチャンクは幻覚を招く。意味の単位で切り、依存要約を添える
- 要約の劣化コピー:階層で束ねるたびに情報は減衰する。重要な業務ルールは上位に伝播させるルールを持たせ、丸め落とさない
- を無視した最適化:読み解きに凝りすぎて、そもそも塩漬け続行か刷新かの判断(前回のテーマ)を後回しにしない
ふくふくでは、御社のコード規模と機密要件に合わせて、この要約パイプラインの設計・構築から支援している。詳しくは レガシー刷新・資産化支援 を参照。まず小さなモジュールで要約→検証→ナレッジ化を一巡させ、精度と手応えを確かめてから広げるのが安全だ。
本連載は、レガシー再生の現場で実際に踏む手順を1歩ずつ言語化していく。前回のコード考古学の6ステップと本稿の要約パイプラインが、読み解きフェーズの両輪だ。続編は読者リアクションに応じて随時追加していく。要約の検証手法、RAG の設計、段階移行の実装など、掘り下げてほしいテーマがあればフィードバックで教えてほしい。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。