0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Microsoft Agent Frameworkでコーディングエージェントを作ってみた

Posted at

はじめに

前回の記事「Microsoft Agent Framework 完全ガイド」では、Microsoft Agent Frameworkの基礎から応用までを詳しく解説しました。

今回は、その知識を活用して実際にAIコーディングエージェントを作ってみた事例を紹介します。Microsoft Agent Framework(MAF)を使えば、複雑なエージェントシステムを驚くほど簡単に構築できることを体感していただければと思います。

この記事で学べること

  • MAFを使った実践的なエージェント開発
  • マルチプロバイダー(OpenAI/Anthropic/Azure)対応の実装方法
  • ツール(ファイル操作、コマンド実行、TODO管理)の実装
  • ミドルウェアによる機能拡張
  • CLIアプリケーションとしての実装パターン

作成したもの

AIコーディングエージェントCLI - ファイル操作、コマンド実行、TODO管理などの開発支援機能を備えた対話型エージェントです。

  • リポジトリ: github.com/nogataka/MAF-sample
  • 主な機能:
    • OpenAI、Azure OpenAI、Anthropicの3つのLLMプロバイダーに対応
    • ファイルの読み書き、検索
    • 安全なシェルコマンド実行
    • TODO管理
    • 対話型チャットモード

プロジェクト構成

まず、プロジェクト全体の構成を見てみましょう。

MAF-sample/
├── coding_agent/
│   ├── agent.py            # エージェントのランタイム生成
│   ├── cli.py              # CLIエントリーポイント
│   ├── config.py           # 設定管理
│   ├── middleware.py       # ツール実行ログ
│   ├── providers/          # プロバイダー実装
│   │   ├── base.py         # プロバイダー抽象クラス
│   │   ├── anthropic.py    # Anthropic実装
│   │   ├── azure.py        # Azure OpenAI実装
│   │   └── openai.py       # OpenAI実装
│   ├── tools/              # ツール群
│   │   ├── file.py         # ファイル操作
│   │   ├── command.py      # コマンド実行
│   │   └── todo.py         # TODO管理
│   └── utils/
│       └── env.py          # 環境変数管理
├── pyproject.toml
└── README.md

シンプルな構成ながら、本格的なエージェントシステムに必要な要素が全て揃っています。


核となるエージェント実装

このセクションでは、AIコーディングエージェントの中核となる実装について詳しく解説します。エージェントの「頭脳」となるシステムプロンプトの設計から、会話履歴を管理するランタイム、そしてエージェントを生成するファクトリーパターンまで、実装の本質的な部分を見ていきましょう。

エージェントの設計思想

コーディングエージェントには、以下の特性が必要です:

  1. 明確な役割定義: システムプロンプトで開発支援の役割を明示
  2. ツール群の提供: ファイル操作、コマンド実行などの実行能力
  3. 安全性: 危険なコマンドの拒否、ファイルサイズ制限
  4. 可視性: ツール実行のログ表示

これらをagent.pyで実現しています。

システムプロンプトの定義

# coding_agent/agent.py (抜粋)

_SYSTEM_PROMPT = """
You are an AI coding assistant embedded in a developer CLI.

Responsibilities:
- Understand the user's software engineering goals before acting.
- Think step-by-step and describe your plan before making changes.
- Prefer reading files before editing them; never guess.
- Use the available tools for filesystem inspection, editing, searching,
  shell commands, and TODO tracking.
- Produce high-quality, maintainable, and secure code that follows
  project conventions.
- Highlight blockers or missing context instead of fabricating answers.
- When you modify files, summarise the change and include short
  explanations only if they aid understanding.
- Keep TODOs up to date for follow-up work.

Always reflect Microsoft Agent Framework best practices and the
AI駆動開発 guidelines.
""".strip()

このプロンプトは、エージェントに**「考えてから行動する」**という重要な特性を与えています。

ランタイムの生成

エージェントのランタイムは、実行中のエージェントの状態を管理する重要なコンポーネントです。会話履歴(AgentThread)を保持し、メッセージのやり取りを管理します。ここでは、プロバイダーに依存しない形でランタイムを設計することで、どのLLMプロバイダーでも同じインターフェースで操作できるようにしています。

# coding_agent/agent.py (抜粋)

@dataclass(slots=True)
class AgentRuntime:
    chat_agent: ChatAgent
    thread: AgentThread
    provider_name: str

    async def run(self, message: str) -> str:
        """メッセージを送信して応答を取得"""
        response = await self.chat_agent.run(message, thread=self.thread)
        text = getattr(response, "text", None)
        if text:
            return text
        return str(response)

    def reset(self) -> None:
        """会話履歴をリセット"""
        self.thread = AgentThread()

ポイント:

  • AgentThreadで会話履歴を管理
  • reset()で会話をリセット可能
  • プロバイダー名を保持して切り替えに対応

