GASでClaude APIの回答が
途中で切れる原因と対処

GASからClaude APIを呼ぶと、回答が文の途中でぷつっと切れることがあります。原因の多くはmax_tokens(応答の出力トークンの上限)です。stop_reasonでの検知と、続きを自動生成する継続リクエストで最後まで取り切る方法を、動くコードで解説します。

|対象: GAS / Claude API / max_tokens

Table of Contents

なぜ回答が途中で切れるのか

Claude APIのリクエストにはmax_tokens(1回の応答で生成できる出力トークンの上限)という必須項目があります。生成した出力がこの上限に達すると、 文の途中でも生成が止まります。これが「回答が途中で切れる」正体です。

厄介なのは、上限に達してもエラーにはならない点です。HTTPステータスは200のまま、 途中までの正しい文が返ってきます。そのため気づかずに「短い回答しか返らない」「JSONが壊れている」と 悩みがちです。まずは切れた原因を判定できるようにするのが第一歩です。

max_tokensが小さい

長文生成やまとめに対して上限が不足し、途中で打ち切られる

出力量を読み違えた

入力は短くても、要約・翻訳・生成の出力が想定より長くなる

一覧をまとめて出力

複数件を1回で出力させ、後半が上限に達して欠落する

JSONで受け取っている

途中で切れると閉じ括弧が無く、JSON.parseが失敗する

stop_reasonで切れたかを判定する

レスポンスJSONのstop_reason(生成が止まった理由)を見れば、途中で切れたかどうかがわかります。正常に完了したときはend_turn、上限で切れたときはmax_tokensになります。まずは全リクエストでこの値を確認しましょう。

// Claude APIを1回呼ぶ最小の関数
function callClaude(messages, maxTokens) {
  const url = "https://api.anthropic.com/v1/messages";
  const apiKey = PropertiesService
    .getScriptProperties()
    .getProperty("CLAUDE_API_KEY");

  const payload = {
    model: "claude-opus-4-8",
    max_tokens: maxTokens, // 応答で生成できる出力トークンの上限(必須)
    messages: messages,
  };

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

  return JSON.parse(res.getContentText());
}

上の共通関数を使い、stop_reasonをチェックします。本文はテキストブロックだけを連結して取り出すのが安全です。

// stop_reason を見て、途中で切れたかを判定する
function askAndCheck(prompt) {
  const data = callClaude(
    [{ role: "user", content: prompt }],
    1024
  );

  // end_turn = 正常終了 / max_tokens = 上限で途中で切れた
  const isTruncated = data.stop_reason === "max_tokens";
  if (isTruncated) {
    Logger.log("回答がmax_tokensで切れました。続きの生成が必要です。");
  }

  // テキストブロックだけを連結して本文を取り出す
  const text = data.content
    .filter(function (block) { return block.type === "text"; })
    .map(function (block) { return block.text; })
    .join("");

  return { text: text, truncated: isTruncated };
}

max_tokensの適切な決め方

一番シンプルな対処はmax_tokensを余裕のある値にすることです。ただし入力トークン数は事前に数えられても、出力が何トークンになるかは実行するまでわかりません。 そのため「絶対に切れない値」を狙うのではなく、想定に合わせて決め、足りないときは検知して対処する前提にします。

// max_tokens はモデルごとの出力上限までしか指定できない
// (例)claude-opus-4-8 / sonnet 系は最大128,000、haiku 系は最大64,000
//   ※ ただし非常に大きな値は非現実的(後述のGAS制約を参照)

const payload = {
  model: "claude-opus-4-8",
  // 短い分類・抽出なら小さく、長文生成なら大きめに
  max_tokens: 4096,
  messages: [{ role: "user", content: prompt }],
};

// ポイント:
// ・入力の長さは count_tokens で事前に測れるが、
//   「出力が何トークンになるか」は事前にわからない
// ・そのため max_tokens は余裕を持って決め、
//   足りないときは stop_reason で検知して継続リクエストする

目安として、分類やタグ付けなど短い出力なら数百、要約や文章生成なら数千程度から始め、切れるようなら増やします。 入力側の見積もりはcount_tokensエンドポイントで正確に測れます(数え方は関連記事を参照)。

続きを自動生成させる(継続リクエスト)

出力量が読めない処理では、max_tokensを上げるより「切れたら続きを生成させる」ほうが確実です。切れた時点までの回答をassistantロールとして会話に積み、その後に「続きを出力して」というuserメッセージを足して再送します。

// 途中で切れたら「続き」を生成させ、最後まで取得する
function askClaudeComplete(prompt, maxRounds) {
  maxRounds = maxRounds || 5; // 無限ループ防止の上限
  const messages = [{ role: "user", content: prompt }];
  let fullText = "";

  for (let i = 0; i < maxRounds; i++) {
    const data = callClaude(messages, 2048);

    const partial = data.content
      .filter(function (b) { return b.type === "text"; })
      .map(function (b) { return b.text; })
      .join("");
    fullText += partial;

    // max_tokens 以外で止まったら完了
    if (data.stop_reason !== "max_tokens") break;

    // ここまでの回答を assistant として積み、続きを促す
    // (assistant を最後のメッセージにすると別の制約に触れるため、
    //   必ず後ろに user メッセージを足す)
    messages.push({ role: "assistant", content: partial });
    messages.push({
      role: "user",
      content: "先ほどの回答が途中で切れました。続きだけを出力してください。",
    });
  }

  return fullText;
}

