個人開発のかたわら複数のブログを並行運用するようになってから、毎朝の定型作業が静かに積み上がっていきました。アクセス状況の確認、更新キューの整理、画像アセットの振り分け。一つひとつは数分でも、サイトが増えるたびに朝の時間が削られていきます。
「判断はモデルに任せて、手は道具に任せる」。そう割り切って Gemini にツールを持たせる構成へ移行したのが、私にとっての自律エージェント開発の始まりでした。
最初に断っておくと、エージェントは魔法ではありません。実体は「モデルがツール呼び出しを提案し、コードが実行し、結果をモデルに返す」というループの反復です。この構造を自分の手で一度書いておくと、フレームワークやマネージドサービスを選ぶときの判断軸が一気に明確になります。ここでは、その最小構成と、運用して初めて見えた設計判断をまとめます。
自動 Function Calling だけで動く最小エージェント
現行の Python SDK は google-genai です(pip install google-genai)。旧 google-generativeai のサンプルが検索上位に残っていますが、クラス構成が異なるため混ぜると動きません。私自身、移行直後に旧 SDK の GenerativeModel を探して時間を溶かしました。
新 SDK の大きな利点は、Python 関数をそのまま渡すと自動 Function Calling が働くことです。docstring と型ヒントから関数宣言が生成され、モデルが必要と判断すれば SDK が関数を実行し、結果を踏まえた最終応答まで返してくれます。
from google import genai
from google.genai import types
# GEMINI_API_KEY 環境変数を自動で読み込みます
client = genai.Client()
def get_article_stats(slug: str) -> dict:
"""指定したスラッグの記事の直近7日間の閲覧数とクリック数を返します。"""
# 実際には GA4 / Search Console のデータストアを引きます
return {"slug": slug, "views": 1280, "clicks": 96}
def list_pending_drafts() -> list[str]:
"""公開待ちの下書きスラッグ一覧を返します。"""
return ["gemini-batch-api-notes", "image-asset-pipeline"]
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="公開待ちの下書きを確認して、直近の人気記事の傾向と合わせて公開順の提案をしてください。",
config=types.GenerateContentConfig(
tools=[get_article_stats, list_pending_drafts],
),
)
print(response.text)このコードだけで、モデルは list_pending_drafts を呼び、必要に応じて get_article_stats を複数回呼び、結果を統合した提案文を返します。ループを自分で書いていないのに複数ステップの実行が成立するのは、SDK が内部でツールループを回しているためです。
つまずきやすいのは docstring の書き方です。自動 Function Calling では docstring がそのままモデルへの説明になるため、「何を返すか」を曖昧に書くと呼び出し判断がぶれます。引数の意味と戻り値の中身を一文ずつ明記する。それだけで呼び出しの安定感が目に見えて変わりました。
手動ループに切り替える判断基準
自動実行は便利ですが、私は本番のパイプラインでは手動ループに切り替えています。理由は二つあります。実行前にツール呼び出しの内容を検査したいこと、そしてステップ数の上限を自分で握りたいことです。
from google import genai
from google.genai import types
client = genai.Client()
publish_tool = types.Tool(function_declarations=[
types.FunctionDeclaration(
name="publish_article",
description="指定スラッグの記事を公開します。破壊的操作のため確認が必要です。",
parameters=types.Schema(
type=types.Type.OBJECT,
properties={"slug": types.Schema(type=types.Type.STRING)},
required=["slug"],
),
),
])
config = types.GenerateContentConfig(
tools=[publish_tool],
# 自動実行を止めて、呼び出し提案だけ受け取ります
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
)
contents = [
types.Content(role="user", parts=[
types.Part(text="レビュー済みの記事を公開してください。対象: gemini-batch-api-notes"),
]),
]
MAX_STEPS = 6 # 暴走防止。経験上、定型運用タスクは6ステップで収まります
for step in range(MAX_STEPS):
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
part = response.candidates[0].content.parts[0]
if not part.function_call:
print(response.text) # ツール提案がなくなったら完了
break
call = part.function_call
# ここで呼び出し内容を検査してから実行します
result = execute_with_allowlist(call.name, dict(call.args))
contents.append(response.candidates[0].content)
contents.append(types.Content(role="user", parts=[
types.Part.from_function_response(name=call.name, response={"result": result}),
]))ポイントは MAX_STEPS です。初期の実装でこれを入れておらず、ツールがエラー文字列を返し続ける状況でモデルが同じ呼び出しを繰り返し、無駄な API 課金だけが積み上がったことがあります。ループの上限は「保険」ではなく必須の部品だと考えるようになりました。
ツールは読み取り系と破壊系で分ける
運用して最も効いた設計判断は、ツールを「読み取り系」と「破壊系」に分けたことです。
統計の取得や下書き一覧の参照は、モデルが何度呼んでも実害がありません。一方で「公開する」「削除する」「外部に投稿する」は、一度実行すると取り消せません。私の実装では、破壊系ツールは許可リスト方式にして、ループ内で実行する前に引数を検証しています。
READ_TOOLS = {"get_article_stats", "list_pending_drafts"}
WRITE_TOOLS = {"publish_article"}
def execute_with_allowlist(name: str, args: dict) -> str:
if name in READ_TOOLS:
return run_tool(name, args)
if name in WRITE_TOOLS:
# 破壊系は対象の実在確認を挟んでから実行します
if not draft_exists(args.get("slug", "")):
return f"対象 {args.get('slug')} が見つからないため中止しました"
return run_tool(name, args)
return f"ツール {name} は許可されていません"「存在しない対象への破壊的操作はツール側で握りつぶす」。この一段を入れてから、モデルの判断ミスがそのまま事故になる経路が塞がれました。モデルの賢さに頼るのではなく、ツールの設計で安全側に倒す。エージェント開発の品質は、実はツール側のコードで決まる部分が大きいと感じています。
マルチエージェントの前に、1エージェント+良いツール
エージェント関連の記事では複数エージェントの協調がよく語られます。私も一度、「分析役」「執筆役」「レビュー役」と分けた構成を試しました。結論として、個人開発の規模では維持コストに見合いませんでした。
役割間の受け渡しプロンプトが増えるほどデバッグが難しくなり、どこで判断が崩れたのか追跡するだけで時間が消えます。同じ成果は「1つのエージェントに、責務の明確なツールを揃える」ほうが安定して出せました。マルチエージェントが活きるのは、役割ごとに必要なコンテキストが本当に分離している場合に限られる、というのが現時点での私の判断です。
なお、ループの実行環境ごと Google 側に任せたい場合は、Managed Agents(公開プレビュー)という選択肢もあります。隔離されたサンドボックスで計画からコード実行までを担ってくれますが、上で書いたような「破壊系ツールの検証を自分のコードで握る」構成とはトレードオフになります。私はまず自前ループで運用の勘所を掴んでから、移せる部分だけを移す段階移行を取っています。
運用で毎月見ている数字
エージェントを回し続けるかどうかは、感覚ではなく数字で判断するようにしています。私が月初に確認しているのは三つです。
ツール呼び出しの成功率(直近は 96% 前後で、低下したら docstring か引数スキーマを疑います)。1タスクあたりの平均ステップ数(2〜3 で安定していれば健全、上振れしたらプロンプトの目標設定が曖昧になっています)。そして usage_metadata から合計したトークン消費です。自動化で浮いた作業時間とこの費用を並べたとき、見合わなくなった機能は畳む。この基準を持っておくと、エージェントを「動いているから残す」ではなく「効いているから残す」で運用できます。
次に試すこと
まずは読み取り系ツールを2つだけ持つ最小エージェントを、自動 Function Calling で動かしてみてください。docstring を一文直すだけで挙動が変わる感覚を掴めたら、手動ループ化とステップ上限の導入に進むのが近道です。
ツール定義をさらに細かく制御したくなったら、Gemini API の Function Calling 活用法が次の一歩になります。基本の呼び出しに不安があれば Gemini APIクイックスタートから固めるのも良い順序です。
同じように日々の運用を少しずつ自動化している方の参考になれば幸いです。