Originally published on tamiz.pro.

Large Language Models(LLM)を取り巻く当初の熱狂は、主にコード生成能力によって駆動されていました。開発者がLLMにReactコンポーネント、Pythonデータパイプライン、またはSQLクエリの作成を依頼し、それが瞬時に表示されるデモを私たちは目にしました。この能力は印象的ですが、AIエージェントが提示するエンジニアリング上の課題とは根本的に異なります。コード生成ツールはステートレスなツールですが、AIエージェントは外部システムと相互作用し、意思決定を行い、時間の経過とともにアクションを実行するステートフルで自律的なエンティティです。

AIエージェントをコード生成ツールのように扱うことが、ほとんどのAIプロジェクトが本番環境に到達できない主な理由です。静的な成果物の生成から動的で多段階のワークフローのオーケストレーションに移行すると、複雑さは構文の正確さからシステム全体の信頼性へと移行します。この深掘りでは、本番品質のAIエージェントがエンジニアリングにおけるパラダイムシフトを必要とする理由を探り、3つの重要な柱に焦点を当てます:厳格な状態管理、包括的な観測可能性、そして決定論的な制御フローです。

根本的な移行:ステートレス生成 vs ステートフル実行

エンジニアリング上のギャップを理解するためには、まずコード生成ツールが行うこととエージェントが行うことの違いを明確にする必要があります。

コード生成ツールRequest -> Responseのループで動作します。入力はプロンプトで、出力はコードのスニペットです。LLMは呼び出し間でメモリを保持せず、データベースのような外部状態を変更せず、ランタイムエラーに基づいて次のステップを自分で決定しません。これはエントロピーの高い関数です。

AIエージェントはループです。環境を観察し、次の最善のアクションについて推論し、そのアクションを実行(ツールやAPI経由で)し、結果を観察します。これによりフィードバックループが作成されます:

Observe -> Think -> Act -> Observe -> Think -> ...

このループは、静的なコード生成には存在しないいくつかのエンジニアリング上の複雑さをもたらします:

  1. 非決定論: エージェントのワークフロー内でのパスは固定されていません。LLMの推論に依存しており、同じ入力でも実行間で異なる場合があります。
  2. 副作用: エージェントはしばしば外部世界と相互作用します(メールの送信、データベースの更新、APIの呼び出し)。これらのアクションは不可逆的であり、慎重に扱う必要があります。
  3. 状態の蓄積: エージェントが複雑なタスクに取り組むにつれて、コンテキストが蓄積されます。この状態が厳格に管理されない場合、エージェントはコンテキストウィンドウのオーバーフローや、古い情報に基づく幻覚に苦しむことになります。
  4. 失敗モード: 構文エラーで失敗するスクリプトとは異なり、エージェントは誤ったツールの選択、無効なパラメータでのAPI呼び出し、またはリトライループへの陥入により、サイレントに失敗する可能性があります。

柱1:厳格な状態管理

従来のソフトウェアエンジニアリングでは、状態は変数、データベースレコード、またはセッションストアを介して管理されます。AIエージェントでは、状態はLLMのコンテキストウィンドウと外部システムの間に断片化されています。この断片化はバグの主な原因となっています。

暗黙的状態の問題

「トピックを調査し、レポートを作成する」という単純なタスクを任されたエージェントを考えてみましょう。素朴な実装では、すべてのステップで会話履歴全体をLLMに渡すかもしれません。会話が長くなるにつれて、コンテキストウィンドウがいっぱいになり、以下のような問題が発生します:

  • コストの爆発: トークン数が増える = APIコストの上昇。
  • パフォーマンスの低下: 長いプロンプトは処理に時間がかかります。
  • 注意の希薄化: LLMは以前の指示やコンテキストの奥深くに埋もれた重要な事実を忘れてしまう可能性があります。

解決策:明示的な状態機械

本番環境向けのエージェントは、LLMのメモリだけに依存すべきではありません。代わりに、進捗を追跡するために明示的な状態機械や構造化データストアを使用する必要があります。

1. 構造化状態ストア

生のテキストをコンテキストにダンプする代わりに、タスクの現在の状態を表す構造化JSONオブジェクトを維持します。この状態には以下が含まれるべきです:

  • タスクの進捗: どのステップが完了、保留、または失敗したか。
  • 主要な事実: 抽出されたエンティティ、データポイント、または下された決定。
  • ツールの出力: 冗長なAPI呼び出しを避けるための、以前のツール呼び出しからキャッシュされた結果。
interface AgentState {
  taskId: string;
  status: 'initializing' | 'researching' | 'drafting' | 'reviewing' | 'completed' | 'failed';
  progress: {
    researchSteps: number;
    totalResearchSteps: number;
    sourcesConsulted: string[];
  };
  context: {
    keyFacts: string[];
    userPreferences: Record<string, any>;
  };
  metadata: {
    createdAt: Date;
    updatedAt: Date;
    lastError?: string;
  };
}

Enter fullscreen mode Exit fullscreen mode

2. 状態の永続化と復旧

