GASでClaude APIの
システムプロンプトで役割と回答ルールを固定する方法

AIの答えが毎回ぶれる、口調が揃わない——その多くは、指示を毎回のuserメッセージに混ぜているのが原因です。役割・口調・ルールはsystem(システムプロンプト)に固定します。GASからの書き方を動くコードで解説します。

|対象: GAS / Claude API / プロンプト設計

Table of Contents

システムプロンプトとは(systemとuserの違い)

Claude APIのリクエストには、system(システムプロンプト)とmessagesという2つの入り口があります。systemはAIの「役割・前提・守るべきルール」を伝える場所で、会話全体の土台になります。messagesの中のuserは「今回してほしい具体的な依頼」です。

system(システムプロンプト)

変わらない指示。役割・口調・禁止事項・出力ルール。毎回の会話の前提になる。

user(ユーザーメッセージ)

その都度変わる依頼。質問文や処理したいデータそのものを入れる。

役割やルールを毎回のuserに混ぜると、質問文の言い回しに引きずられて答えがぶれます。変わらない指示はsystemに、変わるデータはuserに、と分けるのが基本です。

GASでsystemを付けた基本リクエスト

GASからはUrlFetchApp.fetchでClaude APIを呼びます。payloadのトップ階層にsystemを1行足すだけで、AIの役割を固定できます。APIキーはコードに直書きせず、PropertiesService(設定値を安全に保管する仕組み)から読み込みます。

function askClaude() {
  const apiKey = PropertiesService.getScriptProperties().getProperty('CLAUDE_API_KEY');

  const payload = {
    model: 'claude-haiku-4-5',
    max_tokens: 1024,
    // system = AIの役割・前提。毎回の会話の土台になる
    system: 'あなたは自社ECサイトの丁寧なサポート担当です。常に敬体(です・ます)で答えます。',
    // messages = 今回の具体的な依頼
    messages: [
      { role: 'user', content: 'パスワードを忘れました。どうすればいいですか?' }
    ]
  };

  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 json = JSON.parse(res.getContentText());
  Logger.log(json.content[0].text); // AIの回答テキスト
}

ヘッダーのanthropic-versionはAPIのバージョン指定で、必ず付けます。muteHttpExceptions: trueを付けておくと、エラー時もレスポンス本文を読んで原因を確認できます。回答テキストはcontent[0].textに入っています。

役割・口調・ルールを構造化して書く

システムプロンプトは、だらだらと1文で書くより、見出しと箇条書きで整理したほうがAIがルールを取りこぼしません。 「役割」「口調」「守ること」のように区切ると、指示が増えても管理しやすくなります。

// 役割・口調・守ることを見出しと箇条書きで整理する
const SYSTEM_PROMPT = [
  '# 役割',
  'あなたは自社ECサイトのカスタマーサポート担当です。',
  '',
  '# 口調',
  '- 常に敬体(です・ます)で答える',
  '- 専門用語は避け、中学生にも分かる言葉にする',
  '',
  '# 守ること',
  '- 分からないことは推測せず「担当者に確認します」と答える',
  '- 返金や社内規定に関わる約束はしない',
  '- 回答は3文以内にまとめる'
].join('\n');

このように組み立てた文字列を、systemに渡します。処理ごとにuserだけ差し替えられるよう、呼び出し部分を関数にまとめておくと使い回せます。

// systemとuserを分けて渡せる呼び出し関数
function callClaude(systemPrompt, userText) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('CLAUDE_API_KEY');

  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({
      model: 'claude-haiku-4-5',
      max_tokens: 1024,
      system: systemPrompt,                           // 変わらないルール
      messages: [{ role: 'user', content: userText }] // 都度の質問
    }),
    muteHttpExceptions: true
  });

  return JSON.parse(res.getContentText()).content[0].text;
}

function run() {
  const answer = callClaude(SYSTEM_PROMPT, '注文をキャンセルしたいです');
  Logger.log(answer);
}

ルールを書くときは、〜しないより〜するという肯定文のほうが従いやすい傾向があります。「敬語を崩さない」より「常に敬体で答える」と書くイメージです。

例を見せて出力を安定させる(few-shot)

出力の形式(1語で返す、JSONで返すなど)は、言葉で説明するより「手本」を見せるほうが確実です。messagesuserassistantの正解ペアを数組入れる手法を、few-shot(数例提示)と呼びます。

// 手本(正解例)をmessagesに入れて出力の形を安定させる
function classifyInquiry(text) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('CLAUDE_API_KEY');
  const system = '問い合わせを「配送」「返品」「その他」のいずれか1語で分類する。説明や記号は付けない。';

  const messages = [
    // ↓ ここが few-shot(手本)。2往復入れると形が固まる
    { role: 'user', content: '荷物がまだ届きません' },
    { role: 'assistant', content: '配送' },
    { role: 'user', content: 'サイズが合わないので交換したい' },
    { role: 'assistant', content: '返品' },
    // ↓ 実際に分類したい文
    { role: 'user', content: 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({
      model: 'claude-haiku-4-5',
      max_tokens: 20,
      system: system,
      messages: messages
    }),
    muteHttpExceptions: true
  });

  return JSON.parse(res.getContentText()).content[0].text.trim();
}

役割そのものはsystemに置き、「どんな形で返すか」の手本をmessagesに入れる、という分担にすると効果的です。分類・抽出のように答えが決まっている処理ほど、手本が効きます。

長いsystemはプロンプトキャッシュで使い回す

APIは前回の会話を覚えていない(ステートレスな)ため、systemは毎回のリクエストに含める必要があります。用語集やFAQを詰めてsystemが長くなると、その分の入力トークンを毎回支払うことになります。 そこで役立つのがプロンプトキャッシュです。

