Skip to content
Published on

LLMプロンプトエンジニアリング高度技法: Chain-of-Thought・ReAct・Tree of Thoughts 実践適用

Authors
  • Name
    Twitter
LLM Prompt Engineering

はじめに

プロンプトエンジニアリングは、LLMの性能を最大化するための中核技術である。単に質問を投げかけることを超えて、モデルがどのように推論し応答するかを体系的に設計するエンジニアリング分野へと発展した。2022年のGoogleによるChain-of-Thought論文を皮切りに、ReAct、Self-Consistency、Tree of Thoughtsなど多様な高度プロンプティング技法が登場し、LLMの推論能力を飛躍的に向上させてきた。

本記事では、各技法の理論的背景と動作原理を分析し、Pythonの実装コードとともに本番環境で実際に適用する戦略を解説する。特に構造化出力(JSON Mode、Function Calling)、プロンプトテンプレート管理、評価方法論まで包括的に取り上げる。

プロンプトエンジニアリングの基本原則

効果的なプロンプトを設計するためには、以下の4つの原則を理解する必要がある。

1. 明確性 (Clarity)

曖昧な指示を避け、具体的な要件を明示する。「良い記事を書いて」ではなく、「500文字以内の技術ブログの導入部を作成してください。対象読者はジュニアエンジニアで、比喩を使って概念を説明してください」のように記述する。

2. 構造化 (Structure)

役割(Role)、文脈(Context)、指示(Instruction)、出力形式(Format)を明確に分離してプロンプトを構成する。

3. 制約条件 (Constraints)

出力の長さ、形式、トーン、禁止事項などを明示して、望ましい方向に応答を誘導する。

4. 反復改善 (Iteration)

プロンプトは一度で完成しない。出力結果を分析し、継続的に修正・最適化する。

Few-shotプロンプティング戦略

Few-shotプロンプティングは、プロンプトに少数の入出力例を含めることで、モデルがパターンを学習するよう誘導する技法である。Zero-shotと比較して15~25%の精度向上が期待でき、特に一貫したフォーマットの出力が必要なタスクで効果的だ。

from openai import OpenAI

client = OpenAI()

def few_shot_sentiment_analysis(text: str) -> str:
    """Few-shotプロンプティングを活用した感情分析"""
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "与えられたテキストの感情を分析し、ポジティブ/ネガティブ/ニュートラルに分類してください。"
            },
            # Few-shot例 1
            {"role": "user", "content": "この製品は本当に素晴らしいです!配送も早く品質も良いです。"},
            {"role": "assistant", "content": "感情: ポジティブ\n根拠: '素晴らしい'、'早く'、'良い'などポジティブな表現が多数含まれている"},
            # Few-shot例 2
            {"role": "user", "content": "配送が非常に遅く、製品に傷がありました。"},
            {"role": "assistant", "content": "感情: ネガティブ\n根拠: '非常に遅く'、'傷がありました'などネガティブな体験を述べている"},
            # Few-shot例 3
            {"role": "user", "content": "普通レベルの製品です。価格相応です。"},
            {"role": "assistant", "content": "感情: ニュートラル\n根拠: '普通レベル'、'価格相応'などバランスの取れた評価"},
            # 実際の分析対象
            {"role": "user", "content": text}
        ],
        temperature=0.0
    )
    return response.choices[0].message.content

Few-shot設計のコツ:

  • 例は3~5個が最適。多すぎるとコンテキストの無駄、少なすぎるとパターン学習不足
  • 例の多様性を確保する。ポジティブ/ネガティブ/ニュートラル、短文/長文など
  • エッジケースを例に含める
  • 例の順序が結果に影響する場合があるため、ランダムな配置を検討する

Chain-of-Thought (CoT) 深層分析

Chain-of-Thoughtは2022年にGoogleのWeiらの論文で提案された技法で、LLMが最終回答に到達する前に中間推論ステップを明示的に生成するよう誘導する。複雑な推論タスクで精度を40~60%向上させることが知られている。

Zero-shot CoT

