App Store と Google Play に4本のアプリを並行で出していると、リリースのたびに地味に時間を奪われるのがリリースノートの多言語対応です。先日 v2.1.0 を段階公開したとき、英語で下書きしたノートを各言語に直し、ストアごとの文字数制限に収め、トーンを揃える作業に小一時間かかりました。コードを書く時間より、こうした周辺作業の方が体感では長く感じます。
6/15 に Gemini API 経由で antigravity-preview-05-2026(Managed Antigravity Agent)が公開プレビューになったので、この退屈な工程をエージェントに任せる小さなパイプラインを組んでみました。普通の generate_content 呼び出しと何が違い、どこで詰まったのかを残しておきます。
なぜ単発の生成呼び出しではなく Managed Agent なのか
最初は素直に「コミットログを渡して generate_content で各言語のノートを書かせればいい」と思っていました。実際にやってみると、出てくる文章は悪くないのですが、Google Play の「最新情報」は半角500文字相当という制限があり、生成結果がそこを超えると自分で削る作業が戻ってきます。結局、文字数オーバーのたびにプロンプトを書き直して再生成する、という手戻りが発生しました。
Managed Agent が単発の生成と決定的に違うのは、サンドボックス内で計画・推論・コード実行・ファイル操作・ウェブ閲覧を自律的に回せる点です。私の用途では「コミットを分類する」「各言語に訳す」「文字数を数えて制限に収める」という複数工程があり、エージェントが自分で文字数を数えて、超えていたら自分で削るループ を回してくれるのが効きました。人間が制限を監視して差し戻す役割を、そのままエージェントに移譲できる感覚です。
ここで言う「単発生成」と「エージェント」の役割分担の考え方は、Managed Agents API のクラウドとローカルの境界 で整理した内容と地続きです。本記事はその実装版だと思ってください。
全体の流れ
組んだパイプラインは次の4段です。ストアへの実際の投入だけは、必ず人間が最終確認してから行います。エージェントの自律性は文章生成までに留め、公開ボタンは自分で押す、という線引きにしています。
呼び出し側で git log を構造化して抽出する
Managed Agent に「コミット要約+制約」を渡し、計画・分類・多言語生成・文字数検証を任せる
エージェントから JSON で受け取る
受け取った JSON を呼び出し側でも再検証し、制限超過があれば差し戻す
Step 1: コミットログを構造化して取り出す
まず注意したいのが、ローカルの git リポジトリはエージェントのサンドボックスから直接は見えない ことです。最初これに気づかず「リポジトリを見て」と指示してしまい、エージェントが空の作業ディレクトリを覗いて混乱しました。ログは呼び出し側で抽出し、テキストとして渡すのが正解です。
import subprocess
import json
def extract_commits (since_tag: str ) -> list[ dict ]:
"""直近タグからの commit を Conventional Commits 風に分類して返す。"""
# %s=件名, %b=本文 を区切り文字付きで取得
fmt = " %s%x 1f %b%x 1e"
raw = subprocess.run(
[ "git" , "log" , f " { since_tag } ..HEAD" , f "--pretty=format: { fmt } " ],
capture_output = True , text = True , check = True ,
).stdout
commits = []
for entry in raw.split( " \x1e " ):
entry = entry.strip()
if not entry:
continue
subject, _, body = entry.partition( " \x1f " )
# feat: / fix: / perf: などの prefix を type として拾う
ctype = "other"
if ":" in subject:
head = subject.split( ":" , 1 )[ 0 ].strip().lower()
if head in { "feat" , "fix" , "perf" , "refactor" , "chore" , "docs" }:
ctype = head
commits.append({ "type" : ctype, "subject" : subject, "body" : body.strip()})
return commits
if __name__ == "__main__" :
items = extract_commits( "v2.0.0" )
# ユーザーに見せたいのは feat / fix / perf だけ。chore/docs は除外する
visible = [c for c in items if c[ "type" ] in { "feat" , "fix" , "perf" }]
print (json.dumps(visible, ensure_ascii = False , indent = 2 ))
chore や docs をここで落としておくのがポイントです。エージェントに「ユーザー向けでない変更は除外して」と日本語で頼むより、呼び出し側で機械的にフィルタした方が確実で、トークンも節約できます。エージェントには「判断が必要な仕事」だけを残すのが、結果的に品質も安定します。
Step 2: Managed Agent を Gemini API から呼ぶ
google-genai SDK からモデル名 antigravity-preview-05-2026 を指定して呼びます。SDK の基本的な使い方はgoogle-genai SDK の Python クイックスタート にまとめてあるので、ここでは差分だけ示します。
import os
import json
from google import genai
from google.genai import types
client = genai.Client( api_key = os.environ[ "GEMINI_API_KEY" ])
# ストアごとの実用上の文字数上限(半角換算の目安)
STORE_LIMITS = {
"app_store" : 4000 , # 「このバージョンの新機能」
"google_play" : 500 , # 「最新情報」
}
TARGET_LOCALES = [ "ja" , "en" , "zh-Hans" , "ko" , "es" ]
SYSTEM = """あなたはモバイルアプリのリリースノートを書く編集者です。
渡された commit 要約をもとに、ユーザーに伝わる短いリリースノートを各言語で作成します。
- 機能追加・不具合修正・パフォーマンス改善を、開発用語ではなく利用者の言葉で書く
- アプリ名・バージョン番号・固有名詞は翻訳しない(原文のまま残す)
- 各ストアの文字数上限を必ず守る。超えそうなら情報を削って収める
出力は指定された JSON スキーマに厳密に従うこと。"""
def build_prompt (commits, store, locales, limit):
return (
f "# 対象ストア: { store } (上限 { limit } 文字) \n "
f "# 対象言語: { ', ' .join(locales) }\n\n "
f "# commit 要約 \n{ json.dumps(commits, ensure_ascii = False ) }\n\n "
"各言語のリリースノートを作り、文字数が上限以内に収まっているか自分で数えて確認してください。"
)
Managed Agent は「文字数を数える」ような決定的な処理を、自然言語の感覚ではなくサンドボックス内のコード実行で確かめられます。ここが単発生成との一番の差です。文字数の検証を Function Calling として渡しておくと、エージェントは生成→計測→(超過なら)圧縮、を自分で繰り返します。
def validate_length (text: str , limit: int ) -> dict :
"""エージェントが呼び出す検証ツール。超過分を返す。"""
n = len (text)
return { "length" : n, "limit" : limit, "over_by" : max ( 0 , n - limit)}
length_tool = types.Tool( function_declarations = [
types.FunctionDeclaration(
name = "validate_length" ,
description = "リリースノート1件の文字数が上限以内か検証する" ,
parameters = {
"type" : "object" ,
"properties" : {
"text" : { "type" : "string" },
"limit" : { "type" : "integer" },
},
"required" : [ "text" , "limit" ],
},
)
])
def generate_notes (commits, store):
limit = STORE_LIMITS [store]
prompt = build_prompt(commits, store, TARGET_LOCALES , limit)
resp = client.models.generate_content(
model = "antigravity-preview-05-2026" ,
contents = prompt,
config = types.GenerateContentConfig(
system_instruction = SYSTEM ,
tools = [length_tool],
response_mime_type = "application/json" ,
temperature = 0.4 ,
),
)
return json.loads(resp.text)
temperature は 0.4 まで下げています。リリースノートは創造性より一貫性が大事で、高くすると言語ごとにトーンがばらつきました。0.4 前後が、味気なさと安定のちょうど中間という体感です。
Step 3: 受け取った JSON を呼び出し側でも再検証する
エージェントが「上限内に収めた」と言っても、私は呼び出し側でもう一度数えます。プレビュー段階のモデルを本番フローに入れる以上、エージェントの自己申告だけを信じてストアに投げるのは怖いからです。二重チェックは過剰に見えて、実際に1件だけ Google Play の制限を3文字超えていたケースを拾えました。
def verify (notes: dict , store: str ) -> list[ str ]:
"""呼び出し側の最終ガード。問題があればメッセージのリストを返す。"""
limit = STORE_LIMITS [store]
problems = []
for locale, text in notes.items():
if locale not in TARGET_LOCALES :
problems.append( f "想定外の言語: { locale } " )
continue
if len (text) > limit:
problems.append( f " { locale } : { len (text) } 字 / 上限 { limit } 字( { len (text) - limit } 字オーバー)" )
if not text.strip():
problems.append( f " { locale } : 空のノート" )
return problems
for store in ( "app_store" , "google_play" ):
notes = generate_notes(visible, store)
issues = verify(notes, store)
if issues:
print ( f "⚠️ { store } に要修正:" )
for m in issues:
print ( f " - { m } " )
else :
print ( f "✅ { store } : 全 { len (notes) } 言語が上限内" )
この「エージェントに任せつつ、決定的に検証できる部分は呼び出し側で必ず再チェックする」という二段構えは、自律実行を本番に乗せるときの基本姿勢だと考えています。エージェントの判断は信頼しても、検証は機械に任せる、という分担です。
つまずいた3点
実際に4アプリで回してみて、ドキュメントには書かれていない落とし穴が3つありました。いずれも本番運用に乗せる前に回避できると、手戻りがぐっと減ります。
1つ目はサンドボックスのファイルアクセスです。 前述の通り、ローカルの git リポジトリはエージェントから見えません。「リポジトリを解析して」と書くと、エージェントは存在しないファイルを探しに行きます。必要な情報は呼び出し側で抽出し、contents にテキストとして渡し切るのが安定します。
2つ目は過剰翻訳です。 何も指定しないと、エージェントはアプリ名("Beautiful HD Wallpapers")やバージョン番号まで各言語に訳そうとしました。システム指示に「固有名詞は翻訳しない」と明記し、必要なら do-not-translate の語彙リストを添えると収まります。
3つ目はコストとレイテンシです。 最初は「4アプリ × 5言語 × 2ストア」を1ジョブに詰め込みましたが、応答が長くなり、途中で1言語だけ欠ける事故が起きました。アプリ単位・ストア単位にジョブを分割 したら、各レスポンスが短くなり安定しました。Flash 系のエンジンなので分割してもコストはわずかで、むしろ失敗時の再実行範囲が小さくなる利点の方が大きいです。
Before / After
導入前後で、リリースノートにかかる時間は次のように変わりました。
Before : 英語で下書き → 各言語に手で翻訳 → ストアごとに文字数を調整、で約50分/リリース
After : コミットログを抽出 → パイプライン実行 → 人間が最終レビュー、で約6分/リリース
ざっくり8倍ほど速くなった計算ですが、数字以上に大きいのは、「リリースのたびに翻訳のことを考えなくてよくなった」という心理的な軽さです。個人開発では、こうした小さな摩擦の積み重ねがリリース頻度を静かに下げていきます。摩擦を1つ消すたびに、本来やりたい開発に戻れる時間が少しずつ増えていきます。
CLI 移行(6/18)との関係
このパイプラインは Python から API を直接叩いているので、6/18 の Gemini CLI 終了の影響は受けません。ただ、CLI 中心のワークフローを使っている方は、移行のタイミングでこうした自動化を antigravity CLI のスケジュール実行に寄せると、リリース前のチェックフローごと一本化できます。移行で何が変わるかはGemini CLI 終了に備えた依存関係の移行メモ で触れています。
次の一歩
いきなり全アプリ・全言語で動かすより、まずは1アプリ・2言語(日本語と英語)で1回流してみることを推奨します。generate_notes を1ストア分だけ呼び、verify の出力を眺めるだけで、エージェントが文字数制限をどう扱うかの感触がつかめます。そこで挙動が読めたら、言語とアプリを少しずつ足していくのが安全です。
プレビュー段階のモデルを本番に組み込むのは、正直まだ慎重になる部分もあります。それでも、人間の最終確認を残したうえで退屈な工程だけを移譲する使い方なら、十分に実用になると感じています。お読みいただきありがとうございました。