Gemini × Google Workspace を業務に組み込む — Docs・Sheets・Drive を横断させる運用と Apps Script 自動化
月初に AdMob の収益 CSV を Sheets へ流し込み、前月比の変化がどこから来たのかを眺める。個人開発でアプリを出していると、この作業が毎月必ず戻ってきます。以前は Sheets のデータをコピーして別タブの Gemini に貼り、返ってきた文章をまた Docs に貼り直していました。往復そのものが億劫で、気づけば数字を見る回数が減っていました。
Workspace 側に Gemini が入ってから変わったのは、この往復が消えたことです。ただ、消えたのは移動の手間であって、判断の手間ではありませんでした。サイドパネルから返ってくる分析は読みやすい日本語で書かれている分、正しそうに見えます。そこを疑わずに月次の判断へ通してしまうと、静かに間違えます。
機能を並べる代わりに、実際に運用へ組み込むときに詰まった三つ——どこまでサイドパネルで済ませるか、どこから API に落とすか、生成された内容をどう検証するか——を順番に書いていきます。
サイドパネルが本当に強いのは「編集させる」とき
Docs の右側に出るサイドパネルは、文章を生成させる道具として語られがちですが、日々の運用で効いているのは生成より編集のほうです。すでにある文書を選択して「この段落を三分の一にまとめてください」と頼むと、その場で本文が置き換わります。生成物をどこかから運んでくる必要がないので、修正の粒度を細かくできます。
粒度を細かくできると、指示の書き方も変わります。文書全体に一度で完璧な指示を投げるのではなく、段落単位で「ここは結論を先に」「ここは具体例を一つ足す」と刻んでいくほうが、結果として速く仕上がります。一発で決めようとして長い指示文を練る時間のほうが、実は高くつきます。
逆に、サイドパネルに向かない作業もはっきりしています。同じ処理を何十件も繰り返すもの、実行結果をログに残したいもの、決まった時刻に人手なしで動かしたいもの。この三つに当てはまったら、その時点で API 側の話だと私は割り切っています。
作業の性質 向いている手段 判断の目安
一度きりの文書整形・言い換え サイドパネル 結果を目で見て採否を決められる
数件のデータから傾向を掴む サイドパネル 数字を自分で再確認できる規模
同じ処理の反復(10件以上) API / Apps Script 手数が線形に増える
定時実行・無人実行 Apps Script トリガー 人が起動しない前提
結果を後から検証したい API 入力・モデル・出力を保存する必要がある
この表は、迷ったときに三十秒で決めるために手元へ置いているものです。線引きを毎回考え直していると、それ自体が作業になります。
Sheets は「聞き方」で返ってくる質が変わる
Sheets 上のデータを選んで分析を頼むとき、いちばん結果が悪いのは「このデータを分析して」という頼み方です。返ってくるのは、平均と増減を撫でただけの文章になります。
改善の方向はひとつで、答えの形をこちらが先に決めておくことです。AdMob の月次データに対して私自身が使っているのは、次のような聞き方です。
「前月比で収益が変化しています。その変化が (a) インプレッション数の増減、(b) eCPM の増減、(c) その両方、のどれで説明できるかを判定し、寄与の大きい順に二つ挙げてください。数値は列から引用してください」
分解の軸をこちらが提示すると、返答は「傾向を語る文章」から「判定結果」に変わります。判定結果であれば、正しいかどうかを自分で確かめられます。撫でただけの文章は、確かめようがありません。
もう一段効くのが、引用の強制です。「数値は列から引用してください」の一文を足すだけで、根拠のない数字が混ざる頻度が目に見えて下がります。混ざったとしても、引用元の列を見に行けば即座に気づけます。
Drive 横断の要約は Apps Script に落とす
Drive のフォルダをまたいで情報を集める作業は、サイドパネルでもできますが、毎月やるなら自動化した方が確実です。手で起動する限り、忙しい月には飛びます。
次の Apps Script は、指定したフォルダ内のテキスト系ファイルを読み、Gemini API に一度で渡して、要約 Doc を新規作成します。Apps Script のプロジェクト設定でスクリプトプロパティに API キーを入れておけば、そのまま動きます。
const MODEL = 'gemini-3-flash' ;
const ENDPOINT =
'https://generativelanguage.googleapis.com/v1beta/models/' +
MODEL + ':generateContent' ;
function summarizeFolder ( folderId , title ) {
const key = PropertiesService. getScriptProperties ()
. getProperty ( 'GEMINI_API_KEY' );
if ( ! key) throw new Error ( 'GEMINI_API_KEY が未設定です' );
const docs = collectText (folderId);
if (docs. length === 0 ) throw new Error ( '対象ファイルが0件です' );
const brief = [
'以下は同一プロジェクトの複数ファイルです。' ,
'重複する記述はまとめ、次の3見出しで整理してください。' ,
'1. 決まったこと 2. 未決の論点 3. 次にやること' ,
'各項目の末尾に、根拠となったファイル名を括弧で必ず添えてください。' ,
]. join ( ' \n ' );
const body = {
contents: [{
role: 'user' ,
parts: [{ text: brief + ' \n\n ' + docs. join ( ' \n\n --- \n\n ' ) }],
}],
generationConfig: { temperature: 0.2 },
};
const res = UrlFetchApp. fetch ( ENDPOINT , {
method: 'post' ,
contentType: 'application/json' ,
headers: { 'x-goog-api-key' : key },
payload: JSON . stringify (body),
muteHttpExceptions: true ,
});
if (res. getResponseCode () !== 200 ) {
throw new Error ( 'API エラー: ' + res. getContentText (). slice ( 0 , 300 ));
}
const parts = JSON . parse (res. getContentText ())
.candidates[ 0 ].content.parts;
const text = parts. map ( function ( p ) { return p.text || '' ; }). join ( '' );
const doc = DocumentApp. create (title + ' 要約 ' +
Utilities. formatDate ( new Date (), 'Asia/Tokyo' , 'yyyy-MM-dd' ));
doc. getBody (). appendParagraph (text);
return doc. getUrl ();
}
function collectText ( folderId ) {
const out = [];
const files = DriveApp. getFolderById (folderId). getFiles ();
while (files. hasNext ()) {
const f = files. next ();
const mime = f. getMimeType ();
if (mime === MimeType. GOOGLE_DOCS ) {
out. push ( '# ' + f. getName () + ' \n ' +
DocumentApp. openById (f. getId ()). getBody (). getText ());
} else if (mime === MimeType. PLAIN_TEXT ) {
out. push ( '# ' + f. getName () + ' \n ' + f. getBlob (). getDataAsString ());
}
}
return out;
}
書き方で意識している点が三つあります。
ひとつめは muteHttpExceptions: true を付けて、ステータスコードを自分で見ていることです。付けないと Apps Script 側が例外で止まり、レスポンス本文が読めません。API が何を怒っているのかが分からないまま再実行を繰り返すことになります。ここは最初につまずいた注意点で、429 と 400 を取り違えて待機処理を入れてしまい、原因の切り分けに半日を溶かしました。
ふたつめは、parts を配列として結合していることです。応答が単一パートで返るとは限らず、parts[0].text の決め打ちは途中で欠けます。
みっつめは、根拠ファイル名を必ず添えるよう指示に入れていることです。要約は読みやすくなるほど検証しにくくなります。出所が本文に残っていれば、疑わしい行だけを元ファイルに当たれます。
本番運用でもう一点、実行時間の上限にも注意が必要でした。Apps Script の実行は無制限ではないため、ファイル数の多いフォルダをそのまま渡すと途中で打ち切られます。回避策は単純で、collectText() が返す配列を 20 件程度で区切り、フォルダを分けて複数回に分けて走らせることです。一度で全部を処理させようとするより、結果的に速く終わります。
生成された内容を業務判断に通す前の検証
要約や分析を業務の判断材料にするなら、最低限の検証を挟みます。私が入れているのは、追加の API 呼び出しを増やさない範囲での二段です。
一段目は、数値の照合です。返答に含まれる数字を正規表現で抜き出し、元データに存在するかを機械的に確かめます。存在しない数字が一つでもあれば、その要約は手動確認に回します。
function unsourcedNumbers ( answer , sourceText ) {
const nums = (answer. match ( / \d[\d,] * ( \. \d + ) ? / g ) || [])
. map ( function ( s ) { return s. replace ( /,/ g , '' ); })
. filter ( function ( s ) { return s. length >= 2 ; });
const src = sourceText. replace ( /,/ g , '' );
const unique = Array. from ( new Set (nums));
return unique. filter ( function ( n ) { return src. indexOf (n) === - 1 ; });
}
二段目は、フォーマット確認です。指示した三つの見出しが揃っているか、根拠の括弧書きが各項目にあるか。ここが崩れているときは、内容も崩れていることが多いというのが手元での傾向でした。
どちらも数行で書けますが、効果は思ったより大きく、月次データを扱う場面では要約をそのまま信じる回数が明確に減りました。完全な品質判定を目指すのではなく、明らかにおかしいものだけを止める。個人で運用する規模では、この割り切りをお勧めします。
個人開発の運用で決めた三つのルール
複数のアプリとブログを一人で回していると、便利な機能ほど運用ルールを決めないと破綻します。統合機能について、私が決めたのは次の三つでした。
第一に、モデル ID は Apps Script の定数一箇所にだけ書きます。Workspace 側の機能はいずれ内部モデルが入れ替わりますが、自分で書いたスクリプトの側は自分で追従するしかありません。散らしておくと、切り替えのたびに探し回ることになります。
第二に、API キーはスクリプトプロパティに置き、コードに書きません。Apps Script は共有が簡単なぶん、コード内の文字列がそのまま共有されます。
第三に、自動生成した Doc には必ず生成日を入れます。半年後に同じフォルダの要約を見返したとき、いつ時点の情報なのかが分からない Doc は、あってもなくても同じでした。App Store と Google Play の審査記録のように、後から時系列で追いたい種類の情報ほど、この一手間が効いてきます。
現時点で期待しすぎないほうがよいこと
統合が進んでも、境界はあります。外部データの API と直接つないで最新値を引くような使い方は、Workspace の中だけでは完結しません。条件分岐が深く入り組んだ計算ロジックの再現も安定しませんでした。行数の非常に多い Sheets を丸ごと対象にすると、処理時間が読めなくなります。
これらは「できない」というより、「別の道具でやったほうが速い」の領域です。無理に一つの環境へ寄せると、かえって手数が増えます。
次の一歩
まず、Drive の中で毎月見返しているフォルダをひとつ選び、上の summarizeFolder() をその ID で一度だけ走らせてみてください。生成された Doc を読んで、括弧内のファイル名を二つか三つ実際に当たってみる。そこで違和感がなければ、そのフォルダはトリガーで自動化して構いません。違和感があれば、指示文の分解の軸を書き直すところからやり直せます。
自動化するかどうかは、便利かどうかではなく、出てきたものを自分で検証できるかどうかで決めています。実装の参考になれば幸いです。