エージェントがクラッシュしたりタイムアウトしたりした場合、最初からやり直すのではなく、最後に知られている良好な状態から再開する必要があります。これには、重要なチェックポイントでAgentStateをデータベース(例:PostgreSQL、Redis)に永続化する必要があります。

エージェントが再開するときは、状態を読み込み、主要な事実と進捗からコンテキストを再構築し、中断したところから続行します。これは長時間実行されるタスクにとって重要です。

3. コンテキストウィンドウ管理

コンテキスト要約ベクトル検索などの手法を使用して、LLMのコンテキストウィンドウを管理します。履歴全体を渡す代わりに、以下を渡します:

  • 現在のAgentState
  • 以前のインタラクションの要約。
  • ベクトルデータベースから関連する検索されたチャンク。

これによりトークン使用量が削減され、LLMは関連情報に集中できます。

柱2:観測可能性とトレーシング

測定できないものを改善することはできません。従来のソフトウェアでは、ログ、メトリクス、トレースを使用します。AIエージェントでは、これらは非決定論的で多段階のワークフローを処理できるように適応させる必要があります。

従来のログ記録の限界

従来のログは線形でイベントベースです。AIエージェントの実行はノードとエッジのグラフです。"Tool called: search_api"のような単純なログ行は不十分です。なぜなら、以下をキャプチャしないからです:

  • プロンプト: LLMに何が送信されたか?
  • 応答: LLMは何を決定したか?
  • レイテンシ: 各ステップにどれくらいの時間がかかったか?
  • コスト: 何トークンが消費されたか?
  • 信頼度: LLMは不確実性を表現したか?

解決策:LLM向けの分散トレーシング

LLM対応の分散トレーシングフレームワーク(OpenTelemetryなど)を採用します。エージェントのワークフローの各ステップは、トレース内のスパンであるべきです。

エージェントスパンの主要属性

  1. 入出力プロンプト: 各LLM呼び出しの完全なプロンプトと応答を保存します。これは幻覚のデバッグとプロンプトの最適化に不可欠です。
  2. ツール呼び出しの詳細: エージェントがツールを使用する場合、ツール名、入力パラメータ、および出力結果をログに記録します。
  3. トークン使用量: コスト監視のために入出力トークンを追跡します。
  4. レイテンシ: レイテンシをLLM処理時間、ツール実行時間、およびネットワークオーバーヘッドに分解します。

例:OpenTelemetry統合

OpenTelemetryを使用してエージェントのステップを計装する方法の例を以下に示します:

const tracer = opentelemetry.trace.getTracer('ai-agent-tracer');

async function executeResearchStep(agentState, toolClient) {
  return await tracer.startActiveSpan('research.step', async (span) => {
    try {
      // Log the prompt sent to the LLM
      span.setAttribute('llm.prompt', agentState.researchPrompt);

      // Call the LLM
      const response = await llmClient.generate({
        prompt: agentState.researchPrompt,
        model: 'gpt-4-turbo'
      });

      // Log token usage
      span.setAttribute('llm.usage.total_tokens', response.usage.total_tokens);
      span.setAttribute('llm.usage.prompt_tokens', response.usage.prompt_tokens);
      span.setAttribute('llm.usage.completion_tokens', response.usage.completion_tokens);

      // Parse the response to extract tool calls
      const toolCalls = parseToolCalls(response.text);

      // Execute tool calls
      for (const call of toolCalls) {
        const toolResult = await toolClient.execute(call);
        span.setAttribute(`tool.${call.name}.result`, toolResult);
      }

      span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
      return response;
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR, message: error.message });
      throw error;
    } finally {
      span.end();
    }
  });
}

Enter fullscreen mode Exit fullscreen mode

可視化とデバッグ

LangSmith、Phoenix、またはArizeなどのダッシュボードを使用して、トレースを可視化します。これにより、以下が可能になります:

  • ボトルネックの特定: どのステップが遅いかを確認できます。
  • 障害のデバッグ: エージェントが誤ったアクションを選択した理由を理解できます。
  • コストの監視: エージェントの実行ごとのトークン使用量を追跡できます。
  • 実行の比較: トレースを並べて比較することで、異なるプロンプトやモデルのA/Bテストが可能です。

柱3:決定論的な制御フロー

LLMは確率論的です。創造的なタスクには優れていますが、決定論的なロジックには不向きです。本番環境向けのエージェントは、「思考」部分(LLMが処理)と「行動」部分(決定論的なコードが処理)を分離する必要があります。

ハイブリッドアプローチ

LLMにワークフロー全体を制御させないようにします。代わりに、LLMを以下に使用します:

  • 意図認識: ユーザーは何を達成しようとしているか?
  • 情報抽出: どのようなデータが必要か?
  • 意思決定: 次にどのツールを使用すべきか?

決定論的なコードを以下に使用します:

  • オーケストレーション: 状態機械の管理。
  • 検証: ツールの入力が正しいことの確認。
  • エラーハンドリング: 失敗したステップのリトライ。
  • セキュリティ: 入出力のサニタイズ。

