GASでClaude APIの
マルチターン会話(会話履歴)を実装する方法
Claude APIはリクエストごとに独立していて、前の発言を覚えていません。前後の文脈を踏まえて何度も往復する 「マルチターン会話」を実現するには、これまでのやり取りをmessages配列に積んで毎回送る必要があります。その仕組みと履歴の保存・間引きを動くコードで解説します。
Table of Contents
AIは前の会話を覚えていない
まず前提を押さえます。Claude APIはステートレス(リクエストごとに独立して、前の状態を持たない)です。 サーバー側に会話の記憶はありません。下のコードのように1回ずつ質問すると、 「私の名前はタナカです」と伝えた次のリクエストで「私の名前は?」と聞いても答えられません。別々の呼び出しだからです。
// 1回きりの質問。前後のやり取りは覚えていない
function askOnce(prompt) {
const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY"),
"anthropic-version": "2023-06-01",
},
payload: JSON.stringify({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
}),
});
return JSON.parse(res.getContentText()).content[0].text;
}
// 「私の名前はタナカです」→「こんにちは」
// 次に「私の名前は?」と聞いても、別リクエストなので答えられない解決策はシンプルです。これまでの全やり取りを毎回まとめて送ること。 AIに「覚えさせる」のではなく、リクエストのたびに過去の発言を渡して「思い出させる」のがマルチターン会話の本質です。
messages配列にuserとassistantを積む
会話はmessages配列で表現します。ユーザーの発言はrole: "user"、 AIの返答はrole: "assistant"として、時系列で交互に並べます。
[ { role: "user", content: "私の名前はタナカです。" }, { role: "assistant", content: "こんにちは、タナカさん。" }, { role: "user", content: "私の名前は何でしたか?" } ]
このように過去のassistantの返答も含めて渡すと、AIは3番目のuser発言に対して「タナカさんです」と文脈を踏まえて答えられます。 新しい質問を送るたびに、配列の末尾へuserとassistantのペアが積み上がっていきます。
1往復ずつ会話を進める関数
履歴配列を受け取り、ユーザー入力を足して送信し、返答を履歴に積んで返す関数を1つ作ります。system(システムプロンプト=AIの役割や口調を固定する指示)はmessagesとは別枠で渡します。
// 会話履歴(messages配列)を渡して1ターン進める
function chatTurn(history, userInput, systemPrompt) {
// 今回のユーザー発言を末尾に追加
history.push({ role: "user", content: userInput });
const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY"),
"anthropic-version": "2023-06-01",
},
payload: JSON.stringify({
model: "claude-haiku-4-5",
max_tokens: 1024,
system: systemPrompt, // 人格・ルールは履歴とは別枠で固定
messages: history, // これまでの全やり取りを渡す
}),
muteHttpExceptions: true,
});
const answer = JSON.parse(res.getContentText()).content[0].text;
// AIの返答も履歴に積む(次のターンで文脈になる)
history.push({ role: "assistant", content: answer });
return answer;
}呼び出す側は、同じhistory配列を渡し続けるだけです。関数の中で履歴が更新されるので、2ターン目以降は前の文脈が自動的に効きます。
function demoConversation() {
const system = "あなたは丁寧語で答える社内アシスタントです。";
const history = [];
chatTurn(history, "私の名前はタナカです。", system);
const reply = chatTurn(history, "私の名前は何でしたか?", system);
Logger.log(reply); // → 「タナカさんです」のように前の発言を踏まえて答える
// history には user/assistant が交互に4件積まれている
}roleの順番を崩さない注意点
messagesには並び順のルールがあります。ここを崩すとAPIがエラーを返します。
userとassistantを交互に
同じroleを2回続けて並べるのは不可。userの後は必ずassistant、その後またuser
最後はuserで終える
AIに返答させる直前の配列は、末尾がuserであること。assistantで終えたまま送れない
systemはmessagesに入れない
systemはトップレベルの別パラメータ。roleとしてmessagesに混ぜない
空のcontentを送らない
内容が空の発言を積むとエラーの原因。入力が空なら送信前に弾く
特にハマりやすいのが、履歴を間引いたときに先頭がassistantになってしまうケースです。 次のセクションの間引き処理では、先頭が必ずuserになるよう調整しています。
会話履歴をスプレッドシートに保存・復元する
GASの実行は1回ごとに終わり、変数の中身は消えます。会話を続けるには履歴をどこかに保存し、 次の実行で読み戻す必要があります。ユーザーごとに履歴を分けたい社内チャットや問い合わせ対応では、 スプレッドシートに1行1発言で記録するのが分かりやすい方法です。
// 会話履歴をユーザー単位でスプレッドシートに保存・復元する
const HISTORY_SHEET = "会話ログ"; // 列: [0]userId, [1]role, [2]content, [3]時刻
function loadHistory(userId) {
const sheet = SpreadsheetApp.getActive().getSheetByName(HISTORY_SHEET);
const rows = sheet.getDataRange().getValues().slice(1);
return rows
.filter((r) => r[0] === userId)
.map((r) => ({ role: r[1], content: r[2] }));
}
function appendHistory(userId, role, content) {
const sheet = SpreadsheetApp.getActive().getSheetByName(HISTORY_SHEET);
sheet.appendRow([userId, role, content, new Date()]);
}
// 使い方: 保存済み履歴を読み込み → 1ターン進め → 増えた2件を保存
function chatForUser(userId, userInput) {
const system = "あなたは丁寧語で答える社内アシスタントです。";
const history = trimHistory(loadHistory(userId), 10);
const answer = chatTurn(history, userInput, system);
appendHistory(userId, "user", userInput);
appendHistory(userId, "assistant", answer);
return answer;
}1ユーザーの短い会話だけなら、PropertiesServiceやCacheServiceに履歴をJSON文字列で保存する方法も手軽です。用途に合わせて保存先を選びましょう。
履歴を間引いてトークン上限を防ぐ
会話が長くなるほど、毎回送るトークン(AIが文章を扱う最小単位)が増えます。 料金が上がるだけでなく、いずれコンテキスト上限に達してエラーになります。 対策の基本は、直近のNターンだけ残して古い発言を間引くことです。
// 直近maxTurnターン(user+assistantのペア)だけ残して古い発言を間引く
function trimHistory(history, maxTurn) {
const maxMessages = maxTurn * 2; // 1ターン = user1 + assistant1
if (history.length <= maxMessages) return history;
let trimmed = history.slice(history.length - maxMessages);
// 先頭は必ずuserから始める(assistantで始まるとエラー)
if (trimmed.length > 0 && trimmed[0].role !== "user") {
trimmed = trimmed.slice(1);
}
return trimmed;
}先頭がuserから始まるように調整しているのがポイントです。 単純に末尾からN件切り出すと先頭がassistantになることがあり、その状態で送るとエラーになるためです。 古い文脈も残したい場合は、間引くのではなく古い部分をAIで要約して1件のメッセージにまとめる方法もあります。
実務での注意点
1. 人格・ルールはsystemに置く
口調や禁止事項をuser発言に混ぜると、履歴の間引きで消えてしまいます。 systemは履歴とは別枠で毎回渡されるので、一貫した人格やルールはsystemに書きましょう。
2. ユーザーをまたいで履歴を混ぜない
複数人が使う場合、userIdなどで履歴を必ず分けます。 取り違えると他人の会話がAIに渡り、情報漏えいや的外れな回答の原因になります。
3. 固定の前提はプロンプトキャッシュで節約する
長いsystemやマニュアルを毎回送るなら、プロンプトキャッシュを併用すると入力コストを抑えられます。 会話履歴のうち変わらない先頭部分をキャッシュ対象にするのが効果的です。
まとめ
Claude APIのマルチターン会話は、messages配列にuserとassistantを交互に積み、毎回まとめて送ることで実現します。 GASでは実行ごとに変数が消えるため、履歴をスプレッドシートやPropertiesServiceに保存して読み戻すのが要点です。 履歴が伸びたら直近Nターンに間引き(先頭は必ずuser)、人格はsystemで固定する。 この型を押さえれば、社内チャットや問い合わせ対応の下地が作れます。
よくある質問
Claude APIはリクエストごとに独立して処理され、サーバー側で会話状態を保持しません。前のやり取りを踏まえた返事をさせるには、これまでの発言(userとassistant)をすべてmessages配列に入れて毎回送る必要があります。「覚えさせる」のではなく「毎回思い出させる」イメージです。
userとassistantを交互に並べ、最後は必ずuserで終わるようにします。ユーザーの発言(user)とAIの返答(assistant)をペアで積んでいき、新しい質問をuserとして末尾に足してから送信します。roleが2回続いたり、assistantで終わったまま送るとエラーになります。
1ユーザーの短い会話ならPropertiesServiceやCacheServiceが手軽です。ユーザーごとに履歴を分けたい場合や後から見返したい場合は、スプレッドシートに1行1発言で記録するのがおすすめです。GASの実行は1回ごとに終わるため、どこかに保存しておかないと次の実行で履歴が消えてしまいます。
履歴が伸びるほど毎回送るトークン数が増え、料金が上がり、いずれコンテキスト上限に達してエラーになります。対策として、直近のNターンだけ残して古い発言を間引く、または古い部分をAIで要約して1つのメッセージにまとめる方法があります。要点を残しつつ長さを抑えるのがコツです。
systemパラメータ(システムプロンプト)に役割・口調・禁止事項を書きます。systemはmessages配列とは別枠で、会話履歴を間引いても消えないため、毎回同じ人格を保てます。「丁寧語で答える」「社内用語を使う」などのルールはsystemに置くのが定石です。
関連するサービス・記事
Claude API×GASの
社内チャット・自動化を相談する。
会話履歴の設計から社内マニュアルを踏まえた問い合わせ対応まで、Claude APIとGASを組み合わせた自動化をご相談いただけます。