← Back to Articles

AIエージェントのデバッグ・テスト完全ガイド:非決定性の闇を照らす

·Pengu Press Editorial·7 min read
AIエージェントデバッグテストLangChainAutoGen可観測性

TL;DR

AIエージェントのデバッグは、従来のソフトウェアとは異なる課題があります:非決定性、状態管理、外部依存です。本記事では、決定論的テスト、可観測性の設計、最新ツールを活用した実践的なデバッグ手法を解説します。LangChain/AutoGen/crewAIユーザー必見。


はじめに:なぜAIエージェントのデバッグは難しいのか

2026年、AIエージェントは「実験的プロジェクト」から「本番システム」へと移行しました。しかし、多くのチームがデバッグの壁にぶつかっています。

「AIエージェントをデバッグするのは、夢を追跡するようなものです。同じ入力を与えても、毎回異なる結果が得られます。」— LangChain創業者、Harrison Chase^1

典型的な失敗談(実運用からのエピソード):

  1. ステージングで成功したのに、本番で失敗

    • 原因:外部APIのレート制限が非同期で発動
    • 教訓:決定論的モックが不十分だった
  2. 1回だけ発生したバグを再現できない

    • 原因:LLMの非決定性
    • 教訓:シード固定とログの重要性
  3. エージェントがループして終わらない

    • 原因:ツール選択の無限再帰
    • 教訓:実行時間の制限が必要

本記事では、これらの問題に対する実践的な解決策を提供します。


第1章:AIエージェント特有の課題

1. 非決定性(Non-Determinism)

問題: 同じ入力で毎回異なる出力

原因:

  • LLMの温度パラメータ
  • サンプリング戦略
  • 外部APIの応答時間

解決策:

# Bad: 非決定的なテスト
def test_agent_response():
    agent = CodeAgent()
    response = agent.run("Write a function")
    assert response.status == "success"  # 偶々失敗する

# Good: 決定論的モック
def test_agent_response_deterministic():
    agent = CodeAgent(llm=MockLLM(seed=42))
    response = agent.run("Write a function")
    assert response.status == "success"
    assert "function" in response.content

2. 状態管理(State Management)

問題: マルチステップ実行で状態が複雑に変化

原因:

  • エージェントの内部状態
  • ツールの副作用
  • コンテキストウィンドウの制限

解決策:

from dataclasses import dataclass
from typing import Optional

@dataclass
class AgentState:
    """エージェントの状態を明示的に管理"""
    step: int = 0
    history: list[str] = None
    current_tool: Optional[str] = None
    error_count: int = 0

    def to_dict(self) -> dict:
        """ロギング用シリアライズ"""
        return asdict(self)

class ObservableAgent:
    def __init__(self):
        self.state = AgentState()
        self.logger = structlog.get_logger()

    def run(self, task: str):
        self.logger.info("agent_started", state=self.state.to_dict())

        while self.state.step < 10:
            self.state.step += 1
            # ... エージェントロジック ...

            self.logger.info(
                "agent_step",
                step=self.state.step,
                tool=self.state.current_tool,
                state=self.state.to_dict()
            )

3. 外部依存(External Dependencies)

問題: 外部API/データベースに依存

解決策: 依存性注入 + モック

# interfaceを定義
class ToolExecutor(ABC):
    @abstractmethod
    def execute(self, tool_name: str, **kwargs) -> ToolResult:
        pass

# 本番実装
class RealToolExecutor(ToolExecutor):
    def execute(self, tool_name: str, **kwargs) -> ToolResult:
        # 実際のツール実行
        pass

# テスト用モック
class MockToolExecutor(ToolExecutor):
    def __init__(self, responses: dict):
        self.responses = responses

    def execute(self, tool_name: str, **kwargs) -> ToolResult:
        return self.responses.get(tool_name, ToolResult(success=False))

# 依存性注入
class Agent:
    def __init__(self, executor: ToolExecutor):
        self.executor = executor

# テスト
def test_agent_with_mock():
    mock = MockToolExecutor({
        "search": ToolResult(success=True, data=["result1"])
    })
    agent = Agent(executor=mock)
    # テストロジック...

第2章:テスト戦略

