AIエージェント・MCP

AIエージェントの作り方:ツール定義からループ・承認・トレースまでコードで組む

AIエージェントの作り方:ツール定義からループ・承認・トレースまでコードで組む

AIエージェントの作り方を検索すると、仕組みの図解とフレームワークの一覧が並びます。読み終えてもターミナルで何を打てばよいのかは分からないままです。載っているのは構成要素の名前であって、手元で動かした手順ではないからです。

この記事は、実際に動くエージェントを1本組み上げるまでの作業を順に並べます。ツールをJSON Schemaで定義し、モデルの応答からツール呼び出しを取り出して実行し、結果を戻してループを回す。そのうえで許可リストと人の承認、状態の保存、外部ツールのMCP接続、トレースと評価を足していきます。ノーコード基盤で済ませるか自社開発するか外注するかというルート選びはAIエージェントの作り方とは?ノーコード・開発・外注の3ルートと設計手順で扱っているため、本記事は自分で書く場合の実装に絞ります。

まとめ:AIエージェントの作り方は4部品と止める条件で決まる

  • エージェントの実体は、ツール定義・モデル呼び出し・結果の差し戻し・終了条件の4部品を回すループである
  • ツールはJSON Schemaで書き、strict指定で型を守らせる。渡す本数はターン開始時に20本未満を目安にする
  • 最初の1本は素のAPIで書く。フレームワークは状態保存と人の承認が必要になった段階で載せ替える
  • 副作用のある操作は許可リストと承認フラグの2段で守り、失敗はis_errorで戻して自己修復させる
  • MCPの現行版は2026-07-28で版宣言の方式が変わっているため、接続前に対応版を確認する
  • トレースと評価データを最初から残す。残していないと精度が落ちた原因を後から特定できない

エージェントを作り始める前に決める仕事の範囲とツールの権限と終了条件

任せる仕事を1つの業務に絞るための入力形式と完了条件の具体的な書き出し

最初に決めるのは、エージェントに任せる仕事の範囲です。「問い合わせ対応」では広すぎて終了条件が書けません。「商品コードを含むメールを受け取り、在庫数と入荷予定日を返信文の下書きにする」まで絞ると、入力と出力の形が確定します。入力は本文テキスト、出力は下書き文字列と参照した商品コードの配列、という具合です。

この段階で、完了とみなす条件も文章にしておきます。在庫が引けたら完了、商品コードが本文から取れなければ人へ回す、という分岐です。ここが曖昧なまま実装に入ると、モデルが延々と同じツールを呼び続けます。エージェントの失敗の多くは推論性能ではなく、終わり方を決めていないことに起因します。

読み取りと書き込みのツールを分けて承認対象を決めるための権限設計

次に、渡すツールを列挙します。先の例なら在庫照会と発注の2本です。ここで読み取り専用のツールと、外部に副作用を出すツールを明確に分けて名前を付けます。分けておくと、後から承認を挟む対象が機械的に決まります。

本数は増やしすぎないほうが動きます。OpenAIのFunction callingのガイドには、ターン開始時に見せる関数は20本未満を目安にする旨の記載があります。加えてツール定義そのものも入力トークンの消費対象です。AnthropicのTool use with Claudeの料金表では、ツールを1本でも渡した時点で入るシステムプロンプト分がClaude Opus 5でtool_choice auto時に286トークン、any指定時に406トークンと明記されています。ツールを並べるほど毎ターンの固定費が積み上がる構造です。

ツール定義をJSON Schemaで書いてモデルが呼べる形に整える

descriptionは使う場面を書く。型の逸脱はstrictで止める

ツール定義は名前・説明・入力スキーマの3点です。説明文には機能ではなく「どんなときに使うか」を書きます。「在庫を取得する」より「在庫の問い合わせにのみ使う」と書いたほうが、関係ない場面での呼び出しが減ります。

