GASで長文をAIで分割要約する方法
map-reduce要約
1回のリクエストに収まらない長文は、map-reduce方式で扱います。文章を分割し、各パートを部分要約(map)してから1つに統合(reduce)する実装を、動くGASコードで解説します。
Table of Contents
なぜ長文はそのままAIに渡せないのか
数万文字の議事録や書き起こし、長いドキュメントを一度にAIへ渡そうとすると、うまくいかないことがあります。理由は主に3つです。
1. コンテキスト上限を超える
コンテキスト上限(1回のリクエストで送れる入力トークンの上限)を超えると、そもそも受け付けてもらえずエラーになります。
2. 料金と時間が増える
上限内でも、入力が長いほど入力トークン数が増えて料金がかさみ、応答も遅くなります。
3. 要点を取りこぼしやすい
入力が長すぎると、全体をまんべんなく扱いきれず、後半の重要な点が薄まることがあります。
この3つをまとめて解決するのが、文章を分割してから要約するmap-reduce方式です。もともとは大量データを「分割して処理し、後でまとめる」考え方で、長文要約と相性が良い方法です。
map-reduce要約の全体像
処理は大きく3ステップです。長文を扱いやすい大きさに分け、各パートを個別に要約し、その要約をさらに1つにまとめます。
Flow
ポイントは、部分要約(map)では情報を落としすぎないこと、統合(reduce)では重複を除いて読みやすくまとめることです。役割が違うので、後述するようにプロンプトも分けます。
長文を意味のまとまりで分割する
まず長文をチャンク(分割の1かたまり)に分けます。文の途中でぶつ切りにすると意味が壊れるため、段落(空行)の切れ目でまとめながら、目安の文字数を超えたら次のチャンクに送ります。
/**
* 長文を意味のまとまり(段落)を保ったまま分割する。
* @param {string} text 元の長文
* @param {number} chunkSize 1チャンクあたりの目安文字数
* @return {string[]} 分割後のチャンク配列
*/
function splitIntoChunks(text, chunkSize) {
const size = chunkSize || 6000;
// 空行(段落の切れ目)で区切る
const paragraphs = text.split(/\n\s*\n/);
const chunks = [];
let current = '';
paragraphs.forEach(function(para) {
const p = para.trim();
if (!p) return;
// 1段落だけで上限を超える場合は、そのまま1チャンクにする
if (p.length > size) {
if (current) { chunks.push(current); current = ''; }
chunks.push(p);
return;
}
// 今のチャンクに足すと超えるなら、区切って次のチャンクへ
if (current && (current + '\n\n' + p).length > size) {
chunks.push(current);
current = p;
} else {
current = current ? current + '\n\n' + p : p;
}
});
if (current) chunks.push(current);
return chunks;
}分割のコツ
文字数はあくまで目安です。正確にトークン数で管理したい場合は、Claude APIのcount_tokensで実際の入力トークン数を数えてから調整すると安全です。空行がない書き起こしなどは、句点や発言の切れ目で区切るなど、対象に合わせてルールを変えてください。
各チャンクを部分要約する(map)
分割したチャンクを1つずつ要約します。まずは Claude API を呼び出す共通関数を用意します。APIキーはPropertiesServiceに保存し、コードに直書きしません。
const API_URL = 'https://api.anthropic.com/v1/messages';
const MODEL = 'claude-sonnet-5';
/**
* Claude API を1回呼び出してテキスト回答を受け取る。
*/
function callClaude(systemPrompt, userText, maxTokens) {
const apiKey = PropertiesService
.getScriptProperties()
.getProperty('CLAUDE_API_KEY'); // APIキーはコードに直書きしない
const payload = {
model: MODEL,
max_tokens: maxTokens || 1024,
system: systemPrompt,
messages: [{ role: 'user', content: userText }]
};
const res = UrlFetchApp.fetch(API_URL, {
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();
const body = JSON.parse(res.getContentText());
if (code !== 200) {
const msg = body.error ? body.error.message : res.getContentText();
throw new Error('Claude API エラー(' + code + '): ' + msg);
}
// content は配列。テキストブロックの text を取り出す
return body.content[0].text.trim();
}この共通関数を使い、各チャンクを部分要約します。あとで統合する素材なので、数字・固有名詞・決定事項を落とさないようにプロンプトで指示するのがポイントです。
/**
* 各チャンクを部分要約する(map)。
* 後で統合するための素材なので、要点を落とさないよう指示する。
*/
function summarizeChunks(chunks) {
const system =
'あなたは日本語の要約アシスタントです。' +
'渡された文章は長い文書の一部です。' +
'あとで全体を1つにまとめるための素材として、' +
'数字・固有名詞・決定事項を落とさずに5行以内で要約してください。' +
'前置きや「以下が要約です」などの説明は書かないでください。';
return chunks.map(function(chunk, i) {
const summary = callClaude(system, chunk, 512);
Utilities.sleep(500); // レート制限に配慮して少し待つ
Logger.log('部分要約 ' + (i + 1) + '/' + chunks.length + ' 完了');
return '【パート' + (i + 1) + '】\n' + summary;
});
}なぜ Utilities.sleep を入れるのか
短時間に大量に呼び出すと、レート制限(429エラー)になりやすくなります。Utilities.sleep()で少し間隔をあけると安定します。本格的にはリトライ処理と組み合わせます(後述)。
部分要約を1つに統合する(reduce)
部分要約が出そろったら、それらをつなげてもう一度AIに渡し、全体を1つの要約にまとめます。ここでは重複を除いて読みやすくする役割なので、mapとは別のプロンプトを使います。
/**
* 部分要約をまとめて1つの最終要約にする(reduce)。
*/
function reduceSummaries(partialSummaries) {
const system =
'あなたは日本語の要約アシスタントです。' +
'パートごとに要約された内容が渡されます。' +
'重複を除いて全体を1つの要約に統合し、' +
'「概要」「重要ポイント(箇条書き)」「結論」の' +
'3部構成で分かりやすくまとめてください。';
const joined = partialSummaries.join('\n\n');
return callClaude(system, joined, 1500);
}部分要約はすでに短くなっているため、統合の入力は元の長文よりずっと小さくなります。これで、長文全体をコンテキスト上限に収めながら要約できます。
全体を通すメイン関数と使い方
分割・map・reduce をつなぐメイン関数です。チャンクが1つに収まる短い文章なら、分割せずそのまま要約する分岐も入れておきます。
/**
* 長文を map-reduce 方式で要約するメイン関数。
*/
function summarizeLongText(text) {
const chunks = splitIntoChunks(text, 6000);
Logger.log('チャンク数: ' + chunks.length);
// 1チャンクに収まるなら分割せずそのまま要約する
if (chunks.length <= 1) {
const system =
'あなたは日本語の要約アシスタントです。渡された文章を' +
'「概要」「重要ポイント」「結論」の3部構成で要約してください。';
return callClaude(system, chunks[0] || text, 1500);
}
const partials = summarizeChunks(chunks); // map
const finalSummary = reduceSummaries(partials); // reduce
return finalSummary;
}
/**
* 使い方の例:Googleドキュメントを読み取って要約をシートに書き出す。
*/
function run() {
const doc = DocumentApp.openById('ここにドキュメントIDを入れる');
const text = doc.getBody().getText();
const summary = summarizeLongText(text);
SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('要約')
.getRange('A1')
.setValue(summary);
}上のrun()はGoogleドキュメントを読み取る例です。対象はGmailの長文本文、スプレッドシートのセル、Driveのテキストファイルなど、テキストにできるものなら何でも同じ流れで要約できます。
実務での注意点
1. 6分の実行時間制限に注意
GASの1回の実行は最大6分(Workspaceアカウントは30分)です。チャンクが数十個あると、部分要約の呼び出しだけで超えることがあります。対処は、Message Batches APIでまとめて投げる方法や、処理位置を保存して時間主導トリガーで続きから再開する分割実行です。
2. 部分要約が多すぎるときは階層要約にする
チャンクが非常に多いと、部分要約をつなげただけで統合の入力が大きくなりすぎることがあります。そのときは、部分要約をいくつかのグループに分けて中間要約を作り、それをさらにまとめる「階層要約」にします。
/**
* 部分要約が多すぎて統合1回では収まらないときの階層要約。
* 部分要約を groupSize 個ずつまとめ、まとまりが1つになるまで繰り返す。
*/
function reduceHierarchically(partials, groupSize) {
let current = partials;
const size = groupSize || 8;
while (current.length > size) {
const next = [];
for (let i = 0; i < current.length; i += size) {
const group = current.slice(i, i + size);
next.push(reduceSummaries(group)); // グループごとに中間要約
}
current = next;
}
return reduceSummaries(current); // 最後に1つへ統合
}3. 料金は「チャンク数+1回」で見積もる
API呼び出し回数は部分要約のチャンク数に統合の1回を足した数です。事前に見積もりたい場合はcount_tokensで入力トークンを数え、モデルの単価を掛けて概算します。呼び出しのたびにusageを記録しておくと、実際の消費も追えます。
4. 429エラーにはリトライで備える
連続でAPIを呼ぶと、レート制限(429)や一時的なエラーが起きることがあります。Utilities.sleep()の間隔調整に加えて、失敗したら待ち時間を伸ばして再試行する指数バックオフを入れると安定します。
まとめ
長文はそのまま渡すのではなく、分割してから要約するmap-reduce方式で扱うと、コンテキスト上限・料金・精度の3つの問題をまとめて回避できます。要点は次のとおりです。
split(分割)
段落など意味の切れ目で目安の文字数ごとに分ける
map(部分要約)
数字・固有名詞・決定事項を落とさず各パートを要約
reduce(統合)
重複を除いて1つの読みやすい要約にまとめる
応用できる業務用途
よくある質問
3つ問題があります。1つ目はコンテキスト上限(1回のリクエストで送れる入力トークンの上限)を超えるとエラーになること。2つ目は上限内でも入力が長いほど料金と応答時間が増えること。3つ目は入力が長すぎると要点の取りこぼしが起きやすくなることです。そこで文章を分割し、部分ごとに要約してから1つにまとめる map-reduce 方式が有効です。
本記事のコードでは1チャンク6000文字を目安にしています。小さすぎると呼び出し回数が増えて料金と実行時間がかさみ、大きすぎると1回あたりの精度が落ちます。文字数はあくまで目安なので、正確に管理したい場合はClaude APIのcount_tokensで実際のトークン数を数えて調整してください。また、文の途中で切ると意味が壊れるため、段落(空行)などの意味の切れ目で分割するのがコツです。
部分要約の段階で情報が削られるため、1回で全文を渡す場合より細部が落ちることはあります。対策として、部分要約のプロンプトで「数字・固有名詞・決定事項は残す」と明示し、要約を強くしすぎないことが有効です。それでも長文をエラーで処理できないよりは、分割して全体像をつかめる方が実務では役立ちます。
GASの1回の実行は最大6分(Workspaceアカウントは30分)です。チャンクが数十個あると超えることがあります。対処は主に2つです。1つはClaude APIのMessage Batches APIでまとめて投げ、結果を後から取りに行く方法。もう1つは処理済みの位置をPropertiesServiceに保存し、時間主導トリガーで続きから再開する分割実行です。
API呼び出し回数は「チャンク数+統合の1回」です。10チャンクなら11回呼び出します。各回の料金は入力・出力トークン数×モデル単価で決まります。部分要約は入力が長く出力が短い、統合要約は入力が短く出力がやや長い、という配分になります。事前に見積もりたい場合はcount_tokensで入力トークンを数え、モデルの単価を掛けて概算してください。
使えます。ただし話し言葉は空行がない場合があるため、段落で分割できないことがあります。その場合は句点(。)や一定の文字数で区切る、または発言者の切り替わりで区切るなど、対象に合わせた分割ルールに変えてください。分割の考え方(意味の切れ目で分ける)は同じです。
GAS×AIの業務自動化を
相談する。
長文要約・議事録整理・問い合わせ分類など、GASとAIを組み合わせた自動化の開発をご相談いただけます。 内容が固まっていなくても大丈夫です。現在の業務・課題をお聞きした上でご提案します。