ポイントはassistantメッセージを最後にしないことです。途中まで生成した回答を最後のメッセージ (プレフィル)として送る書き方は、現在のモデルでは受け付けられずエラーになります。上記のように必ず後ろに userメッセージを足せば、正常に続きが返ってきます。無限ループを避けるため、繰り返し回数の上限 (maxRounds) も必ず設けましょう。

GAS特有の制約|ストリーミングとタイムアウト

「じゃあmax_tokensを最大まで上げればいい」とはならないのがGASです。Claude API自体は大きな出力 (モデルにより最大128,000トークンなど)に対応しますが、公式にはストリーミング(逐次受信)が前提です。 一方でGASのUrlFetchAppはストリーミング応答に対応しておらず、生成が完了するまでレスポンスを待ち続けます

1. 応答待ちが長くなる

max_tokensを極端に大きくすると、その分だけ生成に時間がかかり、UrlFetchAppの応答待ちが長くなります。 長大な出力を1回で取り切ろうとせず、適度なmax_tokens+継続リクエストに分けるほうが安定します。

2. 6分の実行時間制限

GASには1回の実行あたり約6分の制限があります。継続リクエストを何度も回すような処理では、 この6分に収まるようにするか、途中経過を保存して次の実行に引き継ぐ設計が必要です。

JSONが途中で切れたときの安全な処理

AIの回答をJSONで受け取りJSON.parseする処理では、途中で切れると閉じ括弧が欠けた不完全な文字列になり、parseが例外を投げます。 parseの前にstop_reasonを確認し、max_tokensなら「不完全」として扱うのが安全です。

// 途中で切れたJSONを、そのままparseしない
function parseJsonSafely(data) {
  if (data.stop_reason === "max_tokens") {
    // JSONが閉じられていない可能性が高い
    throw new Error(
      "出力がmax_tokensで切れたため、JSONが不完全です。" +
      "max_tokensを増やすか、1件ずつ処理してください。"
    );
  }

  const text = data.content
    .filter(function (b) { return b.type === "text"; })
    .map(function (b) { return b.text; })
    .join("");

  return JSON.parse(text); // ここまで来れば安全にparseできる
}

JSONの途中欠けは継続リクエストでの復元が難しいため、この場合はmax_tokensを増やすか、 一度に処理する件数を減らして「1レスポンスに1件分」に収める設計が確実です。 スキーマで出力形式を固定する構造化出力を使う場合も、max_tokensで切れたときは無効なJSONになりうる点は同じなので、 stop_reasonの確認は欠かせません。

実務での注意点

1. stop_reasonは毎回ログに残す

切れているのに気づかず後続処理に流すのが最悪のパターンです。stop_reasonをログや作業シートに記録しておくと、 「なぜか出力が短い」といった不具合の原因追跡が一気に楽になります。

2. 出力を短くする指示も有効

プロンプトで「200字以内で」「箇条書き3点で」と長さを指定すると、そもそも切れにくくなります。 max_tokensは安全網、プロンプトでの長さ指定は一次対策と考えると設計しやすくなります。

3. 継続リクエストは料金が増える

続きを生成するたびに、それまでの会話を再送するため入力トークンが積み上がります。 長文を大量に扱う場合は、継続リクエストではなく最初から処理単位を小さく分割するほうが、 料金・実行時間の両面で有利です。

まとめ

GASでClaude APIの回答が途中で切れる主因はmax_tokensで、切れたかどうかはstop_reasonで判定できます。 対処は「max_tokensを適切に決める」「切れたら続きを生成する継続リクエスト」「JSONはparse前にstop_reasonを確認」の3点が基本です。 GASではストリーミングが使えず6分制限もあるため、1回で取り切ろうとせず、適度な単位に分けて回すのが安定運用のコツです。

よくある質問

多くの場合、リクエストで指定したmax_tokens(1回の応答で生成できる出力トークンの上限)に達したためです。上限に達すると、文の途中でも生成が止まり、レスポンスのstop_reasonが「max_tokens」になります。max_tokensは必須項目で、これを小さくしすぎると長い回答が最後まで出力されません。

レスポンスJSONのstop_reason(生成が止まった理由)を見ます。正常に終わった場合は「end_turn」、上限で切れた場合は「max_tokens」になります。JSON.parseした結果のstop_reasonが「max_tokens」かどうかを確認すれば、途中で切れたことをコードで検知できます。

できます。切れた時点までの回答(テキスト)をassistantロールとしてmessages配列に追加し、その後に「続きだけを出力してください」というuserメッセージを足して再度リクエストします。stop_reasonがmax_tokensでなくなるまでこれを繰り返せば、長い出力も分割して最後まで取得できます。

ある程度は解決しますが、GAS特有の制約に注意が必要です。UrlFetchAppはストリーミング応答に対応しておらず、生成完了までレスポンスを待つため、max_tokensを非常に大きくすると応答待ちが長くなります。さらにGASには1回あたり約6分の実行時間制限もあります。必要な長さに合わせて適度な値にし、足りない分は継続リクエストで補うのが安全です。

危険です。stop_reasonがmax_tokensの場合、JSONが閉じられていない不完全な文字列になり、JSON.parseが例外を投げます。構造化された出力を受け取るときは、parseの前にstop_reasonを確認し、max_tokensなら「不完全」として扱い、max_tokensを増やすか出力を分割してください。

GAS×AIの業務自動化を
相談する。

Claude APIとGASを組み合わせた要約・分類・生成の自動化から、実行時間制限やAPI制約の設計まで、 実務で動く仕組みづくりをご相談いただけます。

AI×GAS自動化を見る