8月15日、Towards Data Scienceが「RAG Workflow and Loop Engineering: The Dispatcher That Decides When to Loop and When to Stop」と題した記事を公開した。Agentic RAGの設計において「ループをLLMに任せてエージェントに判断させる」のが近年の主流だが、この記事はその流れに逆らう。ループの起動・停止判断をすべてコードで書き下すという方針を採り、LangGraphやCrewAIのようなエージェントフレームワークとは対照的に、監査可能性と保守性を優先したDispatcherパターンを詳述している。
「いつ止めるか」を決める者がいない問題
RAGパイプラインでループパターンを一つ一つ実装することは難しくない。ページ取得に失敗したら再パース、別セクションを参照していたら再検索、質問がリスト全体を求めていたら集約ループ——それぞれは単体でうまく動く。
問題は、実際のユーザーがそれらを同時に要求してくる点だ。たとえばNIST Cybersecurity Frameworkに対して「GOVERNの全カテゴリを列挙し、サプライチェーンリスクをカバーするのはどれか」と問われると、TOC検索・リスト集約・合成の3パターンが同時に発火する。それぞれが独自の反復ロジックを持ち、「どのループがいつ止まるかを決めるのか」が宙に浮く。
今日の流行解答は「エージェントに任せてLLMに判断させる」だ。LangGraphはグラフのエッジ条件をLLMの出力で動的に切り替え、CrewAIはエージェント同士の委譲でループを制御する。こうしたアプローチは柔軟性が高い反面、なぜそのループが止まったかを事後にトレースしにくいという問題を抱える。この記事が採る方針は異なる。人間が読めるコードで判断を書き下す——具体的には、Dispatcher(どのパターンを起動するかを決める)と有界ループ(何回まで反復するかを明示する)の組み合わせだ。
5段階の制御レベル:この記事の位置付け
記事はまず、同一のPDF QA関数を段階的に強化する5段ロケット構造を示す。ループ制御権がコードにあるかLLMにあるかで、監査可能性の性質が大きく変わるという文脈を示すためだ。
| レベル | 制御の在り処 | 概要 |
|---|---|---|
| Baseline | コード | キーワード検索で1パス、即リターン |
| Upgraded | コード | フル関係パース+TOCルーティング、フィードバックフィールド付き回答を生成(ただし未活用) |
| Workflow(本記事) | コード | Dispatcherがパターンを選択し、有界ループがフィードバックを読んでリトライを制御 |
| Multi-intent | コード | チャットエントリが意図分類し、適切なパイプラインへルーティング |
| Agentic | LLM | 次のステップをLLM自身が選ぶ |
4段目まではコードがループを握り、5段目だけLLMが制御を持つ。この境界線こそが、監査可能性の分岐点だと記事は強調する。
Dispatcher:ループを起動する判断をコードに書く
Dispatcherの核心はdecide_pipeline_patterns関数だ。ParsedQuestion(質問をパースして構造化したオブジェクト。質問タイプ・対象エンティティ・リスト要求の有無などを保持する)とDocumentProfile(文書の種別・構造的特徴をまとめたプロファイル。TOCの有無、セクション深度等を含む)を入力に取り、各パターンのON/OFFフラグ(Activations)を返す。
def pdf_qa_loop(pdf_path, question, *, registry=None, max_iterations=4):
registry = registry or PatternRegistry.default()
parsed = registry.parse_question(question)
line_df, page_df, toc_df = registry.parse_layer1(pdf_path)
doc_profile = registry.detect_document_type(pdf_path)
# Dispatcherがパターンを決定
activations = decide_pipeline_patterns(parsed, doc_profile)
state = _State(parsed=parsed, line_df=line_df,
page_df=page_df, toc_df=toc_df)
outcome = iterate_with_bound(
initial_state=state,
run_pass=lambda s: _one_pass(s, registry, activations, pdf_path),
is_satisfactory=lambda a: not _needs_iteration(a),
adjust=lambda s, a: _adjust_state(s, a, registry, activations, pdf_path),
record=_build_record,
max_iterations=max_iterations,
)
return CompositeOutput(answer=outcome.result, activations=activations,
history=outcome.history,
exhausted=outcome.exhausted)
これがオーケストレーター全体だ。pdf_qa_loop本体は「接着コード1ページ」とも表現されており、各パターンモジュールの上に薄く乗る形になっている。
実行順序も明示的に定義されている:TOC検索→キーワード検索→一次検索→2ホップ解決(「GovDoc第3節参照」のような間接参照を自動解決する機構)の順。構造的アンカーを先に確立してから詳細検索に進む設計だ。
iterate_with_boundは反復上限をハードキャップ4回として持ち、is_satisfactoryがtrueを返した時点でループを打ち切る。上限に達してもループが収束しなかった場合はexhausted=trueフラグが立ち、呼び出し元が検知できる。記事によれば2〜3回の反復で大半のケースはカバーできるとされており、4回は安全マージンだ。
フィードバックループの設計:「暫定出力」を自己批評する
従来のナイーブRAGは4ブロック(パース→質問パース→検索→生成)を直列につないで結果を返すだけだ。検索が外れても、パースが不十分でLLMが幻覚を起こしても、ユーザーは気づかない。
この記事のパイプラインは生成ステップの後に批評ステップを追加し、2本のフィードバックレールを設ける。
complete_answer_found=false→ キーワードを拡張して再検索context_structured=false→ 問題ページを再パース
これらは「大きいループ」(処理ブロックをまたぐ)であり、各ブロック内部の「小さいループ」(画像カスケード、TOC降下、スキーマリトライ等)とは区別される。Dispatcherが管理するのは大きいループのみであり、小さいループは各モジュールが自律的に処理する。
反復の優先順序は「パース修正→語彙拡張→参照解決→リスト集約」が推奨されている。パースが崩れた状態で再検索しても無駄になるためだ。各反復の結果はIterationRecord(1回のループパスで収集されたコンテキスト・スコア・フィードバックフラグをまとめた記録オブジェクト)として蓄積され、historyフィールドに残るため、後から「何回目の反復でどのフラグが変化したか」を完全にトレースできる。
アーキテクチャ:レイヤー分離で保守性を確保
パターンが増えても保守できるよう、コードベースのレイアウトも明示されている。
- 4ブロック(パース・検索・生成等)はそれぞれ独立したモジュールに残す
- 組み合わせロジックだけを
pipeline/フォルダに集約 - 各レイヤー間の契約は型付きオブジェクト(
ParsedQuestion、DocumentProfile、AnswerWithEvidence)で定義
TOC検索を改善するなら検索モジュールだけ変更し、2ホップ解決のバグを直してもオーケストレーターには触れない。「シニアエンジニアが入力から強調表示付き回答まで数分でトレースできる」状態が設計目標として明記されている。LLMが制御を握るAgentic構成と比較して、このトレーサビリティは本パターンの最大の強みといえる。
コンパニオンノートブックで手を動かせる
記事に対応するGitHubリポジトリ(doc-intel/notebooks-vol1)が公開されており、pdf_qa_loopを実行してIterationRecordの履歴を確認し、is_satisfactoryがどのタイミングでループを打ち切るかを実際に確認できる。
詳細はRAG Workflow and Loop Engineering: The Dispatcher That Decides When to Loop and When to Stopを参照していただきたい。