最もシンプルな形式で、「ステップバイステップで考えましょう(Let's think step by step)」という一文を追加するだけでよい。

Few-shot CoT

推論過程を含む例を提供して、より精緻な段階的思考を誘導する。

def chain_of_thought_math(problem: str) -> str:
    """Chain-of-Thoughtを活用した数学問題の解決"""
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "あなたは数学の問題をステップバイステップで解くチューターです。"
                    "各ステップを明確に説明し、最終回答を提示してください。"
                )
            },
            # CoT例: 推論過程を含む
            {
                "role": "user",
                "content": "カフェに客が23人いました。午前に12人が来て、午後に8人が帰りました。現在カフェには何人いますか?"
            },
            {
                "role": "assistant",
                "content": (
                    "ステップバイステップで解いていきます。\n\n"
                    "ステップ1: 初期人数の把握\n"
                    "- 最初にカフェにいた客: 23人\n\n"
                    "ステップ2: 午前の変動を計算\n"
                    "- 12人が追加で来店: 23 + 12 = 35人\n\n"
                    "ステップ3: 午後の変動を計算\n"
                    "- 8人が退店: 35 - 8 = 27人\n\n"
                    "最終回答: 現在カフェには27人がいます。"
                )
            },
            {"role": "user", "content": problem}
        ],
        temperature=0.0
    )
    return response.choices[0].message.content

高度なCoTバリエーション

  • Auto-CoT: 多様な質問を自動的にサンプリングし、推論チェーンを生成してデモンストレーションを構成
  • Layered CoT: 推論を複数パスに分割し、各段階で検証・修正の機会を提供
  • Trace-of-Thought: 小規模モデル(7Bパラメータ)向けに最適化されたCoTで、サブ問題を生成して算術推論を改善

ReActフレームワーク実装

ReAct(Reasoning + Acting)は2022年にYaoらが提案したフレームワークで、LLMが思考(Thought)行動(Action)、**観察(Observation)**のループを繰り返しながら外部ツールと相互作用して問題を解決するパラダイムである。HotpotQAで幻覚(hallucination)を大幅に削減し、ALFWorldベンチマークで既存手法より34%高い成功率を達成した。

import json
import requests
from openai import OpenAI

client = OpenAI()

# ツール定義
def search_wikipedia(query: str) -> str:
    """Wikipedia検索ツール"""
    url = "https://ja.wikipedia.org/w/api.php"
    params = {
        "action": "query",
        "list": "search",
        "srsearch": query,
        "format": "json",
        "srlimit": 3
    }
    resp = requests.get(url, params=params)
    results = resp.json().get("query", {}).get("search", [])
    if not results:
        return "検索結果がありません。"
    return "\n".join([r["title"] + ": " + r["snippet"] for r in results])


def calculator(expression: str) -> str:
    """安全な数式計算機"""
    allowed_chars = set("0123456789+-*/.(). ")
    if all(c in allowed_chars for c in expression):
        try:
            result = eval(expression)
            return str(result)
        except Exception as e:
            return f"計算エラー: {e}"
    return "許可されていない文字が含まれています。"


TOOLS = {
    "search_wikipedia": search_wikipedia,
    "calculator": calculator,
}