型の逸脱は仕組みで止めます。OpenAI側のstrictモードは、各オブジェクトに additionalProperties を false で置き、全プロパティを required に列挙することを求めます。任意項目は必須のままにして型に null を足す書き方です。Anthropic側にもStrict tool useがあり、カスタムツールに strict: true を足すとスキーマどおりの呼び出しが保証されます。出力形式そのものを固めたい場合の考え方はLLM構造化出力とは?JSON Schemaで出力形式を保証する仕組みにまとめています。

仮想環境の準備とツール定義・ディスパッチを1ファイルに置く実装手順

ここから手を動かします。仮想環境を作り、SDKを入れて鍵を通します。

python -m venv .venv
. .venv/bin/activate

pip install anthropic langgraph

export ANTHROPIC_API_KEY="sk-ant-..."

ツール定義は独立したファイルに置きます。後で本数を絞ったり差し替えたりする作業が、この1ファイルの編集で済みます。

# tools.py — 在庫照会と発注の2本だけを渡す
TOOLS = [
    {
        "name": "lookup_stock",
        "description": "商品コードから現在庫数と入荷予定日を返す。在庫の問い合わせにのみ使う。",
        "input_schema": {
            "type": "object",
            "properties": {
                "item_code": {"type": "string", "description": "商品コード。例 A-10293"}
            },
            "required": ["item_code"],
            "additionalProperties": False,
        },
        "strict": True,
    },
    {
        "name": "place_order",
        "description": "商品コードと数量で発注を確定する。在庫不足が確認できた場合にのみ使う。",
        "input_schema": {
            "type": "object",
            "properties": {
                "item_code": {"type": "string"},
                "qty": {"type": "integer", "description": "発注数。1以上"},
            },
            "required": ["item_code", "qty"],
            "additionalProperties": False,
        },
        "strict": True,
    },
]

副作用のあるツール操作を実行側で制限する許可リストと承認フラグの設計

モデルが返した名前をそのまま関数に渡すと、想定外の呼び出しが通ります。実行側に許可リストを置き、副作用のある操作には承認フラグを求める設計です。この10行が、後から権限設計を作り直す手間を消します。

# tools.py の続き — 許可リストと承認の2段で守る
ALLOWED = {"lookup_stock", "place_order"}
CONFIRM_REQUIRED = {"place_order"}

def dispatch(name, args, approved=False):
    if name not in ALLOWED:
        raise ValueError(f"未登録のツール: {name}")
    if name in CONFIRM_REQUIRED and not approved:
        raise PermissionError("人の承認が必要な操作です")
    if name == "lookup_stock":
        return stock_api.get(args["item_code"], timeout=5)
    return order_api.create(args["item_code"], args["qty"], timeout=5)

エージェントループをPythonで書いて実際に動かすところまで

tool_useとtool_resultを往復させる形が最小のエージェントである

ここが本体です。モデルに投げると、応答は stop_reason が tool_use になり、呼びたいツールと引数が tool_use ブロックで返ります。こちらで実行し、結果を tool_result ブロックにして会話に足し、もう一度投げる。この往復がAnthropicのドキュメントに載っている流れであり、エージェントと呼ばれているものの実体です。

下のコードは、その往復に上限手数を付けただけのものです。60行ほどでフレームワークなしに動きます。仕組みの背景はエージェントループとは?自律的に動く仕組み・ReActと終了条件の設計で扱っています。

# agent.py — tool_use と tool_result を往復させる最小ループ
import anthropic
from tools import TOOLS, dispatch

client = anthropic.Anthropic()
MAX_STEPS = 8

def run(task):
    messages = [{"role": "user", "content": task}]
    for step in range(MAX_STEPS):
        res = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=2048,
            tools=TOOLS,
            tool_choice={"type": "auto", "disable_parallel_tool_use": True},
            messages=messages,
        )
        if res.stop_reason != "tool_use":
            return "".join(b.text for b in res.content if b.type == "text")

        messages.append({"role": "assistant", "content": res.content})
        results = []
        for block in res.content:
            if block.type != "tool_use":
                continue
            try:
                out = dispatch(block.name, block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(out),
                })
            except Exception as err:
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": f"error: {err}",
                    "is_error": True,
                })
        messages.append({"role": "user", "content": results})

    raise RuntimeError(f"{MAX_STEPS}手で終わらなかった")

