1. はじめに — この記事を読む価値
この記事では、Microsoft Agent Framework(以下 MAF)を使って「ユーザの要件に対して自律的に 計画 → 実行 → 再評価 を繰り返しながら要件定義の成果物を生成していくReAct like エージェント」の実装に挑戦し、得られた経験と知見を整理しました。
MAF は MicrosoftがサポートするLLM ベースの agent ライブラリAutoGenおよびセマンティックカーネルの後継で、これら二つの統合を目指す2025年10月発表の最新AgentフレームワークOSSです。function calling / structured output / context provider / workflow など基本的な機能は押さえていますが、複雑なシステムの実装には設計思想を理解する必要があったので、この知見を共有できればと思います。
この記事を読み終えると、以下のような知識が得られます。
- MAF の基本的な構成と動作モデル
- エージェントによる反復的な行動制御と PDCA サイクルの設計
- 成果物や中間状態をエージェントが 長期的に保持する方法
- 実際の実装でハマりがちなポイントとその解決方法
さらに、この記事では単に「こう書けば動く」というだけでなく、なぜそう書くのか・どんな設計判断が有効かまで踏み込んで解説します。
2. 背景とモチベーション
近年、LLM を用いたツールやエージェント実装のハンズオン記事は増えていますが、
ほとんどは「単発の対話のあと、最適な関数を呼び出して終了する」というパターンです。
これはたとえば以下のようなものです:
User: 「◯◯ をやりたい」
→ LLM が最適なツールを選択
→ 関数の出力から推論を返し処理終了
→ ユーザは再度次の指示を出す必要有
このパターンは単純で分かりやすいのですが、実際の業務用途では十分ではありません。
たとえば要件定義支援のようなタスクでは、
- ユーザの指示だけでは明確でない部分を補完したい
- 中間成果物(要件リスト、矛盾チェック結果など)を蓄積したい
- 自分で計画を立て、必要に応じてユーザに質問したい
- PDCA のように Planning → Do → Check → Act を回したい
という要件があり、単発のツール呼び出しだけでは実現できません。
2-1. なぜ自律的な反復が必要なのか
従来の LLM 呼び出しは「ユーザ入力 → LLM 推論 → 応答」という一往復処理にとどまりますが、今回のゴールは エージェントが自身で次の行動を判断し続けることです。
言い換えれば:
- エージェントが 現在の成果物を踏まえて次の行動を判断する
- 判断した行動が function_call や追加質問につながる
- 得られた結果を 永続的に蓄積して改善する
というフローが必要でした。
これは単純な「関数を呼んで結果を表示する」処理ではなく、
反復的に行動を計画し、実行し、評価するエージェントの自律制御が求められます。
こういった機構を最新のAgentフレームワークで簡単に実装できるのか?を調査したかったため、今回はMicrosoft Agent Frameworkを検証することにしました。
3. Microsoft Agent Framework (MAF)とは
今回の検証ではMAFの以下の要素を使ってサイクルの実装を試みました
- エージェント — LLM による行動判断とツール統合
- ContextProvider — エージェントのメモリ・長期情報の管理
- Function Calling — LLM からツール呼び出しを行う仕組み
3-1. エージェントの作成
MAFのエージェントは以下のようにLLMへのアクセスクライアントを作成し、.create_agent()やclass ChatAgent()コンストラクタにクライアントを指定することで作成できます。
import asyncio
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
client = AzureOpenAIChatClient(api_key="MyApiKey")
agent = client.create_agent(
instructions="面白ジョークを思いつくAIエージェントです",
name="ジョークエージェント"
)
そしてこのチャットAgentの.run()にてプロンプトをうつことでチャットエージェントと単発の会話ができるようになります。
async def main():
result = await agent.run("Tell me a joke about a pirate.")
print(result.text)
asyncio.run(main())
3-2. threadとContextProvider
前節だとステートレスな対話しかできないため、MAFでは二つのコンテキスト保持機構を用意しています。一つはthreadという会話履歴を保持する仕組みで、agentに紐づいたチャット履歴をthread変数として保持できます。
thread = agent.get_new_thread()
async def main():
result1 = await agent.run("Tell me a joke about a pirate.", thread=thread)
print(result1.text)
result2 = await agent.run("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread=thread)
print(result2.text)
asyncio.run(main())
しかし、こちらはユーザとエージェント間のチャットの会話履歴しか保存しないため、ユーザとのやり取りで得た情報を加工したり、辞書や構造体形式のオブジェクトを保持できないです。そこでMAFが用意しているもう一つのContextProviderです。
ContextProviderはエージェントが基になる推論サービスを呼び出す前と後にカスタム ロジックを実行するためにオーバーライドできる 2 つのメソッドとして以下を用意しています:
| フック | 呼ばれるタイミング | 役割 |
|---|---|---|
invoking() |
モデル呼び出し直前 | 最新メモリをプロンプトとして注入 |
invoked() |
モデル応答の直後 | 応答を解析し状態を反映 |
例えば「functionで処理した内容をinvoked()の処理内でインスタンス変数に保存しておく」とか「invoked()で保存した最新の変数を、invoking()の処理内でプロンプトに埋め込んで最新の情報をLLMに提示する」と言ったことが可能になります。
from collections.abc import MutableSequence, Sequence
from dataclasses import dataclass, field
from pydantic import BaseModel
from typing import Any, Dict, List, Literal
from agent_framework import ContextProvider, Context, ChatAgent, ChatClientProtocol, ChatMessage, ChatOptions
# まずは保存したいデータ型を定義する
class FunctionalRequirementsInfo(BaseModel):
summary: str = "" # 要件定義の要約
functional_requirements: List[str] = field(default_factory=list) # 機能要件一覧
# メモリクラスの定義
class FunctionalRequirementMemory(ContextProvider):
"""ユーザの要件を管理するメモリコンテキストプロバイダ"""
summary: str = ""
functional_requirements: List[str] = field(default_factory=list)
async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
# invoking()はLLMの呼び出し前に実行され、任意の処理を記述できる
# 例:保存したContext情報をプロンプトに付与するなど
instructions = f"前回までの要件定義の要約は次の通りです:{self.summary}\nこれを基に次の要件定義に向けたアクションを実行してください"
return Context(instructions=instructions)
async def invoked(self, request_messages: ChatMessage | Sequence[ChatMessage], response_messages: ChatMessage | Sequence[ChatMessage] | None = None, invoke_exception: Exception | None = None, **kwargs: Any) -> None:
# invoked()はLLMの呼び出し後に実行され、任意の処理を記述できる
# 例:生成AIの推論や、関数呼び出しの返り値をインスタンス変数に保存しておく
for msg in response_messages:
# 一つのresponse messageには関数呼び出し結果など複数のcontentが含まれる
for content in msg.contents:
# contentには"text","function_call","function_result"などのタイプで分けられるので、これらをヒントに好きな処理を実行できる
if content.type == "function_result":
# 例:関数呼び出しの出力が{"functional_requirements":new_value}の形式だったら、保持しているfunctional_requirementsに最新情報を追加する
for k,v in content.result.items():
if k == "functional_requirements":
# ここでセットした値は後にAgentが参照したり、
self.functional_requirements += v
今回の検証では「MAFで要件定義のやり取り情報を保持しておき、参照できる」機能を実装したいので、このContextProviderを利用することになります。
3-3. 関数呼び出し
MAFではエージェントによる関数ツール呼び出し(function calling)機能を提供しています。
ただし、LLMエージェントがどの関数をいつ呼び出すべきか理解するために①DocStringによる説明、または②@ai_functionデコレータによるメソッドのdescription追加が必要になります。
from typing import Annotated
from pydantic import Field
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
return f"The weather in {location} is cloudy with a high of 15°C."
from agent_framework import ai_function
@ai_function(name="weather_tool", description="Retrieves weather information for any location")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
return f"The weather in {location} is cloudy with a high of 15°C."
関数が定義できたらエージェント生成時やagent.run()メソッドの引数にtoolsとして指定することで、エージェントは「ユーザの指示に対して必要な関数ツールの選択と、そのツール必要な引数を推論し、ツールを実行したうえで、次の推論を実行」します。
import asyncio
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
agent = AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions="You are a helpful assistant",
tools=get_weather
)
async def main():
result = await agent.run("What is the weather like in Amsterdam?")
print(result.text)
asyncio.run(main())
この時、注意として「1回のagent.run()で複数の関数をLLMが勝手に計画して呼び出す可能性」があります。例えばtoolsとしてfunction_1(),function_2()があったとして、これら両方を呼び出すべきとLLMが判断して、両者を実行してagent.run()1回分が終了することもあり得ます。
4. やりたいことを具体化した設計
ここからは、反復的な計画実行ループ をどう設計したかを説明します。
特に PDCA サイクルと、要件定義支援エージェントの仕様、成果物と中間情報の扱い方という観点から整理します。
4.1 PDCA サイクルの導入
今回のエージェントは単発の関数呼び出しではなく、エージェントが自身の判断で行動を選択し続ける仕組みを作る必要がありました。そこでMAFの仕組みを用いて、 PDCA(Plan → Do → Check → Act)のようなループを組み込み、LLM の内部意思決定を制御する必要がありました。
エージェントは次のようなサイクルで動きます:
- Plan(計画)
- 現在の要件・履歴を参照し、次に実行すべきアクション(ツール呼び出し/最終回答)を計画する。
- Do(実行)
- LLM が計画したツール呼び出しを実行する。
- Check(チェック)
- 実行結果を解析し、成果物/要件に反映する。
- Act(改善)
- 次の計画に反映するために状態を更新し、必要なら追加質問や再試行を選択する。
このサイクルを while ループで継続的に回すことで、ユーザ入力がなくても LLM 主体で PDCA を続けることができる設計になっています。
4.2 要件定義支援エージェントの仕様
要件定義支援エージェントが担うべき機能は次の通りです:
- 要件の抽出:ユーザ入力から機能要件・非機能要件を整理する
- 曖昧性の解消:不明瞭な部分に対してユーザに明確化質問を行う
- 矛盾チェック:要件間の矛盾や欠落を検出する
- 成果物の蓄積:状態として要件情報・矛盾リスト・質問履歴・アーキ提案を保持する
これらを実現するために、
- RequirementSpecification という ContextProvider に成果物を保持
- DCAAgentState という ContextProvider で PDCA 状態を保持
という 2 層の状態管理 を実装しました。
4.3 成果物と中間情報の扱い方
成果物や中間情報は、関数の戻り値だけではなく状態として保持し、次の計画に利用することが重要です。MAF の仕様でも、ContextProvider を用いることでこれが可能となっています。
具体的には:
- 関数呼び出しの結果(例: 要件リスト)を ContextProvider に蓄積
- ループごとに最新状態をプロンプト先頭に挿入
- LLM の出力を structured output で受け取り履歴に追加
という形で 中間情報を循環利用しています。
4.4 最終的なコード概要
以下、上記を踏まえたコード全容となります。長いので興味のある方ご確認いただければと思います。また、実装についてはなかなか冬休みでやりきることが難しかったので、今年ゆっくり改修していくつもりです。すみません。。。(思った以上にやりたいことに対して、MAFが勝手にやってくれる部分とそうでない部分の範囲がバラバラに感じてました。)
コード全容
import argparse
import asyncio
import json
import os
from collections.abc import MutableSequence
from dataclasses import dataclass, field
from typing import Any, Dict, List, Literal
from agent_framework import (
ChatAgent,
ChatMessage,
Context,
ContextProvider,
ai_function,
)
from agent_framework.azure import AzureOpenAIChatClient
from dotenv import load_dotenv
from pydantic import BaseModel, ConfigDict
load_dotenv(".env")
AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT", "")
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME = os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "")
APIM_SUBSCRIPTION_KEY = os.environ.get("APIM_SUBSCRIPTION_KEY", "")
APIM_CHAT_MODEL_ID = os.environ.get("APIM_CHAT_MODEL_ID", "")
class ToolArgs(BaseModel):
# ここは具体的に必要なキーを明示
user_text: str
# 必要なら他のパラメータを固定フィールドで追加
model_config = ConfigDict(extra="forbid")
class NextActionOutput(BaseModel):
action_type: Literal["tool_call", "final_answer"]
tool_name: str | None
tool_args: ToolArgs | None # Any を使わない
action_result: str | None
comment: str | None
next_planning_state: str | None
model_config = ConfigDict(extra="forbid")
@dataclass
class RequirementSpecification(ContextProvider):
"""ユーザの要件を管理するメモリコンテキストプロバイダ"""
summary: str = ""
functional_requirements: List[str] = field(default_factory=list)
nonfunctional_requirements: List[str] = field(default_factory=list)
clarification_questions: List[str] = field(default_factory=list)
contradictions: List[str] = field(default_factory=list)
architecture_references: List[str] = field(default_factory=list)
client: AzureOpenAIChatClient | None = None
async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
instructions = "## 現在の要件一覧\n"
instructions += f"- 要約: {self.summary}\n"
instructions += f"- 機能要件: {self.functional_requirements}\n"
instructions += f"- 非機能要件: {self.nonfunctional_requirements}\n"
instructions += f"- 確認質問内容: {self.clarification_questions}\n"
instructions += f"- 矛盾点: {self.contradictions}\n"
instructions += f"- アーキテクチャ参照: {self.architecture_references}\n"
return Context(instructions=instructions)
async def invoked(self, request_messages, response_messages=None, invoke_exception=None, **kwargs):
for msg in response_messages:
contents = msg.contents
for content in contents:
if content.type == "function_call":
continue
elif content.type == "function_result":
for k, v in content.result.items():
if hasattr(self, k):
current_value = getattr(self, k)
if isinstance(current_value, list) and isinstance(v, list):
new_value = current_value + v
setattr(self, k, new_value)
print(f"{k} updated:", new_value)
continue
elif content.type == "text":
self.summary = json.loads(content.text).get("comment", "")
continue
else:
print("Unknown content type:", content.type)
@dataclass
class PDCAAgentState(ContextProvider):
planning_state: Literal["plan", "do", "check", "act", "terminate"] = "plan"
planning_reasoning: str = ""
history: List[str] = field(default_factory=list)
action_results: List[str] = field(default_factory=list)
is_terminate: bool = False
iteration_count: int = 1
client: AzureOpenAIChatClient | None = None
async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs) -> Context:
instructions = f"""
以下の JSON スキーマで次の行動を出力してください:
action_type: "tool_call" | "final_answer"
tool_name: 呼び出すツール名 (None 可)
tool_args: 引数値の辞書 (None 可)
action_result: ツール実行結果(None 可)
comment: 注釈
next_planning_state: 次の計画状態、作業を継続する場合は最後の状態を繰り返すこと、またユーザの要件が整理出来たタイミングで "terminate" を指定すること
現在の planning_state: {self.planning_state}
history: {self.history[-5:] if self.history else "なし"}
前のaction_result: {self.action_results[-1] if self.action_results else "なし"}
"""
return Context(instructions=instructions)
async def invoked(self, request_messages, response_messages=None, invoke_exception=None, **kwargs):
try:
for msg in response_messages:
for content in msg.contents:
if content.type == "text":
resp_json = json.loads(content.text)
action_type = resp_json.get("action_type", "other")
next_planning_state = resp_json.get("next_planning_state", "plan")
comment = resp_json.get("comment", "no comment found")
history = (
f"ActionType: {action_type}, NextPlanningState: {next_planning_state}, Comment: {comment}"
)
self.history.append(history)
self.action_results.append(comment)
if content.type == "function_result":
continue
if content.type == "function_call":
continue
if next_planning_state == "terminate":
self.is_terminate = True
print(f"{self.iteration_count}th comment from Agent:", comment)
except Exception as e:
print("Error parsing response_messages for PDCAAgentState:", e)
finally:
self.iteration_count += 1
# ユーザ入力から機能要件を抽出して、要件を要素に持つリストをメモリに保存しつつ返します。
@ai_function(
description='ユーザ入力文から機能要件を抽出してカンマで繋げたものを入力し、要件を要素に持つリストを作ります。入力は以下の形式: "要件1,要件2,..."'
)
def decompose_functional_reqs(user_input: str) -> Dict:
print("*** function called: decompose_functional_reqs with", user_input)
functional_reqs = [x.strip() for x in user_input.split(",") if x.strip()]
return {"functional_requirements": functional_reqs}
# ユーザの入力から非機能要件を抽出して、要件を要素に持つリストをメモリに保存しつつ返します。
@ai_function(
description='ユーザ入力文から非機能要件を抽出してカンマで繋げたものを入力し、要件を要素に持つリストを作ります。入力は以下の形式: "要件1,要件2,..."'
)
def decompose_nonfunctional_reqs(user_input: str) -> Dict:
print("*** function called: decompose_nonfunctional_reqs with", user_input)
nonfunctional_reqs = [x.strip() for x in user_input.split(",") if x.strip()]
return {"nonfunctional_requirements": nonfunctional_reqs}
# ユーザの要件が不明瞭な場合や観点が抜けている場合に、明確化のための質問を生成する
@ai_function(description="ユーザの要件が不明瞭な場合や観点が抜けている場合に、明確化のための質問を生成します。")
def check_clarifications(current_requirements: List[str]) -> Dict:
print("*** function called: check_clarifications with", current_requirements)
new_requirements = []
for req in current_requirements:
user_answer = input(f"requirement '{req}' について、追加の要望があれば解凍してください: ")
new_requirements.append(user_answer)
return {"clarification_questions": current_requirements, "functional_requirements": new_requirements}
# ユーザの要件が矛盾している場合に、その矛盾点を検出する
@ai_function(description="抽出された要件が矛盾している場合に、その矛盾点を検出して返します。")
def detect_contradictions(requirements: List[str]) -> Dict:
print("*** function called: detect_contradictions with", requirements)
return {"issues": requirements}
# 現在のメモリからユーザの要件を整理した内容をマークダウンファイルに出力する
# ユーザに追加の要望がないか尋ねる
# ユーザの要件を決め切れたと判断出来たら処理を終了する
async def main(user_input: str):
# 1. 初期化
client = AzureOpenAIChatClient(api_key=APIM_SUBSCRIPTION_KEY)
# 2. メモリコンテキストプロバイダの初期化
requirement_specification = RequirementSpecification(client=client)
pdca_agent_state = PDCAAgentState(client=client)
async with ChatAgent(
chat_client=client,
instructions="あなたはユーザの要件定義を支援するエージェントです。機能要件と非機能要件を洗い出し、よくユーザの要件の曖昧部分・不足部分がないか毎回現在の要件内容を点検してください。さらに要件に矛盾がある場合はその旨を指摘してください。この要件定義支援を実行する際、現在のプランの実行状況とツールの一覧をもとに、次の計画を立てて行動を選択・実施していくことに気を付けてください。",
tools=[decompose_functional_reqs, decompose_nonfunctional_reqs, check_clarifications, detect_contradictions],
context_providers=[requirement_specification, pdca_agent_state],
) as agent:
thread = agent.get_new_thread()
while True:
if thread.context_provider is not None and hasattr(thread.context_provider, "providers"):
providers = thread.context_provider.providers
requirement_specification = providers[0] if len(providers) > 0 else None
pdca_agent_state = providers[1] if len(providers) > 1 else None
else:
requirement_specification = None
pdca_agent_state = None
result = await agent.run(user_input, thread=thread, response_format=NextActionOutput)
if thread.context_provider:
if thread.context_provider.providers[1].is_terminate:
print("~~~ PDCA エージェントの処理を終了します。~~~ ^ _ ^")
break
print("エージェントの応答:", json.loads(result.text)["comment"])
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="要件の計画・実行・レビューを行うエージェントを実行します。")
parser.add_argument(
"user_input", help='ユーザー入力(要件・依頼内容)を文字列で指定します。例: "Webスクレイパーを作成して。"'
)
args = parser.parse_args()
asyncio.run(main(args.user_input))
以下に 第5章(工夫・推しポイント) と 第6章(まとめ・考察・今後の展望) の Qiita 記事草案(Markdown形式)を作成しました。
読み手が 何を工夫したのか・どこに価値があるのか・今後どこに進めるか が分かるように整理しています。
## 5. 工夫・推しポイント
本章では、実装中に苦労した点とその解決策、さらに今回の設計で特に意識した **推しポイント** を紹介します。
---
### 🚀 5.1 Structured Output の型設計
MAF では `response_format` に **Pydantic モデルを渡すことで LLM の出力を構造化**できます。
これは単純な text 返却よりも安全で、アプリ側で確実に解釈できるメリットがあります。
ただし **Azure OpenAI の Structured Output 仕様は JSON schema validator が厳格**であるため、
追加プロパティを禁止(`extra="forbid"`)した schema の設計が必要でした。
この点は公式ドキュメントでも注意点として触れられています。:contentReference[oaicite:0]{index=0}
```python
class NextActionOutput(BaseModel):
action_type: Literal["tool_call","final_answer"]
tool_name: str | None
tool_args: ToolArgs | None
action_result: str | None
comment: str | None
next_planning_state: str | None
model_config = ConfigDict(extra="forbid")
この設計により、
- LLM が返す次のアクションを確実に構造化できる
- 意図しないプロパティを弾いて検証できる
といった利点が得られました。
🔁 5.2 PDCA の反復ループ制御
従来の単発処理型 LLM の流れは
User Input → LLM → Function Call → Text Output
という 一往復処理 です。しかし、要件定義のようなタスクでは
- 前回の成果物を踏まえながら
- 新しい観点を取り入れて
- 必要ならユーザに質問し
- 状態を更新しながら継続動作
という 反復的な計画・実行・評価サイクル(PDCA) が必要になります。
今回の実装では PDCA を明示的に回すために
-
planning_stateを enums で表現 -
NextActionOutput.next_planning_stateで状態遷移 - ループ内で判定・終了判定を行う
というフローにしました。
if next_planning_state == "terminate":
self.is_terminate = True
この設計により、連続して LLM が自己完結的に動き続ける構造を実現できています。
📌 5.3 ContextProvider を使った成果物保持
MAF の公式ドキュメントでは ContextProvider を実装することでエージェントの コンテキストやメモリを長期的に保持・更新できると説明されています。([Microsoft Learn][1])
たとえば今回の RequirementSpecification では、
async def invoking(...)
# 最新の成果物をプロンプト先頭に注入
async def invoked(...)
# function_result や structured 出力を解析し状態を更新
という形で反復処理の状態を積み上げています。
この “注入 → 実行 → 更新” というパターンは MAF の基本設計でも想定されている構造であり、
エージェントが「今まで積み上げてきた要件・矛盾・質問リスト」を参照しながら計画を立てられるようになります。
💡 5.4 関数単体ではなく「状態と関数」を分離
一般的な LLM + Function Calling の実装では、関数が入力を受けて単純に出力を返すだけですが、
今回の実装では 関数はあくまで純粋処理に留め、状態管理は外部の ContextProvider で担わせる という責務分離を意識しました。
こうすることで、
- 関数自体の再利用性が高まる
- 状態更新のロジックを集中管理できる
- 計画制御の変更や拡張が容易になる
といったメリットが生まれています。
🧠 5.5 履歴と成果物の両方を扱う工夫
エージェントが次の行動を決めるときに重要なのは ただの会話履歴ではなく、現在の成果物(要件等)の状態 です。
これを実現するため、最新成果物だけでなく 履歴の一部をプロンプト内に注入しながら次の Plan を出させるようにしています。
history: [直近の 5 履歴]
前の実行結果: ...
現在の planning_state: ...
という形で状態を整理して提示することで、LLM の判断品質を一定に保てています。
6. まとめ・考察・今後の展望
✅ 6.1 今回の実装でできたこと
この記事では、単発の LLM ツール呼び出しではなく、エージェント自身が自律的に反復的行動を制御する仕組みを MAF で実装しました。
これにより、
- 要件情報を蓄積しながら自律的に function_call を選ぶ
- 計画状態を持ちつつ PDCA ループを回す
- 状態と実行結果を structured output で扱う
- 次の行動を自律的に判断し続ける
といった設計が実現できています。
📊 6.2 考察
今回の実装から得られた知見として以下があります:
◎ LLM だけに任せない設計が重要
単純なプロンプトだけで完結する設計は実運用では脆弱です。
構造化出力や明示的な状態管理を組み込むことで、確実に制御可能なシステムになりました。
◎ ContextProvider は単なる “メモリ” 以上の役割
ContextProvider はメモリというより、状態注入・LLM ガイド役として機能します。実行前に instructions を整形することで、LLM の解釈精度が高まることを体験しました。([Microsoft Learn][2])
🔭 6.3 今後の展望
今後さらに進めたい方向性は次のとおりです:
🔹 Planning の可視化
エージェント内部でどのように次の行動を判断しているかを可視化する仕組みがほしい。
Trace / Thought Log を外部に出力することで “計画の見える化” を進めたいです。
🔹 ワークフローとの統合
MAF はワークフロー機能も提供しており、agent → workflow → agent といった複合的制御も可能です。
これを要件定義後の別フェーズ処理に繋げられるようにしたいです。([Microsoft Learn][3])
🔹 マルチエージェント構成
A2A(agent to agent)で役割を分散する構成も検討できます。
例えば、要件抽出 agent と設計 agent、レビュー agent を連携させることで、専門性分離型の複合エージェントシステム を実現できそうです。([zenn.dev][4])
✨ 6.4 最後に
本稿で紹介した設計・実装は、MAF の可能性と課題を示す1つの実践例です。
フレームワークは急速に進化しており、設計・制御・Memory といった周辺機能も今後強化されていくと期待しています。
この記事が、MAF を使った反復制御系エージェント設計の参考になれば幸いです。
ご意見・コメント・改善案も歓迎します!