Claude APIでAIエージェントを作る実装ガイド【2026】Tool Use・Tool Runner・MCPをPython/TypeScriptで
社内ヘルプデスク、問い合わせ一次対応、業務システムの操作アシスタント——「自社のアプリの中で、AIに考えさせて、道具を使わせたい」という相談が一気に増えました。本記事では、Claude APIを使って自社サービス・社内システムに組み込むAIエージェントを作る方法を、最小の呼び出しから、Tool Useの手動ループ完全版、Tool Runner、structured outputs、MCPコネクタ、モデル選定とコスト、エラー処理まで、PythonとTypeScriptのコードで解説します。そして何より、「そもそもエージェントにすべきか」という判断基準から始めます。
- いきなりエージェントにしない。単発の呼び出し → ワークフロー → エージェントの順に検討し、「手順を事前に書けない」ときだけエージェントにする
- エージェントの本体は「tool_useを受け取り→ツールを実行し→tool_resultを返す」ループ。並列呼び出しの結果は1メッセージにまとめ、失敗は
is_errorで返す - 精度を左右するのはツールのdescription。ツールは少なく、説明は厚く。書き込み系ツールには人の承認ゲートをコード側で入れる
- モデルは
claude-opus-5を基準に、大量・単純処理はclaude-haiku-4-5、バランスはclaude-sonnet-5。思考はthinking={"type": "adaptive"}+effortで調整(旧budget_tokensは使わない) - 手動ループ/Tool Runner/Claude Agent SDK/Managed Agentsは「誰がループを持ち、誰が実行環境を持つか」で選ぶ
この記事の位置づけ(他カテゴリとの違い)
「AIエージェントで開発してみよう」API連携編の多くの記事は、Claude Code(コーディングエージェント)を開発者の相棒として使い、広告媒体・Slack・Shopify・GitHubなどのAPIをつないで自分たちの業務を自動化する話でした。そこでのClaudeは「開発や運用を手伝ってくれる同僚」です。一方、この「🧠 AIを自社サービスに組み込む」カテゴリで扱うのは、自社のアプリや社内システムそのものに、AIの頭脳(LLM API)を部品として埋め込む話です。エンドユーザー(社員や顧客)が触る画面の裏で、あなたのコードがClaude APIを呼び、あなたが定義したツールを使わせる。Claude Codeは作るときの道具として使えますが、完成品の中で動くのはClaude APIとあなたのコードです。


