ClaudeのCitationsは引用形式がPDF・テキスト・カスタムで変わる
Claude CitationsはPDFならページ番号、プレーンテキストなら文字インデックス、カスタムコンテンツならブロックインデックスと、ドキュメント種別ごとに引用の返り方が変わります。実装の落とし穴も解説します。
Citationsが返す引用形式はドキュメント種別で変わる
Claude APIのCitations機能は、documentブロックのcitations.enabledをtrueにするだけで、Claudeの回答に根拠テキストと出典位置を自動で付与します。ここで見落とされがちなのが、出典位置の表現形式がドキュメントの種類によって完全に別物になるという仕様です。PDFはページ番号、プレーンテキストは文字位置、カスタムコンテンツはブロック番号で返ります。3つの形式を混同したままレスポンスをパースすると、正しく動いているのに座標がずれて見える不具合を作り込みます。
対応モデルは現行のアクティブなモデル全てです。ドキュメントはメッセージに直接埋め込めるほか、Files APIにアップロードしてIDを発行してもらい、そのIDで参照する方法も使えます。発行されるIDはfile_idというフィールド名で渡します。テキストの引用のみが対象で、画像からの引用にはまだ対応していません。この制約を先に押さえておくと、後述する3形式それぞれの違いが理解しやすくなります。
引用元にできるのはsourceの中身だけ
documentブロックにはsource(引用対象の本文)のほか、titleとcontextという補助フィールドを付けられます。この2つはClaudeのモデルには渡りますが、引用元にはなりません。titleは長さに制限があるため、ドキュメントの出典URLや取得日時のようなメタデータをテキストやJSON文字列で持たせたいときはcontextに入れるのが正しい使い方です。「なぜこの一文だけ引用されないのか」を調べるときは、まず該当箇所がsourceではなくtitleやcontextに置かれていないかを確認します。
もう1つ見落としやすいのが、1リクエスト内でCitationsのON/OFFを混在させられないという制約です。複数のdocumentブロックを渡す場合、すべてにcitations.enabled: trueを付けるか、すべてに付けないかのどちらかにします。一部だけ有効化しようとするとリクエストが成立しません。
PDFの引用はページ番号(1-indexed)で返る
PDFドキュメントはテキストが抽出され、文単位にチャンク分割されます。画像として保存されたスキャンPDFはテキストを抽出できないため引用対象になりません。返ってくる引用はpage_location型で、start_page_numberとend_page_numberという1始まりのページ番号を持ちます。
{
"type": "page_location",
"cited_text": "Water is essential for life.",
"document_index": 1,
"document_title": "PDF Document",
"start_page_number": 5,
"end_page_number": 6
}file_id経由でPDFを渡す場合は次のようにリクエストします。
curl https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": "'"$FILE_ID"'"},
"citations": {"enabled": true}
},
{"type": "text", "text": "Summarize this document."}
]
}]
}'プレーンテキストの引用は文字インデックス(0-indexed)で返る
プレーンテキストドキュメントも文単位でチャンク分割されますが、返ってくる引用はchar_location型です。start_char_indexとend_char_indexは0始まりの文字位置で、ドキュメント全体の文字列に対するオフセットになります。
{
"type": "char_location",
"cited_text": "The grass is green.",
"document_index": 0,
"document_title": "Example Document",
"start_char_index": 0,
"end_char_index": 20
}RAG(検索拡張生成)のチャンクをそのまま引用させたい場合は、チャンクごとに1つのプレーンテキストドキュメントとして渡すと、Claudeはチャンク内の特定の文を狙って引用できます。RAGパイプライン全体の組み方はClaude RAGの構築ガイドで扱っています。
カスタムコンテンツの引用はブロックインデックス(0-indexed)で返る
カスタムコンテンツドキュメントだけは自動チャンク分割が入らず、渡したcontent配列の要素がそのままチャンク単位になります。返ってくる引用はcontent_block_location型で、start_block_indexとend_block_indexは渡した配列の0始まりのインデックスです。
{
"type": "document",
"source": {
"type": "content",
"content": [
{"type": "text", "text": "First chunk"},
{"type": "text", "text": "Second chunk"}
]
},
"citations": {"enabled": true}
}箇条書きや会話ログのように、文単位のチャンク分割では粒度が粗すぎたり細かすぎたりするドキュメントに向いています。渡した配列の要素数だけ引用の最小単位が決まるので、粒度をこちらで完全に制御できます。
自動チャンク分割とカスタムコンテンツ、どちらを選ぶか
プレーンテキストとPDFは既定で文単位に自動チャンク分割されます。これで足りるのは、Claudeが1文単位、または複数の文を連結した段落単位で引用してくれれば十分な場合です。最小単位を自分で決めたいときは、カスタムコンテンツを選びます。
RAGのチャンクをそのまま引用させたい場合、渡し方は2通りあります。チャンクの中でさらに文単位の引用を許してよいなら、チャンクごとに1つのプレーンテキストドキュメントとして渡し、自動チャンク分割に任せます。チャンクをそれ以上分割してほしくない、あるいは分割ロジックを自分で完全にコントロールしたいなら、チャンクをカスタムコンテンツドキュメントのcontent配列の要素として渡します。どちらも最終的には、APIが自動でチャンクを行うか、渡した配列をそのまま使うかの違いに帰着します。カスタムコンテンツ側は追加の分割が一切かからないため、配列の要素数がそのまま引用の解像度になります。細かく割るほど引用箇所を狭く指せる一方、どこで割るかの設計はこちらの責任になります。
インデックスの終端は含まない — 半開区間の落とし穴
3形式に共通する仕様が1つあります。開始側は含み、終了側は含まない半開区間です。文字インデックスとブロックインデックスは終了側が排他的(exclusive)、ページ番号も終了ページが排他的です。上のプレーンテキスト例なら、引用範囲は文字20を含まず0〜19文字目を指します。
この仕様を見落とすと、引用範囲をハイライト表示するUIで最後の1文字やブロックが欠けたり、逆に1つ余分に含んでしまったりします。境界値のオフバイワンは実装時に必ず確認したい箇所です。単体テストを書くなら、開始と終了が同じ値になる境界ケースを必ず含めておくと安心です。ハイライト表示のロジックを共通化するなら、この半開区間の扱いをユーティリティ関数に切り出しておくと、3形式それぞれで同じミスを繰り返さずに済みます。
ドキュメント自体を指すdocument_indexも3形式共通の仕様です。そのリクエストに含まれる全メッセージのdocumentブロックを、通しで0始まりに数えます。マルチターンの会話で会話履歴をまるごと送り直す実装では、ターン1で渡したドキュメントも数に入るため、2ターン目以降で履歴の一部を間引いて送るとdocument_indexがずれます。ドキュメントを何番目として扱うかは、この「全メッセージを通した通し番号」を基準にコード側で管理する必要があります。
3形式が混在した応答を実際に見る
複数のドキュメントを同じリクエストに渡すと、Claudeの応答はtextブロックの配列になり、根拠にした箇所を含むブロックだけにcitations配列が付きます。地の文と引用付きの文が交互に並ぶ形です。プレーンテキスト・PDF・カスタムコンテンツを1つのリクエストに混ぜて渡した場合、レスポンスの中にchar_location・page_location・content_block_locationの3種類が同居することもあります。パース処理を実装するときは、citations配列の各要素をtypeフィールドで分岐させ、3形式それぞれに対応するフィールド名(start_char_index/start_page_number/start_block_index)を個別に読み取る必要があります。「引用があればとりあえずstart_char_indexを見る」という実装は、PDFやカスタムコンテンツの引用が混ざった瞬間に壊れます。
ストリーミングを使う場合も同様です。引用はcontent_block_deltaイベントの中にcitations_deltaという専用のdelta型で届き、1つのdeltaには常に1件の引用だけが入ります。現在構築中のtextブロックのcitations配列に、届いたdeltaを1件ずつ追記していく実装になります。
ドキュメントタイプの選び方 — 早見表
| タイプ | 向いている用途 | チャンク分割 | 引用形式 |
|---|---|---|---|
| プレーンテキスト | 向いている用途素のテキスト・RAGチャンクの引用 | チャンク分割自動(文単位) | 引用形式文字インデックス(0-indexed) |
| 向いている用途テキストを含むPDFファイル | チャンク分割自動(文単位) | 引用形式ページ番号(1-indexed) | |
| カスタムコンテンツ | 向いている用途箇条書き・会話ログなど粒度を自分で決めたい場合 | チャンク分割なし(渡した配列そのまま) | 引用形式ブロックインデックス(0-indexed) |
.docxや.xlsxのようにdocumentブロックが直接扱えない形式は、プレーンテキストに変換してから渡します。すでにプレーンテキストの.csvや.mdはtext/plainを明示してFiles APIにアップロードできます。
Citationsと組み合わせて使える機能
Citationsはトークンカウント・バッチ処理と併用できます。引用を有効にするとシステムプロンプトへの追加とチャンク処理の分だけ入力トークンがわずかに増えますが、出力側は逆に効率的です。Claudeは内部で標準化された形式で引用位置を出力し、それをAPI側がcited_textとインデックスにパースします。cited_textフィールドは便宜上付与されるだけで出力トークンには数えられず、次のターンに渡すときも入力トークンにはカウントされません。
プロンプトキャッシュとも組み合わせられますが、キャッシュされる対象には注意が必要です。応答に含まれる引用ブロックそのものは直接キャッシュできません。キャッシュできるのは引用元のドキュメントの方なので、documentブロックにcache_controlを付けてキャッシュするのが正しい設計です。長大な技術文書を毎ターン渡すような使い方では、このcache_controlの有無がコストを大きく左右します。
一方で、Citationsはstructured outputsとは併用できません。documentまたはsearch_resultブロックでCitationsを有効にした状態でoutput_config.formatも指定すると400エラーになります。引用ブロックをテキストに挟み込む仕組みが、JSON Schemaへの厳密な制約と両立しないためです。
まとめ
Citationsの引用形式は、PDFがページ番号(1-indexed)、プレーンテキストが文字インデックス(0-indexed)、カスタムコンテンツがブロックインデックス(0-indexed)と、ドキュメント種別ごとに完全に別の形式で返ります。共通するのは、終了側を含まない半開区間である点と、document_indexがそのリクエストに含まれる全メッセージのdocumentブロックを通して0始まりで数えられる点です。titleやcontextは引用元にならないこと、1リクエスト内でCitationsのON/OFFを混在させられないことも、実装でつまずきやすい細部として押さえておく価値があります。
どのドキュメントタイプを選ぶかは、自動チャンク分割に任せてよいか、引用の最小単位を自分で制御したいかで決まります。レスポンスをパースする実装では、この3形式の違いを型(page_location / char_location / content_block_location)で分岐させるのが最も事故が少ない書き方です。ストリーミングでも同じ型情報がcitations_deltaに乗って届くため、パース処理はストリーミング・非ストリーミングの両方で共通化できます。