ランタイムファクトリー

ファクトリーパターンを使うことで、エージェントの生成プロセスを一箇所に集約し、複雑な初期化ロジックをカプセル化します。このcreate_runtime関数は、設定ファイルや環境変数からAPIキーとモデル情報を取得し、適切なプロバイダーを選択して、完全に初期化されたエージェントランタイムを返します。

# coding_agent/agent.py (抜粋)

def create_runtime(
    provider_name: str,
    *,
    config: AgentConfig,
    console: Console,
    model_override: Optional[str] = None,
    temperature: Optional[float] = None,
) -> AgentRuntime:
    """設定からエージェントランタイムを生成"""

    # 1. プロバイダーを取得
    provider = registry.get(provider_name)

    # 2. APIキーを解決(config → 環境変数の順)
    api_key = config.resolve_api_key(provider_name) or _env_api_key(provider_name)

    # 3. モデルを解決
    resolved_model = model_override or config.resolve_model(
        provider_name,
        provider.default_model
    )

    # 4. ミドルウェアを設定
    middleware = [ToolLoggingMiddleware(console)]

    # 5. エージェントを生成
    chat_agent = provider.create_agent(
        runtime_config,
        instructions=_SYSTEM_PROMPT,
        tools=DEFAULT_TOOLS,
        middleware=middleware,
        agent_name="maf-coding-agent",
    )

    return AgentRuntime(
        chat_agent=chat_agent,
        thread=AgentThread(),
        provider_name=provider_name
    )

このファクトリー関数が、設定からエージェントを生成する中心的な役割を果たします。


マルチプロバイダー対応の実装

現代のAIエージェント開発では、複数のLLMプロバイダーに対応することが重要です。OpenAI、Azure OpenAI、Anthropicなど、それぞれのプロバイダーは異なるAPIを持っていますが、エージェントのロジック自体はプロバイダーに依存しないべきです。このセクションでは、プロバイダーパターンを使って、異なるLLMを統一的に扱う実装を見ていきます。

プロバイダー抽象化

異なるLLMプロバイダーを統一的に扱うため、抽象クラスProviderを定義しています。この抽象クラスは、すべてのプロバイダー実装が従うべき共通のインターフェースを定義し、プロバイダーの切り替えを容易にします。

# coding_agent/providers/base.py (抜粋)

@dataclass(slots=True)
class ProviderConfig:
    """プロバイダーのランタイム設定"""
    api_key: str | None
    model: str | None
    endpoint: str | None = None
    deployment: str | None = None  # Azure用
    api_version: str | None = None  # Azure用
    temperature: float | None = None


class Provider(ABC):
    """プロバイダーの統一インターフェース"""

    name: ProviderName
    default_model: str
    available_models: Sequence[str]

    @abstractmethod
    def create_agent(
        self,
        config: ProviderConfig,
        *,
        instructions: str,
        tools: Sequence[Any],
        middleware: Iterable,
        agent_name: str = "coding-agent",
    ) -> ChatAgent:
        """ChatAgentインスタンスを生成"""

OpenAI実装例

OpenAI用のプロバイダー実装を見てみましょう。Provider抽象クラスを継承し、OpenAI特有のクライアント初期化とモデル設定を行います。MAFのOpenAIChatClientを使うことで、OpenAIのAPIを簡単にエージェントに統合できます。

# coding_agent/providers/openai.py (抜粋)

from agent_framework.openai import OpenAIChatClient
from openai import AsyncOpenAI

class OpenAIProvider(Provider):
    """OpenAI用プロバイダー実装"""

    def __init__(self) -> None:
        super().__init__(
            name="openai",
            default_model="gpt-4o",
            available_models=[
                "gpt-4o",
                "gpt-4o-mini",
                "gpt-4-turbo",
                "gpt-3.5-turbo",
            ],
        )

    def create_agent(
        self,
        config: ProviderConfig,
        *,
        instructions: str,
        tools: Sequence[Any],
        middleware: Iterable,
        agent_name: str = "coding-agent",
    ) -> ChatAgent:
        # OpenAIクライアントを作成
        client = AsyncOpenAI(api_key=config.api_key)

        # ChatClientラッパーを作成
        chat_client = OpenAIChatClient(
            client=client,
            model=config.model or self.default_model,
            temperature=config.temperature,
        )

        # ChatAgentを返す
        return ChatAgent(
            name=agent_name,
            description=instructions,
            chat_client=chat_client,
            tools=list(tools),
            function_middleware=list(middleware),
        )

Azure OpenAI実装例

Azure OpenAIは、エンタープライズ向けにセキュリティとコンプライアンスを強化したOpenAIサービスです。この実装では、APIキー認証とMicrosoft Entra ID(旧Azure Active Directory)認証の両方に対応しています。エンドポイント、デプロイメント名、APIバージョンなど、Azure特有の設定を扱います。