ワークフローとエージェントの違い:まず「エージェントにしない」選択肢から
Anthropicは2024年12月に公開した記事「Building effective agents」で、LLMを使ったシステムを大きく2つに分けて整理しています。
- ワークフロー:LLMとツールを、あらかじめコードで決めた手順に沿って動かすもの。「分類→要約→下書き」のように道筋はプログラムが持つ
- エージェント:LLM自身が次に何をするか・どのツールを使うかを動的に決め、結果を見ながら進むもの
そして同記事の重要な主張は、「できるだけシンプルな解から始め、必要なときだけ複雑にせよ」という点です。多くの用途は、適切な情報を渡した1回の呼び出しで十分であり、エージェントはレイテンシとコストを払ってでも柔軟性が要る場面の選択肢だとしています。
同記事が紹介しているワークフローの代表パターンを、社内システムの例に置き換えると次のようになります。
| パターン | 中身 | 社内システムでの例 |
|---|---|---|
| プロンプトチェーン | 出力を次の呼び出しの入力にする直列処理 | 議事録→要点抽出→タスク一覧化→担当者別メール下書き |
| ルーティング | 最初に分類し、専用の処理に振り分ける | 問い合わせを「経理/人事/IT」に分類し、それぞれ専用プロンプトへ |
| 並列化 | 独立した処理を同時に走らせて集約する | 契約書を条項ごとに並列チェック→結果をまとめる |
| オーケストレーター・ワーカー | 司令役が作業を分解してワーカーに振る | 調査依頼を論点ごとに分け、別々に調べて統合 |
| 評価・改善ループ | 生成→評価→修正を基準を満たすまで回す | 社外向け文章を生成し、表記ルールのチェックに通るまで修正 |
判断の目安は次の4つです。ひとつでも「いいえ」なら、エージェントではなく単発かワークフローに留めるのが安全です。
- 複雑さ:手順を事前に書き切れないか?(書けるならワークフローで十分)
- 価値:ターン数が増える分のコストと待ち時間に見合う成果か?
- 実現性:そのタスクはモデルが得意な種類か?(小さく試して確認する)
- 失敗時の影響:間違えても検知・取り消しができるか?(承認・ログ・ロールバック)
環境構築
Anthropicの開発者向けコンソール(Claude Console)でAPIキーを発行し、環境変数ANTHROPIC_API_KEYに設定します。SDKはこの環境変数を自動で読みます。キーはサーバー側だけに置き、ブラウザやモバイルアプリに埋め込まないでください(フロントから直接呼ぶと、キーが利用者に丸見えになります)。
# Python(3.10以上を推奨。仮想環境を切る)
python -m venv .venv && source .venv/bin/activate
pip install anthropic
# TypeScript / Node.js
npm install @anthropic-ai/sdk
npm install zod # Tool RunnerでbetaZodToolを使う場合
# APIキー(.envに置くなら.gitignoreに必ず追加)
export ANTHROPIC_API_KEY="sk-ant-..."Claudeは Anthropic のAPIのほか、Amazon Bedrock、Google Cloud Vertex AI、Microsoft Foundry経由でも利用できます。社内規程で「利用できるクラウドが決まっている」場合は、そちらの経路を検討してください(機能の提供状況は経路ごとに異なるので公式で確認を)。
最小の呼び出し(Python / TypeScript)
すべての機能はMessages API(POST /v1/messages)1本の上に乗っています。ツールも構造化出力も思考も、このリクエストのパラメータです。
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY を自動で読む
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system="あなたは社内ヘルプデスクのアシスタントです。簡潔に日本語で答えます。",
messages=[{"role": "user", "content": "VPNにつながらないときに最初に確認することは?"}],
)
# content はブロックの配列。text 以外(thinking など)も入りうる
for block in response.content:
if block.type == "text":
print(block.text)
print(response.stop_reason, response.usage.input_tokens, response.usage.output_tokens)import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // ANTHROPIC_API_KEY を自動で読む
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
system: "あなたは社内ヘルプデスクのアシスタントです。簡潔に日本語で答えます。",
messages: [{ role: "user", content: "VPNにつながらないときに最初に確認することは?" }],
});
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}押さえておくポイントは3つです。
- モデルIDに日付を付けない:
claude-opus-5のように、公式の一覧にあるIDをそのまま使います - APIはステートレス:会話を続けるときは、過去のやり取りを
messagesに毎回すべて入れて送ります。「会話の記憶」はあなたのアプリ側が持ちます - assistantのprefill(応答の書き出しを先に与える手法)は使わない:最新モデルでは400エラーになります。出力形式の制御は後述のstructured outputsで行います
ツール定義:descriptionが精度を左右する
ツールは「名前」「説明(description)」「入力のJSON Schema(input_schema)」の3点セットで定義します。Claudeはツールの中身のコードを見ることはできません。見えるのはこの定義だけです。つまり、descriptionは新人に渡す業務マニュアルと同じで、ここが薄いと「呼ぶべきでないときに呼ぶ」「必要な引数を推測で埋める」といったミスが増えます。
| 悪い例 | 良い例 | |
|---|---|---|
| name | search | search_faq(何を検索するか分かる) |
| description | 「FAQを検索する」 | 「社内FAQ(IT・経理・人事・総務)を全文検索し、関連度の高い順に最大5件返す。回答を作る前に必ず使う。0件なら言い換えて1回だけ再検索し、それでも無ければ起票を検討する。個人情報や他人の申請状況は検索できない」 |
| 引数の説明 | q: string | query:「検索語。質問文そのままではなく、製品名・エラーメッセージ・手続き名などのキーワード」 |
良いdescriptionには、①何をするか ②いつ使うか(使わないか) ③返り値の形 ④制約・限界が入っています。長く見えても、1ツールあたり3〜5文程度を書くのが目安です。逆に、ツールの数は増やすほど選択ミスが増えるので、「ツールは少なく、説明は厚く」が原則です。似たツールを10個並べるより、引数で切り替えられる1個のほうが安定します。

