はじめに / 対象と前提
Claude API には、モデル自身が Web 検索を実行して最新情報を根拠付きで回答する サーバーサイドツール web_search がある。自前で検索 API(Brave や Tavily 等)を用意して tool use のループを書く必要がなく、リクエストに tool 定義を 1 つ足すだけで動く。
自分が RAG 的な「最新ドキュメント参照つき回答」を作ったとき、ドメイン制限と引用(citations)まわりで 3 回ハマったので、実装手順とあわせてまとめる。
- 想定読者:Claude API を Python から叩いたことがある人
- 環境:Python 3.13 /
anthropicSDK 0.6x / モデルはclaude-sonnet-5で確認 - 料金の注意:Web 検索は通常のトークン代に加えて 検索 1,000 回あたり $10 が別途かかる(検索結果はプロンプトのトークンとしても課金される)
TL;DR
-
toolsに{"type": "web_search_20250305", "name": "web_search"}を足すだけで検索つき回答になる -
allowed_domainsとblocked_domainsは 同時指定不可(400 エラー) - 回答テキストは citations 付きの複数 text ブロックに分割されるので、連結処理を自分で書く
- マルチターンでは検索結果ブロック(
encrypted_content含む)をそのまま次のリクエストに返す
手順 / 動かし方
最小構成はこれだけ。
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=2048,
tools=[{
"type": "web_search_20250305",
"name": "web_search",
"max_uses": 3,
}],
messages=[{
"role": "user",
"content": "Anthropic の最新モデルのコンテキスト長を公式ドキュメントで確認して教えて",
}],
)
ポイントは 2 つ。
-
max_uses:1 ターン内の検索回数上限。指定しないと必要なだけ検索する=課金が読めないので、自分は必ず 3〜5 に絞っている - クライアント側でツール実行ループを書く必要は ない。検索はすべて Anthropic のサーバー側で完結し、1 回の
messages.createで「検索→結果読解→回答」まで返ってくる
レスポンスの content には通常の text ブロックに加えて、server_tool_use(検索クエリ)と web_search_tool_result(検索結果)が混ざって返る。実行結果はこんな並びになる。
server_tool_use → query="Claude context window official docs"
web_search_tool_result → 検索結果 N 件(url, title, encrypted_content)
text → 回答本文(citations 付き)
usage に server_tool_use.web_search_requests が入るので、実際に何回検索したかはここで確認できる。
ハマりどころ
1. allowed_domains と blocked_domains は排他
「公式ドキュメントだけ見てほしいが、コミュニティサイトは除外したい」と考えて両方指定したら、リクエスト自体が弾かれた。
anthropic.BadRequestError: Error code: 400 -
{'error': {'type': 'invalid_request_error',
'message': 'web_search_20250305: only one of allowed_domains or blocked_domains can be specified'}}
原因:仕様として両者は排他。ホワイトリスト方式かブラックリスト方式のどちらかしか選べない。
回避策:確実に読ませたいソースがあるなら allowed_domains 一本に寄せる。
"allowed_domains": ["docs.anthropic.com", "github.com"],
なお https:// は付けない(ドメイン名のみ)。サブドメインは自動で含まれる。
2. 回答テキストが複数 text ブロックに分割される
response.content[0].text を取る従来のコードのままだと、回答の先頭しか取れない。引用付き回答では、citations の区切りごとに text ブロックが分割されるため、content 内に text ブロックが 5 個も 10 個も並ぶ。
回避策:text ブロックを全部連結する。引用元も拾うなら citations を同時に集める。
answer = ""
sources = []
for block in response.content:
if block.type == "text":
answer += block.text
for c in (block.citations or []):
sources.append({"title": c.title, "url": c.url})
citations の中身は url / title / cited_text で、UI に「参照元リンク」を出すのに十分な情報が入っている。
3. マルチターンで encrypted_content を返し忘れると精度が落ちる
会話を続けるとき、直前の assistant 応答を messages に積むが、ここで text ブロックだけ抜き出して返すと、モデルは前ターンで読んだ検索結果を参照できなくなる。検索結果本文は web_search_tool_result 内の encrypted_content(暗号化された本文)として返っており、これを次のリクエストに含めて初めて前ターンの検索内容を引き継げる。
回避策:assistant ターンは加工せず response.content を丸ごと積む。
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "その情報の一次ソースはどこ?"})
自分は最初「トークン節約」のつもりで text だけ返す実装にしていて、2 ターン目の回答が急に曖昧になる現象に 30 分溶かした。
背景・補足
web_search は「サーバーツール」と呼ばれる種類で、自作ツールと違い tool_result をクライアントから返す必要がない。長い検索セッションでは stop_reason が pause_turn で返ることがあり、その場合はレスポンスをそのまま messages に積んで再リクエストすれば続きから再開される。この扱いも自作ツールには無い挙動なので、リトライ処理を書くときに分岐しておくと安全だった。
まとめ
-
web_search_20250305を tools に足すだけで、ループ実装なしの検索つき回答が作れる -
max_usesは必ず指定する(課金と暴走の抑制) -
allowed_domains/blocked_domainsは排他。400 が出たらまずここを疑う - 回答は text ブロック連結+citations 収集で組み立てる
- マルチターンは
response.contentを丸ごと返す(encrypted_contentを削らない)