要点まとめ
- Agent ハーネスは「ブラックボックス」ではなく、明確な構造と原理に基づいた実装パターン
- LLM の出力を解析し、外部ツールに連携し、結果をフィードバックするループが中核
- Theo が実装例を公開することで、開発者が Agent の仕組みを具体的に学べる環境が整備された
- Tool 定義、Call 解析、エラーハンドリング、ループ制御が主要な構成要素
- 基本を理解すれば、カスタム Agent やマルチターンワークフローの構築がシンプルになる
背景と文脈
ここ数年、LLM を活用したエージェント型アプリケーションへの関心が急速に高まっています。OpenAI の Function Calling、Anthropic の Tool Use、その他多くの LLM プロバイダーが Agent 機能を提供するようになりました。しかし、これらのツール連携メカニズムは、外部からは「複雑な魔法」に見えることが多い。
Agent ハーネスとは、LLM がツール呼び出しを自律的に判断・実行し、その結果をループバックして推論を続けるための基盤インフラを指します。多くの商用フレームワーク(LangChain、AutoGen、CrewAI など)は、この Agent ハーネスを内部に持ちながら、実装詳細を隠蔽しています。
結果として、開発者は「なぜこう動くのか」「どこで何が起きているのか」を理解しないまま、API を呼び出すだけになりがちです。
Agent ハーネスの実装例が公開されることで、開発者は Agent の動作原理を「黒箱の中身を白箱にする」プロセスを通じて習得できます。これはフレームワークの選択判断や、トラブルシューティング、カスタマイズ時の自信につながります。
Theo が実装例を公開した背景には、このギャップを埋める狙いがあります。Agent の仕組みは決して不可解ではなく、シーケンシャルなロジックの組み合わせに過ぎないことを実証することで、エコシステム全体のリテラシー向上を促そうとしています。
詳しく見ていく
Agent ハーネスの最小単位構成
Agent ハーネスの動作は、本質的には以下の 4 ステップを何度も繰り返すループです。
- LLM にプロンプトを送信 - 現在のコンテキスト(ユーザー入力、過去の会話履歴、利用可能なツール定義)を含めて
- LLM の応答を解析 - ツール呼び出しが含まれているかを判定
- ツール実行 - 指定されたツールをパラメータ付きで実行し、結果を取得
- 結果をフィードバック - 実行結果を新たなコンテキストとして LLM に返す
最小限の Agent ハーネス実装は、以下のような Python コードで表現できます。
from typing import Any, Dict, List
import json
class SimpleAgentHarness:
def __init__(self, llm_client, tools: Dict[str, callable]):
self.llm_client = llm_client
self.tools = tools
self.messages = []
def run(self, user_input: str, max_iterations: int = 10) -> str:
self.messages.append({"role": "user", "content": user_input})
for _ in range(max_iterations):
# Step 1: LLM に送信
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=self.messages,
tools=self._build_tool_definitions(),
tool_choice="auto"
)
# Step 2: 応答を解析
if response.stop_reason == "end_turn":
# ツール呼び出しなし - 終了
final_message = response.choices[0].message.content
return final_message
# Step 3 & 4: ツール実行 → フィードバック
self.messages.append(response.choices[0].message)
for tool_call in response.choices[0].message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# ツール実行
result = self.tools[tool_name](**tool_args)
# 結果をメッセージに追加
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result)
})
return "Max iterations reached"
def _build_tool_definitions(self) -> List[Dict[str, Any]]:
definitions = []
for tool_name, tool_func in self.tools.items():
definitions.append({
"type": "function",
"function": {
"name": tool_name,
"description": tool_func.__doc__,
"parameters": self._extract_parameters(tool_func)
}
})
return definitions
このコードが示すように、Agent ハーネスの実装は「ループ + LLM API 呼び出し + 関数実行」のシンプルな組み合わせです。
Tool 定義とスキーマ管理
Tool 定義は、Agent が「どんなツールが使えるのか」を理解するためのスキーマです。OpenAI の Function Calling では、以下のような JSON スキーマで tool を定義します。
{
"type": "function",
"function": {
"name": "get_weather",
"description": "与えられた都市の現在の天気情報を取得",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市名"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度単位"
}
},
"required": ["location"]
}
}
}
このスキーマが LLM に渡されることで、LLM は「get_weather という関数があり、location パラメータが必須で、unit はオプション」ということを認識します。
Tool の説明文が不正確・不明確だと、LLM が誤ったツール選択や不正なパラメータ値を生成する可能性が高まります。必ず実装と説明文の整合性を確保し、エッジケースや制約条件も明記してください。
ステート管理とメッセージ履歴
Agent ハーネスの重要な役割は、メッセージ履歴(コンテキストウィンドウ)を管理することです。Agent が推論を続けるためには、以下の情報が必要になります。
- ユーザー入力: 最初のリクエスト
- LLM の推論履歴: これまでの出力
- Tool 実行結果: ツール呼び出しがあった場合の戻り値
メッセージ履歴の管理例:
# 初期状態
messages = [
{"role": "user", "content": "東京の天気を調べて、洗濯物を干せるか判断して"}
]
# LLM 応答(ツール呼び出しあり)
messages.append({
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_001",
"function": {"name": "get_weather", "arguments": "{\"location\": \"Tokyo\"}"},
"type": "function"
}
]
})
# Tool 実行結果
messages.append({
"role": "tool",
"tool_call_id": "call_001",
"content": "東京: 気温 22°C、湿度 65%、曇り、降水確率 10%"
})
# LLM の最終応答(ループ終了)
messages.append({
"role": "assistant",
"content": "東京の天気は曇りで、降水確率が低いため、洗濯物を干すのに適しています。気温も過ごしやすい 22°C です。"
})
このメッセージ履歴を保持することで、Agent は複数ターンのやり取りを通じてコンテキストを保ち、より精密な判断ができるようになります。
ループ制御とエラーハンドリング
Agent ハーネスの実装では、無限ループ防止や例外処理が不可欠です。
def run_agent_with_safeguards(self, user_input: str) -> str:
iteration_count = 0
max_iterations = 10
token_budget = 4000 # トークン使用量の上限
total_tokens_used = 0
while iteration_count < max_iterations:
iteration_count += 1
try:
# LLM 呼び出し
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=self.messages,
tools=self._build_tool_definitions(),
max_tokens=500 # 1 回の応答の最大トークン数
)
# トークン使用量を追跡
total_tokens_used += response.usage.total_tokens
if total_tokens_used > token_budget:
return "Token budget exceeded. Agent halted."
# Tool 呼び出しがない場合は終了
if not response.choices[0].message.tool_calls:
return response.choices[0].message.content
# Tool 実行(タイムアウト付き)
for tool_call in response.choices[0].message.tool_calls:
try:
result = self._execute_tool_with_timeout(
tool_call, timeout=5
)
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result)
})
except TimeoutError:
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": "Tool execution timeout"
})
except Exception as e:
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": f"Tool execution error: {str(e)}"
})
except Exception as e:
return f"Agent error: {str(e)}"
return "Maximum iterations reached"
実装時の落とし穴:無限ループとトークン枯渇。ツール実行結果が曖昧だと LLM が同じツールを何度も呼び出す可能性があり、トークン消費が急速に増加します。必ず反復回数とトークン上限の両方を設定しましょう。
アーキテクチャと仕組み
Agent ハーネスの全体フロー:
このシーケンス図から読み取れるように、Agent ハーネスは LLM と Tool 実行環境の仲介役 です。LLM の判断(Tool 呼び出し指示)を解析し、Tool を実行し、その結果を LLM にフィードバックするループを何度も回します。
内部構造図:
ループ中か"} B -->|初回| C["メッセージ履歴初期化"] C --> D["Tool 定義ビルド"] B -->|ループ中| D D --> E["LLM API 呼び出し"] E --> F{"Tool 呼び出し
あり?"} F -->|なし| G["最終応答を返却"] F -->|あり| H["Tool 呼び出し解析"] H --> I["Tool 実行"] I --> J{"実行成功?"} J -->|成功| K["結果をメッセージ追加"] J -->|失敗| L["エラーメッセージ追加"] K --> M{"反復上限
到達?"} L --> M M -->|いいえ| E M -->|はい| N["タイムアウト応答返却"] G --> O["出力: Agent の応答"] N --> O
他の選択肢との比較
Agent ハーネスの実装方法やフレームワークは複数あります。自分で実装する場合と既存フレームワークを使う場合のトレードオフを表にまとめました。
| 項目 | 自前実装(Theo スタイル) | LangChain | AutoGen | CrewAI |
|---|---|---|---|---|
| 学習曲線 | 低い(基本から始まる) | 中~高(抽象度が高い) | 高(マルチエージェント対応) | 高(ロール分担の概念) |
| カスタマイズ性 | 最高(すべて自分で制御) | 高(プラグイン拡張可) | 中(フレームワーク依存) | 中(スキーマ定義に制限) |
| 実装時間 | 長い(イチから構築) | 中(ボイラープレート削減) | 短い(テンプレート豊富) | 短い(構造化が進む) |
| デバッグ性 | 最高(すべてが見える) | 中(ロギング機能あり) | 中(Agent 間通信が複雑) | 低(内部ロジックが隠蔽) |
| 本番環境対応 | 工数あり(堅牢性確保が必要) | 良い(成熟したコード) | 良い(多数の本番事例) | 良い(エンタープライズ向け) |
| 推奨用途 | 学習・プロトタイプ・特殊要件 | 汎用 Agent・Chatbot | マルチエージェント・議論型 | 組織・ワークフロー型 |
Agent ハーネスの仕組みを理解したうえで、フレームワークを選ぶ方が賢明です。まずは自前実装で基本を掴み、その後、スケーラビリティやチーム開発の効率を考慮してフレームワークへの移行を検討するのが定石です。
実務への影響
エンジニアにとっての意味
Agent ハーネスの実装例が公開される意味は、開発者エンパワーメントです。これまで、Agent を使うには LangChain や OpenAI の API リファレンスに頼るしかありませんでした。しかし、実装例があると、以下のようなことが可能になります。
- トラブルシューティングが簡単に - 「なぜ Agent がこの判断をしたのか」を追跡できるようになり、問題箇所を特定しやすくなります。
- カスタマイズが自信を持ってできる - フレームワーク内の「魔法」を理解することで、独自の機能追加や最適化が怖くなくなります。
- 意思決定が合理的に - フレームワーク選択や実装方針を「理解に基づいて」決められるようになります。
実装パターンの標準化
実装例の公開により、コミュニティ内で Agent ハーネスの「ベストプラクティス」が浮き彫りになります。
- Tool 定義の粒度 - 1 つの Tool は何をする単位が適切か
- エラーハンドリングの戦略 - Tool 実行失敗時にどう対応するか
- コンテキスト管理の最適化 - メモリと推論精度のバランスをどう取るか
これらが標準化されることで、新規プロジェクトの開発速度と品質が大幅に向上します。
セキュリティと信頼性の向上
自前実装では、以下の点を必ず確認してください。
• Tool パラメータのバリデーション(LLM が意図しない値を渡す可能性)
• Tool 実行のサンドボックス化(悪意のある Tool 呼び出しを防ぐ)
• API キーやシークレットの保護(メッセージ履歴に含まれないようにする)
• レート制限(Tool 呼び出し頻度の上限設定)
実装例から学ぶことで、セキュリティリスクを事前に察知し、設計段階で対策を組み込めます。
まとめ
Agent ハーネスは、複雑に見えても、実装レベルでは「LLM 呼び出し + Tool 実行 + ループ制御」の組み合わせに過ぎない。Theo が実装例を公開したことは、AI エンジニアコミュニティにとって大きな転機です。
これまで「ブラックボックスの魔法」と思われていた Agent の仕組みが、白箱化されることで、以下のことが実現します。
- 開発者の自信と理解が深まる
- フレームワークの選択が賢くなる
- カスタマイズや最適化がしやすくなる
- セキュリティリスクへの認識が向上する
今後の AI 開発では、「フレームワークに依存する」のではなく、「基本を理解したうえでフレームワークを活用する」という姿勢が標準になるでしょう。Theo の実装例は、その転換点を象徴しています。
参照ソース
この記事はAI関連コンテンツの解説記事です。