GASでClaude APIのCitationsで
根拠付き回答を作る方法

社内文書をAIに読ませて質問に答えさせると、必ず「その回答、どこに書いてあるの?」という確認が発生します。Claude APIのCitations(引用)機能を使うと、回答のどの一文がどの文書のどこを根拠にしているかを、API側が構造化データで返してくれます。GASからは追加ライブラリなしで使えます。

|対象: GAS / Claude API / 社内文書QA / UrlFetchApp

Table of Contents

Citationsとは何か・プロンプトで頼む方法との違い

Citationsは、渡した文書のどの部分を根拠に回答したかを、API側が構造化データで返す機能です。回答文が複数のtextブロックに分割され、根拠のあるブロックにだけcitationsという配列が付きます。

プロンプトで頼む方式

「引用元も書いて」と指示する。回答文の中に引用が文章として混ざるため、機械的に切り出しにくい

Citations機能

引用箇所がJSONの配列で返る。原文・文書番号・位置がそのまま取れるので、シートやUIに機械的に流し込める

検証のしやすさ

cited_textは渡した文書の実際の文字列。元文書と突き合わせて確認できる

根拠なしの検知

citationsが空なら、文書に無い内容を答えた可能性が高いとコードで判定できる

社内規程の問い合わせ対応、マニュアルの検索、契約書の確認など「根拠の提示が必須」な業務で効きます。GASと組み合わせれば、スプレッドシートに質問を書くだけで回答と出典が埋まる仕組みが作れます。

リクエストの書き方|documentブロックとcitations

やることは2つだけです。文書をtype: "document"のブロックとして渡し、そのブロックにcitations: { enabled: true }を付けます。GASからはUrlFetchAppでJSONを投げるだけで、専用ライブラリは不要です。

// Citationsを有効にした最小リクエスト
function askWithCitations() {
  const apiKey = PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY");

  const manual = [
    "第1条 有給休暇は入社6か月後から付与される。",
    "第2条 有給休暇の申請は取得日の3営業日前までに行うこと。",
    "第3条 慶弔休暇は有給休暇とは別に付与される。",
  ].join("\n");

  const payload = {
    model: "claude-opus-4-8",
    max_tokens: 1000,
    messages: [
      {
        role: "user",
        content: [
          {
            type: "document",
            // プレーンテキストの文書はこの形で渡す
            source: { type: "text", media_type: "text/plain", data: manual },
            title: "就業規則(休暇)",
            // これが引用機能のスイッチ。1リクエスト内では全文書で揃える
            citations: { enabled: true },
          },
          { type: "text", text: "有給休暇はいつから使えますか?根拠も示してください。" },
        ],
      },
    ],
  };

  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true, // エラーでも例外にせずコードで判定する
  });

  if (res.getResponseCode() !== 200) {
    Logger.log("失敗: " + res.getResponseCode() + " / " + res.getContentText());
    return;
  }

  Logger.log(res.getContentText());
}

押さえておくべき決まりが3つあります。1つ目、同じリクエスト内のdocumentブロックは全部有効か全部無効かに揃えること。混在させるとエラーになります。2つ目、titleを付けておくと、レスポンスのdocument_titleにそのまま返ってくるので出典表示が楽になること。3つ目、documentブロックは質問テキストより前に置くのが素直な並びであることです。

APIキーはコードに直接書かず、PropertiesService(スクリプトごとの設定保管庫)から読み込みます。詳しくはGASでAPIキーを安全に保存する方法を参照してください。

レスポンスの構造|textブロックとcitations配列

Citationsを有効にすると、返ってくるcontentの形が変わります。通常はtextブロック1つに全文が入りますが、Citations有効時は文単位に近い粒度で複数のtextブロックに分割され、根拠のあるブロックにだけcitationsが付きます。