手動エージェントループ完全版:社内ヘルプデスク
ここからが本題です。社員からの質問に答える社内ヘルプデスク・エージェントを作ります。ツールは2つだけです。
search_faq:社内FAQを検索する(読み取りのみ・承認不要)create_ticket:情シス・総務などへのチケットを起票する(書き込み・人の承認が必要)
ループの骨格は「Claudeに送る → stop_reasonを見る → tool_useならツールを実行してtool_resultを返す → また送る」の繰り返しです。
import json
import logging
import anthropic
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("helpdesk")
client = anthropic.Anthropic()
MODEL = "claude-opus-5"
MAX_TURNS = 8 # 暴走・コスト青天井を防ぐ上限
SYSTEM = """あなたは株式会社サンプルの社内ヘルプデスク担当です。
- 回答の前に必ず search_faq で社内FAQを確認し、FAQに基づいて答えてください。
- FAQで解決しない、または申請・作業が必要な場合だけ create_ticket を使います。
- 推測で手順を作らないでください。分からないことは分からないと伝えます。
- 回答は日本語で、手順は番号付きで簡潔に。"""
TOOLS = [
{
"name": "search_faq",
"description": (
"社内FAQ(IT・経理・人事・総務)を全文検索し、関連度の高い順に最大5件を返す。"
"回答を作る前に必ず使う。0件なら言い換えて1回だけ再検索する。"
"返り値は {id, title, body, url} のJSON配列。個人の申請状況や人事情報は検索できない。"
),
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "検索キーワード。質問文ではなく製品名・エラー文・手続き名など",
},
"category": {
"type": "string",
"enum": ["it", "accounting", "hr", "general"],
"description": "分かる場合のみ指定。不明なら省略",
},
},
"required": ["query"],
},
},
{
"name": "create_ticket",
"description": (
"担当部署へのチケットを起票する。FAQで解決しない場合や、権限付与・機器交換など"
"人の作業が必要な場合のみ使う。起票前に利用者本人の確認が入るため、"
"summary には利用者が読んで分かる要約を書く。返り値は {ticket_id, status}。"
),
"input_schema": {
"type": "object",
"properties": {
"department": {"type": "string", "enum": ["it", "accounting", "hr", "general"]},
"summary": {"type": "string", "description": "60字以内の件名"},
"detail": {"type": "string", "description": "状況・試したこと・エラー文"},
"priority": {"type": "string", "enum": ["low", "normal", "high"]},
},
"required": ["department", "summary", "detail", "priority"],
},
},
]
# ---- ツールの実装(ここはあなたの社内システムにつなぐ) ----
def search_faq(query: str, category: str | None = None) -> list[dict]:
# 実際は社内検索API・全文検索・ベクトル検索などを呼ぶ。読み取り専用の資格情報で
return [{"id": "faq-102", "title": "VPNに接続できない", "body": "1. 端末を再起動...", "url": "https://intra.example.com/faq/102"}]
def create_ticket(department: str, summary: str, detail: str, priority: str, user_id: str) -> dict:
# 実際はチケット管理システムのAPIを呼ぶ。起票者は必ずログイン中の本人にする
return {"ticket_id": "T-2026-0001", "status": "open"}
def ask_user_approval(tool_input: dict) -> bool:
# 実運用ではUIに確認ダイアログを出す。ここではCLIで代用
print("\n以下の内容で起票します:\n" + json.dumps(tool_input, ensure_ascii=False, indent=2))
return input("よろしいですか? [y/N] ").strip().lower() == "y"
class ToolError(Exception):
"""モデルに伝えてよいエラー(is_error で返す)"""
def run_tool(name: str, tool_input: dict, user_id: str) -> str:
if name == "search_faq":
return json.dumps(search_faq(**tool_input), ensure_ascii=False)
if name == "create_ticket":
# 人の承認ゲート:モデルの判断だけでは書き込ませない
if not ask_user_approval(tool_input):
raise ToolError("利用者が起票を承認しませんでした。起票せずに、他にできることを案内してください。")
return json.dumps(create_ticket(**tool_input, user_id=user_id), ensure_ascii=False)
raise ToolError(f"未知のツールです: {name}")
def run_agent(user_message: str, user_id: str) -> str:
messages = [{"role": "user", "content": user_message}]
for turn in range(MAX_TURNS):
response = client.messages.create(
model=MODEL,
max_tokens=16000,
system=SYSTEM,
tools=TOOLS,
messages=messages,
)
log.info("turn=%d stop=%s in=%d out=%d", turn, response.stop_reason,
response.usage.input_tokens, response.usage.output_tokens)
if response.stop_reason == "end_turn":
return "".join(b.text for b in response.content if b.type == "text")
if response.stop_reason == "refusal":
return "この内容にはお答えできません。担当部署に直接お問い合わせください。"
if response.stop_reason == "max_tokens":
raise RuntimeError("出力が上限に達しました。max_tokens か指示を見直してください")
if response.stop_reason != "tool_use":
raise RuntimeError(f"想定外の stop_reason: {response.stop_reason}")
# assistant の応答は content ごと(thinking ブロックも含めて)そのまま戻す
messages.append({"role": "assistant", "content": response.content})
# 並列呼び出しに備え、全 tool_use の結果を1つの user メッセージにまとめる
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
log.info("tool=%s input=%s", block.name, json.dumps(block.input, ensure_ascii=False))
try:
content = run_tool(block.name, block.input, user_id)
tool_results.append({"type": "tool_result", "tool_use_id": block.id, "content": content})
except ToolError as e:
tool_results.append({"type": "tool_result", "tool_use_id": block.id,
"content": str(e), "is_error": True})
except Exception:
log.exception("tool failed")
# 内部の例外メッセージ(接続先・スタックトレース)はモデルに渡さない
tool_results.append({"type": "tool_result", "tool_use_id": block.id,
"content": "内部エラーで実行できませんでした。", "is_error": True})
messages.append({"role": "user", "content": tool_results})
return "処理が長くなったため中断しました。お手数ですがチケットを直接起票してください。"
if __name__ == "__main__":
print(run_agent("VPNがつながらず、再起動しても直りません。どうすれば?", user_id="u-1234"))このコードには、実運用で効いてくる判断がいくつも入っています。
| 設計 | 理由(トレードオフ) |
|---|---|
MAX_TURNSで上限 | ツールの結果が期待と違うと、モデルが延々と再検索することがある。上限到達時は人に引き継ぐ文言を返し、ログで原因を追う |
response.contentをそのまま戻す | テキストだけ抜き出して戻すと、tool_useブロックやthinkingブロックが失われ、次のリクエストが不整合になる |
| 全tool_resultを1メッセージに | Claudeは1回の応答で複数ツールを並列に呼ぶことがある。結果を別々のメッセージに分けると不正な形になるうえ、並列呼び出し自体をしなくなっていく |
失敗はis_error: Trueで返す | 例外で落とすより、「失敗した」と伝えればモデルが言い換え・代替案・謝罪を自分で組み立てられる。黙って結果を捨てるのは最悪 |
| 内部エラーの詳細を渡さない | 接続先ホスト名やSQLなどがモデル経由で利用者に漏れるのを防ぐ。詳細はサーバーログに |
| 承認ゲートをツール実装側に | 「起票前に確認して」とプロンプトに書くだけでは強制力がない。コードで止めれば、モデルが何を出力しても勝手に書き込まれない |
user_idをモデルに渡さずコードで注入 | 「起票者」をモデルが入力できると、なりすましの余地が生まれる。権限に関わる値はログイン情報から入れる |
refusalも分岐 | 安全上の理由で応答を控えた場合も200で返る。stop_reasonを見ずにcontentを読むと空文字を表示してしまう |
Tool Runner:ループをSDKに任せる
上のループは「ツールを実行して結果を返す」部分がほぼ定型です。SDKのbeta機能Tool Runnerを使うと、関数を定義するだけでループをSDKが回してくれます。Pythonでは@beta_toolデコレーターが、型ヒントとdocstringからinput_schemaを自動生成します。
import json
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool
def search_faq(query: str) -> str:
"""社内FAQを全文検索し、関連度の高い順に最大5件をJSONで返す。回答前に必ず使う。
Args:
query: 検索キーワード。製品名・エラー文・手続き名など
"""
results = [{"id": "faq-102", "title": "VPNに接続できない", "body": "1. 端末を再起動..."}]
return json.dumps(results, ensure_ascii=False)
@beta_tool
def create_ticket(department: str, summary: str, detail: str) -> str:
"""担当部署へのチケットを起票する。FAQで解決しない場合のみ使う。
Args:
department: it / accounting / hr / general のいずれか
summary: 60字以内の件名
detail: 状況・試したこと・エラー文
"""
# 承認ゲートはツール関数の中に置く
if input(f"「{summary}」で起票しますか? [y/N] ").strip().lower() != "y":
return "利用者が起票を承認しませんでした。起票せずに案内してください。"
return json.dumps({"ticket_id": "T-2026-0002", "status": "open"})
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
system="あなたは社内ヘルプデスク担当です。必ずFAQを確認してから答えます。",
tools=[search_faq, create_ticket],
messages=[{"role": "user", "content": "経費精算の締め日を過ぎてしまいました。どうすれば?"}],
)
# 1ターンごとの assistant メッセージが順に得られる(ログや進捗表示に使える)
for message in runner:
for block in message.content:
if block.type == "text":
print(block.text)TypeScriptでは、Zodスキーマで入力を定義するbetaZodToolを使います。入力はZodで検証されてからrunに渡るので、型安全です。
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const searchFaq = betaZodTool({
name: "search_faq",
description: "社内FAQを全文検索し、関連度の高い順に最大5件をJSONで返す。回答前に必ず使う。",
inputSchema: z.object({
query: z.string().describe("検索キーワード。製品名・エラー文・手続き名など"),
}),
run: async ({ query }) => {
const results = [{ id: "faq-102", title: "VPNに接続できない", body: "1. 端末を再起動..." }];
return JSON.stringify(results);
},
});
const finalMessage = await client.beta.messages.toolRunner({
model: "claude-opus-5",
max_tokens: 16000,
tools: [searchFaq],
messages: [{ role: "user", content: "VPNにつながりません" }],
});
for (const block of finalMessage.content) {
if (block.type === "text") console.log(block.text);
}Tool Runnerは「ツールを呼んで結果を返すだけ」の素直なエージェントならコード量を大きく減らせます。一方、承認待ちで処理を中断し、数時間後に再開するような非同期フローや、ターンごとに独自の監査ログ・中断条件を細かく差し込みたい場合は、手動ループのほうが見通しが良くなります。Tool Runnerはbeta機能なので、SDKのバージョン更新時は変更点を確認してください。
strictツールとstructured outputs
業務システムに組み込むと、「モデルの出力を、そのまま次のプログラムに渡したい」場面が必ず出てきます。ここで使う機能は2つです。
strict:ツールの入力をスキーマ通りに保証する
ツール定義のトップレベルに"strict": Trueを付けると、tool_use.inputがinput_schemaに必ず適合するようになります。条件は、スキーマに"additionalProperties": Falseとrequiredを指定することです。enumを外れた値や必須項目の欠落で、下流のAPIが400を返す事故を防げます。
CREATE_TICKET = {
"name": "create_ticket",
"description": "担当部署へのチケットを起票する。FAQで解決しない場合のみ使う。",
"strict": True, # tool_use.input がスキーマ通りであることを保証
"input_schema": {
"type": "object",
"properties": {
"department": {"type": "string", "enum": ["it", "accounting", "hr", "general"]},
"summary": {"type": "string"},
"priority": {"type": "string", "enum": ["low", "normal", "high"]},
},
"required": ["department", "summary", "priority"],
"additionalProperties": False,
},
}structured outputs:最終回答をJSONで受け取る
ツールではなく応答そのものをJSONにしたい場合は、output_configのformatにJSON Schemaを渡すか、Pythonならclient.messages.parse()にPydanticモデルを渡します。以前よく使われた「assistantの書き出しに{を入れておく」prefillは、最新モデルでは400エラーになるので使いません。
from typing import Literal
import anthropic
from pydantic import BaseModel
client = anthropic.Anthropic()
class Triage(BaseModel):
department: Literal["it", "accounting", "hr", "general"]
urgency: Literal["low", "normal", "high"]
needs_human: bool
summary: str
response = client.messages.parse(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "来週入社の新人のPCがまだ届いていません。至急確認したいです。"}],
output_format=Triage,
)
triage = response.parsed_output # 検証済みの Triage インスタンス
print(triage.department, triage.urgency, triage.needs_human)import json
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "来週入社の新人のPCがまだ届いていません。"}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"department": {"type": "string", "enum": ["it", "accounting", "hr", "general"]},
"urgency": {"type": "string", "enum": ["low", "normal", "high"]},
},
"required": ["department", "urgency"],
"additionalProperties": False,
},
}
},
)
text = next(b.text for b in response.content if b.type == "text")
data = json.loads(text)このような「問い合わせの振り分け」は、実はエージェントにする必要のない単発の呼び出しの典型です。ルーティングだけstructured outputsで確定させ、その後の処理をコードで分岐させる——これが冒頭で紹介したワークフローの「ルーティング」パターンです。
adaptive thinkingとeffort
Claudeには、回答の前に内部で考える思考(thinking)の機能があります。現行モデルでは、思考の量をモデル自身が問題の難しさに応じて決めるadaptive thinkingを使い、深さの目安はeffortで指定します。以前のbudget_tokens(思考トークン数を固定で指定する方式)は、新しいモデルでは400エラーになるので使いません。
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
thinking={"type": "adaptive"},
output_config={"effort": "medium"}, # low / medium / high / xhigh / max
tools=TOOLS,
messages=messages,
)| effort | 向いている処理 | 考え方 |
|---|---|---|
low | 分類・短い抽出・定型応答・サブ処理 | ツール呼び出しも少なくまとまり、速く安い |
medium | 一般的なQ&A・ヘルプデスク | 品質を保ちつつコストを下げたいときの一段下げ |
high | 多くの業務エージェントの基準 | 既定値。品質とコストのバランスが良い |
xhigh / max | 長い調査・難しい推論・コーディング | 正確さがコストより重要なときだけ。効果を測ってから上げる |
思考に使ったトークンも出力トークンとして課金されます。effortはモデル選定より先に触れる「品質とコストのつまみ」です。いきなり安いモデルに下げる前に、同じモデルでeffortを下げたら品質が保てるかを評価データで確かめると、判断を誤りにくくなります。
ストリーミング
チャットUIでは、回答を1文字ずつ表示するストリーミングが体感速度を大きく左右します。また、出力が長くなりうるリクエストでは、HTTPのタイムアウトを避ける意味でもストリーミングが推奨です。
with client.messages.stream(
model="claude-opus-5",
max_tokens=16000,
system=SYSTEM,
messages=[{"role": "user", "content": "新しいPCの初期設定手順をまとめて"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message() # usage や stop_reason はここから
print("\n", final.stop_reason, final.usage.output_tokens)const stream = client.messages
.stream({
model: "claude-opus-5",
max_tokens: 16000,
messages: [{ role: "user", content: "新しいPCの初期設定手順をまとめて" }],
})
.on("text", (text) => process.stdout.write(text));
const final = await stream.finalMessage();
console.log("\n", final.stop_reason);Webアプリでは、サーバー側でこのストリームを受け、Server-Sent EventsやWebSocketでブラウザに中継するのが定番です。ツールを使うエージェントでも、get_final_message()で得た最終メッセージを使えば、先ほどの手動ループにそのまま組み込めます。
サーバーツール(web検索)
自分で実装するツールとは別に、Anthropicのサーバー側で実行されるツールがあります。宣言するだけで使え、実行ループをあなたが書く必要はありません。執筆時点では、web検索(web_search_20260209)、web取得(web_fetch_20260209)、コード実行(code_execution_20260521)などが提供されています。
messages = [{"role": "user", "content": "今月発表された主要なクラウド障害の公式報告をまとめて"}]
while True:
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=[{"type": "web_search_20260209", "name": "web_search", "max_uses": 5}],
messages=messages,
)
# サーバー側の処理が長いと pause_turn で一度戻る。そのまま送り返せば続きから再開
if response.stop_reason == "pause_turn":
messages.append({"role": "assistant", "content": response.content})
continue
break
for block in response.content:
if block.type == "text":
print(block.text)社内ヘルプデスクに「最新のOSアップデート情報」を答えさせたい、といった場面で便利です。ただし、社外の情報を持ち込むことになるので、検索先をallowed_domainsで公式サイトに絞る、検索結果の引用元を画面に表示する、といった配慮をセットにしてください。web検索は検索回数に応じた追加料金がかかる点も公式で確認を。
MCPコネクタ:APIからリモートMCPサーバーを使う
MCP(Model Context Protocol)は、Anthropicが2024年11月に公開した、AIアプリと外部のツール・データをつなぐオープン規格です。その後OpenAI・Google・Microsoftなども対応を表明し、業界標準になりつつあります。Messages APIには、リモートのMCPサーバーに直接つなぐMCPコネクタ(beta)があり、MCPクライアントを自前で実装しなくても、そのサーバーのツールをClaudeに使わせられます。
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
betas=["mcp-client-2025-11-20"],
mcp_servers=[{"type": "url", "url": "https://example.com/mcp", "name": "my-mcp"}],
tools=[{"type": "mcp_toolset", "mcp_server_name": "my-mcp"}], # mcp_servers とセットで必須
messages=[{"role": "user", "content": "今週の未対応チケットを一覧にして"}],
)使い分けの考え方はシンプルです。
- 自社の業務ロジックそのもの(起票・承認・社内DB参照)は、通常のツールとして自分のコードに実装する。権限チェックと承認ゲートを確実にかけられる
- 既にMCPサーバーとして公開されている外部サービスや、社内で複数のAIアプリから共通利用したい機能はMCPサーバーにして、コネクタでつなぐ
MCPコネクタはAnthropicのサーバーから接続するため、インターネットから到達できるURLである必要があります(社内ネットワーク内だけのサーバーには届きません)。認証付きのサーバーにつなぐ場合の設定を含め、仕様は公式ドキュメントで確認してください。MCPサーバー自体の作り方はMCPサーバーの作り方と使い方で解説しています。
モデル選定とコスト概算
執筆時点(2026年9月)の主なモデルと料金です(100万トークンあたり、米ドル)。必ず公式の料金ページで最新を確認してください。
| モデルID | 入力 | 出力 | コンテキスト | 向いている用途 |
|---|---|---|---|---|
claude-fable-5-1 | $10 | $50 | 1M | 最上位。長時間・高難度の推論やエージェント作業 |
claude-opus-5 | $5 | $25 | 1M | 既定の推奨。業務エージェントの基準モデル |
claude-sonnet-5 | $2 | $10 | 1M | 品質とコストのバランス。利用者数の多いチャット |
claude-haiku-4-5 | $1 | $5 | 200K | 分類・抽出・ルーティングなど大量・単純処理 |
1問い合わせあたりのコストを概算する
エージェントのコストは「1リクエスト」ではなく「1タスク(1問い合わせ)」で考えます。ループのたびに過去の会話とツール定義を丸ごと送り直すため、入力トークンがターンごとに積み上がるからです。
仮に、社内ヘルプデスクの1問い合わせが3回のAPI呼び出し(検索→再検索→回答)で終わり、合計で入力12,000トークン・出力1,500トークン(思考分を含む)だったとします。
| モデル | 入力分 | 出力分 | 1問い合わせあたり(試算) |
|---|---|---|---|
claude-opus-5 | 12,000×$5/1M=$0.060 | 1,500×$25/1M=$0.0375 | 約$0.10 |
claude-sonnet-5 | $0.024 | $0.015 | 約$0.04 |
claude-haiku-4-5 | $0.012 | $0.0075 | 約$0.02 |
これはあくまで前提を置いた試算で、実際のトークン数はシステムプロンプトの長さ、FAQ本文の量、ターン数で大きく変わります。本番前に実際の問い合わせ数十件でusageを記録し、1タスクあたりの平均と上振れを実測してください。コストを下げるレバーは、効きやすい順に次の通りです。
- プロンプトキャッシュ:毎回同じシステムプロンプトとツール定義をキャッシュに載せる(
cache_control={"type": "ephemeral"})。キャッシュ読み込みは入力料金の約1/10。変わらない部分を先頭に置くのがコツで、usage.cache_read_input_tokensで効いているか確認できる - ツール結果を絞る:FAQ本文を全文返さず、上位数件の要点だけ返す。入力トークンの大半はツール結果であることが多い
- effortを下げる:品質が保てる範囲で
high→medium→low - モデルを使い分ける:振り分けはHaiku、回答はOpus、のように処理ごとに分ける
- Batch API:夜間の一括要約など急がない処理は約50%安いBatch APIに回す

エラー処理とリトライ
SDKは、408・409・429・5xxと接続エラーを既定で2回まで自動リトライします(max_retriesで変更可)。アプリ側では、リトライしても駄目だった場合に種類ごとに違う対応をとれるよう、具体的な例外から順に捕まえます。
import anthropic
client = anthropic.Anthropic(max_retries=4, timeout=120.0)
def ask(messages: list[dict]) -> str | None:
try:
response = client.messages.create(
model="claude-opus-5", max_tokens=16000, messages=messages,
)
except anthropic.RateLimitError:
# 429:自動リトライ後も超過。キューに戻して後で再実行、利用者には混雑表示
return None
except anthropic.BadRequestError as e:
# 400:リクエストの形が不正。リトライしても直らないのでログを見て修正
raise RuntimeError(f"不正なリクエスト: {e.message}") from e
except anthropic.APIStatusError as e:
# その他のHTTPエラー(5xx の過負荷など)
raise RuntimeError(f"APIエラー status={e.status_code}") from e
except anthropic.APIConnectionError as e:
# ネットワーク到達不可・タイムアウト
raise RuntimeError("Claude APIに接続できません") from e
return "".join(b.text for b in response.content if b.type == "text")- 429を減らす:利用が増えたら、同時実行数をキューで制御する。レート上限は組織の利用実績に応じて段階的に上がる
- タイムアウト:エージェントの1ターンは長くなりがち。ユーザー向けの画面ではストリーミングで進捗を見せ、バックエンド処理はジョブキューに逃がす
- 二重実行に注意:リトライでツールが2回実行されることがある。起票のような書き込みは、冪等キー(同じ依頼を2回処理しない仕組み)を持たせる
手動ループ/Tool Runner/Agent SDK/Managed Agentsの使い分け
Anthropicのエコシステムには、エージェントを作る方法が4つあります。名前が似ていて混乱しやすいのですが、「エージェントループ(ハーネス)を誰が持つか」「実行環境(デプロイ先)を誰が持つか」の2軸で整理すると明快です。
| 方式 | あなたが書くもの | ループ/実行環境 | 使えるツール | 向いている場面 |
|---|---|---|---|---|
| Claude API 手動ループ | ループ全体とツール | 自前/自前 | 自分で定義したものだけ | 承認待ちの中断・再開、独自の監査ログなど、制御を完全に握りたい業務システム |
| Tool Runner(SDKのbeta) | ツール関数だけ | SDK/自前 | 自分で定義したものだけ | 自社ツールを使う素直なエージェントを少ないコードで |
| Claude Agent SDK(別製品) | プロンプトとオプション | SDK(Claude Codeのハーネス)/自前 | Read・Write・Edit・Bash・Grepなど組み込み+MCP+サブエージェント | ファイルやコードを扱う汎用エージェントを自社インフラで動かす |
| Managed Agents(beta) | エージェント設定とセッション操作 | Anthropic/Anthropic | ホストされたサンドボックス+MCP+自前ツール | ループも実行環境も任せ、長時間のセッションを動かしたい |
Claude Agent SDKは、Claude Codeをライブラリ化したものです。「社内ドキュメントのフォルダを読んで報告書を作る」のようにファイル操作が中心なら、ツールを自作するより早く形になります。
# pip install claude-agent-sdk
import anyio
from claude_agent_sdk import ClaudeAgentOptions, query
async def main():
options = ClaudeAgentOptions(allowed_tools=["Read", "Grep", "Glob"]) # 読み取り系だけ許可
async for message in query(prompt="docs/ 配下の手順書から、VPN関連の記述を要約して", options=options):
print(message)
anyio.run(main)Managed Agentsは、エージェントの設定(モデル・システムプロンプト・ツール)を作成しておき、実行のたびにセッションを開始すると、Anthropic側がループとサンドボックス(隔離された実行環境)をホストしてくれる形態です。自前でコンテナやスケジューラーを運用したくない場合の選択肢になります。仕様はbetaで変わりうるため、本記事では概念の紹介に留めます。
- 自社の業務データと業務ロジックが中心 → Tool Runnerから始め、制御が複雑になったら手動ループ
- ファイル・コード・シェル操作が中心 → Claude Agent SDK
- 実行環境の運用自体を持ちたくない → Managed Agents
他社の公開事例を見ても、顧客対応のAI化は「全部AIに任せる」一択ではありません。たとえばKlarnaは2024年に顧客対応へAIアシスタントを導入したと公表し、その後、人間による対応も重視する方向に調整したと報じられました。どの方式を選ぶにしても、AIが得意な範囲と、人に引き継ぐ境界線を設計に最初から組み込んでおくことが大切です。
当社がエージェント開発で採っている方針
生成AIエージェント導入支援の現場で大事にしていること
当社は、広告運用と並んで生成AIエージェントの導入支援を行っています。社内システムにAIを組み込む相談を受けたとき、設計段階で必ず確認している方針は次の通りです。
- 最初からエージェントにしない:まず業務を分解し、単発の呼び出しやワークフローで済む部分を切り出します。エージェントにするのは「手順を事前に書けない」部分だけです。結果として、エージェント部分は小さく、テストしやすくなります
- ツールは少なく、説明は厚く:最初は2〜3個のツールで始め、descriptionに「いつ使うか・使わないか・返り値・制約」を書き込みます。ツールを増やすのは、ログで「これが無くて困っている」と確認できてからです
- 人の承認ゲートをコードで持つ:送信・起票・更新・削除など、外に影響が出る操作は、プロンプトではなくツールの実装側で承認を必須にします。読み取りと書き込みで資格情報も分けます
- 評価データを先に作る:実際の問い合わせを匿名化した評価セットを用意し、モデル変更・プロンプト変更・effort変更のたびに同じセットで比べます。「なんとなく良くなった」で本番に出さないためです
- ログとコストを最初から計測する:ターンごとのツール呼び出しと
usageを記録し、1タスクあたりのコストとターン数を監視します
ちなみに、このサイト自体もAIエージェント(Claude Code)と一緒に運用しており、「AIの作業はブランチ→プルリクエスト→人が確認してマージ」という承認ゲートを敷いています。考え方は同じで、AIが間違えても、人が確認するまで外に出ない構造を先に作ることです。詳しくは【自社事例】AIエージェントでメディアサイトを運用する仕組みをご覧ください。

よくある質問
手動ループとTool Runner、最初はどちらで書くべき?
仕組みを理解するために一度は手動ループを書くことをおすすめします。tool_useとtool_resultの往復、stop_reasonの分岐、エラーの返し方を体で覚えておくと、Tool Runnerを使ったときのデバッグが格段に楽になります。そのうえで、ツールを実行して結果を返すだけの素直なエージェントならTool Runnerに寄せ、承認ゲートや独自のログ・中断処理が複雑になる場合は手動ループを選ぶ、という使い分けが現実的です。
Claude Agent SDKとClaude APIのTool Use、何が違う?
Claude APIのTool Useは、あなたが定義したツールだけを使うエージェントを、あなたのコードで組み立てる仕組みです。Claude Agent SDKはClaude Codeをライブラリ化した別製品で、ファイル読み書きやBash、Grepなどの組み込みツールとエージェントループが最初から揃っています。自社アプリの業務ロジックに組み込むならTool Use、ファイルやコードを扱う汎用エージェントを自前のインフラで動かしたいならAgent SDK、が基本の目安です。
エージェントのコストが読めなくて不安です。どう管理すればいい?
まずループの最大ターン数を必ず設け、1タスクあたりのAPI呼び出し回数に上限をかけます。次に、各レスポンスのusage(入力・出力・キャッシュ読み込みのトークン数)をログに残し、1タスクあたりの平均コストを実測します。そのうえで、変わらないシステムプロンプトとツール定義をプロンプトキャッシュに載せる、簡単な処理は安いモデルやlowのeffortに回す、急がない処理はBatch APIに回す、の順で下げていくのが確実です。
社内データを扱うエージェントで一番気をつけることは?
ツールに渡す権限を最小にすることです。モデルが何を出力しても、ツール側のコードが許可しない操作は実行されません。検索は読み取り専用のアカウントで、チケット起票や送信のような書き込みは人の承認を挟む、利用者本人の権限でしかデータを引けないようにする、といった制御をツールの実装側で強制してください。プロンプトで「〜しないで」と頼むだけの制御は、安全策としては不十分です。
まとめ|エージェントは「小さく作って、止める仕組みから」
Claude APIでエージェントを作る技術そのものは、tool_useとtool_resultの往復というシンプルなループです。難しいのは、そもそもエージェントにすべきかの判断、ツールの説明の書き方、そして権限・承認・上限・ログといった「止める仕組み」の設計です。まずは本記事の手動ループをそのまま動かし、自社の業務に合わせてツールを1つずつ差し替えてみてください。
OpenAIやGeminiのAPIと比べてどう違うのか、複数のモデルをどう使い分けるのかは、相方の記事OpenAI・Claude・Gemini API 実装比較で、同じタスクを3社のコードで書き比べながら解説しています。
AIエージェント開発のスキルを、キャリアにつなげる
LLM APIでエージェントを実装できるエンジニアは、転職・独立どちらでも需要が高い領域です。
- AIで独立するには?未経験から案件を取る6ステップ【2026】:自動化・開発スキルを案件にする。フリーランスエージェントの選び方も
- AIが快適に動くノートパソコンの選び方&おすすめ3選【2026】:開発・AIエージェントを動かすならメモリ16GB以上が目安