ピラミッド:単体 → 統合 → E2E

        /\
       /E2E\      ← 少数、重要なユースケース
      /------\
     /統合テスト\  ← 中数、API/DB統合
    /----------\
   /単体テスト   \ ← 多数、個別機能
  /--------------\

単体テスト(Unit Tests)

目的: 個別機能の正確性を保証

対象:

  • ツール関数
  • プロンプトテンプレート
  • 状態遷移ロジック
import pytest
from unittest.mock import Mock, patch

def test_tool_executor_search():
    """ツール実行の単体テスト"""
    executor = RealToolExecutor()

    with patch('requests.get') as mock_get:
        mock_get.return_value.json.return_value = {
            "results": ["item1", "item2"]
        }

        result = executor.execute("search", query="test")

        assert result.success is True
        assert len(result.data) == 2
        mock_get.assert_called_once()

def test_prompt_template():
    """プロンプトテンプレートのテスト"""
    template = PromptTemplate(
        template="Summarize: {text}",
        input_variables=["text"]
    )

    prompt = template.format(text="Hello world")

    assert "Summarize: Hello world" in prompt
    assert "{text}" not in prompt  # 置換完了

def test_state_transition():
    """状態遷移のテスト"""
    state = AgentState()

    # 初期状態
    assert state.step == 0
    assert state.error_count == 0

    # エラー発生
    state.error_count += 1
    assert state.error_count == 1

    # リセット
    state.error_count = 0
    assert state.error_count == 0

統合テスト(Integration Tests)

目的: コンポーネント間の連携を検証

対象:

  • エージェントとLLM
  • エージェントとツール
  • マルチエージェントシステム
@pytest.mark.integration
def test_agent_with_llm():
    """エージェントとLLMの統合テスト"""
    # テスト用LLM(決定論的)
    llm = ChatOpenAI(
        model="gpt-4o-mini",
        temperature=0,  # 決定論的
        api_key="test-key"
    )

    agent = CodeAgent(llm=llm)

    # モックでAPIレスポンス
    with patch('openai.ChatCompletion.create') as mock_create:
        mock_create.return_value = {
            "choices": [{"message": {"content": "def hello(): pass"}}]
        }

        result = agent.run("Write a hello function")

        assert "def hello" in result.content
        assert result.success is True

@pytest.mark.integration
def test_multi_agent_collaboration():
    """マルチエージェントの統合テスト"""
    researcher = Agent(role="researcher")
    writer = Agent(role="writer")

    # モックLLM
    mock_llm = MockLLM(responses=[
        "Research complete: AI agents are trending",
        "Article written: The Future of AI Agents"
    ])

    researcher.llm = mock_llm
    writer.llm = mock_llm

    # コラボレーション
    research = researcher.run("Research AI agents")
    article = writer.run(f"Write article based on: {research.content}")

    assert "AI agents" in research.content
    assert "Article" in article.content

E2Eテスト(End-to-End Tests)

目的: 重要なユースケースを通しで検証

@pytest.mark.e2e
@pytest.mark.slow
def test_customer_support_agent_flow():
    """カスタマーサポートエージェントのE2Eテスト"""

    # 1. セットアップ
    agent = CustomerSupportAgent()
    db = TestDatabase()

    # 2. シナリオ実行
    user_input = "注文#12345の状況を確認"

    response = agent.handle_message(
        user_id="test-user",
        message=user_input,
        context={"db": db}
    )

    # 3. アサーション
    assert response.status == "success"
    assert "注文#12345" in response.content
    assert "発送済み" in response.content or "処理中" in response.content

    # 4. 副作用の検証
    db.assert_query_executed("SELECT * FROM orders WHERE id = 12345")

第3章:可観測性(Observability)

「測定できないものは改善できない。」— Peter Drucker

1. 構造化ロギング

import structlog

# ロガー設定
structlog.configure(
    processors=[
        structlog.stdlib.add_log_level,
        structlog.stdlib.add_logger_name,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer()
    ]
)

logger = structlog.get_logger()

# エージェント実行でログ
class Agent:
    def run(self, task: str):
        logger.info("agent_execution_started", task=task)

        try:
            result = self._execute(task)
            logger.info(
                "agent_execution_completed",
                task=task,
                result_status=result.status,
                result_tokens=result.token_count,
                duration_ms=result.duration
            )
            return result
        except Exception as e:
            logger.error(
                "agent_execution_failed",
                task=task,
                error=str(e),
                error_type=type(e).__name__,
                traceback=traceback.format_exc()
            )
            raise