def react_agent(question: str, max_steps: int = 5) -> str:
    """ReActパターンを実装したエージェント"""
    system_prompt = """あなたはReActエージェントです。以下の形式で応答してください:

Thought: 現在の状況を分析し、次に何をすべきか推論します。
Action: 使用するツールと入力をJSON形式で提供します。
Observation: ツール実行結果を確認します。
... (必要な分だけ繰り返し)
Final Answer: 最終回答を提示します。

使用可能なツール:
- search_wikipedia: Wikipedia検索(入力: 検索クエリ)
- calculator: 数式計算(入力: 数学表現式)"""

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": question}
    ]

    for step in range(max_steps):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            temperature=0.0
        )
        assistant_msg = response.choices[0].message.content

        # Final Answerが含まれていれば終了
        if "Final Answer:" in assistant_msg:
            return assistant_msg.split("Final Answer:")[-1].strip()

        # Actionのパースと実行
        if "Action:" in assistant_msg:
            action_line = assistant_msg.split("Action:")[-1].strip()
            try:
                action_data = json.loads(action_line.split("\n")[0])
                tool_name = action_data.get("tool", "")
                tool_input = action_data.get("input", "")

                if tool_name in TOOLS:
                    observation = TOOLS[tool_name](tool_input)
                else:
                    observation = f"不明なツール: {tool_name}"
            except json.JSONDecodeError:
                observation = "Actionのパースに失敗"

            messages.append({"role": "assistant", "content": assistant_msg})
            messages.append({
                "role": "user",
                "content": f"Observation: {observation}"
            })

    return "最大ステップ数に到達し、回答を生成できませんでした。"

ReActの利点:

  • 推論過程が透明で、デバッグと監査が容易
  • 外部ツール連携により幻覚問題を大幅に緩和
  • エージェントシステムの基盤アーキテクチャとして広く採用

Self-Consistencyサンプリング

Self-ConsistencyはWangら(2022)が提案したデコーディング戦略で、同一プロンプトに対して複数の推論パスを生成し、多数決(majority voting)で最終回答を選択する。CoTと組み合わせるとGSM8Kで17.9%、AQuAで12.2%の精度向上を達成した。

核心的なアイデアはシンプルである。複雑な問題には複数の有効な解法が存在し、それらが同じ正解に収束するならば、その答えが正確である確率が高いということだ。

from collections import Counter
from openai import OpenAI

client = OpenAI()


def self_consistency_solve(
    question: str,
    num_samples: int = 5,
    temperature: float = 0.7
) -> dict:
    """Self-Consistencyサンプリングによる精度向上推論"""
    system_prompt = (
        "数学の問題をステップバイステップで解いてください。"
        "最後の行に '回答: [数値]' の形式で答えを記載してください。"
    )
    answers = []
    reasoning_paths = []

    for i in range(num_samples):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": question}
            ],
            temperature=temperature  # 多様な推論パスを生成
        )
        content = response.choices[0].message.content
        reasoning_paths.append(content)

        # 回答抽出
        for line in content.split("\n"):
            if line.strip().startswith("回答:"):
                answer = line.split("回答:")[-1].strip()
                answers.append(answer)
                break

    # 多数決投票
    if not answers:
        return {"final_answer": "回答抽出失敗", "confidence": 0.0}

    counter = Counter(answers)
    most_common = counter.most_common(1)[0]
    confidence = most_common[1] / len(answers)

    return {
        "final_answer": most_common[0],
        "confidence": confidence,
        "vote_distribution": dict(counter),
        "num_samples": num_samples,
        "reasoning_paths": reasoning_paths
    }


# 使用例
result = self_consistency_solve(
    "学校に生徒が450人います。男子が全体の60%で、"
    "男子の25%がメガネをかけています。メガネをかけている男子は何人ですか?",
    num_samples=7
)
print(f"最終回答: {result['final_answer']}")
print(f"信頼度: {result['confidence']:.1%}")
print(f"投票分布: {result['vote_distribution']}")

Self-Consistency最適化のコツ:

  • temperatureを0.5~0.9の範囲で設定し、多様性と品質のバランスを取る
  • サンプル数は5~10個がコスト効率が良い
  • Universal Self-Consistency(USC)は自由形式テキスト生成にも適用可能

Tree of Thoughts (ToT)

Tree of ThoughtsはYaoら(2023)が提案した技法で、CoTを拡張して複数の推論パスをツリー構造で探索・評価する。Game of 24タスクでGPT-4のCoT精度が4%だったものがToT適用後74%に急上昇した。

ToTの中核構成要素:

  • Thought Decomposition: 問題を中間思考ステップに分解
  • Thought Generator: 各ノードで候補思考を生成(サンプリングまたは提案)
  • State Evaluator: 各思考状態の有望性を評価
  • Search Algorithm: BFS(幅優先)またはDFS(深さ優先)探索
