LLMを使ったアプリを書き始めると、最初は1つのファイルで収まる。しかしプロンプトが増え、APIが増え、機能が増えるにつれてコードは絡み合い、どこに何があるかわからなくなる。

この記事でわかること:

  • LLMアプリのコードがどこで複雑化しやすいか
  • 小〜中規模向けのシンプルな構成例
  • 複数モデル・複数機能に対応した発展構成
  • プロンプト・設定・APIクライアントを分離する具体的なやり方

1ファイルで書き続けると起きること

LLMを呼び出すコードを1ファイルに詰め込むと、次の問題が起きやすい。

プロンプトがコードの中に文字列としてハードコードされているため、変更するたびにロジックの近くを触ることになる。Claude用とGPT用でAPIの呼び出し方が違うにもかかわらず、同じ関数の中に分岐が積み重なっていく。設定値(APIキー、モデル名、temperature)が散在し、どこを変えればよいか一目でわからない。

これらは「関心の分離」ができていないことによる症状だ。LLMアプリには、API呼び出し・プロンプト管理・ビジネスロジック・データ処理・設定管理という5つの独立した責務がある。それぞれを別のファイルやモジュールに切り出すことで、見通しがよくなる。

基本構成:小〜中規模向け

https://apxml.com/courses/prompt-engineering-llm-application-development/chapter-8-application-development-considerations/structuring-llm-application-code

スクリプト1〜3本程度の小さなアプリなら、以下のフラット構成が出発点になる。

my_llm_app/
├── app.py            # メインのアプリロジック
├── config.py         # 設定の読み込み(環境変数、モデル名など)
├── llm_client.py     # LLM APIの呼び出しをまとめた関数・クラス
├── prompt_utils.py   # プロンプトの読み込み・フォーマット処理
├── prompts/          # プロンプトテンプレートファイルを置くフォルダ
│   ├── summarize.txt
│   └── qa.txt
├── utils.py          # 汎用ユーティリティ(出力パース等)
├── .env              # 環境変数(.gitignoreに追加する)
└── requirements.txt

ポイントは llm_client.pyprompt_utils.py を分けることだ。APIの呼び出し方の詳細(認証、リトライ、パラメータ)を llm_client.py に集約し、プロンプトの中身には触れない設計にする。

発展構成:複数機能を持つアプリ向け

要約・Q&A・RAGなど複数の機能を持つアプリは、次のような階層構造にする。

advanced_llm_app/
├── main.py                    # エントリーポイント
├── core/                      # 共通基盤コンポーネント
│   ├── config.py              # 設定読み込み
│   ├── llm_interface.py       # LLM呼び出しの抽象化
│   ├── prompt_manager.py      # プロンプトの一元管理
│   └── output_parser.py       # 出力パース
├── modules/                   # 機能ごとのモジュール
│   ├── qa/                    # Q&A機能
│   │   ├── chain.py
│   │   └── prompts/
│   │       └── retrieval_qa.yaml
│   └── summarization/         # 要約機能
│       ├── service.py
│       └── prompts/
│           └── condense.txt
├── shared/
│   └── data_models.py         # Pydanticモデル
├── tests/
├── .env
└── requirements.txt

core/ に共通基盤を、modules/ に機能ごとの実装を置く。各モジュールが自分のプロンプトファイルを持つ設計にすると、機能追加・削除の影響範囲が明確になる。

LLMクライアントを分離する理由

ClaudeとGPTでは呼び出し方が違う。エンドポイント、パラメータ名、レスポンスの形式がそれぞれ異なる。これらの差異を llm_interface.py の中に閉じ込めておくことで、ビジネスロジック側は「どのモデルを使っているか」を意識せずに済む。

# core/llm_interface.py の例
class LLMClient:
    def __init__(self, provider: str, model: str):
        self.provider = provider
        self.model = model

    def complete(self, prompt: str, **kwargs) -> str:
        if self.provider == "anthropic":
            return self._call_claude(prompt, **kwargs)
        elif self.provider == "openai":
            return self._call_gpt(prompt, **kwargs)

    def _call_claude(self, prompt, **kwargs): ...
    def _call_gpt(self, prompt, **kwargs): ...

ここにレート制限のリトライ処理やリクエストのログ記録も加えると、デバッグが格段に楽になる。

プロンプトを外部ファイルに切り出す

プロンプトをPythonコードの中に直接書くと、プロンプトを変えるたびにコードのテストが必要になる。外部ファイル(.txt.yaml.json)に切り出しておくと、ロジックを変えずにプロンプトだけを変更・評価できる。

# core/prompt_manager.py の例
from pathlib import Path
from jinja2 import Template

class PromptManager:
    def __init__(self, prompts_dir: str = "prompts"):
        self.prompts_dir = Path(prompts_dir)

    def load(self, name: str, **variables) -> str:
        template_text = (self.prompts_dir / name).read_text()
        return Template(template_text).render(**variables)

Jinja2を使うと、{{ topic }} のようなプレースホルダーで動的な値を差し込める。変数を受け取る箇所が明示されるため、プロンプトの仕様が読みやすくなる。

設定と環境変数の扱い

APIキーをコードに直接書くのは論外だが、モデル名やtemperatureなどの設定値もコードに散らばりやすい。config.py で一元管理する。

# config.py の例
import os
from dataclasses import dataclass

@dataclass
class Config:
    anthropic_api_key: str = os.getenv("ANTHROPIC_API_KEY", "")
    openai_api_key: str = os.getenv("OPENAI_API_KEY", "")
    default_model: str = os.getenv("DEFAULT_MODEL", "claude-sonnet-4-6")
    temperature: float = float(os.getenv("TEMPERATURE", "0.7"))
    max_tokens: int = int(os.getenv("MAX_TOKENS", "2048"))

config = Config()

.env ファイルに値を書いて python-dotenv で読み込む方法が一般的だ。.env は必ず .gitignore に追加する。

まとめ

LLMアプリのコードが複雑になる前に構成を整えることで、後からの変更コストを大きく下げられる。最初から完璧な構成を目指す必要はない。

まずは llm_client.py(API呼び出し)と prompts/(プロンプトファイル)と config.py(設定)を分けるだけでも、コードの見通しは格段に改善する。アプリが成長するにつれて、core/modules/ の階層へ移行していけばよい。