if __name__ == "__main__":
    print(run("A-10293の在庫と入荷予定を教えてほしい、という問い合わせに答えて"))

ループの暴走とツール障害時の沈黙を防ぐ手数上限と例外の戻し方の設計

上のコードで効いているのは2か所です。MAX_STEPS はツール呼び出しの上限で、これがないと同じ照会を繰り返したまま課金が伸びます。実務での開始時の目安は8手前後で、軌跡を見て足りない場合だけ増やす方針です。上限に当たったら例外で落とし、人へ回す扱いにします。

もう1か所は例外処理です。ツールが落ちたときにプロセスを止めず、is_error を立てた tool_result で戻します。モデルは引数を直して呼び直せる状態になり、商品コードの桁違いのような入力ミスはその場で自己修復が可能です。思考と行動を交互に置く形の判断基準はReActパターンとは?思考と行動を交互に回す仕組みと採用判断で整理しています。

フレームワークへの載せ替えで実装する状態の保存と人の承認の仕組み

処理の中断と再開が必要になった時点でのフレームワークへの載せ替え判断

素のループで足りなくなる境目は決まっています。処理の途中で人の承認を待ちたい、プロセスが落ちても続きから再開したい、数十分かかる仕事を扱いたい、のどれかが出た時点です。ここを自作すると、実質的にジョブキューとチェックポイントを書くことになります。

LangGraphの公式ドキュメントは、中核の機能として persistence・durable execution・human-in-the-loop・comprehensive memory を挙げています。障害を越えて長時間動き続けること、任意の時点で状態を人が確認して書き換えられることが売りです。OpenAI Agents SDKは Agents・Tools・Handoffs・Guardrails・Sessions・Tracing というプリミティブを持ち、会話履歴の管理と入出力の検査が最初から付いています。型でツールと出力を縛りたい場合はPydantic AIも選択肢の1つです。枠ごとの比較はAIエージェントフレームワークとは?主要8種の比較と受託開発視点の選定基準にまとめています。

人の承認待ちで処理を止めて再開するためのグラフ側の機能と状態保存

LangGraphで承認を挟む場合、止める処理はノードの中に書きます。interrupt を呼ぶとその時点の状態が保存され、実行は戻り値を待つ停止状態です。承認の入力が来たら、同じスレッドIDで再開すると続きから走ります。

# graph.py — 状態を保存して発注前に人の承認で止める
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.types import interrupt

def plan(state):
    # agent.py の run と同じ要領でモデルを呼び、発注の引数を state に置く
    return {"pending": {"item_code": "A-10293", "qty": 20}}

def confirm(state):
    decision = interrupt({"ask": "この発注を実行しますか", "args": state["pending"]})
    return {"approved": decision == "yes"}

def execute(state):
    if not state.get("approved"):
        return {"result": "承認されなかったため発注を取り消した"}
    args = state["pending"]
    return {"result": dispatch("place_order", args, approved=True)}

builder = StateGraph(MessagesState)
builder.add_node(plan)
builder.add_node(confirm)
builder.add_node(execute)
builder.add_edge(START, "plan")
builder.add_edge("plan", "confirm")
builder.add_edge("confirm", "execute")
builder.add_edge("execute", END)

with SqliteSaver.from_conn_string("state.sqlite") as saver:
    graph = builder.compile(checkpointer=saver)
    config = {"configurable": {"thread_id": "order-A-10293"}}
    print(graph.invoke({"messages": []}, config))

チェックポイントをSQLiteに落としているので、承認を待つ間にプロセスを終了させても状態は残ります。会話履歴をどこまで抱えるか、どこから外部ストアに逃がすかの設計はエージェントメモリとは?短期記憶と長期記憶の設計・実装判断で扱っている設計項目です。LangGraph固有の書き方はLangGraphでAIエージェントを実装する手順:状態管理から承認フローまでに手順化しています。