from openai import OpenAI
from typing import List

client = OpenAI()


def generate_thoughts(problem: str, current_state: str, n: int = 3) -> List[str]:
    """現在の状態から可能な次の思考を生成"""
    prompt = f"""問題: {problem}
現在までの推論: {current_state}

この状態から可能な次の推論ステップを{n}個提案してください。
各ステップは異なるアプローチでなければなりません。
形式:
1. [最初のアプローチ]
2. [2番目のアプローチ]
3. [3番目のアプローチ]"""

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.8
    )
    content = response.choices[0].message.content
    thoughts = []
    for line in content.split("\n"):
        line = line.strip()
        if line and line[0].isdigit() and "." in line:
            thoughts.append(line.split(".", 1)[1].strip())
    return thoughts[:n]


def evaluate_thought(problem: str, thought_path: str) -> float:
    """推論パスの有望性を0~1の範囲で評価"""
    prompt = f"""問題: {problem}
推論パス: {thought_path}

この推論パスが正解に到達する可能性を評価してください。
0.0(全く有望でない)~ 1.0(非常に有望)の範囲でスコアを提示してください。
スコアのみ数値で回答してください。"""

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.1
    )
    try:
        score = float(response.choices[0].message.content.strip())
        return min(max(score, 0.0), 1.0)
    except ValueError:
        return 0.5


def tree_of_thoughts_bfs(
    problem: str,
    max_depth: int = 3,
    beam_width: int = 2
) -> str:
    """BFSベースのTree of Thoughts実装"""
    # 初期状態
    current_states = [("", 1.0)]  # (推論パス, スコア)

    for depth in range(max_depth):
        all_candidates = []

        for state, _ in current_states:
            # 各状態から候補思考を生成
            thoughts = generate_thoughts(problem, state)

            for thought in thoughts:
                new_path = f"{state}\nステップ {depth+1}: {thought}" if state else f"ステップ 1: {thought}"
                score = evaluate_thought(problem, new_path)
                all_candidates.append((new_path, score))

        # beam_width分の上位候補を選択
        all_candidates.sort(key=lambda x: x[1], reverse=True)
        current_states = all_candidates[:beam_width]

        print(f"深さ {depth+1}: 最高スコア = {current_states[0][1]:.2f}")

    # 最終回答生成
    best_path = current_states[0][0]
    final_prompt = f"""問題: {problem}
推論パス: {best_path}

上記の推論に基づいて最終回答を作成してください。"""

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": final_prompt}],
        temperature=0.0
    )
    return response.choices[0].message.content

ToT適用時の注意点:

  • API呼び出し回数が指数的に増加するため、コストを慎重に管理する必要がある
  • 単純な問題には過度な技法である。複雑な計画、創造的な文章作成、パズル解きに適している
  • beam_widthとmax_depthを問題の複雑さに合わせて調整する

構造化出力(JSON Mode, Function Calling)

本番環境ではLLMの出力をプログラマティックに処理する必要がある。2026年現在、主要LLMプロバイダーがネイティブの構造化出力をサポートしており、Pydanticなどのバリデーションライブラリと組み合わせることで堅牢なパイプラインを構築できる。

JSON Modeの活用

from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional

client = OpenAI()


class CodeReview(BaseModel):
    """コードレビュー結果スキーマ"""
    file_path: str = Field(description="レビュー対象ファイルパス")
    severity: str = Field(description="重要度: critical, warning, info")
    category: str = Field(description="カテゴリ: security, performance, style, logic")
    line_number: Optional[int] = Field(description="該当行番号")
    message: str = Field(description="レビューコメント")
    suggestion: str = Field(description="改善提案コード")


class CodeReviewResult(BaseModel):
    """全体コードレビュー結果"""
    reviews: List[CodeReview]
    summary: str = Field(description="レビュー要約")
    overall_score: int = Field(description="総合スコア (1-10)")