// レスポンスの構造(抜粋・イメージ)
{
  "content": [
    {
      "type": "text",
      "text": "有給休暇は入社から6か月後に付与されます。",
      "citations": [
        {
          "type": "char_location",          // テキスト文書なら文字位置
          "cited_text": "第1条 有給休暇は入社6か月後から付与される。",
          "document_index": 0,               // 何番目のdocumentブロックか
          "document_title": "就業規則(休暇)",
          "start_char_index": 0,
          "end_char_index": 28
        }
      ]
    },
    {
      "type": "text",
      "text": "なお申請期限は別途定められています。"
      // 根拠のない文にはcitationsが付かない
    }
  ]
}

位置情報の形は文書の種類で変わります。プレーンテキストならchar_locationstart_char_index/end_char_index)、PDFを渡した場合はpage_locationstart_page_number/end_page_number、1始まり)です。コードではtypeを見て分岐させます。

そして最も重要なのがcited_textです。ここには渡した文書の実際の文字列がそのまま入るため、元文書と照合すれば引用が本物かどうかを機械的に検証できます。

GASで解析する|本文と引用リストに分ける

textブロックが分割されるため、json.content[0].textだけを見ると回答が途中で切れます。全ブロックを走査して連結するのが正しい読み方です。

// レスポンスを解析し、本文と引用リストに分けて取り出す
function parseCitations(json) {
  const body = [];      // 回答本文
  const citations = []; // 引用の一覧

  json.content.forEach(function (block) {
    if (block.type !== "text") return;
    body.push(block.text);

    // citationsは根拠のあるブロックにだけ付く。無い場合は undefined
    (block.citations || []).forEach(function (c) {
      citations.push({
        text: block.text,
        source: c.document_title || "文書" + c.document_index,
        quote: c.cited_text,
        // 位置情報の形はtypeで変わるので分岐する
        where:
          c.type === "page_location"
            ? "p." + c.start_page_number + "-" + c.end_page_number
            : c.type === "char_location"
            ? c.start_char_index + "〜" + c.end_char_index + "文字目"
            : "",
      });
    });
  });

  return { answer: body.join(""), citations: citations };
}

citationsは根拠のあるブロックにしか付かないので、block.citations || []のように未定義を吸収してから回します。この関数を通せば、あとは本文と引用リストを好きな形で表示・保存するだけです。

スプレッドシートの質問に根拠付きで回答する

実務で一番使える形がこれです。「社内文書」シートに規程やマニュアルを、「質問」シートに問い合わせを入れておき、実行すると回答と出典が埋まる仕組みを作ります。

// 質問シートを読み、根拠付きで回答してシートに書き戻す
function answerQuestionsWithSources() {
  const apiKey = PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY");
  const ss = SpreadsheetApp.getActive();
  const docs = loadDocuments(ss.getSheetByName("社内文書"));
  const sheet = ss.getSheetByName("質問");

  const last = sheet.getLastRow();
  if (last < 2) return;

  // A列:質問 / B列:回答 / C列:出典 の想定
  const rows = sheet.getRange(2, 1, last - 1, 3).getValues();

  rows.forEach(function (row, i) {
    const question = String(row[0] || "").trim();
    if (!question || row[1]) return; // 未入力・回答済みは飛ばす

    const json = callClaudeWithDocs(apiKey, docs, question);
    if (!json) return;

    const parsed = parseCitations(json);

    // 出典は「文書名: 引用文(位置)」の形で1セルにまとめる
    const sources = parsed.citations
      .map(function (c) {
        return c.source + ": 「" + c.quote + "」(" + c.where + ")";
      })
      .join("\n");

    sheet.getRange(i + 2, 2).setValue(parsed.answer);
    sheet.getRange(i + 2, 3).setValue(sources || "根拠なし(要確認)");

    Utilities.sleep(500); // 連続呼び出しを少し緩める
  });
}

// 社内文書シート(A列:タイトル / B列:本文)をdocumentブロックの配列にする
function loadDocuments(sheet) {
  const last = sheet.getLastRow();
  if (last < 2) return [];

  return sheet
    .getRange(2, 1, last - 1, 2)
    .getValues()
    .filter(function (r) {
      return String(r[1] || "").trim() !== "";
    })
    .map(function (r) {
      return {
        type: "document",
        source: { type: "text", media_type: "text/plain", data: String(r[1]) },
        title: String(r[0] || "無題"),
        citations: { enabled: true },
      };
    });
}

