LLM アプリ開発 #10 AI エージェントを作る

読了 4分

第6回で、Claude がツールを一度呼び、私たちが結果を返す流れを見ました。エージェントはこれを一段引き上げたものです。Claude が目標を受け取り、自分でどのツールをどの順で使うかを判断し、複数の段階を踏んで仕事を最後まで処理します。今回はそのエージェントを作ります。

エージェントとは #

これまでは私たちが流れを決めていました。「検索して、その結果で答えよ」のように、順序がコードに埋め込まれていました。エージェントは違います。私たちはツールと目標だけを与え、何を先にするか、次に何をするかは Claude が決めます。ツールを使い、結果を見て、次の行動を決め、またツールを使うことを、目標を達成するまで繰り返します。

この繰り返しをエージェントループと呼びます。じつは第6回のツール実行ループと構造が同じです。違うのは、ツールが複数で、段階が何度もあり、順序を Claude が決めるという点です。

複数のツールを与える #

エージェントにはふつう、ツールを複数与えます。たとえば文書検索と計算ツールを一緒に与えると、Claude は質問に応じて二つを自分で組み合わせます。

agent_tools.py
tools = [
    {
        "name": "search_docs",
        "description": "社内文書から質問に関連する内容を検索する。",
        "input_schema": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
    },
    {
        "name": "calculate",
        "description": "数式を計算する。例: '1200 * 0.1'",
        "input_schema": {
            "type": "object",
            "properties": {"expression": {"type": "string"}},
            "required": ["expression"],
        },
    },
]

def run_tool(name: str, args: dict) -> str:
    if name == "search_docs":
        return search_docs(args["query"])   # 第8回の検索を再利用
    if name == "calculate":
        return str(eval(args["expression"]))  # 例。実際には安全な計算機を使う
    return "不明なツールです。"

「返金手数料はいくら?」と尋ねると、Claude はまず search_docs で手数料の規定を探し、計算が必要なら calculate を続けて呼びます。この順序を私たちが組まなくても、Claude が決めます。

エージェントループ #

ループ自体は第6回と同じですが、安全のため最大の繰り返し回数を設けます。エージェントがツールを際限なく呼んで止まらない状況を防ぐためです。

agent_loop.py
import anthropic

client = anthropic.Anthropic()

def run_agent(goal: str, max_steps: int = 10) -> str:
    messages = [{"role": "user", "content": goal}]

    for _ in range(max_steps):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            return next(b.text for b in response.content if b.type == "text")

        messages.append({"role": "assistant", "content": response.content})

        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = run_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result,
                })
        messages.append({"role": "user", "content": tool_results})

    return "最大ステップ数に達しました。"

print(run_agent("返金手数料の規定を探し、5万ウォンの注文の手数料を計算してください。"))

Claude がツールを呼ぶのをやめて end_turn で答えを終えると、結果を返します。そうでなければツールを実行し、結果を入れ直してループを続けます。max_steps は安全装置です。何らかの理由でエージェントが終えられないとき、決めた回数で止まります。

エージェントが合う仕事 #

エージェントが常に正解ではありません。段階が事前にすべてわかる仕事なら、コードで順序を組むほうが速くて予測しやすいです。エージェントは、事前に順序を決めにくい仕事に合います。質問によって必要なツールと段階が変わり、途中の結果を見て次を決める必要がある場合です。

費用とリスクも合わせて見ます。エージェントは複数回呼び出すので、単一の呼び出しより高く、遅いです。また誤ったツールを呼んだり誤った判断をしたりしうるので、取り消しにくい作業(決済、削除など)には人の確認をはさむのが安全です。

よくつまずくところ #

  • 終了条件がないmax_steps のような上限がないと、エージェントが同じツールを繰り返して止まらず、費用が暴走しかねません。必ず上限を設けます。
  • 危険なツールをそのまま任せるeval のような危険な実行や、決済・削除のようなツールは、そのまま自動実行してはいけません。入力を検証し、必要なら実行前に人の承認を取ります。
  • ツールの説明が重なる — 複数ツールの説明が似ていると、Claude がどれを使うか迷います。各ツールの用途を明確に分けて書きます。

まとめ #

今回は、Claude が自分でツールを選び、複数の段階を処理するエージェントを作りました。

  • エージェントはツールと目標だけ与えれば、順序を自分で決めて目標を達成するまでツールの使用を繰り返します。
  • ループは第6回と同じですが、安全のため最大の繰り返し回数を設けます。
  • 事前に順序を決めにくい仕事に合い、高く危険になりうるので、終了条件と人の確認を設けます。

ここまではツールを私たちが直接定義してつなぎました。次回の「LLM アプリ開発 #11 MCP でツールを接続する」では、ツールを接続する標準である MCP を扱います。毎回ツールを手で書く代わりに、すでに作られたツールサーバーに Claude をつなぐ方法です。

X