ClaudeのCitations機能をハンズオンで体験しよう：引用の仕組みを徹底理解

## はじめに

<blockquote class="twitter-tweet" data-media-max-width="800"><p lang="ja" dir="ltr">Claude Citations API の挙動を把握するためのハンズオンのノートブック作成しました！！<br>だいぶ処理の中身を理解できた！ <a href="https://t.co/eXKIIAqMMk">https://t.co/eXKIIAqMMk</a> <a href="https://t.co/JMUu0tMSkL">pic.twitter.com/JMUu0tMSkL</a></p>&mdash; Maki@Sunwood AI Labs. (@hAru_mAki_ch) <a href="https://twitter.com/hAru_mAki_ch/status/1883916894056685899?ref_src=twsrc%5Etfw">January 27, 2025</a></blockquote> 

Anthropicの大規模言語モデル Claude には、回答する際に文書からの引用元を自動的に示す「Citations」機能が用意されています。この機能を使うことで、「モデルがどの部分を参照して回答しているのか」を明確に可視化できます。本ハンズオンでは、Google Colab形式のコード例を交えながら、Citationsの仕組みや設定方法をわかりやすく解説します。  

### 対象読者  

- 自前の文書（テキスト・PDFなど）を使ったQAを行いたい方  
- Claudeの引用機能で回答の根拠を明確に示し、ユーザの信頼性を高めたい方  
- 従来のプロンプト制御や埋め込み検索（RAG）と併用し、さらなる精度を目指したい方  

### 到達目標  

- Citations を有効にするためのAPI呼び出し方法を理解する  
- レスポンスに含まれる引用情報の構造（引用テキスト、インデックスなど）を扱えるようになる  
- Plain Text、PDF、Custom Content ドキュメントの違いを理解し、用途に応じて使い分けできるようになる  

それでは早速始めていきましょう！

## 事前準備

### Python環境のセットアップ

まずは Google Colab を開き、必要なPythonライブラリをインストールします。  
例として `requests` を使ったAPI呼び出しと、`base64` を用いたPDFのエンコードを行う場合を想定します。

```python
!pip install requests
```