// 長いsystemは配列にして cache_control を付けると使い回せる
const payload = {
  model: 'claude-haiku-4-5',
  max_tokens: 512,
  system: [
    {
      type: 'text',
      text: LONG_SYSTEM_PROMPT,            // 共通ルール・用語集・FAQなど長い前提
      cache_control: { type: 'ephemeral' } // ここまでをキャッシュ対象にする
    }
  ],
  messages: [{ role: 'user', content: userText }]
};
// 2回目以降、同じsystemを送るとキャッシュヒットで入力料金が下がる

systemを配列にしてcache_controlを付けると、2回目以降は同じ部分が割引価格で再利用されます。スプレッドシートの数百行に同じ長いsystemを回すときほど効果が大きくなります。 詳しくはプロンプトキャッシュの記事で解説しています。

スプレッドシートの一括処理に組み込む

システムプロンプトの真価は、同じルールを大量のデータに適用するときに出ます。 スプレッドシートに並んだ問い合わせを1行ずつ読み、共通のsystemで返信下書きを作る例です。

// スプレッドシートの問い合わせに、同じルールで一括返信下書き
function replyDraftForRows() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const last = sheet.getLastRow();
  if (last < 2) return;
  const values = sheet.getRange(2, 1, last - 1, 2).getValues(); // A:本文 B:状態

  const system = [
    'あなたは丁寧な日本語のサポート担当です。',
    '敬体で、3文以内、謝意→対応→次の案内の順で返信文の下書きを書く。',
    '断定できない社内情報には触れない。'
  ].join('\n');

  values.forEach((row, i) => {
    const inquiry = row[0]; // A列: 問い合わせ本文
    const done = row[1];    // B列: 生成済みフラグ
    if (!inquiry || done) return; // 空行・処理済みはスキップ

    const draft = callClaude(system, inquiry); // 前掲のcallClaudeを再利用
    sheet.getRange(i + 2, 3).setValue(draft);  // C列に下書きを書き込み
    sheet.getRange(i + 2, 2).setValue('done'); // 二重生成を防ぐ
    Utilities.sleep(500); // レート制限対策に軽く間隔を空ける
  });
}

全行に同じルールが効くため、返信のトーンが揃います。状態列(B列)で処理済みを記録すると、途中で止まっても続きから再開でき、二重生成も防げます。 件数が多く6分の実行時間制限に触れそうなときは、時間主導トリガーで小分けに回します。

指示に従わないときのコツと注意点

肯定文で、ルールは数を絞る

禁止事項を並べすぎると、かえって守られにくくなります。本当に外せないルールに絞り、〜するという肯定文で書きます。

形式は言葉より手本で示す

出力形式が崩れるときは、systemの説明を増やすより、前掲のfew-shotで正解例を1〜2組見せるほうが早く安定します。 それでも崩れると困る処理では、Structured Outputsでスキーマを固定します。

systemに機密や外部由来の文章を混ぜない

systemはあくまで自社が決めた固定ルールを置く場所です。ユーザーが入力した文章や外部から取り込んだテキストはuser側に入れ、systemと混ぜないようにします。指示とデータを分けておくと、想定外の入力に振り回されにくくなります。

systemだけで安全性は保証されない

「返金の約束はしない」と書いても、100%守られる保証はありません。返信は下書きとして人が確認する、金額に関わる回答は自動送信しない、といった運用面の歯止めも併せて設計します。

まとめ

AIの答えを揃える鍵は、指示とデータを分けることです。役割・口調・ルールはsystemに固定し、その都度変わる質問やデータはuserに入れます。systemは見出しと箇条書きで整理し、形式はfew-shotの手本で見せると安定します。長くなったらプロンプトキャッシュ、形式を厳密にしたいならStructured Outputsと組み合わせれば、スプレッドシートの大量処理でもぶれない出力が得られます。

よくある質問

systemは「AIの前提や役割・守るべきルール」を伝える場所で、毎回の会話の土台になります。userメッセージは「今回してほしい具体的な依頼」です。役割や口調、禁止事項などの変わらない指示はsystemに、その都度変わる質問文や対象データはuserに入れる、と役割を分けると、指示がぶれにくくなります。

問題ありません。日本語で役割・口調・ルールを書けば、日本語で自然に従います。むしろ日本語の敬体(です・ます)で答えさせたい業務では、systemも日本語で「常に敬体で答える」と明記したほうが安定します。箇条書きや見出しで整理して書くと、AIがルールを取りこぼしにくくなります。

3つ試してください。1つ目は「〜しない」より「〜する」と肯定文で書く。2つ目は守ってほしいルールを箇条書きにして数を絞る。3つ目はmessagesに正解例(手本)を1〜2往復入れる(few-shot)ことです。特に出力形式の指定は、言葉で説明するより実例を見せるほうが効きます。

APIはステートレス(前回の会話を覚えていない)なので、毎回のリクエストにsystemを含める必要があります。ただしsystemが長い場合は、プロンプトキャッシュを使うと2回目以降は同じ部分を割引価格で使い回せます。systemを配列にしてcache_control: { type: 'ephemeral' } を付けるだけです。

systemで「JSONだけを返す」と指示し、messagesに正解例を入れる方法でもかなり安定します。より確実にしたい場合は、Structured Outputs(構造化出力)を使うとスキーマどおりのJSONが保証されます。分類やデータ抽出のように形式が崩れると困る処理では、後者が安全です。

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

問い合わせ対応・分類・要約など、Claude APIとGASを組み合わせた自動化をご相談いただけます。プロンプト設計から運用まで対応します。

AI×GAS自動化を見る