はじめに

Heroku社が提唱した「12-Factor App」は、クラウドネイティブなアプリケーション開発のベストプラクティスをまとめたものです。これをAIエージェント開発に応用して、dexhorthy氏がまとめたものが「12 Factor Agents」です。

この12 Factor Agentsを読んでみて、かなり学びがあったので、この記事では12 Factorの概要を簡単にまとめておきます。

なお、自分用にまとめて補足や所感を追加しているので、詳細はぜひ原文を参照してください。また、この記事は50%を人が書いて、50%をAIが補完しています。

12 Factor Agents の背景

「AIエージェントを作ろう!」

そう思って意気揚々とLangChainやCrewAIなどのフレームワークを使い始めた開発者は多いのではないでしょうか。 最初は順調に進みます。デモは動くし、簡単なタスクもこなせる。でも、いざ本番環境に投入しようとすると……。

「品質が70-80%で頭打ちになる」

実際にプロダクションレベルで動かそうとすると、残りの20-30%が重要になります。ここで品質が落ちると、ユーザーの信頼を失い、ビジネスに悪影響を及ぼします。 では、なぜAIエージェントはこの壁にぶつかるのでしょうか?

なぜそうなるのか? - フレームワークの限界と現実のギャップ

多くのAIエージェントフレームワークは「プロンプトを与えて、ツールを渡して、ゴールに到達するまでループさせる」というパターンを推奨します。理論的には美しいアプローチです。でも現実はどうでしょうか?

# よくあるパターン
while True:
    next_step = llm.decide_next_action(context)
    if next_step == "done":
        break
    result = execute_tool(next_step)
    context.append(result)

このアプローチの問題点は以下のとおりです。

  • エラーが起きたときの制御が難しい
  • コンテキストウィンドウがすぐに溢れる
  • 人間の承認が必要な場面で止められない
  • デバッグが地獄

実際、プロダクションで動いている「AIエージェント」と呼ばれるものの多くは、ほとんどが決定的なコードで、要所要所にLLMを使っているだけです。

12 Factor Agentsとは

そこで登場するのが「12 Factor Agents」です。これは、dexhorthy氏が提唱する、プロダクション品質のAIエージェントを作るための12の設計原則です。

重要なのは、これらの原則はフレームワークに依存しないということ。既存のコードベースに段階的に適用できる、実践的なパターン集という点です。以降では、この12の原則を簡単に紹介していきます。

Factor 1: Natural Language to Tool Calls(自然言語をツール呼び出しに変換)

エージェント構築において最も一般的なパターンの一つは、自然言語を構造化されたツール呼び出しへの変換です。

(所感:あまりによくあるパターンなので、あんまり意識しないかもで、詳細は割愛します)

Factor 2: Own Your Prompts(プロンプトを管理する)

プロンプトはコードと同じくらい大切な資産という原則です。一般的なエージェントフレームワークは簡単に始められますが、プロンプトの細かい調整やリバースエンジニアリングが困難です。フレームワークに隠されたプロンプトに依存するのは危険です。

プロンプトは明示的に管理し、バージョン管理し、テストすべきです。プロンプトは自前で書いて、関数形式で明確な入力・出力を持つファーストクラスのコードとして扱うのが良いでしょう。

Factor 3: Own Your Context Window(コンテキストウィンドウを管理する)

コンテキストウィンドウは有限のリソースです。「とりあえず全部入れる」では、すぐに限界が来ます。

// ❌ アンチパターン:履歴を全部入れる
const context = conversation.getAllHistory();

// ✅ 良いパターン:必要な情報だけを選別
const context = {
  recentMessages: conversation.getLastN(5),
  relevantDocs: search.findRelevant(query, limit=3),
  systemPrompt: prompts.getConcise()
};

また、LLMに渡す情報を普通のチャット形式ではなく、XMLやJSONなどの構造化された独自形式で整理して管理すると良いです。これによって情報密度が上がります。トークン数を削減でき、LLMがより正確に状況を理解して適切な判断ができるようになります。

(所感:コンテキストを絞るのは非常に大事なのと、XML などの意味が構造化された形式で渡すのはかなり効果的な印象)

Factor 4: Tools Are Just Structured Outputs(ツールは単なる構造化出力)

ツールが複雑である必要はありません。ツールは、LLMからの構造化された出力であり、決定論的なコードを実行するためのものです。LLMは何をするかを決定しますが、その実行方法は決定論的なコードによって制御されます。

従来の「AIにツールを渡して好きに使わせる」アプローチではなく、「AIには構造化されたJSONを出力してもらい、その内容に基づいて確実なコードを実行する」というアプローチが、実際のプロダクションでは信頼性が高くなります。

Factor 5: Unify Execution State and Business State(実行状態とビジネス状態を統一)

従来は実行状態とビジネスの状態の別々に管理していました。だが、この場合は不整合が起きやすいです。

エージェントの実行状態と、アプリケーションのビジネスロジックの状態を別々に管理すると、不整合が起きがちになります。

そこで代わりに、コンテキストウィンドウ(会話履歴)から実行状態を推測できるように設計します。

// ❌ 複雑な分離管理
const state = {
  execution: { step: 3, waiting: true, retries: 1 },
  business: { messages: [...], tools: [...] }
};

