なぜ検索拡張型AIが求められるのか
大規模言語モデル(LLM)の活用が本格化する中で、「情報の鮮度」と「回答の正確性」は最も重要な課題です。Claude API は 2026 年 3 月に Web Search Tool と Programmatic Tool Calling を正式版(GA)としてリリースし、さらに Dynamic Filtering (動的フィルタリング)という強力な機能を追加しました。
ここではWeb Search Tool・Dynamic Filtering・Citations API の 3 つを組み合わせて、出典付きで正確な回答を返す検索拡張型AIアシスタント を本番環境で構築する方法を、アーキテクチャ設計からコード実装まで体系的に解説します。
従来の RAG(Retrieval-Augmented Generation)は自前のベクトルデータベースが必要でしたが、Web Search Tool を使えば インターネット全体をナレッジベース として活用できます。さらに Dynamic Filtering を組み合わせることで、検索結果のノイズを排除し、トークン消費を最適化しながら高精度な回答を実現できます。
検索を軸にした自律型システムの全体設計は Claude API で実装する自律リサーチエージェント も合わせてご覧ください。
アーキテクチャ全体像 — 3つのAPIを統合する設計
検索拡張型AIアシスタントのアーキテクチャは、次の 3 層で構成されます。
検索層(Web Search Tool) : ユーザーの質問に基づいてリアルタイムでWeb検索を実行し、関連するソースを収集します。web_search_20260209 バージョンでは、ドメインフィルタリングやユーザーロケーション設定にも対応しています。
フィルタリング層(Dynamic Filtering + Code Execution) : 検索結果をコード実行で動的にフィルタリングし、関連性の低い結果を除外します。この層がトークン効率と回答精度を大幅に向上させるカギです。
回答生成層(Citations API) : フィルタリングされた情報を基に回答を生成し、各記述に対して出典情報を自動的に付与します。
// アーキテクチャの概念コード
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
// 検索拡張型アシスタントの基本構成
async function searchAugmentedAssistant ( userQuery : string ) {
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 8096 ,
tools: [
{
type: "web_search_20260209" ,
name: "web_search" ,
// Dynamic Filtering: コード実行で検索結果をフィルタリング
dynamic_filtering: { enabled: true },
// ドメイン制限(オプション)
allowed_domains: [ "docs.anthropic.com" , "github.com" ],
},
],
// Citations を有効化
citations: { enabled: true },
messages: [
{
role: "user" ,
content: userQuery,
},
],
});
return response;
}
この設計により、ユーザーの質問 → Web検索 → 動的フィルタリング → 出典付き回答生成 という一気通貫のパイプラインが構成されます。
Web Search Tool の本番設定 — GA版の全パラメータ解説
Web Search Tool は 2026 年 3 月にベータ版から正式版(GA)に昇格し、ベータヘッダーなしで利用できるようになりました。本番環境で最大限に活用するための設定を見ていきます。
基本設定
// Web Search Tool の本番設定
const tools = [
{
type: "web_search_20260209" ,
name: "web_search" ,
// ユーザーのロケーション情報(検索結果のローカライズに使用)
user_location: {
type: "approximate" ,
country: "JP" ,
region: "Tokyo" ,
},
// 許可ドメインリスト(オプション: 信頼性の高いソースに限定)
allowed_domains: [
"docs.anthropic.com" ,
"platform.claude.com" ,
"github.com/anthropics" ,
],
// ブロックドメインリスト(オプション: 低品質ソースを除外)
blocked_domains: [ "spam-site.example.com" ],
},
];
Dynamic Filtering の有効化
Dynamic Filtering は、Claude がコード実行環境を使って検索結果をプログラム的にフィルタリングする機能です。これにより、コンテキストウィンドウに入る前にノイズが除去され、トークン消費の削減と回答精度の向上を同時に達成 できます。
// Dynamic Filtering を有効化した設定
const toolsWithFiltering = [
{
type: "web_search_20260209" ,
name: "web_search" ,
dynamic_filtering: {
enabled: true ,
},
user_location: {
type: "approximate" ,
country: "JP" ,
},
},
];
Dynamic Filtering が有効な場合、Claude は内部的にサンドボックス環境でコードを実行し、検索結果の関連性スコアリングや重複除去を自動で行います。この処理は Web Search と組み合わせた場合、コード実行のコストは無料 です。
ドメイン戦略の設計
本番環境では、用途に応じたドメイン戦略が重要です。
技術ドキュメント検索 : 公式ドキュメントサイトのみに限定し、古い情報やコミュニティの誤情報を排除します。
ニュース収集 : ニュースサイトを許可リストに入れ、SNSやフォーラムは除外します。
汎用検索 : ドメイン制限なしで Dynamic Filtering に任せ、結果の品質をコードで判定させます。
// 用途別のドメイン戦略
const domainStrategies = {
// 技術ドキュメント検索
techDocs: {
allowed_domains: [
"docs.anthropic.com" ,
"platform.claude.com" ,
"developer.mozilla.org" ,
"nodejs.org" ,
],
},
// ニュース収集
news: {
allowed_domains: [
"www.anthropic.com" ,
"techcrunch.com" ,
"theverge.com" ,
],
blocked_domains: [ "reddit.com" , "twitter.com" ],
},
// 汎用(Dynamic Filtering に委ねる)
general: {
dynamic_filtering: { enabled: true },
// ドメイン制限なし
},
};
Dynamic Filtering 実践 — コード実行で検索精度を最大化する
Dynamic Filtering は Web Search Tool の最も強力な機能の一つです。Claude が検索結果に対してコードを自動実行し、関連性の低い結果をコンテキストウィンドウに入れる前に除外します。
動作の仕組み
Claude が Web 検索を実行し、複数の検索結果を取得する
サンドボックス環境で JavaScript/Python コードが自動実行される
各結果の関連性・信頼性・鮮度をスコアリングする
スコアの低い結果をフィルタリングし、上位の結果のみ保持する
フィルタリング後の結果を基に回答を生成する
カスタムフィルタリングロジックの実装
より高度な制御が必要な場合は、Programmatic Tool Calling を使ってフィルタリングロジックをカスタマイズできます。
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
async function advancedSearchWithFiltering ( query : string ) {
// Step 1: Web Search を実行
const searchResponse = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 4096 ,
tools: [
{
type: "web_search_20260209" ,
name: "web_search" ,
dynamic_filtering: { enabled: true },
},
],
messages: [
{
role: "user" ,
content: `以下のトピックについて最新の情報を検索してください: ${ query }` ,
},
],
});
// Step 2: 検索結果からソース情報を抽出
const sources = extractSources (searchResponse);
// Step 3: カスタムフィルタリング
const filteredSources = sources. filter (( source ) => {
// 24時間以内の記事を優先
const isRecent =
Date. now () - new Date (source.publishedDate). getTime () <
24 * 60 * 60 * 1000 ;
// 信頼度スコアの閾値
const isReliable = source.reliabilityScore > 0.7 ;
return isRecent || isReliable;
});
// Step 4: フィルタリングされたソースで回答生成 + Citations
const finalResponse = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 8096 ,
citations: { enabled: true },
messages: [
{
role: "user" ,
content: [
{
type: "text" ,
text: `以下の情報源を基に、「${ query }」について回答してください。必ず出典を明記してください。` ,
},
// フィルタリング済みソースをドキュメントとして渡す
... filteredSources. map (( source ) => ({
type: "document" as const ,
source: {
type: "url" as const ,
url: source.url,
},
title: source.title,
})),
],
},
],
});
return finalResponse;
}
// ソース情報の抽出ヘルパー
function extractSources ( response : Anthropic . Message ) {
const sources : Array <{
url : string ;
title : string ;
publishedDate : string ;
reliabilityScore : number ;
}> = [];
for ( const block of response.content) {
if (block.type === "web_search_tool_result" ) {
for ( const result of block.content) {
if (result.type === "web_search_result" ) {
sources. push ({
url: result.url,
title: result.title,
publishedDate: result.page_age || new Date (). toISOString (),
reliabilityScore: calculateReliability (result.url),
});
}
}
}
}
return sources;
}
// ドメインベースの信頼度スコア算出
function calculateReliability ( url : string ) : number {
const trustedDomains : Record < string , number > = {
"docs.anthropic.com" : 1.0 ,
"platform.claude.com" : 1.0 ,
"www.anthropic.com" : 0.95 ,
"github.com" : 0.9 ,
"developer.mozilla.org" : 0.9 ,
};
for ( const [ domain , score ] of Object. entries (trustedDomains)) {
if (url. includes (domain)) return score;
}
return 0.5 ; // デフォルトスコア
}
フィルタリング効果の測定
Dynamic Filtering の効果を定量的に測定するには、以下の指標を追跡します。
// フィルタリング効果の測定
interface FilteringMetrics {
totalSearchResults : number ; // 検索結果の総数
filteredResults : number ; // フィルタリング後の結果数
tokensBeforeFiltering : number ; // フィルタリング前のトークン数
tokensAfterFiltering : number ; // フィルタリング後のトークン数
filteringRatio : number ; // フィルタリング率
tokenSavings : number ; // トークン削減率
}
function measureFilteringEffect (
before : number ,
after : number
) : FilteringMetrics {
return {
totalSearchResults: before,
filteredResults: after,
tokensBeforeFiltering: before * 500 , // 平均500トークン/結果と仮定
tokensAfterFiltering: after * 500 ,
filteringRatio: 1 - after / before,
tokenSavings: 1 - after / before,
};
}
// 期待される効果:
// - フィルタリング率: 40-60%(不要な結果を半分以上除去)
// - トークン削減: 40-60%(入力トークンコストの大幅削減)
// - 回答精度: 関連性の低い情報による混乱が減り、15-25%向上
Citations API 統合 — 出典付き回答で信頼性を担保する
Citations(引用)機能は、Claude の回答に出典情報を構造的に埋め込む機能です。ハルシネーション防止の強力な手段であり、検索拡張型アシスタントには不可欠です。
自前のナレッジに根拠を引かせる実装は search_result ブロックで自前ナレッジに根拠を引かせる Citations 実装メモ を参照してください。ここでは本番環境での高度な活用パターンに焦点を当てます。
Citations の有効化とレスポンス解析
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
async function searchWithCitations ( query : string ) {
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 8096 ,
tools: [
{
type: "web_search_20260209" ,
name: "web_search" ,
dynamic_filtering: { enabled: true },
},
],
// Citations を有効化
citations: { enabled: true },
messages: [
{
role: "user" ,
content: query,
},
],
});
// レスポンスから引用情報を構造化して抽出
const result = parseResponseWithCitations (response);
return result;
}
interface CitedContent {
text : string ;
citations : Array <{
sourceUrl : string ;
sourceTitle : string ;
citedText : string ;
startIndex : number ;
endIndex : number ;
}>;
}
function parseResponseWithCitations (
response : Anthropic . Message
) : CitedContent {
const result : CitedContent = { text: "" , citations: [] };
for ( const block of response.content) {
if (block.type === "text" ) {
// テキストブロック内の引用を解析
if ( "citations" in block && Array. isArray (block.citations)) {
for ( const citation of block.citations) {
if (citation.type === "web_search_result_location" ) {
result.citations. push ({
sourceUrl: citation.url,
sourceTitle: citation.title || "" ,
citedText: citation.cited_text || "" ,
startIndex: citation.start_char_index,
endIndex: citation.end_char_index,
});
}
}
}
result.text += block.text;
}
}
return result;
}
引用の信頼度スコアリング
回答の信頼性を定量化するため、引用の密度と品質を測定するシステムを構築します。
interface AnswerReliability {
citationDensity : number ; // 引用密度(文あたりの引用数)
uniqueSources : number ; // ユニークなソース数
trustedSourceRatio : number ; // 信頼できるソースの割合
overallScore : number ; // 総合信頼度スコア(0-1)
}
function assessAnswerReliability (
cited : CitedContent
) : AnswerReliability {
const sentences = cited.text. split ( / [。.!?] / ). filter (Boolean);
const uniqueUrls = new Set (cited.citations. map (( c ) => c.sourceUrl));
const trustedCount = cited.citations. filter (( c ) =>
isTrustedDomain (c.sourceUrl)
). length ;
const citationDensity =
sentences. length > 0 ? cited.citations. length / sentences. length : 0 ;
const trustedRatio =
cited.citations. length > 0
? trustedCount / cited.citations. length
: 0 ;
return {
citationDensity,
uniqueSources: uniqueUrls.size,
trustedSourceRatio: trustedRatio,
overallScore:
citationDensity * 0.3 +
Math. min (uniqueUrls.size / 5 , 1 ) * 0.3 +
trustedRatio * 0.4 ,
};
}
function isTrustedDomain ( url : string ) : boolean {
const trusted = [
"anthropic.com" ,
"platform.claude.com" ,
"github.com" ,
"developer.mozilla.org" ,
"nodejs.org" ,
"typescriptlang.org" ,
];
return trusted. some (( domain ) => url. includes (domain));
}
// 使用例:
// const reliability = assessAnswerReliability(citedContent);
// if (reliability.overallScore < 0.5) {
// // 信頼度が低い場合、追加検索を実行
// console.warn("Low reliability score — triggering additional search");
// }
本番アーキテクチャ — 検索拡張型アシスタントの完全実装
ここまでの要素を統合し、本番環境で使える検索拡張型AIアシスタントの完全な実装を示します。
エンドツーエンドの実装
import Anthropic from "@anthropic-ai/sdk" ;
// --- 型定義 ---
interface SearchAssistantConfig {
model : string ;
maxTokens : number ;
systemPrompt : string ;
domainStrategy : "techDocs" | "news" | "general" ;
reliabilityThreshold : number ;
maxRetries : number ;
}
interface AssistantResponse {
answer : string ;
citations : Array <{
url : string ;
title : string ;
citedText : string ;
}>;
reliability : {
score : number ;
uniqueSources : number ;
};
usage : {
inputTokens : number ;
outputTokens : number ;
searchQueries : number ;
};
}
// --- メインクラス ---
class SearchAugmentedAssistant {
private client : Anthropic ;
private config : SearchAssistantConfig ;
constructor ( config : Partial < SearchAssistantConfig > = {}) {
this .client = new Anthropic ();
this .config = {
model: "claude-sonnet-4-6" ,
maxTokens: 8096 ,
systemPrompt: `あなたは正確で最新の情報を提供するAIアシスタントです。
回答には必ず出典を明記し、不確実な情報は「~と見られます」と表記してください。
検索結果に基づかない情報は「一般的な知識として」と前置きしてください。` ,
domainStrategy: "general" ,
reliabilityThreshold: 0.5 ,
maxRetries: 2 ,
... config,
};
}
async answer ( query : string ) : Promise < AssistantResponse > {
let attempt = 0 ;
let lastError : Error | null = null ;
while (attempt <= this .config.maxRetries) {
try {
const response = await this . executeSearch (query);
const parsed = this . parseResponse (response);
// 信頼度チェック
if (
parsed.reliability.score < this .config.reliabilityThreshold &&
attempt < this .config.maxRetries
) {
console. log (
`Reliability ${ parsed . reliability . score } below threshold, retrying with broader search...`
);
attempt ++ ;
continue ;
}
return parsed;
} catch (error) {
lastError = error as Error ;
attempt ++ ;
if (attempt <= this .config.maxRetries) {
// 指数バックオフ
await new Promise (( r ) =>
setTimeout (r, Math. pow ( 2 , attempt) * 1000 )
);
}
}
}
throw new Error (
`Search failed after ${ this . config . maxRetries } retries: ${ lastError ?. message }`
);
}
private async executeSearch (
query : string
) : Promise < Anthropic . Message > {
return await this .client.messages. create ({
model: this .config.model,
max_tokens: this .config.maxTokens,
system: this .config.systemPrompt,
tools: [
{
type: "web_search_20260209" ,
name: "web_search" ,
dynamic_filtering: { enabled: true },
user_location: {
type: "approximate" ,
country: "JP" ,
},
},
],
citations: { enabled: true },
messages: [
{
role: "user" ,
content: query,
},
],
});
}
private parseResponse (
response : Anthropic . Message
) : AssistantResponse {
let answerText = "" ;
const citations : AssistantResponse [ "citations" ] = [];
let searchQueries = 0 ;
for ( const block of response.content) {
if (block.type === "text" ) {
answerText += block.text;
if ( "citations" in block && Array. isArray (block.citations)) {
for ( const c of block.citations) {
if (c.type === "web_search_result_location" ) {
citations. push ({
url: c.url,
title: c.title || "" ,
citedText: c.cited_text || "" ,
});
}
}
}
}
if (block.type === "server_tool_use" && block.name === "web_search" ) {
searchQueries ++ ;
}
}
const uniqueSources = new Set (citations. map (( c ) => c.url)).size;
const trustedCount = citations. filter (( c ) =>
isTrustedDomain (c.url)
). length ;
const score =
citations. length > 0
? (Math. min (uniqueSources / 5 , 1 ) * 0.5 +
(trustedCount / citations. length ) * 0.5 )
: 0 ;
return {
answer: answerText,
citations,
reliability: { score, uniqueSources },
usage: {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
searchQueries,
},
};
}
}
// isTrustedDomain は前述の実装を再利用
function isTrustedDomain ( url : string ) : boolean {
const trusted = [
"anthropic.com" ,
"platform.claude.com" ,
"github.com" ,
];
return trusted. some (( d ) => url. includes (d));
}
// --- 使用例 ---
async function main () {
const assistant = new SearchAugmentedAssistant ({
model: "claude-sonnet-4-6" ,
reliabilityThreshold: 0.4 ,
});
const result = await assistant. answer (
"Claude API の最新の料金体系と、コスト最適化のベストプラクティスを教えてください"
);
console. log ( "=== 回答 ===" );
console. log (result.answer);
console. log ( " \n === 出典 ===" );
result.citations. forEach (( c , i ) => {
console. log ( `[${ i + 1 }] ${ c . title }: ${ c . url }` );
});
console. log ( " \n === メトリクス ===" );
console. log ( `信頼度スコア: ${ result . reliability . score . toFixed ( 2 ) }` );
console. log ( `ユニークソース数: ${ result . reliability . uniqueSources }` );
console. log ( `入力トークン: ${ result . usage . inputTokens }` );
console. log ( `出力トークン: ${ result . usage . outputTokens }` );
console. log ( `検索クエリ数: ${ result . usage . searchQueries }` );
}
main (). catch (console.error);
// 期待される出力例:
// === 回答 ===
// Claude API の料金体系は2026年3月時点で...(出典付きの詳細な回答)
// === 出典 ===
// [1] Pricing - Claude API Docs: https://platform.claude.com/docs/...
// [2] ...
// === メトリクス ===
// 信頼度スコア: 0.75
// ユニークソース数: 4
// 入力トークン: 3200
// 出力トークン: 1800
// 検索クエリ数: 2
コスト最適化戦略 — 検索拡張の費用対効果を最大化する
検索拡張型アシスタントでは、通常の API 呼び出しに加えて Web 検索のコストが発生します。ここでは、コストを最小限に抑えながら品質を維持する戦略を解説します。
コスト構造の理解
Web Search Tool のコストは以下の構造です。
検索コスト : 検索クエリ 1 回あたり固定料金が発生します。Claude は 1 リクエスト内で複数回の検索を実行する可能性があるため、max_uses パラメータで上限を設定できます。
コード実行コスト : Dynamic Filtering のコード実行は Web Search と組み合わせた場合 無料 です。これは非常に大きなメリットです。
トークンコスト : 検索結果はコンテキストウィンドウのトークンとして計上されます。Dynamic Filtering で不要な結果を除去することがトークン削減に直結します。
コスト最適化の実装パターン
// コスト最適化設定
const costOptimizedConfig = {
// 検索回数の上限を設定
tools: [
{
type: "web_search_20260209" as const ,
name: "web_search" ,
dynamic_filtering: { enabled: true },
// 1リクエストあたりの最大検索回数
max_uses: 3 ,
},
],
// Prompt Caching と組み合わせる
// システムプロンプトをキャッシュ可能にする
system: [
{
type: "text" as const ,
text: "あなたは検索拡張型AIアシスタントです..." ,
cache_control: { type: "ephemeral" as const },
},
],
};
// コスト追跡ユーティリティ
class CostTracker {
private totalInputTokens = 0 ;
private totalOutputTokens = 0 ;
private totalSearches = 0 ;
private totalCacheHits = 0 ;
record ( usage : {
input_tokens : number ;
output_tokens : number ;
cache_read_input_tokens ?: number ;
}, searches : number ) {
this .totalInputTokens += usage.input_tokens;
this .totalOutputTokens += usage.output_tokens;
this .totalSearches += searches;
if (usage.cache_read_input_tokens) {
this .totalCacheHits += usage.cache_read_input_tokens;
}
}
getSummary () {
// Sonnet 4.6 の料金: $3/M input, $15/M output
const inputCost = ( this .totalInputTokens / 1_000_000 ) * 3 ;
const outputCost = ( this .totalOutputTokens / 1_000_000 ) * 15 ;
const cacheSavings =
( this .totalCacheHits / 1_000_000 ) * 3 * 0.9 ;
return {
totalInputTokens: this .totalInputTokens,
totalOutputTokens: this .totalOutputTokens,
totalSearches: this .totalSearches,
estimatedCost: `$${ ( inputCost + outputCost ). toFixed ( 4 ) }` ,
cacheSavings: `$${ cacheSavings . toFixed ( 4 ) }` ,
netCost: `$${ ( inputCost + outputCost - cacheSavings ). toFixed ( 4 ) }` ,
};
}
}
Prompt Caching × Web Search の組み合わせ
Web Search Tool のリクエストでも Prompt Caching は有効です。同じシステムプロンプトを繰り返し使う場合、キャッシュにより入力トークンを最大 90% 削減できます。
プロンプトキャッシュでのコスト削減の実例は Claude API のプロンプトキャッシュで月額コストを半分にした実装メモ も参照してください。
// Prompt Caching + Web Search の統合パターン
async function cachedSearchQuery (
client : Anthropic ,
query : string
) {
return await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 4096 ,
// システムプロンプトをキャッシュ
system: [
{
type: "text" ,
text: `あなたは技術調査に特化したAIアシスタントです。
以下のルールに従ってください:
1. 検索結果を基に正確な情報のみを回答する
2. 出典を必ず明記する
3. 不確実な情報は明示する
4. 日付が古い情報は注意喚起する
5. コード例は動作確認済みのものだけ提示する` ,
cache_control: { type: "ephemeral" },
},
],
tools: [
{
type: "web_search_20260209" ,
name: "web_search" ,
dynamic_filtering: { enabled: true },
max_uses: 3 ,
},
],
citations: { enabled: true },
messages: [{ role: "user" , content: query }],
});
}
// 連続クエリでのコスト削減効果:
// 1回目: 入力 ~2000トークン(キャッシュ書き込み 1.25x)
// 2回目以降: キャッシュヒット(0.1x = 90%削減)
// Dynamic Filtering のコード実行: 無料
エラーハンドリングとフォールバック戦略
本番環境では、検索失敗・レート制限・タイムアウトなど様々なエラーに対処する必要があります。
多層フォールバックの実装
import Anthropic from "@anthropic-ai/sdk" ;
class ResilientSearchAssistant {
private client : Anthropic ;
constructor () {
this .client = new Anthropic ();
}
async answer ( query : string ) : Promise < string > {
// Layer 1: Web Search + Dynamic Filtering + Citations
try {
return await this . searchWithFullFeatures (query);
} catch (error) {
console. warn ( "Full search failed, falling back..." , error);
}
// Layer 2: Web Search のみ(Dynamic Filtering なし)
try {
return await this . searchBasic (query);
} catch (error) {
console. warn ( "Basic search failed, falling back..." , error);
}
// Layer 3: モデルの知識のみで回答
try {
return await this . answerFromKnowledge (query);
} catch (error) {
console. error ( "All layers failed" , error);
throw new Error ( "Service temporarily unavailable" );
}
}
private async searchWithFullFeatures (
query : string
) : Promise < string > {
const response = await this .client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 8096 ,
tools: [
{
type: "web_search_20260209" ,
name: "web_search" ,
dynamic_filtering: { enabled: true },
max_uses: 5 ,
},
],
citations: { enabled: true },
messages: [{ role: "user" , content: query }],
});
return this . extractText (response);
}
private async searchBasic ( query : string ) : Promise < string > {
const response = await this .client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 4096 ,
tools: [
{
type: "web_search_20260209" ,
name: "web_search" ,
max_uses: 2 ,
},
],
messages: [{ role: "user" , content: query }],
});
return this . extractText (response);
}
private async answerFromKnowledge (
query : string
) : Promise < string > {
const response = await this .client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 4096 ,
system:
"Web検索が利用できないため、あなたの知識に基づいて回答してください。" +
"情報の鮮度に注意し、最新でない可能性がある場合はその旨を明記してください。" ,
messages: [{ role: "user" , content: query }],
});
return this . extractText (response);
}
private extractText ( response : Anthropic . Message ) : string {
return response.content
. filter (( b ) => b.type === "text" )
. map (( b ) => (b as Anthropic . TextBlock ).text)
. join ( "" );
}
}
レート制限への対応
// レート制限を考慮したリクエストキュー
class RateLimitedQueue {
private queue : Array <() => Promise < void >> = [];
private processing = false ;
private requestsPerMinute = 50 ;
private interval = ( 60 / this .requestsPerMinute) * 1000 ;
async enqueue < T >( fn : () => Promise < T >) : Promise < T > {
return new Promise (( resolve , reject ) => {
this .queue. push ( async () => {
try {
const result = await fn ();
resolve (result);
} catch (error) {
// 429エラーの場合は retry-after を尊重
if (
error instanceof Anthropic . APIError &&
error.status === 429
) {
const retryAfter = 60 ; // デフォルト60秒
console. log (
`Rate limited. Retrying after ${ retryAfter }s...`
);
await new Promise (( r ) =>
setTimeout (r, retryAfter * 1000 )
);
try {
const retryResult = await fn ();
resolve (retryResult);
} catch (retryError) {
reject (retryError);
}
} else {
reject (error);
}
}
});
this . processQueue ();
});
}
private async processQueue () {
if ( this .processing) return ;
this .processing = true ;
while ( this .queue. length > 0 ) {
const task = this .queue. shift ();
if (task) {
await task ();
await new Promise (( r ) => setTimeout (r, this .interval));
}
}
this .processing = false ;
}
}
実践ユースケース — 業界別の活用例
ユースケース 1: 技術ドキュメント検索ボット
開発チーム向けに、公式ドキュメントから正確な回答を返す社内ボットを構築する例です。
const techDocBot = new SearchAugmentedAssistant ({
model: "claude-sonnet-4-6" ,
systemPrompt: `あなたは開発チームの技術質問に回答するボットです。
回答は必ず公式ドキュメントを出典として示してください。
公式ドキュメントに記載がない場合は「公式ドキュメントに記載が見つかりませんでした」と回答してください。` ,
domainStrategy: "techDocs" ,
reliabilityThreshold: 0.7 ,
});
// 使用例
const answer = await techDocBot. answer (
"Claude API の Extended Thinking はどのように設定しますか?"
);
// → 公式ドキュメントに基づく正確な回答 + 出典URL
ユースケース 2: リアルタイムニュース分析
最新ニュースを収集し、分析レポートを生成する例です。
const newsAnalyst = new SearchAugmentedAssistant ({
model: "claude-sonnet-4-6" ,
systemPrompt: `あなたはAI業界のニュースアナリストです。
最新のニュースを検索し、以下の形式で分析レポートを作成してください:
1. 要約(3行以内)
2. 主要なポイント
3. 影響分析
4. 出典一覧` ,
domainStrategy: "news" ,
reliabilityThreshold: 0.5 ,
});
const report = await newsAnalyst. answer (
"今週のAnthropic・Claude関連の最新ニュースを分析してください"
);
公式ドキュメントに書かれていない、運用してみて気づいたこと
個人開発で壁紙アプリなどを保守していると、「この SDK の書き方はまだ最新か」「この挙動は先週の更新で変わっていないか」を確かめる場面が地味に多く、そのたびに公式ドキュメントとリリースノートを行き来していました。AdMob SDK の更新でアプリ側の実装が変わっていないかを確認するときも、同じ煩わしさがありました。そこでこの記事で組んだ検索拡張型アシスタントを、自分用の小さな確認ボットとして手元で動かしてみました。運用の中で、ドキュメントには載っていない気づきがいくつかありました。
Dynamic Filtering の効きは、質問の粒度で大きく変わります。 「Claude API の web_search のパラメータ一覧」のような具体的な質問では、フィルタ後に残る検索結果が体感で半分以下になり、入力トークンも目に見えて減りました。一方で「最近の AI の動向」のような漠然とした質問では、フィルタがほとんど絞り込めず、トークン削減はわずかでした。絞り込む基準がはっきりしている質問ほど、フィルタリングの恩恵は大きい。手を動かしてみての実感です。
信頼度スコアの閾値は、用途ごとに分けたほうが破綻しません。 技術ドキュメントの確認では reliabilityThreshold を 0.7 と高めにし、出典が公式ドメインに寄っているときだけ回答を返すようにしたところ、誤った断定が明確に減りました。逆にニュースの要約では 0.7 だと再検索が増えて待ち時間が伸びたため、0.5 に下げています。ひとつの数値で全ユースケースを賄おうとすると、どこかで必ず無理が出ます。
多層フォールバックは「静かに劣化する」ことに価値があります。 検索が失敗したときにエラーを返すのではなく、モデルの知識だけで答えつつ「最新でない可能性があります」と添える。この一言があるだけで、確認ボットとしての信頼が保てました。落ちないことよりも、落ちかけたときにどう振る舞うか。そこに運用の実質が詰まっているように思います。
まとめ
Claude API の Web Search Tool・Dynamic Filtering・Citations API を組み合わせることで、出典付きで正確な回答を返す検索拡張型AIアシスタント を構築できます。GA 版への昇格により安定性が向上し、Dynamic Filtering のコード実行が無料で提供されていることもあり、本番環境での導入ハードルは大きく下がっています。
主要なポイントを振り返ると、Dynamic Filtering でトークン消費を 40〜60% 削減しつつ検索精度を向上させること、Citations API で回答の信頼性を構造的に担保すること、Prompt Caching との組み合わせでさらなるコスト削減が可能なこと、そして多層フォールバック戦略で本番環境の信頼性を確保する点が肝心です。
まずは dynamic_filtering と citations を有効にした最小構成から始め、自分がよく投げる質問で信頼度スコアがどのくらいになるかを測ってみてください。閾値やドメイン戦略は、その実測値を見てから調整するのが結局いちばんの近道です。実装の一助になれば嬉しいです。お読みいただきありがとうございました。
送信前にトークン数を見積もる方法は Claude API Token Counting でコストを最適化する方法 も合わせてご覧ください。