function callClaudeWithDocs(apiKey, docs, question) {
  const payload = {
    model: "claude-opus-4-8",
    max_tokens: 1500,
    system:
      "あなたは社内規程の案内担当です。渡された文書に書かれている内容だけで回答し、" +
      "書かれていない場合は「文書に記載がありません」と答えてください。",
    messages: [
      {
        role: "user",
        // documentブロックを先に、質問テキストを後ろに置く
        content: docs.concat([{ type: "text", text: question }]),
      },
    ],
  };

  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });

  const code = res.getResponseCode();
  if (code !== 200) {
    Logger.log("APIエラー " + code + ": " + res.getContentText());
    return null;
  }
  return JSON.parse(res.getContentText());
}

システムプロンプトで「文書に書かれている内容だけで答える」と明示しているのがポイントです。Citationsは引用箇所を返す機能であり、文書外の知識で答えることを禁止する機能ではありません。指示とセットで使ってはじめて「社内文書に基づく回答」になります。

質問が多い場合はGASの6分の実行時間制限に注意してください。処理済みの行を飛ばす作りにしておけば、時間切れになっても次回の実行で続きから再開できます。

複数文書を渡してdocument_indexで出典を特定する

documentブロックは複数渡せます。document_indexcontentに並べたdocumentブロックの順番(0始まり)なので、元の配列と突き合わせればどの規程が使われたかが分かります。

// 複数文書を渡し、document_indexで出典を突き止める
function askAcrossDocuments(question) {
  const apiKey = PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY");

  // 配列の並び順がそのまま document_index(0始まり)になる
  const docs = [
    { title: "就業規則", body: "有給休暇は入社6か月後から付与される。" },
    { title: "経費規程", body: "交通費の申請は月末締め、翌月5日までに提出する。" },
    { title: "情報セキュリティ規程", body: "社外PCへの業務データ保存は禁止する。" },
  ];

  const payload = {
    model: "claude-opus-4-8",
    max_tokens: 1200,
    messages: [
      {
        role: "user",
        content: docs
          .map(function (d) {
            return {
              type: "document",
              source: { type: "text", media_type: "text/plain", data: d.body },
              title: d.title,
              citations: { enabled: true },
            };
          })
          .concat([{ type: "text", text: question }]),
      },
    ],
  };

  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() !== 200) return;

  const json = JSON.parse(res.getContentText());

  // どの規程が使われたかを集計する
  const used = {};
  json.content.forEach(function (block) {
    (block.citations || []).forEach(function (c) {
      const name = docs[c.document_index] ? docs[c.document_index].title : "不明";
      used[name] = (used[name] || 0) + 1;
    });
  });

  Logger.log(used); // 例: { 経費規程: 2 }
}

「経費の質問なのにセキュリティ規程が引用されている」といったズレも、この集計で見つけられます。どの文書が実際に使われているかを記録しておくと、渡す文書を絞り込む判断材料にもなります。

根拠ゼロの回答を検知して人のチェックに回す

Citationsの副次的な、しかし実務で最も価値のある使い方が「引用が付かなかった回答を機械的に見つける」ことです。文章はあるのにcitationsが1件もない回答は、文書に無い内容を答えている可能性があります。

// 引用が付かない回答を検知して、人のチェックに回す
function needsHumanReview(json) {
  let hasText = false;
  let hasCitation = false;

  json.content.forEach(function (block) {
    if (block.type !== "text") return;
    if (block.text.trim()) hasText = true;
    if (block.citations && block.citations.length > 0) hasCitation = true;
  });

  // 文章はあるのに根拠がゼロ = 文書に無い内容を答えた可能性がある
  return hasText && !hasCitation;
}

function notifyIfUnsupported(json, question) {
  if (!needsHumanReview(json)) return;

  MailApp.sendEmail({
    to: "review@example.com",
    subject: "[要確認] 根拠なしのAI回答が発生しました",
    body: "質問: " + question + "\n\n社内文書に根拠が見つからない回答です。内容を確認してください。",
  });
}