MCPで既存ツールをつなぎ2026年7月版の仕様変更に備える

自作ラッパーを増やす前に確認する接続先製品のMCPサーバーの有無

社内のデータベースやチケット管理を触らせたい段階になると、ツールを1つずつ書くのが重くなります。ここで接続の手間を抑える仕組みがMCPです。既にMCPサーバーが公開されている製品なら、こちら側はクライアントとして接続するだけでツール一覧が取れます。接続の手順とtool定義・権限の設計はAIエージェントにMCPで外部ツールを接続する実装手順にあります。

ツールが数百本に増える場面では、全部を毎ターン見せる形が破綻します。Anthropic側にはツールを必要時に読み込む仕組みが用意されており、定義の固定費を抑えられる仕組みです。前述のとおりツール定義はトークンを食うため、本数が増える前提の設計では効いてきます。

接続前に対応プロトコル版を確かめる。現行は2026-07-28である

MCPで実装が滑る箇所は版の扱いです。Versioningのページによると、版はYYYY-MM-DD形式で、後方互換を保つ変更では上がりません。現行版は 2026-07-28 です。各リクエストは _meta の io.modelcontextprotocol/protocolVersion で版を宣言し、Streamable HTTP では MCP-Protocol-Version ヘッダにも同じ値を載せます。サーバーが未対応なら UnsupportedProtocolVersionError が対応版の一覧付きで返るため、こちらは版を落として再送できます。

2025-11-25以前の版は initialize によるハンドシェイクで版を決めていました。2026-07-28版の仕様では server/discover が必須RPCとして用意され、対応版・機能・識別情報を1リクエストで取れます。古い記事のコードをそのまま持ってくると、この差でつながりません。接続前に相手方の対応版を確認する手順を組み込んでおきます。

# MCP 2026-07-28 の Streamable HTTP へ版を宣言して問い合わせる
curl -s https://mcp.example.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "server/discover",
        "params": {
          "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}
        }
      }'

本番で落ちないエージェントに向けたトレースと評価とコスト計測の実装

精度低下の原因を特定するためのツール呼び出し回数と実行軌跡の記録

エージェントは、同じ入力でも通る経路が変わります。精度が落ちたという申告を受けたとき、手元に残っているのが最終出力だけだと原因に届きません。1回の実行につき、呼んだツール名・引数・戻り値・所要時間・トークン数を1行1レコードで残します。

属性名は独自に決めず、OpenTelemetryのGenAI向け規約に寄せます。この規約は本体リポジトリから分離され、semantic-conventions-genaiという専用リポジトリで管理されています。以前のopentelemetry.io配下のページは移動告知に置き換わっているため、参照先の差し替えが必要です。スパン木の切り方と記録範囲はエージェントトレーシングとは?スパン木の設計・gen_ai属性・記録範囲で具体化しています。

勘による修正を避けるための評価データ20件の準備と変更前後の比較

プロンプトやツール説明の書き換えは、直した箇所以外の挙動も動かします。入力と期待結果の組を20件そろえ、変更のたびに流す運用です。20件で差が付かない場合は50件に増やしますが、件数より境界の案件と過去に失敗した案件を混ぜるほうが判別力が上がります。

測る軸は最終出力の正しさだけではありません。ツール呼び出しの回数、余計な照会の有無、承認に回った件数も並べます。軌跡そのものを採点する考え方はAIエージェント評価とは?軌跡・ツール呼び出し・タスク達成を測る指標にあります。コストは公表単価から見積もらず、実測したトークン数に単価を掛けて月額に直す計算です。ツール定義の固定費が毎ターン乗るため、見積もりと実額は2倍近く開くことがあります。

エージェントを自分で組むか既製の枠に載せるかを分ける採用条件と見送り場面

最初の1本は素のAPIで書く。枠から入ると詰まる場所が見えない

