AI エージェントは、モデルが回答を返した時点で必ずしも仕事を完了しているとは限りません。複数の作業項目が残っている、品質基準を満たしていない、もう一段の推敲が必要、といった状況でも 1 回の応答で終了することがあります。
Microsoft Agent Framework の AgentLoopMiddleware は、1 回の agent.run(...) の内側でエージェントを条件付きで再実行します。create_harness_agent(...) は、履歴、Todo、動作モード、コンテキスト圧縮などを組み込んだ Agent を生成する関数です。
本記事では、ワークショップ教材 12_agent_loop_and_harness_agent.ipynb のステップに沿って、両者の役割と組み合わせ方を解説します。このノートブックには内部の理解を促進するため、デバッグ出力のための Helper をたくさん含んでいます。
全体像: ツールループの外側に再実行ループを置く
通常のエージェントにも、モデルがツールを要求し、その結果を受け取って再度モデルを呼ぶ「ツールループ」があります。AgentLoopMiddleware が追加するのは、その外側のループです。モデルがテキスト回答を返した後に外部条件を評価し、必要ならエージェント全体をもう一度実行します。
この構造により、完了マーカー、Todo の残数、別モデルによる評価などを、モデル自身の終了判断とは独立した停止条件にできます。反復的な改善、Todo の消化、バックグラウンドタスク、Judge による評価が代表的な用途として挙げられます。
最初に押さえる重要パラメーター
AgentLoopMiddleware の設計では、「続けるか」「次に何を渡すか」「何を進捗として残すか」を別々に考えます。最初に次の表を押さえておくと、後続のコード例で各設定が担う役割を追いやすくなります。
| パラメーター | 既定値 | 役割 | 設計上の要点 |
|---|---|---|---|
should_continue |
必須 | 各実行後に継続か停止かを判定する |
bool または (bool, feedback) を返す。形式検査、Todo 残数、Judge など、完了条件の中心になる |
max_iterations |
10 |
エージェントの最大実行回数を制限する | 上限到達時は should_continue を呼ばずに停止する。None は無制限なので通常は避ける |
next_message |
短い継続指示 | 次の iteration に渡す入力を作る |
feedback や未完了 Todo を具体的な次の指示へ変換する。None を返す場合の挙動は fresh_context により異なる |
record_feedback |
last_result.text |
各 iteration の記録を progress へ追加する |
応答全文ではなく短い要約を返すと、後続入力のトークン増加を抑えられる |
inject_progress |
True |
progress を次の入力へ自動注入する |
False では自動注入せず、コールバックの progress 引数からだけ参照できる |
fresh_context |
False |
iteration 間で履歴とセッション状態を引き継ぐかを決める |
True は元の入力と進捗から再開し、セッションも Loop 開始前へ戻す。Todo の継続には False が必要 |
return_final_only |
False |
非ストリーミング時の戻り値を制御する |
True は最終応答だけを返す。ストリーミング時には作用しない |
additional_instructions |
None |
全 iteration に共通する追加指示を先頭へ挿入する |
with_judge(...) は評価基準をエージェントへ伝えるために使用する |
1. AgentLoopMiddleware の最小例
should_continue は各実行後に呼ばれます。True なら継続、False なら停止です。bool の代わりに (bool, feedback) を返すと、停止判断の理由を後続のコールバックへ渡せます。
次は、応答に完了マーカーが出るまで再実行する最小例です。
from agent_framework import (
Agent,
AgentLoopMiddleware,
AgentResponse,
AgentSession,
TodoProvider,
create_harness_agent,
todos_remaining,
todos_remaining_message,
)
COMPLETE_MARKER = "<complete/>"
def marker_missing(
*,
iteration: int,
last_result: AgentResponse,
**_: object,
) -> tuple[bool, str]:
keep_going = COMPLETE_MARKER not in last_result.text
reason = f"run {iteration}: marker {'missing' if keep_going else 'found'}"
return keep_going, reason
marker_loop = AgentLoopMiddleware(
should_continue=marker_missing,
max_iterations=5,
fresh_context=True,
inject_progress=False,
return_final_only=True,
)
marker_agent = Agent(
client=create_chat_client(),
instructions=f"改善が完了したら末尾に {COMPLETE_MARKER} を付けてください。",
middleware=[marker_loop],
)
ワークショップコンテンツでは LLM の非決定性によって初回終了しないよう、「最低 3 回実行する」デモ用ゲートを使っています。実運用では、上のようなマーカー、スキーマ検証、テスト結果など、タスクの完了を表す条件へ置き換えます。
停止条件の評価順序
現行実装では、次の順序で停止します。
- ツール承認待ちがあれば呼び出し元へ制御を返す。
-
max_iterationsに到達していれば停止する。この場合should_continueは呼ばれない。 - それ以外では
should_continueを評価する。
max_iterations の既定値は 10、with_judge(...) の既定値は 5 です。None を指定すると無制限になりますが、停止不能とコスト増大を防ぐため、通常は正の上限を設定します。
2. feedback と progress を渡す
record_feedback は各 iteration の結果を progress ログへ変換します。inject_progress=True にすると、そのログが次の入力へ追加されます。
def summarize_iteration(
*,
iteration: int,
last_result: AgentResponse,
**_: object,
) -> str:
one_line = " ".join(last_result.text.split())
return f"run {iteration}: {one_line[:140]}"
feedback_loop = AgentLoopMiddleware(
should_continue=marker_missing,
max_iterations=5,
record_feedback=summarize_iteration,
inject_progress=True,
fresh_context=True,
return_final_only=True,
)
record_feedback を省略した場合、既定では last_result.text 全体が progress に記録されます。本番の Loop では短い要約を返すことを推奨しています。長い応答をそのまま蓄積すると、後続 iteration の入力トークンが増えるためです。
セッションを維持する fresh_context=False では、過去の履歴との重複を避けるため最新の progress だけが注入されます。セッションがない場合、または fresh_context=True の場合は、全 progress が注入されます。
3. context と戻り値を選ぶ
fresh_context と return_final_only は、それぞれ異なる問題を制御します。
| 設定 | False |
True |
|---|---|---|
fresh_context |
履歴とセッション状態を次の iteration に引き継ぐ | 初回入力と progress を基準に再開し、セッションも Loop 開始前の状態へ戻す |
return_final_only |
非ストリーミングでは全 iteration と注入メッセージを集約して返す | 非ストリーミングでは最後の応答だけを返す |
fresh_context=True では、ローカル履歴だけでなくサービス側の conversation ID と session.state もスナップショットへ復元されます。そのため、Loop 中に provider が書き込んだ作業状態は次の iteration へ残りません。継続情報は progress だけが担います。
選択基準は以下。
- 文章の推敲や独立した再生成には
fresh_context=Trueが向いている - ツール結果、Todo、動作モードなどの状態を積み上げる処理には
fresh_context=Falseが必要 - API 利用者に最終成果だけ返すなら
return_final_only=Trueを使う - 監査やデバッグで全過程が必要なら
return_final_only=Falseを使う
4. TodoProvider で作業完了まで回す
TodoProvider は Todo の作成、完了、削除、取得用ツールをエージェントへ追加します。既定の TodoSessionStore は Todo を AgentSession.state に保存するため、同じセッション内で状態が継続します。
todo_provider = TodoProvider()
todo_loop = AgentLoopMiddleware(
should_continue=todos_remaining(),
next_message=todos_remaining_message,
max_iterations=8,
fresh_context=False,
return_final_only=True,
)
todo_agent = Agent(
client=create_chat_client(),
instructions=(
"複数工程の依頼は Todo に分解し、完了した項目を順に更新してください。"
"すべての Todo を完了してから最終回答を返してください。"
),
context_providers=[todo_provider],
middleware=[todo_loop],
)
todo_session = AgentSession()
todo_response = await todo_agent.run(
"東京発、京都 2 泊 3 日の旅行を計画してください。",
session=todo_session,
)
todos_remaining() は実行中の agent.context_providers から TodoProvider を解決し、未完了項目が 1 件でもあれば True を返します。todos_remaining_message は未完了項目を次の指示に列挙します。
ここで fresh_context=True にすると、各 iteration 前に session.state が Loop 開始時点へ戻され、作成・完了した Todo が失われます。Todo と組み合わせる場合は fresh_context=False にします。
Todo が増え続ける、または誤って完了されない場合に備え、max_iterations は必ず設定します。Todo は進行状態であり、品質保証そのものではありません。成果物の妥当性が重要な場合は、決定的な検査または Judge と組み合わせます。
実際の Todo プロンプト
## タスク
作業項目を追跡するためのタスクリストを利用できます。
ユーザーからタスクの依頼を受けた際は、以下の手順に従って作業を管理してください:
1. その依頼が、完了までに複数の手順を必要とするもの(複雑)か、1つの手順で完了できるもの(単純)かを判断します。
2. 複雑な場合は、そのタスクを管理しやすいタスク項目に分割し、リストに追加します。
3. 単純なタスクの場合は、ToDo項目を追加せず、直接タスクを完了させてください。
### ToDoに関する一般的なガイドライン
効果的なToDoを作成するために、明確化が必要な点についてはユーザーに質問してください。
ユーザーから計画に対するフィードバックがあった場合は、それに応じて新しい項目を追加したり、不要な項目を削除したりして、ToDoを調整してください。
実行中は、ToDoリストを使ってやるべきことを把握し、完了した項目には「完了」のマークを付け、不要になった項目は削除してください。
ユーザーが話題を変えたり、考えを変えたり、新しい依頼に切り替えたりした場合は、必要に応じて、関係のない項目や古い項目を削除したり、リストをクリアしたり、新しい項目を追加したりして、ToDoリストを適切に更新するようにしてください。
タスクを管理するには、以下のツールを使用してください:
- todos_add を使用して、複雑な作業を追跡可能な項目に分解します(1つまたは複数の項目を一度に追加できます)。
- `todos_complete` を使用して、完了した項目を完了としてマークします(1つまたは複数の同時処理に対応)。項目がどのように完了したかを説明する理由も併せて記載してください。
- `todos_get_remaining` を使用して、まだ未処理の作業を確認します。
- `todos_get_all` を使用して、完了した項目を含む全リストを確認します。
- `todos_remove` を使用して、不要になった項目を削除します(1つまたは複数の同時処理に対応)。"
5. with_judge で品質評価を入れる
AgentLoopMiddleware.with_judge(...) は、別のチャットクライアントに元の依頼と最新応答を渡し、要求が満たされたかを評価させます。Judge が未完了と判定すると、その reasoning が feedback として次の iteration に渡されます。
judge_loop = AgentLoopMiddleware.with_judge(
create_chat_client(),
criteria=[
"要件をすべて満たしている",
"短い箇条書きのまとめを含む",
"最後に注意点を 1 つ書く",
],
max_iterations=5,
fresh_context=True,
)
judge_agent = Agent(
client=create_chat_client(),
instructions="日本語で簡潔な技術解説を書いてください。",
middleware=[judge_loop],
)
Judge はエージェントとして実行されるのではなく、judge_client.get_response(...) を直接呼びます。ツール、セッション、Agent middleware を通らないため、評価処理が同じ Loop へ再帰することを防ぎます。構造化出力 JudgeVerdict が利用できない場合は、明示的な verdict marker を使うフォールバックがあります。
6. create_harness_agent の最小構成
create_harness_agent(...) は、長いタスク向けの機能を設定した通常の Agent を返します。ノートブックでは外部依存を減らすため、Web 検索とツール自動承認を無効にしています。
harness_agent = create_harness_agent(
client=create_chat_client(),
name="HarnessBasics",
agent_instructions=(
"計画してから作業してください。"
"必要なら Todo を使い、最後に短いまとめを返してください。"
),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_tool_auto_approval=True,
)
現行実装で自動で用意される主な要素は次のとおりです。
| 機能 | 既定の扱い |
|---|---|
| 履歴 |
InMemoryHistoryProvider を使用 |
| コンテキスト圧縮 | 両方のトークン上限、またはカスタム strategy がある場合に構成 |
| Todo |
TodoProvider を追加 |
| 動作モード |
AgentModeProvider を追加 |
| ファイルメモリ |
FileMemoryProvider を追加 |
| ファイルアクセス |
file_access_store を渡した場合だけ追加 |
| Skills |
skills_provider または skills_paths を渡した場合だけ追加 |
| バックグラウンドエージェント |
background_agents を渡した場合だけ追加 |
| ツール承認 | 既定で有効 |
| Web 検索 | クライアントが対応し、無効化されていなければ追加 |
| OpenTelemetry | Agent の計測レイヤーを利用 |
既定のコンテキスト圧縮は、max_context_window_tokens と max_output_tokens の両方を指定した場合に構成されます。片方でも欠け、カスタム strategy もない場合は圧縮されません。
7. Harness Agent を実行する
戻り値は通常の Agent なので、実行 API は変わりません。provider の履歴と状態を関連付けるため、同じ AgentSession を渡します。
harness_session = harness_agent.create_session()
harness_response = await harness_agent.run(
"Agent Framework を学ぶ 30 分ミニ勉強会の流れを提案してください。",
session=harness_session,
)
print(harness_response.text)
Harness はモデルを特別な種類へ置き換えるものではありません。create_harness_agent(...) は、通常の Agent に長時間タスクを支える指示、コンテキストプロバイダー、ミドルウェア、ツールを組み込み、その Agent を返す関数です。
8. provider をカスタマイズする
用途に不要な provider は明示的に外せます。以下は Todo、Mode、Compaction を無効化した構成です。
compact_harness_agent = create_harness_agent(
client=create_chat_client(),
name="CompactHarness",
agent_instructions="短い単発タスクだけを処理してください。",
disable_todo=True,
disable_mode=True,
disable_compaction=True,
disable_web_search=True,
disable_tool_auto_approval=True,
)
この構成でも履歴とファイルメモリは残ります。ファイルメモリも不要なら disable_file_memory=True を追加します。既定のファイルメモリはカレントディレクトリの agent-file-memory を使用するため、ローカルファイルを作る副作用があります。
無効化は「軽量そうだから」ではなく、タスクの状態モデルから決めます。複数工程なら Todo、会話が長くなるなら圧縮、セッションをまたぐ作業記憶が必要ならファイルメモリ、というように必要性を説明できる機能だけを残すと、プロンプトとツール面が把握しやすくなります。
9. Harness Agent と Loop を組み合わせる
本番向けには、Harness 専用の loop_should_continue、loop_next_message、loop_max_iterations を使う構成が実現できます。
looped_harness_agent = create_harness_agent(
client=create_chat_client(),
name="LoopedHarnessAgent",
agent_instructions=(
"複数工程の依頼は Todo に分解してください。"
"execute モードでは全 Todo を完了してから終了してください。"
),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
loop_should_continue=todos_remaining(looping_modes=["execute"]),
loop_next_message=todos_remaining_message,
loop_max_iterations=8,
disable_web_search=True,
)
loop_should_continue を使うと、Harness は AgentLoopMiddleware を Tool Approval より外側へ配置します。各 iteration が承認処理を含む完全な Agent 実行になり、承認待ちの応答が出た場合は Loop を止めて呼び出し元へ返します。
ノートブックのステップ 11 は middleware=[harness_loop] を使います。これは Loop インスタンスを直接保持し、コールバックをラップして provider と middleware の呼び出し順を可視化するためです。ワークショップコンテンツの観測目的には適していますが、Harness の標準的な Loop 配置を得るには loop_should_continue=... を使います。
内部で呼ばれるイベントが多数あるので、整理のためノートブック内に Harness Trace Console を描画するようにしています。
参考:どの停止条件を選ぶか
| 停止条件 | 追加のモデル呼び出し | 適した用途 | 注意点 |
|---|---|---|---|
| 完了マーカー | なし | 単純な段階的改善 | モデルが早く出す、または出さない可能性がある |
todos_remaining() |
Judge 分はなし | 分解可能な複数工程 | Todo の見せかけ完了を別途検証する |
| Python の決定的検査 | 通常なし | JSON Schema、テスト、数値条件 | 検証規則をコード化できる場合に限る |
with_judge() |
iteration ごとに 1 回 | 自然言語品質、複数の評価観点 | コスト、判定の揺れ、信頼境界が増える |
実務では、まず決定的な検査を優先し、それだけでは評価できない品質を Judge で補う構成が扱いやすくなります。どの方式でも、独立した max_iterations を安全上限として持たせます。
完成した Harness Agent は Foundry の Hosted Agent としてデプロイ!
DEMO UI
まとめ
AgentLoopMiddleware は、モデルが一度回答した後に、完了条件を外部から再評価する仕組みです。should_continue、record_feedback、next_message を分けることで、停止判定、進捗の圧縮、次の指示を独立して設計できます。
create_harness_agent(...) は、履歴、Todo、Mode、Memory、Compaction などを通常の Agent へ接続します。Todo のようにセッション状態を使う処理では fresh_context=False、文章の再生成では fresh_context=True というように、継続させたい情報を基準にコンテキスト戦略を選ぶことが重要です。
両者を組み合わせると、長いタスクを「計画する」「実行する」「評価する」「必要ならやり直す」という一連の流れを、1 回の agent.run(...) の内側に構成できます。ただし、Loop はモデル呼び出し回数を増やしますので、停止上限、短い progress、トレース、Judge の信頼境界までを 1 つの設計として扱う必要があります。
GitHub
microsoft-agent-framework-workshop-1.13.0
参考