次に、AnthropicのAPIを呼び出すためにAPIキーを設定します。  
APIキーは、[AnthropicのDeveloper Console](https://console.anthropic.com/)等で取得したものを使用してください。

```python
import os
from google.colab import userdata
# os.environ["ANTHROPIC_API_KEY"] = "YOUR_ANTHROPIC_API_KEY_HERE"
os.environ["ANTHROPIC_API_KEY"] = userdata.get('ANTHROPIC_API_KEY')
```

> **Note**: Colabで実行する場合には、毎回セッションが切れると消えてしまうため、必要に応じて再設定します。



## Citations 機能とは？

AnthropicのClaudeに文章やPDFなどのドキュメントを与え、回答の中でその出典（引用箇所）を細かく示してもらう機能です。例えば以下のような特徴があります。

- **引用箇所のトークン削減**  
  `cited_text` はレスポンスの出力トークンに含まれないため、通常のプロンプトベースのソリューションよりもトークン使用量を削減できる可能性があります。  

- **引用の信頼性向上**  
  モデル内部で標準化されたフォーマットに従い引用を返すため、抽出される引用範囲が文書のどこに存在しているか明確になります。  

- **引用内容の品質向上**  
  単なるプロンプト制御よりも、関連性の高い箇所をより的確に引用してくれるとレポートされています。



## Citations 機能を有効にする手順

AnthropicのAPIにドキュメントを送信する際、JSON形式で `citations.enabled = true` を設定したドキュメントを含めます。

1. **ドキュメントの形式を指定**  
   - Plain Text  
   - PDF (Base64エンコード)  
   - Custom Content (独自に分割単位を指定する場合)

2. **ドキュメントが「チャンク化」される**  
   - Plain Text/PDF は自動的に文章単位(センテンス単位)でチャンク化されます。
   - Custom Content を使うと、こちらで与えたブロック単位でのみ引用が行われます。

3. **Claudeから引用付きの回答が返ってくる**  
   - 複数の `text` ブロックがあり、それぞれに `citations` リストが含まれる場合があります。  
   - PDFならページ番号、テキストなら文字列インデックス、カスタムコンテンツならブロックインデックスで引用箇所が指定されます。

以下で具体的なハンズオンを通して説明していきます。



## Plain Text ドキュメントを使った例

### サンプル文書の用意

ここでは非常にシンプルなテキストを用意します。

```python
document_text = """The grass is green. The sky is blue."""
```

このテキストは2文で構成されています。「草は緑、空は青」という内容ですね。

### APIリクエスト用のJSON作成

上記テキストを「Plain Text ドキュメント」としてAnthropic APIに送ります。  
ここで `citations.enabled = true` を設定することで引用を有効にしています。

```python
import json

payload = {
    "model": "claude-3-5-sonnet-20241022",  # 適宜更新・変更する
    "max_tokens": 1024,
    "messages": [
        {
            "role": "user",
            "content": [
                {
                    "type": "document",
                    "source": {
                        "type": "text",
                        "media_type": "text/plain",
                        "data": document_text
                    },
                    "title": "My Document",
                    "context": "This is a trustworthy document.",
                    "citations": {"enabled": True}
                },
                {
                    "type": "text",
                    "text": "What color is the grass and sky?"
                }
            ]
        }
    ]
}

json_payload = json.dumps(payload, indent=2)
print(json_payload)
```

JSONペイロードの中で、ドキュメントが `type: "document"` として渡されていることや、  
`citations.enabled = true` が設定されていることに注目してください。

### APIを呼び出す

次に、`requests` を使ってAPIリクエストを送ります。  
（`ANTHROPIC_API_KEY` は事前に環境変数に設定済みとします）

```python
import requests
import os

api_url = "https://api.anthropic.com/v1/messages"
headers = {
    "content-type": "application/json",
    "x-api-key": os.environ["ANTHROPIC_API_KEY"],
    "anthropic-version": "2023-06-01"
}

response = requests.post(api_url, headers=headers, data=json_payload)
result = response.json()
result
```

実行後、`result` にはClaudeからの応答がJSON形式で返ってきます。  
Citations 有効の場合、`"content"` フィールドが以下のような形で返ってくる可能性があります。

```json
{
  "content": [
    {
      "type": "text",
      "text": "According to the document, "
    },
    {
      "type": "text",
      "text": "the grass is green",
      "citations": [
        {
          "type": "char_location",
          "cited_text": "The grass is green.",
          "document_index": 0,
          "document_title": "My Document",
          "start_char_index": 0,
          "end_char_index": 20
        }
      ]
    },
    ...
  ]
}
```

上記はあくまでも構造の一例です。実際には回答内容に応じて動的に変わる点に注意してください。



## PDF ドキュメントを使った例

次にPDFファイルを渡す場合です。PDFの文字データをBase64エンコードしてAPIに送る方法になります。  
（スキャン画像PDFのように文字データが無いものは引用できません）

### PDFをBase64エンコード

ColabにサンプルPDFをアップロードしたり、ローカル環境にあるPDFファイルを想定します。

```python
import base64

with open("/content/真実の水面.pdf", "rb") as f:
    pdf_data = f.read()

encoded_pdf = base64.b64encode(pdf_data).decode("utf-8")
```

### PDFドキュメントとしてリクエストJSONを作成

```python
pdf_payload = {
    "model": "claude-3-5-sonnet-20241022",
    "max_tokens": 1024,
    "messages": [
        {
            "role": "user",
            "content": [
                {
                    "type": "document",
                    "source": {
                        "type": "base64",
                        "media_type": "application/pdf",
                        "data": encoded_pdf
                    },
                    "title": "PDF Document Title",
                    "context": "Any additional context if needed",
                    "citations": {"enabled": True}
                },
                {
                    "type": "text",
                    "text": "Please summarize the document with citations."
                }
            ]
        }
    ]
}
```

### APIを呼び出す

```python
pdf_json_payload = json.dumps(pdf_payload, indent=2)
pdf_response = requests.post(api_url, headers=headers, data=pdf_json_payload)
pdf_result = pdf_response.json()
pdf_result
```

PDFの場合は、引用が以下のようにページ番号で表される可能性があります（1-indexed）:

```json
{
  "type": "page_location",
  "cited_text": "Some text from page 1.",
  "document_index": 0,
  "document_title": "PDF Document Title",
  "start_page_number": 1,
  "end_page_number": 2
}
```



## Custom Content ドキュメントを使った例

文章を任意のブロック単位で分割しておき、その粒度で引用をしてもらいたい場合に使います。  
自動チャンク化（文章単位など）を避けたい場合に便利です。

### カスタムブロックの用意

例として、2つの文章をあえて独立ブロックとして定義します。

```python
custom_content = [
    {"type": "text", "text": "First chunk of text."},
    {"type": "text", "text": "Second chunk of text."}
]
```

### Custom Content のリクエストJSON作成

```python
custom_payload = {
    "model": "claude-3-5-sonnet-20241022",
    "max_tokens": 1024,
    "messages": [
        {
            "role": "user",
            "content": [
                {
                    "type": "document",
                    "source": {
                        "type": "content",
                        "content": custom_content
                    },
                    "title": "Document Title",
                    "context": "Additional metadata for the model",
                    "citations": {"enabled": True}
                },
                {
                    "type": "text",
                    "text": "Please cite which block contains the text 'Second chunk of text'."
                }
            ]
        }
    ]
}
```

### API呼び出し

```python
custom_json_payload = json.dumps(custom_payload, indent=2)
custom_response = requests.post(api_url, headers=headers, data=custom_json_payload)
custom_result = custom_response.json()
custom_result
```

レスポンスに引用がある場合、例えば以下のように `content_block_location` が使われます:

```json
{
  "type": "content_block_location",
  "cited_text": "Second chunk of text.",
  "document_index": 0,
  "document_title": "Document Title",
  "start_block_index": 1,
  "end_block_index": 2
}
```

（`start_block_index` ~ `end_block_index` は0-indexed、かつ end は排他的）



## レスポンスの解析

Citations 有効時のレスポンスは、基本的に以下のような大枠の構造をとります。

```json
{
  "completion": "...",
  "stop_reason": "...",
  "content": [
    {
      "type": "text",
      "text": "この文章は～"
    },
    {
      "type": "text",
      "text": "引用部分",
      "citations": [
        {
          "type": "char_location" or "page_location" or "content_block_location",
          "cited_text": "...",
          "document_index": 0,
          ...
        }
      ]
    }
  ],
  ...
}
```

`content` 配列内には複数の要素がありますが、`citations` が付いているのはその一部になります。  
引用情報は、たとえば `result["content"]` を走査して取り出すことが可能です。

### 引用されたテキストと元のドキュメント位置を抽出

```python
def extract_citations(content_blocks):
    extracted = []
    for block in content_blocks:
        if block.get("type") == "text":
            citations = block.get("citations", [])
            for c in citations:
                # ここでは type, cited_text, document_index などを取得
                extracted.append({
                    "block_text": block["text"],
                    "citation_type": c["type"],
                    "cited_text": c["cited_text"],
                    "document_index": c["document_index"],
                    "document_title": c.get("document_title"),
                })
    return extracted

citations_data = extract_citations(result.get("content", []))
for c in citations_data:
    print(c)
```

ここで得られる `cited_text` は、トークン課金には含まれない部分です（追加でお得、といったイメージ）。



## 機能の互換性と留意点

1. **Token費用**  
   Citations を有効にすると、ドキュメント解析のためにわずかに追加トークンを消費しますが、引用部分そのものは出力トークンに数えられません。

2. **バッチ処理やキャッシュ**  
   他のAPI機能（Prompt Caching、Batch APIなど）と組み合わせても動作します。

3. **画像PDFは非対応**  
   テキスト抽出ができないPDFは引用の対象にならない点に注意してください。

4. **タイトルやコンテキストは引用対象にならない**  
   `title` や `context` フィールドはモデルへのヒントとしては利用されますが、そこからの文字列は引用されません。



## まとめ

Citations 機能を使うことで、Claudeが回答の根拠となる原文を明確に示し、また引用テキスト部分のトークンコストを節約できます。  
運用時は以下を意識してください。

- ドキュメントは適切な形式(Plain Text、PDF、Custom Content)を選ぶ。  
- `citations.enabled = true` を忘れずに設定。  
- レスポンスで引用部分をJSONパースして必要な箇所を取得。  

ぜひ自身のアプリケーションやワークフローに応用してみてください。  

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>