このチェックを挟むだけで、AI回答をそのまま顧客や社員に出す運用のリスクが大きく下がります。通知先をSlackにしたい場合は、GASからSlackに自動通知する方法の実装をそのまま差し替えれば動きます。

実務での注意点(制約・トークン・精度)

1. 構造化出力(JSON強制)とは併用できない

Citationsを有効にしたままoutput_configのフォーマット指定を使うとエラーになります。JSONで受け取りたい処理と根拠が欲しい処理は、リクエストを分けて設計してください。

2. 文書はまとめて渡さず、必要な分だけ渡す

社内文書を全部渡せば精度が上がるわけではありません。トークン数(=コストと処理時間)が増えるうえ、無関係な文書が引用されるノイズも増えます。章・節の単位で分割し、質問に関係しそうなものだけを渡すのが現実的です。同じ文書を毎回渡すならプロンプトキャッシュの併用でコストを抑えられます。

3. 引用が付いていても内容の正しさは保証されない

Citationsが保証するのは「その一文が文書のこの部分に対応している」という紐付けまでです。解釈が正しいかは別問題で、たとえば古いバージョンの規程を渡していれば、正しく引用された誤った回答が返ってきます。渡す文書の鮮度管理は人間側の責任です。

4. エラー処理は必ず入れる

muteHttpExceptions: trueとレスポンスコードの判定はセットです。リクエスト過多で429が返る場合は、待ち時間を延ばしながら再試行する仕組みを入れておくと、夜間バッチが途中で止まりません。

まとめ

GASからClaude APIのCitationsを使う手順は単純です。文書をdocumentブロックで渡し、citations: { enabled: true }を付けて、返ってきた複数のtextブロックを走査するだけです。 覚えておくべきは3点。回答は分割されるので全ブロックを連結すること、document_indexは渡した順番であること、そして引用ゼロの回答は要注意フラグとして使えることです。 「AIの回答は根拠が分からないから業務に使えない」という壁を、実装レベルで越えられる機能です。

よくある質問

渡した文書のどの部分を根拠に回答したかを、APIが構造化データで返してくれる機能です。回答文が複数のtextブロックに分割され、根拠のあるブロックにはcitations配列が付きます。配列には引用した原文(cited_text)と、何番目の文書か(document_index)、文書のタイトル(document_title)、そして文字位置などの位置情報が入ります。AIに「引用元も書いて」とお願いする方法と違い、位置情報がAPI側で付与されるため、存在しない引用が作られにくいのが利点です。

messagesのcontentにdocumentブロックを入れ、そのブロックに citations: { enabled: true } を付けます。documentブロックのsourceには、プレーンテキストなら { type: "text", media_type: "text/plain", data: 本文 } を指定します。注意点として、1つのリクエスト内のdocumentブロックは全部有効にするか全部無効にするかのどちらかにする必要があります。混在させることはできません。

文書の種類によって変わります。プレーンテキストの文書ではtypeがchar_locationになり、start_char_indexとend_char_indexで文字位置が返ります。PDFを渡した場合はpage_locationになり、start_page_numberとend_page_number(1始まり)でページ番号が返ります。コードで扱うときはtypeを見て分岐させるのが安全です。

使えません。Citationsを有効にしたリクエストでoutput_configのformat指定を併用すると400エラーになります。JSON形式の結果が欲しい場合は、Citations付きで回答を受け取ってからGAS側で組み立てるか、引用が不要な処理と分けて2回呼び出す設計にしてください。

全文をそのまま渡すとトークン数が増え、コストと実行時間が膨らみます。実務では、スプレッドシートやDrive上の文書をあらかじめ章・節の単位に分けておき、質問に関係しそうな数件だけをdocumentブロックとして渡す形が現実的です。同じ文書を繰り返し使うならプロンプトキャッシュの併用も検討してください。

根拠を示せるAI活用を
相談する。

社内規程やマニュアルをAIに読ませ、出典付きで回答する仕組みをGASで構築します。文書の分割設計から運用フローまでまとめて対応します。

AI×GAS自動化を見る