def structured_code_review(code: str, language: str = "python") -> CodeReviewResult:
    """構造化出力を活用したコードレビュー"""
    response = client.beta.chat.completions.parse(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    f"あなたはシニア{language}開発者です。"
                    "与えられたコードをレビューし、構造化された形式でフィードバックを提供してください。"
                )
            },
            {"role": "user", "content": f"以下のコードをレビューしてください:\n\n{code}"}
        ],
        response_format=CodeReviewResult
    )
    return response.choices[0].message.parsed

Function Callingパターン

Function CallingはLLMが事前定義された関数を呼び出せるようにし、外部システムとの安定した統合を実現する。

import json
from openai import OpenAI

client = OpenAI()

# ツールスキーマ定義
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_stock_price",
            "description": "株式の現在価格を取得します",
            "parameters": {
                "type": "object",
                "properties": {
                    "symbol": {
                        "type": "string",
                        "description": "株式シンボル(例: AAPL, GOOGL)"
                    },
                    "currency": {
                        "type": "string",
                        "enum": ["USD", "KRW", "JPY"],
                        "description": "表示通貨"
                    }
                },
                "required": ["symbol"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_alert",
            "description": "株価アラートを設定します",
            "parameters": {
                "type": "object",
                "properties": {
                    "symbol": {"type": "string"},
                    "target_price": {"type": "number"},
                    "direction": {
                        "type": "string",
                        "enum": ["above", "below"]
                    }
                },
                "required": ["symbol", "target_price", "direction"]
            }
        }
    }
]


def function_calling_agent(user_message: str) -> str:
    """Function Callingを活用したエージェント"""
    messages = [
        {
            "role": "system",
            "content": "あなたは株式情報を提供する金融アシスタントです。"
        },
        {"role": "user", "content": user_message}
    ]

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )

    msg = response.choices[0].message

    # Function Call処理
    if msg.tool_calls:
        for tool_call in msg.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)

            # 実際の関数実行(ここではモックデータ)
            if func_name == "get_stock_price":
                result = json.dumps({
                    "symbol": func_args["symbol"],
                    "price": 185.50,
                    "change": "+2.3%"
                })
            elif func_name == "create_alert":
                result = json.dumps({
                    "status": "success",
                    "alert_id": "alert_12345"
                })
            else:
                result = json.dumps({"error": "Unknown function"})

            messages.append(msg)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

        # 最終応答生成
        final_response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages
        )
        return final_response.choices[0].message.content

    return msg.content

本番プロンプト管理戦略

本番環境でプロンプトを効果的に管理するためには、コードと同等レベルのエンジニアリングプラクティスが必要である。

バージョン管理

プロンプトを別ファイルに分離してGitで管理する。各変更の理由とパフォーマンスへの影響を記録する。

A/Bテスト

新しいプロンプトをデプロイする際、一部のトラフィックにのみ適用してパフォーマンスを比較する。精度、レイテンシ、コストの3つの指標を同時にモニタリングする。

ガードレール

プロンプトインジェクション防御、出力バリデーション、有害コンテンツフィルタリングなどの安全装置を必ず実装する。

プロンプトテンプレートシステム

from dataclasses import dataclass, field
from typing import Dict, Any
from datetime import datetime


@dataclass
class PromptTemplate:
    """プロンプトテンプレート管理クラス"""
    name: str
    version: str
    template: str
    metadata: Dict[str, Any] = field(default_factory=dict)
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())

    def render(self, **kwargs) -> str:
        """変数を置換して最終プロンプトを生成"""
        result = self.template
        for key, value in kwargs.items():
            placeholder = f"__{key.upper()}__"
            result = result.replace(placeholder, str(value))
        return result


# プロンプトレジストリ
class PromptRegistry:
    def __init__(self):
        self._templates: Dict[str, PromptTemplate] = {}

    def register(self, template: PromptTemplate):
        key = f"{template.name}:{template.version}"
        self._templates[key] = template

    def get(self, name: str, version: str = "latest") -> PromptTemplate:
        if version == "latest":
            candidates = [
                v for k, v in self._templates.items()
                if k.startswith(f"{name}:")
            ]
            return max(candidates, key=lambda t: t.version)
        return self._templates[f"{name}:{version}"]