ではなく、

// ✅ 統一されたイベントストリーム
const thread = [
  { type: 'user_message', content: '...' },
  { type: 'tool_call', name: 'search', args: {...} },
  { type: 'tool_result', data: {...} },
  { type: 'waiting_for_approval', reason: '...' },
  // 現在の状態は最新のイベントから推測可能
];

とします。現在の状態は、イベント履歴を辿ればわかります。

Factor 6: Launch/Pause/Resume with Simple APIs(シンプルなAPIで起動・停止・再開)

AIエージェントも普通のプログラムと同じように、起動・一時停止・再開・停止といった基本操作を簡単にできるべきだという原則です。

これが必要な理由は次のとおりです。

  • 効率性:無駄な待機時間を削減
  • 信頼性:途中で止まっても安全に再開できる
  • 統合性:他のシステムと連携しやすい
  • 監視性:どの段階にいるかが分かりやすい

Factor 7: Contact Humans with Tool Calls(人間への連絡もツール呼び出しで)

LLMは「普通のテキスト」か「ツール実行」かを最初の1文字で判断しています。例えば、「その」から始まったら、文章が返ります。これが不安定な動作の原因になります。そこで、常にJSON形式で返すようにします。

その際、人間への連絡もツールとして扱います。イメージとしては、「エージェントが困った時に『ちょっと聞きたいことがあるんですが…』と人間に連絡してくるシステム」として作るということです。

Factor 8: Own Your Control Flow(制御フローを管理する)

AIに自動実行させると、さまざまな問題が起こります。たとえば、AIが「危険な操作」を選択した瞬間に、それを止める方法がないということです。また、長時間かかる処理の間、システムがメモリ上で延々と待機し続けるという非効率性もあります。データ分析が6時間かかる場合、その間ずっとプロセスが動き続け、サーバーリソースを無駄に消費します。さらに、途中でシステムが落ちれば全てが最初からやり直しになってしまいます。

そこで、AIの動作を3つのパターンに分けて、それぞれ最適な制御方法を適用します。

  • 質問確認パターン:AIが曖昧な指示に困った時に人間に確認を求める
  • 情報取得パターン:データ取得やファイル読み込みなど低リスクな操作を自動実行する
  • 高リスク操作パターン:デプロイやデータ変更など重要な操作について、実行前に必ず人間の承認を求める

Factor 9: Compact Errors into Context Window(エラーをコンテキストウィンドウに圧縮)

AIエージェントの大きな利点の一つに「自己修復能力」があります。これは、何かの処理が失敗した時に、AIがエラーメッセージを読み取って自分で問題を解決する能力のことです。たとえば、AIがファイルを読み込もうとして「ファイルが見つかりません」というエラーが出た場合、優秀なAIなら「パスが間違っているかもしれない」「ファイル名にタイポがあるかもしれない」と推測して、修正した内容で再度実行をtryします。

エラーが起きた場合の実装方法もtry -> catchのloopで作れますが、無限リトライすると危険であるため、通常は最大リトライ回数を設定します。

ただし、エラーをそのままコンテキストに追加するだけでは不十分な場合があります。長いスタックトレースや複雑なエラーメッセージは、コンテキストウィンドウを圧迫し、AIの判断を混乱させる可能性があるためです。そこで、エラー情報を要約したり、関連性の低い過去のイベントを削除したりして、コンテキストを最適化してください。

Factor 10: Small, Focused Agents(小さく、集中したエージェント)

現在のAI開発でよく見られる間違いの一つに「万能エージェント」を作ろうとすることがあります。つまり、一つのエージェントにあれもこれもやらせようとする取り組みです。しかし、結果的に何もうまくできないエージェントを作ってしまうのがよくあるパターンです。

そこで、「一つのことを非常にうまくやる小さなエージェント」を作りましょう。

小さく集中したエージェントのメリットは次のとおりです。

  • コンテキストウィンドウが管理しやすいサイズに収まるため、LLMのパフォーマンスが向上する
  • 各エージェントの責任範囲が明確になり、問題が発生した際の原因特定や修正が容易になる
  • 個別のエージェントをテストすることで、システム全体の信頼性が向上する

Factor 11: Trigger from Anywhere(どこからでもトリガー可能に)

ユーザーがいる場所でエージェントが動くようにしましょう。

エージェントは、Webhook、CLI、API、Slack、メールなど、さまざまな方法でトリガーできるようにしてください。

(所感:DevinはSlackと連携するのを推奨していることから、原則に沿っていることがわかる)

Factor 12: Make Your Agent a Stateless Reducer(ステートレスなリデューサーにする)

最後の原則は、関数型プログラミングの考え方から来ています。

「AIエージェントは基本的にforループである」という前提があるとして、それを関数型プログラミング的に表現すると、AIエージェントは左foldになります。(reduce操作とも同じ概念)

(補足:ここでのタイトルのリデューサー/Reducerとは、「現在の状態」と「新しいアクション」を受け取って「新しい状態」を返す純粋な関数のこと)

まとめ

というわけで、ざっと12原則を紹介してみました。冒頭にも書きましたが詳細は、12-Factor Agentsの内容をぜひ参照してください。