7月14日、Substackメディア「bobbytables.io」が「The Agentic Loop: Three loops in a trench coat」と題した記事を公開した。AIエージェントの実装において「単一ループ」として語られがちなエージェントループが、実際には3つの独立したループで構成されているという設計思想を、コード例を交えながら詳細に解説している。
「トレンチコートを着た3人組」——ループ分解のなぜ
LLMを使ったエージェントを実装したことがある人なら、「エージェントループ」という言葉に馴染みがあるはずだ。しかし記事が指摘するように、これを「1つのループ」として捉えるのは過度な単純化だ。実態は3つのループが重なり合った構造——記事のタイトルを借りれば「トレンチコートを着た3人組」——である。
単一ループとして実装してしまうと、ループごとに異なる責務・タイムスケール・失敗モードが混在し、設計の見通しが悪くなる。3つに分解することで、それぞれの境界と連携ポイントが明確になり、障害箇所の特定やスケーリング戦略の検討がしやすくなる。
その3つとは:
- インファレンスループ(Inference Loop)
- ツールループ(Tool Loop)
- ヒューマンループ(Human Loop)
最外殻:インファレンスループ
インファレンスループは、LLMのチャット補完APIを叩く最も外側のループだ。その責務は3つある。
- チャット補完APIの呼び出し
- ツール使用リクエストをツールループに委譲する
- チャット履歴の永続化(ツール結果や追加メッセージの管理)
重要な点は、ほとんどのLLMプロバイダーのAPIはステートレス設計であるということだ。AnthropicもOpenAIも、「前回の会話」を覚えていない。毎回、会話の全履歴をリクエストに含めて送信する必要がある。「大きな会話はトークンを早く消費する」という警告が出るのはそのためだ。
chatMessages = [
{ role: "system", content: "You are a couples therapist. ..." },
{ role: "user", content: "I kinda miss Laney, what should I write to her?" }
]
continueInferenceLoop = true
while continueInferenceLoop
inferred = aiClient.completeChat(messages: chatMessages)
chatMessages.push({
role: "assistant",
content: inferred.message.content,
toolCalls: inferred.toolCalls,
})
if inferred.toolCalls.length == 0
continueInferenceLoop = false
else
# Handle tools (see more later)
end
end
中間層:ツールループ
「LLMは瓶の中の脳だ。ツールを与えて初めてエージェントになる」——記事はこう表現している。
LLMがツールを「使う」と判断した場合、1ターンで複数のツール呼び出しを推論することがある。これを処理するのがツールループだ。ツール定義はAPIリクエスト時にシステムプロンプトのトークンストリームにシリアライズされて渡される。
実装上で押さえるべき注意点が3つある。
1. ツール名やパラメータはハルシネーションし得る
ツール呼び出しも推論テキストである以上、存在しないツール名や不正なパラメータが返ってくることがある。Tool Not Found: "call_my_ex_girlfriend" のような防御的ハンドリングが必須だ。
2. tool_call_idは必ず返す
APIはtool_call_id(Anthropicではtool_use_id)を返す。これを後続のリクエストに含めないと、APIがエラーを返す。
3. エラーはコンテンツとして表現する
一部プロバイダーはツール結果にエラーコードを持たない。その場合、<tool_call_error>Phone number blocked</tool_call_error> のようにXMLタグでエラーを表現するのが実践的な手法だ(AnthropicはAPIレベルでis_errorフィールドを持つ)。
最難関:ヒューマンループ
記事が「実装上最も難しい」と明言するのがこのヒューマンループだ。
ツール実行を人間(または何らかの承認機構)に委ねる仕組みで、承認・拒否・別方向への誘導を行う。ただし記事が正直に認めているように、これは厳密な意味でのプログラム的ループではない。コードの制御の外——承認者側の判断プロセス——にループが存在する。
while tool = inferred.toolCalls.shift
result = ""
case tool.name
when "send_ex_girlfriend_an_email"
decision = wait_for_approval(tool.parameters) # ここで処理がブロックされる
if decision.approved?
send_email(tool.parameters["email_message"])
result = "Email sent"
elsif decision.denied_with_instructions?
result = "<tool_call_denied_with_instructions>
#{decision.instructions}
</tool_call_denied_with_instructions>"
else
result = "<tool_call_error>Tool call was not approved</tool_call_error>"
end
end
chatMessages.push({ role: "user", content: [{ type: "tool_result", ... }] })
end
このループが難しい理由は実装上の制約にある。数時間単位でコードをブロックし続けることはできない。サーバーが再起動する可能性もあるし、並行して大量のリクエストをさばく必要もある。
記事はこの問題を解決する仕組みとしてTemporalのような耐久実行フレームワーク(Durable Execution)の存在に言及している。耐久実行フレームワークとは、プロセスの再起動やネットワーク障害が発生しても実行状態を自動的に永続化・再開できる仕組みで、ヒューマンループのように「数時間単位で人間の応答を待つ」処理に特に有効だ。承認待ちの状態をフレームワーク側が保持するため、アプリケーション側は単純なコードで長時間の中断・再開を表現できる。
3ループの全体像
3つのループの関係を整理すると以下のとおりだ。
- インファレンスループ — チャット補完APIを呼び出し、ツール呼び出しをツールループに委譲する
- ツールループ — モデルが要求するツール実行を処理し、承認が必要な操作をヒューマンループに渡す
- ヒューマンループ — 承認・拒否・追加指示を行い、その結果をツール結果としてインファレンスループに伝播させる
この3ループ構造は、個別の機能追加にも自然に対応する。たとえばRAG(Retrieval-Augmented Generation)はツールループ内で検索ツールとして実装でき、プログレッシブディスカバリー(情報を段階的に収集しながら次のアクションを決定する手法)はインファレンスループの複数ターンにわたる推論として位置づけられる。自動ツール承認チェックはヒューマンループを迂回するルールとしてツールループ内に実装する形になる。いずれの拡張も、どのループに手を入れるべきかが明確になるため、設計の見通しが保たれる。
単一ループとして語られてきたエージェントループを3層に分解し、それぞれの責務とトレードオフを明確にしたこの整理は、実装者がシステム設計を見直す際の実用的な視点を提供している。
詳細はThe Agentic Loop: Three loops in a trench coatを参照していただきたい。