# 使用例
registry = PromptRegistry()
registry.register(PromptTemplate(
    name="code_review",
    version="2.1",
    template=(
        "あなたはシニア__LANGUAGE__開発者です。\n"
        "以下のコードをレビューしてください。\n\n"
        "レビュー基準:\n"
        "- セキュリティ脆弱性\n"
        "- パフォーマンス問題\n"
        "- コードスタイル\n\n"
        "コード:\n__CODE__"
    ),
    metadata={"model": "gpt-4o", "temperature": 0.0}
))

プロンプト評価方法論

プロンプトの効果を客観的に測定するためには、体系的な評価パイプラインが必要である。

評価指標:

指標説明測定方法
精度正解との一致率自動比較またはLLM-as-Judge
一貫性同一入力に対する出力変動性複数回実行後の標準偏差
レイテンシ応答までの所要時間API呼び出し時間測定
コストトークン消費量ベースのコスト入出力トークン数追跡
安全性有害コンテンツ生成率安全性分類器適用
形式準拠率指定出力形式の準拠率スキーマバリデーション

LLM-as-Judgeパターン: 自動評価が困難な自由形式テキストに対して、別のLLMを評価者として活用し品質をスコア化する。評価基準をルーブリックとして明示し、評価者LLMにもCoTを活用させると評価精度が向上する。

技法比較表

技法核心原理精度向上API呼び出し数適したタスク複雑度
Zero-shot直接質問ベースライン1回単純タスク
Few-shot例示ベース学習+15~25%1回形式の一貫性
Zero-shot CoT段階的推論誘導+20~40%1回数学、論理
Few-shot CoT推論例示提供+40~60%1回複雑推論
ReAct推論+行動ループタスク依存複数ツール活用
Self-Consistency多数決投票+10~18%5~10回推論精度
Tree of Thoughtsツリー探索+70%(特定課題)複数計画、パズル
Structured Outputスキーマ強制形式100%1回データ抽出

実践チェックリスト

本番環境にプロンプトをデプロイする前に、以下の項目を確認すること。

  1. 技法選択: タスクの複雑さに合った技法を選択したか?単純タスクにToTを使うのは無駄である
  2. 例示品質: Few-shotの例は多様で正確か?エッジケースを含んでいるか?
  3. CoT活用: 推論が必要なタスクで段階的思考を誘導しているか?
  4. 出力形式: 構造化出力が必要な場合、JSON ModeまたはFunction Callingを活用しているか?
  5. コスト管理: Self-ConsistencyやToT適用時にAPIコストを見積もり予算を設定したか?
  6. 評価パイプライン: プロンプト変更の効果を測定する自動化された評価システムがあるか?
  7. ガードレール: プロンプトインジェクション防御と出力バリデーションロジックが実装されているか?
  8. バージョン管理: プロンプトがGitで管理され変更履歴が追跡されているか?
  9. モニタリング: 精度、レイテンシ、コストをリアルタイムでモニタリングしているか?
  10. フォールバック戦略: プロンプト失敗時の代替戦略(よりシンプルなプロンプト、デフォルト値返却など)が準備されているか?

おわりに

プロンプトエンジニアリングは単なる「上手な質問の仕方」ではなく、LLMの推論能力を体系的に引き出すエンジニアリング分野である。Chain-of-Thoughtで段階的推論を誘導し、ReActで外部ツールと連携し、Self-Consistencyで信頼性を高め、Tree of Thoughtsで複雑な問題を探索する -- これらの技法の原理を理解し、適材適所に活用することが核心である。

特にo1、o3のような推論ネイティブモデルの登場により一部の技法はモデル自体に内在化される傾向にあるが、プロンプト設計の基本原則と本番管理戦略は依然として有効である。プロンプトをコードのように管理し、評価パイプラインで品質を保証し、継続的に改善する文化を構築することが本番LLMシステムの成功の鍵である。