# coding_agent/providers/azure.py (抜粋)

from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import DefaultAzureCredential

class AzureOpenAIProvider(Provider):
    """Azure OpenAI用プロバイダー実装"""

    def __init__(self) -> None:
        super().__init__(
            name="azure",
            default_model="gpt-4o",
            available_models=["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
        )

    def create_agent(
        self,
        config: ProviderConfig,
        *,
        instructions: str,
        tools: Sequence[Any],
        middleware: Iterable,
        agent_name: str = "coding-agent",
    ) -> ChatAgent:
        # 認証方法を決定
        if config.api_key:
            credential = config.api_key
        else:
            credential = DefaultAzureCredential()

        # AzureChatClientを作成
        chat_client = AzureOpenAIChatClient(
            azure_endpoint=config.endpoint,
            azure_deployment=config.deployment,
            api_version=config.api_version or "2024-05-01-preview",
            azure_ad_token_provider=credential
                if not isinstance(credential, str) else None,
            api_key=credential if isinstance(credential, str) else None,
            temperature=config.temperature,
        )

        return ChatAgent(
            name=agent_name,
            description=instructions,
            chat_client=chat_client,
            tools=list(tools),
            function_middleware=list(middleware),
        )

プロバイダーレジストリ

プロバイダーレジストリは、利用可能なすべてのプロバイダーを一元管理する仕組みです。起動時に各プロバイダーを登録することで、実行時にプロバイダー名から適切な実装を取得できます。この仕組みにより、新しいプロバイダーの追加はregister()呼び出し1行で完結します。

# coding_agent/providers/__init__.py (抜粋)

from .anthropic import AnthropicProvider
from .azure import AzureOpenAIProvider
from .base import registry
from .openai import OpenAIProvider

# 全プロバイダーを登録
registry.register(OpenAIProvider())
registry.register(AnthropicProvider())
registry.register(AzureOpenAIProvider())

この設計により、新しいプロバイダーの追加が非常に簡単になります。


ツールの実装

エージェントの能力は、提供するツールによって決まります。このセクションでは、ファイル操作、シェルコマンド実行など、コーディングエージェントに必要な主要なツールの実装を見ていきます。MAFでは@ai_functionデコレーターを使って、通常のPython関数を簡単にLLMツールに変換できます。

ファイル操作ツール

ファイルの読み書きは、コーディングエージェントの基本機能です。MAFの@ai_functionデコレーターを使うと、Python関数を直接ツールとして登録できます。ここでは、ファイルサイズ制限などの安全対策を組み込んだ実装を紹介します。

# coding_agent/tools/file.py (抜粋)

from agent_framework import ai_function
from typing import Annotated

@ai_function(
    name="read_file",
    description="Read the contents of a file with a safe length cap.",
)
def read_file(
    file_path: Annotated[str, "Path to the target file"],
) -> str:
    """ファイル内容を読み込み(20万文字で打ち切り)"""

    path = _resolve_path(file_path)
    if not path.exists() or not path.is_file():
        raise ValueError(f"File does not exist: {path}")

    text = path.read_text("utf-8", errors="ignore")

    # 安全性のため、長すぎるファイルは切り詰め
    if len(text) > 200_000:
        return text[:200_000] + "\n\n[Truncated output due to size limit]"

    return text


@ai_function(
    name="write_file",
    description="Write text content to a file, creating parents if necessary.",
)
def write_file(
    file_path: Annotated[str, "Path to create or overwrite"],
    content: Annotated[str, "Text content to persist"],
) -> str:
    """ファイルに書き込み、必要なら親ディレクトリを作成"""

    path = _resolve_path(file_path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")

    return f"Successfully wrote {path} (size={len(content)} characters)"

ポイント:

  • Annotatedで型と説明を同時に定義
  • @ai_functionデコレーターで自動的にツールスキーマを生成
  • ファイルサイズ制限で安全性を確保

コマンド実行ツール

コーディングエージェントにシェルコマンド実行能力を与えることで、テストの実行やビルドプロセスの実行など、開発ワークフローを自動化できます。しかし、この機能は危険を伴うため、慎重な安全対策が必要です。ここでは、危険なコマンドのブラックリスト、タイムアウト機能、作業ディレクトリの検証を実装しています。

# coding_agent/tools/command.py (抜粋)

import asyncio
from agent_framework import ai_function

# 危険なパターンのブラックリスト
_DANGEROUS_PATTERNS = [
    "rm -rf /",
    "mkfs",
    "dd if=",
    ":(){",  # fork bomb
    "sudo rm",
    "chmod -R 777 /",
    "shutdown",
]


@ai_function(
    name="execute_command",
    description="Execute a shell command with guardrails and capture output.",
)
async def execute_command(
    command: Annotated[str, "Shell command to execute"],
    working_directory: Annotated[str | None, "Directory the command should run in"] = None,
    timeout_seconds: Annotated[int, "Maximum time before termination"] = 30,
) -> str:
    """シェルコマンドを実行し、出力をキャプチャ"""

    # 危険なパターンをチェック
    lowered = command.lower()
    for pattern in _DANGEROUS_PATTERNS:
        if pattern in lowered:
            raise ValueError(
                "Command rejected because it matches a dangerous pattern"
            )

    # 作業ディレクトリの検証
    cwd = Path(working_directory).resolve() if working_directory else Path.cwd()
    if not cwd.exists() or not cwd.is_dir():
        raise ValueError(f"Working directory does not exist: {cwd}")

    # コマンドを実行
    process = await asyncio.create_subprocess_shell(
        command,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        cwd=str(cwd),
    )

    try:
        stdout, stderr = await asyncio.wait_for(
            process.communicate(),
            timeout=timeout_seconds
        )
    except asyncio.TimeoutError:
        process.kill()
        raise TimeoutError(f"Command timed out after {timeout_seconds} seconds")

    # 結果をフォーマット
    output_segments = [f"Exit code: {process.returncode}"]
    if stdout:
        output_segments.append(f"STDOUT:\n{stdout.decode('utf-8').strip()}")
    if stderr:
        output_segments.append(f"STDERR:\n{stderr.decode('utf-8').strip()}")

    return "\n\n".join(output_segments)

安全性のポイント:

  • 危険なコマンドパターンをブラックリストで拒否
  • タイムアウト機能でハングを防止
  • 作業ディレクトリの存在確認

ミドルウェアによる機能拡張

MAFのミドルウェアパターンは、ツール実行の前後に処理を挿入する強力な仕組みです。ロギング、認証、メトリクス収集、エラーハンドリングなど、横断的な関心事をツールのコアロジックから分離できます。このセクションでは、ツール実行を可視化するロギングミドルウェアの実装を見ていきます。

ツール実行ログの実装

ツールが呼び出されたときに、その引数と実行結果を可視化するミドルウェアを実装しました。Richライブラリを使って、美しくフォーマットされたログを表示することで、エージェントの動作を理解しやすくし、デバッグを容易にします。

# coding_agent/middleware.py

from agent_framework import FunctionMiddleware, FunctionInvocationContext
from rich.console import Console
from rich.panel import Panel


class ToolLoggingMiddleware(FunctionMiddleware):
    """ツール実行時のログを表示するミドルウェア"""

    def __init__(self, console: Console | None = None) -> None:
        self._console = console or Console()

    async def process(
        self,
        context: FunctionInvocationContext,
        next: Any,
    ) -> None:
        # ツール名と引数を取得
        tool_name = getattr(context.function, "name", "tool")
        arguments = getattr(context, "arguments", {})

        # 実行前ログ
        self._console.print(
            Panel.fit(
                repr(arguments),
                title=f"[yellow]Tool call[/yellow] {tool_name}",
                border_style="yellow",
            )
        )

        try:
            # ツールを実行
            await next(context)
        except Exception as exc:
            self._console.print(f"[red]Tool {tool_name} failed:[/red] {exc}")
            raise

        # 実行後ログ(結果の最初の500文字のみ)
        result = getattr(context, "result", None)
        preview = str(result)
        if len(preview) > 500:
            preview = preview[:500] + ""

        if preview.strip():
            self._console.print(
                Panel.fit(
                    preview,
                    title=f"[green]Tool result[/green] {tool_name}",
                    border_style="green",
                )
            )

このミドルウェアにより、ツール実行が可視化され、デバッグが容易になります。


設定管理

アプリケーションの設定管理は、ユーザビリティとセキュリティの両立が重要です。このセクションでは、Pydanticを使った型安全な設定管理と、ユーザーのホームディレクトリに設定ファイルを永続化する実装を紹介します。

Pydanticベースの設定

PydanticのBaseModelを使うことで、設定の型安全性を確保し、自動バリデーションとJSON シリアライゼーションを実現します。APIキー、モデル名、温度パラメータなど、プロバイダーごとの設定を一元管理し、~/.maf-coding-agent/config.jsonに永続化します。

# coding_agent/config.py (抜粋)

from pydantic import BaseModel, Field
from pathlib import Path
import json

CONFIG_DIR = Path.home() / ".maf-coding-agent"
CONFIG_FILE = CONFIG_DIR / "config.json"


class AgentConfig(BaseModel):
    """永続化された設定"""

    # Anthropic設定
    anthropic_api_key: Optional[str] = Field(default=None)
    anthropic_model: Optional[str] = Field(default=None)
    anthropic_temperature: Optional[float] = Field(default=None)

    # OpenAI設定
    openai_api_key: Optional[str] = Field(default=None)
    openai_model: Optional[str] = Field(default=None)
    openai_temperature: Optional[float] = Field(default=None)

    # Azure設定
    azure_api_key: Optional[str] = Field(default=None)
    azure_endpoint: Optional[str] = Field(default=None)
    azure_deployment: Optional[str] = Field(default=None)
    azure_api_version: Optional[str] = Field(default=None)
    azure_temperature: Optional[float] = Field(default=None)

    # デフォルトプロバイダー
    default_provider: Optional[str] = Field(default=None)

    def resolve_api_key(self, provider: str) -> Optional[str]:
        """プロバイダーのAPIキーを解決"""
        if provider == "anthropic":
            return self.anthropic_api_key
        if provider == "openai":
            return self.openai_api_key
        if provider == "azure":
            return self.azure_api_key
        return None

    def resolve_model(self, provider: str, fallback: str) -> str:
        """プロバイダーのモデルを解決"""
        if provider == "anthropic" and self.anthropic_model:
            return self.anthropic_model
        if provider == "openai" and self.openai_model:
            return self.openai_model
        if provider == "azure" and self.azure_deployment:
            return self.azure_deployment
        return fallback


def load_config() -> AgentConfig:
    """設定ファイルから読み込み"""
    if not CONFIG_FILE.exists():
        return AgentConfig()

    try:
        raw = json.loads(CONFIG_FILE.read_text("utf-8"))
        return AgentConfig(**raw)
    except json.JSONDecodeError:
        # 壊れたファイルをバックアップして新規作成
        backup_path = CONFIG_FILE.with_suffix(".bak")
        CONFIG_FILE.rename(backup_path)
        return AgentConfig()


def save_config(config: AgentConfig) -> None:
    """設定ファイルに保存"""
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_FILE.write_text(
        json.dumps(
            config.model_dump(exclude_none=True),
            indent=2,
            ensure_ascii=False
        ),
        encoding="utf-8",
    )

Pydanticを使う利点:

  • 型安全性
  • 自動バリデーション
  • JSON シリアライゼーション

CLIの実装

優れたCLIツールは、直感的なコマンド体系と親切なヘルプメッセージを持っています。このセクションでは、Typerを使った使いやすいコマンドラインインターフェースの実装を見ていきます。対話型チャット、ワンショット実行、設定管理など、実用的な機能を提供します。

Typerによるコマンド定義

Typerは、Pythonの型ヒントを活用してCLIを構築できるモダンなフレームワークです。関数の引数とデコレーターだけで、自動的にコマンドラインパーサーとヘルプメッセージが生成されます。ここでは、chatrunconfigmodelsの4つのコマンドを実装しています。

# coding_agent/cli.py (抜粋)

import typer
from rich.console import Console
from rich.prompt import Prompt

app = typer.Typer(
    add_completion=False,
    help="Microsoft Agent Framework coding agent CLI"
)
console = Console()


@app.command()
def chat(
    message: Optional[str] = typer.Argument(None, help="Optional one-shot message"),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="Provider key"),
    model: Optional[str] = typer.Option(None, "--model", "-m", help="Model identifier"),
    temperature: Optional[float] = typer.Option(None, "--temperature", help="Sampling temperature"),
) -> None:
    """対話型チャットセッションを開始"""

    config = load_config()
    provider_name = _select_provider(config, provider)
    runtime = _create_runtime(provider_name, config=config, model=model, temperature=temperature)

    # ワンショット実行
    if message:
        async def oneshot() -> None:
            reply = await runtime.run(message)
            if reply:
                console.print(f"[assistant]{reply}[/assistant]")
        asyncio.run(oneshot())
        return

    # インタラクティブモード
    asyncio.run(_interactive_chat(runtime))


@app.command()
def run(
    message: str = typer.Argument(..., help="Single message to run"),
    provider: Optional[str] = typer.Option(None, "--provider", "-p"),
    model: Optional[str] = typer.Option(None, "--model", "-m"),
    temperature: Optional[float] = typer.Option(None, "--temperature"),
) -> None:
    """単一リクエストを実行"""

    config = load_config()
    provider_name = _select_provider(config, provider)
    runtime = _create_runtime(provider_name, config=config, model=model, temperature=temperature)

    async def oneshot() -> None:
        reply = await runtime.run(message)
        if reply:
            console.print(f"[assistant]{reply}[/assistant]")

    asyncio.run(oneshot())


@app.command()
def config(
    provider: Optional[str] = typer.Option(None, "--provider", "-p"),
    default_provider: Optional[str] = typer.Option(None, "--set-default"),
) -> None:
    """APIキーとデフォルト設定を対話的に登録"""

    cfg = load_config()

    # プロバイダーごとに設定を入力
    targets = [provider] if provider else ["anthropic", "openai", "azure"]
    for target in targets:
        if target == "anthropic":
            key = typer.prompt(
                "Anthropic API key",
                hide_input=True
            )
            cfg.anthropic_api_key = key.strip() or None

            model = typer.prompt(
                "Anthropic default model",
                default="claude-3-5-sonnet-20241022",
            )
            cfg.anthropic_model = model.strip()
        # OpenAI、Azureも同様...

    save_config(cfg)
    console.print("[info]Configuration saved.[/info]")


@app.command()
def models(
    provider: Optional[str] = typer.Option(None, "--provider", "-p")
) -> None:
    """利用可能なモデル一覧を表示"""

    config = load_config()
    provider_name = _select_provider(config, provider)
    available = list_available_models(provider_name)

    console.print(f"[info]Models for {provider_name}:[/info]")
    for model_name in available:
        console.print(f"  - {model_name}")

対話型チャットの実装

対話型チャットは、エージェントとの継続的な会話を可能にする機能です。/resetで会話履歴をクリア、/exitで終了など、特殊コマンドもサポートします。エラーハンドリングを適切に行い、ユーザーに親切なフィードバックを提供することで、安定した使用体験を実現しています。

# coding_agent/cli.py (抜粋)

async def _interactive_chat(runtime: AgentRuntime) -> None:
    """インタラクティブチャットループ"""

    console.print(
        "[info]Interactive mode. Type /reset to clear history, /exit to quit.[/info]"
    )

    while True:
        try:
            message = Prompt.ask("[user]You[/user]")
        except (EOFError, KeyboardInterrupt):
            console.print("\n[info]Goodbye![/info]")
            return

        if not message.strip():
            continue

        # 特殊コマンド
        if message.strip().lower() in {"/exit", "exit", ":q"}:
            console.print("[info]Session ended.[/info]")
            return

        if message.strip().lower() in {"/reset", "reset"}:
            runtime.reset()
            console.print("[info]Conversation reset.[/info]")
            continue

        # エージェント実行
        try:
            reply = await runtime.run(message)
        except Exception as exc:
            console.print(f"[error]Execution failed:[/error] {exc}")
            continue

        if reply:
            console.print(f"[assistant]{reply}[/assistant]")
        else:
            console.print("[warning]No textual response produced.[/warning]")

セットアップと使い方

ここからは、実際にこのコーディングエージェントをセットアップして使用する方法を説明します。GitHubからクローンしてインストールし、設定を行い、実際に使ってみるまでの流れを、ステップバイステップで解説します。

インストール

まず、リポジトリをクローンして必要なパッケージをインストールします。Python 3.10以上が必要です。仮想環境を使うことで、システムのPython環境を汚さずにインストールできます。

# リポジトリをクローン
git clone https://github.com/nogataka/MAF-sample.git
cd MAF-sample

# 仮想環境を作成(推奨)
python -m venv .venv
source .venv/bin/activate  # Windowsの場合: .venv\Scripts\activate

# 依存パッケージをインストール
pip install --upgrade pip
pip install -e .

設定ウィザード

インストールが完了したら、使用するLLMプロバイダーのAPIキーを設定します。対話型の設定ウィザードを使えば、質問に答えるだけで簡単に設定できます。

# 対話的に設定を登録
maf-coding-agent config

実行すると以下のように聞かれます:

Anthropic API key: ****************
Anthropic default model [claude-3-5-sonnet-20241022]:
Anthropic default temperature (空で標準値): 0.7

OpenAI API key: ****************
OpenAI default model [gpt-4o]:
OpenAI default temperature (空で標準値):

Azure OpenAI API key (Enterで環境変数を利用):
Azure OpenAI endpoint (https://<resource>.openai.azure.com): https://my-resource.openai.azure.com
Azure OpenAI deployment name: gpt-4o-deployment
Azure OpenAI API version [2024-05-01-preview]:
Azure OpenAI default temperature (空で標準値):

Default provider [anthropic/openai/azure/ask] (ask): anthropic

Configuration saved.

設定は~/.maf-coding-agent/config.jsonに保存されます。

使い方の例

設定が完了したら、様々な方法でエージェントを使用できます。対話型チャット、ワンショット実行、モデル一覧の確認など、用途に応じて使い分けましょう。

対話型チャット

対話型チャットモードでは、エージェントと継続的に会話できます。会話履歴が保持されるため、前の会話内容を踏まえた応答が得られます。

# デフォルトプロバイダーで起動
maf-coding-agent chat

# プロバイダーを指定
maf-coding-agent chat --provider anthropic

# モデルと温度を指定
maf-coding-agent chat --provider openai --model gpt-5-mini --temperature 1

実行例:

Interactive mode. Type /reset to clear history, /exit to quit.
You: src ディレクトリにある Python ファイルを全て列挙して

╭────────── Tool call list_files ──────────╮
│ {'directory': 'src', 'recursive': True} │
╰──────────────────────────────────────────╯

╭───────── Tool result list_files ─────────╮
│ Listing `src` (recursive=True):          │
│ [FILE] src/main.py                       │
│ [FILE] src/utils.py                      │
│ [DIR]  src/models                        │
│ [FILE] src/models/__init__.py            │
│ [FILE] src/models/user.py                │
╰──────────────────────────────────────────╯

Assistant: src ディレクトリに以下の Python ファイルが見つかりました:
- `src/main.py`
- `src/utils.py`
- `src/models/__init__.py`
- `src/models/user.py`

ワンショット実行

単発のタスクにはrunコマンドが便利です。スクリプトから呼び出すこともできるため、自動化にも活用できます。

# 単発のコマンド実行
maf-coding-agent run "現在のディレクトリのファイル一覧を見せて"

# 異なるプロバイダーで実行
maf-coding-agent run "requirements.txt に書かれているパッケージを説明して" --provider openai

モデル一覧の確認

使用可能なモデルを確認できます。プロバイダーごとに異なるモデルが利用できるため、用途に応じて選択できます。

# デフォルトプロバイダーのモデル一覧
maf-coding-agent models

# 特定のプロバイダーのモデル一覧
maf-coding-agent models --provider azure

出力例:

Models for anthropic:
  - claude-3-5-sonnet-20241022
  - claude-3-5-haiku-20241022
  - claude-3-opus-20240229

実際の使用例

ここでは、実際の使用シーンを想定した具体例を見ていきます。ツールがどのように呼び出され、どのような結果が返されるかを理解することで、エージェントの動作イメージを掴めるでしょう。

ファイルの読み書き

最も基本的な操作として、ファイルの読み込みと要約を依頼してみます。エージェントはファイルを読み込むためにread_fileツールを自動的に呼び出します。

You: README.md を読んで要約して

╭─── Tool call read_file ───╮
│ {'file_path': 'README.md'}│
╰───────────────────────────╯

╭─── Tool result read_file ──╮
│ # MAF-sample               │
│ AI coding agent CLI...     │
│ (ファイル内容)              │
╰────────────────────────────╯

Assistant: このプロジェクトは、Microsoft Agent Frameworkを使用したAIコーディングエージェントのCLIツールです。
主な機能として、ファイル操作、コマンド実行、TODO管理などを備えています。

コマンド実行

次に、テストの実行を依頼してみます。エージェントはexecute_commandツールを使ってpytestを実行し、結果を報告してくれます。

You: このディレクトリで pytest を実行して結果を見せて

╭──── Tool call execute_command ────╮
│ {'command': 'pytest',             │
│  'working_directory': '.'}       │
╰───────────────────────────────────╯

╭─── Tool result execute_command ───╮
│ Exit code: 0                      │
│ STDOUT:                           │
│ =================== test session =│
│ collected 15 items                │
│ ...                               │
│ =================== 15 passed ====│
╰───────────────────────────────────╯

Assistant: pytestを実行しました。全15個のテストが成功しています。

プロバイダーの切り替え

マルチプロバイダー対応の利点は、簡単にLLMを切り替えられることです。タスクに応じて最適なモデルを選択できます。

You: /exit

$ maf-coding-agent chat --provider openai --model gpt-5-mini --temperature 1

You: Pythonでフィボナッチ数列を生成する関数を書いて、fib.py に保存して

╭───── Tool call write_file ─────╮
│ {'file_path': 'fib.py',       │
│  'content': 'def fib...'}     │
╰────────────────────────────────╯

╭──── Tool result write_file ────╮
│ Successfully wrote fib.py      │
│ (size=245 characters)         │
╰────────────────────────────────╯

Assistant: フィボナッチ数列を生成する関数を fib.py に保存しました。

MAFを使うメリット

今回のコーディングエージェント開発を通じて、Microsoft Agent Frameworkの以下のメリットを実感しました。他のフレームワークと比較しても、MAFの開発体験は優れています。それぞれのメリットを具体的に見ていきましょう。

1. 驚くほど簡単なツール実装

@ai_functionデコレーターを使うだけで、Python関数がLLMのツールになります。

@ai_function(name="my_tool", description="...")
def my_tool(param: Annotated[str, "説明"]) -> str:
    return "結果"

これだけで、エージェントが自動的にツールを呼び出せるようになります。

2. マルチプロバイダー対応が標準

MAFは最初からOpenAI、Azure、Anthropicなど複数のプロバイダーをサポートしており、切り替えも簡単です。

3. ミドルウェアパターンによる拡張性

FunctionMiddlewareを継承するだけで、ツール実行の前後に独自の処理を挿入できます。
ログ記録、認証、メトリクス収集など、様々な用途に対応できます。

4. 型安全性とPythonらしさ

Pydanticやdataclassを活用した型安全な実装が可能で、IDEの補完も効きます。
Pythonicな書き方ができるため、学習コストが低いです。

5. 会話履歴の自動管理

AgentThreadが会話履歴を自動的に管理してくれるため、開発者は本質的なロジックに集中できます。


トラブルシューティング

実際に使用する中で遭遇する可能性のある問題と、その解決方法をまとめました。エラーメッセージから原因を特定し、適切に対処しましょう。

エラー: "No API key found"

原因: 選択したプロバイダーのAPIキーが設定ファイルにも環境変数にも見つかりません。

解決策:

# 設定ウィザードを実行
maf-coding-agent config

# または環境変数で設定
export ANTHROPIC_API_KEY="your-key-here"
export OPENAI_API_KEY="your-key-here"

エラー: "Command rejected because it matches a dangerous pattern"

原因: 実行しようとしたコマンドが、危険なコマンドパターンのブラックリストに一致しました。rm -rf /sudo rmshutdownなどの破壊的なコマンドは、安全性のため自動的にブロックされます。

解決策: コマンドを見直すか、必要に応じてcommand.py_DANGEROUS_PATTERNSを調整してください。

ツールが呼ばれない

原因: エージェントがツールを使う必要性を認識できていない可能性があります。システムプロンプトが不明瞭、ツールのdescriptionが曖昧、またはモデルの温度設定が高すぎて確定的な動作をしていない可能性があります。

解決策:

  1. ツールのdescriptionをより具体的に記述
  2. システムプロンプトでツールの使用を促す
  3. temperatureを調整して動作を制御する(--temperature 0.5など)

さらなる拡張のアイデア

このエージェントは基礎的な実装ですが、MAFの柔軟性により様々な拡張が可能です。以下では、実際のプロジェクトで役立つ拡張アイデアをコード例とともに紹介します。これらの拡張を追加することで、より強力な開発支援ツールに成長させることができます。

1. Git操作ツール

バージョン管理を自動化するGit操作ツールを追加すると、エージェントがコミット、プッシュ、ブランチ作成などを自動で行えるようになります。

@ai_function(name="git_commit", description="Stage and commit changes")
def git_commit(message: Annotated[str, "Commit message"]) -> str:
    subprocess.run(["git", "add", "."], check=True)
    result = subprocess.run(
        ["git", "commit", "-m", message],
        capture_output=True,
        text=True
    )
    return result.stdout

2. Webサーチツール

最新の情報や技術ドキュメントを検索する機能を追加すると、エージェントの知識を拡張できます。Serp APIやGoogle Custom Searchなどの検索APIと連携します。

@ai_function(name="web_search", description="Search the web")
async def web_search(query: Annotated[str, "Search query"]) -> str:
    # Serp API、Google Custom Search などを使用
    results = await search_api.query(query)
    return format_results(results)

3. データベース操作

SQLiteなどのデータベースにクエリを実行する機能を追加すると、データ分析やレポート生成などが可能になります。

@ai_function(name="query_db", description="Query SQLite database")
def query_db(sql: Annotated[str, "SQL query"]) -> str:
    conn = sqlite3.connect("app.db")
    cursor = conn.cursor()
    cursor.execute(sql)
    results = cursor.fetchall()
    return format_table(results)

4. ワークフロー機能

MAFのWorkflowビルダーを使うと、複数のエージェントを協調させて、より複雑なタスクを実行できます。計画立案→実装→レビュー→テストのように、専門化されたエージェントを順次実行することで、品質の高い成果物を得られます。

from agent_framework.workflows import SequentialWorkflow

workflow = SequentialWorkflow()
    .add_agent(planner_agent)      # 計画立案
    .add_agent(coder_agent)        # コード生成
    .add_agent(reviewer_agent)     # レビュー
    .add_agent(test_agent)         # テスト実行

まとめ

Microsoft Agent Frameworkを使えば、わずか数百行のコードで実用的なAIコーディングエージェントを構築できることがわかりました。

この記事のポイント:

簡潔な実装: @ai_functionデコレーターだけでツールを作成

マルチプロバイダー: OpenAI、Anthropic、Azureを統一的に扱える

拡張性: ミドルウェアパターンで機能を柔軟に追加

型安全性: Pydanticで設定管理、型ヒントでIDE補完

実用性: ファイル操作、コマンド実行、TODO管理など実際に使える機能

次のステップ:

Microsoft Agent Frameworkは、AIエージェント開発の新しいスタンダードになる可能性を秘めています。
ぜひ、皆さんも試してみてください!


参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?