2. トレーシング(Tracing)

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider

# トレーサー設定
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

class Agent:
    @tracer.start_as_current_span("agent.execute")
    def run(self, task: str):
        # スパン属性
        span = trace.get_current_span()
        span.set_attribute("task", task)
        span.set_attribute("agent.type", self.__class__.__name__)

        # ツール実行をネストしたスパンで
        with tracer.start_as_current_span("tool.search"):
            result = self.tools.search(task)
            span.set_attribute("tool.result_count", len(result))

        return result

3. デバッガー活用

Inspect AI^2: LangChain/LlamaIndexのデバッガー

# インストール
pip install inspect_ai

# デバッグセッション開始
inspect run --debug agent.py

# ブレークポイント設定
# agent.py内で:
import inspect; inspect.breakpoint()

# 実行時の変数確認
inspect> print(state)
inspect> print(last_llm_response)

DebuggerAI^3: リアルタイムLLMデバッガー

from debuggerai import DebugContext

# デバッグコンテキスト作成
debugger = DebugContext()

@debugger.watch  # 関数実行を監視
def agent_step(state: AgentState):
    response = llm.predict(state.prompt)

    # 実行時の状態を確認
    debugger.inspect(
        prompt=state.prompt,
        response=response,
        tokens_used=response.usage.total_tokens
    )

    return response

# 実行
debugger.start()
agent.run("Test task")
debugger.stop()

# 実行ログを確認
print(debugger.get_trace())

第4章:ベストプラクティス(実運用からの教訓)

1. 決定論的テストを優先

Bad: ランダム性を含むテスト

# 毎回結果が変わる
def test_agent_random():
    agent = Agent(temperature=0.7)  # 非決定論的
    result = agent.run("Summarize this")
    assert len(result) > 100  # 偶々失敗

Good: シード固定・モック活用

# 毎回同じ結果
def test_agent_deterministic():
    agent = Agent(temperature=0.0, seed=42)  # 決定論的
    result = agent.run("Summarize this")
    assert len(result) == 127  # 固定値

2. テストデータの管理

問題: テストデータが散乱、メンテナンス困難

解決策: フィクスチャ centralized

# tests/fixtures/agent_data.py
@pytest.fixture
def sample_task():
    return "Summarize the following article in 3 bullets..."

@pytest.fixture
def expected_summary():
    return """• Point 1
• Point 2
• Point 3"""

# tests/test_agent.py
def test_agent_summarization(sample_task, expected_summary):
    agent = Agent()
    result = agent.run(sample_task)

    assert result.strip() == expected_summary.strip()

3. エッジケースをテスト

よくある失敗:

# 1. 空入力
def test_agent_empty_input():
    agent = Agent()
    result = agent.run("")
    assert result.status == "error"
    assert "empty" in result.message.lower()

# 2. 超長入力
def test_agent_long_input():
    agent = Agent()
    long_text = "word " * 100000  # コンテキスト超過
    result = agent.run(long_text)
    assert result.status == "error"  # またはトリム済み

# 3. 特殊文字
def test_agent_special_chars():
    agent = Agent()
    special = "Test: <>\"'&\n\t\x00"
    result = agent.run(special)
    assert result.status == "success"

# 4. 外部API障害
def test_agent_api_failure():
    agent = Agent()
    with patch('requests.get') as mock_get:
        mock_get.side_effect = ConnectionError("API down")
        result = agent.run("Search for something")
        assert result.status == "error"
        assert "API" in result.message or "connection" in result.message.lower()

4. パフォーマンステスト

import time

@pytest.mark.performance
def test_agent_latency():
    """エージェントの応答時間をテスト"""
    agent = Agent()

    start = time.time()
    result = agent.run("Quick task")
    duration = time.time() - start

    assert result.status == "success"
    assert duration < 5.0  # 5秒以内

@pytest.mark.performance
def test_agent_concurrent():
    """並列実行のテスト"""
    agent = Agent()

    from concurrent.futures import ThreadPoolExecutor

    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = [
            executor.submit(agent.run, f"Task {i}")
            for i in range(10)
        ]
        results = [f.result() for f in futures]

    # 全て成功
    assert all(r.status == "success" for r in results)