例:ガードレールと検証

LLMの出力を盲目的に信頼してはいけません。常にスキーマに対して検証してください。

import { z } from 'zod';

const ToolCallSchema = z.object({
  tool: z.enum(['search', 'extract', 'summarize']),
  params: z.object({
    query: z.string(),
    filters: z.object({ dateRange: z.string().optional() }).optional()
  })
});

async function safeToolCall(llmResponse: string) {
  try {
    // Parse the LLM's response
    const parsed = JSON.parse(llmResponse);

    // Validate against the schema
    const validated = ToolCallSchema.parse(parsed);

    // Execute the tool
    return await executeTool(validated.tool, validated.params);
  } catch (error) {
    // Handle validation errors gracefully
    if (error instanceof z.ZodError) {
      logger.warn('Invalid tool call structure', { error: error.errors });
      return { error: 'Invalid tool call structure' };
    }
    throw error;
  }
}

Enter fullscreen mode Exit fullscreen mode

非決定論の処理

LLMの出力は非決定論的であるため、可変性を処理するための戦略が必要です:

  1. 温度低減によるリトライ: ステップが失敗した場合、より決定論的な結果を得るために温度を下げてリトライします。
  2. 自己修正: エージェントが自身の出力を批評し、洗練することを許可します。たとえば、ツール呼び出しが失敗した場合、エラーメッセージをLLMに返し、パラメータの修正を依頼します。
  3. フォールバック: 重要なステップには決定論的なフォールバックを用意します。たとえば、LLMがデータの抽出に失敗した場合、ルールベースのパーサーをバックアップとして使用します。

本番環境のベストプラクティス

1. 小さく始めて、素早く反復する

最初から複雑なマルチエージェントシステムを構築しないでください。特定の課題を解決する単一のエージェントワークフローから始めます。その1つのエージェントについて、状態管理、観測可能性、および制御フローを正しく実装します。その後、徐々に複雑さを追加していきます。

2. セキュリティファースト

AIエージェントは、インジェクション攻撃、プロンプトインジェクション、およびデータ漏洩に対して脆弱になる可能性があります。常に以下を行ってください:

  • ユーザー入力をサニタイズする。
  • ツールの出力を検証する。
  • ユーザーの権限に基づいてツールへのアクセスを制限する。
  • 状態ストア内の機密データを暗号化する。

3. 監視と反復

AIは「設定して忘れる」ものではありません。エージェントのパフォーマンスを継続的に監視します。以下を探します:

  • 高いエラー率: 特定のステップが頻繁に失敗しているか?
  • 高いレイテンシ: 特定のステップが遅いか?
  • 高いコスト: 特定のプロンプトが非効率的か?
  • ユーザーフィードバック: ユーザーは結果に満足しているか?

このデータを使用して、プロンプトを洗練し、状態管理を最適化し、観測可能性を改善します。

結論

コード生成ツールから本番環境向けのAIエージェントへの移行は、単なるスケーリングの問題ではなく、根本的なエンジニアリング上の課題です。静的なコードを書くという考え方から、動的でステートフルなワークフローをオーケストレーションするという考え方へのシフトが必要です。

厳格な状態管理、包括的な観測可能性、および決定論的な制御フローを実装することで、信頼性が高く、コスト効率が良く、安全なAIエージェントを構築できます。これらの柱はオプションの追加機能ではなく、本番品質のAIシステムの基盤です。

AIエンジニアリングの未来は、より良いモデルだけでなく、より良いシステムに関するものです。これらのエンジニアリング原則を習得すれば、次世代のインテリジェントアプリケーションを構築するための十分な準備が整います。

よくある質問

Q: LangChainやLlamaIndexのような既存のフレームワークを本番環境向けエージェントに使用できますか?

A: はい、ただし注意が必要です。これらのフレームワークはプロトタイピングや状態・観測可能性の抽象化の提供には優れています。しかし、本番環境では、セキュリティ、パフォーマンス、およびコストに関する特定の要件を満たすために、カスタマイズまたは拡張が必要になることがよくあります。これらを「ブラックボックス」として扱うことは避け、基盤となる仕組みを理解してください。

Q: 数時間または数日かかる長時間実行エージェントはどのように扱えばよいですか?

A: 定期的にエージェントの状態を保存するために、永続的な状態ストレージ(データベースなど)を使用します。最後のチェックポイントからエージェントを再開するメカニズムを実装します。エージェントのステップを非同期にトリガーするために、AWS LambdaやAzure Functionsのようなイベント駆動型アーキテクチャの使用を検討してください。

Q: 誤った動作をするAIエージェントをデバッグする最善の方法は何ですか?

A: 分散トレーシングを使用して、エージェントの実行フローを可視化します。LLMが予期しない決定を下したステップや、ツール呼び出しが失敗したステップを探します。これらのステップのプロンプトと応答を分析して問題を特定します。A/Bテストを使用して、異なるプロンプトやモデルを比較します。

本番環境対応のAIシステム構築に関するさらなる洞察については、Tamiz's Insightsをご覧ください。