ここは言い切ります。1本目は素のAPIでループを書くべきです。理由は学習ではなく、詰まる場所の特定にあります。フレームワークから入ると、ツールが呼ばれない原因が説明文なのか、スキーマなのか、枠側の変換なのかを切り分けられません。前掲の60行を一度通しておくと、以降のどの枠を触っても内部で何が起きているかが読めます。

枠へ載せ替える条件は3つに絞れます。人の承認を挟んで中断と再開が必要になった場合、複数のエージェントに仕事を振り分ける構成になった場合、トレースと評価を自作せず既製品に寄せたい場合です。どれも該当しないうちは、素のループのままで運用したほうが障害時に速く直せます。分業構成の判断はマルチエージェントとは?仕組み・構成パターンと採用判断で扱っています。

見送る場面は手順が確定している業務と説明責任が重い業務である

作らないほうがよい場面もあります。手順が完全に確定している業務です。条件分岐が固定なら、ワークフローとして書いたほうが速く安く、失敗もしません。エージェントが効くのは、経路が入力ごとに変わり、事前に分岐を列挙できない仕事だけです。

もう1つは、判断の根拠を毎回説明しなければならない業務です。同じ入力で経路が変わる性質は、監査に耐える説明と相性がよくありません。この場合は、判断はルールで行い、エージェントは情報収集と下書きに留めます。どの範囲を任せるか、評価データの設計から実装までを外部に預けたい場合は、生成AI開発・AI受託開発で自社データを使った検証から請けています。

よくある質問

フレームワークを使わずに書いても実務で通用しますか?

単一のエージェントで、途中の中断も分業も不要なら通用します。本記事の60行がそのまま運用に乗る規模です。逆に、承認待ちで数時間止める、途中でプロセスが落ちても再開する、といった要件が出たら自作は勧めません。永続化と再開は自前で作ると障害の巣になります。

ツールは何本まで渡してよいものですか?

OpenAIのガイドはターン開始時に20本未満を目安としています。理由は2つあり、選択肢が増えると誤った呼び出しが増えること、定義そのものが毎ターン入力トークンを消費することです。20本を超えるなら、役割ごとにエージェントを分けるか、必要時に読み込む仕組みへ切り替えます。

同じ質問でも毎回違う経路を通るのは不具合ですか?

仕様どおりの挙動です。到達すべき結果が同じなら、経路の揺れ自体は問題になりません。困るのは経路が長くなって課金と待ち時間が伸びる場合で、そこは手数上限とツール本数で抑えます。経路を固定したいなら、その業務はワークフローとして書くほうが適します。

モデルはどれを選べばよいですか?

実装中は安いモデルで回し、評価データを流す段で上位モデルと突き合わせます。ツール選択の判断は差が出やすく、必須引数が欠けたときに聞き返すか勝手に埋めるかも比較対象です。Anthropicのドキュメントには、引数が足りない場面でOpusのほうが不足を認識しやすい旨の記載があります。

作ったエージェントの運用にどれくらい手間がかかりますか?

公開後の手間の大半はツール側の変更対応です。接続先APIの仕様が変わればスキーマと説明文を直し、評価データを流して回帰を見ます。月に数時間が目安ですが、評価データとトレースを残していない場合にかかる手間はこの数倍です。最初に仕込むかどうかで運用費が変わります。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

ほか 2 件の記事からもリンクされています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.23 テックブログ Renta!不正アクセスでサービス停止:再開未定の現状と運営側の停止判断・証拠保全・段階的再開
  2. 2025.01.09 コラム アドレサブルCM(Addressable CM)とは?フジテレビで出る表示の意味・仕組みと視聴データの扱い
  3. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  4. 2025.12.10 コラム 今の50代は何世代?バブル世代・団塊ジュニア・氷河期の境目と特徴
  5. 2026.04.13 コラム ゴルフ賞金の税金はいくら?1,000万円で源泉194万円・賞金額別の手取りと副賞の車、海外賞金【令和8年分】

RELATED POSTS 関連記事

目次