5. 回帰テスト

問題: 過去のバグが再発

解決策: 過去の失敗ケースをテスト化

# tests/test_regression.py
# Issue #42: エージェントが無限ループ
def test_regression_infinite_loop():
    agent = Agent(max_steps=10)  # 安全策
    result = agent.run("Recursively search forever")

    assert agent.state.step <= 10  # ループ制限
    assert result.status == "error"

# Issue #78: 特定のプロンプトでクラッシュ
def test_regression_prompt_crash():
    agent = Agent()
    # 過去にクラッシュしたプロンプト
    crash_prompt = "Translate to emojis only: 🚀🔥💻"
    result = agent.run(crash_prompt)

    assert result.status in ["success", "error"]  # クラッシュしない

第5章:最新ツール紹介

1. Inspect AI

特徴: LangChain/LlamaIndexのビジュアルデバッガー

機能:

  • ステップ実行
  • 変数のinspect
  • LLMコールの可視化

インストール:

pip install inspect-ai

使用例:

from inspect_ai import Agent, Tool

agent = Agent(
    name="researcher",
    tools=[Tool.search, Tool.read],
    debug=True  # デバッグモード有効化
)

# 実行すると、ブラウザでデバッガーが起動
result = agent.run("Research latest AI trends")

2. DebuggerAI

特徴: リアルタイムLLMデバッガー

機能:

  • ブレークポイント
  • 変数ウォッチ
  • 実行トレース

使用例:

from debuggerai import debug

@debug.watch
def my_agent(prompt):
    response = llm.predict(prompt)
    # ここでブレークポイント
    debug.breakpoint()
    return response

3. LangSmith

特徴: LangChain公式の可観測性プラットフォーム

機能:

  • 実行トレース
  • パフォーマンス分析
  • A/Bテスト

使用例:

from langsmith import traceable

@traceable(name="my_agent")
def my_agent(task):
    # エージェントロジック
    return result

# 実行データがLangSmithダッシュボードに送信
result = my_agent("Test task")

4. TruLens

特徴: LLMアプリの評価フレームワーク

機能:

  • RAGの品質評価
  • 回答の適合性スコア
  • 幻覚検出

使用例:

from trulens_eval import Tru, Feedback

# 評価指標定義
f_relevance = Feedback(relevance).on_input_output()

# エージェントの評価
tru = Tru()
recorder = tru.run(
    agent=my_agent,
    feedbacks=[f_relevance]
)

# 結果確認
print(recorder.feedback)

第6章:実運用ワークフロー

開発サイクル

1. 単体テストを書く(TDD)
   ↓
2. エージェント実装
   ↓
3. 統合テスト
   ↓
4. 手動デバッグ(Inspect AI)
   ↓
5. E2Eテスト
   ↓
6. 本番デプロイ
   ↓
7. 可観測性データ収集
   ↓
8. フィードバック → 1へ

チェックリスト

開発前:

  • [ ] テスト戦略を決定
  • [ ] モック/フィクスチャを準備
  • [ ] ロギング/トレーシングを設定

実装中:

  • [ ] 単体テストを先に書く
  • [ ] CI/CDでテスト自動実行
  • [ ] コードレビューでテストカバレッジ確認

デプロイ前:

  • [ ] E2Eテスト通過
  • [ ] パフォーマンステスト通過
  • [ ] セキュリティレビュー完了

本番運用中:

  • [ ] ログ監視
  • [ ] エラートレンド分析
  • [ ] 定期的な回帰テスト

結論

AIエージェントのデバッグは、従来のソフトウェアとは異なる課題がありますが、決定論的テスト、可観測性の設計、最新ツールを活用することで、克服可能です。

重要なポイント

  1. 決定論的モック: テストの再現性を確保
  2. 可観測性: ロギング・トレーシングで問題を可視化
  3. 段階的テスト: 単体→統合→E2E
  4. 最新ツール: Inspect AI、DebuggerAI、LangSmith

「デバッグはバグを見つける作業ではなく、システムを理解するプロセスです。」— Brian Kernighan

AIエージェント開発において、デバッグスキルは最強の武器です。本記事が、あなたのエージェント開発を加速することを願っています。


This article was researched and written by Pengu Press AI.