Python主導LLMアプリケーションの設計スタイルを考える
はじめに
本記事は、「Pythonが制御を握り、LLMが知性を提供する」という設計思想を、プロトタイプで試した際のアーキテクチャパターンをまとめたものです。
前提知識:
本記事で扱う内容:
- LLMとPythonの役割分担
- Phase分離による処理の構造化
- 各Phaseの責務と境界
- データフローの設計
本記事に含まれないもの:
- 数値比較・ベンチマーク(→ 比較分析レポートを参照)
- 実装コードの全体像(概念的な説明、部分的な説明のみ)
注意:
これは小規模プロトタイプでの実験結果に基づく整理です。本番運用では別のアプローチが適している場合もあります。
システム概要:自然言語検索の実現
提供機能
仮想のアプリケーションとして、AWS What's New情報を自然言語で検索するパイプラインを想定しています。実装も確認済みですが、この記事では概念的なアーキテクチャにフォーカスします。

検証用Webアプリ - 裏でPython主導LLMアプリケーションが動作
検索例:
✅ "先月の東京のEC2"
→ 2025年12月の ap-northeast-1 での Amazon EC2 関連アップデート
✅ "クリスマス頃のASEANでコンテナ関連を要約"
→ 2025年12月中旬~下旬の東南アジアでのECS/EKS関連アップデートの要約
3つの検索軸
ユーザー入力は3つの検索軸に分解され、検索が実行されます。
| 軸 | 自然言語表現例 | 解釈内容 |
|---|---|---|
| 時間 | 「先月」「クリスマス頃」「最近3ヶ月」 | 相対表現・イベント基準・具体日付 |
| サービス | 「EC2」「機械学習」「コンテナ」 | 略称・カテゴリ・複数指定 |
| リージョン | 「東京」「ASEAN」「アジア」 | 都市名・組織名・地域名 |
対応範囲:
- 順不同・複数指定・部分指定に対応
- 相対表現(「先月」)、イベント(「クリスマス頃」)、組織名(「ASEAN」)にも対応
Phase分離アーキテクチャ
ここで一度、今回のアーキテクチャ全体像を図で整理します。
以下は、LLM主導(Agent型)と、本記事で採用している Python主導(パイプライン型)の設計スタイルの違いを示したものです。

Python主導パイプライン型アーキテクチャの全体像
この図の右側に示した Python主導のパイプラインを、次節以降で Phase 単位に分解して説明していきます。
設計の考え方
今回のアプローチは、Pythonが処理フローを制御することです。整理のポイントは:
- 制御フローはPythonで明示的に定義
- 各Phaseは単一責任
- Phase間のデータフローは構造化データ
- LLMの役割は各Phaseで限定
4つのPhaseと責務
| Phase | 責務 | 入力 | 出力 | 技術選択 |
|---|---|---|---|---|
| Phase 1 | 自然言語の構造化 | 自然言語クエリ | 構造化された意図 | LLM |
| Phase 2 | キーワード解決 | 標準化された値 | 検索条件 | Python(ルール+グラフ探索) + LLMフォールバック |
| Phase 3 | データベース検索 | 検索条件 | 検索結果 | PythonでSQL組み立て+実行 |
| Phase 4 | 結果整形 | 検索結果 | ユーザー向け出力 | Pythonテンプレート or LLM |
LLMをPythonの制御フローに組み込めた理由
本プロトタイプでは、Strands Agentsが提供する構造化出力(Structured Output)機能を利用しています。 この機能を使うことで、すべてのPhaseにおいて、LLMの戻り値を明示的に定義された構造化データとして扱うことができます。
構造は pydantic で定義され、LLMの出力はそのスキーマに一致する形で生成されます。
# Pydantic モデルで戻り値の構造を定義
class IntentResult(BaseModel):
time_expression: Optional[str]
region_keywords: Optional[list[str]]
service_keywords: Optional[list[str]]
action: Literal["list", "count", "latest", "summarize", "analyze"]
max_results: int
# LLM 呼び出し時にスキーマを指定
agent = create_intent_understanding_agent()
invocation = agent(
user_request,
structured_output_model=IntentResult # ← スキーマを強制
)
# 戻り値は必ず IntentResult 型として取得可能
intent: IntentResult = invocation.structured_output
print(intent.service_keywords) # type-safe
この仕組みにより、Pythonで処理した場合でも、LLMにフォールバックした場合でも、戻り値の型を常に揃えられるという状態が成立します。その結果、制御フローをPython側に固定したまま、LLMをPythonコードの中に安全に埋め込むことが可能になりました。
これが、構造化出力をすべてのLLM呼び出しで採用した最大の理由です。
Phase 1: Intent Understanding
自然言語クエリを構造化データに変換
入力: "先月の東京のEC2"
↓ LLM処理
出力: {
time_expression: "last month",
region_keywords: ["Tokyo"],
service_keywords: ["EC2"],
action: "list"
}
ポイント:
- 順不同入力にも対応
- 表現揺れの吸収(EC2, Elastic Compute Cloudなど)
- 部分指定もOK
- 再現性確保のためtemperature=0でLLM使用
Phase 2: Keyword Resolution
Phase 2a: 時間表現の正規化
入力: "last month" (+ 現在時刻)
↓ Rule-based処理
出力: {
date_from: "2025-12-01",
date_to: "2026-01-01"
}
実装:
- Fast Path: ルールベースで高速処理
- Slow Path: 未定義表現はLLMにフォールバック
Phase 2b: キーワード拡張・解決
入力: {region_keywords: ["Tokyo"], service_keywords: ["EC2"]}
↓ Graph探索 + LLMフォールバック
出力: {
region_codes: ["ap-northeast-1"],
service_keywords: ["EC2", "Amazon EC2", "Elastic Compute Cloud", ...]
}
- 頻出パターンはPythonで高速処理
- 複雑な表現はLLMで解決
- グラフ探索で関係性を展開
Phase 3: Search Execution
構造化検索条件でデータベースを検索
入力: {
date_from: "2025-12-01",
date_to: "2026-01-01",
region_codes: ["ap-northeast-1"],
service_keywords: ["EC2", ...]
}
↓ SQL実行
出力: {
items: [{id: "item-001", title: "...", url: "...", service: "EC2"}, ...]
}
- PythonでSQL組み立て+実行
- 再現性・高速性・デバッグ容易性を確保
Phase 4: Result Formatting
検索結果をユーザー向けに整形
- 単純なlist/count/latest → Pythonテンプレートで高速出力
- 複雑な要約・分析 → LLMで自然言語生成
入力: {items: [...]}
↓ Pythonテンプレート展開
出力:
## 検索結果: 5件
- [EC2 GameLift Streams](...)
- [RDS Zero-ETL to EC2](...)
入力: {items: [11件のアップデート情報]}
↓ LLM要約
出力:
## 要約
ASEAN地域では、主にAuroraとDynamoDBの機能拡張が行われました。...
データフローの全体像
Phase 1 → IntentResult →
Phase 2 → KeywordResolutionResult →
Phase 3 → SearchResult →
Phase 4 → FormattedOutput
- Phase間は構造化データで接続
- 型安全・デバッグ容易・疎結合を実現
まとめ
設計原則:
- Pythonが制御
- LLMは知性提供に徹する
- 決定論的処理を最大化
効果:
- 高速性
- 再現性
- コスト効率
- 柔軟性
- 保守性
- デバッグ容易性
注意: 小規模プロトタイプの結果であり、本番運用では別アプローチが必要な場合もあります。
次のステップ
Discussion