# danicat.dev — Daniela Petruzalek (Full Corpus) > テクノロジスト、ゲーマー、猫好き — Googleのシニアデベロッパーリレーションズエンジニア > For the compact index, see [llms.txt](https://danicat.dev/ja/llms.txt). - Website: https://danicat.dev/ja/ - Compact Index (llms.txt): https://danicat.dev/ja/llms.txt - Full Corpus (llms-full.txt): https://danicat.dev/ja/llms-full.txt - Language Editions: [English (llms-full.txt)](https://danicat.dev/llms-full.txt) | [Português (llms-full.txt)](https://danicat.dev/pt-br/llms-full.txt) | [日本語 (llms-full.txt)](https://danicat.dev/ja/llms-full.txt) - Agent Skills Catalog: https://skills.danicat.dev/ (LLMs index: https://skills.danicat.dev/llms.txt | JSON manifest: https://skills.danicat.dev/catalog.json) --- # コードを書くことの先へ:センス、スキル、そしてテックリード - **Markdown URL**: https://danicat.dev/ja/posts/beyond-writing-code/index.md - **HTML URL**: https://danicat.dev/ja/posts/beyond-writing-code/ - **Date**: 2026-10-28 - **Categories**: Perspectives - **Tags**: agent-skills, antigravity, career, code-review, subagents, vibe-coding - **Summary**: コーディングエージェント全盛期において、ソフトウェアエンジニアの役割が文法の記述からアーキテクチャの審美眼、サブエージェント、Agent Skills、そしてテックリードとしての振る舞いへとどう変化するかを解説します。 基底モデルが世界中のあらゆる知識をその重みに焼き付けたとき、人間には一体何が残されるのでしょうか? 何十年もの間、ソフトウェアエンジニアとしての私たちのアイデンティティは「コードを書く技術」そのものと結びついていました。文法を極め、標準ライブラリを記憶し、自分自身の手で組み上げたシステムに誇りを持ってきたのです。AIは単に退屈な定型コード(ボイラープレート)を片付け、毎週何時間もの自由時間を私たちに返してくれると聞かされていました。しかし現実は違いました。私たちが何年もかけて習得したものをモデルが数秒で生成する様を目の当たりにし、業界全体で深刻なアイデンティティ・クライシスが引き起こされています。機械がコードを書けるのなら、私たちはどこに居場所を見出せばよいのでしょうか? 人間は**灯台(ビーコン)**であるべきです。まさにモデルが「すべて」を知っているからこそ、あらゆる道筋が同等にもっともらしく見えてしまいます。モデルには、**センス(審美眼)**を持って情報を優先順位付けし、順序立てて創造することができません。ここに私たちの新しい仕事の本質があります。まるで暗い洞窟を探検するように、私たちが懐中電灯を握り、進むべき道を照らし出し、モデルが次にどこへ集中すべきかを決定するのです。 私たちの役割は、コードの記述から「オーケストレーション(調律・指揮)」へと進化しました。実のところ、AIコーディングが登場する以前から、これは多くの人にとって目新しいことではありません。ソフトウェアエンジニアのキャリアにおける自然なステップアップそのものです。キャリアのはしごを登るにつれて、現場で直接手を動かす時間は減り、コーチとしての役割を担う機会が増えていきます。チームのアウトプットに責任を持ち、すべてのコード行に目を通すのをやめて重要な部分に集中し、仕事を他者に委ねること(デリゲーション)のメリットとデメリットに向き合うようになります。つまり、テックリードになるということです。 ## 誰もがテックリードに昇格した世界 ![2026年にはすべてのエンジニアがテックリードに昇格した](everyone-is-a-tech-lead.png "テックリードへの昇格おめでとうございます。(追記:人事部より給与バンドは据え置きと確認されました。)") 従来のキャリアパスでは、この移行は段階的に進んでいました。現場の最前線で何年も過ごし、ジュニアからシニアへと成長した後に、リードやエンジニアリングマネージャー、アーキテクトといった役職に就き、ジュニアエンジニアが実装の詳細を担当する一方で、大局的な視点に専念するようになっていました。 しかし今や、誰もがコーディングエージェントによる仮想チームを従えています。そのため、最も経験の浅いジュニアエンジニアでさえ、優れたテックリードになる方法を学ぶ必要があります。人間を率いるためではなく、エージェントを正しく導くためにです。 「何を達成するために導くのか?」これこそが最も重要な問いです。私たちが依然としてプログラムを構築していることに変わりはありませんが、それを表現する方法が変わったのです。 私の人生の中で、真に「色褪せない(エバーグリーンな)」本がいくつかあります。そのひとつが『計算機プログラムの構造と解釈』(SICP)です。なぜ今、この古典が重要なのでしょうか? それは、この本がプログラムの本質と、エンジニアとして「呪文を唱えて」それらを構築するプロセスを見事に言い表しているからです。 > *「計算プロセスは、コンピュータの中に棲む抽象的な存在である。プロセスは進化するにつれて、**データ**と呼ばれる別の抽象的な対象を操作する。プロセスの進化は、**プログラム**と呼ばれる規則のパターンによって指示される。人々はプロセスを指示するためにプログラムを作る。実質的に、私たちは自らの呪文によってコンピュータの精霊を召喚しているのだ。」* > > *「……プロセスを召喚するために私たちが使うプログラムは、魔法使いの呪文に似ている。それらは、プロセスに実行させたいタスクを規定する、秘術的かつ難解なプログラミング言語の記号表現から丹念に構成されている。」* > > —— ハロルド・アベルソン、ジェラルド・ジェイ・サスマン『計算機プログラムの構造と解釈』(1985年) 私たちは今でも、コンピュータに指示を与えるためにプロセスを召喚する仕事を続けています。変わったのは、呪文を紡ぎ出す言語だけです。難解な記号表現の代わりに自然な英語(日常言語)で呪文を書くようになった今、エージェントをオーケストレーションし、正しい軌道に乗せ続けることこそが仕事の最も重要な部分となりました。これは、コードを書く量が減り、コードを読む量が劇的に増えることを意味します。また、これまで以上に仕様(プロンプト)を書く機会が増えています。AI以前からテックリードを務めていた人なら、これらはすべて日常茶飯事の仕事に聞こえるはずです。 では、どうすればこのスキルを磨くことができるのでしょうか? かつての世界では、優れたテックリードやマネージャーは、自分たちがキャリアを通じて培ってきた批判的思考やセンスをチームに身につけさせるため、コーチングやメンタリングに多くの時間を費やしていました。数々の現場の傷跡(トラブル対応)から得られたプロセス、行動規範、テクニックの知識を少しずつ伝授していったのです。先ほど私が行った重箱の隅をつつくような厳格なコードレビューですか? それは、何年も前に私自身の細部への不注意が原因で本番環境をダウンさせてしまった苦い経験があるからに他なりません。 現代の世界では、人間のメンターや自らの失敗から学ぶだけでなく、エージェントの振る舞いを注意深く観察し、**彼らの**過ちから学ぶ必要があります。それらの失敗は猛烈な速度で蓄積されるため、エージェントは究極の「圧縮されたフィードバックループ」を提供してくれます。午後の一握りの時間の中で、何十もの微妙な障害モード、競合状態、ハルシネーション(幻覚)による設計破綻が目の前で次々と展開され、従来のリリースサイクルの10倍の速さでアーキテクチャの審美眼を鍛え上げることができるのです。 本質的に、私たちの現在の仕事は「情報の流れをオーケストレーションすること」であり、エージェントが私たちが望む成果物を生み出すための最適な環境を常に整えることです。 私たちが目の当たりにしている進化のスピードを考えると、今日最適なものが明日も最適であるとは限りません。進化のペースに遅れないよう、私たちはツールやプロセスを絶えず洗練させていく必要があります。かつてないほど、ソフトウェアは「使い捨て」の時代に入りました。明日にはまた新しい呪文が必要になるからこそ、私たちの呪文は迅速かつ的確でなければならないのです。 ## モデルが自律的に行動できるよう情報を構造化する > *「熟練したソフトウェアエンジニアは、結果として生じるプロセスが意図したタスクを実行することを合理的に確信できるように、プログラムを体系化する能力を備えている。彼らは自分たちのシステムの挙動を事前に視覚化できる。予期せぬ問題が破滅的な結果につながらないようにプログラムを構造化する方法を知っており、問題が発生したときにはプログラムを**デバッグ**できる。優れた設計の計算システムは、優れた自動車や原子炉と同様に、部品ごとに独立して構築、交換、デバッグできるようにモジュール構造で設計されている。」* > > —— ハロルド・アベルソン、ジェラルド・ジェイ・サスマン『計算機プログラムの構造と解釈』(1985年) 上記の引用文の「プログラム」という言葉を「エージェント」に置き換えてみてください。その構造はそのまま通用します。私たちは、優れた計算システムを生み出せるように、エージェントを活用したコーディングプロセスを体系化する方法を学ぶ必要があります。 エージェントに知識を与える最も重要な方法のひとつが、[Agent Skills](https://danicat.dev/ja/posts/20260829-the-pragmatic-guide-to-agent-skills/)を通じたアプローチです。モデルは人間に極めて近い振る舞いをします(人間が生成したデータで訓練されている以上、当然のことです)。そのため、私たちがチームの生産性や成果物の品質を高めるために適用してきた手法は、そのままエージェントにも応用できます。自身の現場経験やノウハウ、業界のベストプラクティスに基づき、モデルの挙動を的確に導くスキルを作成する絶好の環境が整っているのです。 そうした手法のひとつが、複雑なタスクを小さく分割することです。アジャイル開発の根幹をなすこの考え方は、仕様駆動開発(spec-driven development)、実装計画、そして[サブエージェント](https://danicat.dev/ja/posts/20260722-the-rise-of-the-subagents/)という枠組みを通じてエージェントコーディングにもそのまま適用できます。 私自身は、サブエージェントを「設計する情報フローの構成要素」として扱っています。これは多くの意味で従来の並行処理(concurrency)の扱いに似ていますが、コンテキストを隔離する上でも極めて重要な役割を果たします。結局のところ、モデルに必要なのは「適切なアテンション(注意)」だけなのです。タスクを互いに独立した直交的な単位に分割し、個々のサブエージェントに処理させることで、エージェントの貴重なアテンションを単一のタスクだけに集中させ、品質と仕様遵守率を飛躍的に向上させることができます。 ワークフローを手動で組み立てる際や、後世に残るスキルを構築する際には、特に以下の点が役に立ちます。 1. **コーディネーターの役割**: コーディネーター(親セッション)が常にサブエージェントへ作業を委譲し、メインセッションを可能な限り迅速に解放することが重要です。これにより、サブエージェントが作業を進めている間もユーザーは対話を継続でき、リアルタイムに指示やコメントを与えて軌道修正できます。 2. **タスクの直交性**: タスク間の依存関係や競合を最小限に抑えるため、互いに独立した単位に切り分けます。まず順序依存を理解し尊重した上で、サブエージェント同士が互いの作業を上書きしたり矛盾した仮定を置いたりしないよう、確信を持って並列化します。 3. **実績あるパターンの再現**: コーチングやメンタリングと同様に、人間のチームや従来のソフトウェア工学で機能してきたパターンはエージェントにも有効です。文献に立ち返り、エージェントに伝授できる新旧のパターンを継続的に探求してください。 私は過去数ヶ月にわたり、自らの[スキルカタログ(skill catalog)](https://skills.danicat.dev)を段階的に構築してきました。モデルやエージェント実行環境の進化に伴い、スキルは急速に*腐敗*するため、一度作れば終わりというものではありません。私はそれらを絶えずアップデートしており、陳腐化しつつあるスキルを可視化して改修するか除外するかを判断できるよう、スキルに「鮮度スコア」を付与することにしました。 日々の開発で私が頻繁に活用しているスキルの例をいくつか挙げます。 - **`double-diamond`**: アジャイルのダブルダイヤモンド手法に基づき、実装前に必ず課題発見と要件定義を強制するプロダクト開発ワークフロー。 - **`parallel`**: タスクを複数のスレッドに展開(ファンアウト)し、最後に結果を照合(マップリデュース)する汎用並行処理パターン。 - **`uno-reverse`**: モデルの過信や過剰エンジニアリング(オーバーエンジニアリング)の傾向を是正するための敵対的レビュー(アドバーサリアルレビュー)。 - **`less-is-more`**: 肥大化したアプリを作るのを防ぎ、一度にひとつのことを完璧に行うよう指示する設計哲学。 - **`intercom`**: 関連するプロジェクトで作業している複数のエージェント同士が自律的に対話できるようにする連携スキル。 - **`show-me-the-money`**: モデルと議論したものの完全に実装を忘れていた機能のアイデアを、会話履歴から発掘するスキル。 - **`abstract`**: 哲学的な原則を用い、個別の事例や状況を抽象化して汎用的な知識へと昇華させるスキル。 ## エージェントコードにおける「割れ窓理論」 > *「正常に動作するコンピュータにおいて、計算プロセスはプログラムを正確無比に実行する。したがって、魔法使いの弟子のように、初心者のプログラマは自らが召喚したものの結果を理解し、予期することを学ばなければならない。プログラム内の小さな誤り(通常**バグ**や**グリッチ**と呼ばれる)であっても、複雑で予期せぬ結果を引き起こす可能性がある。……しかし、現実世界のプログラミングには、細心の注意、専門知識、そして知恵が求められる。」* > > —— ハロルド・アベルソン、ジェラルド・ジェイ・サスマン『計算機プログラムの構造と解釈』(1985年) 私たちが呼び出したものの結果を予期できなければ、手綱を握られていないエージェントは、私たちが気付くよりも速く小さな構造的欠陥を増殖させてしまいます。この問題は何も新しいものではありません。人間が書いたコードでも常に起きていたことですが、以前は進行がはるかに緩やかだったため目立たなかっただけです。かつてそれを止めようとした人たちは、決まって「重箱の隅をつつく偏屈なレビュアー」と見なされ、ごく一部から愛される一方で大半からは煙たがられていました。 心理学では、これは1969年の実験に端を発する「割れ窓理論(Broken Windows Theory)」として知られる現象です。研究者たちが治安の良い住宅街に放置車両を1週間置いたところ、車には何の異変も起きませんでした。2週目に研究者のひとりがスレッジハンマーで窓ガラスを1枚叩き割ったところ、わずか数時間のうちに車は解体され、部品をすべて持ち去られてしまいました。 割れたガラスが引き金だったのです。それは「誰も気にしていない」というシグナルとなり、コミュニティ全体が一斉に配慮をやめてしまいました。私は何年もの間、コードでも全く同じことが起きると主張し続けてきました。 コードの中にひとつの割れ窓(例えばタイポ、不適切な命名、脆弱なテスト、誤った抽象化など)が生じると、私たちの心理的な関心低下スイッチが入ってしまいます。そして問題は時間の経過とともに雪だるま式に悪化し、小さな欠陥が巨大な崩壊へと膨れ上がります。 エージェントの世界では、既存のコードが新しいコードを生成する際のバイアスとなるため、この問題はさらに増幅されます。既存のコードが粗悪な場合、適切に指示しない限り、エージェントはそれを「許容される標準」と判断し、問題を加速度的に悪化させていきます。 コードレビューはエンジニアリング業務の中で決して派手な作業ではなく、エンジニアの中には仕事の一部と見なさない人さえいます。しかし実際には、最も重要な業務のひとつです。 ただし、これは**すべてのコード行を一行ずつ精査する**という意味ではありません。 私を含め、AIが生成した何千行ものコードを延々とレビューしたい人などいません。審美眼(センス)とは、構文をチェックしたり機械的なバグを見つけたりすることではありません。それは決定論的ツールが担うべき領域です。私が『[エージェント時代におけるコードレビューのやり方](https://danicat.dev/ja/posts/20260303-code-reviews-in-2026/)』で書いたように、コンパイラ、リンター、自動テストで検出できる問題なら、人間がいちいち目を凝らすべきではないのです。 私たちは「AIにはAIと自動化で対抗する」必要があります。決定論的な品質ゲートを用いて機械的なノイズを排除し、レビューの時間を重要なアーキテクチャ境界の検証に集中させ、残りはサンプリング検査に留めます。現代の開発サイクルは従来の「手作りの職人芸的コーディング」よりも何倍も高速であるため、サンプリングによって万が一問題を即座に拾えなかったとしても、すぐに次の機会で捉えることができます。したがって、レビューは真にクリティカルな要所に絞り込み、残りは自動化されたプロセスを信頼して委ねるべきです。 ## 約束された「自由時間」はどこへ消えたのか 私たちはかつてない速度でソフトウェアを構築しています。実装のボトルネックが解消されたことで、より緊急性の高いタスクに押し出されてお蔵入りしていた数々のアイデアが、一気に実現可能になりました。放棄されたプロジェクトの山に埋もれる前に、何が機能し何が機能しないかを確かめるためのプロトタイプを次々と迅速に生み出せるようになったのです。 しかし、開発が加速したからといって、メンタルモデル(概念の体系化)を構築する必要性が消えるわけではありません。私たちのコア業務は、エージェントが私たちが真に意図したものを構築できるよう、コンテキスト、制約、そしてフィードバックループを設計することです。むしろ、私たちの仕事は以前より難しくなりました。技術的な知識自体はコモディティ化しましたが、それらを束ねるセンスや批判的思考力はいまだ希少なままだからです。 結局のところ、私たちは今もコンピュータの精霊を召喚する魔法使いであり続けています。ただ、私たちの呪文がまたひとつ上の抽象化レイヤーへと到達しただけに過ぎないのです。 --- # A2UI と Genkit でエージェント向けインターフェースを設計する - **Markdown URL**: https://danicat.dev/ja/posts/designing-agentic-interfaces-with-a2ui-and-genkit/index.md - **HTML URL**: https://danicat.dev/ja/posts/designing-agentic-interfaces-with-a2ui-and-genkit/ - **Date**: 2026-10-27 - **Categories**: Agent Development - **Tags**: a2ui, agent-skills, ai-agents, gemini, genkit, golang, osquery - **Series**: Gemini for Go Developers - **Summary**: A2UI、カスタムコンポーネントカタログ、Human-in-the-loop インタラプトを活用して、Genkit Go エージェントをテキストチャットボットからジェネレーティブ UI へと進化させる方法を解説します。 [前回の記事](https://danicat.dev/ja/posts/20261006-building-agentic-backends-with-genkit-go/) では、AI 診断エージェントである AIDA のバックエンドを再構築し、旧来の Python と SQLite による RAG 構成から、コンパイル済みの Genkit Go バイナリとオンデマンドの Agent Skills による構成へと刷新しました。 バックエンドが整ったところで、次はいよいよフロントエンドです。Go による頭脳とオンデマンドスキルを手に入れたとはいえ、AIDA はまだ実質的にはチャットボットに過ぎませんでした。プロンプトを入力すれば `osquery` を実行し、長文のテキストをそのまま返してくるだけです。いくら AIDA がレトロな世界観を持っているとはいえ、時代遅れの体験に縛り付けておく必要はありません。 最新のエージェント向け UI は、より表現力豊かな UI 要素を活用できるだけでなく、一律で静的な画面構成を押し付けるのではなく、ユーザーのその瞬間のニーズに応じて柔軟に適応するべきです。 本記事では、AIDA のインターフェースをモダナイズします。コードの解説に入る前に、まず現代的なエージェント向け UI に求められる要件と、[Agent to User Interface (A2UI)](https://a2ui.org/) プロトコルの仕組みを見ていきましょう。 ## A2UI とは? A2UI は Google が提唱するオープンプロトコルであり、AI エージェントがネイティブな UI コンポーネントを通じて直接対話できるようにする仕組みです。テキストのみによる返答にとどまらず、現在取り組んでいるタスクに合わせて、ダッシュボード、グラフ、テーブル、操作ボタンなどを動的に組み立てて提示できます。 UI のスタイリングや描画の主導権はフロントエンド側が完全に保持したまま、エージェントは会話の文脈やデータに基づいて「何を表示すべきか」を判断します。 A2UI の連携は、主に次の3つの概念を中心に成り立っています。 - **カタログ(Catalog)**: パネル、ゲージ、チャート、テーブル、ボタンといった UI の構成要素と、そのスキーマを定義した共通メニューです。バックエンドのエージェントとフロントエンドのクライアント双方が、あらかじめこのカタログに合意しておきます。組み込みの基本カタログを使うことも、ドメインに特化したカスタムコンポーネントを定義することも可能です。 - **サーフェス(Surfaces)**: クライアント上で描画される動的な UI キャンバス(``)です。エージェントは処理を進めながら、どのカタログコンポーネントを画面に配置し、そのプロパティをどう更新するかを指示する構造化 JSON メッセージ(`application/a2ui+json`)をストリーミング送信します。 - **アクション(Actions)**: ユーザーがコンポーネントを操作したとき(ボタンのクリック、テーブルの行選択、3D グラフのノード選択など)に、サーフェスからエージェントへ送り返される双方向イベントです。アクションにより、ユーザーが次のプロンプトを入力することなく、クリック操作がそのまま次の会話ターンへと直結します。 ## AIDA の新しいフロントエンド仕様 私たちが目指すのは、A2UI を用いて、ハードコードされた固定 UI からエージェントが文脈に応じて自在に画面を構成できる仕組みへの移行です。そのためには、AIDA 専用のコンポーネントカタログを定義して利用可能にし、フロントエンド側で A2UI サーフェスを適切に描画できるよう再配線する必要があります。 以下が、新しく生まれ変わった診断コックピットの実際の動作画面です。AIDA は必要に応じて 3D プロセスグラフ、HUD ゲージ、そしてリアルタイムの `osquery` プロセステーブルを動的に組み立てます。 ![3Dプロセスグラフ、HUDゲージ、インタラクティブなosqueryプロセスマトリクスを備えたリアルタイムA2UI v0.9診断サーフェスを描画するAIDA](aida-diagnostic-surface.png "カスタムA2UIコンポーネントで描画されたAIDA診断サーフェス:3Dプロセスグラフ、HUDゲージ、osqueryによるリアルタイムプロセステーブル。") 以下のプロンプトは、この要件を仕様へと落とし込んだものです。 ```markdown Upgrade AIDA's web interface from a static chat UI to a generative diagnostic cockpit using A2UI and Genkit. Requirements: 1. Keep the retro NEC PC-98 cyberpunk look and feel (AIDA's pixel avatar, CRT scanlines, and neon palette), with a fixed host-status sidebar showing live osquery system vitals. 2. Replace the plain Markdown chat output with an A2UI surface powered by a custom "aida" component catalog for system diagnostics: HUD panels, gauges, charts, filterable data tables, 3D system visualisations, text blocks, and action buttons. 3. Wire up two-way A2UI actions so clicking a table row, 3D node, or button sends a follow-up diagnostic action back to AIDA. 4. Add a human-in-the-loop shell command tool so AIDA can actually fix issues she finds on the host, pausing for explicit Approve/Deny confirmation in the UI before executing any command. /grill-me ``` このプロンプトだけで一発で完璧な出力を得ようと期待してはいけません。実際には、後述の「推奨スキル」セクションで説明するように、コーディングエージェントに適切なスキル、MCP、ツールを渡しておくことが極めて重要です。 プロンプトの末尾にある `/grill-me` スキルのヒントにも注目してください。このスキルは Antigravity(および多くの最新コーディングエージェント)に標準搭載されており、指示が曖昧だったり詳細が不足していたりする場合にエージェントから質問を投げかけさせる役割を持ちます。これにより構想が洗練され、最初から完璧なプロンプトを書かなければならないというプレッシャーから解放されます。 上記のプロンプトは非常にシンプルですが、`/grill-me` が裏で多くの作業を肩代わりしてくれます。後述するように、コンポーネントカタログの全詳細をプロンプトに書き込まなくても済むのは、不足している情報を Antigravity が対話を通じて質問し補ってくれるからです。 ### 推奨スキル Genkit を使用するため、公式の [Genkit Skills](https://github.com/genkit-ai/skills)(`developing-genkit-go` や `developing-genkit-js` を含む)をインストールして有効化しておくことを強くお勧めします。これらのスキルには、エージェントやフローの構築方法だけでなく、クライアント側およびサーバー側の双方で A2UI 要素を扱うノウハウが含まれています。 さらに、`gemini-api-dev` スキルを導入しておくことで、エージェントが古いモデルへ後退せず最新モデルを選択できるようになります。このスキルは [Google Skills](https://github.com/google-gemini/gemini-skills/blob/main/skills/gemini-api-dev/SKILL.md) リポジトリから単体で導入することも、[Gemini Cloud Developer Plugin](https://cloud.google.com/blog/topics/developers-practitioners/introducing-the-google-cloud-developer-plugin-for-ai-coding-agents) の一部として導入することも可能です。 ## A2UI カタログの設計 従来のフロントエンド開発では、満たすべきユーザージャーニーに合わせてインターフェースを設計します。A2UI アプリケーションでもその基本は変わりませんが、従来のような固定されたアプリケーション画面を設計するわけではありません。その代わりに、目的を達成するために必要な個々のコンポーネントを設計することに集中します。 これらのコンポーネントが集まってエージェントの**カタログ**を構成し、エージェントがオンデマンドで UI を適応させる柔軟性をもたらします。A2UI には一般的な用途をカバーする**基本カタログ(Basic Catalog)**が付属しているため、必ずしもゼロからカタログを設計する必要はありません。しかし、今回は独自の世界観と体験を作り出すため、AIDA 専用のカスタムカタログを設計するのが最適な選択肢でした。 AIDA のために設計した7つのコンポーネントは以下のとおりです。 - **Panel**: 関連するコンポーネントを行、列、グリッド状にグループ化するフレーム付き HUD コンテナです。ステータスバッジ(`nominal`、`warning`、`critical`)を備えており、異常のあるサブシステムを一目で把握できます。 - **Scene3D**: システムトポロジー(プロセスツリー、CPU コア、メモリマトリクス、ネットワークメッシュ、ディスクセクター、サーマルゾーンなど)を視覚化するためのインタラクティブな 3D ワイヤーフレームキャンバスです。クリック可能なノードを備え、ドリルダウン調査が可能です。 - **CyberChart**: CPU 負荷、ネットワークスループット、レイテンシなどのメトリクスを描画する複数系列チャート(折れ線、棒、面、散布図、レーダー)です。 - **HudGauge**: ディスク使用率、バッテリー残量、スワップ負荷などの容量やパーセンテージを視覚的に表示するメーター(円弧、プラズマバー、セグメントバー)です。 - **DataMatrix**: `osquery` の調査結果を表示するソート・フィルタリング可能なテーブルです。行をクリックすると、その項目に対する追加調査がトリガーされます。 - **TextBlock**: 要約、コマンド出力、コピー可能なスニペットを表示する等幅フォントの診断ノートです。 - **ActionButton**: 不審なプロセスの詳細調査や修復処理の実行など、AIDA がワンクリックで実行できる次のアクションを提示するための対話型ボタンです。 基本カタログは見た目の美しさや豊かな体験よりも汎用的な機能性を重視しているため、こうしたドメイン特化型の計測器・計器類を設計することは大きな差別化につながります。 ## Go バックエンドでの A2UI の組み込み バックエンドで AIDA に新しい UI 構成要素へのアクセス権を与えるため、起動時に Genkit A2UI プラグインを有効化し、診断ツールと並んでカスタムコンポーネントカタログを読み込みます。 ```go // internal/agent/agent.go g := genkit.Init(ctx, genkit.WithExperimental(), genkit.WithPromptDir("./prompts"), genkit.WithDefaultModel("vertexai/gemini-3.8-flash"), genkit.WithPlugins( &googlegenai.VertexAI{}, &middleware.Middleware{}, &a2uix.A2UI{}, // A2UI ミドルウェアの登録 ), ) // 1. カスタム "aida" A2UI コンポーネントカタログの登録 if _, err := a2uix.LoadCatalog(g, AidaCatalog()); err != nil { return nil, err } // 2. 読み取り専用診断ツール (osquery) と HITL シェルコマンド (command) の登録 osquery.RegisterTool(g, runner) command.RegisterTool(g) // 3. prompts/aida.prompt から単一の AIDA エージェントを定義 aidaAgent := genkitx.DefinePromptAgent(g, "aida", aix.WithDescription("AIDA — AI Diagnostic Agent"), aix.WithSessionStore(sessionStore), aix.WithNamedPrompt("aida", PromptInput{OS: runtime.GOOS}), ) ``` ここでは、Gemini Enterprise(Vertex AI)およびミドルウェアプラグインと並んで `&a2uix.A2UI{}` で Genkit を初期化し、診断・修復ツールを登録した上で、`aida` プロンプトエージェントを `prompts/aida.prompt` にバインドしています。最大のポイントは `a2uix.LoadCatalog(g, AidaCatalog())` です。これによりカスタムコンポーネントカタログが Genkit ランタイムに登録され、A2UI ミドルウェアがそれらのコンポーネントをモデルに提示し、モデルの出力を検証できるようになります。 ### カタログと A2UI ミドルウェアの定義 Go では、カスタム A2UI カタログ(`a2uix.Catalog`)はコンポーネント定義のリストとして表現されます。各エントリにはコンポーネント名 `Name`、人間向けの説明 `Description`、および期待されるプロパティ `Props` を指定します。 ```go // internal/agent/catalog.go func AidaCatalog() *a2uix.Catalog { return &a2uix.Catalog{ ID: "aida", Components: []a2uix.CatalogComponent{ { Name: "HudGauge", Description: "A visual meter for capacity and percentage metrics.", Props: "label: string (required); value: number (required); unit?: string; status?: 'nominal' | 'warning' | 'critical'", }, { Name: "ActionButton", Description: "An interactive button that triggers a follow-up diagnostic or remediation action.", Props: "label: string (required); action: string (required); variant?: 'primary' | 'warning' | 'danger'", }, // ... Panel, Scene3D, CyberChart, DataMatrix, TextBlock }, } } ``` バックエンド側では、A2UI は Genkit ミドルウェアとして機能します。モデルは通常の Markdown テキストを返す代わりに、描画したいコンポーネントを記述した `a2ui` 形式の JSON ブロックを出力します。ミドルウェアはストリーム上でこれらのブロックをインターセプトし、登録済みカタログに照らして検証した上で、ブラウザ向けに `application/a2ui+json` メッセージとしてパッケージ化します。 このパイプラインは、`prompts/aida.prompt` 内で宣言的に構成します。 ```yaml --- model: vertexai/gemini-3.8-flash description: AIDA — AI Diagnostic Agent input: schema: os: string tools: - run_osquery - runCommand config: thinkingConfig: thinkingLevel: LOW tools: - googleSearch: {} use: - genkit-middleware/skills: skillPaths: - ./skills - a2ui: catalogId: aida validate: strict --- ``` デフォルトでは、`a2ui` ミドルウェアは登録された `"aida"` カタログからコンポーネントの説明とプロパティ仕様を読み出し、それらをシステムプロンプト(`instructions: system`)へ直接注入します。これにより、`internal/agent/catalog.go` が単一の信頼できる情報源(Single Source of Truth)として保たれます。Go 側でコンポーネントを修正すれば、手動でスキーマを同期させることなくプロンプト側も自動的に更新されます。 モデルが `a2ui` ブロックをストリーミング出力する際、`validate: strict` によって各 JSON メッセージが有効な A2UI エンベロープを持ち、`"aida"` カタログに存在するコンポーネント型のみを使用しているかが厳格にチェックされます。モデルが存在しないコンポーネント型を捏造したり、壊れた JSON を出力したりした場合は、即座にエラーとなって早期失敗(Fail-Fast)します。 ## ジェネレーティブなフロントエンドの構築 Go バックエンドから検証済みの A2UI メッセージがストリーミングされると、ブラウザ側ではそれらの JSON ペイロードを実際の DOM 要素に変換するクライアントランタイムが必要になります。`web/` 配下のフロントエンドは、TypeScript と [Lit](https://lit.dev/) で作られた軽量アプリケーションであり、以下の3つの役割を担います。 1. **コンポーネントカタログの対応付け**: クライアント側では、7つの `"aida"` 計器のそれぞれを Lit の Web コンポーネント(`web/src/aida-catalog/`)として登録し、クライアント側のプロパティスキーマとペアにして管理します。Go バックエンドはコンポーネントの型名のみを検証するため、ブラウザ側がこのスキーマを使って受け取ったプロパティを検証し、デフォルト値を適用してデータをバインドしてから描画します。 2. **ストリームからのサーフェス描画**: Genkit のクライアント SDK が `/agents/aida` に接続し、受信した A2UI メッセージを A2UI の `MessageProcessor` に渡します。プロセッサはチャンクが届くたびに、画面上の `` 要素をリアルタイムで更新します。 3. **ユーザーアクションのエージェントへの送信**: アクションボタン、テーブルの行、または 3D ノードをクリックすると、A2UI アクションがディスパッチされます。クライアントはそのイベントを、チャットの次のターンとして Go バックエンドに送り返します。 ```ts // web/src/aida-cockpit.ts const agent = remoteAgent({ url: "/agents/aida" }); const chat = agent.chat(); const processor = new MessageProcessor([aidaCatalog], async (action) => { // UI のクリックイベント(ボタン、テーブル行、3D ノード)を Go エージェントへ送り返す const turn = chat.sendStream({ message: actionToMessage(action as any) }); await consumeTurnStream(turn); }); ``` コックピットを最初に開いたとき、左側のサイドバーは `/api/system-status` をポーリングしてリアルタイムの `osquery` システム状態を表示し、メインエリアにはクイック診断ランチャーが並びます。 ![PC-98風ピクセルアバター、osqueryによるリアルタイムホスト状態サイドバー、クイック診断ランチャーカードを備えたAIDA診断コックピットの初期画面](aida-cockpit-landing.png "サイドバーにリアルタイムのホスト状態を表示し、クイック診断ランチャーカードを備えたAIDA診断コックピットの初期画面。") ランチャーをクリックするか質問を入力すると、プロンプトが `/agents/aida` に送信されます。AIDA が `osquery` の結果をストリーミングで返送するにつれて、`MessageProcessor` がランチャー画面を先ほど確認した動的な診断サーフェスへと置き換えていきます。 ## エージェントインタラプトによる Human-in-the-loop の実現 `osquery` によるマシンの調査は完全に読み取り専用であるため、AIDA が自律的にクエリを実行しても安全です。しかし、双方向の A2UI アクションを導入したことで、暴走したプロセスの強制終了やキャッシュの消去など、発見した問題の修復を提案する `ActionButton` を描画できるようにもなりました。 ホスト上でのシェルコマンドの実行を野放しにするわけにはいきません。こうしたアクションを安全に制御するため、Genkit はネイティブな Human-in-the-loop 機構である**ツールインタラプト(tool interrupts)**を提供しています。ツールは現在のターンを一時停止し、ユーザーの明示的な承認を待ってから、要求された副作用を実行します。 ### ツールの実行一時停止と再開 Go では、`genkitx.DefineInterruptibleTool` を使って `runCommand` を定義します。この関数は、AIDA がツールを初回実行しているのか、それともユーザー入力後に再開しているのかを示す確認ペイロード(オプション)を受け取ります。 1. **初回呼び出し(一時停止):** AIDA が最初に `runCommand` を呼び出した時点では、確認ペイロードは `nil` です。シェルコマンドを即座に実行するのではなく、ツールは提案されたコマンド、実行理由、リスクレベルを含む `tool.Interrupt(...)` を返してターンを一時停止します。 2. **再開呼び出し(実行または拒否):** ユーザーが UI 上で **Approve(承認)** または **Deny(拒否)** をクリックすると、Genkit はユーザーの決定内容を渡してツール関数を再度呼び出します。 ```go // internal/command/tool.go package command func RegisterTool(g *genkit.Genkit) { genkitx.DefineInterruptibleTool(g, "runCommand", ai.WithDescription("Runs a shell command on the host with operator approval"), func(ctx *ai.ToolContext, input CommandInput, res *CommandConfirmation) (CommandOutput, error) { // 1. 初回呼び出し:ターンを一時停止し、オペレーターに承認を求める if res == nil { return CommandOutput{}, tool.Interrupt(CommandInterrupt{ Command: input.Command, Reason: input.Reason, Risk: input.Risk, }) } // 2. 再開呼び出し:拒否された場合、エラーではなく構造化フィードバックを返す if !res.Approved { return CommandOutput{ Command: input.Command, Status: "rejected", Stderr: fmt.Sprintf("Operator denied execution: %s", res.Feedback), Executed: false, }, nil } // 3. 承認時:30秒のタイムアウト付きでコマンドを実行する execCtx, cancel := context.WithTimeout(ctx.Context, 30*time.Second) defer cancel() out, err := exec.CommandContext(execCtx, "sh", "-c", input.Command).CombinedOutput() // ... stdout と終了コードを含む CommandOutput を返す }, ) } ``` `tool.Interrupt` はターンを一時停止する際、`CommandInterrupt` 構造体(`Command`、`Reason`、`Risk`)をレスポンスストリームに直接シリアライズして流し込むため、フロントエンドは追加の API エンドポイントを用意することなくユーザー確認用プロンプトを表示するために必要な全情報を取得できます。 ### コックピットでのコマンド承認と拒否 `runCommand` がインタラプトをトリガーすると、ストリーミング中のターンは一時停止し、ブラウザはレスポンスオブジェクトを通じてインタラプトペイロードを受け取ります。コックピットは AIDA のステータスバッジを `APPROVAL REQUIRED`(承認待ち)へと切り替え、インラインの承認カードを描画します。そのカードには、実行予定の正確なコマンド、理由、リスクレベルが表示され、任意のフィードバック入力欄と **Approve** / **Deny** ボタンが配置されます。 ![提案されたシェルコマンド、リスクバッジ、フィードバック入力欄、承認・拒否ボタンを表示し、runCommandのHuman-in-the-loopインタラプトで一時停止したAIDAコックピット](aida-hitl-interrupt.png "シェルコマンド実行前にユーザーの承認を求めるため、Genkitのツールインタラプトで一時停止したコックピット。") いずれかのボタンをクリックすると、一時停止されていたツールリクエストに判定結果が紐付けられ、Genkit クライアントの `chat.resumeStream(...)` が呼び出されます。Go バックエンドはコマンドの実行(またはスキップ)を完了し、更新されたゲージやコマンド出力を備えた AIDA の検証ダッシュボードをサーフェスへ直接ストリーミングで返送します。 ![runCommandインタラプトを承認した後のAIDAコックピット。ディスク容量ゲージとシェルコマンド出力を備えた検証用A2UIサーフェスを描画している様子](aida-hitl-resumed.png "承認後の実行再開:AIDAがコマンドを実行し、サーフェス上に検証ダッシュボードをストリーミング描画した様子。") ## まとめ [`go-aida`](https://github.com/danicat/go-aida) における AIDA の書き直しが完了したことで、フルスタックのアーキテクチャが完成しました。前編・後編の2回にわたる記事を通じて、私たちは Python と SQLite によるかつての RAG プロトタイプと静的なチャット画面を、単一バイナリの Genkit Go サービスとリアルタイムの A2UI 診断コックピットへと完全に刷新しました。 この開発フェーズを通じて、特に際立った設計上の教訓が2つあります。 1. **ドメイン特化型カタログは汎用 UI ブロックに勝る:** AIDA に7つのカスタム診断計器を与えたことで、ネストされたカードの中に Markdown の長文が並ぶだけだった出力は、生きたシステムダッシュボードへと生まれ変わりました。カタログはフロントエンドとバックエンドの双方を結ぶ共有コントラクトとして機能します。フロントエンドのコンポーネントを更新したら Go 側のカタログも合わせ(逆も同様)、`a2ui` ミドルウェアがそれらのスキーマをシステムプロンプトに注入して `validate: strict` を強制することで、モデルは常に有効でサポートされた計器を描画できるようになります。 2. **ツールインタラプトがホスト修復の安全性と双方向性を両立する:** Genkit のツールインタラプトを利用することで、Go バックエンドはシェルコマンドを実行する前に一時停止し、ユーザーが承認または拒否した後にクリーンに処理を再開できます。致命的なエラーではなく構造化された拒否フィードバックを返すことで、エージェントは会話の流れを壊すことなく状況に適応し、より安全な代替案を提示できます。 --- # Antigravity UI Extensionsでビジュアルツールを構築する - **Markdown URL**: https://danicat.dev/ja/posts/building-sidecars-and-ui-extensions-in-antigravity/index.md - **HTML URL**: https://danicat.dev/ja/posts/building-sidecars-and-ui-extensions-in-antigravity/ - **Date**: 2026-10-13 - **Categories**: Agentic Coding - **Tags**: agent-skills, antigravity, gemini, tutorial, vibe-coding - **Summary**: Google Antigravity 2.22.0の新機能「UI Extensions」でサイドパネルに双方向のUIを構築する方法を解説。 普段のブログ記事ではかなり地に足のついたトーンで書くことが多い私ですが、今回ばかりは*本当に*ワクワクしています。先週、Antigravityチームがリリースした [Antigravity 2.22.0](https://antigravity.google/releases?tab=hub&version=2.22.0) に、私の度肝を抜く新機能が含まれていたからです。それが、Antigravityプラグインにおける **UI Extensions** のサポートです。なぜこれが重要なのでしょうか?コーディングエージェントを使った開発体験(DX)そのものを根本からより良いものへと変えてしまうからです。さっそく見ていきましょう。 UI Extensionsを使うと、Antigravityのサイドパネル内にカスタムユーザーインターフェースを描画できます。これが素晴らしい最大の理由は、画像編集、ソフトウェアテスト、ログマイニング、データ分析といった、これまでなら別々の外部アプリを立ち上げる必要があった機能をオンボードで取り込み、Antigravityのインターフェースを自在に拡張できるようになることです。 たとえば、次のアプリのUIを設計している場面を想像してみてください。これをテキストだけで説明するのは、たいていひどい体験になります。絵を描けばテキストよりもはるかに正確に意図を伝えられますし、マルチモーダルであるGeminiなら描いた図をそのまま理解してくれます。UI Extensionsが登場する前は、[Excalidraw](https://excalidraw.com/) のような画像エディタや作図ツールを立ち上げてスケッチを描き、それをコピー&ペーストしてプロンプトに貼り付けていました。それ自体が最悪というわけではないものの、少し面倒な作業でした。しかしUI Extensionsがあれば、AntigravityのUI内に本格的な作図ツールを常駐させ、ボタンを1回クリックするだけで注釈付きの画像をコーディングエージェントのセッションへ送り、即座に反応してもらうことができます。 これは氷山の一角にすぎません。Antigravityから一歩も出ることなく、その場で画像やログ、あらゆる補助データを添えて作業内容に注釈をつけ、視覚的に問題を説明できること——これこそが、ついに「未来に生きている」と私に確信させた理由です。 それを証明するために、今すぐ試せるUI Extensions搭載のオープンソースプラグインを2つ作成しました。マルチレイヤー描画・編集・AI画像リファインメントに対応する [**Antigravity Paint Studio (`agy-paint-studio`)**](https://github.com/danicat/agy-paint-studio) と、Go言語で2Dゲームを構築し視覚的にデバッグするための [**Antigravity Game Studio (`agy-game-studio`)**](https://github.com/danicat/agy-game-studio) です。 これらが裏側でどのように動いているのか見ていきましょう。 ## UI extensionの構造 UI extensionとは、インタラクティブなWebパネルを公開し、Antigravityインターフェース内に描画できる [**sidecar(サイドカー)**](https://antigravity.google/docs/sidecars/) を含んだプラグインのことです。通常のプラグインと同様に、スキル(Skills)やMCP、ルール(Rules)を同梱できるため、特定のドメイン(たとえば画像編集など)を中心とした完結した体験を構築できます。 プラグインはワークスペース単位(`.agents/plugins`)またはグローバル単位(`~/.gemini/config/plugins`)のどちらにもインストール可能です。スキルと同様に、所定のディレクトリ構造に従う必要があります。 ```text plugins// ├── plugin.json # 必須のマーカー兼マニフェストファイル ├── mcp_config.json # MCPサーバー定義(任意) ├── hooks.json # フック定義(任意) ├── skills/ # スキル用ディレクトリ(任意) │ └── / │ └── SKILL.md ├── agents/ # サブエージェント定義テンプレート(任意) │ └── .md ├── rules/ # ルール用ディレクトリ(任意) │ └── .md └── sidecars/ └── / └── sidecar.json # UI extensionsに必須 ``` 通常のサイドカーはどのプログラミング言語でも構築できますが、UI extensionsでは [Node.js](https://nodejs.org/) を活用します。Antigravityにはエージェントのメインプロセスとサイドカープロセス間の通信を簡略化する **Sidecar SDK** が同梱されており、ポートの自動検出やメッセージングといった処理はすべてSDKが自動的に引き受けてくれるためです。 余談ですが、[Devoxx Belgiumでの「State of Agentic Coding」セッション](https://www.youtube.com/watch?v=nHRSwiFNQ9E) で実演したように、Node.jsやSidecar SDK、UI Extensionsのサポートを一切使わずに同じ体験を実現すること自体は可能です。ただ、自前で実装するアプローチの問題は、ポート割り当て(および競合の回避)やセッションの検出・通信をすべて泥臭く処理しなければならない点にあります。Sidecar SDKを使えば、そうした面倒が一気に解消されます。 ![AntigravityのUI extensionとサイドカーのアーキテクチャ](architecture-diagram.png "Antigravityがプラグインのサイドカープロセスを監視し、補助サイドパネルとアクティブな会話を橋渡しする仕組み") エントリーポイントは `sidecars//sidecar.json` です。このファイルで、バックグラウンドプロセスの起動方法とWeb UIをサイドパネルに描画する方法をAntigravityに伝えます。 ```json { "command": "node", "args": ["main.mjs"], "restart_policy": "always", "display_name": "My Extension", "has_web_ui": true, "ui_config": { "views": [ { "path": "/", "entrypoint": "SIDECAR_UI_ENTRYPOINT_AUX_PANE", "title": "My Extension" } ] } } ``` ここで注目すべきは `has_web_ui` 属性と `ui_config` プロパティです。この2つが、通常のサイドカーをUI extensionへと変える鍵になります。 `"command": "node"` はAntigravity同梱のNode.jsランタイムと `sidecar_sdk` を使用するため、`npm install` も `node_modules` も一切不要です。`main.mjs` のバックエンド実装は、わずか数行のJavaScriptで完結します。 ```javascript import { readFileSync } from 'node:fs'; import { SidecarApp } from 'sidecar_sdk'; const app = new SidecarApp(); // Re-reading on each request lets edits appear when you reopen. app.page('/', () => readFileSync(new URL('./index.html', import.meta.url), 'utf8') ); // POST routes return JSON and verify the sidecar token automatically. app.api('/api/save', (data) => ({ saved: true, name: data.name })); app.run(); ``` フロントエンド側では、HTMLの `` に `` を追加するだけで、Antigravityのライト/ダークテーマ変数が同期され、`window.sidecar` が公開されます。これにより、UIからバックエンドの呼び出し(`fetch()`)、開いているワークスペースの取得(`getWorkspaceUris()`)、そしてアクティブなチャットターンへの直接メッセージ送信(`agent.sendMessage()`)が可能になります。有効化すると、右上の **⋮** メニューの **Extensions** から開いたり、`file_extensions` に一致するファイルをクリックしたり、チャット内の `sidecar://` ピルをクリックしてパネルを開いたりできます。 ## 自分のUI extensionsを簡単に作る方法 こうした実装の詳細を知っておくのはいざという時に役立ちますが、正直なところ、皆さんがこれらの拡張機能を手書きしなくて済むことを願っています。というのも、Antigravityは「自己認識(self-aware)」を持っており、UI extensionsの作り方を自分自身で理解しているため、ただ作ってと頼むだけでいいからです。明示的に指示したい場合は、`/plugin` スキルや `/ui-extension` スキルを呼び出すよう頼むことができます。また、カスタマイズ全般について「教え込みたい」場合は `/agy-customizations` スキルも使えます。 「Antigravity Paint Studio」も「Antigravity Game Studio」も、まさにこの方法で構築しました。それでは、両方の拡張機能を見ていきましょう。 ## Paint StudioでAntigravityから出ずに画像を編集する **Paint Studio** は、冒頭で触れたユースケースそのものから生まれました。「百聞は一見に如かず」と言うように、コンテキストスイッチなしで、画像を使って自分の抱えている問題をエージェントに説明したかったのです。つまり、Antigravityの中でゼロから素早くアイデアをスケッチしたり、ワークスペースやチャットセッション内の画像をクリックしてサイドパネルで開き、その上にメモを書き込んでエージェントに送り返せる場所が必要でした。 ![補助サイドパネルにドッキングされたPaint Studioで猫のスケッチを水彩画風イラストへリファインしている様子](paint-studio-sidepane.png "補助サイドパネルにドッキングされたPaint Studio。左側の10秒で描いた猫の落書きを、右側で水彩画風のイラストに変換している") この構成が本当に楽しいのは、ワークフローの **両側にAIがいる** 状態になることです。左側のチャットパネルにはコーディングエージェントがおり、右側のPaint Studio内部にもGemini(この場合はNano Banana 2 Lite、別名 `gemini-3.1-flash-lite-image`)が直接組み込まれています。上のスクリーンショットでは、あの小さな黒猫「smol void」を10秒ほどで描いた後、数回のリファインメントステップを経て、Nano Banana 2 Liteがサイドパネルに表示されている水彩画へと仕上げてくれました。 また、Paint StudioではAntigravityのもうひとつの興味深いUI機能である **Generative UI(生成UI)** も活用しています(実際、[Paint Studioの最初のプロトタイプ](https://x.com/danicat83/status/2100298197931372652) は `/generative_ui` だけで作ったものでした)。Generative UIがUI Extensionsと異なるのは、チャットウィンドウ内にインラインで描画される点です。スキル `/generative_ui` によって起動され、Paint Studioのような再利用可能なサーフェスを作る用途にも、1回限りのデータ可視化のような使い捨てのサーフェスを作る用途にも使えます。 [Post on X](https://x.com/danicat83/status/2100298197931372652) Generative UIは、高さ最大500pxのサンドボックス化されたiframeを描画する `` ブロックを発行することで動作します。このiframe内には、インラインJavaScriptとホストが提供するセマンティックなTailwindクラス(`bg-[var(--card)]` や `text-[var(--foreground)]` など)を使って **ほぼ** 何でも配置できます。ただし、コンテンツセキュリティポリシー(CSP)によって外部CDNはAntigravity同梱のTailwindスクリプト(`https://www.gstatic.com/antigravity/web/dev/tailwindcss.min.js`)を除いてすべてブロックされており、WebAssembly(WASM)を実行することもできません。 Paint Studioでは、フルUI(サイドパネルやポップアウト表示用)とシンプルUI(`/generative_ui` によるインラインパネル用)のどちらも描画できる専用エンドポイントを用意しています。さらにプラグインには `/paint-studio` スキルを同梱してあるため、ちょっとした図が必要になったときにすぐスタジオを開ける便利なショートカットとして使えます。 ![/paint-studio と /generative_ui によってインライン描画されたPaint Studioキャンバスと黒猫smol voidのスケッチ](paint-studio-inline.png "/paint-studio スキルを呼び出すと、素早いスケッチ用のインラインパネルが /generative_ui 経由で読み込まれる") さらに便利な機能として、`sidecar.json` に拡張機能で開くファイル形式(`*.psd` や `*.png` など)を登録しておくこともできますし、Extensionsメニューから白紙のキャンバスとしてアプリを起動することも可能です。 ```json { "ui_config": { "display_name": "Paint Studio", "views": [ { "path": "/", "entrypoint": "SIDECAR_UI_ENTRYPOINT_AUX_PANE", "title": "Paint Studio", "file_extensions": [".psd", ".png", ".jpg", ".jpeg", ".webp"] } ] } } ``` これにより、完全な双方向コミュニケーションが確立されます。任意のファイルをクリックしてスタジオで開き、注釈やレタッチ、AIによる改善を加えて、ボタン1つでチャットへ送信できるのです。 ## Antigravity Game Studioで2Dゲームをテストプレイ&デバッグする ゲーム開発用のスキルを作って以来、これをどうすれば次のレベルへ引き上げられるかずっと考えていました。スキルは基本的にプロンプトなので、それだけで到達できる範囲には限界があります。ゲーム開発には、アートディレクション、アセット制作、レベルデザイン、HUDなど、視覚的な要素が非常に大きく関わってくるからです。 Paint Studioで新しい体験を探求した後、ゲーム開発者向けの開発体験も同じように再設計できないかと考え始め、そこで生まれたのが **Antigravity Game Studio** です。最初はあらゆるツールを詰め込もうとしたのですが、収拾のつかない巨大な塊になってしまいました。そこで、視覚的な入力から最も恩恵を受けられるたった1つの機能に絞り込むことにしました。それが **テストプレイ(playtesting)** です。 UI extensionである **Playtest Studio** が行うことはただ1つ。Goで書かれたゲームをWebAssemblyにコンパイルし、サイドパネル内でそのまま60 FPSでライブプレイ&デバッグできるようにすることです。プレイ中、Playtest Studioは(DVRのように)直近 **10秒間のリプレイバッファ** を常時記録しています。 ![DVR注釈モードで一時停止中のPlaytest Studio](playtest-studio-annotate.png "Playtest Studioでゲームを一時停止し、10秒間のタイムラインを巻き戻してスポーンバグの上に赤枠とメモを書き込んでいる様子") ゲーム内で何かおかしな挙動を見つけても、デバッグのループはわずか数秒で完結します。 1. `F8`(または `` ` ``)キーを押して **一時停止(Pause)** し、10秒間のタイムラインをコマ送りで巻き戻してバグが発生した決定的瞬間を捉えます。 2. 問題のあるスプライトの上に直接ボックスや矢印を描き込むか、画面周囲の余白に短いメモを書きます。 3. **📸 Frame** または **🎞️ 10s GIF** をクリックし、注釈付きのスクリーンショット(または10秒間のアニメーションGIFリプレイ)を、キャプチャされたWebAssemblyのコンソールログと一緒にチャットセッションへ直接送り、エージェントに修正してもらいます。 スタジオはこれらすべての情報を構造化されたメッセージとしてエージェントに送り返すため、テキストだけのプロンプトよりもはるかに曖昧さの少ない状態で、即座に修正作業に取りかかることができます。 ![Playtest StudioからAntigravityのチャットセッションに送信された構造化バグレポートメッセージ](playtest-bug-report.png "Playtest Studioからのバグレポート付きメッセージをAntigravityが受信した瞬間のスクリーンショット") 以下の動画では、このワークフロー全体の実際の動きをご覧いただけます。エージェントが作り終えたばかりの2Dタワーディフェンスゲームを初めてPlaytest Studioで開き、視覚的なフィードバックと10秒間のGIFをチャットに送って、その場でゲームの経済バランスを調整しています。 [YouTube Video](https://www.youtube.com/watch?v=A2zOv5Xoh0M) ## まとめ エージェントのすぐ隣に常駐し、ワンクリックで対話できるカスタムビジュアルツールを自作できる機能は、コーディングワークスペースのあり方を根本から変えてしまいます。外部アプリへのコンテキストスイッチやスクリーンショットのコピー&ペーストを繰り返す代わりに、今取り組んでいる課題に合わせてAntigravityのインターフェースそのものを自在に形作ることができるのです。 これらのプラグインを実際に試してみたい方や、Antigravityに自分専用のUI extensionsを作らせるインスピレーションとして活用したい方は、ぜひGitHubリポジトリをチェックしてみてください。[**`danicat/agy-paint-studio`**](https://github.com/danicat/agy-paint-studio) と [**`danicat/agy-game-studio`**](https://github.com/danicat/agy-game-studio) の両方を公開しています。 それでは、楽しいスケッチを! --- # Python製のRAGエージェントをGoとGenkitで書き直した理由 - **Markdown URL**: https://danicat.dev/ja/posts/building-agentic-backends-with-genkit-go/index.md - **HTML URL**: https://danicat.dev/ja/posts/building-agentic-backends-with-genkit-go/ - **Date**: 2026-10-06 - **Categories**: Agent Development - **Tags**: agent-skills, ai-agents, gemini, genkit, golang, osquery, rag - **Series**: Gemini for Go Developers - **Summary**: PythonとSQLite RAGで構築した旧式の診断エージェントを、Genkit、型付きdotprompt、オンデマンドAgent Skillsを用いて単一バイナリのGoエージェントへ刷新した全貌を解説します。 1年少し前、私は人生で初めての自律型エージェントであるAI診断エージェント「[AIDA(AI Diagnostic Agent)](https://danicat.dev/ja/posts/20250531-diagnostic-agent/)」を作りました。彼女の使命は、オープンソースツールである [osquery](https://osquery.io/) を介してオペレーティングシステムにクエリを実行し、コンピュータのトラブルを診断することです。その間に何度かアップデートを重ねてきましたが、最近になるまでそのアーキテクチャ自体を根本から問い直すことはありませんでした。 当初PythonでAIDAを作った頃、モデルの能力は今とは異なる水準(当時は `gemini-2.5-flash`)にあり、[Agent Skills](https://agentskills.io) のような手法もまだ発明されていませんでした。`osquery` にはmacOS、Linux、Windowsごとに異なる数百ものテーブルが存在するため、[SQLiteを使ったローカルRAGソリューション](https://danicat.dev/ja/posts/20251103-building-aida-part-2/)を構築してスキーマ定義をオンデマンドで注入していました。データセット自体は小さく静的(1チャンクにつき1テーブル、エンベディングも1回限り)だったものの、そのためにベクトルデータベースを用意するのは大がかりすぎました。さらに当時のモデルはRAGツールへの検索クエリを常にうまく組み立てられるわけではなく、誤ったテーブルを取得したり関連テーブルを見落としたりすることがありました。 問題はRAGだけではありませんでした。AIDAのコードベース全体が、エンジニアリングされたというよりは黎明期の「バイブコーディング(vibe coding)」で場当たり的にハックされたものだったのです。 私は10年以上前から言い続けていますが、今こそこれまで以上に当てはまる真理があります。もしコードベースが足かせになっているなら、思い切って更地にしてゼロから作り直しましょう。v1の成長痛を経験したからこそ、v2で何をシンプルにすべきかが明確になります。かつては書き直し(リライト)に数ヶ月もの労力と社内政治コストが必要でしたが、今の時代なら昼食前に3つのクリーンなアーキテクチャを試作できます。 [Goにおける3大エージェントライブラリを比較](https://danicat.dev/ja/posts/20260825-gemini-for-go-developers-part-3-building-agents/)([GenAI SDK](https://pkg.go.dev/google.golang.org/genai)、[Genkit](https://genkit.dev)、[ADK](https://adk.dev))した上で、私は **Genkit** を使ってAIDAをGoでゼロから書き直すことにしました。PythonとRAGからGenkit Goへ移行したことで、次の4つが実現できました。 1. **AIDAを単一の自己完結型Goバイナリにコンパイル**:ローカルのCLIツールとしても、Cloud Run上のHTTPサービスとしてもそのまま動作します。 2. **カスタムSQLite RAGパイプラインをオンデマンドのAgent Skillsに置換**:あいまいな検索クエリに頼ることなく、OS固有の `osquery` テーブルカタログ全体を必要に応じて読み込めます。 3. **`dotprompt` とGo構造体によるコンパイル時の型安全性**:プロンプト入力から構造化出力まで型安全に検証できます。 4. **単発フローからステートフルなマルチエージェント委譲への進化**:ノイズの多いシステムテレメトリとWebリサーチを分離できます。 それでは、新しいアーキテクチャの各要素が実際にどう機能するかを順番に見ていきましょう。 ## コーディングエージェントとローカルワークフローのセットアップ アプリケーションコードを書く前に、2つのものが必要です。開発用UIとコマンドラインユーティリティを提供するローカルの `genkit` CLI、そしてコーディングエージェントにGenkit Goの使い方を教えるAgent Skillsです。 ### Genkit CLIとDev UI `genkit` CLIをインストールするには、公式のインストールスクリプトを実行します。 ```sh curl -sL cli.genkit.dev | bash ``` `genkit` CLIは、Genkit Goにおけるローカル開発の中枢を担います。プログラムを `go run .` で直接実行すれば通常のGoバイナリとして動作しますが、実行コマンドの先頭に `genkit start --` を付けることで、**Genkit Developer UI**(デフォルトは `http://localhost:4000`)にアクセスできるようになります。ここでは、Genkitプログラムの各コンポーネントを個別に視覚的に確認し、テストすることができます。 ```sh genkit start -- go run . ``` Dev UIでは、登録されたフロー、プロンプト、エージェントを対話的にテストできるほか、プロンプト、ツール呼び出し、トークン使用量、レイテンシのトレースを検査できます。 Dev UIに加えて、`genkit` CLIはフローの実行、トレースの可視化、ドキュメントの閲覧を行うためのコマンドラインヘルパーも提供しています。最後のドキュメント閲覧機能は、実装フェーズにおけるモデルのハルシネーションを防ぐのに特に役立ちます。 以下はいくつかの使用例です。 ```sh # Run a specific flow once from the terminal and exit (--stream or --wait optional) genkit flow:run diagnose '{"userQuestion": "how is my disk usage?"}' --non-interactive -- go run . # Browse and read the embedded Genkit Go documentation offline genkit docs:list go genkit docs:read go/middleware.md ``` ### 推奨されるAgent Skills 最近のSDKにおけるゴールドスタンダードは、公式のAgent Skillsを同梱して提供することであり、Genkitも例外ではありません。Genkit Goプログラムを開発するなら、探すべきスキルはこちらです。 - [`developing-genkit-go`](https://github.com/genkit-ai/skills): フロー、dotprompt、組み込みミドルウェア、実験的なエージェント機能を網羅した公式のGenkit Goスキルです。 また、Geminiモデルを使って開発するため、`gemini-api-dev` スキルもインストールしておくと便利です。 - [`gemini-api-dev`](https://github.com/google-gemini/gemini-skills): 最新のモデルIDや思考(thinking)設定を扱うための公式Geminiスキルです。 これらはVercelの [`skills` CLI](https://github.com/vercel-labs/skills)を使ってワークスペースにインストールできます。 ```sh npx skills add genkit-ai/skills --skill developing-genkit-go npx skills add google-gemini/gemini-skills --skill gemini-api-dev ``` あるいは、[`kungfu`](https://github.com/danicat/kungfu) にカタログを登録して、ジャストインタイム(JIT)で読み込むこともできます。 ```sh kungfu catalog add genkit-ai/skills kungfu catalog add google-gemini/gemini-skills ``` スキルの代わりにMCPサーバーを使いたい場合は、CLIに[MCPサーバー](https://genkit.dev/docs/go/mcp-server/)(`genkit mcp`)も同梱されています。最近の私は個人的にスキルの方へ傾倒していますが、特定のニッチなユースケース(ドキュメント検索など)においてはMCPにも依然として価値があります。 ## コアコンセプト Genkitは、もともとFirebaseチームによって作られ、その後独立したプロジェクトへと成長した生成AIアプリケーション向けのオープンソースフレームワークです。オリジナルの言語はJSですが、Genkit Goは熟練したGo開発者にとっても驚くほどイディオマティック(Goらしい自然な書き味)に感じられます。もっとも、ところどころにまだ少し粗削りな部分は残っていますが。`genkit.Handler` のようなアダプターや、ミドルウェア、強く型付けされたフローやプロンプトといったおなじみの構成要素のおかげで、典型的なGenkitプログラムは従来のWebサービスとさほど変わらない姿になります。 ### フロー(Flows) Genkitにおける基本単位は**フロー(flow)**です。これは、生成AIの呼び出しと前処理・後処理のロジックをまとめてラップする、強く型付けされた関数です。まずは、`osqueryi` を介してオペレーティングシステムにクエリを実行するツールを備えた、シングルショットの診断フローとしてAIDAをスタートさせましょう。 ```go type AIDARequest struct { OS string `json:"os,omitempty" jsonschema_description:"The target operating system (defaults to runtime.GOOS)"` UserQuestion string `json:"userQuestion" jsonschema_description:"The diagnostic question to investigate"` } type AIDAResponse struct { Message string `json:"message" jsonschema_description:"The diagnostic findings and recommendations"` } type OsqueryInput struct { Query string `json:"query" jsonschema_description:"The SQL query to execute in osquery."` } g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}), genkit.WithDefaultModel("vertexai/gemini-3.8-flash"), ) runOsquery := genkit.DefineTool(g, "runOsquery", "Runs a system inspection query using osquery.", func(ctx *ai.ToolContext, in OsqueryInput) (string, error) { out, err := exec.CommandContext(ctx, "osqueryi", "--json", in.Query).CombinedOutput() if err != nil { return "", fmt.Errorf("osqueryi failed: %w: %s", err, out) } return string(out), nil }, ) diagnoseFlow := genkit.DefineFlow( g, "diagnose", func(ctx context.Context, req AIDARequest) (AIDAResponse, error) { if req.OS == "" { req.OS = runtime.GOOS } resp, err := genkit.Generate(ctx, g, ai.WithSystem(fmt.Sprintf("You are the Emergency Diagnostic Agent for diagnosing %s operating system failures. Your name is AIDA.", req.OS)), ai.WithPrompt(req.UserQuestion), ai.WithTools(runOsquery), ) if err != nil { return AIDAResponse{}, fmt.Errorf("diagnose flow failed: %w", err) } return AIDAResponse{Message: resp.Text()}, nil }, ) ``` `genkit.DefineTool` は `OsqueryInput` 構造体を使ってJSONスキーマを生成し、`runOsquery` ツールを登録します。一方、`genkit.DefineFlow` はコールバック関数とともに名前付きでフローを登録します。コールバックのシグネチャを見ると、型付きのHTTPハンドラーに非常によく似ていることがわかります。`context.Context` と強く型付けされたリクエスト構造体を受け取り、型付きのレスポンスと標準的なGoの `error` を返します。 `DefineTool`、`DefineFlow`、そして `Generate` の第1引数として渡されている変数 `g` に注目してください。`g` は `*genkit.Genkit` インスタンスであり、プログラム内のすべてのフロー、プロンプト、ツール、モデル、ミドルウェアの中央レジストリとして機能します。`main` 内で `genkit.Init` を使って一度だけ `g` を初期化し、それを明示的に渡していきます。 正直なところ、私はコードが明示的であることを好むため、`genkit.WithDefaultModel` はあまり好きではないのですが、多数のフローを定義する場合にはボイラープレートを少し減らすのに役立ちます。フロー内にモデルをハードコードするよりも優れた選択肢については、このあとプロンプトのセクションで見ていきます。 `genkit.Init` は、ランタイムレジストリと可観測性(オブザーバビリティ)スタックの両方をセットアップします。フロー内のすべてのモデル呼び出しとツール呼び出しは、そのフローのトレース配下の子スパンとして自動的に記録されます。また、フロー内でデータベースへのクエリや外部APIの呼び出しといったカスタムの前処理・後処理を行う場合、そのロジックを `genkit.Run` でラップすれば、トレースのウォーターフォール内に独立した名前付きサブステップとして記録されます。 開発中は、CLIやDev UIからフローを実行できます。本番環境では、フローを通常のGo関数呼び出し(例:`myFlow.Run`)として呼び出すことも、HTTPエンドポイントとして公開することもできます。`genkit.Handler` を使えば、たった1行でフローをHTTPハンドラーに変換できます。 ```go mux := http.NewServeMux() mux.HandleFunc("POST /diagnose", genkit.Handler(diagnoseFlow)) port := os.Getenv("PORT") if port == "" { port = "3400" } // Note: "server" requires the server plugin: // import "github.com/firebase/genkit/go/plugins/server" log.Fatal(server.Start(ctx, "0.0.0.0:"+port, mux)) ``` アプリケーションに登録されているすべてのフローを一度に公開したい場合は、`genkit.ListFlows(g)` をループ処理できます。 ```go mux := http.NewServeMux() for _, flow := range genkit.ListFlows(g) { mux.HandleFunc("POST /"+flow.Name(), genkit.Handler(flow)) } log.Fatal(server.Start(ctx, "0.0.0.0:"+port, mux)) ``` なお、`genkit.Handler` 自体は認証処理を一切行わないため、公開エンドポイントは標準の `net/http` 認証ミドルウェアでラップするようにしてください。 ### プロンプト(Prompts) フローのコード内にプロンプトやモデル設定をハードコードする方法は、小規模なプロジェクトでは機能しますが、プロジェクトが大きくなるにつれて保守が難しくなります。今日ではコーディングエージェントが作業の大部分を担ってくれるとはいえ、長年培われてきたエンジニアリングのベストプラクティスを捨て去るべきではありません。コードの整理・構造化もその1つです。 Genkitは、[dotprompt](https://genkit.dev/docs/go/dotprompt/)(`*.prompt` ファイル)と呼ばれるフォーマットを使ったプロンプトテンプレートをサポートしています。`dotprompt` のプロンプトには、モデルへの指示だけでなく、モデルの選択や設定のためのメタデータ、スキーマ定義、システム指示、さらには会話履歴などを含めることができます。 AIDAのプロンプト、ツール、モデル設定を `diagnoseFlow` から切り出して `prompts/aida.prompt` に抽出すると、次のようになります。 ```markdown --- model: vertexai/gemini-3.8-flash tools: - runOsquery config: thinkingConfig: thinkingLevel: low input: schema: os: string userQuestion: string output: format: json schema: message: string --- {{role "system"}} You are the Emergency Diagnostic Agent for diagnosing {{os}} operating system failures. Your name is AIDA. {{role "user"}} {{userQuestion}} ``` デフォルトでは、`genkit.Init` は作業ディレクトリ内の `./prompts` から `.prompt` ファイルを自動的に読み込みます。エージェントを単一バイナリとして配布したい場合は、`//go:embed` を使って `prompts` ディレクトリをバイナリに直接埋め込み、`genkit.WithPromptFS` に渡すことができます。 ```go import "embed" //go:embed prompts/* var promptsFS embed.FS func main() { g := genkit.Init(context.Background(), genkit.WithPromptFS(promptsFS)) // ... } ``` プロンプトにはコードから `genkit.LookupPrompt` メソッドでアクセスでき、`myPrompt.Execute` で実行します。 ```go aida := genkit.LookupPrompt(g, "aida") resp, err := aida.Execute(ctx, ai.WithInput(map[string]any{ "os": runtime.GOOS, "userQuestion": "how is my disk usage?", })) ``` しばらくGoを書いている方なら、あの `map[string]any` は私と同じくらい気になるはずです。ありがたいことに、Genkitでは**データプロンプト(data prompts)**(`genkit.LookupDataPrompt`)と `genkit.DefineSchemaFor` を使うことで、先ほど定義した `AIDARequest` と `AIDAResponse` 構造体を直接 `aida.prompt` にバインドできます。 ```go // In prompts/aida.prompt, reference the Go types by name: // // input: // schema: AIDARequest // output: // schema: AIDAResponse // // Register both schemas with Genkit so the prompt file can resolve them: genkit.DefineSchemaFor[AIDARequest](g) genkit.DefineSchemaFor[AIDAResponse](g) aida := genkit.LookupDataPrompt[AIDARequest, *AIDAResponse](g, "aida") resp, _, err := aida.Execute(ctx, AIDARequest{ OS: runtime.GOOS, UserQuestion: "how is my disk usage?", }) // resp is of type *AIDAResponse // Note: The second return value is the raw *ai.ModelResponse ``` `DefineSchemaFor` と `LookupDataPrompt` を使えば、Go側で一度スキーマを定義するだけで、入力からパース済みの出力までコンパイル時の完全な型安全性を維持できます。 ### プラグイン(Plugins) Genkitは `plugins/` 名前空間配下のパッケージによって拡張します。プラグインの最も一般的な用途はモデルファミリーの登録ですが、`server` や `middleware` のようにより幅広い機能を提供するプラグインもあります。`server` は登録しなくても使えるという点で特殊なケースですが、ほとんどのプラグインは `genkit.Init` の呼び出し時に初期化します。 ```go import ( "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/googlegenai" "github.com/firebase/genkit/go/plugins/middleware" ) g := genkit.Init(ctx, genkit.WithPlugins( &googlegenai.VertexAI{}, // Gemini via Vertex AI &middleware.Middleware{}, // Registers Retry, Fallback, Skills, etc. ), ) ``` ミドルウェアという概念はほとんどのGo開発者にとっておなじみのはずですが、このプラグインはフォールバック、リトライ、そしてAgent Skillsといった重要な機能への入り口となるため、特筆しておく価値があります。次に、これを詳しく見ていきましょう。 ## SQLite RAGをAgent Skillsミドルウェアに置き換える AIDAは先ほど定義した `runOsquery` ツールを通じてすでに `osquery` コマンドを実行できますが、各オペレーティングシステム(`darwin`、`linux`、`windows`)にどのテーブルやカラムが存在するかについては、依然としてモデルの一般的な学習データに頼って推測しています。 旧式のPython版AIDAでは、1チャンクに1つのテーブル定義を格納したローカルのSQLite RAGデータベースでこの問題を解決していました。データセット自体は小さく静的だったものの、RAGの仕組みを構築するのは手間がかかり、さらに微妙な弱点がありました。当時の `gemini-2.5-flash` は最初に何を検索すべきかを推測する必要があり、RAGへの検索クエリが少しでも外れると、誤ったテーブルを取得したり関連するテーブルを完全に逃したりしていたのです。 今回のGoによる書き直しでは、RAGパイプラインを完全に削除し、[Agent Skills](https://agentskills.io) に置き換えました。Antigravityに[osqueryリポジトリ](https://github.com/osquery/osquery)へのリンクを渡し、プラットフォームごとの全テーブルとスキーマのカタログをまとめた3つのOS固有スキル(`darwin`、`linux`、`windows`)を `./skills` 配下に生成しました。ベクトル検索が正しいテーブルを返すのを祈る代わりに、AIDAは現在のOSに対応するスキルをオンデマンドで読み込み、完全なスキーマリファレンスを確実に取得します。 Genkitでは `ai.WithUse` を使って生成呼び出しにミドルウェアをアタッチします。そして `plugins/middleware` パッケージは、リトライ、モデルのフォールバック、および [Agent Skills](https://agentskills.io) の組み込み実装を提供しています。 ```go resp, err := genkit.Generate(ctx, g, ai.WithSystem(fmt.Sprintf("You are AIDA, a %s operating system diagnostic assistant.", runtime.GOOS)), ai.WithPrompt("My machine is running out of file descriptors. How do I inspect open files with osquery?"), ai.WithTools(runOsquery), ai.WithUse( &middleware.Retry{MaxRetries: 3, InitialDelayMs: 1000, BackoffFactor: 2}, &middleware.Fallback{ Models: []ai.ModelRef{ googlegenai.ModelRef("vertexai/gemini-3.5-flash-lite", nil), }, }, &middleware.Skills{ SkillPaths: []string{"./skills"}, }, ), ) ``` HTTPミドルウェアと同様に、`ai.WithUse` は指定した順序で合成されます。`Fallback` の前に `Retry` を配置することで、プライマリモデルをリトライした上で、それでも失敗した場合にセカンダリモデルへフォールバックします。 `&middleware.Skills{}` に `./skills` を指定すると、Genkitは各サブディレクトリをスキャンし、スキルの説明文(description)をシステムプロンプトに注入するとともに、`use_skill` ツールを登録します。これにより、AIDAは適切なOSスキーマやクエリパターンをオンデマンドで読み込めるようになります。 なお、`dotprompt` とは異なり、現時点では `Skills` ミドルウェアは埋め込みファイルシステム(`embed.FS`)をサポートしていないため、デプロイメントには `skills` ディレクトリを同梱する必要があります。 また、`genkit.Init` 時に `&middleware.Middleware{}` を登録しているため、これらのミドルウェアを `prompts/aida.prompt` 内で直接宣言することも可能です。 ```yaml --- model: vertexai/gemini-3.8-flash tools: - runOsquery use: - name: genkit-middleware/retry config: maxRetries: 2 - genkit-middleware/skills --- ``` さらに高度なユースケースでは、`ai.Middleware` インターフェースを実装してカスタムミドルウェアを作成し、生成、モデル、またはツール呼び出しを独自のロジックでインターセプトしてラップすることもできます。 ## Developer UIでの確認 ここまでの完全なコードは、コンパニオンリポジトリの [`github.com/danicat/gemini-for-go-developers/part-4/01-flow`](https://github.com/danicat/gemini-for-go-developers/tree/main/part-4/01-flow) にあります。 Genkit Developer UIでAIDAをテストするには、そのディレクトリから `genkit start` でアプリケーションを起動します。 ```sh export GOOGLE_CLOUD_PROJECT=your-project-id export GOOGLE_CLOUD_LOCATION=global # required for Gemini 3.x genkit start -- go run . ``` `http://localhost:4000` を開き、サイドバーから **diagnose** フローを選択して、以下のテストペイロードで実行してみてください。 ```json { "userQuestion": "what is my battery status?" } ``` 私のマシンでこのプロンプトを実行した結果がこちらです(右側のパネルのトレースで、スキルがアクティベートされている様子に注目してください)。 ![diagnoseフローを実行中のGenkit Developer UI](https://danicat.dev/ja/posts/building-agentic-backends-with-genkit-go/image-1.png) ## ステートフルなエージェント(実験的機能) 私は普段、実験的(experimental)な機能には手を出さないようにしているのですが、これだけは例外です。フローだけを使ってエージェントを実装することも十分に可能ですが、チャットボットや直列エージェント、並列エージェントといったより複雑なパターンを開発しようとすると、ゴルーチン、同期処理、エラーハンドリングなどを駆使して適切なパターンを自ら作り込むのにそれなりの労力がかかります。 エージェント抽象化の導入により、エージェント作成の宣言的な側面に焦点を当てたはるかに使い勝手の良いAPIが提供され、このプロセスが大幅に簡素化されます。 ### エージェントの定義 このAPIはまだ実験的機能であるため、Genkitの初期化時に `genkit.WithExperimental()` を指定して有効化する必要があります。セッションストアをバックエンドに持つ、ステートフルなマルチターンのターミナルエージェントへとAIDAを進化させる方法は以下の通りです。 ```go import ( aix "github.com/firebase/genkit/go/ai/exp" "github.com/firebase/genkit/go/ai/exp/localstore" genkitx "github.com/firebase/genkit/go/genkit/exp" ) g := genkit.Init(ctx, genkit.WithExperimental(), genkit.WithPlugins(&googlegenai.VertexAI{}, &middleware.Middleware{}), ) store := localstore.NewInMemorySessionStore[struct{}]() aida := genkitx.DefineAgent(g, "aida", aix.InlinePrompt{ ai.WithModelName("vertexai/gemini-3.8-flash"), ai.WithSystem(fmt.Sprintf("You are AIDA, a %s operating system diagnostic assistant. Use runOsquery to inspect the host.", runtime.GOOS)), ai.WithTools(runOsquery), ai.WithUse(&middleware.Skills{SkillPaths: []string{"./skills"}}), }, aix.WithSessionStore(store), ) // Interactive terminal session maintaining state across turns scanner := bufio.NewScanner(os.Stdin) var sessionID string fmt.Print("AIDA> ") for scanner.Scan() { input := strings.TrimSpace(scanner.Text()) if input == "" || input == "exit" { break } var opts []aix.InvocationOption[struct{}] if sessionID != "" { opts = append(opts, aix.WithSessionID[struct{}](sessionID)) } out, err := aida.RunText(ctx, input, opts...) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Print("AIDA> ") continue } sessionID = out.SessionID fmt.Printf("\n%s\n\nAIDA> ", out.Message.Text()) } ``` `out.SessionID` を保持して後続のターンに渡すことで、セッション全体にわたって会話履歴が維持されます。型パラメータの `[struct{}]` は、カスタムのセッション状態を付与せず、メッセージ履歴のみを永続化したいことをGenkitに伝えています。 フロー周辺のコードを改善するために使ったのと同じテクニックが、エージェントにもそのまま使えます。プロンプトのハードコードを避けるには、`genkitx.DefinePromptAgent` を使って `prompts/aida.prompt` をエージェントに直接バインドできます。 また、リモートフロントエンドから接続できるようにAIDAをHTTP経由で公開するには、`genkitx.AllAgentRoutes` を使って標準のGoルーターにターン実行、スナップショット、中断(abort)の各エンドポイントをマウントします。 ```go mux := http.NewServeMux() for _, route := range genkitx.AllAgentRoutes(g) { mux.HandleFunc(route.Pattern(), route.Handler()) } ``` ステートフルなエージェントの完全な実装は、コンパニオンリポジトリの [`github.com/danicat/gemini-for-go-developers/part-4/02-agent`](https://github.com/danicat/gemini-for-go-developers/tree/main/part-4/02-agent) で確認できます。 ### マルチエージェント委譲 1つのタスクを遂行するのに複数のエージェントが必要になるケースもあります。マルチエージェント・アーキテクチャの利点には、関心の分離(エージェントの注意力を高め、コンテキストの劣化=コンテキストロットを減らすため)や、並列化(応答時間を短縮するため)などがあります。 Genkitの実験的な `middlewarex.Agents` ミドルウェアを使うと、`aida` をメインのコーディネーター(調整役)ペルソナとして維持しつつ、AIDAの調査作業を2つのスペシャリストエージェントに分割できます。 - **`inspector`**: `runOsquery` とOS固有の `osquery` スキルを介して、ホストのテレメトリをクエリすることだけに専念します。 - **`researcher`**: Google検索グラウンディングを使用して、AIDAのメインの会話履歴を肥大化させることなく、見慣れないプロセス、バイナリ、エラーメッセージ、CVEをWeb上で調査します。 ```go // requires: // import ( // "google.golang.org/genai" // middlewarex "github.com/firebase/genkit/go/plugins/middleware/exp" // ) inspector := genkitx.DefineAgent(g, "inspector", aix.InlinePrompt{ ai.WithModelName("vertexai/gemini-3.8-flash"), ai.WithSystem(fmt.Sprintf("Inspect %s system telemetry using osquery and report diagnostic findings.", runtime.GOOS)), ai.WithTools(runOsquery), ai.WithUse( &middleware.Retry{MaxRetries: 5}, &middleware.Skills{SkillPaths: []string{"./skills"}}, ), }, aix.WithDescription[struct{}]("Inspects host metrics, running processes, and system configuration using osquery."), ) researcher := genkitx.DefineAgent(g, "researcher", aix.InlinePrompt{ ai.WithModelName("vertexai/gemini-3.8-flash"), ai.WithSystem("Research unfamiliar processes, binaries, error messages, and CVEs using Google Search and report concise findings."), ai.WithConfig(&genai.GenerateContentConfig{ Tools: []*genai.Tool{ {GoogleSearch: &genai.GoogleSearch{}}, }, }), }, aix.WithDescription[struct{}]("Searches the web for information on unfamiliar processes, binaries, error codes, and CVEs."), ) aida := genkitx.DefineAgent(g, "aida", aix.InlinePrompt{ ai.WithModelName("vertexai/gemini-3.8-flash"), ai.WithSystem(fmt.Sprintf("You are AIDA, a %s operating system diagnostic assistant. Delegate host inspection to inspector and web research to researcher, then synthesize findings for the user.", runtime.GOOS)), ai.WithUse( &middleware.Retry{MaxRetries: 5}, &middlewarex.Agents{ Agents: []aix.AgentRef{inspector.Ref(), researcher.Ref()}, MaxDelegations: 5, }, ), }, aix.WithSessionStore(store), ) ``` 委譲(delegation)はデフォルトでは同期的に実行されますが、AIDAに長時間実行されるスペシャリストのタスクをバックグラウンドで開始させ、ターンをまたいでその結果を結合させたい場合は、`middlewarex.Agents` で `Async: true` を設定することもできます。 1つ心に留めておくべきなのは、レイテンシのトレードオフです。まったく同じ `"what is my battery status?"` というクエリを両方のバージョン(`thinkingLevel` を `low` に設定)で計測したところ、シングルエージェント版(`02-agent`)は3回のLLM呼び出しで **9.56秒** で完了したのに対し、このマルチエージェント版(`03-multi-agent`)は5回のLLM呼び出しで **17.02秒** かかりました。1回あたりの呼び出しレイテンシはほぼ同じ(約3.3秒)であるため、追加の約7.5秒は純粋に、`aida` が `inspector` に委譲してからそのレポートを要約するための、コーディネーターの2回分の追加往復(ラウンドトリップ)から生じています。 並列プログラミングとよく似ていて、小さな *N* に対するコーディネーションのオーバーヘッドは、単純なタスクをかえって遅くすることがあります。ちょっとしたクエリであれば単一のエージェントの方が高速です。しかし実際のシステムにおいては、ノイズの多いツール出力を隔離し、コンテキスト使用量を最適化し、全体的な応答品質を向上させるために、1つの「巨大エージェント(uber-agent)」を特化型のサブエージェントに分割することは、多くの場合その追加の往復に見合うだけの価値があります。 マルチエージェントの完全なサンプルは、コンパニオンリポジトリの [`github.com/danicat/gemini-for-go-developers/part-4/03-multi-agent`](https://github.com/danicat/gemini-for-go-developers/tree/main/part-4/03-multi-agent) にあります。 ## 本番環境へのデプロイ ### ランタイム Genkit Goアプリケーションは `net/http` を使用する標準的なGoバイナリにコンパイルされるため、独自のランタイムへのロックインはありません。つまり、HTTPサーバーをホストするために普段使っているプラットフォームなら基本的にどれでも、Genkitアプリの実行環境としてそのまま利用できるということです。 私の第一選択肢は常にサーバーレスのコンテナランタイムです。Google社内での私は主に **Google Cloud Run** を使用しており、Cloud Runでは提供されていない機能がどうしても必要な致命的なブロッカーにぶつかった場合にのみ、より複雑なプラットフォーム(**Google Kubernetes Engine (GKE)** や **Gemini Enterprise** など)を選択します。 また、かつてはこうしたプラットフォーム選定の意思決定が今よりもずっと重要だった、という点にも触れておく価値があります。以前はプラットフォームの乗り換え(リプラットフォーミング)に多大な労力がかかったため、皆最初から「正解」を選びたがる傾向がありました。しかしコーディングエージェントがある今、アプリを新しいプラットフォームに適応させる作業は数日(場合によっては数時間)で終わります。ですから、プロジェクトの初期段階から複雑さを持ち込む言い訳はもはや存在しないのです。 これが理論上の話です。そして実践においては、過去1年間でCloud Runでは実現できないケースに遭遇したことは一度もありませんでした。ただただ、それくらい便利なのです!主な例外は、[ADK](https://adk.dev) を中心に据えてマネージドなセッション永続化やエンタープライズコネクタをそのまま活用したい場合であり、その点では([GoでAIエージェントを構築する](https://danicat.dev/ja/posts/20260825-gemini-for-go-developers-part-3-building-agents/)でも触れた)Gemini Enterprise Agent Platformとの間に強力なシナジーがあります。 なお、通常はエージェント(特にAntigravityのようなGeminiベースのもの)がCloud Runへのデプロイで助けを必要とすることはありません。ですが、もしあなたのエージェントが機嫌を損ねているようなら、[github.com/google/skills](https://github.com/google/skills/tree/main/skills/cloud/cloud-run-basics) からCloud Runスキルをインストールして、少し背中を押してあげるとよいでしょう。 ### 可観測性(Observability) ローカル開発中、`genkit start` は完全な実行トレースと構造化ログを `.genkit/traces`(これは `.gitignore` に追加しておくべきです)に記録するため、Dev UIでそれらを検査できます。本番環境でも、監視バックエンド上でトークン使用量、レイテンシ、そしてステップごとのトレースに対する同じ可視性を確保したいところです。 Genkitは[OpenTelemetry](https://opentelemetry.io/)の上に構築されているため、主に2つの選択肢があります。Google Cloudにデプロイする場合は、`googlecloud` プラグイン(または内部で同じエクスポーターを実行する `firebase` プラグイン)を使えば、すべてがCloud Logging、Cloud Trace、Cloud Monitoringに送信されます。チームでDatadog、Grafana、Honeycombなどの別のバックエンドを使用している場合は、標準のOTLPエクスポーターをGenkitのトレーサープロバイダーに直接アタッチできます。 典型的なCloud Runデプロイメントでは、`genkit.Init` を呼び出す前にGoogle Cloudエクスポーターを有効にします。 ```go import "github.com/firebase/genkit/go/plugins/googlecloud" func main() { ctx := context.Background() googlecloud.EnableGoogleCloudTelemetry(&googlecloud.GoogleCloudTelemetryOptions{ // Raw prompts and responses are logged by default; disable this for sensitive data: DisableLoggingInputAndOutput: true, // Set to true if you want to test Cloud export locally under `genkit start`: ForceDevExport: false, }) g := genkit.Init(ctx, // if you are deploying to GCP, auth via Gemini Enterprise (aka Vertex AI) is preferred genkit.WithPlugins(&googlegenai.VertexAI{}), ) // ... } ``` 代わりにOTLPバックエンドを使用したい場合は、`genkit.Init` の前に標準のOpenTelemetryスパンプロセッサを登録し、そのシャットダウン関数を `defer` します。 ```go import ( "context" "log" "github.com/firebase/genkit/go/core/tracing" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) func registerOTLP(ctx context.Context) func(context.Context) error { // Configured via standard OTEL_EXPORTER_OTLP_* environment variables. exp, err := otlptracegrpc.New(ctx) if err != nil { log.Fatalf("failed to build OTLP exporter: %v", err) } tp := tracing.TracerProvider() tp.RegisterSpanProcessor(sdktrace.NewBatchSpanProcessor(exp)) return tp.Shutdown } ``` ## まとめ AIDAをゼロから書き直した今回のプロセスは、過去12ヶ月でAIエンジニアリングがいかに成熟したかを改めて実感させてくれます。かつてはカスタムのSQLite RAGパイプライン、ローカルのエンベディングモデル、そして壊れやすい数百行のPythonグルーコード(これはPythonへの批判ではなく、私自身の初期のバイブコーディングへの反省です)を必要としていたものが、今ではGenkitとオンデマンドのAgent Skillsによって、型安全な単一のGoバイナリへとコンパイルできるようになりました。 Genkit Goへ移行したことで、`dotprompt` によるコンパイル時のスキーマ検証、ミドルウェアを通じた組み込みのリトライやスキル注入、そしてクリーンなマルチエージェント委譲を手に入れつつ、標準的なGoのHTTPサービスならではの高速な起動とシンプルなデプロイ性を維持できました。Gemini 3.8 Flashのような高速なモデルと組み合わせたときのキビキビとした応答性は、旧バージョンとは比較にならないほど快適です。 本記事ではAIDAのコアランタイムとバックエンドアーキテクチャに焦点を当てましたが、フロントエンド(A2UIと生成UIインタラクション設計)については続編の記事で詳しく掘り下げる予定です。これからGoでエージェントを構築しようとしている方や、初期に作ったRAGプロトタイプに限界を感じ始めている方は、ぜひGenkitを試してみてください。 --- # Agent Skills実践ガイド:開発効率を最大化するスキル作成術 - **Markdown URL**: https://danicat.dev/ja/posts/the-pragmatic-guide-to-agent-skills/index.md - **HTML URL**: https://danicat.dev/ja/posts/the-pragmatic-guide-to-agent-skills/ - **Date**: 2026-08-29 - **Categories**: Agentic Coding - **Tags**: agent-skills, antigravity, gemini-cli, vibe-coding - **Summary**: コーディングエージェント向けカスタムAgent Skillsの実践ガイド:ドキュメント更新、プロセス自動化、エージェント特性における具体的な活用法を詳しく解説。 信じられないかもしれませんが、[**Agent Skills**](https://agentskills.io)標準が誕生してからまだ1年も経っていません。[Anthropicによる最初のブログ記事](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)が公開されたのは2025年10月のことで、当初はClaude Codeの拡張機能として導入され、同年12月に正式なオープン標準となりました。この仕様は極めて実用的であったため、瞬く間に主要なコーディングエージェントやエージェント開発フレームワークに採用され、AIモデルにモジュール化された指示、決定論的なスクリプト、専門的なドメインワークフローを装備するためのデファクトスタンダードとなりました。 そして現在、多くの開発者がスキルやプログレッシブ・ディスクロージャー(段階的開示)モデルの利点を知るようになりました。しかし、私たちはスキルを真に効果的に活用し、自ら作成できているでしょうか? この記事では、スキルの発見や出所(来歴)の管理から、開発と最適化に至るまで、Agent Skillsを扱う上での主要な課題と実践的なアプローチを探っていきます。 本文中で紹介するすべてのスキルとサンプルは、Antigravity CLIおよびGemini 3.7 Flashを使用して検証していますが、他のエージェント基盤(ハーネス)やモデルをお使いの方もぜひ試してみてください。それでは、Agent Skillsの世界へ飛び込んでみましょう! ## スキルを作るべきか、既存のスキルを使うべきか ソフトウェアエンジニアが常に直面するジレンマ:「自分で作るべきか、それとも既存のものを調達すべきか?」 この問いにはさまざまなアプローチがありますが、私のプラグマティック(実践的)な判断基準を共有します: ```goat +-----------------------------------------+ | Is this process specific to your repo | | or codebase? | +-----------------------------------------+ | | Yes | | No | v | +-----------------------+ | | Do you know a skill |<-------------------+ | | that does the job? | | | +-----------------------+ | | | | | | Yes | | No | | v v | | +--------------------+ +------------------+ | | | Passes security & | | Spent too much | | | | integrity checks? | | time looking? | | | +--------------------+ +------------------+ | | | | | | | | Yes | No | Yes | No | | | | | | v | | | | | +---------------+ | | | | | | Search for | | | | | | | a skill | | | | | | +---------------+ | | | | | | | | | v v +------------+ | | +------------------+ | | | Build your own | +--------------------->| skill | | +------------------+ | | v v +-----------------------------------+ | Profit! | +-----------------------------------+ ``` 多少入り組んだフローチャートに見えるかもしれませんが、要点は明快です:信頼できる正式なソースがあるか、攻撃ベクトルに晒されていないという絶対的な確信がない限り、**自前で作るほうが良い**ということです。 スキル作成にかかる労力は非常に小さいため、自作をお勧めしない状況はほとんどありません。唯一の例外は、スキルの品質を判断するためのドメイン知識が不足している場合です。その場合、誤った挙動のエコーチェンバー(悪循環)を生み出してしまうリスクがあるため注意が必要です。 十分な知識があるなら、自前のスキル作成はコーディングエージェントによる開発者生産性を飛躍させる究極のハックとなります。だからこそ、すべての本格的なエンジニアは「スキルを作るスキル」をマスターすべきだと私は確信しています(言葉遊びのようですが)。 ## 「スキルを作るスキル」を極める 役立つスキルを作るための秘訣は、エージェントの行動パターンを観察することに尽きます。たった1回のコーディングセッションですぐに完璧なスキルを思いつくのは難しいかもしれませんが、エージェントを使い込み、その挙動を観察すればするほど、カスタムスキルで埋めるべき知識や振る舞いのギャップが見えてきます。 具体的な例を見ればより分かりやすいでしょう。まずは、私たちが作成できるスキルの種類(レベル分類)を見ていきましょう: ### レベル1:ドキュメント(Documentation) 特定の技術やライブラリについて解説し、モデルの内蔵知識を補強または上書きするスキルです。例えば、学習データに含まれる古い情報が原因で、モデルが非推奨のライブラリや古いSDK、陳腐化したコーディングパターンを提案してくることは日常茶飯事です。スキルを使ってソフトウェアに関するモデルの知識を最新化することは、この技術の最も基本的かつ効果的な使い道です。 ドキュメントスキルの典型的な構成は、最新のナレッジ、コマンドライン、コードスニペット、外部URLをまとめた単一の`SKILL.md`ファイルです。扱う領域が複雑な場合は、`SKILL.md`を参照ファイルやアセットファイルに分割することで、プログレッシブ・ディスクロージャーの利点を最大限に引き出すことができます。 エージェントが古いパターンや古いSDKバージョンを使っているのを見かけたとき、あるいは最適とは言えない実装を繰り返し生成するときに、このタイプのスキルを作成してください。多くのベンダーが提供するスキルもこのカテゴリに属します。例えば、主要なGoogle製品向けのスキルを集めた[Google Skills](https://github.com/google/skills)リポジトリなどが代表例です。 私が作成したドキュメントスキルの例として、[ebitengineer](https://skills.danicat.dev/game-dev/ebitengineer/)があります。このスキルは、Ebitengineで2Dゲームを開発している際に、行列演算の順序の誤り、モジュール分割の不備、状態管理の欠如、本番用フォントではなくデバッグ用テキストの乱用など、繰り返し発生する問題に気づいたことをきっかけに作成したものです。 ### レベル2:プロセス(Process) [コードレビューの実施](https://danicat.dev/ja/posts/20260303-code-reviews-in-2026/)、セキュリティ監査、パフォーマンス分析など、特定のプロセスやワークフローを厳密に実行させるスキルです。このタイプのスキルには、ドキュメント的な知識だけでなく、タスクを実行するためのカスタムスクリプトやCLIツールが同梱されるのが一般的です。これにより、毎回モデルにゼロからスクリプトを書かせる手間を省くことができます。 同じタスクを繰り返しエージェントに指示していることに気づいたとき、特にスクリプト化できる決定論的なステップが存在する場合は、プロセススキルを作成すべきです。私が多用しているプロセススキルとしては、ソーシャルメディア、Google Analytics、Google Search Consoleからデータを収集・分析する[analytics](https://skills.danicat.dev/analytics)スキルセットがあります。 オンラインでの存在感を最適化することは私の仕事の重要な一部ですが(誰にも見られないコンテンツを作っても意味がありません)、各プラットフォームのアナリティクス画面を1つずつ手作業で確認していた作業を自動化するためにこれらのスキルを開発しました。現在ではこれらのスキルを基盤とした「司令塔(command centre)」が整い、より多くの高品質なコンテンツ作成に集中できるようになりました。 ご自身の開発プロセスを振り返ってみてください。日次、週次、月次で繰り返している作業はありませんか? それらをスキル化して貴重な時間を節約し、成果物の品質を高めましょう(あるいはその両方を実現しましょう)! ### レベル3:特性(Traits) エージェントの性格、批評的な姿勢、思考様式そのものを変化させるスキルです。 このカテゴリで特に有名なのが、Matt Pocock氏による[`/grill-me`](https://github.com/mattpocock/skills)です。あまりの人気に、現在ではほとんどのエージェント実行環境(ハーネス)に標準機能として組み込まれています。このスキルの目的は、エージェントがユーザーに質問を投げかけることで必要な情報を引き出し、推測やハルシネーション(もっともらしい嘘)で要件を埋めてしまうのを防ぐことにあります。 私が気に入っている特性ベースのスキルには、[swarm-coding](https://skills.danicat.dev/agents/swarm-coding)、[double-diamond](https://skills.danicat.dev/agents/double-diamond)、[uno-reverse](https://skills.danicat.dev/agents/uno-reverse)などがあります。 Swarm codingは、サブエージェントの並列オーケストレーションに取り組むために私が最初に書いたスキルです。作成の全プロセスについては[サブエージェントの台頭](https://danicat.dev/ja/posts/20260722-the-rise-of-the-subagents/)で詳しく書いていますので、仕組みに興味がある方はぜひご覧ください。 Double diamondはswarm codingの精神的後継であり、課題の探索(discovery)と構想(inception)のフェーズを組み込んでいます。アジャイル開発やデザイン思考に親しんでいる方ならお馴染みの手法でしょう。「発散」と「収束」を繰り返す原則に基づき、まずは問題空間を探索するために発散し、その後具体的な技術解へと収束させていきます。例えば、本サイトの大規模な新機能を計画する際は、次のようにセッションを開始できます: ```text /double-diamond I would like to plan phase 3 of this website. I want to improve the visualization of skill cards, add metrics, and a like/star button so we can rank skills by popularity. ``` Uno reverseは、実装を担当するエージェントたちの「エコーチェンバー(同調の罠)」を打破するために、あえて敵対的・批評的な人格を持たせたエージェントです。この敵対的エージェントは提案された実装を批判し、不要な肥大化を削ぎ落とす機会を探ることで、過剰設計(オーバーエンジニアリング)への対抗策として機能します。アーキテクチャレビューの健全性チェックとしてよく活用しています: ```text Please run a /double-diamond process to refine ADR-0002. At the review phase, use an /uno-reverse agent to antagonise the proposal and present the results. ``` これらのスキルは単体でも非常に強力ですが、組み合わせることで真価を発揮します。私のお気に入りは、1つのプロンプトで `/grill-me` + `/double-diamond` + `/uno-reverse` を同時に実行することです。これにより、モデルはまず要件を詰める質問をし、次に複数の実装案を深く調査・発散し、最後にそれらを批判的に検証して最適なアーキテクチャへと収束させてくれます。 新しいスキルの着想を得るには、これまでのキャリアの中で自分やチームの仕事の質を高めてくれた(人間的な)スキルやファシリテーション技術を思い出してみてください。特にサブエージェントの登場以降、AIに対してマネジメント手法を適用するケースが増えていますが、これは偶然ではありません。AIも放っておけば人間と同じような連携ミスや偏りを起こしてしまうからです。 ## 継続的な改善ループ スキルを作るのは素晴らしいことですが、その価値を最大限に引き出すには継続的な改善が不可欠です。私の場合、実際のセッションでスキルを使うたびに微調整(ブラッシュアップ)を行っています。実際の開発現場で使って初めて、粗削りな部分やコンテキストの不足、バグに気づくことが多いからです。また、新しい高性能モデルが登場した際もスキルを見直す絶好の機会です。モデルの推論能力向上に合わせてスキル側もアップグレードできます。 この作業を頻繁に行うため、私は[`/skill-optimizer`](https://skills.danicat.dev/agents/skill-optimizer)というスキルを作成しました。[agentskills.io](https://agentskills.io)に掲載されているベストプラクティスに独自のノウハウを加えたもので、段階的開示モデルが適切に機能しているか検証し、補助スクリプトでトークン数を推定して各セクションを適切なサイズに保ちます。また、スキルの説明文を内部実装ではなく「得られるメリット」に焦点を当てるよう最適化します。例えば最適化前の私の[search-analytics](https://skills.danicat.dev/analytics/search-analytics)スキルは、検索アルゴリズムの内部構造に関する不要な記述で溢れていましたが、これはスキルの利用には関係のないトークンの無駄でした。本当に必要なのは「データをどう読み込み、どう問い合わせるか」だけです。 手動での改善に加えて、自動化されたアプローチもあります。それが**agent dreaming**と呼ばれる、過去のエージェントセッションから教訓を抽出する概念です。Antigravityでは、`/learn`スラッシュコマンドを使ってこのドリーミングを実行できます。学習セッションを開始すると、エージェントは会話履歴からパターンを分析して改善点を探し、コンテキストファイルの更新、設定ガードレールの調整、あるいは新しいカスタムスキルの生成などを提案してくれます。例えば、長時間のデバッグセッションの後には、次のように実行するだけです: ```text /learn look at our debugging sessions with Ebitengine matrix transforms and state management. Extract the recurring bugs and fixes into a new documentation skill with clear anti-patterns and examples. ``` ## アクティベーション:暗黙的より明示的が勝る スキルの説明文(description)を微調整して、モデルが必要なときに100%自動でスキルを呼び出してくれるようにしようと、私は途方もない時間を費やしてきました。しかし、これまでのところその試みはすべて失敗に終わっています。高い確率で呼び出されるレベルには到達できますが、現時点で完璧な自動呼び出し率を達成するのは困難です。 そのため、私はPythonの設計哲学(Zen of Python)から古い格言を取り入れ、開発スタイルを改めました:*「暗黙的であるよりは、明示的であるほうがいい(Explicit is better than implicit)」*。エージェントが自発的に意図したスキルを呼び出してくれる善意に期待するのではなく、呼び出してほしいスキルとそのタイミングを明確に指示するのです。 Antigravityでは、スキルがスラッシュコマンドに自動マッピングされるため、`/swarm-coding`と入力するだけで即座にスキルが呼び出されます。また、スラッシュ記法はプロンプトの先頭だけでなく文中でも機能し、スラッシュ付きの単語がスキル名であるという明確なヒント(hint)としてモデルに伝わります。例えばプロンプトの末尾に`/grill-me`を添えるだけでも、エージェントは確実にそのスキルを発動します。最近のAntigravity CLIでは、複雑または曖昧な指示を与えた際に、ハーネスがプロンプトの末尾に自動で`/grill-me`を補完してくれる場面も見られます。 ## スキルが増えすぎることの弊害 スキルの活用を本格化させると、誰もが必ずある問題に直面します。段階的開示モデルがどれほど優れていても、何十ものスキルを常時インストールし続けると、MCPサーバーと同様にコンテキストの肥大化(Context Bloat)を招いてしまいます。 これを改善するには、手動の運用規律によるアプローチと、より高度な抽象化レイヤーを導入するアプローチの2つがあります。 1つ目の方法はシンプルです:真に必要な場合を除き、スキルをグローバルにインストールしないこと。スキルはユーザー単位(通常は`~/.agents/skills`)にインストールすることも、特定のワークスペース単位(プロジェクト内の`/.agents/skills`)にインストールすることも可能です。 この方法は小〜中規模では有効ですが、多数のプロジェクトに無数のローカルスキルが分散してくると管理が難しくなります。それらをどうやって最新に保つのか? あるスキルの改善を他のプロジェクトにどう反映するのか? そして`.agents`ディレクトリはGitにコミットすべきなのか? といった問題が生じます。 この問題に対する最適な解決策は、「中央集約(またはフェデレーション)」と「出所(来歴・provenance)の追跡」を組み合わせることだと考えています。その第一歩となるのが**スキルカタログ**です。従来のスキル共有はGitのクローンやフォルダの手動コピーに頼っていましたが、公式のAgent Skills標準はリモートでの発見や配布をツール側に委ねています。そこで私は、Web経由でインデックスを公開するオンラインカタログマニフェストを`skills.danicat.dev`に作成しました。実際のマニフェストはこちらで確認できます:[https://skills.danicat.dev/catalog.json](https://skills.danicat.dev/catalog.json)。 スキルカタログは「スキルの発見可能性(discoverability)」の問題を解決します。エージェントにカタログを参照させることで、事前にインストールすることなく利用可能なスキルを把握させることができます。ただし、検索のたびにカタログ全体を読み込んでいてはコンテキスト肥大化を招くため、効率的なスキル検索手順が必要です。 また、カタログは「カタログからスキルへ」という一方向の流れを解決するだけです。スキルを最新に保つには、「スキルからカタログへ」という逆方向の参照が必要になります。これこそが私が「スキルの出所(skill provenance)」と呼んでいるものです。仕様では来歴の管理方法は規定されていませんが、管理には2つのアプローチがあります:1) 各インストールの出所を記録する外部スキルマネージャーを使うか、2) スキルのフロントマター(メタデータ)に直接カタログ情報を記載するかです。私は後者のほうが純粋に宣言的(declarative)であり、裏で何が起きているか明瞭であるため優れていると考えています。以下は私のカタログにおける典型的なフロントマターの例です: ```yaml --- name: swarm-coding description: > Orchestrates multi-agent hierarchical swarms using a divide-and-conquer architecture for complex, multi-system, or orthogonal engineering initiatives (e.g., concurrent backend, frontend, database, QA). Manages hierarchical Lead Agents and Specialists, disjoint work allocations, and strict parent-child communication. Activate whenever the user mentions 'swarm', requests multi-agent team coordination, or needs context isolation across multiple technical domains. license: Apache-2.0 metadata: category: agents tags: "swarm, subagents, parallel, orchestration, strategy, complexity, coordination" author: Daniela Petruzalek (daniela@danicat.dev) version: "0.2.0" catalog: https://skills.danicat.dev --- ``` `name`、`description`、`license`、`metadata`は仕様で定義された標準フィールドです。`metadata`の構造は自由ですが、私は推奨されている`version`と`author`を標準として採用し、さらに`category`、`tags`、`catalog`を拡張しました。カテゴリとタグは分類と検索を容易にし、`catalog`は正確な出所を記録します。これにより、スキルマネージャーは指定されたカタログURLにアクセスし、より新しいバージョンが存在するかどうかを確認するだけで簡単にアップデートを行えます。 エージェントが必要に応じて時々カタログを参照してスキルをインストールする程度であれば、カタログ全体の読み込みによるコンテキスト消費は見過ごせるかもしれません。しかし、理想を言えばもっとスマートな運用を行いたいところです。そこで登場するのが`kungfu`です。 ## "I know kung fu." [`kungfu`](https://github.com/danicat/kungfu)は、スキル管理を効率化するために私が開発したCLIツールです。名前の由来は映画『マトリックス』で主人公ネオの脳に武術のカンフーが直接「インストール」される有名なシーンから来ています。これこそが私がエージェントに対して抱いていた夢でした:必要な専門知識をいつでもオンデマンドで注入できるようにすることです。 私はエージェントに`kungfu` CLIの使い方を教える`kungfu`スキルを1つ用意しており、常時読み込んでおく必要があるのはこのスキルだけです。エージェントが新しいスキルを必要とした場合、`kungfu find`を使って名前、カテゴリ、タグ、説明文からカタログを検索します。レーベンシュタイン距離を用いた入力ミスの許容や「もしかして?」の提案機能も備わっています。検索結果からスコア順に最適なスキルが提示され、エージェントはそれを`kungfu load`でジャストインタイム(JIT)に即時読み込むか、`kungfu learn`で恒久的にインストール(グローバルまたはワークスペース)できます。以下の画像は、そのJIT読み込みの流れを示したものです: ![Terminal screenshot showing kungfu JIT loading the seo-optimizer skill](image.png "kungfu JIT loading the seo-optimizer skill") `kungfu`はインストールされたすべてのスキルを管理し、`kungfu list`で利用可能なスキル(ローカルまたはオンライン)を一覧表示できます。また、`kungfu update`はローカルのカスタマイズを保護しながら、サーバー側で更新されたスキルを安全にアップデートします。 `kungfu`はこの`catalog.json`の規約とフロントマターの拡張メタデータに依存しているため、リポジトリがマニフェストを公開しているか、スキル自身が出所を宣言している必要があります。コミュニティに存在する任意のスキルリポジトリとも連携できるよう、`~/.config/kungfu/state.json`にローカル状態を記録し、インストールの方法を問わずスキルの出所を追跡できるようにしています。 「マニフェストがあるのに、なぜわざわざメタデータにもカタログ情報を書くのか?」と思われるかもしれません。理由は2つあります。1つ目は宣言的な記述を好むからであり、2つ目は`kungfu`を使わずに手動でスキルを配置した場合でも、後から状態の調整やライフサイクル管理ができるようにするためです。 ## まとめ Agent Skillsは、静的な基盤モデルと日々の実践的なエンジニアリング・ワークフローとのギャップを埋めるための最も効果的な方法の1つです。古いSDKパターンを排除するためのシンプルなドキュメントスキルから始めるもよし、プロセススキルで反復的な分析作業を自動化するもよし、あるいは特性ベースのエージェントスウォームを探索するもよし、その投資対効果は極めて迅速に現れます。 [skills.danicat.dev](https://skills.danicat.dev)からアイデアを自由に取り入れたり、[`kungfu`](https://github.com/danicat/kungfu)を使ってオンデマンドでスキルを管理・読み込んだりしてみてください。どのような手法を選ぶにせよ、最も重要なのはプロセスを継続的に改善し、agentic codingの真の可能性を引き出し続けることです。 --- # GoでAIエージェントを構築する:GenAI SDK、Genkit、ADKの徹底比較 - **Markdown URL**: https://danicat.dev/ja/posts/gemini-for-go-developers-part-3-building-agents/index.md - **HTML URL**: https://danicat.dev/ja/posts/gemini-for-go-developers-part-3-building-agents/ - **Date**: 2026-08-25 - **Categories**: Agent Development - **Tags**: adk, ai-agents, cloud-run, gemini, genkit, golang - **Series**: Gemini for Go Developers - **Summary**: Goでレトロゲーム鑑定士エージェントを3つのパラダイム(Go GenAI SDKのツールループ、Genkitの型付きフロー、Google ADKのマルチエージェント)で実装し比較します。 コーディングエージェントを使ってGoコードを書くことは、AI活用の片面にすぎません。もう一つの側面は、GoでAI対応アプリケーションや自律型エージェントのランタイムそのものを設計・構築することです。 Goエコシステムには複数のライブラリが存在するため、どの抽象化レイヤーを選ぶべきかは必ずしも自明ではありません。本ガイドでは、エージェントの根本的な仕組みを紐解き、具体的なユースケースである**レトロゲーム鑑定士(Retro Game Appraiser)**を定義して、Goにおける3つの異なるパラダイムで段階的に構築していきます。 1. **[Go GenAI SDK](https://pkg.go.dev/google.golang.org/genai)** を使って直接構築する低レベルのエージェントループ 2. **[Genkit](https://genkit.dev)** を使って構築する構造化されたフローベースのパイプライン 3. Googleの **[Agent Development Kit (ADK)](https://adk.dev)** を使って構築するモジュール型マルチエージェント・セッション対応システム 最後に、クラウド上でGoエージェントを本番運用するための堅牢なランタイム環境(実行基盤)について比較・考察します。 本記事で紹介したすべてのサンプルの完全なソースコードは、GitHubのコンパニオンリポジトリ [**danicat/gemini-for-go-developers**](https://github.com/danicat/gemini-for-go-developers/tree/main/part-3) で公開しています。 ## エージェントの解剖学 「エージェント」という言葉は曖昧に使われがちですが、現代のAIエンジニアリングにおいては明確なアーキテクチャ定義が存在します。それは、**大規模言語モデル(LLM)**、1つ以上の**ツール**(実行可能な関数やAPI)、そしてフィードバックループ内で動作する**実行ハーネス(Harness)**から構成される自律型システムです。 ツールを持たない言語モデルは、単なるテキスト生成器やチャットボットに過ぎません。静的な学習時の重みやプロンプト内で渡されたコンテキストに基づいてのみ応答を生成します。モデルに「主体性(Agency)」— 外部の状態を検査し、仮説を検証し、現実世界でアクションを実行する能力 — を与えるには、実行可能なツールへの接続が不可欠です。 重要なアーキテクチャ上のポイントは、言語モデルが外部コードやAPIを直接実行することは決してないという点です。その代わり、**ローカルハーネス**(私たちが作成するGoプログラム)が仲介役として機能します。モデルが外部情報やアクションが必要だと判断すると、構造化された「関数呼び出し(Tool Call)」リクエストを発行します。ハーネスは対応するGo関数を実行して出力を取得し、その結果をモデルのターンに注入します。モデルは新しいコンテキストを評価し、さらに別のツールを呼び出すか、ユーザーへの最終回答を生成するのに十分なデータが揃ったかを判断します。 ```mermaid sequenceDiagram autonumber actor User as ユーザー participant Harness as エージェントハーネス (Go) participant Model as Gemini LLM (Search Grounding有効) participant Catalog as ローカルカタログDB User->>Harness: プロンプト: 「SFC版マザー2の完品が$350で見つかりました。買いですか?」 Harness->>Model: リクエスト (システムプロンプト + search_catalogツール + Google検索グラウンディング) Model-->>Harness: ツール呼び出し: search_catalog(query="EarthBound") Harness->>Catalog: ローカルインベントリを検索 Catalog-->>Harness: 返却: owned=true, condition="カセットのみ", price_paid=$180 Harness->>Model: ツール実行結果: {owned: true, format: "Loose", paid: 180} Note over Model: Geminiがサーバー側でGoogle検索グラウンディングを実行し相場を取得 Model-->>Harness: 最終自然言語回答 (検索引用メタデータ付き) Harness->>User: 「すでにカセットのみを所有しています。箱・説明書付きの完品(CIB)で$350なら非常にお買い得です...」 ``` すべてのツールがローカルクライアントでの実行を必要とするわけではありません。Geminiは、Googleのインフラ上で直接実行される**Google検索グラウンディング(Google Search grounding)**のようなネイティブ統合ツールをサポートしています。リクエスト構成で宣言すると、APIが市場価格の検索を透過的に解決し、ローカルネットワークの往復なしに応答へグラウンディングメタデータを注入します。一方、プライベートな業務データやロジックについては、スキーマ宣言、引数のディスパッチ、結果の返却をGoハーネス側が担当します。 ## エージェント設計: レトロゲーム鑑定士 3つの実装手法を直接比較するため、すべてのスタックでまったく同じエージェント「**レトロゲーム鑑定士(Retro Game Appraiser)**」を構築します。 私のようにレトロゲームを収集している方なら、これは非常にあるあるな悩みです。レトロゲーム市場やフリーマーケットを訪れた際、大好きなゲームを見つけて購入し、帰宅してから「あ、すでに持ってた…」と気づくことが一度や二度ではありません。さらに、レトロゲーム市場は価格変動が激しく、状態による価格差(カセットのみ vs 箱説付き完品 / CIB vs 未開封)が大きく、海賊版(ブートレグ)の流通も後を絶ちません。 前回の買い出しの際、Geminiを使ってリアルタイムに店頭価格をダブルチェックしてみたところ、驚くほど役に立ちました。実店舗ならではの購入体験やショップ支援のために多少のプレミアムを払うのは喜ばしいことですが、誰しも偽物を掴まされたり法外なぼったくり価格を払ったりはしたくありません。 私たちの鑑定士エージェントは、個人のコレクションを照合しつつ、Web上の最新相場データを参照してこの課題を解決します。 ### 機能とユーザーの対話例 コレクターは以下のような自然言語でエージェントに質問します。 * *「クロノ・トリガーは私のコレクションにありますか?」* * *「スーパーファミコン版のMOTHER2(EarthBound)で、箱・説明書付きの極上品が$350で見つかりました。すでに持っていますか?また、現在の相場と比べてお買い得ですか?」* * *「悪魔城ドラキュラX 月下の夜想曲はいくらで購入しましたか?市場価値は上がっていますか?」* ### ツール定義(Tool Contracts) 在庫や価格のハルシネーション(もっともらしい嘘)を防ぐため、エージェントは2つの信頼できる情報源を利用します。 1. **`search_catalog`(ローカルツール):** コレクターのローカルデータベースを検索するGo関数。タイトルやハード名で検索し、所有状況、状態、購入日、購入価格を返します。 2. **`google_search`(検索グラウンディング):** Web上のオークション履歴や専門店の相場を検索し、最新の平均取引価格や状態別の価格帯を取得するサーバー側ツール。 ### 推論戦略 購入相談を受けたエージェントは以下の手順で推論を進めます。 1. ローカルカタログを検索し、そのゲームをすでに所有しているか(どの状態で持っているか)を確認する。 2. Google検索を実行し、指定されたハードおよび状態における現在の適正相場を調べる。 3. データを統合して実践的なアドバイスを作成する。提示価格と相場を比較し、状態アップグレード(カセットのみから完品への買い替えなど)の価値を判断して、明確な購入判断を提示します。 まずは、Go GenAI SDKを使った直接的な実装から見ていきましょう。 ## Go GenAI SDKでエージェントを実装する **[Go GenAI SDK](https://pkg.go.dev/google.golang.org/genai)**(`google.golang.org/genai`)を使用したエージェント構築は、Gemini APIプロトコルに1対1で対応する**最も低い抽象度**のアプローチです。フレームワーク層が存在しないため、ツールのディスパッチループ、会話履歴の管理、終了判定をGoコードで明示的に制御します。 この低レベルな手法は、単発のスクリプト、実験的な検証、ファンクションコーリングやグラウンディングの挙動の学習、または完全に独自のループ制御を行いたい場合に最適です。依存関係のない単一のバイナリにコンパイルされるため、Cloud Run、Kubernetes、仮想マシンなど、あらゆる標準的なマイクロサービス環境に容易にデプロイできます。 以下が完全な実装コードです。 ```go package main import ( "bufio" "context" "fmt" "log" "os" "os/signal" "strings" "syscall" "google.golang.org/genai" ) // GameItem represents a collectible item in the user's personal inventory. type GameItem struct { Title string `json:"title"` Platform string `json:"platform"` Year int `json:"year"` Condition string `json:"condition"` // e.g. "Loose Cartridge", "CIB (Complete in Box)", "Mint" PricePaid float64 `json:"price_paid"` Notes string `json:"notes"` } // localCatalog simulates an inventory database for retro games. var localCatalog = []GameItem{ { Title: "Chrono Trigger", Platform: "Super Nintendo (SNES)", Year: 1995, Condition: "CIB (Complete in Box)", PricePaid: 210.00, Notes: "Includes original map and registration card.", }, { Title: "EarthBound", Platform: "Super Nintendo (SNES)", Year: 1994, Condition: "Loose Cartridge", PricePaid: 180.00, Notes: "Authentic board verified; label in excellent shape.", }, { Title: "Castlevania: Symphony of the Night", Platform: "Sony PlayStation", Year: 1997, Condition: "CIB (Black Label)", PricePaid: 135.00, Notes: "Original soundtrack disc included.", }, } // searchCatalogTool searches the local collection for matching games. func searchCatalogTool(args map[string]any) map[string]any { query, _ := args["query"].(string) queryLower := strings.ToLower(strings.TrimSpace(query)) var matches []GameItem for _, item := range localCatalog { if strings.Contains(strings.ToLower(item.Title), queryLower) || strings.Contains(strings.ToLower(item.Platform), queryLower) { matches = append(matches, item) } } if len(matches) == 0 { return map[string]any{ "found": false, "message": fmt.Sprintf("No items matching %q found in your collection.", query), } } return map[string]any{ "found": true, "count": len(matches), "results": matches, } } func main() { ctx := context.Background() // Initialise GenAI client for Gemini Enterprise client, err := genai.NewClient(ctx, &genai.ClientConfig{ Project: os.Getenv("GOOGLE_CLOUD_PROJECT"), Location: "global", Backend: genai.BackendEnterprise, }) if err != nil { log.Fatalf("failed to create client: %v", err) } // 1. Declare custom function schema for collection lookup catalogToolDecl := &genai.FunctionDeclaration{ Name: "search_catalog", Description: "Search the collector's personal inventory for owned games by title or platform.", Parameters: &genai.Schema{ Type: genai.TypeObject, Properties: map[string]*genai.Schema{ "query": { Type: genai.TypeString, Description: "Game title or platform to search (e.g. 'EarthBound', 'SNES').", }, }, Required: []string{"query"}, }, } // 2. Configure model tools: custom function declaration + Google Search grounding config := &genai.GenerateContentConfig{ SystemInstruction: &genai.Content{ Parts: []*genai.Part{ {Text: "You are an expert Retro Game Appraiser. When evaluating purchases, check the user's " + "collection catalog first to see if they already own the item, then check current market " + "prices using Google Search to evaluate whether the deal is fair, overpriced, or a bargain."}, }, }, Tools: []*genai.Tool{ { FunctionDeclarations: []*genai.FunctionDeclaration{catalogToolDecl}, }, { GoogleSearch: &genai.GoogleSearch{}, }, }, } // 3. Graceful shutdown on Ctrl+C (SIGINT) or SIGTERM sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { <-sigChan fmt.Println("\nGoodbye!") os.Exit(0) }() model := "gemini-3.8-flash" var contents []*genai.Content fmt.Println("Retro Game Appraiser (SDK Agent)") fmt.Println("Type your question below, or 'exit' (Ctrl+C / Ctrl+D) to quit.") fmt.Println("-----------------------------------------------------------------") scanner := bufio.NewScanner(os.Stdin) for { fmt.Print("\nUser: ") if !scanner.Scan() { fmt.Println("\nGoodbye!") break } input := strings.TrimSpace(scanner.Text()) if input == "" { continue } if strings.EqualFold(input, "exit") { fmt.Println("Goodbye!") break } contents = append(contents, &genai.Content{ Role: "user", Parts: []*genai.Part{genai.NewPartFromText(input)}, }) // 4. The Agent Loop: model generation -> tool dispatch -> feedback -> until final answer for { resp, err := client.Models.GenerateContent(ctx, model, contents, config) if err != nil { log.Printf("error generating content: %v", err) break } if len(resp.Candidates) == 0 || resp.Candidates[0].Content == nil { log.Println("received empty response candidate from model") break } // Append the model's response to the conversation history modelContent := resp.Candidates[0].Content contents = append(contents, modelContent) // Check if the model requested any client-side tool executions funcCalls := resp.FunctionCalls() if len(funcCalls) == 0 { fmt.Printf("\nAppraiser: %s\n", resp.Text()) break } // Execute each requested tool and prepare response parts var responseParts []*genai.Part for _, call := range funcCalls { fmt.Printf("[Harness] Executing tool: %s(args=%v)\n", call.Name, call.Args) var result map[string]any switch call.Name { case "search_catalog": result = searchCatalogTool(call.Args) default: result = map[string]any{"error": fmt.Sprintf("unsupported tool: %s", call.Name)} } responseParts = append(responseParts, genai.NewPartFromFunctionResponse(call.Name, result)) } // Return tool execution results as a user turn contents = append(contents, &genai.Content{ Role: "user", Parts: responseParts, }) } } } ``` ### SDKエージェントを実行する この例を実行するには、Google Cloudプロジェクトを設定し、Application Default Credentialsでログインします(`gcloud auth application-default login`)。 ```sh export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" go run main.go ``` インタラクティブなチャットセッションがターミナル上で開始されます。鑑定士エージェントと複数ターンにわたって対話でき、`exit`と入力するか、`Ctrl+C`または`Ctrl+D`(EOF)を押すことでいつでも安全に終了できます。 ```text Retro Game Appraiser (SDK Agent) Type your question below, or 'exit' (Ctrl+C / Ctrl+D) to quit. ----------------------------------------------------------------- User: I found a copy of EarthBound for SNES in mint Complete-in-Box (CIB) condition for $350. Do I already own it, and is $350 a good deal compared to current market prices? [Harness] Executing tool: search_catalog(args=map[query:EarthBound]) Appraiser: Here is your collection check and appraisal for **EarthBound (SNES)**: 1. **Current Collection Status**: - You currently own **EarthBound** on Super Nintendo as a **Loose Cartridge**, purchased for **$180.00**. 2. **Market Price Appraisal**: - Verified market sales for an authentic, **Complete-in-Box (CIB)** copy of EarthBound typically range between **$1,200.00 and $1,500.00** depending on the condition of the box, tray, and original player's guide. 3. **Recommendation**: - At **$350.00**, a genuine Mint CIB copy is an **exceptional deal** (more than 70% below prevailing market value). - **Caution**: Because EarthBound is one of the most heavily counterfeited SNES titles, inspect the box printing, registration card, and PCB board carefully before completing the transaction. If verified authentic, this is an outstanding opportunity to upgrade your loose copy to CIB. User: exit Goodbye! ``` SDKでの実装は、制御フローの仕組みが非常に分かりやすいのが特徴です。昨今の高性能なモデルと広大なトークンウィンドウを使えば、配管やディスパッチループを自前で書くこと自体は驚くほど簡単です。 しかし、「自前で作れる」からといって「作るべき」とは限りません。自作したフレームワークコードの1行1行は、将来にわたる保守の負担となり、並行処理やシリアライズに関する潜在的なバグの温床になります。独自のエージェントフレームワークを書くことは、機能開発の観点からは余計な回り道であり、最悪の場合は本来達成すべきビジネス目標からリソースを奪う技術的負債になりかねません。「最も優れたコードは書かないコードであり、次に優れたコードは最小限の自作コードで課題を解決するコードである」という原則を思い出す価値があります。 ## エージェント開発フレームワーク 生のSDKループは低レベルの仕組みを理解したり特殊なループを組むのには最適ですが、本番環境のアプリケーションではより高い抽象化が求められます。 * **スキーマの自動リフレクション:** `&genai.Schema{...}` でJSONスキーマを手動定義するのは冗長で誤りが発生しやすい作業です。フレームワークはGoの構造体やドキュメントコメントから直接スキーマを推論します。 * **可観測性と分散トレーシング:** 本番環境では、OpenTelemetryのトレース、ツールごとのレイテンシ、トークン消費量の追跡をコードに手を加えずに取得できる必要があります。 * **プロンプト管理:** プロンプトをGoコード内にハードコードすると、プロンプトエンジニアとの協業が難しくなり、バイナリのリリースとは独立したテンプレートのバージョン管理ができません。 * **セッション永続化と状態管理:** ステートレスなHTTPリクエスト間で複数ターンの会話履歴を安全に保持するには、スレッドセーフで疎結合なストレージが必要です。 * **モデルの移植性:** Go SDKはGemini専用ですが、フレームワークを使えばビジネスロジックを変更することなくモデルプロバイダを切り替えたりローカルモデルをテストしたりできます。 こうした要件に応えるため、Goエコシステムには2つの主要なオープンソースフレームワークが存在します。**Genkit** と **Agent Development Kit (ADK)** です。 | 比較軸 | Go GenAI SDK | Genkit Go | Agent Development Kit (ADK) | | :--- | :--- | :--- | :--- | | **抽象度** | **低**(Gemini APIへの1:1マッピング) | **中**(構造化ワークフロー) | **高**(自律型マルチエージェント) | | **コアアーキテクチャ** | 明示的な `for` ループとディスパッチ | **Flows** (`genkit.DefineFlow`) & Tools | **Agents**, Runners & Session Services | | **得意なユースケース** | スクリプト、仕組みの学習、特殊なループ制御 | 単発AIアプリ(CLI、Webサービス)、決定論的パイプライン、単一ドメイン | 会話型チャットエージェント、マルチエージェント連携、長期メモリ & RAG | | **モデル対応** | Gemini専用 | マルチモデル(Google GenAI, Vertex AI, Ollamaなど) | マルチモデル(ADKモデルアダプター経由) | | **推奨デプロイ環境** | 任意のHTTPホスト(Cloud Run, K8s, VM) | 任意のバックエンド; **Cloud Run**(推奨) | **Gemini Enterprise**(セッション/RAG推奨)または **Cloud Run** | それでは、レトロゲーム鑑定士をそれぞれのフレームワークで実装してみましょう。 ## Genkitでエージェントを実装する **[Genkit](https://genkit.dev)** は**中レベルの抽象度**に位置し、AIアプリケーションにソフトウェアエンジニアリングの規律と組み込みの可観測性をもたらします。Genkitでは、すべてが **Flows**(型安全で観測可能なパイプライン)と **Tools**(自動スキーマ生成を備えたGo関数)を中心に構成されます。 Genkitは、**単発の処理**(CLIツール、バッチ処理、Webhookなど)、決定論的な処理パイプライン、単一ドメインに特化したエージェント(以前構築した[マルチモーダル写真修復パイプライン](https://danicat.dev/ja/posts/20260216-genkit-go/)など)に最適です。プラグインを通じて複数のモデルをサポートし、標準的なGo HTTPサーバーとして動作するため、コンテナ管理と自動スケーリングに優れた **Google Cloud Run** が最適なデプロイ先となります。 以下はGenkit Goによる実装です。 ```go package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/googlegenai" "google.golang.org/genai" ) // GameItem represents a collectible item in the user's personal inventory. type GameItem struct { Title string `json:"title"` Platform string `json:"platform"` Year int `json:"year"` Condition string `json:"condition"` PricePaid float64 `json:"price_paid"` Notes string `json:"notes"` } // localCatalog simulates an inventory database for retro games. var localCatalog = []GameItem{ { Title: "Chrono Trigger", Platform: "Super Nintendo (SNES)", Year: 1995, Condition: "CIB (Complete in Box)", PricePaid: 210.00, Notes: "Includes original map and registration card.", }, { Title: "EarthBound", Platform: "Super Nintendo (SNES)", Year: 1994, Condition: "Loose Cartridge", PricePaid: 180.00, Notes: "Authentic board verified; label in excellent shape.", }, { Title: "Castlevania: Symphony of the Night", Platform: "Sony PlayStation", Year: 1997, Condition: "CIB (Black Label)", PricePaid: 135.00, Notes: "Original soundtrack disc included.", }, } type CatalogRequest struct { Query string `json:"query" jsonschema:"description=The game title or platform to search in the inventory"` } type CatalogResponse struct { Found bool `json:"found"` Message string `json:"message,omitempty"` Count int `json:"count,omitempty"` Results []GameItem `json:"results,omitempty"` } type AppraiserRequest struct { Prompt string `json:"prompt" jsonschema:"description=The collector's question or purchase offer to evaluate"` } type AppraiserResponse struct { Appraisal string `json:"appraisal"` } func main() { ctx := context.Background() // 1. Initialise Genkit with Vertex AI plugin g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{ ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"), Location: "global", }), ) // 2. Define strongly-typed tool with automatic schema generation catalogTool := genkit.DefineTool( g, "search_catalog", "Search the collector's personal inventory for owned games by title or platform.", func(ctx *ai.ToolContext, req CatalogRequest) (CatalogResponse, error) { queryLower := strings.ToLower(strings.TrimSpace(req.Query)) queryWords := strings.Fields(queryLower) var matches []GameItem for _, item := range localCatalog { itemText := strings.ToLower(item.Title + " " + item.Platform) allMatch := true for _, word := range queryWords { if !strings.Contains(itemText, word) { allMatch = false break } } if allMatch { matches = append(matches, item) } } if len(matches) == 0 { return CatalogResponse{ Found: false, Message: fmt.Sprintf("No items matching %q found in personal collection.", req.Query), }, nil } return CatalogResponse{ Found: true, Count: len(matches), Results: matches, }, nil }, ) // 3. Define structured appraisal flow with typed request and response appraiserFlow := genkit.DefineFlow( g, "appraise_game", func(ctx context.Context, req AppraiserRequest) (AppraiserResponse, error) { resp, err := genkit.Generate(ctx, g, ai.WithModelName("vertexai/gemini-3.8-flash"), ai.WithSystem( "You are an expert Retro Game Appraiser. Assist collectors by evaluating prospective purchases, "+ "cross-referencing their personal inventory, and assessing fair market valuations. "+ "Always search the collection catalog using search_catalog before providing purchase recommendations.", ), ai.WithConfig(&genai.GenerateContentConfig{ ThinkingConfig: &genai.ThinkingConfig{IncludeThoughts: true}, Tools: []*genai.Tool{ { GoogleSearch: &genai.GoogleSearch{}, }, }, }), ai.WithPrompt(req.Prompt), ai.WithTools(catalogTool), ) if err != nil { return AppraiserResponse{}, fmt.Errorf("appraisal generation failed: %w", err) } return AppraiserResponse{Appraisal: resp.Text()}, nil }, ) // 4. Mount flow directly using Genkit's built-in HTTP handler mux := http.NewServeMux() mux.Handle("POST /api/appraise", genkit.Handler(appraiserFlow)) port := os.Getenv("PORT") if port == "" { port = "8080" } server := &http.Server{ Addr: ":" + port, Handler: mux, } // Graceful shutdown on Ctrl+C (SIGINT) or SIGTERM serverCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go func() { log.Printf("Retro Game Appraiser (Genkit) listening on :%s", port) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server failed: %v", err) } }() <-serverCtx.Done() log.Println("\nShutting down server gracefully...") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("server forced shutdown: %v", err) } log.Println("Server exited cleanly.") } ``` ### Genkitフローを実行する Google Cloudプロジェクト環境変数を設定してGenkitサーバーを起動します。 ```sh export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" export PORT=8080 go run main.go ``` 別のターミナルから鑑定リクエストを送信します。 ```sh curl -s -X POST http://localhost:8080/api/appraise \ -H "Content-Type: application/json" \ -d '{"data": {"prompt": "I found a copy of EarthBound for SNES for $350. Do I own it, and is it a good deal?"}}' | jq . ``` フローが実行され、カタログツールを呼び出して構造化された鑑定結果が返されます。 ```json { "appraisal": "### 1. Catalog Check\n**Yes, you already own it.**\n* **Title:** *EarthBound* (SNES, 1994)\n* **Status in Collection:** Loose Cartridge\n* **Condition/Notes:** Authentic board verified; label in excellent shape.\n* **Price Paid:** $180\n\n---\n\n### 2. Market Appraisal & Deal Analysis\n* **Loose Cartridge:** The current going market rate for an authentic loose copy ranges between **$320 and $380**. At **$350**, it is priced right at **fair market value**—neither an overpriced listing nor a significant bargain.\n* **Complete in Box (CIB) / Boxed with Guide:** If this listing happens to include the original big box and strategy guide with scratch-and-sniff cards, $350 would be an extraordinary steal (CIB copies regularly sell for **$1,500–$2,500+**).\n\n---\n\n### 3. Recommendation\n* **Pass (if Loose):** Since you already have an authentic copy in excellent condition, paying retail market price ($350) for a duplicate loose cart does not offer strong value or upside.\n* **Buy immediately (if Complete/Boxed):** Only pull the trigger if it includes the original packaging or represents a major condition upgrade/variant.\n* **Buyer Beware:** If you do ever consider another copy, always inspect the PCB (printed circuit board) screws and chips, as *EarthBound* is one of the most frequently counterfeited games on the SNES." } ``` Genkitを使うことで、手動のディスパッチループを書く必要がなくなります。引数をGoの構造体にアンマーシャリングし、関数を実行して結果をモデルに戻し、各ステップのテレメトリトレースを自動的に収集してくれます。 ## Agent Development Kit (ADK) でエージェントを実装する Genkitが構造化されたアプリケーションパイプラインに主眼を置いているのに対し、Googleの **[Agent Development Kit (ADK)](https://adk.dev)** は、自律型会話エージェント、マルチエージェントのオーケストレーション、永続セッションやエンタープライズRAGを必要とする複雑なシステム向けに設計された**高レベルの抽象化**を提供します。 ADKは、エージェントのライフサイクル、サブエージェントへのタスク委譲、エージェント間通信プロトコル(A2A)を標準化しています。Genkitと同様に、モジュール式のモデルアダプターによりマルチモデルに対応しています。 実行基盤としては、自前でデータベースを組むことなくマネージドなセッション永続化やエンタープライズグラウンディングを利用したい場合は **Gemini Enterprise Agent Platform** が最適です。コンテナ化されたステートレスマイクロサービスとして独自に管理したい場合は **Google Cloud Run** が適しています。 以下はADK v2による実装です。 ```go package main import ( "context" "fmt" "log" "os" "strings" "google.golang.org/genai" "google.golang.org/adk/v2/agent" "google.golang.org/adk/v2/agent/llmagent" "google.golang.org/adk/v2/cmd/launcher" "google.golang.org/adk/v2/cmd/launcher/full" "google.golang.org/adk/v2/model/gemini" "google.golang.org/adk/v2/tool" "google.golang.org/adk/v2/tool/functiontool" "google.golang.org/adk/v2/tool/geminitool" ) // GameItem represents a collectible item in the user's personal inventory. type GameItem struct { Title string `json:"title"` Platform string `json:"platform"` Year int `json:"year"` Condition string `json:"condition"` PricePaid float64 `json:"price_paid"` Notes string `json:"notes"` } // localCatalog simulates an inventory database for retro games. var localCatalog = []GameItem{ { Title: "Chrono Trigger", Platform: "Super Nintendo (SNES)", Year: 1995, Condition: "CIB (Complete in Box)", PricePaid: 210.00, Notes: "Includes original map and registration card.", }, { Title: "EarthBound", Platform: "Super Nintendo (SNES)", Year: 1994, Condition: "Loose Cartridge", PricePaid: 180.00, Notes: "Authentic board verified; label in excellent shape.", }, { Title: "Castlevania: Symphony of the Night", Platform: "Sony PlayStation", Year: 1997, Condition: "CIB (Black Label)", PricePaid: 135.00, Notes: "Original soundtrack disc included.", }, } type CatalogRequest struct { Query string `json:"query" jsonschema:"The game title or platform to search in the inventory."` } type CatalogResponse struct { Found bool `json:"found"` Message string `json:"message,omitempty"` Count int `json:"count,omitempty"` Results []GameItem `json:"results,omitempty"` } func main() { ctx := context.Background() // 1. Initialise Gemini Model adapter for Gemini Enterprise model, err := gemini.NewModel(ctx, "gemini-3.8-flash", &genai.ClientConfig{ Project: os.Getenv("GOOGLE_CLOUD_PROJECT"), Location: "global", Backend: genai.BackendEnterprise, }) if err != nil { log.Fatalf("failed to create Gemini model: %v", err) } // 2. Wrap collection lookup as an ADK Function Tool catalogTool, err := functiontool.New(functiontool.Config{ Name: "search_catalog", Description: "Search the collector's personal inventory for owned games by title or platform.", }, func(ctx agent.Context, req CatalogRequest) (CatalogResponse, error) { queryLower := strings.ToLower(strings.TrimSpace(req.Query)) var matches []GameItem for _, item := range localCatalog { if strings.Contains(strings.ToLower(item.Title), queryLower) || strings.Contains(strings.ToLower(item.Platform), queryLower) { matches = append(matches, item) } } if len(matches) == 0 { return CatalogResponse{ Found: false, Message: fmt.Sprintf("No items matching %q found in personal collection.", req.Query), }, nil } return CatalogResponse{ Found: true, Count: len(matches), Results: matches, }, nil }) if err != nil { log.Fatalf("failed to create catalog tool: %v", err) } // 3. Define autonomous LLM Agent appraiserAgent, err := llmagent.New(llmagent.Config{ Name: "retro_game_appraiser", Model: model, Description: "Expert appraiser that analyzes retro video game purchases and collection inventory.", Instruction: "You are an expert Retro Game Appraiser. Assist collectors by verifying collection " + "status with search_catalog, assessing condition variants, and offering objective buying recommendations.", Tools: []tool.Tool{ catalogTool, geminitool.GoogleSearch{}, }, }) if err != nil { log.Fatalf("failed to create appraiser agent: %v", err) } // 4. Configure launcher and execute config := &launcher.Config{ AgentLoader: agent.NewSingleLoader(appraiserAgent), } l := full.NewLauncher() if err = l.Execute(ctx, config, os.Args[1:]); err != nil { log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax()) } } ``` ### ADKエージェントを実行する ADKにはユニバーサルランチャー(`full.NewLauncher()`)が用意されており、HTTPルーティングやJSONのマーシャリング、セッション管理の定型コードを手動で書く必要がありません。コマンドライン引数をランチャーに渡すだけで、アプリケーションコードを1行も変更することなく、複数の対話モードでエージェントを実行できます。 ターミナルで対話型チャットセッションを開始するには、以下を実行します。 ```sh export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" go run main.go ``` ランチャーがコンソールモードで起動し、ターミナル上で直接鑑定エージェントと会話できます。 ```text User: Do I have Chrono Trigger in my collection? Agent: Yes, you have Chrono Trigger in your collection! Here are the details from your inventory: * Title: Chrono Trigger * Platform: Super Nintendo (SNES) * Release Year: 1995 * Condition: CIB (Complete in Box) * Price Paid: $210.00 * Notes: Includes original map and registration card. User: What did I pay for it? Agent: You paid $210.00 for it. ``` ADKが会話状態とセッションの継続性を自動的に管理するため、過去のやり取りのコンテキストを維持したまま自然に追加の質問を行えます。 また、ADKに組み込まれた開発者向けWeb UIとREST APIサーバーを起動することも可能です。 ```sh go run main.go web webui api ``` ブラウザで `http://localhost:8080` にアクセスすると対話型チャット画面が開き、リアルタイムのストリーミング応答、セッション履歴、およびツール実行(カスタムの `search_catalog` 関数ツールと組み込みの `GoogleSearch` によるグラウンディングの両方)の詳細な実行ステップを確認できます。 ADKは、エージェント定義(`llmagent`)、ツール(`functiontool` や組み込みの `geminitool.GoogleSearch`)、実行環境(`launcher` と `runner`)の間で明確な責務の分離を提供し、複雑なマルチエージェント階層や会話アシスタントに最適な選択肢となります。 ## エージェント実行ランタイム環境 Goでエージェントコードを記述することは行程の半分に過ぎません。完成したエージェントを本番運用するには、長時間のストリーミング応答、バックグラウンドでのツール実行、安全な認証情報の管理、急なトラフィック変動に耐えうる実行基盤が必要です。 Goは最小限のリソース消費で単一バイナリにコンパイルされるため、極めて高速かつ安価にホスティングできます。フレームワークや状態管理の要件に応じて、主に2つのデプロイ先が存在します。 ### Cloud Run: コンテナ化バックエンドの最適解 **[Google Cloud Run](https://cloud.google.com/run)** は、SDK直接実装、Genkitフロー、ADK Runnerのいずれであっても、Goエージェントをコンテナとしてホスティングする最適な環境です。 Dockerのマルチステージビルドを活用することで、Goエージェントを実行時依存関係のない軽量な `scratch` や `distroless` イメージにコンパイルできます。重いインタプリタや仮想マシンを起動する必要がないため、ゼロからアクティブインスタンスへと素早くスケールし、重量級ランタイムで発生しがちなコールドスタートのオーバーヘッドを最小限に抑えられます。 Cloud Runの主なメリット: * **真のゼロスケールと高並行性:** エージェントの実行中やツール応答の処理中など、実際にCPUを消費したミリ秒単位でのみ課金されます。Goの軽量なゴルーチンにより、1つのインスタンスで極小のメモリフットプリントのまま数百の同時ターンを処理できます。 * **最大60分の延長タイムアウト:** デフォルトのタイムアウトは5分ですが、Cloud Runは最大60分(3,600秒)まで設定可能です。多段階の推論、広範なディープリサーチ、サブエージェント群の連携処理であっても、途中で中断されることなく完了できます。 * **双方向ストリーミングとWebSockets:** HTTP/2のチャンク転送、Server-Sent Events(SSE)、WebSocketsをネイティブサポートしています。Gemini Live APIを用いた音声やリアルタイムのマルチモーダル対話においても、シームレスな通信が可能です。 * **セッションアフィニティ(Sticky Sessions):** エージェントがメモリ上に一時キャッシュを保持する場合、クライアントIPやCookieによるセッションアフィニティ(`--session-affinity`)を有効にして、同一インスタンスへリクエストをルーティングできます。 * **疎結合な耐障害性:** 本番環境では、Firestore、Redis、Cloud SQLなどのマネージドDBに状態を永続化すべきです。長時間のターン中に接続が切断された場合でも、インタラクションIDを使ってコンテキストを損なうことなく再開できます。 * **Workload Identity IAMによるセキュアな認証:** コード内にAPIキーをハードコードする必要はありません。GoエージェントはCloud Runのサービスアカウント環境を利用して、Gemini Developer API、Vertex AI、Cloud Storageへ安全に認証されます。 ### Gemini Enterprise Agent Platform: マネージドセッションとエンタープライズRAG **Agent Development Kit (ADK)** を使用して、長期にわたるセッション、永続メモリ、社内データへのグラウンディングを必要とするエンタープライズソリューションを構築する場合、**Gemini Enterprise Agent Platform**(Vertex AI Agent Engineの進化版)がフルマネージドなサーバーレス基盤を提供します。 独自のデータベースを構築して永続化アダプターを開発する代わりに、以下の機能が標準で提供されます。 * **疎結合なセッション永続化:** 長期的な会話履歴ストレージ(ADKの `SessionService`)とエフェメラルなストリーミング実行ループ(再接続時に自動再開可能な `LiveSession`)の明確な分離。 * **エンタープライズグラウンディングとベクトル検索:** 社内ナレッジベース(Google Drive、BigQuery、社内リポジトリ)へのネイティブコネクタと、高スループットなセマンティック検索を実現するVertex AI Vector Search(ストレージ最適化ティア対応)。 * **安全なサンドボックスコード実行:** エージェントが安全にコードを動的生成・実行できる隔離された環境(データ分析、Python/Goスクリプトなど)。ホストインフラを危険に晒すことはありません。 * **Agent-to-Agent(A2A)プロトコル:** 組織内の異なる領域に存在する独立したエージェント同士が、機能を検出し、スキーマを合意し、タスクを相互に委譲できる標準化されたプロトコル。 * **セキュリティ、エージェントID、Model Armor:** きめ細やかなIAM権限、VPC Service Controls境界、およびプロンプトインジェクションやデータ漏洩、ポリシー違反を検知・防御するModel Armorランタイム保護。 ## 次に読むべきガイド 同じエージェントを3通りの方法で実装してみると、それぞれのトレードオフが明確に浮かび上がります。**Go GenAI SDK** は低レベルの実行ループを完全に制御したい場合に適しており、**Genkit** は型安全なフロー、プロンプト管理、HTTPサービス構築で真価を発揮し、**ADK** はセッション駆動のマルチエージェントシステムに構造化されたプリミティブを提供します。 このスターターサンプルから実際のアプリケーションへと発展させる実践例については、[**Python製のRAGエージェントをGoとGenkitで書き直した理由**](https://danicat.dev/ja/posts/20261006-building-agentic-backends-with-genkit-go/)をご覧ください。PythonとSQLite RAGで書かれた旧式のエージェントを、型付き `dotprompt` テンプレート、Agent Skillsミドルウェア、マルチエージェント委譲を用いて単一バイナリのGoエージェントへと刷新するプロセスを詳しく解説しています。 Happy hacking! --- # なぜGoはコーディングエージェントに最適なのか(そしてそのチューニング方法) - **Markdown URL**: https://danicat.dev/ja/posts/gemini-for-go-developers-part-2-coding-with-gemini/index.md - **HTML URL**: https://danicat.dev/ja/posts/gemini-for-go-developers-part-2-coding-with-gemini/ - **Date**: 2026-08-17 - **Categories**: Agentic Coding - **Tags**: agent-skills, antigravity, gemini, godoctor, golang, mcp - **Series**: Gemini for Go Developers - **Summary**: AIコーディングハーネスでGoのエージェンティック親和性を最大化。Antigravity、MCPサーバー、Agent Skills、godoctorを活用した実践的なGo開発環境を解説します。 AIコーディングエージェントとGoを組み合わせるとき、どうすれば最高の成果を引き出せるでしょうか?本記事では、Goにおけるエージェンティック・コーディングのための実践的でシグナル密度の高い開発環境の構築方法を探求します。 まずはAI時代においてもプログラミング言語の選択が重要である理由を考察し、エージェントハーネス(実行環境)やオープンなカスタマイズ規格(Rules、MCP、Skills、Hooks、Subagents、Plugins)の全体像を整理した上で、日々のワークフローでGoのエージェンティック親和性を最大化するための推奨セットアップを紹介します。 ## AI時代になぜGoを使うのか? 本題に入る前に、まずは根本的な疑問を解消しておきましょう。AI時代において、私たちがどのプログラミング言語を選ぶかは、そもそも重要なのでしょうか? かつて言語の選定は、ほぼ常にチーム内の既存の専門知識によって決まっていました。新しい構文やイディオム、開発ツールの癖を習得するには貴重な時間が必要であり、大きな技術的転換に迫られない限り、チームは慣れ親しんだ快適な領域にとどまっていました。 しかしAIの登場によって、その力学は一変しました。モデルが必要なボイラープレートコードをオンデマンドで生成してくれる現代において、構文はもはや参入障壁ではありません。5年前の古いStack Overflowのスレッドが自分のコンパイラエラーと完全に一致することを祈りながら何時間も過ごすくらいなら、AIチューターと一緒に見慣れないスタックを学ぶほうがはるかに快適です。 では、なぜ今言語の選定を気にする必要があるのでしょうか? それは、**言語エコシステム** と **エージェンティック親和性(agentic affinity)** という2つの主要なテーマに行き着きます。 ここで言う言語エコシステムとは、その言語が持つ「重力」によって引き寄せられるものすべてを指します。活発にメンテナンスされているSDK、ライブラリ、ドキュメント、コミュニティの熱量、そして業界の知見などです。一方、**エージェンティック親和性** は、私たちが今日生きている世界に特有の概念です。私はこれを「エージェントを誘導してその言語でコードを書かせることがどれだけ容易か」と定義しています。高いエージェンティック親和性は、第一にモデルがその言語でコードを生成する準備がどれだけ整っているか、第二にそのコードを検証・テスト・保守するために必要なツールをエージェントがどれだけ自在に扱えるかによって決まります。 エージェンティック親和性は当然ながら、モデルの学習時にその言語に関するデータがどれだけ豊富に存在したかに左右されるため、公開コードや技術文献が豊富な人気言語が自然と有利になります。とはいえ、データ量だけがすべてではありません。歴史の中で大規模なパラダイムシフトを経験した古い言語ではベストプラクティスが断片化しており、推論時にモデルが非推奨の古いパターンを提案してしまう原因になります。 私の個人的な経験から言えば、Go、Python、JavaScriptなどの言語は、デフォルトで高いエージェンティック親和性を備えています。中でもGoは際立っています。高い可読性、厳格な静的型付け、そして「暗黙的であるより明示的である方が良い(explicit is better than implicit)」というPython的とも言える設計思想により、構文のハルシネーション(幻覚)が極めて発生しにくくなっています。さらに重要なのは、Goの高速なコンパイル、標準のテスト機能、そして洗練された標準ツールチェーンが、コーディングエージェントに対して自律的にエラーを検知・修正するための即時かつ決定論的なフィードバックループを提供することです。(Goの設計思想がAI支援ソフトウェアエンジニアリングとどのように合致しているかについて詳しく知りたい方は、[Google Developers Blogに掲載されたCameron Balahan氏とRichard Seroter氏によるエッセイ](https://developers.googleblog.com/why-go-is-an-ideal-language-for-ai-assisted-software-engineering/) をご覧ください)。 一方で、RやCのような言語はスペクトラムの対極に位置します。Rはニッチな学術向け言語であり、エージェントの手厚いサポートがあっても、私の [read.dbc](https://github.com/danicat/read.dbc) パッケージをCRAN(Rのパッケージ配布システム)上で適切にメンテナンスし続けることができません。CRANのパイプラインから報告される問題を、モデルも私自身も再現できないためです。またC言語では、ガードレール(安全装置)が少ないため、些細なミスが致命的かつ潜在的なバグへと発展しやすく、充実したツール環境(エルゴノミクス)がなければエージェントが早期にバグを発見するのは困難です。 最終的にエージェンティック親和性は、エージェントの「ループ」の速度と応答性によって測られます。すなわち、人間の細かな介入(マイクロマネジメント)なしに、エージェントが最初から自律的にコードをビルドし、テストし、ベンチマークを取り、修復できるかどうかが鍵となります。 ## 適切なエージェントサーフェスを選択する フロンティアモデルの性能競争は目覚ましいものがありますが、コード生成においてモデル単体は方程式の1ピースに過ぎません。エージェントを使ったコーディングの応答品質や開発者体験(DevEx)は、モデルを実行するハーネスに大きく左右されます。 Geminiを活用してコーディングを行う場合、最も自然な選択肢は **Antigravityエコシステム** です。Googleは Antigravity 2.0 のリリースに伴い、好みの作業スタイルに合わせてエコシステムを3つの明確なサーフェスに整理しました。 ### Antigravity 2.0 Antigravityエコシステムの中核を成すのが、デスクトップアプリケーションである **Antigravity 2.0**(**Agent Manager** とも呼ばれます)です。ここでの最大の変更点は、エージェントとの対話体験を前面に押し出し、コードを背後に配置した設計にあります。従来のIDEに慣れ親しんだユーザーにとって、最初は少し衝撃的かもしれません。コードベースを探索するファイルツリーはなく、ファイルを手動で直接編集する手段もありません。すべての対話はエージェントを通じて行われます。開発者のコントロールは、エージェントに何をすべきかを指示し、Googleドキュメント感覚のコメント機能でエージェントの作業に注釈を付けることにあります。 ### Antigravity CLI (`agy`) Agent ManagerのようなUIは比較的新しい概念ですが、ターミナルベースのエージェントUIは Claude、Aider、Cline などによって以前から広く普及しています。2025年6月、Googleも独自のターミナルUIとして Gemini CLI を発表しましたが、現在は新しい **Antigravity CLI** (`agy`) へと移行・一本化されています。 Gopherとして特に嬉しいポイントは、`agy` がGoで実装されている点です(従来のGemini CLIはTypeScript製でした)。これにより、ターミナルでの体感レスポンスが目に見えて軽快になっています。 ### Antigravity IDE 視覚的で使い慣れたコードエディタ環境を好む開発者のために、Googleはコンパニオンアプリとして **Antigravity IDE** も提供しています。VS Codeをベースに構築されているため、おなじみのIDE要素がそのまま配置されており、Geminiと対話するためのエージェントサイドパネルが統合されています。 率直にお話しすると、最近の私はコーディング目的でIDEを開くことは滅多にありません。IDEを使うのは、執筆プロセスの大部分を手動で行っている記事(この記事など)の執筆やレビューをするときくらいです。コードに関しては、現在手作業で編集することはほとんどなくなりました。 ## コーディングに適用されるエージェント標準規格 どのサーフェスを選択した場合でも、Antigravityは単体で高い能力を発揮しますが、固有の癖もあります。Antigravityの真価をフルに引き出す最良の方法は、エージェントに適切な **カスタマイズ** を施すことです。 Antigravityは、rules、skills、MCP、hooks、subagents、sidecars、pluginsといった確立された標準や新興のエージェント規格を幅広くサポートしています。ただし、これらのカスタマイズ機能のサポート状況は、現時点ではAntigravityの各サーフェス間で **完全には統一されていません**。 それぞれのカスタマイズが実践でどのように機能するかを見ていきましょう。 ### エージェントへの指示とルール(Agent instructions and rules) エージェントへの指示という概念は [**AGENTS.md**](https://agents.md/) イニシアチブによって標準化され、Antigravityでは `AGENTS.md` や `GEMINI.md` ファイル(または `.gemini/rules/` や `.agents/rules/` 下のモジュール化されたルール)を通じてサポートされています。 `AGENTS.md` は、「AIエージェントのための `README.md`」と捉えることができます。プロジェクトのアーキテクチャ上の制約、テスト実行コマンド、コーディング規約など、エージェントが把握すべき必須情報でありながら、人間向けのドキュメントには冗長になりがちなコンテキストを記述する場所です。 正直なところ、Agent Skillsが登場してからは、私自身 `GEMINI.md` を更新する機会が減り、多くのリポジトリで古いファイルが放置されたままになっています(エージェントたちよ、すまない!)。それでも、エージェントを正しい方向へ導く上では依然として有用です。ただし、プロンプトベースの指示は「ガードレール(強制力)」というよりも「推奨事項」として機能するため、長時間のセッションではエージェントがルールを徐々に見落としたり逸脱したりする可能性がある点に留意してください。 ### Model Context Protocol (MCP) [**Model Context Protocol**](https://modelcontextprotocol.io/) (MCP) は、AIアプリケーションを外部のツールやデータソースと接続するためのオープン標準規格です。 MCPは主に3つのコアプリミティブを提供します。 1. **Tools:** エージェントが実行可能な関数(データベースへの問い合わせ、リンターの実行、ビルド検証など)。 2. **Resources:** エージェントが閲覧できる読み取り専用のデータソース(ドキュメントファイル、DBスキーマ、システムログなど)。 3. **Prompts:** 定型化されたワークフローテンプレート。 実際の現場で私たちが何よりも重視するのはToolsであり、多くのクライアントはResourcesやPromptsをサポートすらしていません。ResourcesはTools経由で代替でき(データを取得する専用ツールを用意すればよい)、Promptsははるかに柔軟なAgent Skillsの登場によってほぼ使われなくなりました。 Skillsにはスクリプトを同梱できるため、MCPを完全に廃止してSkillsへ一本化しようとする動きもあります。しかし、ライフサイクル管理の観点(特にHTTPS経由の利用)では、依然としてMCPに大きな優位性があります。例えば企業としてMCPサーバーをWebサービスとしてデプロイすれば、クライアント側は一度設定するだけで常に最新のドキュメントに即座にアクセスできます。一方Skillsの場合、全クライアントが確実に最新バージョンのみを参照する仕組みには、大規模運用においてまだ解決すべき課題が残されています。 ### Agent skills [skill(スキル)](https://agentskills.io) は、指示書(`SKILL.md`)、任意のヘルパースクリプト、ドキュメントを同梱したディレクトリであり、特定のエンジニアリングワークフローを実行する方法をエージェントに教育します。 Skillsの中核となるアーキテクチャ概念は **段階的開示(progressive disclosure)** です(本番環境での活用パターンは [Agent Skills 実践ガイド](https://danicat.dev/ja/posts/20260829-the-pragmatic-guide-to-agent-skills/) を参照してください)。数百ページに及ぶドキュメントを最初からコンテキストウィンドウに流し込むのではなく、システムはまずスキルの名前と説明文のみをエージェントに渡します。エージェントがタスクとスキルの適合性を認識した時点で初めて、完全な `SKILL.md` の指示を読み込み、同梱されたスクリプトをオンデマンドで実行します。これにより、専門知識を常時利用可能にしつつ、コンテキストウィンドウの圧迫や無関係な情報によるモデルの混乱を防ぐことができます。 エージェントはスキルの説明文に基づいて自動起動を試みますが、自動判定のみに頼るのは危険です。Antigravityではデフォルトですべてのスキルがスラッシュコマンドとしてマップされるため、プロンプト内の任意の場所で `/` と入力することで強制的にスキルを起動できます。ワークフローにとって重要なスキルであれば、プロアクティブに起動することで多くのトラブルを回避できます。 ### Hooks RulesやPrompts、さらにはSkillsが「ソフトな誘導」を提供するのに対し、[**hooks**](https://danicat.dev/ja/posts/20260610-mastering-hooks/) はエージェントの実行ループに決定論的な制御を導入します。Hooksは、ツール実行前(`PreToolUse`)、ツール実行後(`PostToolUse`)、モデル推論前(`PreInvocation`)、セッション終了時(`Stop`)など、エージェントのライフサイクルにおける特定の瞬間に介入するコールバック関数です。 LLMは非決定論的であるため、プロンプトで「コード編集後は必ずリンターを実行すること」と指示しても、実行されるかどうかは運任せになります。一方、Hooksはハーネスによって制御され、指定されたイベントに対して確実に実行されます。 ただし、Hooksを設計する際に常に注意すべき落とし穴が1つあります。モデルはHooksを回避するのが非常に得意だということです。ええ、残念ながらこれは実際に起こります。Hookがエージェントの危険な操作を阻止したとしても、エージェントはエージェント設定を改ざんしたり、トリガー条件をごまかそうとしたり、最悪の場合はHookスクリプトそのものを書き換えたりして、直後にHookを出し抜こうとします。 こう言うのは心苦しいのですが、以前はHooksを多用していたものの、新世代のモデルは賢くなりすぎて裏をかくようになってしまったため、私は徐々にHooksからSkillsへと移行しています。子育てと同じですね。「禁止するのではなく、教育する」のです。 ### Subagents [**subagents(サブエージェント)**](https://danicat.dev/ja/posts/20260722-the-rise-of-the-subagents/) は、コンテキストウィンドウの問題に対するもう1つの解決策であり、並列実行などの興味深いパラダイムを可能にします。メインエージェントが問題領域を分割し、個別のタスクに特化したエージェントを生成して作業を委任します。 分かりやすい例として、フロントエンドとバックエンドの両方を持つWebサービスの開発が挙げられます。ここでの変更は本質的に互いに直交しています。フロントエンドはHTML、CSS、JavaScriptを必要とし、バックエンドはGo、Python、そしておそらくSQLを必要とします。フロントエンドとバックエンドのタスクでは、コーディング規約もビルドパイプラインも異なります。両者の契約(APIインターフェース)を除けば共通点はなく、同一のコンテキストウィンドウで作業を行うと、一方の情報が他方にとって単なるノイズになってしまいます。 タスクを2つ(またはそれ以上)のサブエージェントに分割することで、各サブエージェントが担当レイヤーに集中できるようになり、まったく異なる技術スタックが混ざり合ってコンテキストが劣化するリスクを抑えられます。 もう1つの好例は、完了したばかりの作業について客観的なレビューを行いたい場合です。エージェントにサブエージェントでのコードレビューを実行させれば、「先入観のない新鮮な視点」でコードを精査できるメリットが得られます。 ### Sidecars > 注記:本稿執筆時点では、sidecarsはAntigravity 2.0でのみ動作します。`agy` CLIやIDEでは利用できません。 **Sidecars** は、セッションの実行中、エージェントと並行して動作するバックグラウンドプロセスです。Antigravityの定義された再起動ポリシーによってライフサイクルが管理される永続プロセスや、スケジュール実行されるプロセスを構成できます。 私自身はまだsidecarsを本格的に探究できていないため、現時点で議論できることは多くありませんが、同僚のMete Atamel氏が解説記事を公開していますので、詳細は [彼の記事](https://medium.com/google-cloud/where-does-antigravity-look-for-sidecars-20e7002b9246) をご覧ください。 ### Agent plugins Pluginsは要するに、Rules、MCP、Skills、Hooks、Agents、Sidecarsを1つの配布ユニットとしてまとめた「カスタマイズバンドル」です。私自身もPluginsの実験を続けてきましたが、残念ながら期待通りにスムーズには動作していません。例えば、プラグインに同梱したHooksが正しく実行されるように設定するのに非常に苦労したものの、結局うまく機能しませんでした。 また、プラグインの機能サポートはサーフェスによって非標準的です。例えば、SidecarsはAntigravity 2.0のプラグインでのみサポートされ、カスタムエージェントはAntigravity CLIでのみサポートされるといった具合です。 では、Pluginsは何に役立つのでしょうか? 答えはMCPとSkillsです。偶然か必然か、業界全体も新しい [Agent Plugins](https://agent-plugins.org/specification) 標準によってこの方向に収束しつつあります。彼らの論理では、Hooks、Agents、Rules、LSPサーバーなどは依然としてクライアント依存が強いため、現時点では標準化の対象外とされています。 ## Goのエージェンティックツールキットを構築する エージェントハーネスとカスタマイズの全体像を把握したところで、Go開発用のツールキットを揃えていきましょう。必須のコミュニティツールと、AIネイティブな専門拡張機能の2つに分類して紹介します。 ### 必須のコミュニティツール Goの標準ツールチェーンはエージェントにとって強固なベースラインを提供しますが、以下のコミュニティツールを導入することで、コード品質とリリース自動化をさらに向上させることができます。 - [**`golangci-lint`**](https://golangci-lint.run/): 数十種類の高速リンターを1回のパスで一括実行し、`go vet` が見逃す未処理エラー、型アサーションの不備、変数のシャドーイング、並行処理の落とし穴を捕捉します。 - [**`goreleaser`**](https://goreleaser.com/): バイナリを配布するプロジェクト向けに、`.goreleaser.yaml` からのクロスプラットフォームビルド、リリースパイプラインの管理、チェンジログ生成を自動化します。 - [**`modernize`**](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize) / **`go fix`**: 最新のGoリリースに合わせてコードを解析し、手動のスライス/マップループやmin/maxヘルパーを現代的な組み込み関数へと機械的にアップグレードします。 - [**`deadcode`**](https://pkg.go.dev/golang.org/x/tools/cmd/deadcode): プログラム全体の到達可能性解析(reachability analysis)を行い、パッケージ間の未使用関数や到達不能コードを特定します。 - **`selene` と `testquery`(手前味噌ですが):** テストスイートの検証と改善のために私がメンテナンスしている2つのオープンソースツールです。[**`selene`**](https://github.com/danicat/selene) は、ASTに意図的な欠陥を注入してテストがバグを確実に検出できるかを検証するGo向けミューテーションテストツールです。[**`testquery`**](https://github.com/danicat/testquery) は、Goのテスト結果やテストごとのカバレッジに対してSQLクエリを実行できるCLIです。ニッチではありますが、テストスイートの最適化に役立っています。 ### AIネイティブな専門ツール 上記のツールは人間の開発者向けに作られたものですが、コーディングエージェントは標準のシェルコマンド経由でこれらを直接実行できます。続いて、AIネイティブなワークフローに特化したMCPやSkillsを見ていきましょう。 #### 公式 `gopls` MCPサーバー 従来のIDEにおいて、`gopls` は型チェック、参照検索、シンボル定義などのセマンティック認識(意味解析)をエディタに提供します。しかし、エージェントがヘッドレス環境やターミナル環境で動作する場合、通常はコードを生のテキストファイルとして扱います。 このギャップを埋めるため、Goチームは [**`gopls`**](https://pkg.go.dev/golang.org/x/tools/gopls) にネイティブのMCPサポートを追加しました。MCPモードで `gopls` を実行すると、言語サーバーの型チェッカーとインデックスが実行可能なツールとしてモデルに直接公開されます。これにより、エージェントはコンパイラ自身のセマンティックモデルを利用してパッケージ階層を探索し、型シグネチャを検査できるようになります。 #### `godoctor` MCPサーバー `gopls` MCPサーバーに対する私の愚痴の1つは、それがLSP APIの上に設計されている点です。LSPはそもそもエージェントを想定して設計されたものではなく、人間のキーストロークに合わせたタイピング速度とインタラクティブ性のために作られました。一方、エージェントのワークフローはトランザクショナルです。そこで私は、AIネイティブなGo開発ツールを提供するために [**`godoctor`**](https://github.com/danicat/godoctor) を開発しました。 現行バージョンの `godoctor` は以下の機能を提供します。 - **`smart_edit`:** 自動 `go vet` 検証とタイポ補正機能を備えたAST認識型ファイルエディタ。編集によってコンパイルエラーや構文エラーが発生した場合、変更を自動的にロールバックし、周囲の識別子に基づいて「もしかしてこれですか?」という誘導プロンプト付きで修正案を提示します。 - **`smart_build`:** モジュール衛生管理(`go mod tidy`、`modernize`、`goimports`)、パッケージのビルド、カバレッジ付きテスト実行、`golangci-lint` によるリント検証を1回のパスで実行する自動検証パイプライン。 - **`smart_test`:** `testquery` および `selene` をサポートする、実践的なテストパイプライン。 - **`read_docs`:** `go doc` ベースのドキュメント検索機能。サンプルコードの表示に対応し、モジュール構成に依存しないフォールバックシステムを備えています。 #### プラットフォーム知識と自己改善 `godoctor` や `gopls` がローカルコードの意味解析を担当する一方で、コーディングエージェントが外部サービスと連携する際には、リアルタイムなプラットフォーム知識やワークフローのプレイブックも必要になります。Go、GCP、Geminiを組み合わせて開発する上で不可欠なクラウドおよびAPIリソースは以下の通りです。 - **Google Developer Knowledge MCP (`developerknowledge.googleapis.com/mcp`):** 公式のGoogle Cloud、Gemini Enterprise (Vertex AI)、Google APIドキュメントにエージェントを直接接続します。 - **Gemini Docs MCP (`gemini-api-docs-mcp.dev`):** 最新のGemini APIエンドポイント、SDK更新情報、構成パターンに関するリアルタイムなドキュメントを提供します(詳細は [Geminiコーディングエージェントガイド](https://ai.google.dev/gemini-api/docs/coding-agents) を参照)。 - **公式 Google Skills:** [**`github.com/google/skills`**](https://github.com/google/skills) および [**`github.com/google-gemini/gemini-skills`**](https://github.com/google-gemini/gemini-skills) には、Googleが公式にメンテナンスするSkills(`gemini-api-dev`、`gemini-live-api-dev`、`gemini-interactions-api` など)が収録されています。 - **コミュニティおよび個人カタログ:** 私の個人カタログ [**`skills.danicat.dev`**](https://skills.danicat.dev)(または [**GitHub**](https://github.com/danicat/skills))では、エンジニアリングのベストプラクティス、2Dゲーム開発、生成メディア(Lyria、Nano Banana Pro)向けのスキルを閲覧できます。 さらに、独自のカスタム拡張機能で自分自身のワークフローを最適化するためのツールもあります。 - **AgentSkills MCP (`agentskills.io/mcp`):** [Agent Skillsオープン仕様](https://agentskills.io) やスキル作成のベストプラクティスを検索・取得するための公式MCPサーバー。日々の業務の一環として自作スキルを作成する際に大活躍します(ぜひ作るべきです!)。 - **MCP開発向けSkills:** エージェントスキル開発用のMCPがあるなら、[MCP開発用のAgent Skills](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-with-agent-skills) があってもいいはずですよね? ええ、見間違いではありません(笑)。少しニッチですが、私のツール群からもわかるように、自分専用のMCPを作成することも開発環境を改善する優れた方法です。 ## Gopherのための5分間エージェントセットアップ Geminiを使って今すぐGo開発を始めたい方のための、5分で完了する推奨クイックスタートです。 1. 公式サイトから [Antigravity](https://antigravity.google) をダウンロードします。 2. 推奨MCPサーバーを設定します。 - [Gemini Docs MCP](https://ai.google.dev/gemini-api/docs/coding-agents): `npx add-mcp "https://gemini-api-docs-mcp.dev"` - [Developer Knowledge MCP](https://developers.google.com/knowledge/mcp): APIを有効化し、ドキュメントの手順に従って設定します。 - [Agent Skills MCP](https://agentskills.io): 任意のページのコピーボタンを展開して手順を確認します。 - [godoctor](https://github.com/danicat/godoctor): 1行インストールスクリプトを実行します。 3. 推奨Skillsを追加します。 - [Gemini API development](https://ai.google.dev/gemini-api/docs/coding-agents): `npx skills add google-gemini/gemini-skills --skill gemini-api-dev` - [Swarm coding](https://danicat.dev/ja/posts/20260722-the-rise-of-the-subagents/): `npx skills add github.com/danicat/skills/agents/swarm-coding` 4. 自律ループを試運転する: エージェントにプロンプトを入力して、自律的な検証パスを実行させます。 > Run a smart build on this package with godoctor, address any findings, and evaluate the test suite with selene. ## まとめ 本記事では、Goの高速なコンパイラと厳格なツールチェーンが高いエージェンティック親和性をもたらす理由、カスタマイズ規格(Rules、MCP、Skills、Hooks、Subagents、Plugins)の組み合わせ方、そしてAntigravityにおける実践的なGo開発環境の構築方法について解説しました。 開発ワークフローが整ったら、次はテーブルの反対側へと回り、Goで自律型エージェントそのものを構築してみましょう。 * Goにおける3大エージェントアーキテクチャの比較については、[**GoでAIエージェントを構築する:GenAI SDK、Genkit、ADKの徹底比較**](https://danicat.dev/ja/posts/20260825-gemini-for-go-developers-part-3-building-agents/)をご覧ください。 * コンパイル済みGoバイナリ内でAgent Skillsやマルチエージェント委譲を動かす実践例については、[**Python製のRAGエージェントをGoとGenkitで書き直した理由**](https://danicat.dev/ja/posts/20261006-building-agentic-backends-with-genkit-go/)をご覧ください。 --- # Geminiモデルファミリー:Gopherのための実践ガイド - **Markdown URL**: https://danicat.dev/ja/posts/gemini-for-go-developers-part-1-model-family/index.md - **HTML URL**: https://danicat.dev/ja/posts/gemini-for-go-developers-part-1-model-family/ - **Date**: 2026-08-08 - **Categories**: Applied GenAI - **Tags**: gemini, genai-sdk, golang, llm, multimodal - **Series**: Gemini for Go Developers - **Summary**: Gemini 3.x Pro、Flash、Flash-Lite、Nano Bananaモデルの比較から思考レベル(thinking levels)の調整、公式Go GenAI SDKによるマルチモーダル実装までを解説します。 Goで信頼性の高いAIソフトウェアを構築する第一歩は、土台となるモデルの特性を正しく理解することです。高レベルなエージェントフレームワークを導入したり複数ステップのワークフローを組んだりする前に、どのモデルがワークロードに適しているか、推論バジェットをどう調整するか、そしてどのAPIサーフェスがアーキテクチャに合っているかを見極める必要があります。 本ガイドでは、Geminiモデルファミリーの全体像を探り、思考レベル(thinking levels)をはじめとする主要な設定パラメータを紐解きながら、公式の [Go GenAI SDK](https://pkg.go.dev/google.golang.org/genai) を使って最初のマルチモーダルアプリケーションを作成します。 ## Geminiモデルファミリー 「Google」を検索の代名詞として使うのと同じように、私たちは「Gemini」をGoogleのAI製品全般を指す単一の名前として扱いがちです。しかし実際には、Geminiは運用上の異なるトレードオフに合わせて構築された、複数のモデルからなるファミリーです。 見出しを飾るのは最先端のフロンティアモデルですが、費用対効果の高いエンジニアリングを実現するには、小型モデルや特化型モデルをいつ活用すべきかを理解しておくことが不可欠です。モデルの階層(Tier)によってレイテンシは大きく異なるため、モデルの選定は[ユーザー体験](https://services.google.com/fh/files/blogs/google_delayexp.pdf)やプロダクトの定着率にも直接影響を与えます。 あらゆるタスクに対して思考レベル(thinking levels)を高く設定したGemini Proモデルを選びたくなるかもしれませんが、それが常に正しい判断とは限りません。多くの場合、より良い結果が得られないまま、リクエストごとのレイテンシとAPIコストが無駄に増加するだけです。 ### モデルの命名規則 Geminiカタログを把握するには、Googleがモデルにどのような命名を行っているかを理解しておくと役立ちます。標準的なモデル識別文字列は次のパターンに従います: \[ \text{[family]}-\text{[version]}-\text{[tier]}{-\text{[modifier]}} \] 例えば、`gemini-3.7-flash` や `gemini-3-pro-image` のようになります。 **Family(ファミリー)**: ほとんどのモデルはGeminiファミリーに属しますが、GoogleにはVeoやLyriaといった他のモデルファミリーもあります。 **Version Numbers(バージョン番号)**: 知能、コンテキストウィンドウの処理能力、指示追従性における世代ごとの飛躍を表します。 **Model Tiers(モデル階層)**: * **Pro**: 複雑なマルチステップ推論向けに設計されています。 * **Flash**: 速度を重視したバランスの取れたモデルです。 * **Flash-Lite**: 速度と高スループットが求められるシンプルなタスク向けに最適化されています。 **Modifiers(修飾子)**: `gemini-3.1-flash-image` の `image` や `gemini-3.1-flash-live-preview` の `live` のように、サブファミリーや特化分野を示します。また、`-preview` や `-exp`(experimental / 実験的)などのライフサイクル修飾子が含まれる場合もあります。 ### モデル概要 フロンティアモデルであるGemini 3.xをはじめとする、主要なGeminiモデルの概要は以下の通りです: #### Gemini 3.x Gemini 3.xは主力となるフロンティアモデルラインであり、Pro、Flash、Flash-Liteの各階層で提供されています。これらの汎用モデルは、コード生成やソフトウェアエンジニアリングタスクにおける第一選択肢でもあります。 現在の主なモデル: - `gemini-3.6-flash`: マルチモーダル推論やエージェンティックタスク向けの高速な主力モデル - `gemini-3.5-flash-lite`: 高スループットなマイクロサービス向けの最安・超高速モデル - `gemini-3.1-pro-preview`: 複雑なマルチステップ推論や深層コードベース分析向けの高度なモデル #### Gemini画像モデル(Nano Banana) 技術的にはGeminiファミリーの一部ですが、画像生成に特化したモデルであり、マルチモーダルな入力と出力(画像およびテキスト)の両方を提供します。ゼロからの画像生成だけでなく、既存画像の編集も可能です。 現在の主なモデル: - `gemini-2.5-flash-image`(通称 Nano Banana) - `gemini-3-pro-image`(通称 Nano Banana Pro) - `gemini-3.1-flash-image`(通称 Nano Banana 2) - `gemini-3.1-flash-lite-image`(通称 Nano Banana 2 Lite) #### Veo [ネイティブ音声付き動画生成](https://ai.google.dev/gemini-api/docs/veo)に特化したモデルです。動画はテキストプロンプトや、トランジションを指定するキー画像(開始フレームおよび終了フレーム)、参照画像に基づいて生成されます。Veo 3.1は最大8秒のクリップを生成しますが、7秒単位で最大20回まで拡張可能です。 現在の主なモデル: - `veo-3.1-generate-preview` - `veo-3.1-lite-generate-preview`(高速生成) #### Lyria [Lyria](https://ai.google.dev/gemini-api/docs/music-generation) は音楽生成に特化しており、インストゥルメンタルとボーカルの両方の楽曲を制作できます。Lyriaはテキストと画像の両方を入力として受け付け、画像を着想(インスピレーション)として作曲に活用します。歌詞は自ら指定することも、モデルに作成させることも可能です。 現在の主なモデル: - `lyria-3-pro-preview` - `lyria-3-clip-preview`(30秒のショートクリップ) #### Gemma [Gemma](https://ai.google.dev/gemma/docs) は、Googleが提供するオープンウェイト(open-weights)モデルファミリーです。Geminiを支える技術と同じテクノロジーでトレーニングされていますが、自前のインフラストラクチャにデプロイして運用できるように設計されています。Googleが提供するモデルにとどまらず、Gemmaには多種多様なユースケースに合わせてファインチューニングされたモデルを生み出す[強力なコミュニティ](https://deepmind.google/models/gemma/gemmaverse/)が存在します。 一部のGemmaモデルはローカルマシン上で実行できるほど軽量であり、ネットワーク接続が制限されている環境や存在しない環境でのユースケースを可能にします。より大規模なモデルは非常に高い能力を備えており、データの主権(sovereignty)や完全なネットワーク分離が求められるユースケースに対応できます。 #### その他の注目モデル - Liveモデル: 初期のモデルがバッチやリクエスト・レスポンス処理を担うのに対し、Googleはリアルタイムストリーミング用のライブモデルも提供しています。名前に `-live` が含まれているのが特徴です(例:`gemini-3.1-flash-live-preview`)。 - Text-to-speech: オーディオタグを使用したナレーション制御により、テキストから音声を生成します(`gemini-3.1-flash-tts-preview`)。 - Computer use: 画面を「見て」、ブラウザタスクを自動化できるモデルです(`gemini-2.5-computer-use-preview-10-2025`)。 このように、Geminiは単一のモデルをはるかに超えた存在です。基本的なチャットボットからマルチモーダルな生成、高度なエージェンティック機能までを網羅する包括的なスイートとなっています。 各モデルの詳細な仕様については、[公式Geminiモデルドキュメント](https://ai.google.dev/gemini-api/docs/models) を参照してください。 ## 追加機能 標準的なテキスト補完にとどまらず、Geminiモデルは複雑なアプリを構築するための追加機能をサポートしています。ここでは特に重要な機能をいくつか紹介します。 ### 思考(Thinking) Gemini 2.5以降のモデルは、マルチステップの計画、論理的思考、コーディング、および数学的処理能力を大幅に向上させる内部推論プロセスを使用します。最終的なレスポンスを生成する前に、モデルは内部で「思考トークン(thinking tokens)」を生成してエッジケースを分析し、複数ステップの戦略を立案します。 思考は、2.5の `thinking budget` や 3.x の `thinking level` という設定パラメータを使用して制御できる機能です。バジェットや思考レベルを高く設定するほど、モデルは推論フェーズにより多くの時間とトークンを費やします。 思考が有効な場合、課金対象となる総出力トークン数には、生成された出力テキストとモデルが生成した思考トークンの両方が含まれます。タスクの複雑さに基づいて思考レベルを調整することは、プロダクション環境のサービスにとって極めて重要なステップです。 ### 組み込みツールとファンクションコーリング(Function calling) ファンクションコーリングを使用すると、Geminiモデルを外部ツール、API、データベースと連携させることができます。Geminiは、組み込みツール(`google_search` や `code_execution` など)と、アプリケーションレベルで定義されたカスタム関数の両方をサポートしています。 ファンクションコーリングには主に3つのユースケースがあります: - **アクションの実行(Take Actions):** 会議のスケジュール設定、メール送信、請求書作成、スマートホーム機器の操作など、APIを介して外部システムと連携します。 - **知識の拡張(Augment Knowledge):** 外部データベース、マイクロサービス、ナレッジベースから、リアルタイム情報やプライベートな情報を取得します。 - **能力の拡張(Extend Capabilities):** LLMの限界を超える高精度の数学計算、データ変換、グラフ生成などを実行します。 #### ファンクションコーリングの仕組み ファンクションコーリングは、アプリケーションとモデルの間で次の4ステップの実行プロセスに従います: 1. **ツールの宣言(Declare tools)**: 関数の宣言(関数名、明確な説明、パラメータのJSON Schema)を定義し、リクエスト設定で渡します。 2. **モデルがツールの意図を特定(Model identifies tool intent)**: モデルはプロンプトとツール宣言を検査します。ツールが必要な場合、関数名と引数を含む構造化されたツール呼び出しの意図(intent)を返します。 3. **関数コードの実行(Execute function code)**: モデル自身はコードを実行*しません*。アプリケーションが関数呼び出しリクエストを受け取り、対応するローカルロジックを実行して結果を取得します。 4. **関数の結果を返却(Return function result)**: 実行結果を関数結果ステップとしてモデルに送り返します。モデルはこのデータを使用して最終的な自然言語レスポンスを生成するか、追加のツール呼び出しが必要かを判断します。 ### 構造化出力(Structured outputs) Geminiモデルを設定して、提供された [JSON Schema](https://ai.google.dev/gemini-api/docs/structured-output) に厳密に準拠したレスポンスを生成させることができます。これにより、テキストからの構造化データ抽出が簡単になり、モデルのレスポンスをデータ構造に変換する際の壊れやすいパース処理が不要になります。 RESTペイロードに生のJSON Schemaを記述するだけでなく、Google GenAI SDKを使用すると、Pythonの [Pydantic](https://docs.pydantic.dev/) やGoの構造体タグ(struct tags)など、言語ネイティブの構文構造を使用してスキーマを定義できます。 ## プログラムからモデルを利用する モデルのエコシステムと各種機能を把握したところで、これらのAPIをGoアプリケーションに組み込む方法を見ていきましょう。 用途に応じて異なるモデルが存在するのと同様に、複数のAPIサーフェスが提供されています。まずは最も基本的な「Generate Content」から見ていきましょう。 ### Generate Content API これは最も基本的な[生成API](https://ai.google.dev/api/generate-content#method:-models.generatecontent)です。単一のリクエストを受け取ってレスポンスを返すステートレスなインターフェースです。複数ターンにわたる会話の場合、アプリケーションは呼び出しごとに完全なチャット履歴を送信する必要があります。 そのため、コンテキストウィンドウの制限内に収まるように会話履歴を能動的に管理する必要があります。一般的に、アプリケーションは履歴が一定のしきい値に達した時点で要約を行います。長時間のセッションにおける入力コストを削減するために、Gemini APIはGemini 2.5以降のすべてのモデルで[暗黙的キャッシュ(implicit caching)](https://ai.google.dev/gemini-api/docs/caching)をサポートしているほか、大容量ペイロード向けの[明示的キャッシュ(explicit caching)](https://ai.google.dev/gemini-api/docs/generate-content/caching)も利用できます。 Generate Content APIはシンプルなステートレス生成に適していますが、より新しく高機能なInteractions APIへと段階的に置き換えが進んでいます。 ### Interactions API > 注:現時点では、Interactions APIは公式のGo GenAI SDKでまだサポートされていません。実装の進捗状況はこの [GitHub issue](https://github.com/googleapis/go-genai/issues/658) で追跡されています。 [Interactions API](https://ai.google.dev/gemini-api/docs/interactions-overview) は、シンプルなチャットやツール利用から複雑なエージェンティックワークフローまで、あらゆるタスク向けに設計されたGoogleの統合インターフェースです。会話履歴をサーバー側で管理できるため、アプリケーション側で履歴を管理する必要がありません。 ### Live API [Live API](https://ai.google.dev/gemini-api/docs/live-api) は、WebSocketを介したリアルタイムかつ双方向の音声・動画の会話を可能にします。ユーザーの発話や割り込みを自動的に検知し、ライブセッション内でWeb検索やファンクションコーリングなどのツールを直接サポートしながら、自然な音声インタラクションを実現します。 ### Batch API [Batch API](https://ai.google.dev/gemini-api/docs/batch-api) を使用すると、大量のデータを半額の料金で非同期に処理できます。ジョブはオフピーク時間帯にバックグラウンドで実行され(通常24時間以内に完了)、緊急性の低いワークロードに最適です。 ### Managed Agents API [マネージドエージェント(Managed agents)](https://ai.google.dev/gemini-api/docs/agents) は、AIエージェントが自律的にタスクを計画・実行できるフルホスト型のランタイム環境を提供します。単一のAPI呼び出しを行うだけで、PythonやNodeなどの事前インストール済みランタイムを備えたOS分離のLinuxサンドボックスがプロビジョニングされ、エージェントがコードの実行、ファイル管理、Webの閲覧を自律的に行えるようになります。 Googleは、最初から使える2つの事前構築済みマネージドエージェントを提供しています: - **Antigravity Agent** (`antigravity-preview-05-2026`): コード実行、ファイル管理、Webアクセスをこなす、Gemini 3.6 Flash(Gemini 3.5 FlashまたはFlash-Liteにも設定可能)駆動のデフォルト汎用エージェント。 - **Deep Research Agent** (`deep-research-preview-04-2026`): 複数のWebソースからデータを収集し、詳細なリサーチレポートをバックグラウンドでまとめる自律型リサーチエージェント。 また、システムルールをインラインで定義したり、`AGENTS.md` ファイルをマウントしたり、構造化されたスキルディレクトリ(`SKILL.md`)をアタッチしたり、ローカルファイル、Cloud Storageバケット、Gitリポジトリをリモートワークスペース(`/workspace`)に直接マウントすることで、Antigravityエージェントを拡張することもできます。 ## アクセスと請求 Geminiを統合する際、Googleはニーズに応じて2つの主要なアクセスおよび請求モードを提供しています: 1. **Google AI Studio (Google AI)**: APIキー(`GEMINI_API_KEY` または `GOOGLE_API_KEY`)を使用してGemini API経由でリクエストをルーティングします。プロトタイピング、個人プロジェクト、個人開発アプリ、迅速な開発着手に最適です。 2. **Gemini Enterprise(旧 Vertex AI)**: Google Cloud IAM、Application Default Credentials (ADC)、サービスアカウントキー、またはOAuth 2.0ユーザートークンを使用して、Google Cloudエンドポイント経由でリクエストをルーティングします。厳格なデータプライバシー、セキュリティコンプライアンス、SLA、GCPリソース管理、確約利用割引が必要なエンタープライズの本番ワークロードに最適です。 ## Go GenAI SDK それでは、実際のGoコードでの動作を見ていきましょう。 GoアプリケーションにGeminiを統合するための公式SDKは [`google.golang.org/genai`](https://pkg.go.dev/google.golang.org/genai) です。 すべてのGoogleモデルに対応し、Google AIとGemini Enterpriseの両方の認証方式をサポートするよう設計されているため、「統合SDK(unified SDK)」と呼ばれることもあります。すでに非推奨(deprecated)となった旧パッケージ `github.com/google/generative-ai-go` を置き換えるものです。 `go get` でインストールします: ```bash go get google.golang.org/genai ``` Gemini Enterpriseの認証と請求を利用する実装例は以下の通りです: ```go package main import ( "context" "fmt" "log" "os" "google.golang.org/genai" ) func main() { ctx := context.Background() // Initialize the client for Gemini Enterprise (Vertex AI) client, err := genai.NewClient(ctx, &genai.ClientConfig{ Project: os.Getenv("GOOGLE_CLOUD_PROJECT"), Location: "global", // Nano Banana models are served globally Backend: genai.BackendEnterprise, }) if err != nil { log.Fatalf("failed to create GenAI client: %v", err) } prompt := "Generate a high-resolution, cute image of a fluffy cat wearing a tiny wizard hat." // Call Nano Banana 2 Lite (gemini-3.1-flash-lite-image) resp, err := client.Models.GenerateContent( ctx, "gemini-3.1-flash-lite-image", genai.Text(prompt), nil, ) if err != nil { log.Fatalf("failed to generate image: %v", err) } // Extract generated image bytes from response parts for _, candidate := range resp.Candidates { if candidate.Content == nil { continue } for _, part := range candidate.Content.Parts { if part.InlineData != nil && part.InlineData.Data != nil { filename := "cute_cat.png" if err := os.WriteFile(filename, part.InlineData.Data, 0644); err != nil { log.Fatalf("failed to save image: %v", err) } fmt.Printf("Successfully generated and saved cat picture to %s!\n", filename) return } } } log.Fatal("no image data returned in response") } ``` このサンプルを実行するには、AI Platform APIが有効化されたGoogle Cloudプロジェクトが必要です。例: ```sh export GOOGLE_CLOUD_PROJECT="your-project-id-goes-here" go run main.go ``` 実行結果は次のようになります: ![Goターミナルで生成された魔法使いの猫の画像出力](image.png "AIの真の目的:無限の猫画像生成") これは低レベルSDKの仕組みを示すシンプルな例ですが、ローカルのCLIツールから本番環境のエージェントランタイムまで、すべて同じ基本プリミティブの上で動いています。 ## 次に読むべきガイド GeminiのモデルカタログとGo GenAI SDKの基本を押さえたら、目的に合わせて次の2つのステップに進むことができます。 * Geminiを活用してGoコードの作成・検証・リファクタリングを高速化したい場合は、[**なぜGoはコーディングエージェントに最適なのか(そしてそのチューニング方法)**](https://danicat.dev/ja/posts/20260817-gemini-for-go-developers-part-2-coding-with-gemini/)をご覧ください。 * ツール呼び出しを行う自律型エージェントを構築し、Go GenAI SDKと [Genkit](https://genkit.dev/) や [ADK](https://adk.dev/) といった高レベルフレームワークを比較したい場合は、[**GoでAIエージェントを構築する:GenAI SDK、Genkit、ADKの徹底比較**](https://danicat.dev/ja/posts/20260825-gemini-for-go-developers-part-3-building-agents/)に進んでください。 --- # 生成AIとアート:それは本当にただの「スロップ」なのか? - **Markdown URL**: https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/index.md - **HTML URL**: https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/ - **Date**: 2026-07-27 - **Categories**: Perspectives - **Tags**: ai, art - **Summary**: 生成AIアートは単なる手抜きのスロップなのか?歴史的変革の類似点、プロンプトのドメイン言語、創作意図とゲートキーピングの本質を考察。 「スロップ(Slop / 粗悪な生成物)」は、このテクノロジーサイクルを象徴する侮蔑的な言葉です。とても便利な言葉ではあります。プロンプト欄に適当な単語を3つ打ち込み、何の推敲もなしに公開された手抜きのコンテンツでフィードが埋め尽くされる――あのうんざりするような疲労感を的確に表現しているからです。しかし、ろくな検討もなしに、まったく新しいアートのカテゴリ全体を切り捨てる言葉として使ってしまってよいものでしょうか? ## 免責事項:本稿はAIの学習に関するものではありません 本題に入る前に、まずはっきりとさせておきたい前提(いわゆる「部屋の中の象」)があります。ここで学習データやWebスクレイピング、著作権法について議論するつもりはありません。私にはこれらの分野について語る専門性はありませんし、語るつもりも毛頭ありません。 ![Training Phase vs Inference Layer Diagram](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/you_are_not_here.png) 一方で、私はプラグマティスト(現実主義者)です。これらのモデルはすでに存在しており、今後消えることもありません。私たちが好むと好まざるとにかかわらず、人々はこれらを使い続けるでしょう。ですから、そうした議論は専門家に任せ、いま目の前にあること――すなわち「推論(Inference)」、言い換えればプロンプティングを通じてアートを生成するプロセスに焦点を当てたいと思います。 ## テクノロジーとアートの歴史的サイクル 世界史を学ぶとき、美術史も並行して学ぶことが多いのはなぜか、考えたことはあるでしょうか? あらゆる重大な技術革命は、アートを生み出す新たな手法をもたらしてきました。活版印刷、写真、コンピューターの誕生など、私たちはこれまでに何度もそうした潮流を目にしてきました。そして興味深いことに、あらゆる新しい芸術形態は、その時代の「守旧派(オールドガード)」から激しい批判を浴びてきたのです。 17世紀のオランダでは、オランダ写実派の画家たちが商人や酒場の情景、静かな台所といった日常の風景を描きました。しかし当時の古典派の画家たちは、それを「安っぽく、商業的で、手抜きだ」と切り捨てました。産業革命の時代には、新古典主義者たちがロマン主義の感情豊かな表現スタイルを「熱病のうわ言」と嘲笑しました。それでもなお、どちらの潮流も美術館を埋め尽くし、やがて未来のアーティストたちが乗り越えるべき「伝統的な規範」となったのです。 AIアートをめぐる現在の怒りも、まさにこのパターンに当てはまります。いま私たちが目の当たりにしているのは、新しい表現媒体と、職人技(クラフトマンシップ)に対する古い固定観念との衝突にほかなりません。 ## ある人にとってのスロップ、別の人にとってのアート そもそも、「スロップ」を生み出すのにAIなど必要ありませんでした。画像生成モデル(Text-to-Image)が登場するはるか昔から、社会はどこまでがアートで、どこからが手抜きのノイズなのかを議論し続けてきました。本質的な問いは「どこに境界線を引くのか?」ということです。 いくつかのアート作品と、その対比となる作品を見てみましょう。 ### 古典的な肖像画 vs. 大量生産されたポップアート | *モナ・リザ*(レオナルド・ダ・ヴィンチ) | *モナ・キャット*(ロメロ・ブリット) | | :---: | :---: | | ![Mona Lisa by Leonardo Da Vinci](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/mona_lisa.jpg) | ![Mona Cat by Romero Britto](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/mona_cat.png) | ダ・ヴィンチの『モナ・リザ』は文句なしにファインアートとして受け入れられていますが、ロメロ・ブリットの『モナ・キャット』はどうでしょうか? 工業的な手法や大量生産を用い、模倣的であることから、批評家はブリットの作品を「商業的スロップ」と呼ぶことがよくあります。あなたはどう思いますか? その批判は妥当でしょうか、それとも単なる好みの問題でしょうか? 彼の作品が人によってまったく異なる感情を呼び起こす以上、この問いに唯一の正解はありません。 ### 意図 vs. 実行 | *エッケ・ホモ(この人を見よ)*(エリアス・ガルシア・マルティネス) | *修復された「エッケ・ホモ」*(セシリア・ヒメネス) | | :---: | :---: | | ![Original Ecce Homo](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/ecce_homo_original.jpg) | ![Restored Ecce Homo](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/ecce_homo_restored.jpg) | 傷んだフレスコ画の修復作業として、大半の人から「お粗末な仕事」とみなされたものが、結果としてスペインの小さな町ボルハにとって「怪我の功名(幸運なハプニング)」となりました。インターネットのおかげで、1930年頃にエリアス・ガルシア・マルティネスによって描かれた[『エッケ・ホモ』](https://en.wikipedia.org/wiki/Ecce_Homo_(Garc%C3%ADa_Mart%C3%ADnez_and_Gim%C3%A9nez))への関心は爆発的に高まり、ポップアートのバイラルヒットとなって世界的な観光地へと変貌したのです。これはアマチュア修復家セシリア・ヒメネスのビジョンに技術が追いつかなかった例ですが、たとえ本来の意図とは異なる形になったとしても、その試み自体がアートになりました。 | *風船と少女*(バンクシー) | *愛はごみ箱の中に*(バンクシー) | | :---: | :---: | | ![Girl with Balloon by Banksy](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/banksy_girl_with_balloon.png) | ![Love is in the Bin by Banksy](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/banksy_love_is_in_the_bin.jpg) | 意図と実行が一致しなかったものの、結果として新たなアート作品を生み出したもう一つの有名な例が、バンクシーの『Love is in the Bin(愛はごみ箱の中に)』です。オークションで落札された瞬間に絵画を裁断するはずだった仕掛けが途中で停止し、『風船と少女(Girl with Balloon)』は半分だけ裁断された状態で止まりました。100万ポンド以上で落札されたばかりのアート作品が目の前で細切れにされていく体験全体が、それ自体ひとつの芸術表現だったと言えるでしょう。この作品が最後にオークションに出品されたときには、なんと1,800万ポンド以上の値がつきました! ### ギャラリーのバナナ vs. 忘れられたキウイ | *コメディアン*(マウリツィオ・カテラン) | *忘れられたキウイ*(ダニエラ・ペトルザレック) | | :---: | :---: | | ![Comedian by Maurizio Catellan](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/maurizio_catellan_comedian_banana.jpg) | ![Forgotten Kiwi](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/forgotten_kiwi.jpg) | これは現代における「低労力アート」の最たる例かもしれませんが、それでもオークションで12万ドル(約1,800万円)もの高値がつきました。マウリツィオ・カテランはバナナをダクトテープでギャラリーの壁に貼り付けました。購入者はどうしたか? 食べました。文脈と『Comedian(コメディアン)』という作品名がなければ、ただの法外に高いバナナにすぎません。これをスロップと見る人もいれば、消費主義、あるいはアートそのものに対する批評と見る人もいます。 なぜ『Comedian』はアート作品で、私が冷蔵庫の奥に置き忘れていたこのキウイはアートではないのでしょうか?(ご安心ください。『Comedian』の購入者と同じく、写真を撮った後においしくいただきました。)実際のところ、アートとは必要な労力や物理的な物体そのものだけで定義されたことなど一度もありません。それは文脈、意図、そして鑑賞者の反応によって決まるのです。 冗談抜きで、私の『忘れられたキウイ(Forgotten Kiwi)』がカンファレンスの登壇やこのブログで紹介された今、これもまた実際にアートになったのかもしれません。 ## アートをゲートキーピングすべきではない 新たな表現媒体が登場すると、ゲートキーパー(門番)たちは何が「本物のアート」であるかを定義しようとします。そしてほぼ例外なく、その定義を肉体的な労力、手作業の技能、あるいは伝統的な道具と結びつけようとするのです。 19世紀に写真が登場したとき、画家たちはそれを拒絶しました。「ボタンを押すだけなんて一瞬であり、絵筆の技術を必要としないからアートではない」と主張したのです。写真がアートとして広く認められるまでには何十年もかかりました。 ![Eyes of the Night by Jade Gosrani](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/eyes_of_the_night_leopard.jpg) ボタンを押すのは一瞬です。しかし、ジェイド・ゴスラニの『Eyes of the Night』のような写真を撮影するには、何日も野生動物を追跡し、光を極め、辛抱強く待ち続ける必要があります。アートとは、ボタンを押す物理的な動作ではなく、ビジョン、周到な準備、そして意図の中にこそ存在するのです。 私たちは他の分野において、すでにビジョンと手作業による実行を切り離して考えています。 ![Frieren Concert in London](frieren_orchestra_london.jpg "ロンドンでのフリーレン・オーケストラコンサート。写真提供: https://x.com/AnimeBlurayUK") 音楽では、『葬送のフリーレン』の劇伴音楽の功績を作曲家のEvan Callに帰します。ライブコンサートにおいて、Call自身がすべての楽器を演奏するわけではありません。指揮者のもとで何十人もの演奏家が音を奏で、作曲家はビジョン、構成、そして総譜(スコア)を提供するのです。 | *AKIRA*(大友克洋、1988年) | *タイタニック*(ジェームズ・キャメロン、1997年) | | :---: | :---: | | ![Akira 1988](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/akira_1988_bike_slide.jpg) | ![Titanic Behind the Scenes](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/titanic_behind_the_scenes_green_screen.jpg) | 映画の世界では、大友克洋の『AKIRA』は16万枚以上の手描きセル画を使用しています。大友自身がすべてのセル画を描いたわけではありません。スタジオ全体が彼のビジョンを具現化したのです。ジェームズ・キャメロンの『タイタニック』でも、何十人ものクルーが模型を製作し、視覚効果を動かしました。私たちが称賛するのは、現場のすべての技術者ではなく、物語を形作った映画監督です。 Wikipediaには、アートに関する次のような包括的な[定義](https://en.wikipedia.org/wiki/Art)が示されています: > *「アート(芸術)とは、創造的または想像力豊かな才能を活用した作品を中心とする多様な文化活動であり、一般に感情的な力、概念的なアイデア、技術的熟練度、あるいは美しさの表現を通じて、価値ある体験を呼び起こすことが期待されるものである」* 興味深いことに、そのすぐ次の段落では、この定義ですら共通認識ではないことが認められています: > *「何が芸術を構成するのかについて一般的に合意された定義は存在せず、その解釈は歴史や文化を通じて大きく変化してきた」* 百科事典ですらアートを厳密に定義することはできません。技術とは単なる手作業の労力だけを指すのではないため、ゲートキーピングは必ず破綻します。技術の本質は「クリエイティブ・ディレクション」へと移行していくのです。 ## スロップの実践的な定義 スロップとは、ビジョン、努力、そして意図(intent)の欠如から生み出されるものだと私は考えています。いわばコンテンツ制作のファストフード版です。数学的な表現を好む方のために言うなら、**スロップはインスピレーション(創作意図・ひらめき)に反比例する**と言えます。 \[ \text{SLOP} \propto \frac{1}{\text{INSPIRATION}} \] インスピレーションとは、頭の中にあるアイデアを取り出し、他者とつながる形へと昇華させることです。ツールに対してインスピレーションを一切持ち込まなければ、モデルがどれほど優れていようと、得られるのはスロップだけです。 `anime girl` という2単語だけの手抜きプロンプトと、ビジョンを具現化するために画像生成のドメイン言語を丹念に探求した次のプロンプトを比較してみてください: ```text Character: teenage girl with pink hair, shoulder length, wavy. Blue eyes, gentle smile. School uniform, white blouse, tartan skirt. Scenario: classroom, back seat, close to the window. Action: girl is sitting at her desk, book open in front of her, but she is looking at the window, holding her chin with her left hand, melancholic look, pensive. Style: anime, watercolor, 90's, pastel tones, shoujo Environment: midday light, natural light from the window, artificial light from the ceiling Camera: low angle, profile picture three-quarter view ``` そして生成された結果がこちらです: ![90年代少女アニメ風の教室シーン](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/anime_girl_classroom_90s_shoujo.jpg) 2つ目のプロンプトは、画風、構図、光、レイアウトに関する知識に基づいています。生成するたびに出力は微妙に変化しますが、プロンプトが明確な方向性を示しているため、雰囲気や構図、全体のディレクションは一貫して保たれます。 画像生成であれコード記述であれ、モデルを効果的に誘導するためには、その表現媒体における**ドメイン言語**を習得しなければなりません。映画的な画像であればカメラアングルやライティング(*low angle*, *three-quarter view*, *rim light*)を理解する必要があり、イラストであれば画風やスタイル(*90s shoujo*, *watercolor*)を知る必要があります。 ![プロンプト構造ガイド](https://danicat.dev/ja/posts/genai-and-art-is-it-really-just-slop/prompt_structure_guide_kantakanta.jpg) 構造化されたプロンプト([@kantakanta1233](https://x.com/kantakanta1233) 氏による上記のフレームワークなど)は、スタイル、主題、特徴、衣装、ポーズ、構図、背景、照明、仕上げ、ネガティブキーワードという10の特定領域を網羅しています。このようなプロンプトを構築すること自体がクリエイティブ・ディレクションであり、プロンプトを作成した後も最終成果物を得るまでに何度も生成と微調整を繰り返します。これらすべてが創作プロセスの一部なのです。 ## 優れたプロンプトであるだけでは不十分、それは「あなた自身の」プロンプトでなければならない この問いはGDG Community Summitでの私の登壇中に挙がったもので、ここでも触れておく価値があると感じました。「誰かが多大な時間とインスピレーションを注ぎ込んで完璧なプロンプトを作り上げたとして、別の人がそのプロンプトをそのままコピーし、最小限の手間で実行した場合、それはアートと言えるのか?」という問題です。 このケースにおいては、名画を贋作したり、Stack Overflowからコードをコピペしたりする行為と何ら変わりありません。プロンプトをそのまま盗用して実行しただけの人は、単なる「模倣者(コピーキャット)」です。もちろん、インターネット上の画像が出所(プロベナンス)として作者本人によって生み出されたものか、単にプロンプトをコピーした第三者によるものかを追跡するのは困難です。最終的には、何がアートであり何が手抜きの模倣品であるかを判断する上で、信頼と評判(レピュテーション)が極めて重要な役割を果たすことになるでしょう。 ## ソフトウェアエンジニアリングとの類似点 すでにお気づきかもしれませんが、ここでアートについて論じていることはすべて、私たちのソフトウェアエンジニアリングの現場にもそのまま当てはまります。多くのエンジニアがすでにAIコーディングを取り入れている一方で、いまだにそれを「バイブコーディング(vibe coding)」のスロップとして切り捨てる人々も少なくありません。「これがエンジニアのキャリアを台無しにしている」という危機感から、強い反発が生まれているのです。 私にとって、AIはその正反対の存在です。仕事の退屈な部分を自動化してくれるだけでなく(もう二度と単純なCRUD APIなんて書きたくありません!)、自分一人では到底成し得なかったことまで実現可能にしてくれます。 たとえば、私がエンジニアの世界に飛び込んだ最大の理由はゲームを開発したかったからでしたが、その夢は実現しませんでした(後悔はしていませんが)。しかし今、AIのサポートによって参入障壁が劇的に下がり、限られた時間やスキルであってもゲーム制作を楽しめるようになりました。自分一人で作曲はできませんが、GeminiやLyria 3のサポートがあれば、モデルを自分のビジョンへと誘導できます。すべてのスプライトやアセットを自力で描き上げる根気はありませんが、Nano Bananaを使えばモデルを導いて必要なアセットを生成し、何日も何週間もかけることなく、わずか数時間でプレイ可能なデモを完成させられます。 アートが単に絵筆を握ることだけではないのと同様に、エンジニアリングも単にエディタにコードを打ち込むことだけではありませんでした。ピクセルのレンダリングやGoのボイラープレートコードの記述といった機械的な作業をAIツールが担うようになるにつれ、私たちの役割はスタックのより上位層――ビジョン、アーキテクチャ、ドメイン知識、そして意図――へと移行しています。 今回のパニックは、人間の創造性の終焉ではありません。活版印刷、写真、コンパイラが登場したときと同じ、おなじみのサイクルの通過点にすぎないのです。初期の反発は、古い仕事のやり方を守ろうとする防衛本能です。しかし時間が経つにつれて抵抗は薄れ、人々はツールを受け入れ、その媒体は当たり前のものとなっていきます。今日、批評家たちが「スロップ」と拒絶しているものは、明日の標準的な道具となり――そしてやがて、未来のクリエイターたちが乗り越えようとする「古い標準」になっていくのです。 あなたはどう考えますか? AIアートは一時的な摩擦のフェーズにあると思いますか、それとも異なる見方をしていますか? --- # サブエージェントの台頭 - **Markdown URL**: https://danicat.dev/ja/posts/the-rise-of-the-subagents/index.md - **HTML URL**: https://danicat.dev/ja/posts/the-rise-of-the-subagents/ - **Date**: 2026-07-22 - **Categories**: Agentic Coding - **Tags**: agile, antigravity, subagents - **Summary**: Antigravityにおけるサブエージェントの進化と、Go言語での自律型コーディングスウォームをオーケストレーションするSwarm Codingスキルを解説。正直に告白すると、最初に**サブエージェント(subagents)**について読んだとき、私は少し懐疑的でした。個別のコンテキストウィンドウでタスクを実行するメリットは理解できましたが、並列で数十、あるいは数百ものエージェントを立ち上げるという発想はまったくありませんでした。というより、そうすることのメリットが見出せなかったと言ったほうが正確かもしれません。 バックグラウンドで2、3台のコーディングエージェントを管理するだけでも、私のメンタル帯域(脳のリソース)は大きく消費されます。私が並列で作業を行うのは、エージェントが長時間に及ぶ処理で忙しいと分かっているときだけです。2〜3台のエージェントを管理するだけでこれほど大変なのに、何百ものエージェントを並行管理するなんて、とても想像すらできませんでした。 答えにたどり着くまでにずいぶん時間がかかりましたが、その答えとは実は「**自分で管理する必要はない**」だったのです! サブエージェントの管理責任は、エージェント自身に委ねてしまえばよいのです。「計算機科学のあらゆる問題は、新しい抽象化レイヤーを追加することで解決できる」と言いますが、今回も例外ではありませんでした。 この記事では、過去12ヶ月ほどの間に私が目にしてきたサブエージェント・パラダイムの進化を振り返り、その経験を**Swarm Coding**(スウォームコーディング)という1つのエージェントスキルへと昇華させたプロセスを紹介します。要約(TL;DR)だけ読みたい方は、次のセクションをスキップして、記事末尾のスキル定義と解説へ直接進んで構いません。 ## サブエージェント進化の簡単な(そして不完全な)タイムライン サブエージェント自体は決して新しいものではありません。私は、サブエージェントという言葉が定着するずっと前から、モデル呼び出しをMCPツールとしてパッケージ化することで、この手法を使っていました。例えば、初期リリースの[**GoDoctor**](https://danicat.dev/ja/posts/20250729-how-to-build-an-mcp-server-with-gemini-cli-and-go/)には、カスタムの[コードレビュー](https://danicat.dev/ja/posts/20260303-code-reviews-in-2026/)プロンプトを指定してGeminiを呼び出す`code_review`ツールがありました。このツールは事実上のサブエージェントでしたが、挙動はハードコードされており、会話を継続することはできませんでした(技術的には可能でしたが、毎回バイアスのないレビューを求めていたため実装しませんでした)。 昨年の冬頃、主要なコーディングエージェント(Claude、Gemini CLIなど)が、Markdownファイルで定義されたカスタムサブエージェントのサポートを次々と開始しました。専門知識を厳選されたツールセットと一緒にパッケージ化する手段として、私はこのパターンがとても気に入りました。理想的には、GoDoctorも単なるツールの集合体ではなくスペシャリストエージェントであるべきでしたが、エコシステムが目まぐるしく変化し、サブエージェントの標準仕様がなかなか安定しなかったため、結局そのようには実装しませんでした。 それから数ヶ月が経ち、2026年5月にAntigravity 2.0がサブエージェントのサポートを追加しましたが、ひとつ落とし穴(制約)がありました。サブエージェントは、`DefineSubagent`ツールの呼び出しによってオンザフライ(その場)で動的に定義される仕様だったのです。当初の`DefineSubagent`は柔軟性に乏しく、現在の(デフォルトの)エージェントを新しいプロンプトで複製するだけのものでした。クリーンなコンテキストが得られるメリットはあるものの、エージェントの再利用性という面では失うものがありました。私が思い描いていたGoDoctorの進化が阻まれてしまったため、あまり満足できませんでした。 デフォルトエージェントとは異なるモデルやツールセットを持つカスタムエージェントを定義できなかったため、私は一旦サブエージェントの存在を忘れ、Gemini CLIでうまく動いていたものをAntigravity CLIへ移植することに注力しました(結果は上々でした)。 私がサブエージェントのアイデアを再考することになったのは、6月に[Richard Seroter](https://seroter.com/2026/06/01/one-prompt-four-subagents-and-ninety-seconds-to-get-a-working-app/)が公開したこのプロンプトがきっかけでした: > Let's build a hotel room booking app for Seroter Hotels consisting of a Go backend API and a web frontend. > > First, launch the **Engineering Manager** agent to design the API and frontend, saving the design and a Mermaid diagram into an artifact called 'architecture.md'. > > Once the design is ready, launch three agents in parallel: > 1. **Test Manager**: Write a simple API test plan and append it to 'architecture.md'. > 2. **Backend Engineer**: Build a clean Go REST API with standard error handling based on the design. > 3. **Frontend Engineer**: Build a responsive web UI using a simple CSS framework like Tailwind to interact with the API (skip UI testing). > > As soon as the Test Manager finishes the plan, have them hand it off to the Backend Engineer, who reads the plan from 'architecture.md' and adds the Go tests to the code. After both engineers finish building, the Test Manager runs the tests. Finally, spin up both components and a browser so I can test the live app. このプロンプトには非常に興味深い提案が含まれており、私はこのパターンを再検討することにしました。しかし、依然として2つの懸念が残っていました。1つ目は、サブエージェントを前提とした思考に合わせて、自分のプロンプティングスタイルをどれだけ適応させなければならないかという点。そして2つ目は、そもそもなぜこのような書き方をしなければならないのか、という疑問です。 私は極めて現実的(プラグマティック)です。品質や速度において明確なメリットがなければ、余計な労力をかけたくありません。サブエージェントの観点で考えることは、従来のプログラミングにおける並行処理(コンカレンシー)の設計とよく似ています。最初の問いは「これはそもそも並列化可能なのか?(parallelisable)」であり、2つ目の問いは「追加のオーバーヘッドでわずかな利益が吹き飛んでしまわないか?(それだけの価値があるのか?)」です。 Richardのプロンプトにおいて、明確に直交(独立)しているコンポーネントはバックエンドとフロントエンドの開発だけです。実装すべき明確なインターフェース契約(APIコントラクト)さえあれば、互いに依存しません。しかし、それ以外のエージェント同士は何らかの依存関係を持っており、並列というよりは直列(シーケンシャル)な処理になります。 そうであるならば、得られるメリットは並列処理による速度向上ではなく、「コンテキストの分離」そのものに由来することになります。そして、この規模でその効果を測定するのは容易ではありません。 私はその後の数週間、「サブエージェントを最大限に活かすには、どのような役割同士が直交しているべきなのだろうか?」という問いを頭のバックグラウンドで走らせながら過ごしました。 そして、ベルリンで開催されたGDE Summitで刺激的な議論を重ねる中で、ついに核心にたどり着いたのです。重要なのは、**人間(自分)**がプロンプトでサブエージェントを定義することではなく、いつサブエージェントを立ち上げるべきかをエージェント自身が判断できるように**教育(指示)する**ことだったのです。要するに、私は自分がリードエンジニアとしてチームにタスクを割り振る感覚で考えていましたが、本当に必要だったのは、コーディングエージェント自身をリードエンジニアに仕立て上げることでした。 ## スウォームコーディングの誕生 複雑なタスクを小さく分解し、チームメンバー間で作業を分散できるようにすることは、私にとって決して目新しいことではありません。Developer Relations(DevRel)の世界に入る前、私はTech LeadやPrincipal Engineerを務めていました。こうしたタスクは、特に私のようにアジャイル(Agile)出身のエンジニアにとっては、テクニカルリーダーシップのまさに日常茶飯事(基本中の基本)です。 これと同じTLの論理が、サブエージェントのスウォーム(群れ)を編成する際にもそのまま当てはまります。各エージェントが、他のエージェントから完全に独立して作業できる自己完結型のタスクを持っている状態を作る必要があります。タスクが着手可能(workable)であるためには、明確な着手条件(いわゆるDefinition of Ready)と、明確な完了条件(Definition of Done)が欠かせません。 ちなみに、この種のタスク分解や管理業務を「仕事の中で一番好きだ」と答える人はあまり多くありません(私自身も含めて)。だからこそ、本質的に「テクニカルリーダーシップの強化版」になってしまうような新しいプロンプティングスタイルの構築に、私が心理的な抵抗を感じていた理由もお分かりいただけるでしょう。 そこで、自分がエージェントたちのTLとして振る舞う代わりに、発想を逆転させて、エージェント自身にTLになってもらい、私のビジョンを実行するためのチームを自ら編成させるように教えることにしたのです。こうして生まれたのが、swarm codingの[初期バージョン](https://github.com/danicat/skills/blob/a9f57b10127d8bd23ed4867d64d168063a3726f4/swarm_coding/SKILL.md)でした。主要部分の抜粋を以下に示します: > Swarm Coding は、複雑なタスクに取り組むために複数のサブエージェントを並列で使用する新しい開発パラダイムです。「分割統治」戦略に基づいています。この戦略の主なメリットは、コンテキストの分離と品質の向上にあります。自己完結した小さなタスクをサブエージェントに割り当てることで、コンテキストの希薄化を防ぎ、ソリューションを極めて集中的に洗練させることができます。例えば、swarm codingを行わない場合、フロントエンドとバックエンドの両方を実装するエージェントは、双方に必要なスキルセット(異なる技術スタック、異なるベストプラクティスなど)が無関係であるため、注意が散漫になりがちです。 > > ## ROLE > > あなたはスウォームコーディネーター(SWARM COORDINATOR)です。あなたの役割は複雑なタスクを分解し、実行のためにサブエージェントへ委任(DELEGATE)することです。ユーザーや親コーディネーターから明示的に要求されない限り、どんなに単純に見えるタスクであっても、自分自身でタスクを実行してはなりません。ユーザーや親エージェントから指示コマンドを受信できるよう、通信チャネルは常にオープンにしておいてください。 > > ## AGENT BUDGET > > これは、タスクを実行するために生成を許可されたサブエージェントの総数です。エージェントの予算(BUDGET)をフルに活用するか、可能な限りそれに近づけることが推奨されます。これは価値の低いタスクにリソースを浪費することではなく、最高品質の成果物を得るために予算(BUDGET)の最適な配分を見つけることを意味します。 > > ## TEAM BUILDING > > 単純(SIMPLE)なタスクの場合、タスクを直交(独立)する要素に分解し、各要素に1つ以上の専門(SPECIALIST)エージェントを割り当てます。 > 複雑(COMPLEX)なタスクの場合、タスクをより小さな領域に分解し、それぞれにリード(LEAD)エージェントを割り当てます。リードエージェントにはタスク実行のためにエージェント予算の一部を配分します。リードエージェントは swarm coding スキルをアクティブにし、それぞれの領域における SWARM COORDINATOR となります。 > リードエージェントと実行(EXECUTOR)エージェントの完全なツリーが完成するまで、再帰的に処理を進めます。 > > ## COMMUNICATION > > SWARM COORDINATOR は、自身の配下にあるサブエージェントと直接通信する責任を持ちます。サブエージェント同士が直接メッセージをやり取りしてはなりません。同じ階層のエージェント間の連携は、設計ドキュメント(DESIGN DOCUMENTS)を通じて行います。設計ドキュメントへのすべての変更がスクアッド内のエージェントに確実にブロードキャストされるようにすることは SWARM COORDINATOR の責務です。競合が発生した場合は、SWARM COORDINATOR が曖昧さを排除し、決定を下す責任を持ちます。 > > ## PLANNING > > 計画(PLANNING)は最優先(FIRST CLASS)の取り組みであり、スウォームを活用して行われるべきです。各エージェントはそれぞれの専門知識を活かして計画に貢献します。スクアッドの SWARM COORDINATOR の役割は、チームが作成した計画の各パートをレビューし、不整合に対処し、対立がある場合に決定を下すことです。 > > ## EXECUTION > > 実行フェーズでは、主要なマイルストーンに沿ってスウォームの進捗を監視し、最終目標から逸脱しないよう必要に応じてエージェントを誘導(ステアリング)します。コーディネーターとして扱えるのはアーティファクト(ARTIFACTS)のみである点に注意してください。すべての開発タスクは、末端のサブエージェント(leaf sub-agents)によって処理される必要があります。 スキルを100%手作業でゼロから書いたのはこれが初めてでした。そうしなければ自分の思い描くビジョンを実現するのが極めて難しかったからです。このプロンプトは、スウォームを「再帰的」に動作させ、エージェント予算(Agent Budget)の残量だけで自分がコーディネーターになるか否かを自己判断させようとしたため、少し野心的すぎました。結果として、期待通りには動きませんでした。 実際に起こったのは、コーディネーターから与えられたタスクの指示が他のあらゆるルールよりも優先されてしまい、サブエージェントがエージェント予算を気にすることなく、いきなり実行モードに飛び込んでしまうという問題でした。スキルの現行バージョンでは、サブエージェントを起動するためのより明確なガイドラインとプロンプトテンプレートを用意することで、この問題を解決しています。 ## スウォームを実際に動かしてみる **swarm coding** スキルの現行バージョンは、[GitHubのdanicat/skillsリポジトリ](https://github.com/danicat/skills)で公開しています。以下のコマンドを使って、お気に入りのコーディングエージェントにインストールできます: ```bash $ npx skills add github.com/danicat/skills --skill swarm-coding ``` > **Note**: このスキルは現在も活発に開発が進められている(work in progress)ため、特定の実装や挙動に固定したい場合はフォークして利用することをおすすめします。 肩慣らしにぴったりの面白いプロンプトを紹介しましょう。ぜひ Antigravity CLI で試してみてください: > /swarm-coding agent budget 50. Develop a 2D tower defense survival game using Go and Ebitengine. The game should be feature complete and have one single screen level. Include an intro sequence, title screen, game win and game over screens as well. Track the high score at the end of each playthrough. Use 32x32 sprites with up to 256 colors each. The sprites should be custom designed for this game and each movement should have at least 3 frames of animation, but ideally 8. Tiles should be 32x32 as well. The level view is top down, movement is on four directions. The player should have access to 4 types of units and 4 types of buildings. The enemy waves should have 8 types of monsters, including one boss monster. Use typical build and attack phases with custom UIs for each. To create art, use vector graphics and/or dot (pixel) art creating each asset manually using binary data. Sound effects should be generated mathematically as well. The whole vibe of the game should match the 16-bit era, but with modern gameplay features. 以下は、私の開発環境での実行結果です: ![Swarm Defense](image-1.png "スウォームによって生成されたゲームのスクリーンショット") 最初のビルドではスプライトの描画バグで画面全体が真っ暗になってしまったため、完全な「一発成功(one-shot)」とは言えません。しかし、その問題を指摘するプロンプトをもう1回投げるだけで、上記のように正常なゲーム画面が立ち上がりました。 以下は、ボス戦(かわいそうに、ボスには手も足も出ませんでした)を含む実際の動作動画です: この動画に含まれるすべてのアセットはプログラムによって生成されたものです。言い換えれば、Antigravity は画像生成モデルへのアクセス権を持っていませんでした。そのため、機転を利かせてビットマップレベルでバイナリデータから直接スプライトを描画する必要があったのです。 このテクニックがこれほどうまく機能したのは、スウォームによって各エージェントが専門特化し、単一のタスクに集中できたからです。以前、単一のエージェントに同様のプロンプトを投げたことがありますが、大抵は中途半端な結果に終わっていました。1つのエージェントに互いに直交するタスクを過剰に詰め込むと、絵に描いたような「器用貧乏(master of none)」に陥ってしまいます。しかし委任(デリゲーション)を導入すれば、各エージェントは自己完結した1つのタスクに専念でき、持てる最高のパフォーマンスを発揮できるのです。 ## Antigravity 2.0 と Antigravity CLI におけるサブエージェントのサポート 本稿執筆時点では、サブエージェント機能は Antigravity 2.0 と Antigravity CLI の間でやや不均一に実装されています。これら2つのインターフェースは異なるワークフローを想定して作られているため、サブエージェント機能が一時的に分岐(diverge)しているのです。どちらのツールも急速に進化しているため、成熟に伴ってこの機能差は縮まっていくと予想されます。 その根底において、両環境は共通の基盤エンジンを共有しています。サブエージェントを起動するとタスクが委託され、直ちに制御がユーザーへと戻ります。サブエージェントは完全にクリーンな状態で動作します。デフォルトセッションと同じモデルを使用しつつも、完全に分離された独立コンテキストで起動するため、会話履歴が混入・漏洩することはありません。親エージェントは一意のIDを介してサブエージェントと通信します。未承認のコマンドを実行しようとした場合は、ユーザーへとパーミッション要求がエスカレーション(バブルアップ)されます。 両インターフェースの主な違いは以下の通りです: - **Antigravity 2.0**:管理はビジュアル中心です。グラフィカルなサイドバーを使って実行中のタスクを追跡したり、会話ログを閲覧したり、実行を停止したりできます。カスタムエージェントは `DefineSubagent` ツールを使ってオンザフライで動的に作成されます。プラグインによるサブエージェントのサポートはありません。 - **Antigravity CLI**:エージェントの動的生成に加え、Markdown ファイルを使ってカスタムエージェントを静的に定義することも可能です。Markdown 内の frontmatter オプションを使用することで、特定のモデルを固定したり、利用可能なツールを制御したりできます。また、CLI はプラグイン内に Markdown 形式で定義されたカスタムサブエージェントの読み込みもサポートしています。 現在のスウォーム環境を設定する上では、これらインターフェースの違いを把握しておくことが重要です。とはいえ、前述の通り、ツールの進化とともにこれらの機能はいずれ統合(収束)していくはずです。 ## ぜひ実際に試してみてください サブエージェントの真価を実感する最善の方法は、ご自身の手で実際に試してみることです。私のサンプルプロンプトを再現してみるのも、独自のアイデアを試してみるのも良いでしょう。きっとその仕上がりに驚かされるはずです。スウォームを使って面白いものを作ったら、ぜひ教えてください。その間、私は [Swarm Defense](https://github.com/danicat/swarm-defense) のブラッシュアップをもう少し進めておこうと思います。:) - swarm coding をはじめとするスキル一式は [GitHub の danicat/skills リポジトリ](https://github.com/danicat/skills) から確認できます - Antigravity のダウンロードおよび詳細については [Antigravity 公式サイト](https://antigravity.google) をご覧ください --- # 2026年におけるエージェントコーディングの現在地:Skills、Hooks、Subagents - **Markdown URL**: https://danicat.dev/ja/posts/state-of-agentic-coding/index.md - **HTML URL**: https://danicat.dev/ja/posts/state-of-agentic-coding/ - **Date**: 2026-06-15 - **Categories**: Agentic Coding - **Tags**: agent-skills, antigravity, hooks, mcp, subagents - **Summary**: 2026年におけるエージェントコーディングの最新動向。プランニングモード、Agent SkillsとMCPの比較、サブエージェントのオーケストレーション、本番環境における決定論的Hooksを振り返る。 コーディングエージェントを活用して生産性を高めるための主要な実践手法をまとめた記事 [Taming Vibe Coding](https://danicat.dev/ja/posts/20251206-taming-vibe-coding/) を公開してから、半年以上が経過しました。 その記事の内容の多くは現在でも十分に通用するものですが、この半年間でこの分野には非常に多くの進展がありました。そこで、私の考え方がそれからどのように進化してきたのか、簡単なアップデートを共有することにしました。 ## プロンプティングとコンテキストエンジニアリング プロンプティングは依然として重要であり、適切に構造化されたプロンプトは多くの時間を節約してくれます。しかし、プランニングシステムの登場により、以前ほど成否を分ける決定打ではなくなりました。現在のコーディングエージェントの多くはプラン(あるいはプランニング)モードを搭載しており、コーディングに飛びつく前に数ターンかけてタスクを「ブレインストーミング」し、実装プランを練り上げます。 これにより、コードが書かれる前にプランをレビューしてエージェントを誘導できるため、多くの時間とトークンを節約できます。一方で変わらないのは、一貫した結果を保証するための明確な受入基準(acceptance criteria)の必要性です。これは、私がプランをレビューする際にも普段最も注意を払っている部分です。 Agent Skills の普及によってコンテキストエンジニアリングは大幅に向上し、最近では AGENTS.md(あるいは GEMINI.md、CLAUDE.md など)を書くことをほとんど気にしなくなりました。Antigravity には Rules という概念があり、これは本質的にはコンテキスト最適化ですが、私はスキルを好んで Rules をほとんど使っていません。AGENTS.md や Rules で行う必要があり、他のテクニックでは実現できないような要件には、今のところ出会っていません。 セマンティック検索などを利用した検索拡張生成(RAG)に関連するコンテキストエンジニアリングの側面は、専門知識を扱う上で依然として重要です。これはメモリシステムの動作原理でもあり、外部の依存関係を扱う際には、パッケージドキュメントをコンテキストに注入するこのテクニックを今でも使っています。 ## Model Context Protocol の台頭と衰退(?) MCP サーバーを立ち上げる代わりに、Agent Skills と CLI ツールを組み合わせて使うことを好む人が増えています。その考えも尊重しますが、私自身はエージェントにシェルの直接アクセス権を与えることに多少の不信感があるため、ローカルで実行する MCP サーバー([GoDoctor](https://danicat.dev/ja/posts/20250729-how-to-build-an-mcp-server-with-gemini-cli-and-go/) や [Speedgrapher](https://danicat.dev/ja/posts/20250805-introducing-speedgrapher/) など)にツールをパッケージ化する方を好んでいます。私にとって変わった唯一の点は、MCP サーバーのインストールに対してはるかに選別的になり、ほとんどの場合は自作のカスタムサーバーだけを設定するようになったことです。 また、私は MCP サーバー単体で使うことはほとんどなく、「スキル + MCP」の組み合わせで使用しています。スキルがプロセスを記述し、MCP がツール群を公開します。これは MCP の instructions(指示)自体を最適化しようとするよりも優れている場合が多いです。かつて私はその最適化に何百時間も費やしましたが、コーディングエージェントに完全に無視される結果に終わりました。 普段は「MCP + スキル」を使うことが多いですが、本当に重要な案件では「MCP + スキル + Hooks」という "スーパーコンボ" を使います。Hooks の役割は、私が進めたい方向にエージェントを強制的に向かわせることです。私はこれを「エージェントをレールに乗せる」、あるいは「エージェントの自律性(agency)を下げる」と呼ぶことがあります。Hook システムを使うことで、望ましくないアクションをブロックし、使わせたいツールを使うようエージェントの背中をそっと押し(gentle nudge)、ツールが呼び出されるかどうかの確率的な要素を排除できます。言い換えれば、エージェントの振る舞いを決定論的に強制できるのです。 ## あらゆるプロセスにスキルを 私は再現性を持たせたいプロセスのためにスキルを作成しています。例えば、私が最も頻繁に使うスキルの一部はテクニカルライティングやレビューに関するものです。私は(このブログのように)常にコンテンツを制作しているからです。また、エージェントが扱うのに苦労しそうだとわかっている技術についてもスキルを書きます。これには通常、新しい技術、ニッチなプロジェクト、あるいは自作のツールなどが含まれます。 例えば、つい最近も A2UI ワークショップの準備で大変な思いをしました。A2UI はエージェント向けユーザーインターフェースを開発するための比較的新しいプロトコルです。あまりに新しい概念であり、私にも非常に具体的な指導ニーズがあったため、エージェントは手厚いサポートと多くの試行錯誤なしには理解できませんでした。最初のハードルを乗り越えた後は、この知識をスキルとしてパッケージ化しておくことで、次回似たような作業を行う際に物事をスムーズに進められるようになります。 ネガティブな面としては、私はスキルを最新かつ整理された状態に保つのがひどく苦手だということです。この問題を解決することが MCP にとっての名誉挽回の瞬間になり得ると考えています。現在、プロトコル仕様にスキルを追加する提案がなされているからです。残念ながら、それがいつ実現するのか(そもそも実現するのか)の見通しは立っていません。そのため、現時点では自分たちでスキルを管理し続けるしかありません。理論的には、プロンプトを段階的開示(progressive disclosure)に使い、スクリプトが必要な部分にツールを使うことで、MCP で「スキル風」の体験を作り出すことも可能ですが、現在のバックログを考えると、まだ試すには至っていません。 ## サブエージェントの台頭(そして衰退知らず) [Subagents](https://danicat.dev/ja/posts/20260722-the-rise-of-the-subagents/)(サブエージェント)は、界隈の誰もが話題にしている「次のクールなトレンド」です。そのアイデアは、分離されたコンテキストウィンドウを持つ独立したエージェントとしてタスクを起動し、並列化することです。これにはコンテキストの最適利用、タスク間の汚染防止、コンテキストの早期劣化(context rot)の回避という利点があります。また、各タスクが自己完結しておりメインのコンテキストウィンドウを汚染しないため、圧縮の必要性も低減します。コーディングハーネスのサポートという点では、スキルを宣言するのと同じように、それぞれ独自のシステムプロンプト、モデル、設定を持つエージェントを事前宣言できるハーネスもあれば、Antigravity のようにメインセッションの「クローン」としてエージェントをアドホックに生成し、それぞれに独立したコンテキストウィンドウを持たせる方式を支持するものもあります。 アドホックなエージェント生成では、1つのプロンプトから3つの異なるエージェントを立ち上げて並列処理させるといった思い切ったことができますが、Antigravity にも事前宣言されたエージェントがあればよいのにと感じます。事前宣言ができれば、自分なりに「厳選した」エージェントをポータブルな形でパッケージ化できるからです。また、エージェント間でタスクを並列化することは、習得するのが決して容易なスキルではありません。この1年でエージェントにタスクを渡すことには慣れましたが、それらを効率的にオーケストレーションする方法については、あまり深く考えていませんでした。 多くの面で、これはプロダクトオーナーやチームリードがタスクを分解し、チームがそれらにどう取り組むかを考えるときに使うのと同じ「筋肉」です。ただサブエージェントの場合、固定されたチームの代わりに、望むだけ何人でも「チームメンバー」を作成できます。結局のところ、私は「クールだから」という理由でエージェントを並列化することにはあまり関心がなく、「それによってより良い成果が得られるのか?」という問いの方を重視しています。 もしその問いへの答えが **いいえ(no)** であるなら、1度に1つのエージェントを実行する方が賢明です。プランニングに費やす労力や、タスクを慎重に分割する精神的な負担(mental toil)が報われないからです。これが、私がエージェントを事前定義できるようにしたい理由です。必要なときのためにエージェントを特化させておくだけでよく、それらが並列で動くかどうかは私にとって大して重要ではないからです。 ## Hooks は最高の相棒 最後にとっておきのお気に入りを残しておきました。Hooks です。Hooks は、エージェントのライフサイクルにおける特定のイベント発生時にトリガーされるコールバックです。先週、[Hooks に関する完全な記事](https://danicat.dev/ja/posts/20260610-mastering-hooks/) を書きましたので、この記事を読み終えたらぜひチェックしてみてください。要するに、モデルは予測不可能であり、極めて簡単に脱線(off the rails)してしまいます。Hooks はモデルにガードレールを追加し、運任せにすることなく望む方向へ導くための優れた手段です。それだけでなく、エージェントハーネスにモニターを接続してデータを収集し、応答品質を向上させる(例えば永続的なメモリシステムを追加するなど)ことも可能にしてくれます。 ## まとめ 業界の進化スピードは速く、第一線にとどまるためには、新しいプロセスや技術が登場したときにそれらを柔軟に取り入れ、足かせとなっている古い荷物を手放す適応力が必要です。それでも、(この記事を含め)世にあるいかなるガイドも唯一の真実だとは思わないでください。この技術はまだ初期段階にあり、誰もが手探りの学習体験の渦中にあります。鍵となるのは、自分の環境にとってどのような技術やワークフローが最も機能するのか、自ら実験して確かめることです。 この記事では、私にとってうまくいっていることと、私の考えがどのように進化してきたかを共有しましたが、私はすべてを知っているわけではなく、常に学び続けています。私たちはよく「AI のトレーニング」について語りますが、自分の脳をトレーニングすることの方がはるかに重要であることを忘れないでください。AI を思考停止の言い訳にせず、実験、学習、そして改善のイテレーションを続けましょう。そして、何か面白い発見があれば、ぜひシェアしてください! --- # コーディングエージェントの Hooks をマスターする:実践ガイド - **Markdown URL**: https://danicat.dev/ja/posts/mastering-hooks-in-coding-agents/index.md - **HTML URL**: https://danicat.dev/ja/posts/mastering-hooks-in-coding-agents/ - **Date**: 2026-06-10 - **Categories**: Agentic Coding - **Tags**: antigravity, automation, hooks, security - **Summary**: Google Antigravity をはじめとするコーディングエージェントの Hooks 活用ガイド。PreToolUse や PostToolUse イベントをインターセプトしてリンターや DLP ガードレールを適用する方法を徹底解説。 コーディングエージェントの能力の進化スピードは凄まじいものがあります。私が初めて彼らに触れたのは、Google に入社して間もない約1年前のことでした。当時、大きな話題を集めていたのは Model Context Protocol(MCP)でした。これはツールの場当たり的な個別実装を、ポータブルで再利用可能な実装へと置き換える(ほかにも多くのメリットを持つ)柔軟な技術です。 それから12か月が経ち、今では多くの人が MCP から次のトレンドである Agent Skills へと移行しているように見えます。「段階的開示(progressive disclosure)」を取り入れることで、スキルはコンテキストをより効率的に活用できるようにし、結果として全体的なトークンエコノミーを改善します。推論コストの上昇を考えれば、スキルが一気に脚光を浴びたのも不思議ではありません。 MCP とスキルの双方に熱狂的なファン層が存在しますが、この期間に登場したコーディングエージェントの標準規格の中には、これら人気の技術ほど頻繁には言及されないものの、極めて重要なものがあります。それが Hooks(フック)です。 MCP やスキルが(ツールや知識を追加することで)エージェントの能力を拡張することに焦点を当てているのに対し、Hooks は異なるレイヤーで動作します。エージェントループや開発プロセス全体を、より厳密にコントロールできるようにするのです。 ## エージェントの Hooks とは 「フック」という名前は最初はピンとこないかもしれませんが、実質的には「コールバック」と同じです。つまり、エージェントの処理ライフサイクルにおける特定の瞬間に呼び出されるプロシージャ(手続き)を指します。 Hooks は次の3つの要素で構成されます: - トリガーイベント:フックが呼び出されるタイミングです。通常は実行フェーズ(pre または post)とコンテキスト(ツール呼び出しやモデル呼び出しなど)で構成されます。たとえば Antigravity では、`PreToolUse`、`PostInvocation`、エージェント終了時の `Stop` といったイベントがあります。 - 条件またはフィルター:トリガーイベントに対する正規表現です。たとえばツール呼び出しの場合、フィルターはツール名や引数を含むことができます。たとえば、`run_command(git)` というツール呼び出しに対してフックを作成することが可能です。 - プロシージャ:シェルスクリプトやコマンドとして記述されるフックの本体です。プロシージャは、操作の許可・拒否の制御、モデルやツール呼び出しの完全なオーバーライド、あるいはログ記録やテレメトリ収集といった副作用(side effects)の実行に利用できます。 ## どんなときに Hooks を使うべきか Hooks は、エージェントのライフサイクルの特定の瞬間に介入し、カスタムコマンドやスクリプトを注入します。適切なタイミングを捉えて介入することで、処理フローを制御し、本来なら非決定論的(再現性がない)になりがちなプロセスに決定論的な結果をもたらすことができます。 たとえば、開発者はシステムプロンプトや AGENTS.md ファイル(または同様の設定)を使ってコーディングガイドラインを適用しようとすることがよくあります。しかし、大規模言語モデル(LLM)の非決定論的な性質上、プロンプトベースのガイドラインには実行の保証がありません。まったく同一のプロンプトであっても異なる結果が生じることがあり、エージェントがプロンプトの一部を選択的に無視してしまうことすらあります。 プロンプトの代わりに Hooks を使うことで、特定のアクションを強制できます。たとえば、コード編集のたびにコードが常にクリーンであることを確認するため、静的解析ツール(いわゆるリンター)を必ず実行させたいとしましょう。プロンプトに「編集後は必ずリンターを実行すること」と書いても、検証を実行するかどうかはエージェント任せになってしまいます。エージェントがその編集を「軽微」だと勝手に判断した場合、このステップをスキップしてしまうかもしれません。しかし、代わりにフック(この例ではファイル編集ツールを対象とする `PostToolUse`)を作成すれば、コード編集後に静的解析ツールが実行されることを決定論的に担保できます。 こうしたライフサイクルイベントをインターセプトすることで、エージェントの挙動制御、メトリクス収集、ワークフローの安全性確保など、さまざまなパターンを実装できます。以下でその代表例を見ていきましょう。 ### エージェントを専用ツールへ誘導する Hooks は多彩なシナリオで役立ちますが、私のお気に入りのユースケースは、エージェントの周囲にガードレールを設置すること、言い換えれば「エージェントの自律性(agency)をあえて適度に制限する」ことです。 これは、`PreToolUse` フックと、ツールへのアクセスを拒否してコーディングエージェントに「ステアリングヒント(steering hint)」を返すスクリプトを組み合わせることで実装できます。このステアリングヒントには、代わりに実行させたい指示を含めます。たとえば、Go ソースファイルの読み取りにシェルコマンドを使わせたくない場合、ステアリングヒントは次のようになります:"Tool call blocked - run_command(cat): do not use 'cat' for reading *.go files, use 'smart_read' instead"。 ### 悪意あるプロンプトを検知・遮断する `PreInvocation` フックを使えば、入力されたプロンプトをインターセプトし、セキュリティヒューリスティクスや軽量な分類モデルに照らして評価できます。プロンプトがジェイルブレイク(脱獄)の試みのように見える場合、フックはリクエストを即座にブロックし、実行ループに到達する前にバックエンドシステムを保護できます。 ### 認証情報の漏洩を防ぐ 開発者が誤って env ファイルや認証情報を、コーディングエージェントが読み取るアクティブなファイルに貼り付けてしまうことがあります。ファイル読み取りを監視する `PostToolUse` フックや、LLM に送信されるペイロードをスキャンする `PreInvocation` フックは、信頼性の高い DLP(Data Loss Prevention:データ損失防止)ゲートとして機能します。高エントロピーな文字列や標準的な API 形式に一致する文字列を検出した場合、フックはシークレットを動的にマスキング(リダクト)するか、実行を中断して認証情報の安全性を保ちます。 ### メモリ管理の自動化 エージェントは通常、[Agent Platform Memory Bank](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/memory-bank) や [MemPalace](https://github.com/mempalace/mempalace) のような外部メモリシステムに接続されていない限りステートレスです。 エージェントに記憶機能を追加するアプローチの1つとして、記憶の保存と検索をツールとして登録する方法があります。しかしこれを行うと、エージェントがそれらのツールを明示的に呼び出す判断をするかどうかに依存してしまいます。 Hooks の仕組みを利用すれば、メモリの永続化と検索を自動化できます。たとえばセッション終了時(`Stop` フックを使用)や、一定のターン数経過後(ステップ数やモデル呼び出し回数を監視)に[メモリ生成](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/memory-bank/generate-memories#triggering-memory-generation)をトリガーできます。 逆に、セッション開始時やモデル呼び出し前(`PreInvocation` フックなど)にメモリを自動検索することも可能です。たとえば Agent Platform Memory Bank では、[スコープ](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/memory-bank/fetch-memories#retrieve-all)(ユーザー ID など)やクエリに基づく[類似度](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/memory-bank/fetch-memories#similarity-search)でメモリを取得できます。これは本質的に、メモリを活用した検索拡張生成(RAG)です。 ### テレメトリデータの収集 Hooks の仕組みは、テレメトリコレクターやロガーを配置する場所としても最適です。エージェントの内部動作に対する優れた可視性が得られます。個人的には、AI が世界を支配する前にエージェントとより良好な関係を築くべく(冗談です :))、自分がどれだけ悪態をついているかを自覚するための「罵倒カウンター(curse word counter)」フックを作ってみたいとずっと企んでいます。 ## Antigravity での Hooks 設定 エージェントエンジンごとにコールバックの用語は異なりますが、このセクションでは **Antigravity における Hooks の方言(仕様)** に焦点を当てます。 仕様の全容については、公式の [Antigravity Hooks ドキュメント](https://antigravity.google/docs/hooks) を参照してください。 Antigravity は、ワークスペース直下の `.agents/` ディレクトリ内にある `hooks.json`(またはホームディレクトリにあるグローバル設定 `~/.gemini/config/hooks.json`)を読み込みます。 先ほど説明したステアリングヒントと静的解析を実装する設定例を以下に示します: ```json { "linter-safety-gate": { "PostToolUse": [ { "matcher": "write_to_file|replace_file_content|multi_replace_file_content", "hooks": [ { "type": "command", "command": "./scripts/run-linter.sh", "timeout": 15 } ] } ] }, "restrict-cat-on-go": { "PreToolUse": [ { "matcher": "run_command", "hooks": [ { "command": "./scripts/steer-go-reads.py" } ] } ] } } ``` これらのフックへの入力は `stdin` 経由で JSON オブジェクトとして渡されます。ここにはツール引数(`toolCall.args`)、アクティブなワークスペースパス(`workspacePaths`)、現在のセッションログのファイルパス(`transcriptPath`)などのコンテキストが含まれます。スクリプト側でこれらを評価・計算し、Antigravity に対して `"allow"`(許可)、`"deny"`(拒否)、あるいは `"ask"`(ユーザーへの確認要求)を指示する JSON レスポンスを `stdout` に出力します。 たとえば、渡されたペイロードを解析してエージェントを誘導するシンプルな Python スクリプト(`steer-go-reads.py`)は、次のように記述できます: ```python import sys import json def main(): # Read and parse the incoming trigger event payload from stdin try: payload = json.load(sys.stdin) except Exception as e: # Standard safety gate fallback print(json.dumps({ "decision": "deny", "reason": f"Failed to parse stdin payload: {e}" })) return tool_call = payload.get("toolCall", {}) tool_name = tool_call.get("name") tool_args = tool_call.get("args", {}) # Match the specific tool and check arguments if tool_name == "run_command": command_line = tool_args.get("CommandLine", "") # Detect if command attempts to cat any Go source files if "cat" in command_line and ".go" in command_line: response = { "decision": "deny", "reason": "Tool call blocked - run_command(cat): do not use 'cat' for reading *.go files, use 'smart_read' instead." } print(json.dumps(response)) return # Default to allow if no rules are violated print(json.dumps({ "decision": "allow" })) if __name__ == "__main__": main() ``` ## コントロールを取り戻す エージェントはますますスマートで高速になり、私たちがかつてないスピードでコードを生成できるよう支援してくれます。しかし、制御を欠いたスピードは大惨事を招く元凶でしかありません。私は登壇時によくこんな比喩を使います。スピードが出る車が好きなら、何よりも気にかけるべきはエンジンではなくブレーキです。もしブレーキがエンジンよりも非力なら、車を止めることができず、安全性が損なわれます。 コーディングエージェントにもまったく同じ考え方を適用すべきです。コードを高速に書きたいのであれば、品質を犠牲にしてバグを混入させないための強力な制御システムが欠かせません。それを怠れば、遅かれ早かれアプリケーションが深刻なトラブルに見舞われることになります。 Hooks は、AI の自律性と堅牢なソフトウェアエンジニアリングとのギャップを埋め、私たちが再び主導権を握るためのガードレールを構築するのに最適な仕組みです。最近 [Joe Bertolami 氏の記事](https://venturebeat.com/technology/agentic-ai-solved-coding-and-exposed-every-other-problem-in-software-engineering) で読んだ通り、「コードを書くこと自体がボトルネックだったことなど一度もない」のです。私たちが何十年もかけて培ってきたエンジニアリングのベストプラクティスを決して忘れず、エージェントに適材適所のツールを備え付けることで、現代のエージェンティック・コーディングの体験を心から楽しみましょう。 --- # Antigravity 2.0 への銀河ヒッチハイク・ガイド - **Markdown URL**: https://danicat.dev/ja/posts/the-hitchhikers-guide-to-antigravity-2-0/index.md - **HTML URL**: https://danicat.dev/ja/posts/the-hitchhikers-guide-to-antigravity-2-0/ - **Date**: 2026-05-21 - **Categories**: Agentic Coding - **Tags**: antigravity, cli, google-cloud, python, sdk - **Summary**: Google I/O 2026 で発表された Antigravity 2.0 の完全解説。Agent Manager 特化デスクトップ、Go製 agy CLI、Python SDK によるプログラム制御まで網羅。 [Google I/O 2026](https://blog.google/innovation-and-ai/technology/developers-tools/google-io-2026-developer-highlights/) が幕を閉じた今、発表された数々の新リリースを振り返り、それらが現在および近い将来の開発ワークフローにどう影響するのかを整理してみましょう。多くの魅力的な発表がありましたが、今回は開発者に最も大きなインパクトを与える [Antigravity 2.0](https://antigravity.google/blog/introducing-google-antigravity-2-0) のリリースと、[Antigravity CLI](https://antigravity.google/blog/introducing-google-antigravity-cli) や [Antigravity SDK](https://antigravity.google/blog/introducing-google-antigravity-sdk) を含む拡張された Antigravity(agy)エコシステム([Google I/O 2026 Antigravity ハイライト](https://antigravity.google/blog/google-io-2026) を参照)にフォーカスします。 技術的な詳細に入る前に、今回のローンチに関してネット上で多くの騒ぎ(残念ながら好意的なものばかりではありません)が起きていることに触れておく必要があります。その最大の理由は、Antigravity 2.0 がメインの Antigravity デスクトップアプリから IDE 環境を切り離したことをはじめ、開発フローの多くの面で破壊的変更(breaking changes)を導入したためです。 さらに、Antigravity CLI への移行に伴う [Gemini CLI の非推奨化のアナウンス](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/) も、移行に与えられた猶予期間の短さ(加えて後述するいくつかの癖)が原因で、コミュニティではあまり歓迎されませんでした。基本的にユーザーには 2026 年 6 月 18 日までの移行期限が与えられており、I/O から実質 1 か月しかありません。率直に言って、これは決して十分な期間とは言えません。 これについては以前にも書きましたが、愛用していたプロダクトが非推奨・終了になる悔しさは痛いほどよくわかります。私自身、Gmail との争いに敗れ、すっかり忘れ去られてしまったメールクライアントである Google Inbox を今でも惜しんでいます。綺麗事を言うつもりはありません。Google には優れたプロダクトを終了させてきた評判(悪名)が確かにあります。しかし、個人的な好みを別にして大局的に見れば、自らのプロダクトをこれほど大胆に幕引きできる Google の姿勢には、むしろ感銘を受けています。 多くの人は Google に対して、テクノロジーに関連するあらゆる領域で創造的破壊(ディスラプション)を牽引することを期待していると思います。特に AI の進歩によって環境がめまぐるしく変化する今日においては、ある方向から別の方向へと舵を切る(ピボットする)のには多大な勇気と決断力が必要です。私は普段からアジャイルについて多くを語っていますが、Google は形式ばったアジャイル手法とは結びつけられにくいものの、経験豊かなアジリストなら誰もが組織にとって最も価値ある特質の一つとして認める特徴を備えています。それは、素早く進路を修正し、ピボットし、実験し、失敗から学び、反復(イテレーション)を繰り返す能力です。 現状維持(コンフォートゾーン)に甘んじることなく、自らを再発明し続ける力こそが、Google を常に最前線に立たせている理由です。すべての実験が成功するわけではなく、むしろ多くの実験が失敗すること自体が織り込み済みです。そうして何が機能し、何が機能しないかを学んでいきます。得られた教訓を胸に次の目標へと進み、より新しいプロダクトへと反映させていくのです。 今回のリリースからも多くの教訓が得られるはずですが、最終的にテクノロジーそのものを見つめれば、目指しているゴール(エンドゲーム)は自ずと明らかになるでしょう。より高度なプロダクトを構築するためにリソースを集約しつつ、私たちは本格的なエージェンティック(自律型エージェント)時代へと舵を切っているのです。 ## 新しい Antigravity デスクトップアプリの解説 デスクトップアプリにおける最大の変更点は、IDE コンポーネントの削除です。Antigravity 1.x ではアプリが VS Code のフォークをベースにしていたため、コードのナビゲーションや編集を行うおなじみの IDE 機能と、エージェントと対話するためのアシスタントボックスが一体となっていました。 それだけでなく、「Agent Manager」と呼ばれるセカンダリ UI も用意されており、複数のチャットセッション(いわゆる「conversations」)を高い視点から俯瞰できました。このビューでエージェントを監視し、入力待ちになったときに対応することで、多数のプロジェクトを並行して進めることができたのです。 新しいデスクトップアプリの最大の変化は、Antigravity 2.0 がこの Agent Manager 体験をメインに据え、IDE 部分を完全に切り離した点です(IDE 部分は独立したオプションのアプリとなりました)。 ![新しい Agent Manager のインターフェース](image.png "プロジェクトと会話に特化した、クリーンな新しい Agent Manager インターフェース") ベテラン開発者にとって、これは非常に大きな摩擦(フリクション)となりました。長年頼りにしてきた使い慣れたエディタツールが、突如として消え去ってしまったからです。agy 2.0 の UI 上でもファイルを見ることはできますが、それは agy が現在作業しているファイルに限られ、直接編集することはできません。すべての操作は、プロンプトまたはファイルへのアノテーション(注釈)を通じて行われます。 ![agy 2.0 のファイルビュー](image-2.png "UI 上でファイルを確認できますが、直接編集することはできません") エージェントとの対話フロー自体は、過去 1 年間にエージェントとともにコーディングしてきた人ならすでに馴染みのあるものでしょう。プロンプトを与えると実装計画(implementation plan)が作成され、インラインコメントやトップレベルのプロンプトを使ってそれをレビューできます。承認されると、エージェントは自律して実行に移ります。UI の設定にもよりますが、時折許可を求めて処理を一時停止することがあり、許可を与えることも、任意の進路修正を添えて却下することもできます。 ![ユーザー入力を求める Agent Manager](image-1.png "リクエストを却下する際に、指示を与えるステアリングコメントを追加できます") 拡張性の面では、agy 2.0 は MCP や Skills といったこの 1 年で広く親しまれるようになった共通標準規格をサポートしているほか、1.x から引き継がれた独自の「Rules」メカニズム(本質的にはコンポーザブルな AGENTS.md)や、旧 Gemini CLI の拡張機能をベースにした新しいプラグインシステムも備えています。プラグインを使用すると、追加ルール、スラッシュコマンド、MCP サーバー、スキル、サブエージェントをまとめてパッケージ化でき、Gemini CLI の拡張機能との後方互換性も維持されています(つまり、CLI 拡張機能を agy にインストールできますが、その逆はできません)。 全体として、IDE の統合を惜しむ人たちのフラストレーションは理解できるものの、私個人の第一印象としては、**同じ**アプリ内に IDE が統合されていなくても全く困りませんでした。Gemini CLI を使っていた頃から、手動で編集したいときのために常に VS Code を並行して立ち上げていましたし、agy 2.0 でも全く同じワークフローを適用しています。実際、最近の私は VS Code をほぼテキストエディタとして使っており、本格的な IDE 機能は滅多に使いません。メモ帳に変えても大して変わらないくらいですが、いくつかのショートカットがマッスルメモリーとして染み付いていることだけが、今でも VS Code を使い続けている唯一の理由です。 正直に言って、agy 1.x や他のコーディングエージェントと比較して agy 2.0 に何か画期的な変化があるわけではありませんが、よりすっきりとした外観はとても気に入っていますし、自作プラグインによるカスタマイズを始めてこそ真価を発揮するだろうと考えています。現在、[GoDoctor](https://danicat.dev/ja/posts/20250729-how-to-build-an-mcp-server-with-gemini-cli-and-go/) と [Speedgrapher](https://danicat.dev/ja/posts/20250805-introducing-speedgrapher/) を Gemini CLI 拡張機能から agy プラグインへとアップグレードする作業を進めており、何かお見せできるものができ次第、報告したいと思います。 ## Antigravity CLI ターミナルユーザー向けには、コマンドライン体験が新しい [**Antigravity CLI**](https://antigravity.google/blog/introducing-google-antigravity-cli)(通称 `agy CLI`)として再構築されました。最初は少し戸惑うかもしれませんが、CLI だけを使用する予定であっても、認証プロセスを共有しているため agy 2.0 アプリをインストールしておく必要があります。agy CLI は Gemini CLI の正当な後継であり、100% の機能一致とまではいかないものの、主要な機能はすでにしっかりと揃っています。具体的には、[hooks](https://danicat.dev/ja/posts/20260610-mastering-hooks/)、[skills](https://danicat.dev/ja/posts/20260829-the-pragmatic-guide-to-agent-skills/)、[MCP](https://danicat.dev/ja/posts/20250817-hello-mcp-world/)、[サブエージェント](https://danicat.dev/ja/posts/20260722-the-rise-of-the-subagents/)、そしてプラグインです。 CLI 全体が Go で書き直されたこと(Gemini CLI は TypeScript 製でした)は、よりキビキビとした動作が期待できるため、私を大いに喜ばせてくれました。一方で、最大の批判点の一つは、現時点で agy CLI がクローズドソースであることです。これはオープンだった Gemini CLI からの後退のように感じられるかもしれません。少し前までは Gemini CLI のコードが一般に「流出」したことについて冗談を言い合っていましたが、今や私たちのメインのコーディングエージェント自体がクローズドソースになってしまったため、残念ながらこのジョークは笑えないものになってしまいました。 私自身にはどうすることもできないため、気に病まないことに決めました。これが良い決断か悪い決断かを判断するのは時期尚早ですが、以前 Gemini CLI に貢献した方々をはじめ、コミュニティの悔しさは痛いほどよくわかります。せめてもの救いは、プラグインシステムを中心に活発なオープンソースコミュニティが今後も発展していくであろう点です。少なくとも私自身は、頼もしい Go エキスパート・サブエージェントと vibe-writing コンパニオンを近いうちに皆さんにお届けできるよう、自分のプラグイン開発に取り組んでいます。 ![agy CLI のインターフェース](image-3.png "Gemini CLI や Claude Code を使ってきた人なら違和感なく馴染める UI") UI については、これまでに CLI コーディングエージェントを使ったことがある人なら驚くようなことはないでしょう。第一印象として、ターミナルの描画は Gemini CLI の TypeScript による描画よりも確かに優れていると感じられます。また、agy 2.0 と同様に、すっきりとした外観も非常に好印象です。個人的には、Gemini CLI は機能が増えすぎて UI も肥大化しすぎていたと感じていたので、このクリーンなインターフェースはとても新鮮に感じられます。「Less is more(少ないことは、より豊かなこと)」は私の大好きな言葉の一つですが、agy CLI はこの点において期待に応えてくれています。 (今のところ)あまりうまく機能していない点としては、主に拡張機能との互換性が挙げられます。移行パスは用意されているものの、必ずしも期待通りに動作するとは限らず、私が今週のほとんどの時間を費やして godoctor と speedgrapher の書き直しに専念してきたのも、自動移行に頼りたくなかったからです。それに加えて、プロジェクトベースの認証にも問題が発生しており、これについては近いうちに修正されることを期待しています。現在のところ、プロジェクトベースの認証が動作しなかったため、個人の Google Pro サブスクリプションで使用しています。 Gemini CLI からの移行ユーザーにとって頭の痛いもう一つの問題である請求周りの複雑さには立ち入りませんが、私個人の見解としては、agy CLI にはいくつかの課題があるものの、素晴らしい可能性も秘めています。今のところ革新的な部分は多くありませんが(変更の大部分は内部で行われているため)、致命的な欠点(ディールブレイカー)も見当たりません。Gemini CLI で行っていたことはすべて agy CLI でも可能であり、新たに学ぶべきこともごくわずかです。そのため、仮に Gemini CLI の移行猶予期間がもっと長かったとしても、ワークフローの将来性を確保するために、できるだけ早く移行することをおすすめします。 ## Antigravity SDK ここまでの議論は古いプロダクトを新しいものに置き換える話が中心であり、革新的というよりは漸進的なアップデートのように感じられたかもしれません。だからこそ、[**Antigravity SDK**](https://antigravity.google/blog/introducing-google-antigravity-sdk) のリリースは私にとって最も刺激的な発表でした。変更の大部分が内部で行われていると述べましたが、それこそがエージェントを支援するこの統合プラットフォームの構築であり、Antigravity SDK は開発者である皆さんがその基盤にアクセスするための手段なのです。 以下は、わずか 15 行足らずのコードでワークスペースを問い合わせるエージェントの実装例です。 ```python import asyncio from google.antigravity import Agent, LocalAgentConfig async def main(): config = LocalAgentConfig() async with Agent(config) as agent: response = await agent.chat("What files are in the current directory?") print(await response.text()) if __name__ == "__main__": asyncio.run(main()) ``` この [Python](https://xkcd.com/353/ "import antigravity") ライブラリにより、開発者はまったく同じエージェント向けランタイムとオーケストレーション・ハーネスにプログラムからアクセスできるようになります。この SDK はランタイム非依存であり、15 行足らずのコードでステートフルなエージェントループを立ち上げることが可能です。組み込みツール、カスタム関数、Model Context Protocol サーバー、サブエージェント、再利用可能なスキルなど、モジュール化された機能を単一のパイプラインの下でサポートします。 ## はじめに Antigravity に関連するすべてのリリースに共通するトレンドは、「コードファースト」から「デザインファースト」へのシフトです。ソフトウェア開発体験全体が、コードを編集することからエージェントを調整・協調させることを中心に再設計されています。この変化に向けて開発環境を準備するために、次のアクションを検討してみてください。 1. **デスクトップアプリをダウンロードする**: [antigravity.google](https://antigravity.google) にアクセスしてデスクトップアプリケーションをインストールします。 2. **ターミナルのワークフローを移行する**: `agy` CLI をインストールし、インポートコマンドを実行して、**2026 年 6 月 18 日**の非推奨期限までに Gemini CLI の設定を移行します(詳細は [移行アナウンス](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/) を参照)。 3. **SDK を試してみる**: Python ライブラリをインストールし、[Antigravity の機能](https://antigravity.google/docs/features) をチェックして、agy SDK を使用したカスタムエージェントの構築を始めましょう。 ```bash pip install google-antigravity ``` ## 参考リソース 今回のリリースについてさらに詳しく知り、追加の技術情報を確認するには、以下のリソースをチェックしてください。 * **[Introducing Google Antigravity 2.0](https://antigravity.google/blog/introducing-google-antigravity-2-0)**: 2.0 エコシステムの公式アナウンス。 * **[Introducing Google Antigravity CLI](https://antigravity.google/blog/introducing-google-antigravity-cli)**: Go 製の新しいターミナルインターフェースに関する詳細解説。 * **[An Important Update: Transitioning Gemini CLI to Antigravity CLI](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/)**: Gemini CLI ユーザー向けの詳細な移行スケジュールとガイドライン。 * **[Introducing Google Antigravity SDK](https://antigravity.google/blog/introducing-google-antigravity-sdk)**: Python でエージェントをプログラムからオーケストレーションする方法。 * **[Google I/O 2026 Developer Highlights](https://blog.google/innovation-and-ai/technology/developers-tools/google-io-2026-developer-highlights/)**: 今年の Google I/O における主要な開発者向け発表。 * **[Google I/O 2026: Antigravity Announcement](https://antigravity.google/blog/google-io-2026)**: Google I/O における Antigravity の重要な最新情報とハイライト。 * **[Google Antigravity Documentation & Features](https://antigravity.google/docs/features)**: Antigravity の機能と安全性コントロールに関する包括的ガイド。 --- # アジャイルからエージェンティックへ:現代のエンタープライズ開発ガイド - **Markdown URL**: https://danicat.dev/ja/posts/from-agile-to-agentic/index.md - **HTML URL**: https://danicat.dev/ja/posts/from-agile-to-agentic/ - **Date**: 2026-05-15 - **Categories**: Agentic Coding - **Tags**: agile, gemini-cli, mcp, software-engineering - **Summary**: アジャイル開発プラクティスをエージェンティックなワークフローへ昇華させる実践ガイド。プロンプトとしてのストーリー作成、MCP による対話型アーキテクチャ、非コーディングエージェントの活用を詳解。 過去10年ほどの間にソフトウェア業界で過ごした経験がある方なら、おそらく一度は「アジャイル変革(アジャイルトランスフォーメーション)」を経験したことがあるでしょう。終わりの見えないスプリントプランニングに同席し、デイリースタンドアップで進捗を共有しながら、「これらに本当に意味があるのだろうか?」と疑問を抱いたことさえあるかもしれません。 こうしたフラストレーションは珍しいものではなく、大抵はプロダクトマネジメントとエンジニアリングの現場における現実の乖離から生じています。ビジネスリーダーは予測可能性を求めますが、ソフトウェア開発は本質的に予測不可能な営みです。厳格なメトリクスやダッシュボードによって予測可能性を無理に押し付けようとすると、アジャイルは失敗します。方法論が硬直した官僚主義へと化してしまうのです。各種セレモニーは無駄なオーバーヘッドに感じられ、ユーザーストーリーの作成といったプラクティスも退屈なルーチンワークへと成り下がってしまうため、開発者には不満が募ります。 一方で、成熟したアジャイル組織では、リーダーシップ層が不確実性を受け入れ、ビジネスにとって最良の選択を下せるようチームに権限を委譲(エンパワー)します。メトリクスやダッシュボードを追うことが主目的ではなくなり、代わりにビジネス価値の提供へと焦点がシフトするのです。これはすべてを開発者に丸投げするという意味ではなく、真のクロスファンクショナルチームとして開発者と緊密に協働していくことを意味します。 この記事を正しく読み解いていただくためにも、この前置きは欠かせません。まずはアジャイルマニフェストの原点に立ち返っていただく必要があります。その[中核となる価値観(コアバリュー)](https://agilemanifesto.org/)を思い出してください: > プロセスやツールよりも個人と対話を > 包括的なドキュメントよりも動くソフトウェアを > 契約交渉よりも顧客との協調を > 計画に従うことよりも変化への対応を この記事では、こうしたアジャイルの基盤となるプラクティスを、エージェンティックな現代のワークフローへとどのように橋渡しできるかを探求していきます。 ## エージェンティック開発のワークフロー エンタープライズ規模でのスケーリングを議論する前に、まず共通のベースラインを整理しておきましょう。これらは過去の記事でも触れてきたテーマですが、全体の文脈を揃えるためにおさらいしておきます: 1. **ストーリー作成はプロンプティングそのもの(Story writing is prompting):** エージェントを指揮する上で最も難しいのは、適切なコンテキストを与えることです。多くの開発者は質の高いストーリーを書くのに苦労しがちですが、これこそが現代において磨くべき最も重要なスキルです。優れたストーリーを書くスキルは、優れたプロンプトを書くスキルと完全に一致します。明確なビジネス上の妥当性と期待される成果を定義し、エージェントが消化しやすい形で情報を整理する——アジャイルの用語でいう「**Definition of Ready(DoR:準備完了の定義)**」に他なりません。 2. **優先順位付けがワークフローを決定づける(Prioritisation dictates the workflow):** 従来のバックログリファインメントは、AI ツールの使い分けに直結します。「*ビジネス価値(Business Value)*」と「*技術的確実性(Technical Certainty)*」の2軸でタスクに**優先順位を付ける**ことで、フォアグラウンドで同期的に進める作業([Gemini CLI](https://geminicli.com/) などとのペアプログラミング)と、バックグラウンドの非同期エージェント([Jules](https://danicat.dev/ja/posts/20250521-jules/) など)へ委任すべき作業を判断できます。 3. **エージェンティック・コーディングサイクル(The agentic coding cycle):** コーディングエージェントは極めて強力ですが、再現性や一貫性に欠けることが少なくありません。本質的に非決定的(Non-deterministic)だからです。この問題は、[決定論的なエンジニアリングプラクティス](https://danicat.dev/ja/posts/20251206-taming-vibe-coding/)を取り入れることで緩和できます。私はこれをよく「エージェントの自由度(Agency)を意図的に下げる」と表現しています。例えば、ビルドプロセスが常に「ビルド → テスト → リント → デプロイ」という手順であるなら、それをプロンプトで逐一指示すべきではありません。セッションが長引くにつれて、エージェントはいずれかのステップを確実に忘れてしまうからです。真の解決策は、そのプロセスを一連のカスタムツールとしてパッケージ化し、エージェントに渡すことです。そうすれば、途中のステップを勝手に省く余地を完全に排除できます。 この3つのプラクティスを押さえていれば、個人としてはすでに効率的なエージェンティック開発者と言えます。しかし、エンジニアリング組織全体を効果的に機能させるにはどうすればよいのでしょうか? ## 対話型アーキテクチャと組織知の共有 エンタープライズソフトウェアの開発は困難を極めますが、特定のコードが*なぜ*そのように書かれたのかという背景を後から思い出すのも同じくらい骨の折れる作業です。チーム内の属人的な知識(Tribal knowledge)はあっという間に風化してしまいます。シニアエンジニアが退職すれば、その組織知も一緒に失われてしまうのが常です。この問題に対する従来の解決策は社内 Wiki に網羅的なドキュメントを残すことでしたが、Wiki はメンテナンスが滞りがちで、検索性も低く、ルールとして徹底させるのも困難です。 長年、組織的な知識共有について私のお気に入りの記事の一つに、Martin Fowler のブログに掲載された [Scaling Architecture Conversationally](https://martinfowler.com/articles/scaling-architecture-conversationally.html) があります。著者らは、優れたアーキテクチャとはトップダウンの指示によってではなく、対話を通じて浸透していくものだと主張しています。また、そうした対話を ADR(Architecture Decision Records:アーキテクチャ決定記録)として形式知化し、時間の経過とともに風化させないためのアプローチについても論じています。 ADR は単なる Wiki 以上の価値を持ちます。意思決定がなされた特定の時点における歴史的スナップショットを提供するからです。その決定を正当化するに至った具体的な前提条件、仮説、制約事項が克明に記録されます。一見シンプルな概念ですが、これは将来のチームが必要に応じて変更を加えるための大きな力になります。最初の選択が*なぜ*行われたのかが記録されているため、当時の決定が現在も妥当であるかを客観的に評価でき、制約条件が変化した際にも(新たな ADR を発行することで)自信を持って過去の決定を更新・上書きできるのです。 エンジニアとしてのキャリアを重ねるにつれ、この仕事の最も重要な部分は「不確実性のマネジメント」にあると強く実感するようになりました。ADR は、自分たちが何を理解していて、何をまだ理解していないのかを率直に見つめ直すためのツールの1つです。「最初からすべてを知っている必要はない」と早く気付くほど、状況は好転します。これこそがアジャイルの本質です。前に進むために必要十分な情報だけを把握し、学びを積み重ねて不確実性を減らし、イテレーションを回していく。ソフトウェアは生き物であり、永遠に「完成」することはありません。 ADR は非構造化な Wiki に対して多くの利点を持ちますが、依然として大きな弱点を抱えています。それは「人間が ADR の存在を認知し、遵守すること」に依存している点です。特に大規模組織では、コミュニケーションそのものがボトルネックになります。情報のサイロ化が広がり、組織の各部門間で足並みを揃えるためだけに膨大な労力が費やされてしまうのです。 ### エージェント経由で組織知を配信する 現代においてアーキテクチャをスケールさせるには、この組織知を直接エージェントへと注入しなければなりません。人間を介した公式の周知アナウンスだけに頼るのではなく、社内規程や ADR、統制手順、コーポレートスタンダードをテクノロジーによって直接エージェントへと配信・適用できるのです。組織固有のコンテキストがエージェントのコンテキストウィンドウ内に常駐していれば、組織のプラクティスが常に最新の状態で確実に適用されます。 アーキテクチャの視点から見ると、MCP サーバーはこの種の情報を公開・配信するための理想的な媒体です。アーキテクチャ委員会やセキュリティ委員会などの意思決定機関が新たな指針を出すたびに、中央で一元管理・更新できます。プロンプトやツール、Agent Skills はエージェントの振る舞いを制御する効果的な手段であり、エンジニアが操作するコーディングエージェントだけでなく、CI/CD パイプライン内で稼働する自動化エージェントからも共通して利用できます。 Agent Skills がまだ MCP 仕様の標準に含まれていないのは過渡期の課題ですが、現在専用のワーキンググループで標準化が進められています。MCP サーバーを介してスキルをエージェントへ直接動的配信できるようになれば、スキルを常に最新に保つ運用の課題が解消され、開発者に新たな規約を周知・浸透させる摩擦は劇的に低減されるはずです。 ### プロダクトドキュメントも「消費されるプロダクト」である 社内規程にとどまらず、まったく同じ仕組みがプロダクトのドキュメントにも当てはまります。従来の開発では、チームAが社内 API を構築した場合、開発者ポータルに OpenAPI 仕様書を公開し、チームBがそのマニュアルを読むことを期待していました。しかしエージェンティックの時代において、静的なドキュメントは摩擦(フリクション)を生む原因にしかなりません。自チームのプロダクトが他チームに使われることを想定しているなら、そのドキュメントも相手の開発ツールが直接利用(コンシューム)できる形式であるべきです。 チームAがサービスをリリースする際には、API スキーマ、インテグレーション例、コンプライアンスチェック機能をツール群として公開する専用の MCP サーバーも併せて提供すべきです。チームBの開発者がそのサービスと連携したい場合、自身のコーディングエージェントをチームAの MCP サーバーに接続するだけで済みます。エージェントが自律的に API 構造を問い合わせ、連携ルールを読み解き、クライアントコードを自動生成してくれるのです。「人間がマニュアルを読む」世界から「エージェントが API を解釈する」世界へと移行することで、アーキテクチャの設計意図や連携パターンを組織全体で齟齬なく正確に保つことができます。 ## セレモニーの自動化:非コーディングエージェントの活用 コーディングエージェントの話題ばかりに時間を費やしがちですが、多くのアジャイル導入現場を悩ませているマネジメントオーバーヘッドを削減する上では、非コーディングエージェント(Non-coding agents)を活用した最適化の余地が数多く存在します。 会議の議事録作成や要約といった手近なタスクから、バックログの優先順位の再設定、ユーザーストーリーのリファインメント、スパイク(調査タスク)のチケット起票に至るまで、非コーディングエージェントを導入することで、管理業務に奪われていた多くの労力をエンジニアリング本来の価値創造へと取り戻すことができます。 こうしたプロセス指向のエージェントがチームを強化する具体例をいくつか挙げます: * **ストーリーのリファインメントと細分化(Story Refinement & Breakdown):** プロダクトオーナーが大まかなエピックを作成すると、エージェントがそれをレビューし、考慮漏れのエッジケースや暗黙の技術的前提、未処理のエラーパスなどを洗い出します。特定のスキルへのアクセスを付与しておけば、組織標準に自動的に準拠させることができます。不確実な論点があれば、さらなる技術調査用の「スパイク(Spike)チケット」として自動起票することも可能です。 * **DoR(準備完了の定義)と DoD(完了の定義)の監査:** 多くのアジャイル現場において、DoR や DoD は形骸化した Wiki のチェックリストに過ぎず、忘れ去られがちです。エージェントを既存のカンバンボード(Jira や GitHub Projects など)と連携させれば、ルールの遵守をプロアクティブに担保できます。チケットが「Ready for Dev(開発準備完了)」に移動した際、バックグラウンドのエージェントが内容をスキャンし、API スキーマや UI モックアップなどの必要なコンテキストが実際に添付されているかを確認します。不備があればステータス遷移にフラグを立てて警告します。同様に、チケットをクローズする前にも、テストコードの追加やドキュメントの更新が完了しているかをエージェントが検証します。 * **データ駆動のレトロスペクティブ(振り返り):** スプリントの振り返りは、直近の出来事だけに引きずられる「利用可能性バイアス(Recency bias)」に陥りがちです。非コーディングエージェントは、スプリント期間中のチケット遷移履歴、プルリクエストのレビューコメント、チャットのスレッドを客観的にレビューし、データアナリストとして機能します。例えば「特定のマイクロサービスを変更したチケットは、PR レビューで平均4日間滞留している」といった事実を提示し、チーム内でドメイン知識のサイロ化が発生していないか見直すきっかけを与えてくれます。 ## エージェントマネージャーによるワークフローのスケーリング ここ1年ほど、業界は主に単一エージェントの体験、とりわけコーディングエージェントの洗練に注力してきました。その過程で MCP、skills、hooks といった新たな標準規格が次々と登場し、定着しつつあります。 その結果、ソフトウェアエンジニアのキャリアにおけるコアスキルは「コードを書くこと」から「エージェントをオーケストレーションすること」へとシフトしました。しかし、ここには見落とされがちなボトルネックが存在します。それは「人間の認知負荷(Cognitive load)」です。[AI の利用によって引き起こされる新たなバーンアウト(燃え尽き症候群)](https://techcrunch.com/2026/02/09/the-first-signs-of-burnout-are-coming-from-the-people-who-embrace-ai-the-most/)に関する報告もすでに出始めています。 非同期エージェントにタスクを委任するのは魅力的に思えますが、バックグラウンドで走るタスクは確実にメンタル帯域(脳の処理リソース)を消費します。タスクが進行中であることを意識し続け、完了したら出力をレビューし、そのコンテキストを自分のメインワークフローへと再統合しなければなりません。特に無関係な複数タスクを並行処理している場合、完全なコンテキストスイッチが発生するため、負担はさらに跳ね上がります。AI と同様に、人間側もまた「コンテキストの問題」に苦しんでいるというのは皮肉な話です。 しかし、[ソフトウェアエンジニアリングの基本定理](https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering)が述べているように: > コンピュータサイエンスにおけるあらゆる問題は、もう一層の間接化(Indirection)を導入することで解決できる。……間接化の層が多すぎるという問題を除いては。 今年、私たちは「エージェントマネージャー(Agent Managers)」、つまり他のエージェントを管理・統括するエージェントの台頭を目撃しています。この概念は最初コーディング分野(例えば [Antigravity 2.0](https://danicat.dev/ja/posts/20260521-the-hitchhikers-guide-to-antigravity-2-0/) や [`scion`](https://github.com/googlecloudplatform/scion) など)で見られましたが、より広範な意味合いを持っています。 しかし、ここでさらに大きな課題が生まれます。[1つのエージェントの仕事をレビューする](https://danicat.dev/ja/posts/20260303-code-reviews-in-2026/)だけでも、あるいは数体であっても難しいのに、エージェント群(フリート)全体の仕事を一体どうやってレビューすればよいのでしょうか? 簡単な答えはありませんが、私が考えるに、私たちはマルチエージェントシステムに対する信頼を着実に構築していくしかありません。あるいはセキュリティの格言にあるように、「信頼せよ、されど検証せよ(Trust, but verify)」です。 プロンプティング手法の工夫、hooks、サンドボックス、決定論的ツールの導入によって単一エージェントへの信頼性を高めてきたのと同様に、エージェントマネージャーに対しても適切な「品質ゲート(Quality gates)」を組み込む手法を確立していく必要があります。エージェントの評価(Evaluation)、監査性(Auditability)、そしてより成熟したエンジニアリングプラクティスが、この変革を支える基本となるでしょう。 それでも、私たちは必ずこの壁を乗り越えられると信じています。私たちがコンパイラから出力されたアセンブリコードを一行ずつ目視確認することなく安心して実行できるのは、何十年にもわたるエンジニアリングプラクティスの積み重ねがあるからです。エージェントマネジメントの世界も、まったく同じ道を歩むことになるはずです。 ## 未来の展望:エージェンティック・カンバン コーディングエージェントから「コード」を取り去り、私たちが構築している「プロダクト」そのものに焦点を当てたらどうなるでしょうか? 私がこの思考実験を行ったところ、現在私たちが使っているカンバンボードとそう大きく変わらない形に行き着くことに気づきました。ただし、人間がチケットをピックアップするのではなく、エージェント同士が主に対話を繰り広げながら進めていくのです: ![エージェンティック・カンバンのモックアップ](image.png "Google Stitch で作成したエージェンティック・カンバンのモックアップ") ここには、開発の各ステージ(backlog、to do、in progress など)を表すおなじみのカラムが並んでいますが、それぞれのカラムにはチケットを次のステージへと進めるために協働するエージェント群が配置されています。カラムにグローバルスキルを追加することで、アーキテクチャ標準や統制手順といった重要なコンテキストを、関与するすべてのエージェントに共有できます。チケットをクリックしてエージェント間の会話フローを追うことで、すべてのステップを監査可能です。エージェントの方向性を修正したい場合は、チケットにコメントを追加します。人間のレビュープロセスを挟みたいなら、自分自身を「エージェント」の一員として組み込めばよいのです。 アジャイルの視覚的マネジメントとエージェントマネージャーの実行力を組み合わせることで、認知負荷の限界を打破できます。アジャイルとエージェンティックの世界は、こうして見事に一周して結びつくのです。過去のセレモニーは未来のダッシュボードへと進化を遂げ、かつて終わりのないスプリントプランニングで私たちが学んだすべては、この先訪れる未来のための準備に過ぎなかったのだと証明されるでしょう。 このアプローチについてどう思われますか? ぜひ下のコメント欄や、各種 SNS で皆さんのご意見を聞かせてください。 --- # Gemini CLIで作るマッチ3アーケードゲーム - **Markdown URL**: https://danicat.dev/ja/posts/match3-game-gemini-cli/index.md - **HTML URL**: https://danicat.dev/ja/posts/match3-game-gemini-cli/ - **Date**: 2026-05-07 - **Categories**: Agentic Coding - **Tags**: codelab, gemini-cli, golang, subagents, vibe-coding - **Summary**: Gemini CLIとGo言語(Ebitengine)を活用した2Dマッチ3ゲーム開発チュートリアル。プランモードでの設計からサブエージェントによる視覚テスト自動化まで徹底解説。 > **注記:** この記事は Gemini CLI を対象に書かれたものですが、Gemini CLI は非推奨となり **Google Antigravity 2.0** へと移行しました。新しい Antigravity CLI(`agy`)、SDK、および最新エコシステムの詳細については、[Antigravity 2.0 への銀河ヒッチハイク・ガイド](https://danicat.dev/ja/posts/20260521-the-hitchhikers-guide-to-antigravity-2-0/) をご覧ください。 > **最新コードラボ:** Antigravity 向けにアップデートされた本チュートリアルの最新バージョンは [goo.gle/cloud-crush-agy](https://goo.gle/cloud-crush-agy) をご覧ください。 私がそもそもソフトウェア開発者になったきっかけは、子どもの頃にビデオゲームに夢中になったことでした。数え切れないほどの時間をゲームのプレイに費やし、それがどうやって作られているのかに強く惹かれていました。父はテレビやコンピューターの仕組みについて一生懸命説明してくれましたが、当時の私には全く理解できませんでした。 10代になってようやく我が家にもインターネットが開通し、少しずつ理解できるようになりました。同年代の若者たちがチャットルームに集まり、ICQでメッセージを送り合い、Orkutのプロフィール作りに熱中している間、私はゲーム開発のチュートリアルを読み漁っていました。あの頃は本当に懐かしく、良い時代でした。 年月が経ちましたが、結局プロのゲーム開発者になることはありませんでした。私のキャリアはデータベース、データエンジニアリング、バックエンドサービス、そしてクラウドへと進んでいきました。その選択を後悔してはいません。それでも時折、「自分の手でインディーゲームを作ったらどんな気分だろう?」と考えることがあります。 ですが、今はどうでしょう? エージェンティック・コーディング(agentic coding)の台頭によって、ゲームをはじめとする複雑なアプリケーションの開発は圧倒的に身近になり、もはや想像するだけで終わらせる必要はなくなりました。これからお見せするように、完全に動作し、クラウドへデプロイされたゲームを今すぐ作ることができるのです。 この記事には2つの楽しみ方があります。1つは、生成AIを試してみたいゲーム開発志望者としての視点。もう1つは、新しいエージェンティック・コーディングのスキルを楽しく身につける題材としてゲーム開発を活用する、プロの開発者としての視点です。どちらの立場であっても、本記事では Gemini CLI の2つの機能、**プランモード(plan mode)**と**サブエージェント(sub-agents)**を紹介します。ですがその前に、まずは技術選定について少し話しましょう。 ## プロジェクトに最適な技術を選定する方法 これはどのソフトウェアチームにとっても、常に重要な決断です。使い慣れたツールを使うべきか? 新しい市場トレンドに乗るべきか? それとも自分たちで内製すべきか? 大企業は通常、すでに熟知しているツールを使い続けます。ツールの変更を正当化するには、極めて説得力のある理由が必要です。その理由は、市場コストの変動や人材不足といった外部要因かもしれません。あるいは、新しい技術スタックに対応するためのチームの再教育コストの高さといった内部要因かもしれません。 エージェンティック・コーディングはこの力学を一変させました。AIがボイラープレート(定型コード)の作成を担ってくれるため、現在ではプログラミング言語の選定自体の重要度は下がり、システム全体のアーキテクチャこそが重要になっています。私たち開発者にとって、これは大きな解放です。新しい構文の習得に何ヶ月も費やすことなく、課題に合わせて技術スタックを柔軟に切り替えられるのです。 言語の相対的な重要性が下がるなら、一体何が残るのか?と疑問に思うかもしれません。私の答えは**「パターン」**です。ソフトウェアを単一のブラックボックス(サイロ)としてではなく、システム全体の集合体としてどう構造化するかという視点です。これはマクロレベル(システム設計)でもミクロレベル(プログラム設計)でも同様です。コードの1行1行が何をしているかをすべて把握している必要はありませんが、ソフトウェアの各構成要素がどう連携しているかを把握しておくことは**絶対に必要**ですし、エージェントを**正しい**実装の方向へ巧みに誘導する(ステアリングする)方法を知っておくことも**不可欠**です。 では、すべてをBASICで書いていた時代に戻れるということでしょうか? もちろん違います。言語の選択は、単なる言語仕様の選択にとどまらないからです。言語には固有の機能群と広大なエコシステムがついて回ります。私たちが目指す目的に対して、最もフィットする技術を選定しなければならないことに変わりはありません。唯一変わったのは、「そのチームがその言語をすらすら書けるかどうか」というハードルがもはや決定的な問題ではなくなった点です。チームに確かなソフトウェアエンジニアリングの基礎さえあれば、最新のコーディングエージェントがそのギャップを容易に埋めてくれます。 ある選定基準が過去のものとなる一方で、新たな基準が生まれます。今回の場合、着目すべきは「コーディングエージェントが対象言語で高品質なソフトウェアをどれだけ生成しやすいか」という点です。 今回のプロジェクトで私が Go を選んだのには、主に2つの理由があります。1つは、コーディングエージェントが極めて扱いやすい軽量な言語であること(自作の `godoctor` MCP サーバーも役立っています!)。もう1つは、ebitengine を中心とした成熟したオープンソースのゲーム開発エコシステムが存在することです。 Three.js で作ることもできたでしょうか? もちろんです。ですが、私はコンソールやアーケードゲームのプレイフィールに極力近づけたかったため、コンパイル言語によるゲームであることが必須でした。また、今回は2Dゲームのみを対象としているため、Unity や Unreal Engine のような大規模ゲームエンジンは不要です。さらに、ebitengine は Nintendo Switch のニンテンドーeショップで商用ゲームが配信されている実績もあり、「いつか自分のゲームをパブリッシュしたい」という夢を掻き立ててくれます(もちろん今回のゲームではありませんが)。 Go の強みについても少し触れておきましょう。コンパイル言語である Go は、開発プロセスのごく初期段階で多くのエラーを検知できます。Python でも同様のゲーム開発は可能ですが、インタープリタ言語であるためテストサイクルがもたつきます。さらに、Go はローカルマシン向けにネイティブコンパイルできるだけでなく、Web 向けに WebAssembly(WASM)へコンパイルすることも可能です。つまり、わずかな変更を加えるだけで、作ったゲームをそのまま Web サービスとしてデプロイできるのです。 ## ソフトウェアアナリストの帰還 エージェントが Go コードの記述や、サーバーおよび WASM バイナリのコンパイルといった重労働を一手に引き受けてくれるとしても、設計に関する私たちの責任が軽くなるわけではありません。 ソフトウェアエンジニアリングのあり方はシフトしています。私たちは小手先の構文に頭を悩ませる時間を減らし、よりハイレベルなパターンを考察することに多くの時間を費やすようになっています。 ある意味では、かつての「ソフトウェアアナリスト(システムアナリスト)」の時代に回帰しているようにも感じられます。1行ずつ手作業でコーディングする代わりに、人間の要求を精密な指示セットへと翻訳し、AI に実際のコードを書かせることが私たちの主たる責務になるのです。 私自身、専門的なゲーム開発の経験があるわけではありませんが、いちゲーマーおよび愛好家として、ゲームで実現したい内容を表現するための**ドメイン言語(専門用語・概念)**には精通しています。プロンプトを特定のキーワード(例えば「arcade game」「match 3」など)に根ざさせたり、共通認識のある具体例(例えば「16ビットや32ビット世代のパズルゲームにインスパイアされつつ、現代的なアレンジを加えたBGMが欲しい」など)を使ったりすることで、ゲーム体験がまったくない人が作ろうとするよりも、はるかに的確にエージェントへ意図を伝えることができます。 この点を強調しておきたいのは、たとえコーディング作業そのものが副次的なスキルになったとしても、パターンや機能を的確に記述する能力は、依然として極めて重要なソフトウェアエンジニアリングのコアスキルであるということです。バックエンドであれ、フロントエンドであれ、あるいはその中間であれ、自分の扱う領域のドメイン言語を熟知していなければなりません。 ## プランモードを使った設計から実装への移行 ドメイン言語を知ることは第一歩ですが、最初から完璧なワンショットプロンプトを書くことはほぼ不可能です。私たち Developer Relations(DevRel)は、デモや登壇でワンショットプロンプトをよく使いますが、人前で披露できるようになるまでに、そのワンショットプロンプトを何時間もかけて磨き上げているという舞台裏はあまり語られません。 完璧なプロンプトの作成はアートとサイエンスの融合であり、ドメイン言語をどれほど深く理解していても、必ず抜け漏れ(ギャップ)が生じます。幸いなことに、デモやプレゼンテーション以外の実際の開発では、一発で成功させる必要はどこにもありません。また、プロンプトを一人だけで抱え込む必要もありません。エージェント自身がプロンプトの具体化を手助けしてくれるからです。ここで役立つのが**プランモード(plan mode)**です。 プランモードでは、Gemini CLI はコードを書き始める前に、まず実装計画を策定します。これにより、エージェントと対話を重ねながら計画を洗練させ、意図した通りの方向に実装が進むようコントロールできます。 エージェントとの通常の対話中でも、会話の流れに応じてプランモードへの移行が提案されることがあります(例えば「計画を立てよう」といったフレーズを含むプロンプトに対する応答など)。しかし、エージェントの判断に任せるだけでなく、`/plan` コマンドを使って手動でいつでも切り替えることができます。 プランモード中、エージェントはリクエストに基づいた実装計画を組み立てるだけでなく、`ask_user` ツールを使って前提を確認するための質問を投げかけてくることがあります。計画がまとまるとレビューを求められるので、前提条件の認識違いを修正したり、機能を追加・削除したりして、計画を自在にコントロールできます。 例えば、今回作成したマッチ3ゲーム向けの、ある程度練り込んだ(とはいえ完璧からは程遠い)プロンプトがこちらです。 ```txt Build a Match-3 game called 'Cloud Crush' in Go using Ebitengine v2. The entire game screen should have background.png as background. The play area should be an 8x8 grid with white background. On the right side of the play area include a side panel with UI elements like player score and how to play instructions. The side panel should have a solid background colour to help with readability of the UI. Use standard GCP product logos (e.g. Compute Engine, Cloud Storage, BigQuery, etc.) as the game gems. These logos are provided in the gcp_sprites.png file. The logos are saved as 64x64 sprites but scale them as necessary based on the screen resolution. Implement swapping, clearing 3+ gems, and gravity. Use ebitengine native font rendering (size 48 for titles and size 24 for normal text) for all text and not the debug print. The font should be monospaced (golang.org/x/image/font/gofont/gomono). Keep the UI tidy and harmonic, e.g. centered text should always be adjusted based on text length, not just guess based on estimates. ``` このプロンプトはゲームの多くの要素をカバーしていますが、エージェントから「画面解像度はどうするか?」「アニメーションは滑らかにするか、それとも静的な切り替えにするか?」といった追加の詳細を尋ねられるのが一般的です。 計画の詳細度に納得したら、エージェントに実装の開始を指示します。これでプランモードは終了します。ここから先は通常のコーディングタスクと何ら変わりません。数ターンの対話を経ることで、次のように動作するゲームが立ち上がります。 ![Cloud Crush Gameplay Screenshot](https://danicat.dev/ja/posts/match3-game-gemini-cli/cloud-crush-gameplay.png) ## ブラウザエージェントによるWebテストの自動化 ゲーム開発において最も難易度が高い領域の1つが「テスト」です。考えうるあらゆるゲーム状態を網羅したり、描画関数が画面上に正しい要素を描いているかを検証したりする標準的なユニットテストを書くことは困難です。無理に書こうとしても、壊れやすく、保守が面倒で、時間を浪費する結果になることは目に見えています。 これは自動テストを一切書くべきではないという意味ではなく、純粋なコードでテストすべき領域と、人間によるプレイテストが必要な領域には境界があるということです。例えば、衝突判定(コリジョン)や経路探索のようなアルゴリズムのユニットテストは有効ですが、多様な解像度でのUI表示確認などは人間が行う方が適しています(例えば「このフォントは読みやすいか?」をどうやってユニットテストすればよいでしょうか?)。 ……少なくとも、今まではそうでした。*ここでサブエージェントの登場です。* フロンティアモデルのマルチモーダル機能とエージェントの巧みな活用によって、視覚的な検証(ビジュアルバリデーション)の自動化が現実のものとなりました。Gemini CLI におけるサブエージェントとは、メインの会話とは独立した独自のコンテキストウィンドウで実行される、専門化されたペルソナです。サブエージェントを活用することで、ベースのコーディングワークフローにあらゆる拡張機能を追加できます。 今回のテストシナリオでは、CLI に同梱されている `@browser_agent` という試験運用版(Experimental)のエージェントを使用します。試験運用機能であるため、`settings.json` を編集して[手動で有効化](https://geminicli.com/docs/core/subagents/#enabling-the-browser-agent)する必要があります。以下は、ビジョンモデルを指定してブラウザエージェントを有効化する最小限の `settings.json` 設定例です。 ```json { "agents": { "overrides": { "browser_agent": { "enabled": true } }, "browser": { "visualModel": "gemini-2.5-computer-use-preview-10-2025" } } } ``` 通常、ブラウザエージェントはスクリーンリーダーなどが使用する隠れた構造情報である「アクセシビリティツリー(accessibility tree)」を読み取ることで Web ページを操作します。しかし、今回のマッチ3ゲームはすべて単一の HTML canvas 要素上に描画されます。アクセシビリティツリーから見れば、単なる巨大な空白のボックスにしか見えません。 ここでビジョンモデルを追加することが状況を一変させます。エージェントに `visualModel`(`gemini-2.5-computer-use-preview-10-2025` など)を設定することで、エージェントは文字通り「目」を持ちます。スクリーンショットを撮影し、視覚的なレイアウトを解析して、画面上でクリックすべき正確な X/Y 座標を自ら割り出すのです。 デプロイされた Cloud Run アプリケーションを手動でクリックして回る代わりに、`@browser_agent please test the live URL...` と指示するだけで、サイトへアクセスし、ゲームを1ラウンドプレイして、正常動作している画面のスクリーンショットを撮影してくれます。 これはゲームフィール(手触り・爽快感)を確かめる人間によるプレイテストの完全な代用品にはなりませんが、ターミナルから一歩も出ることなく、UIが正しくレンダリングされていることを証明する視覚的検証を自動化してくれます。 ## セキュリティの不安をアウトソーシングする 実装が完了しUIの検証も済んだら、忘れてはならないのがセキュリティです。 私はアプリケーションセキュリティの専門家ではないため、Web アプリのセキュリティ体制(security posture)を厳密に評価する適任者ではありません。しかし、エージェンティック・コーディングが私のゲームエンジン経験の不足を補ってくれたのと同様に、サブエージェントはセキュリティ専門知識の不足を補ってくれます。オーケストレーターである私自身が、クロスサイトスクリプティング(XSS)のあらゆる攻撃ベクトルを把握している必要はありません。クリーンなコンテキストを持つ専門エージェントを立ち上げ、脆弱性を洗い出させる方法さえ知っていればよいのです。 Markdown ファイル(`.gemini/agents/security-auditor.md`)に[カスタムエージェント](https://geminicli.com/docs/core/subagents/#creating-custom-subagents)を定義することで、隔離された実行環境を用意し、`@security_auditor` で呼び出すことができます。 ```markdown --- name: security_auditor description: Specialized in finding security vulnerabilities in code. kind: local tools: - read_file - grep_search model: gemini-3-flash-preview temperature: 0.2 max_turns: 10 --- You are a ruthless Security Auditor. Your job is to analyze code for potential vulnerabilities. Focus on: 1. SQL Injection 2. XSS (Cross-Site Scripting) 3. Hardcoded credentials 4. Unsafe file operations When you find a vulnerability, explain it clearly and suggest a fix. Do not fix it yourself; just report it. ``` エージェントには特定のシステムプロンプト(Markdown ファイルの本文)と、`read_file` や `grep_search` といったツール(フロントマターで指定)を与えます。独立したコンテキストループで動作するため、メインの会話履歴を散らかす(トークンを圧迫する)こともありません。 私はこの監査エージェントを *Cloud Crush* のコードベースに向けて実行し、ハードコードされた認証情報や安全でないファイル操作、デプロイ時のリスクがないかを精査させました。カスタムセキュリティエージェントが専任の人間のセキュリティエンジニアを完全に代替できるわけではないにせよ、自分ひとりでは見落としがちな基本的な防御レイヤーを確実に担保してくれます。 ## 新しい開発ワークフロー このワークフローは、私が考えるソフトウェア開発の新たなスタンダードを示すものです。エージェントを活用してコードを生成しつつ、アーキテクチャや品質基準を担保するためにカスタムツール、Agent Skills、サブエージェントを積極的に構築・配備していきます。 勘の良い読者の方なら、この記事ではステップごとの手順解説をあえて最小限に留めていることにお気づきかもしれません。それは、この開発体験に特化した本格的なコードラボ(Codelab)を用意しているからです。以下のリンクからコードラボにアクセスすれば、本記事で解説したすべてをステップ・バイ・ステップで実際に体験し、自分だけのマッチ3ゲームを作り上げることができます。 **Codelab: [Gemini CLI で作るマッチ3アーケードゲーム](https://codelabs.developers.google.com/next26/gemini-cli-match3-golang#0)** もちろん、何か質問があれば、私のSNSでお気軽に声をかけてください! --- # エージェント時代におけるコードレビューのやり方 - **Markdown URL**: https://danicat.dev/ja/posts/code-reviews-in-2026/index.md - **HTML URL**: https://danicat.dev/ja/posts/code-reviews-in-2026/ - **Date**: 2026-03-06 - **Categories**: Agentic Coding - **Tags**: code-review, vibe-coding - **Summary**: AI生成コードとPull Requestをレビューするための実践ガイド。精査すべきアーキテクチャの要所と、リンターに委ねるべき機械的タスクを解説します。 2025年はエージェンティック・コーディング(agentic coding)が台頭した年でした(どうやら「バイブコーディング(vibe coding)」という言葉はもう廃れつつあるようです)。AIアシスタントや自律型エージェントワークフローの普及により、かつてないペースで新機能が次々と世に送り出されています。自社のコードベースの何十パーセントがAIによって完全に書かれているかを企業が誇らしげに語るのも、今や日常茶飯事です。 これらが良いことか悪いことかはまだ分かりませんが(私は個人的には良いことだと思っています)、このコーディング速度の向上には代償も伴います。生成される膨大な量のコードをレビューするのは骨が折れる作業であり、コードレビューは急速にボトルネックになりつつあります。一部のチームやオープンソースプロジェクトの中には、AI生成のPull Request(PR)の受け入れを全面的に拒否するという強硬手段に出たところさえあります。 AIを禁止することで一時的に一息つくことはできますが、長期的には良い選択肢だとは思いません。私のお気に入りの架空の種族が言うように、「抵抗は無意味だ(Resistance is futile)」というわけです。この新たな次元の生産性を生き抜くには、機械のほうが得意な仕事を人間がやるのをやめる必要があります。「AIにはAIで対抗する(Fight AI with AI)」のです。しかし、AIだけではありません。昔ながらの決定論的(deterministic)なツール群も絶大な威力を発揮します。リンター(linter)で検出できる問題なら、人間である私がわざわざ目を凝らす必要はありません。フォーマッターが自動修正してくれるなら、なおさら気にする必要はないのです。 私の「異論覚悟の持論(unpopular take)」をお話ししましょう。私は、コードを書いたのが人間かエージェントかなど気にしていません。オープンソースにおいて、コントリビューションは本質的に「ゼロトラスト(zero-trust)」です。百戦錬磨のFAANGエンジニアが書いたコードであれ、スリランカの高校生が書いたコードであれ、評価基準に違いはないはずです。それなら、AIが書いたかどうかをなぜ気にする必要があるのでしょうか? 理論上は人間が作成するPRのほうが小さいはずですが、この業界で20年ほど働いてきた経験上、私も数々のメガPR(巨大なPR)を嫌というほど見てきました。そのため、巨大なPRや雑なPRへの対応は今に始まった新しい問題ではないと断言できます。 私はコードをありのまま(額面通り)に評価します。ちゃんと動くか?安全か?既知の問題を解決しているか?ロードマップの方向性と一致しているか?チームの基準に準拠しているか? これが、本日の記事で、外部からのコントリビューションを扱う時だけでなく、私自身のAI生成コードを扱う時にも、私がどのようにコードレビューに取り組んでいるかについてお話ししたい理由です。というのも、実際には「AIとコーディングする」ということは、「AIを常にコードレビューし続ける」ことと同義だからです。 ## 私が実際に気にしていること 最近コードをレビューする時、私はますますハイレベル(高所大所)な視点で見るようになっています。ある意味、手動で書くコードが少なくなるほど、コードの細かな個別要素には気を配らなくなります。私が何らかの形でリーダーシップを担ったすべてのチームで、常に言ってきたことがあります。それは「コードは使い捨てである」ということです。この言葉は、今ほど真実味を帯びている時代はありません。もう一度繰り返します。**コードは使い捨てです**。使い捨てではないもの——それこそが、コードを開発する過程で獲得した「システムに関する知識(system knowledge)」です。この知識こそが、ある実装から別の実装へと確実に引き継がれ、たとえばAPIをv1からv2へと移行する際にも活きてくる本質的な価値なのです。 2回目の開発がより簡単になるのは、多くのことを発見し、多くの曖昧さを減らすという試行錯誤や生みの苦しみ(growing pains)をすでに経験しているからです。何がうまくいき、何がうまくいかなかったかを学びました。何が過剰設計(overengineered)で、何が設計不足(underengineered)だったのか。知識を集め、イテレーションを回し、進化させること——これこそがソフトウェアエンジニアリングの本質です。そしてこの種の知識こそが、AI時代を生き残るものです。コード自体は、単なる実装の詳細に過ぎません。 この哲学を踏まえ、私がコードレビュー時に重視しているポイントをいくつか挙げてみましょう(すべてを網羅しているわけではありませんが、主要なものです)。 ### アーキテクチャとシステム設計 AIモデルは全体像(big picture)を捉えるのが苦手で、安易な近道(ショートカット)を選びがちです。私のレビューでは、ハードコードされた値や設定値、問題領域の過度な単純化(AIはコーディングの要求をプロトタイプやデモとして扱いがちです)、そして逆説的ですが「過剰設計(over-engineering)」といった兆候を見逃さないようにしています。またAIモデルには、「本番レディ(production-ready)=複雑であること」と思い込んでしまう厄介な性質もあります。要するに、バランス感覚やプラグマティズム(実用主義)を保つのが苦手なのです。これらは人間が経験を通じて培うものであり、言葉でプロンプトに落とし込むのが難しい領域でもあります。 ### パブリックAPIとモジュール 私たちが構築しているもののエルゴノミクス(使いやすさ・人間工学)は極めて重要です。パブリックAPIは、それを利用する一般的な開発者にとって「直感的で心地よい(feel right)」ものである必要があります。優れた設計のインターフェースは直感的で、誤用しにくく、泥臭い内部実装をコードベースの他の部分からきれいに隠蔽してくれます。私はインターフェースが堅牢で適切にスコープ設定されているかを確認し、APIの露出面(surface area)を可能な限り最小限に抑えることを目指します。APIが使いにくい場合、基礎となるコードがどれほどエレガントであるかは問題になりません。コードは使いやすく、十分にドキュメント化されていますか?パブリックAPIの出来が良いかどうかを見極める優れた指標の一つが、テストコードの質です。設計の悪いAPIは、本質的にテストを書くのが難しいからです。 ### アルゴリズムとパターン LLMは放っておくと、問題を解決するために最もナイーブで力任せ(ブルートフォース)な方法を選びがちです。本来ならバルクインサート(一括挿入)戦略をとるべき場面で、エージェントが二重ループを回して数行ごとにコミットしながら大規模なデータ移行を行おうとするのは典型例です。あるいはより根本的なレベルでは、マップや辞書(dictionary)を使うべき場面でリストを使ってしまうようなケースです。データ構造とアルゴリズムが問題領域に実際に適合しているかを検証することで、深刻なパフォーマンス低下を防ぐことができます。目標は、単にテストをパスするコードではなく、「スケールするコード」です。とはいえ、「早すぎる最適化(premature optimization)」も依然として禁物です。扱うデータセットが小さく限定的であれば、多少実行速度が落ちてもシンプルで圧倒的に読みやすいアプローチのほうが、大抵の場合は優れています。 ### 依存関係 新しいパッケージを追加するたびに、外部リスク、潜在的なセキュリティ脆弱性、そしてメンテナンスのオーバーヘッドが生じます。アプリケーションを小さく保つことは、攻撃対象領域(アタックサーフェス)を減らすことにつながります。GenAI SDKや主要なWebフレームワークのような中核ツールは素早くパスしますが、それ以外の外部パッケージは厳しく精査します。わずかな依存関係を追加するくらいなら、少しのコードコピー(または再実装)のほうがマシです(*A little copying is better than a little dependency*)。コードの生成や保守が容易になればなるほど、「コードの再利用」に固執する必要性は薄れます。特に再利用のために新たな攻撃経路をコードベースに持ち込んでしまうくらいなら、なおさらです。 ### アンチパターンと品質の問題 エラーの握りつぶし(無視やもみ消し)、意図しない副作用、グローバルな状態、ミュータブル(可変)な状態、リソースリーク、未使用の関数や変数など、挙げればきりがありません。各言語特有のイディオム(慣用表現)も大切です。私はこれらを非常に重視していますが、同時にこれらは[golangci-lint](https://golangci-lint.run/)(Go)や[ruff](https://docs.astral.sh/ruff/)(Python)のような静的解析ツール(リンター)を活用して自動化するのが最も容易な領域でもあります。 ### テスト容易性 テストが書きにくいコードは通常、設計が悪く、将来の変更を拒絶するようになります。関心事の明確な分離、クリーンな入力、そして純粋関数が理想です。優れたテストはコードが正しく動くことを証明し、将来のリファクタリングにおける安全網を提供します。UIコンポーネントや複雑なシステムの場合、厳密な単体テストのカバレッジよりも実践的なテスト戦略を重視しますが、コアロジックは必ずテストでカバーされている必要があります。 状況はプロジェクトごとに千差万別であるため、私はすべてのプロジェクトに画一的なカバレッジ目標を設定することはやめました。しかし、テストされるべき箇所が確実にテストされているかは把握する必要があります。理想的にはハッピーパス(正常系)の100%と、サッドパス(異常系・エラー系)の妥当な割合をカバーすることですが、すべてのコードで100%のカバレッジを目指すような無理はしません。優れたオブザーバビリティ戦略と分かりやすいエラーメッセージさえ整っていれば、未知のエラーモードが発生しても後からテストスイートに追加できるため、長期的な成功を担保できます。 ### ベンチマーキング クリティカルパス(重要処理経路)においては、パフォーマンスに対する「推測」ではなく「実際の測定値」が必要です。トラフィックの多いコンポーネントに影響を与える変更については、遅いコードが本番環境に混入するのを防ぐため、明確なベンチマークの提示を必須としています。 ### 無駄のないロギング ログはアクションにつながる(actionable)ものでなければなりません。無駄なログはクラウド利用料を跳ね上げるだけでなく、機密情報やプライベートな情報の漏洩リスクにもつながります。開発中の詳細なロギング自体は構いませんが、マージ前には必ず整理・削除すべきです。 ## 私が(ほとんど)気にしないこと 細かいディテールは自動化ツールに任せることで、私はより本質的で難しい課題に集中できます。機械にできる仕事なら、人間がやるべきではありません。 ### 1行1行の細かなコード LLMが生成したすべてのコード行を逐一チェックするのは、コンパイラや静的解析ツールの仕事です。私はそうした細部ではなく、ロジックやコンポーネント間の接続点に焦点を当てます。 ### フォーマット Go言語を書き始めて以来、私はコードフォーマットのスタイル論争に関わったことがありませんが、いまだにそうした議論が存在する界隈があることも知っています。最善の策は、チーム標準を策定し、リンターとフォーマッターにすべてを任せることです。標準ルールが定まっていれば、コーディングエージェントもそれに従いやすくなります。CIパイプラインのチェックをパスしているなら、それで十分です。 ### 些細な構文やコードの書き方 1つの問題を解決するアプローチは無数にあり、特定の構文スタイルを強要することは開発者の自由度を狭めるだけです。ロジックが破綻していない限り、`for`ループを使っていようがリスト内包表記を使っていようが、私はまったく気にしません。 ### デバッグ 私はデバッグセッション(実際にデバッガーを使うという厳密な意味で)をほとんど行いません。私にとってデバッグは最後の手段であり、「ここを通った(I AM HERE)」のようなprint文(本来ならログ行であるべきもの)を大量に散りばめることの同義語に過ぎないからです。 何かが意図通りに動かないときは、その問題をシミュレートする新しいテストを作成します。問題を再現した後に何が起きているのか把握できないとすれば、それはオブザーバビリティやログが不足している証拠です。そのため、私はそれらの改善に注力します。 ### エクスポートされていない識別子(非公開の名前) 名前(識別子)が関数ローカルであったり、スコープが限られていたりする場合、複数の関数やファイルにまたがって使われる識別子ほどにはこだわりません。コードをざっと流し読みして、あまりに不自然なものを見かけたら整理するために立ち止まるかもしれませんが、そうでなければモデルが使うと判断した命名をそのまま受け入れます。 ### マイナーな依存関係 主要フレームワークやクライアントライブラリ以外の外部依存関係のことです。これらはセキュリティのベースラインを満たしている限り、そこまで心配する必要はありません。もちろん、セキュリティ脆弱性やライセンス上の問題がないかのチェックは依然として必須です。しかし、たった1つの「ヘルパー」関数のためだけに外部パッケージをインポートしているなら、私は100%の確率でその関数を自分のコード内で再実装し、余計な依存関係を排除します。 ## まとめ これは万人に当てはまる画一的なプロトコル(手順)ではありません。また、コードベースをどのように計測・計装しているかについても、語るべきことはたくさんあります。コードレビュー単体ですべての潜在的な問題を拾い上げることはできません。だからこそ、エージェンティック・コーディングの時代においては、これまで以上に自動化を強く推奨しているのです。 現代のコーディングエージェントには、モデルを制約してより決定論的な出力を得るための多くの拡張パターンが存在します。[Agent Skills](https://danicat.dev/ja/posts/20260829-the-pragmatic-guide-to-agent-skills/)、[Hooks](https://danicat.dev/ja/posts/20260610-mastering-hooks/)、[MCP ツール](https://danicat.dev/ja/posts/20250817-hello-mcp-world/)、ポリシー、ルール……。これらのツールを活用してエージェントの作業スコープに明確な境界線を設けることで、開発者の体験はずっと快適になります。 車は、ブレーキの性能が許す速度でしか走ることができません。お気に入りのコーディングエージェントのガードレール(制約や制御手法)を学ぶことに投資し、自動化できない本質的な部分のレビューにこそ、皆さんの貴重な時間を使ってください。 Happy coding! Dani =^.^= --- # skill-creator で Agent Skills を構築する - **Markdown URL**: https://danicat.dev/ja/posts/gemini-cli-skills-part-2/index.md - **HTML URL**: https://danicat.dev/ja/posts/gemini-cli-skills-part-2/ - **Date**: 2026-02-27 - **Categories**: Agentic Coding - **Tags**: agent-skills, gemini-cli, vibe-coding - **Series**: Agent Skills - **Summary**: Gemini CLI の skill-creator を使った Agent Skills 設計ガイド。latest-version、Ruff を活用した pyhd、find-examples、de-sloppify の4つの実践例を解説。 > **追記 (2026年):** Gemini CLI は **Google Antigravity 2.0** へと進化しました。本記事で解説している Agent Skills の基本概念や構造は引き続き有効ですが、最新プラットフォームと機能の概要については [Antigravity 2.0 への銀河ヒッチハイク・ガイド](https://danicat.dev/ja/posts/20260521-the-hitchhikers-guide-to-antigravity-2-0/) をご覧ください。 [パート 1: Gemini CLI で Agent Skills をマスターする](https://danicat.dev/ja/posts/20260128-agent-skills-gemini-cli/)では、Agent Skills がどのように Gemini CLI へと新機能を追加するのかを紹介しました。タスク固有の指示をエージェントに与えつつ、コンテキストをクリーンに保つ実践的な例として、`experiment-analyst` スキルを取り上げました。 今回は、スキル設計のコア原則を深掘りし、私が日常的に使っているスキルの実践例をいくつか紹介します。 ## Agent Skills とは パート1を見逃した方のために、簡単におさらいしておきましょう。Agent Skills は、コーディングエージェントに「ジャストインタイム(必要なときだけ)」の専門知識を提供するために設計されたオープンスタンダードです。専門知識が必要なタイミングでのみコンテキストへ追加される仕組みになっているため、いわゆるコンテキストの肥大化(context bloat)を防ぐことができます。この設計思想を専門用語では **Progressive Disclosure(段階的開示)** と呼びます。コアとなる指示(`SKILL.md`)は極力スリムに保ち、詳細なリファレンスやスクリプトは別ファイルに切り出して、*必要なときだけ*ロードするのです。 ディスク上では、スキルは `SKILL.md` ファイルと、必要に応じたバンドルリソース(Bundled Resources)を含むひとつのフォルダとして構成されます。 ```text skill-name/ ├── SKILL.md (必須: 名前、説明、コアとなる指示のみ) └── Bundled Resources (オプション) ├── scripts/ (繰り返し実行するタスク用の実行可能コード) ├── references/ (オンデマンドで読み込まれるドキュメント。例: API スキーマ) └── assets/ (出力で使用されるテンプレートやバイナリファイル) ``` ## `skill-creator` スキル スキルを手動で書くことももちろん可能ですが、Gemini CLI には `skill-creator` というメタスキルが標準で組み込まれており、これを使うと作業が格段に楽になります。 このスキルを呼び出すには、Gemini CLI にスキルの作成(またはリファクタリング)を依頼するだけです: > *「ソフトウェアパッケージの実際の最新バージョンを取得する新しいスキルを作って。バージョンのハルシネーションを起こさないようにしたいんだ。」* 「スキル作成」に関するプロンプトであれば通常は自動で `skill-creator` がトリガーされますが、もしモデルの機嫌が悪くて反応が鈍い場合は、次のように明示的に指示することもできます: > *「skill-creator を使って、AI 生成テキストの AI っぽさ(スロップ)を排除する de-sloppify スキルを書いて(気を悪くしないでね)」* Gemini CLI は、スキルのボイラープレート(雛形)を出力する前に、いくつか細かい要件を確認してくることがあります。最近追加された [`ask_user` ツール](https://geminicli.com/docs/tools/ask-user/#ask_user-ask-user) を使ってユーザーに対話的に質問してくれるのですが、実際にこれが動く様子は見ていてとても面白いです。 ## スキルを作るべきタイミング 私の個人的なワークフローでは、スキルを作成する目的は主に2つあります: 1. **自分独自のワークフローやプロセスを手順化するため**(例:自分好みのコードレビューの手順、リポジトリの初期化方法、ブログ記事の品質評価など) 2. **特定のツール、言語、技術に関する専門知識を付与するため**(例:Genkit プロジェクトの構造と動かし方、ADK を使ったエージェント開発の流れなど) ある意味で、スキルはスラッシュコマンド(私は MCP プロンプトとして保存することが多いです)とツールの間に位置する中間的な概念だと言えます。スラッシュコマンドを作るときは「再現可能なプロセス(手順)」を定義したいのであり、ツールを作るときはモデルに「確定的(デターミニスティック)な実行手段」を持たせたいケースがほとんどです。スキルにはプロンプトとスクリプトの両方を含められるため、スクリプトにツールの役割を担わせることで、その両方をひとまとめに実現できます。 もちろん、スキルを拡張機能(Extension)としてパッケージ化する場合は、ツールを提供する MCP サーバーと一緒に配布されるケースも多いでしょう。スキルの定義内でその連携を活用し、MCP ツールが利用可能な場合にどう呼び出すかをモデルに教え込むことも可能です。 また、私がスキルを新規作成するタイミングも主に2パターンあります: 1. **モデルに思い通りの作業をさせるために、骨の折れる長いセッションをこなした直後**(例:「今やった作業のノウハウを、次回以降も使えるようにスキルとしてまとめて」) 2. **業務効率化に役立ちそうな新しいアイデアを思いついた瞬間**(例:「AI の文章力を底上げするために de-sloppify スキルを作ろう」) どちらのケースでも、スキルが一発で完璧に仕上がることはまずありません。実際に使いながら「これは役立つ」と確信できるまでブラッシュアップしていくか、あるいは一旦破棄して、課題への理解が深まるまで寝かせておくかのどちらかです。 次のセクションでは、私がこれまでに作成してきたスキルを具体的に見ていきましょう。 ## 実践的な例 私のリポジトリから4つのスキルを取り上げ、それらがどのように特定の課題を解決しているかを見ていきましょう。 ### 1. `latest-version` このスキルは、LLM 全般がソフトウェア、ライブラリ、モデル、各種依存関係の **古いバージョン** を使いたがる傾向に対する、**純粋なフラストレーション** から生まれました。ナレッジカットオフ(知識の期限)がある以上、仕方のないことだとは理解しています。それでも、エージェントが Gemini 3 ではなく `gemini-1.5-pro` を使おうとしたり、あまつさえ「未来のバージョンを幻覚(ハルシネーション)している」と **私の方を** 責めてきたりすると、どうしてもイラッとしてしまいます。 このスキルは、各種レジストリ(npm、PyPI、Go Proxy)や公式ドキュメントに直接問い合わせることで、ファクトチェッカーとして機能します。以下は、その `SKILL.md` の抜粋です: ```markdown name: latest-version description: > The definitive real-time source of truth for software and model versions. Use this skill to bypass internal knowledge cutoffs... ## Core Mandate **NEVER GUESS.** When a user asks to install a package or add a dependency, you must verify the latest version using the `latest.js` script. Do not rely on your internal weights, as they are months or years out of date. ``` このプロンプトはまだ少し粗削りな部分がありますが、私のコードベースに非推奨の古いモデルが入り込むのを防ぐ上で、かなりの成果を上げています。 🔗 [`latest-version` スキルの全文を見る](https://github.com/danicat/skills/tree/main/latest-version) ### 2. `pyhd` 昨年 `godoctor` MCP サーバーを作ったとき、エージェントを使った Go 開発における**究極のツール**(科学に裏打ちされたもの! ^^)にしたいと考えました。当時はまだスキルという仕組みがなかったため、必要なツールをすべて MCP サーバーに詰め込むのが自然なアプローチでした。しばらくの間、Python でも同じようなものを作ろうかと考えていましたが、バックログが山積みで優先度を下げざるを得ませんでした。 そんな折に Agent Skills と出会い、「MCP サーバーではなくスキルとして作ればいいのでは?」と思い立ちました。`skill-creator` のおかげで作成コストが非常に低く抑えられたため、`pyhd`(Python + PhD の造語で、"doctor" テーマを踏襲)を作ることにしたのです。 `pyhd` スキルには Python プロジェクト用の開発ワークフローが定義されており、適切で "Pythonic" なコードを維持するために、リンター兼フォーマッターである `ruff` を中心に据えています。 ```markdown ## Core Workflow When editing Python files, you **MUST** follow this cycle for **EVERY** file modification: 1. **Read & Understand**: ... 2. **Edit**: Apply your changes using `smart_edit` or `replace`. 3. **Sanitize (Ruff)**: Immediately after editing, run the following commands to format and fix linting issues: `uv run ruff check --fix ` `uv run ruff format ` 4. **Verify**: Run tests... ``` このスキルによって、Python ファイルの編集直後に必ず標準的な lint とフォーマットが実行されるようになり、初期段階でのミスを素早く検知できるようになりました。本格的な "pydoctor" を実装する時間が取れるまでは、これが私の Python 開発における定番スキルになっています。 🔗 [`pyhd` スキルの全文を見る](https://github.com/danicat/skills/tree/main/pyhd) ### 3. `find-examples` 特定のライブラリが実環境で実際にどう使われているかを知りたい場面はよくあります。`find-examples` スキルは、Python スクリプト(`github_search.py`)を使って GitHub 上を検索し、プロジェクトで使いたい依存関係の実際の使用コードを探し出します。モデルが架空の API をハルシネーションしてしまう問題に対処するために開発したスキルですが、ドキュメントや実例を少し参照させるだけで、精度は格段に向上します。 GitHub の公開検索のみを利用しているためパーソナルアクセストークン(PAT)は不要で、一般的な Google 検索よりも的確なコード例を拾ってこられる傾向があります。 ```markdown ### 1. Search for Repositories (Multi-Language) Run the `github_search.py` script. If you can't find many examples in your target language, add related languages supported by the SDK. ### 4. Clone and Inspect Clone the selected repositories into the `_examples` folder. Once cloned, use `list_files`, `smart_read`, or `grep_search` to find relevant implementation details. ``` また、マルチ言語対応(ポリグロット)の SDK 向けに、別言語でのコード例も探し出せる機能を追加しました。最近作ったばかりのスキルなのでまだ使い込んではいませんが、面白いユースケースの例として紹介しました。 🔗 [`find-examples` スキルの全文を見る](https://github.com/danicat/skills/tree/main/find-examples) ### 4. `de-sloppify` このスキルは、AI 特有の典型的な文章パターン(いわゆる AI スロップ)をチェックするために使っています。語彙の選択、文の長さのバリエーション、構造の反復パターンなどに基づいて「スロップスコア(slop score)」を算出するスクリプトが含まれています。 NLTK を使って品詞タグ付けを行い、無修正のモデル出力によく見られる名詞の過密使用や受動態の多用を検出します。スクリプトはローカル環境で実行され、検出された具体的なマーカーをレポートとして出力してくれます。 🔗 [`de-sloppify` スキルの全文を見る](https://github.com/danicat/skills/tree/main/de-sloppify) ## まとめ スキルが一発で完璧に仕上がることは滅多にありません。スキルを磨く最も効果的な方法は、実際の開発で使い倒すことです。エージェントが特定のステップでつまずいていたり、見当違いのコンテキストを引っ張ってきたりしているのを見かけたら、再び `skill-creator` を使ってスキルを更新するよう依頼してみてください。 ぜひ普段のワークフローを振り返ってみてください。AI に毎回同じルールや前提を言い聞かせているタスクはありませんか? それこそが、あなたの次のカスタムスキルにうってつけの候補です。 自分だけのスキルを作る準備はできましたか? まずは [公式ドキュメント](https://geminicli.com/docs/cli/skills/) で基本をチェックし、アイデアのヒントが必要なら GitHub の [danicat/skills リポジトリ](https://github.com/danicat/skills) も覗いてみてください。 Happy coding! Dani =^.^= --- # Genkit と Gemini 3 で古い写真に命を吹き込む - **Markdown URL**: https://danicat.dev/ja/posts/genkit-go-photo-restoration/index.md - **HTML URL**: https://danicat.dev/ja/posts/genkit-go-photo-restoration/ - **Date**: 2026-02-16 - **Categories**: Applied GenAI - **Tags**: gemini, genkit, golang, nano-banana, tutorial - **Summary**: Genkit Go と Gemini 3 Pro Image (Nano Banana Pro) を使ってネイティブ4K写真復元ツールGlowUpを構築するチュートリアル。dotpromptテンプレートとFlow実装を解説。 仕事柄、さまざまな人と知り合う機会が多いのですが、よく話題にのぼるのが私のルーツについてです。東欧系の響きを持つ苗字であるだけでなく、見た目もまさにその系統なので、ポーランドかチェコの出身だろうとよく思われます。そのため、実際にはブラジル出身だと伝えると、大抵の人は驚きます。 我が家は歴史的な記録を残すのが本当に**苦手**で、家族の誰も自分たちのルーツを正確には把握していません。おそらく自分たちでもそれを自覚しているからこそ、把握できているわずかな家族の歴史や、それすらも薄れつつあることについてよく話をします。歳を重ねるにつれて、真っ先に失われていくのは記憶であり、やがて書類や写真も劣化していきます。最後に祖母に会ってから30年が経ち、彼女の顔がおぼろげな輪郭でしか思い出せないと気づいたときには、何とも言えない切なさがこみ上げてきます。だからこそ、私にとって写真はかけがえのない存在なのです。薄れゆく自分自身の記憶の風化に抗うための、最後の砦だからです。 近年撮影されたものなら、簡単に複製してクラウドにいくらでも冗長なバックアップを保存できます。しかし、ここで話しているのはデジタル時代以前の大切な形見です。たとえ何年も前にスキャンしていたとしても、多くの写真はすでに何十年分もの埃やカビ、経年劣化に見舞われています。時間は止まっていても、状態が自然に良くなることはありません。 ですが、生成AIの進化のおかげで、決してすべてが失われたわけではありませんでした。経年変化によるダメージを修復するだけでなく、カラー化してアップスケールし、現代のクオリティへと蘇らせることができるようになったのです。こうして生まれたのが、「GlowUp」という小さなツールです。 以下はそのような復元の一例です。 ![バナナパイを作る祖母の傷んだモノクロ写真(オリジナル)](original.jpg "オリジナル:特製バナナパイを作る私の祖母") ![Nano Banana Pro を使って高精細4Kで復元・カラー化した写真](restored.png "復元後:Nano Banana Pro による修復とカラー化") この記事では、[Gemini Nano Banana Pro](https://ai.google.dev/gemini-api/docs/image-generation) と [Genkit Go](https://genkit.dev/docs/get-started/?lang=go) を使って、GlowUp をゼロから構築する方法を紹介します。 ## 構成要素 今回 Nano Banana Pro(別名 Gemini 3 Pro Image Preview)を選んだのは、現在 Gemini ファミリーの中で最も高度な画像処理モデルだからです。標準の Nano Banana(Gemini 2.5 Flash Image)も素晴らしいモデルですが、Pro バージョンのほうがより高品質な出力が得られ、多少の試行錯誤は必要なものの、指示に対する追従性(instruction following)も優れていると感じています。 クライアントサイドの実装では、[go-genai](https://pkg.go.dev/google.golang.org/genai) のような低レベル SDK を直接使うのではなく、Genkit を採用しました。低レベルなコードを記述する場合と比べて、次のような優れた開発体験(QoL)が得られるためです。 - **モデルに依存しない設計(Model-agnostic)**: プラグインを差し替えるだけで、Google 以外のモデルやローカルモデルを含め、異なるモデルを自在に検証可能 - **Dev UI の標準サポート**: モデルやプロンプトのテスト、モデル呼び出しのトレースなどがブラウザの UI で手軽に行える - **柔軟なアーキテクチャ**: CLI アプリケーションとしても Web サーバーとしても同一ロジックを容易にパッケージ化可能 GlowUp は、コマンドラインツールとしても Web サーバーとしても実行できる単一バイナリとして構築されています。この柔軟性のおかげで、手元のターミナルからローカルでサクッと修復を実行することも、同じコードをクラウドサービスとしてデプロイすることもできます。将来的には、父でも自分の写真コレクションを簡単に修復できるような Web アプリに発展させることも可能です。 ## Genkit Go の基本 [Genkit](https://firebase.google.com/docs/genkit) は、AI アプリケーション開発に本番水準の信頼性と開発プラクティスをもたらすために設計されたオープンソースフレームワークです。Go エンジニア*なら、AI 機能のための「標準ライブラリ」のような存在と考えると分かりやすいでしょう。_(* もし Go 以外の言語をお使いの場合でも、Genkit は JavaScript / TypeScript や Python もサポートしていますので公式ドキュメントをチェックしてみてください。)_ Go 版 Genkit での最小限の「Hello World」は以下のようになります。フレームワークの初期化に `googlegenai` プラグインを使用している点に注目してください。 ```go package main import ( "context" "fmt" "log" "net/http" "github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/googlegenai" "github.com/firebase/genkit/go/plugins/server" // Import the server plugin ) func main() { ctx := context.Background() // Initialize Genkit with the Google GenAI plugin (Vertex AI) g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{})) // Define a simple Flow genkit.DefineFlow(g, "hello", func(ctx context.Context, name string) (string, error) { // Generate text using a model resp, err := genkit.GenerateText(ctx, g, ai.WithModelName("vertexai/gemini-2.5-flash"), ai.WithPrompt(fmt.Sprintf("Say hello to %s", name))) if err != nil { return "", err } return resp, nil }) // Start the flow server manually mux := http.NewServeMux() // Register all flows defined in 'g' for _, flow := range genkit.ListFlows(g) { mux.HandleFunc("POST /"+flow.Name(), genkit.Handler(flow)) } if err := server.Start(ctx, ":8080", mux); err != nil { log.Fatal(err) } } ``` この短いコードスニペットの中には、多くの重要な処理が凝縮されています。各要素を少し詳しく見ていきましょう。 ### プラグイン(Plugins) コードを Vertex AI、Google AI、Ollama などのプロバイダーに接続するアダプターです。Google のモデルを利用する場合、`googlegenai` プラグインを使用します。このプラグインは両方のバックエンドに対応しています。 * **Google AI (Studio):** API キーを使用。プロトタイピングや個人プロジェクトに最適です。 ```go // Use Google AI (API Key) googlegenai.Init(ctx, &googlegenai.Config{APIKey: "MY_KEY"}) ``` * **Vertex AI (Google Cloud):** Google Cloud IAM 認証を使用。本番環境のワークロードやエンタープライズ機能に推奨されます。 ```go // Use Vertex AI (Cloud Auth) googlegenai.Init(ctx, &googlegenai.VertexAI{ProjectID: "my-project", Location: "us-central1"}) ``` **注:** 旧バージョンの Genkit を使ったことがある方は、個別の `vertexai` プラグインと `googleai` プラグインに見覚えがあるかもしれません。現在これらは単一の `googlegenai` プラグインに統合されています。 ### モデル(Models) コンテンツを生成する実際の LLM(Gemini や Claude など)です。`vertexai/gemini-2.5-flash` のような文字列でモデル名を指定して参照します。 ```go resp, err := genkit.GenerateText(ctx, g, ai.WithModel("vertexai/gemini-2.5-flash"), ai.WithTextPrompt("Tell me a joke")) ``` ### プロンプト(Prompts) 上の例のようにコード内にプロンプトを直接ハードコードすることもできますが、保守性を高めるためには独立したファイルに切り出すのがベストプラクティスです。Genkit は外部プロンプトの読み込みに `dotprompt` を使用します。 `dotprompt` ファイル(`*.prompt`)は、**Frontmatter** と **Template** の2つの主要部分で構成されます。 **1. Frontmatter(設定)** * **`model`**: モデル識別子(例: `vertexai/gemini-2.5-flash`) * **`config`**: `temperature` や `topK` などの生成パラメーター、またはモデル固有の設定(例: `imageConfig`) * **`input`**: Go コードから渡される引数を定義する JSON スキーマ * **`output`**: 構造化出力用のスキーマ **2. Template(指示)** プロンプト本文には Handlebars 構文を使用します。 * **変数(Variables)**: `{{theme}}` のようなプレースホルダーは、入力スキーマで定義した値に置換されます。 * **ロール(Roles)**: `{{role "system"}}` や `{{role "user"}}` ヘルパーにより、システム指示とユーザーのクエリを明確に構造化できます。 * **メディア(Media)**: `{{media url=myImage}}` ヘルパーにより、マルチモーダルデータ(画像や動画)をモデルのコンテキストに直接注入できます。 ```yaml --- model: vertexai/gemini-2.5-flash input: schema: theme: string --- {{role "system"}} You are a helpful assistant. {{role "user"}} Tell me a joke about {{theme}}. ``` ### フロー(Flows) Genkit において **Flow(フロー)** は処理実行の基本単位であり、以下の機能を提供します。 1. **可観測性(Observability)**: すべてのフロー実行時に、Genkit Developer UI や Google Cloud Trace で確認できるトレースとメトリクス(レイテンシ、トークン消費量、成功率など)が自動生成されます。 2. **型安全性(Type Safety)**: フローは入力・出力スキーマによって厳密に型付けされており、複数の AI 処理をパイプラインとして連結する際のランタイムエラーを防ぎます。 3. **デプロイの容易さ(Deployability)**: フローの実行ロジックはサーバー配信ロジックから完全に分離されています。デプロイする際は `genkit.Handler` でラップするだけで標準の `http.Handler` に変換できるため、Go の標準ライブラリ(`net/http`)や好みの Web フレームワークにそのまま組み込めます。 ```go // Define a flow myFlow := genkit.DefineFlow(g, "myFlow", func(ctx context.Context, input string) (string, error) { return "Processed: " + input, nil }) // Expose it as an HTTP handler http.HandleFunc("/myFlow", genkit.Handler(myFlow)) ``` ## Nano Banana Pro 今回の写真復元を支えるエンジンは、愛称として「Nano Banana Pro」と呼ばれる **Gemini 3 Pro Image** です。 これは、前世代(および現行の「Flash」モデル)と比べても飛躍的な進化を遂げています。Gemini 2.5 Flash は非常に高速で基本的な画像生成(`gemini-2.5-flash-image`)に対応していますが、**Nano Banana Pro**(`gemini-3-pro-image-preview`)は高度なマルチモーダル推論を行うために設計されています。 単にピクセルを「見る」だけでなく、意味論的なコンテキストを理解します。「紙の表面についた傷」と「顔の傷跡」を正確に見分けることができ、1950年代のキッチンなら現代的なフローリングではなくリノリウムの床が敷かれているはずだといった背景知識も把握しています。 ### 主な違い * **Flash (`gemini-2.5-flash-image`)**: 速度とコスト効率に最適化。サムネイルやシンプルなイラスト作成に向いています。最大解像度は 1024x1024。 * **Pro (`gemini-3-pro-image-preview`)**: 忠実度と推論力に最適化。写真復元には欠かせないネイティブ **4K 解像度**(最大 4096px)の生成に対応しています。 このモデルは出力を微調整するための `imageConfig` パラメーターも受け付けます。 * `imageSize`: `"4K"` または `"2K"` * `aspectRatio`: `"16:9"`、`"4:3"`、`"1:1"` など 注意すべき重要な点として、このモデルは常にテキストと画像の両方を含むインターリーブ形式(interleaved)のレスポンスを返します。他の画像生成モデルとは異なり、画像データ単体での出力はサポートされていません。そのため、後述する抽出ロジックのように、マルチパートレスポンスの中から画像データを適切に見つけ出す処理が必要になります。 **注:** 執筆時点では、このモデルは Vertex AI の `global` リージョンでのみ利用可能です。Vertex AI クライアントの設定時にはご注意ください。 ## 各コンポーネントの連携 続いて、GlowUp がこれらのパーツをどのように連携させているかを見ていきましょう。写真修復の専門家としてのペルソナを定義する**プロンプトファイル**と、画像処理を実行する**フロー**を使用します。 ### プロンプト モデルの設定と指示を定義するために `.prompt` ファイルを使用します。ここで `4K` 解像度を指定しているため、Go のコード側をシンプルに保てている点に注目してください。 ```yaml --- model: vertexai/gemini-3-pro-image-preview config: imageConfig: imageSize: "4K" input: schema: url: string contentType: string --- {{role "system"}} You are GlowUp, a professional-grade photo restorer. Your goal is to provide a "surgical" restoration service that transforms vintage, damaged, or monochrome photographs into high-fidelity 4K colourised versions. RULES: 1. **Grounding**: You are strictly grounded in the original source pixels. Do NOT add new objects (trees, people, buildings, etc.) that are not present in the source. Additionally, do NOT remove any elements from the source, unless they are clearly defects that do not belong in the original scene. 2. **Fidelity**: Preserve the original facial expressions and identity of subjects. Do NOT "beautify" or alter features in a way that changes the person's identity. 3. **Background**: Preserve background fidelity. Overexposed light sources (like windows) must remain as light sources. Do not "fill in" missing details with invented scenery. 4. **Colourisation**: If the image is monochrome, colourize it realistically, respecting historical accuracy where possible. 5. **Upscaling**: Output a high-fidelity image. {{role "user"}} Restore this photo. Image: {{media url=url contentType=contentType}} ``` ### フロー Go のコードは非常にシンプルで無駄がありません。この構成では、フロー定義がプロンプトをロードし、マルチモーダル入力をモデルに渡す役割を担います。 ```go // main.go (Flow Definition) type Input struct { URL string `json:"url,omitempty"` ContentType string `json:"contentType,omitempty"` } func defineGlowUpFlow(g *genkit.Genkit) *core.Flow[Input, string, struct{}] { return genkit.DefineFlow(g, "glowUp", func(ctx context.Context, input Input) (string, error) { prompt := genkit.LookupPrompt(g, "glowup") if prompt == nil { return "", errors.New("prompt 'glowup' not found") } resp, err := prompt.Execute(ctx, ai.WithInput(input)) if err != nil { return "", fmt.Errorf("generation failed: %w", err) } return resp.Media(), nil }) } ``` ローカルファイルをそのまま扱えるように、`fileToDataURI` ヘルパー関数を用意しています。この関数はローカルファイルを読み込み、`http.DetectContentType` で MIME タイプを自動判別した上で、Gemini API が要求する標準の Base64 Data URI 形式にエンコードします。これにより、拡張子をハードコードすることなく、さまざまなスキャン形式の画像を忠実度を保ったまま処理できます。 ```go func fileToDataURI(path string) (uri, contentType string, err error) { data, err := os.ReadFile(path) if err != nil { return "", "", err } contentType = http.DetectContentType(data) encoded := base64.StdEncoding.EncodeToString(data) uri = fmt.Sprintf("data:%s;base64,%s", contentType, encoded) return uri, contentType, nil } ``` Nano Banana Pro は入力画像からアスペクト比を自動で推測できるため、比率を計算して注入するような複雑なロジックは不要です。ピクセルデータをそのまま渡し、モデルに処理を任せるだけで機能します。 ## 実行方法 家族の歴史をつなぐ大切な思い出の写真が色あせてしまっているなら、ぜひ試してみてください。時間の経過に埋もれかけていた瞬間を取り戻し、本来あるべき鮮明さを蘇らせる素晴らしい手段になります。 1. **リポジトリのクローン**: ```bash git clone https://github.com/danicat/glowup cd glowup ``` 2. **認証情報の設定**(ロケーションは `global` を指定します): ```bash export GOOGLE_CLOUD_PROJECT=your-project-id export GOOGLE_CLOUD_LOCATION=global ``` 3. **修復の実行**: ```bash go run main.go restore --file old_photo.jpg ``` ## 既知の課題と制限事項 復元処理は十分に機能しますが、いくつかの注意点や癖もあります。開発中に見つかった課題を共有します。 * **指示の遵守精度(Instruction Adherence):** Nano Banana Pro は最先端モデルですが、プロンプトの指示を完全には反映しきれないことがたまにあります。意図通りの結果を得るまでに数回試行が必要になる場合があります。今回はプロンプトの細かなチューニングにそこまで時間をかけていないため、プロンプトエンジニアリングによってさらに改善できる余地があります。 * **Dev UI でのモデル一覧表示:** `googlegenai` プラグインに、Dev UI 上で利用可能なモデル一覧が自動展開されない不具合があります。モデル名を手動指定して「動的」に参照することは可能ですが、試行錯誤の段階で少し手間がかかります(JS 版では問題なく動作します)。すでに [Issue を報告済み](https://github.com/firebase/genkit/issues/4783)で修正もマージされていますが、古いバージョンを利用している場合はご注意ください。 ## おわりに GlowUp の構築は、AI を使って自分の過去や家族の記憶と感情的につながり直す、非常に有意義な実験となりました。AI に対しては悲観論も少なくありませんが、私自身がそもそも AI にワクワクしていたのは、まさにこうした心動かされるものづくりができるからでした。 この記事で使用した写真は、この技術の可能性を示すほんの一例に過ぎません。すでに第2弾の記事も執筆中であり、そこではさらに応用して、子供の頃に大好きだった思い出のカードゲームを復元するプロジェクトに挑戦しています。 要するに、その可能性は無限大です。技術的な課題であれ個人的なテーマであれ、この記事が皆さん自身の身近な課題に向き合い、解決するためのツールを作るきっかけになれば幸いです。 **自分でも作ってみたいですか?** この写真復元ツールをゼロから作れる[ステップ・バイ・ステップの Codelab](https://codelabs.developers.google.com/cloud-genkit-go-nano-banana?hl=en#0) を公開しています。 詳細については、[Genkit ドキュメント](https://firebase.google.com/docs/genkit) や [GlowUp のソースコード](https://github.com/danicat/glowup) もぜひご覧ください。 **Happy coding!** Dani =^.^= --- # Gemini CLI で Agent Skills をマスターする - **Markdown URL**: https://danicat.dev/ja/posts/agent-skills-gemini-cli/index.md - **HTML URL**: https://danicat.dev/ja/posts/agent-skills-gemini-cli/ - **Date**: 2026-01-29 - **Categories**: Agentic Coding - **Tags**: agent-skills, gemini-cli, mcp, vibe-coding - **Series**: Agent Skills - **Summary**: Gemini CLI における Agent Skills の構築と設定方法を解説。段階的開示(Progressive Disclosure)、オンデマンドなプロンプト読み込み、決定論的な Python 分析スクリプトの活用法を網羅。 > **追記 (2026年):** Gemini CLI は **Google Antigravity 2.0** へと進化しました。本記事で解説している Agent Skills の基本概念や構造は引き続き有効ですが、プラットフォームの概要については [Antigravity 2.0 への銀河ヒッチハイク・ガイド](https://danicat.dev/ja/posts/20260521-the-hitchhikers-guide-to-antigravity-2-0/) を、本番環境向けの最新パターンやスキルカタログ、トークン最適化については [Agent Skills 実践ガイド](https://danicat.dev/ja/posts/20260829-the-pragmatic-guide-to-agent-skills/) をご覧ください。 先週 [Tenkai](https://danicat.dev/ja/posts/20260120-improving-agentic-coding-with-science/) について記事を書いた際、実験分析に関する重要な側面——「実験結果からいかにして洞察を抽出するか」については触れませんでした。サマリーや統計指標、検定結果を確認できる使い勝手のよいフロントエンドはあるものの、単なるサマリーから各設定の微妙なニュアンスを汲み取るのは非常に困難です。 たとえば、ファイルの読み取り操作(`read_file` や godoctor の `smart_read` など)は、タスクが失敗したシナリオや完了までに時間がかかったシナリオと強い相関があることにしばしば気づきます。これは読み取り操作自体が悪いからでしょうか?いいえ、エージェントがエラーから復帰するために、ソースコードを再度読み直して知識をリフレッシュする必要があったからです。つまり、読み取り操作の多さと処理の遅延や失敗との間には強い相関関係があるものの、決して因果関係があるわけではありません。統計学者が言うように、「相関関係は因果関係を意味しない(相関は因果ではない)」のです。 ここ数週間数多くの実験を重ねる中で、毎回モデルに対してより深い分析を行うよう指示するのはあまり効果的ではないとすぐに気づきました。通常このようなシナリオでは、エージェントのコンテキスト(`GEMINI.md` など)に分析指示を追加するか、必要なプロンプトを MCP サーバーに格納してスラッシュコマンドにマッピングするかのいずれかを行います。 どちらのアプローチも機能はしますが、それぞれに限界があります。実行したいタスクごとにエージェントのコンテキストを拡張していくと、コンテキストの肥大化(context bloat)を招き、エージェントの動作精度が低下します。一方でプロンプトごとにスラッシュコマンドを作成する場合、エージェントは設計上それらのコマンドの存在を認識できないため、人間が明示的にコマンドを呼び出さなければなりません。 幸いなことに、**Agent Skills** はこの両方の強みを組み合わせたソリューションを提供してくれます。Agent Skills は [Gemini CLI](https://geminicli.com) の新機能で、エージェントへオンデマンドに専門能力を付与するために設計されました。エージェントツールと同様に動作します(実際、スキルはツール呼び出しによってアクティブ化されます)が、スキルを使うことで、エージェントが専門的なタスクを実行するために必要なプロンプトや補助ファイルにオンデマンドでアクセスでき、必要なタイミングでのみそれらをコンテキストに読み込むことができます。 完全な技術仕様は[公式ドキュメント](https://geminicli.com/docs/cli/skills/)に記載されていますが、本記事では使い始めるための基本を解説します。 ## スキルの基本構造 スキルとは、プロンプトと、必要に応じてドキュメントやスクリプトなどの補助ファイルをまとめたフォルダに過ぎません。 ```text my-skill/ ├── SKILL.md (必須) 指示とメタデータ ├── scripts/ (オプション) 実行可能なスクリプトやツール ├── references/ (オプション) 静的なドキュメントやリファレンス例 └── assets/ (オプション) テンプレートやバイナリリソース ``` `SKILL.md` はスキルの指示プロンプトを記述するファイルです。スキル名と説明文(description)を定義する簡潔なフロントマターを持ちますが、それ以外は通常の Markdown ファイルです。 ```text --- name: <一意のスキル名> description: <スキルが何を行うか、Gemini がいつ使用すべきかの説明> --- <エージェントの振る舞いやスキルの使用手順に関する指示> ``` プロジェクトにスキルを追加するには、`.gemini/skills` 配下にフォルダを作成します。たとえば上記の `my-skill` であれば、`.gemini/skills/my-skill` に配置します。Gemini CLI は、以下の優先順位に従ってスキルを自動的に探索します。 1. **Workspace** (`<プロジェクト名>/.gemini/skills`) 2. **User** (`~/.gemini/skills`) 3. **Extensions** (`~/.gemini/extensions/<拡張機能名>/skills`) ここで重要なのは、Gemini CLI の起動時点ではスキルの名前(name)と説明(description)のみが認識されているという点です。それ以外のファイルや詳細な指示はすべて、スキルがアクティブ化された際に初めて**オンデマンド**で読み込まれます。 それでは、実験分析ワークフローを改善するために、私が実際にどのようにスキルを活用しているかを見ていきましょう。 ## `experiment-analyst` スキル 私は `experiment-analyst` スキルを、Gemini CLI に実験の評価を依頼したときにアクティブ化されるよう設計しました。ディレクトリ構成は以下のようになっています。 ```text experiment-analyst/ ├── SKILL.md <-- 分析ガイドライン ├── references/ │ └── tenkai_db_schema.md <-- データベーススキーマ。エージェントが毎回自力で探索する必要を省く └── scripts/ ├── analyze_experiment.py <-- フロントエンドで行っている分析の一部を再現 ├── analyze_patterns.py <-- 洞察を抽出するために共通パターンを深掘り ├── get_experiment_config.py <-- 実験設定の詳細を取得 └── success_determinants.py <-- ツール呼び出しの分析と相関の算出 ``` ### 専門家のペルソナを定義する `SKILL.md` ファイルには、分析の手順や規律を定義します。エージェントに何をすべきかを教えつつも、単なる機械的で型通りの振る舞い(cookie-cutter)にならないようバランスを取っています。重要なポイントの一つは、エージェントが安易に結論へ飛びつくのを防ぎ、より堅実で客観的なペルソナを確立することです。私自身、エージェントのすべての主張を検証し、すべての結論を慎重に受け止めるようにしていますが、このバージョンによって、手動では発見に多大な労力を要したであろう興味深い洞察を数多く得ることができました。 ```text --- name: experiment-analyst description: Expertise in analysing Tenkai agent experiments. Use when asked to "analyse experiment X" to determine success factors, failure modes, and behavioural patterns. --- # Experiment Analyst ## Core Mandates 1. **Evidence-Based:** Never make claims without data. Cite specific Run IDs. 2. **Correlation ≠ Causation:** A tool might be correlated with failure (e.g., `read_file`) because it's used for recovery. Always investigate the *context* of usage. 3. **Comparative:** Always contrast the performance of alternatives. ``` 注: [SKILL.md の完全な内容](https://github.com/danicat/skills/blob/main/experiment-analyst/SKILL.md) はこちらから確認できます。 ### スキルのアセット 今後数週間にわたってこの話題を何度も取り上げることになりますが、本質的に**非決定論的(non-deterministic)**であるエージェントを扱う際、品質を担保する唯一の方法は、エージェントに**決定論的(deterministic)**なツールを提供することです。スキルはこの哲学に見事に合致しています。タスクの実行方法をエージェントの「推測」に委ねるのではなく、一貫した方法でタスクを実行するスクリプトを同梱できるからです。 実験分析スキルの作成にあたり、エージェントに自由な探索の余地を残しつつも、毎回車輪の再発明をしてほしくはありませんでした。そのため、以下のような事前パッケージ化されたスクリプトを同梱しています。 - `analyse_experiment.py`: フロントエンドにあるものと同様の実験サマリーを再現しつつ、シェルコマンドのツール呼び出しを適切にグルーピング - `analyse_patterns.py`: ツール利用パターンを特定するために、エージェントの会話履歴からサンプルを抽出 - `get_experiment_config.py`: 実験定義を取得し、エージェントが実験設定を正確に把握できるよう支援 - `success_determinants.py`: タスクの成功結果とツール呼び出しの相関関係を算出 また、エージェントがアドホックなクエリを実行することを選んだときのために、`references/tenkai_db_schema.md` でデータベーススキーマを提供しています。これにより、エージェントは毎回スキーマを探索し直す必要がなくなります(このスキーマは実行間で比較的安定しています)。 この構成が完璧だと言うつもりはありませんし、まだ洗練に膨大な時間を費やしたわけでもありません。しかし、この情報と事前パッケージ化されたスクリプトの組み合わせによって、私が普段エージェントに探索させたい質問の大部分を十分にカバーできています。 ## おわりに Agent Skills は、エージェントワークフローの設計における重要なパラダイムシフトを示しています。巨大なコンテキストプロンプト(`GEMINI.md` にすべてを詰め込むなど)から脱却し、モジュール化されたオンデマンドの機能へと移行することで、私たちは2つの課題を同時に解決できます。エージェントのコンテキストをクリーンに保ち(トークン消費を抑え)、汎用的な推論パフォーマンスを犠牲にすることなく、高度に専門化された専門知識を発揮させることができるのです。 私の場合、`experiment-analyst` スキルのおかげで、退屈で反復的な作業を半自動化されたフローへと昇華させることができました。求めている分析を実行するための十分な一貫性と柔軟性が手に入りました。現在、MCP サーバーを「プロンプトのデータベース」として使用していた従来のアプローチから移行し、ワークフローの他の部分もスキルへと順次アップグレードすることを検討しています。 コミュニティの皆さんがどのようなスキルを構築されるか、今から非常に楽しみです。ぜひご自身のワークフローを見つめ直してみてください。毎回繰り返し指示を与えている部分はありませんか?専任の専門家を必要としている場面はありませんか?それこそが、次に作成すべきスキルの候補です。 **追記:** 実践的なユースケースを掘り下げた [パート2: skill-creator で Agent Skills を構築する](https://danicat.dev/ja/posts/20260227-gemini-cli-skills-part-2/) を公開しました。あわせてご覧ください。 Happy coding! --- # 科学の力を借りてコーディングエージェントを改善する - **Markdown URL**: https://danicat.dev/ja/posts/improving-agentic-coding-with-science/index.md - **HTML URL**: https://danicat.dev/ja/posts/improving-agentic-coding-with-science/ - **Date**: 2026-01-21 - **Categories**: Agentic Coding - **Tags**: agents, ai, gemini-cli, golang, mcp, testing, vibe-coding - **Summary**: Go製フレームワークTenkaiとウェルチのt検定、フィッシャーの正確確率検定、スピアマン相関などの統計的検定を用いて、AIコーディングエージェントの設定を客観的に評価・比較する方法を解説。 ソフトウェア開発における[決定論から非決定論への移行](https://newsletter.pragmaticengineer.com/p/martin-fowler)は、現在私たちが業界全体として直面している最大の課題の一つです。 誰しも経験があるのではないでしょうか。AIエージェントと作業していて、「システムプロンプトにこれを追加したらどうなるだろう?」とか「この新しいMCPツールへのアクセスを与えてみたらどうだろう?」と思いつきます。 変更を加えて一度実行してみると、動いた。大成功です!自分は天才だと感じて、同僚やフォロワー、おばあちゃん、果ては飼い猫にまでシェアします。ところが1時間後、まったく同じタスクでもう一度実行してみると、無残にも大失敗するのです。その変更は本当に良かったのでしょうか?それとも最初の実行でたまたま運が良かっただけなのでしょうか?「今回はやり方を間違えたのかもしれない……」——再起動(オフにしてオンに)して、もう一度試してみると今度は動いた……本当にそうでしょうか? 実際のところ、LLMの仕組みそのものに内在するランダム性があるだけでなく、こうした結果をもたらす多くの交絡因子(confounding factors)が存在しています。 私たちはあまりにも長い間、決定論的な世界に生きてきた(本当にそうだったでしょうか?)ため、私たちソフトウェアエンジニアはこのレベルの不確実性に対処する心構えができていません。最初から教えられてきた通り、コンピューティングは[精密科学(exact science)](https://ja.wikipedia.org/wiki/%E7%B2%BE%E5%AF%86%E7%A7%91%E5%AD%A6)であり、厳密なものだと期待してしまうのです。 ## バイブス vs. 科学 私は[規律あるバイブコーディング](https://danicat.dev/ja/posts/20251206-taming-vibe-coding/)についてよく話しています。言い換えれば、高品質な結果を得るためには、バイブコーディングに手法(メソドロジー)を取り入れることが不可欠だということです。これは新しい問題ではありません。私たちは何十年もの間ソフトウェアの品質問題と向き合ってきましたが、参入障壁が下がり、コード生成のスピードが劇的に上がったことで、この問題がまったく新しい桁違いの規模へと引き上げられました。 人間のチームがより良い成果を出せるように考案された昔ながらの手法が、AIエージェント相手でも非常によく機能することを「発見」するたび、私はいつも驚かされます。でも実際、誰にとっても驚くことではないはずです。結局のところ、人間だって本質的に非決定論的な存在なのですから。AIは、すでによく知られたパターンを増幅しているに過ぎません。 それらの原則に基づいてかなりまともなワークフローを考案することはできましたが、「これが絶対に正しいやり方だ」と言い切れるレベルの確信には決して至りませんでした。それは、時折発生する失敗やリグレッション(予期せぬ劣化)が私の自信を削ぐからです。「この `GEMINI.md` は本当にGo開発者にとって究極のプロンプトなのだろうか?」「このタスクに対して最善のシステムプロンプトを実現できたのか?」「私のMCPツールは考えうる最高のAPIなのだろうか?」——毎日あまりにも多くの疑問に圧倒され、昨年公開したコンテンツのほとんどについては、断定的な表現を避け、すべてを**ケーススタディ**(「『これ』を試したら、『あれ』が起きた。以上」)として位置づけてきました。科学文献において、ケーススタディは[最もエビデンスレベルが低い](https://ja.wikipedia.org/wiki/%E6%A0%B9%E6%8B%A0%E3%81%AB%E5%9F%BA%E3%81%A5%E3%81%8F%E5%8C%BB%E7%99%82)ものの一つです。 技術ブログで見かける典型的な議論ではないことはよく分かっています。ここで少し寄り道をして、私の「過去の人生(前世)」についてお話しさせてください。ソフトウェアエンジニアのキャリアに落ち着く前、私は医学部に通っていました(卒業はしていませんが、その話はまた別の機会に)。医学や生物学の分野では、未知の起源を持つシステムから「真実」を抽出する必要があるため(基本的に世界をリバースエンジニアリングしているわけです。なんて面白いんでしょう!)、研究者たちは実験や、バイアス、ノイズ、ランダム性への対処にはるかに慣れています。研究データをそのまま提示するだけでは不十分で、潜在的な交絡や偏りを取り除くために統計的に検証しなければなりません。サンプルサイズ、アルファ、p値、Studentのt検定……学生時代にテストや実験で扱っていた頃はこれらの用語が大嫌いでしたが、今日これほど役立つことになるとは夢にも思っていませんでした。 もちろん、これは生物学に限った話ではありません。エンジニアリングでも研究の際に統計的手法を適用することはよくありますが、私たちの分野では他の分野ほど広く普及していないように感じます。例えば、私がレコメンデーションシステムに携わっていた頃、機械学習アルゴリズムを最適化するためにA/Bテストは欠かせない業務でした。UXリサーチャーも、どのインターフェースが優れているかを判断するために広範なA/Bテストを実施しています。 コーディングエージェントの実験からあて推量を排除する方法を見つけたいと考えた私は、データを収集して統計分析を行う実験フレームワークを構築するのが最善だと気づきました。これによって、「なんとなく動いている気がする」から「(95%の信頼水準で)確実に機能していると**分かっている**」という境地へと前進できるようになったのです。 ## Tenkaiのご紹介:エージェント実験フレームワーク この課題を解決するために、私は **Tenkai**(日本語の「展開」に由来し、アニメ好きのためのささやかなイースターエッグでもあります)を構築しました。これは、コーディングエージェントのさまざまな設定や構成を統計的な厳密さをもって評価・検証するために設計された Go 製フレームワークです。 ![Tenkaiの実験ビュー](image-3.png "実験実行中のTenkaiインターフェース") これをAIエージェントのための実験室(ラボ)だと考えてみてください。プロンプトを一度だけ実行して神頼みする代わりに、Tenkai を使えば同じタスクを何度も繰り返し実行し(サンプルサイズとなる N 回の反復)、複数の選択肢(異なる設定のバリエーション)を統計的検定を用いて相互に比較する実験を行えます。 例えば、Alternative A をデフォルトの設定(実験の「対照群 / コントロール」)、Alternative B を試してみたい新しいシステムプロンプトだとしましょう。実験では両方を N 回ずつ実行し、B の方が統計的に有意に高速か、高効率か、あるいは高精度かを示すレポートを出力します。その差異が単なる偶然やノイズによるものである場合、フレームワークは有意とは判定しないため、結果を安心して無視できます。 ### 仕組み Tenkaiのワークフローは次の通りです。 1. **シナリオを定義する:** シナリオは標準化されたコーディングタスクです(例:「このGoパッケージのバグを修正する」「新しいReactコンポーネントを実装する」)。これには「コンパイルできるか?」「テストに合格するか?」といった検証ルールが含まれます。シナリオが成功したと見なされるには、指定したすべての検証基準に合格する必要があります。 ![シナリオのリストビュー](image-2.png "シナリオはコーディングタスクです。実験に1つ以上のシナリオを追加でき、すべて同じ基準で検証されます") 2. **テンプレートを作成する:** 何をテストしたいかを定義します。例えば、「Alternative A(コントロール)」をGemini 2.5 Flashモデルとし、「Alternative B」をMCPサーバーを設定したGemini 2.5 Flashとすることができます。同じ実験テンプレート内に最大10個の選択肢を設定でき、それらはすべてコントロールと比較されます。 ![代替案エディタ](image-1.png "代替案エディタでは、エージェントコマンド、コマンドライン引数(flags)、システムプロンプト、GEMINI.md、settings.jsonをオーバーライドできます") 3. **実験を実行する:** Tenkaiは各選択肢に対して各シナリオを複数回実行します。実行は一時的なワークスペースに隔離され、タイムアウトが管理され、ツール呼び出しからシェル出力に至るまですべてのイベントが記録されます。また、並行度(concurrency)を指定してタスクを並列実行できるため、結果を長時間待たずに済みます。 ### 統計的厳密さ(科学のアプローチ) 現在フレームワークに組み込まれている統計的検定は以下の通りです。 * **ウェルチのt検定(Welch's t-test)**:実行時間、トークン消費量、リンターの問題数などの連続指標に使用します。標準のt検定ではなくウェルチのt検定を採用しているのは、グループ間の等分散性を前提としないためです。これは、パフォーマンスプロファイルが大きく異なる可能性のあるモデル同士を比較する際に不可欠です。 * **フィッシャーの正確確率検定(Fisher's Exact Test)**:成功率の比較に使用します。10回や20回といった少数のサンプルサイズで作業する場合、標準的なカイ二乗検定は不正確になる可能性があります。フィッシャーの検定は、80%の成功率が60%よりも本当に優れているのか、それとも単なる幸運の連続なのかを判断するための信頼できるp値を与えてくれます。 * **マン・ホイットニーのU検定(Mann-Whitney U Test)**:ノンパラメトリック検定の主力ツールです。成功した実行と失敗した実行の間で、ツール呼び出しの「分布」を比較するために使用します。ツール呼び出し回数は正規分布に従わない(ゼロが非常に多い!)ため、マン・ホイットニーのU検定は、特定のツールが「勝利した(成功した)」実行において有意に多く使用されているかどうかを特定するのに役立ちます。 * **スピアマンの順位相関係数(Spearman's Rho)**:「成功の決定要因」を特定するために使用します。特定のツール呼び出しと、実行時間やトークン消費量などの指標との間の相関を計算することで、Tenkaiは新しいMCPツールが真に**成功要因(Success Driver)**となっているのか、それともコストを膨らませるだけの邪魔者なのかを教えてくれます。 ## リアルタイムのインサイトとダッシュボード 実験の進行中、エージェントの思考プロセスをリアルタイムに観察し、どのツールを呼び出しているかを確認し、どこでスタックしているかを特定できます。これは、数十の同時並行実行を通じてエージェントの「頭の中を覗き込む」ことができるような体験です。 ダッシュボードの最も強力な機能の一つは、3つの異なる「レンズ」を使用して、分析をその場でフィルタリングできることです。 * **すべての実行(All Runs):** 生の真実。すべての壊滅的なタイムアウトやシステムエラーを含みます。これは、システム全体の**信頼性(reliability)**を測る主要な指標です。 * **完了のみ(Completed Only):** 終端状態(成功または検証失敗)に達した実行のみをフィルタリングします。外部のタイムアウトによるノイズを取り除き、リンターの問題や実行時間などの**品質(quality)**指標を分析する場合に使用します。 * **成功のみ(Successful Only):** 「ゴールドスタンダード」ビュー。成功した実行だけを見ることで、なぜ成功できたのかを推論し始めることができます。ここでは、成功と強い相関を持つツールを特定するために、**スピアマンの順位相関係数(Spearman's Rho)**と**マン・ホイットニーのU検定**のp値を算出します。 ## 自身の実験から得られた初期結果 私は Tenkai を使用して、[godoctor](https://github.com/danicat/godoctor) を理想的な Go 専用 MCP サーバーへと磨き上げてきました。私の仮説は、モデルに特化したツールを提供することで、コーディングタスクの遂行がより効果的になるというものです。例えば、モデルがクライアントライブラリの API を把握するためにいつドキュメントを読み込むかをモデル自身に判断させるのではなく、モデルが `go get` を呼び出すたびにドキュメントを自動返却することで、ドキュメントを「強制的に」読み込ませています。これにより、API のハルシネーション(幻覚)や、モデルが間違ったバージョンのパッケージを取得したと思い込んで `go get` と `go mod` の間で格闘し続ける「依存関係の地獄ループ」を大幅に防ぐことができました。 初期の結果では、モデルのバージョンを最新世代に固定すると、この差が縮まることも示されています。—— Gemini CLI はデフォルトで「auto」モードで起動し、Gemini 2.5 と Gemini 3(Flash および Pro バージョンの両方)のどちらを呼び出すかを自動的に判断します。バージョンを Gemini 3 Pro(モデルID `gemini-3-pro-preview`)に固定すると、モデルははるかに賢くなり、そうした競合シナリオにおいて(コマンドラインで `go doc` を起動するなどして)自らドキュメントを調達することが多くなり、godoctor のツールセットの影響力は相対的に小さくなります。 ![デフォルト設定と、godoctor(コアツールあり・なし)の2つの設定を比較した実験サマリーテーブル](image.png "godoctorをコアツールと並行して有効にしても大きな影響はなく、トークンを浪費する可能性もありましたが(p < 0.1)、コアツールを無効にしてgodoctorの使用を強制した後は、タスクの平均所要時間が30%以上短縮されました(p < 0.01)") また、ツールの定着・採択(Tool Adoption)に関しても多くの課題がありました。モデルはデフォルトで、トレーニングで使用された組み込みツールを好んで使用する強い傾向があります。「より賢い」ツールを提供しても、モデルは慣れ親しんだコンフォートゾーンから抜け出すのに苦労します。私はこれに関して多くの実験設計で失敗を重ねましたが、最終的に私の実験プロセス自体に根本的な欠陥があったことに気づきました。**ツールの採択(adoption)**と**ツールの有効性(efficacy)**は直交する概念です。同じ実験で両方を同時にテストしようとしたため、有効な結果が得られなかったのです。そこで、モデルの組み込みツールへのアクセスを遮断し、ツールの有効性のみをテストするように方向転換しました。これにより、godoctor ツールの真の実力(良し悪し)について、ようやく確かなシグナルが得られ始めました。 godoctor がどのように形作られてきたか、そして現在の API にどのようにたどり着いたかに興味がある方は、[2月1日の FOSDEM](https://fosdem.org/2026/schedule/event/3BD3Z9-making_of_godoctor_an_mcp_server_for_go_development/) で登壇してお話しします。もちろん、参加できない方もご安心ください。今後数週間かけて、ブログでもさらに詳しく書いていく予定です。 ## まとめ 私たちはソフトウェア開発における転換点(ティッピングポイント)に立っています。1行1行すべてのコードを人間が書く世界から、インテリジェンスを**オーケストレーション**する世界へと移行しつつあるのです。 しかし、オーケストレーションには測定が不可欠です。測定できないものを改善することはできません。コードの書き手からインテリジェンスのオーケストレーターへの移行は、仕事が減ることを意味するのではなく、**異なる種類の仕事**をするようになることを意味します。私たちの主たる責務は、もはやシンタックス(構文)だけではなく、**コンテキスト、ツール、そしてガードレール**なのです。 加えた変更の影響を厳密に測定しなければ、それはエンジニアリングではなく、単なるギャンブルに過ぎません。コーディングエージェントに対してエビデンスに基づくアプローチを取ることで、単に「動いたときにカッコいい」だけでなく、ビジネスの基盤として信頼できるシステムをようやく構築できるようになるのです。 コードを確認したり、ご自身で実験を実行してみたい方は、[GitHub 上の Tenkai リポジトリ](https://github.com/danicat/tenkai)をご覧ください。 実験を楽しみましょう! o/ Dani =^.^= --- # 2025 年の振り返り:Google 入社とバイブコーディングを手懐けた 1 年 - **Markdown URL**: https://danicat.dev/ja/posts/my-2025-year-in-review/index.md - **HTML URL**: https://danicat.dev/ja/posts/my-2025-year-in-review/ - **Date**: 2026-01-13 - **Categories**: Perspectives - **Tags**: career, community, reflection, vibe-coding - **Summary**: 2025年の振り返り:Google DevRelへの参加、規律あるバイブコーディングの確立、GoDoctorやSpeedgrapherなどMCPサーバーの開発、そして2026年への展望。 2025 年がバックミラーの向こうへと去りゆく中、私は(当然のように)コーヒーを片手に腰を下ろし、この 1 年に起きた目まぐるしい出来事を整理しようとしています。今年を表す言葉を 1 つ選ぶとしたら、それは **加速(acceleration)** でしょう。 業界全体が急激に進化を遂げただけでなく、私自身の個人としての歩みも、プロフェッショナルとしての速度も、予想をはるかに超える勢いでギアが上がりました。Google への入社から、AI 時代におけるエンジニアのあり方の再定義まで、2025 年はまさに根本的な「リファクタリング」の 1 年でした。 ## Google での新たなチャプター 4 月、私は Google の Developer Relations チームに加わりました。何十年も抱き続けてきた夢でしたが、現実は期待以上に素晴らしいものでした。もちろん、めまぐるしい日々ではありました。イギリスやヨーロッパ中を巡り、[ブラジル](https://danicat.dev/ja/posts/20251029-six-months-at-google/) の開発者コミュニティと再会する時間も作ることができました。 しかし、移動した物理的な距離以上に大きかったのは、知的な探求の旅でした。私は Google Cloud エコシステムへ真っ先に飛び込み、[Vertex AI](https://danicat.dev/ja/posts/20250605-vertex-ai-sdk-python/)、[MCP](https://danicat.dev/ja/posts/20250817-hello-mcp-world/)、そして [Agent Development Kit (ADK)](https://danicat.dev/ja/posts/20251020-diagnostic-agent-with-adk/) に深く没頭し、実際に手を動かしながら実践経験を重ねました。それはまるでソフトウェアの未来を一気に駆け抜ける集中講座のようで、その一瞬一瞬が本当に刺激的で楽しい時間でした。 ## バイブコーディングの年 2024 年が AI と「遊ぶ」年だったとすれば、2025 年はそれを **本格的に受け入れる(embrace する)** 年でした。私は懐疑派から、[「バイブコーディング」](https://danicat.dev/ja/posts/20251206-taming-vibe-coding/) の熱烈なエバンジェリストへと転身しました。ただし、皆さんが思い浮かべる [**あの**](https://x.com/karpathy/status/1886192184808149383?lang=en) バイブコーディングではありません。 「コードの存在すら忘れてしまう」のは罠だと気づいたからです。むしろ、エンジニアリングの規律 — 明確な要件定義、コンテキスト管理、厳格なテスト — こそがこれまで以上に *不可欠* であると学びました。だからこそ私は、ひと味違ったバイブコーディング、すなわち「規律あるバイブコーディング」を提唱しています。「バイブコーディングなんていう俗語を使うべきではない」と主張する人もいるかもしれませんが、仕方ありません。この言葉、あまりにキャッチーなのですから! さらに私は、開発者が「パイロット」として舵を取り、[Gemini CLI](https://geminicli.com/) や [Jules](https://jules.google/) といった AI エージェント部隊を指揮する [モダンな開発者ワークフロー](https://danicat.dev/ja/posts/20250714-developer-workflow/) を確立しました。 このシフトは単なる思想にとどまらず、極めて実践的なものでした。自分自身の課題を解決するために、まったく新しいツールをいくつも「バイブコーディング」しました: * **[GoDoctor](https://danicat.dev/ja/posts/20250729-how-to-build-an-mcp-server-with-gemini-cli-and-go/):** AI にイディオマティックで高品質な Go コードの書き方を教える MCP サーバー。 * **[Speedgrapher](https://danicat.dev/ja/posts/20250805-introducing-speedgrapher/):** テクニカルライティングにおける退屈な定型作業を自動化するツールキット。 * **[AIDA](https://danicat.dev/ja/posts/20251031-building-aida/):** 『スタートレック』の宇宙船コンピューターという夢を具現化した、自然言語で対話できる診断エージェント。 ## Hello, MCP world! ツールといえば、2025 年は **Model Context Protocol (MCP)** がすべてを一変させた年でした。これを AI における「USB-C モーメント」と呼ぶ人もいますが、私に言わせれば「AI にとっての HTTP/REST」です。私たちは過去 20 年間にわたり、システムをプログラムから呼び出せるようにするための自動化を築き上げてきました。そしてエンジニアリングの次なるサイクルは、レガシーシステムを改修し、エージェントから自律的に呼び出せる(agentic)新たなシステムを構築することです。ついに、AI モデルを現実世界と接続するための標準プロトコルを手に入れたのです。 私はこの技術に大きく賭け、[GopherCon UK での基調講演](https://danicat.dev/ja/posts/20250817-hello-mcp-world/) に登壇し、自作サーバーの構築方法について精力的に記事を執筆しました。コミュニティがこの標準規格を中心に急速に結束していく様子を目の当たりにし、自分の確信が正しかったと強く実感しました。私たちはもはや単にチャットボットとおしゃべりしているだけではありません。自律的にアクションを *実行する* システムを構築しているのです。 ## 個人的なリファクタリング コードとカンファレンスに明け暮れるだけではありませんでした。自分自身の「物理ハードウェア」をリファクタリングする時間もしっかり確保しました。嬉しいことに、今年は **20 kg 以上** の減量を達成できたのです!大人になって以来、今が間違いなく人生最高のコンディションです。大好きな仕事に打ち込める恵まれた環境に身を置くことこそが、何よりの健康法なのだと実感しています。 また、「時間」という概念そのものについても深く思索を巡らせました。[なぜ年齢を重ねると時間が加速するように感じるのか](https://danicat.dev/ja/posts/20250811-a-model-for-perceived-time/) に関する探求記事は、読者の皆さんから大きな反響をいただきました。シャワーを浴びているときのふとした思いつきを AI で掘り下げてみたのがきっかけでした。「AI は人間の創造性を損なう」という意見も耳にしますが、私自身は、普段なら時間や気力、根気が足りなくて深掘りできないようなアイデアや思考実験を AI と一緒に探求するのが大好きです。私にとって AI の真の力とは、「一人では決して手をつけなかったであろうこと」を実現可能にしてくれる点にあります(残念ながら皿洗いはまだやってくれませんが、アニメ風のロボットメイドが発売されたら真っ先に並んで買うつもりです!)。 ## 2026 年に向けて さて、次はどこへ向かうのでしょうか? 2025 年が **導入(adoption)** — 各種ツールを学び、日々のワークフローへ組み込む — の 1 年だったとすれば、2026 年は **カイゼン(改善 / kaizen)** の年になると確信しています。これらのツールがどう振る舞い、何が有効で何がそうでないのか、学ぶべきことはまだまだ山ほどあります。私たちはエンジニアコミュニティ全体で実験を繰り返し、新しい試みに挑戦し、そのプロセスと知見をドキュメントに残してきました。今こそ、そうしたすべての学びを集約し、単なるコード生成から生活をより豊かにする完全自律型エージェントに至るまで、この技術を活用する成熟度を一段上のステージへと引き上げる時です。 さらに私は、**パーソナライズされたソフトウェア(Personalized Software)** の領域を一段と深く掘り下げていく予定です。自分専用の特注ツールを作るための参入障壁は、完全に崩壊しました。何か課題に直面したとき、「既存のアプリを探す」のではなく「自分で作ってしまう」時代が到来したのです。Speedgrapher のように自分のニーズに完璧に合わせたツールをさらに生み出すと同時に、皆さんが同じように自作できるようノウハウを発信し、この最前線を開拓し続けたいと考えています。 また、眠っていたゲーム開発者としての情熱を解き放ち、少なくとも 1 回は Game Jam に参加したいと思っています。今年は挑戦してみたものの、仕事の立ち上げやキャッチアップに追われてゲームを完成させることができませんでした。今はずいぶん足場が固まって落ち着いてきたので、少し背伸びをすれば、1 本か 2 本のゲームを世に送り出せるはずです。 この素晴らしい旅を一緒に歩んでくださり、心から感謝しています。ブログ記事を読んでくださった方、講演を見てくださった方、一緒にバイブコーディングを楽しんでくださった皆さん — 本当にありがとうございました! 新たなバイブス、洗練されたコード、そしてもっとたくさんの猫の写真にあふれた 2026 年を楽しみにしています。 o/ Dani =^.^= --- # Vibe Coding を手懐ける:エンジニアのための実践ガイド - **Markdown URL**: https://danicat.dev/ja/posts/taming-vibe-coding/index.md - **HTML URL**: https://danicat.dev/ja/posts/taming-vibe-coding/ - **Date**: 2025-12-06 - **Categories**: Agentic Coding - **Tags**: agile, architecture, software-engineering, tdd, vibe-coding - **Summary**: バイブコーディングにソフトウェア工学の規律を適用する実践ガイド。プロンプトテンプレート、コンテキスト設計、2x2ツール選定、適応型TDDを解説。 一年の終わりは、自分が成し遂げたこと、そしてやっておけばよかったと悔やまれることを振り返る絶好の時期です。今年(2025年)は私にとって極めて密度の濃い一年でした。4月に Google に入社し、AI の新時代に向けて自分自身を「リファクタリング」するノンストップの挑戦が始まったのです。一年を締めくくろうとしている今、その努力は十分に報われたと胸を張って言えます — 私は確実に、より良いエンジニアへと成長しました。 本稿では、私自身の「バイブコーディング(vibe coding)」に対する理解がどのように深まり、どんな教訓を得たのかを共有します。vibe coding は一般的に、非エンジニアが自然言語を使ってソフトウェアを構築するための手法として語られがちです。しかし私の目的は、そこに**エンジニアリングの規律**を融合させることで、より高品質で再現性の高い成果をいかに引き出せるかを示すことにあります。 もちろん、「[バイブスに身を任せ、コードが存在することすら忘れてしまう](https://x.com/karpathy/status/1886192184808149383?lang=en)」という提唱当初の定義は百も承知ですし、私がこれから語る考え方のいくつかは、その定義と真っ向から衝突するかもしれません。私自身は「コードの存在を忘れる」ことなど決してないからです。とはいえ、この言葉はすでに「AI 支援コーディング」全般を指す代名詞へと定着しつつあります。そこで本稿では、vibe coding を「**エンジニア本人ではなく、LLM がコードの大半を記述するコーディング手法**」と定義して議論を進めることにします。 ## 動機:なぜバイブコーディングなのか? 具体的な実践ノウハウに深く切り込む前に、私のバックグラウンドを少し共有しておきましょう。どのような立ち位置からこの結論に至ったのか、文脈を理解してもらうためです。 私は 20 年以上にわたりソフトウェアエンジニアとして働き、「何が良いコードを構成するのか」についての確固たる感覚を培ってきました。「小手先の賢さ(cleverness)」よりも可読性や保守性を最優先すること、オーバーエンジニアリングを避けること、そしてバーティカルスライス(thin slices)とタイトなフィードバックループを何よりも重視することを学んできたのです。 しかし、シニアエンジニアからプリンシパルエンジニアへとキャリアを進めるにつれ、私の主業務は「自らコードを書くこと」から「チーム全体の生産性と成果(effectiveness)を最大化すること」へとシフトしていきました。エピックを起票し、スコープを交渉し、チームのアウトプット全体を監督する仕事です。これは多くのベテランエンジニアが直面するアイデンティティの危機でもあります。「Pull Request に自分の名前が載らなくなっても、自分はまだエンジニアと呼べるのだろうか?」と。責任が重くなるにつれて連日ミーティングに追われ、「本来のエンジニアリング」から遠ざかっているような焦燥感を覚えるようになります。 この葛藤は、この業界にいる誰もが遅かれ早かれ直面するものだと思います。そして必然的に、次のような問いへと突き当たります —「エンジニアであることとは、単にコードを書くことなのか? それとも、もっと本質的な何かなのか?」 正直に告白すると、私の最初の vibe coding 体験は散々なものでした。初期の ChatGPT が生成するコードはお世辞にも良いとは言えず、すぐに使うのをやめてしまいました。再び試してみようと思い直したのは、2024 年半ばのことです。基盤モデルは驚くべき進化を遂げていました。AI が私の思いつきもしなかった実装方針を提案し、しかもそれが客観的に見て私自身のアプローチよりも明らかに優れていたのです。このとき初めて、生成 AI(GenAI)が真に実用的なツールになったと実感しました。 私は GenAI をプロトタイプ作成や「ラバーダック・デバッグ」の相手としてツールボックスに加えました。使えば使うほど、その有用性に魅了されていきました。逆に、手作業でゼロからコードを書くことへのワクワク感は次第に薄れていました。いくら API を何本も書いてきたところで、その新鮮さはいつか失われます。私たちは往々にして、新しい価値を創造しているのではなく、過去のパターンの再生産を繰り返しているにすぎないからです。 そんなふうに自分のキャリアの選択に疑問を抱き始めていた矢先、Google への転職という転機が訪れました。 Gemini や AI エージェントについて対外的に発信するミッションを背負ったことで、私はスキルセットの本格的なアップデートに乗り出しました。LLM の基礎から [Gemini CLI](https://geminicli.com/)、そして [Jules](https://jules.google/) に至るまで徹底的に掘り下げたのです。ほんの数ヶ月のうちに、日々のコーディングで AI を日常的に使いこなすようになっていました……しかし何より大きかった変化は、**「コードを書く楽しさ」が再び甦ってきたこと**でした! 私にとって最大のブレイクスルーは、「思考と同じスピードでコードをキーボードで打ち込むことはできなくても、**頭に浮かんだアイデアをそのまま文字にして伝えることならできる**」という気づきでした。vibe coding を行うとき、私は AI モデルを「自分の手の代行役」として活用しています。タイピングという物理的な実装作業をモデルに委任し、自分自身は本質的なソリューション設計に全神経を集中させるのです。 ## プロンプティングスキルを磨く vibe coding の世界で最初に極めるべきスキルは、プロンプティング(prompting)です。ここ数ヶ月、親友のように気さくに雑談するスタイルから、命令口調で指図したり厳しく問い詰めたりするスタイルまで、ありとあらゆるアプローチを試してきました。そして — 当然といえば当然ですが — 最も高い成果を上げたのは、**明確でプロフェッショナルなトーンを一貫して維持すること**でした。 LLM は本質的に非決定論的(non-deterministic)です。指示に曖昧さが含まれていると、この非決定性は一気に跳ね上がります。クリエイティブな解決策を模索させるためにあえて意図的な曖昧さを残すケースもありますが、日常的な開発の大半においては、曖昧さは可能な限り排除すべきです。 プロフェッショナルなトーンを保つことは、LLM 側にも同様の質とトーンを「返答として引き出す」効果があります。こちらが雑でくだけた指示を出せば、LLM もその雑さを忠実に真似てしまいます。スラング混じりの API ドキュメントを生成させたいのでもない限り、常に正確でブレのない指示を心がけましょう。 「LLM を擬人化しすぎだ」と批判されることもありますが、私はあえて繰り返します。モデルは人類が生み出した言語コーパスによって訓練されているため、**生身の同僚に対して通用する優れたコミュニケーションスキルは、そのまま LLM に対しても有効**なのです。この事実は、プロンプトテンプレートの構造を見ればよりはっきりと理解できます。 ### 優れたアプローチ:プロンプトテンプレート 私が実践しているプロンプト作成手法は、アジャイル開発のカンバンボードでチケット(ストーリー)を起票するプロセスと驚くほどそっくりです。 誰しも、雑に書かれたユーザーストーリーに頭を抱えた経験があるはずです。「API を更新する」というタイトルだけのチケットを意気揚々と開いてみたら、説明欄が真っ白だった……。ログもなければ、コンテキストも、対象のソースコードへのリンクすらありません。コードを書くどころか、関係者を捕まえて仕様を確認する作業に何時間も浪費させられます。 ![不十分に書かれたチケットが並ぶアジャイルボード](agile-board.png "こんな風にストーリーを書くチームで働いたことはありませんか?") こうした事態が起きるのは、起票者が問題の背景を「誰が見ても自明だ」と思い込んでいるからです。しかし 24 時間も経てば、その「自明だったはずの文脈」は霧散し、起票者本人ですら意図を解読できない謎のメモだけが残されます。 だからこそ、私の GitHub で最も古くから公開されている資産の一つが、この [チケットテンプレート](https://gist.github.com/danicat/854de24dd88d57c34281df7a9cc1b215) の Gist なのです。これは、以下の 4 つの要素によって記述の明確さを強制します。 ```markdown - Context - To dos - Not to dos (optional) - Acceptance Criteria ``` * **Context**:実装の「理由(Why)」を明確にし、関連ドキュメントやリソース(アーティファクト)へのリンクを提供します。 * **To dos**:大枠のタスクをリストアップします。 * **Not to dos**:スコープの境界線を厳密に定義します(ネガティブな制約は、曖昧さを削ぎ落とす上で極めて強力な武器になります)。 * **Acceptance Criteria**:タスクの「成功条件」を定義します。 このテンプレートを埋めた時点で、エンジニアリングとしての「思考」は大部分が完了しています。あとは実装するだけです。そしてこれこそが、まさに LLM が真価を発揮する領域です。コンテキストと To dos(および Not to dos)を与え、「受け入れ基準を満たすコードを生成して行間を埋める」ことをモデルに委任するわけです。 例えば、REST API に新しいエンドポイントを追加するためのプロンプト(チケット)は、次のようになります。 ```markdown Implement /list endpoint to list all items of the collection to enable item selection on the frontend. TO DO: - /list endpoint returns the list of resources - Endpoint should implement token based auth - Endpoint should support pagination - Tests for happy path and common failure scenarios NOT TO DO: - other endpoints, they will be implemented in a future step Acceptance Criteria - GET /list returns successful response (2xx) - Run `go test ./...` and tests pass ``` これは簡略化した一例ですが、核心は極めて明快です。人間の開発チームに秩序と健全さをもたらすチケットテンプレートの構造こそが、LLM へのプロンプティングにとっても完璧なフォーマットなのです。 ### さらに優れたアプローチ:コンテキストエンジニアリング 上記のテンプレートは日常の大半のケースで機能しますが、Tool Calling(ツール呼び出し)を通じて外部 URL や外部データソースから情報を取得させようとする場合、予期せぬ結果を招くことがあります。問題の本質は、ツールに依存している場合、「そのツールを実際に呼び出すかどうか」の判断が LLM の裁量に委ねられている点にあります。一部のモデルは過度に「自信過剰」であり、外部ソースを参照するよりも自身の事前学習知識に頼ろうとします。あたかも「こんなの実装したこと何千回もあるんだから、ドキュメントなんて読む必要ないだろ?」と豪語する人間のエンジニアと同じです。 もう一つの典型的な落とし穴は、モデルが実際にツールを実行する代わりに、ツールの実行結果をハルシネーション(幻覚)で捏造してしまうケースです。モデルの振る舞いそのものが原因である場合、応答の品質を劇的に高めるアプローチは 2 つあります。**コンテキストエンジニアリング(Context Engineering)** と **システム指示(System Instructions)のチューニング** です(よく考えれば、後者も会話チェーンの基底レイヤーにおけるコンテキストエンジニアリングの一形態に他なりません)。 コンテキストエンジニアリングとは、実際のリクエストをモデルに投げる前に、必要なすべての情報(少なくとも必要だと分かっている重要パーツ)をあらかじめコンテキスト内に流し込んで準備(プライミング)しておく手法です。例えば、Go 向けの Agent Development Kit(ADK)を使ってエージェントを開発する場合を考えてみましょう。通常なら次のようなプロンプトを書くかもしれません。 ```markdown Write a diagnostic agent using ADK for Go. The diagnostic agent is called AIDA and it uses Osquery to query system information. The goal is to help the user investigate problems on the system the agent is running on. Before starting the implementation, read the reference documents. References: - https://osquery.io - https://github.com/google/adk-go TODO: - Implement a root_agent called AIDA - Implement a tool called run_osquery to send queries to osquery using osqueryi - Configure the root_agent to use run_osquery to handle user requests - If the user says hi, greet the user with the phrase "What is the nature of the diagnostic emergency?" Acceptance Criteria - Upon receiving hi, hello or similar, the agent greets the user with the correct phrase - If asked for a battery health check, the agent should report the battery percentage and current status (e.g. charging or discharging) ``` これは悪くないプロンプトですが、いささか情報量が多すぎます。使用しているコーディングエージェントとその日の巡り合わせ次第では、モデルが自律的に調査を行い、適切な SDK を見つけ出し、診断エージェントを一発で正常に構築してくれるかもしれません。しかし運が悪ければ、モデルは「ADK」を「Agent Development Kit」ではなく「Android Development Kit」だと勘違いしたり、存在しない架空の API をでっち上げたりして、正解に辿り着くまでに(開発者が横から何度も軌道修正を入れながら)貴重な時間とトークンを浪費することになります。 最初からプロジェクトで ADK for Go を使うと決まっているのですから、まずはエージェントにパッケージの公式ドキュメントを直接読み込ませることで、コンテキストを確実にプライミングできます。 ```markdown Initialize a go module called "aida" with "go mod init" and retrieve the package github.com/google/adk-go with "go get" Read the documentation for the package github.com/google/adk-go using the "go doc" command. ``` 本題のタスクを与える前にこの手順を踏んでおくことで、モデルには ADK for Go を正確に扱うための前提知識が注入され、不毛な試行錯誤や終わりのない Web 検索を丸ごとスキップできます。タスクの成否を分ける二大要素は、**「正確なドキュメント」** と **「具体的なコード例」** です。この両方をあらかじめコンテキストに与えておけば、モデルを放任して迷走させるよりも、はるかに適切に振る舞ってくれます。 ### 百聞は一見にしかず:画像による指示 要求事項をテキストだけで表現し尽くすのが難しいケースもあります。[AIDA](https://danicat.dev/ja/posts/20251031-building-aida/) に取り組んでいた際、私は「レトロサイバーパンクでキュートなアニメ」風の独特な UI 美学を求めていました。これを言葉だけで事細かに説明しようとするのは骨が折れますが、**「実物を見せる」** アプローチなら一瞬で伝わります。私は気に入った UI のスクリーンショットを 1 枚用意し、それを再現するよう Gemini CLI に指示しました。 Gemini 2.5 Flash のような最新モデルはマルチモーダルネイティブであるため、画像の内容を高い精度で「理解」できます。プロンプト内で画像ファイルを参照し、「`UI を更新して、この @image.png のようなデザインに仕上げてください`」と伝えるだけで済むのです。 なお、この `@` によるファイル参照記法はツール依存(この例では Gemini CLI の機能)ですが、プロンプト内にリソースやファイルを注入するための一般的なデファクトスタンダードとなりつつあります。いわば「メールの添付ファイル」のような感覚で捉えてください。 私はこの手法を**「スケッチ駆動開発(Sketch-Driven Development)」**と呼ぶのが気に入っています。Draw.io や Excalidraw などの作図ツールを立ち上げ、欲しい画面構成をラフスケッチとして描いて渡すことが多いからです。以下の画像は、AIDA のインターフェース刷新時に実際に使用したラフスケッチです。 ![AIDAのレイアウトスケッチ](aida-sketch-layout.png "AIDA のレイアウトスケッチ") そして、このスケッチをもとにモデルが生成した実際のインターフェースがこちらです。 ![AIが生成したインターフェース](aida-generated-interface.png "AI が生成したインターフェース") 単にスケッチを渡すだけでなく、画像の上に直接「注釈(アノテーション)」を書き込んで、各パーツの役割を明確に指示するのも極めて効果的なテクニックです。例えば下図のように黒い枠線だけだと「どれが入力フォームで、どれがボタンなのか」をモデルが見分けるのは困難です。 ![名前入力用のテキストボックスと確認ボタンを備えたシンプルなインターフェース](simple-interface-annotated.png "名前入力欄と送信ボタンを赤字で注釈したインターフェース設計図") これに対応するプロンプトは次のようになります。 ```markdown Create a UI for this application using @image.png as reference. The UI elements are in black, and in red the annotations explaining the UI elements. Follow the best practices for organising frontend code with FastAPI. The backend code should be updated to serve this UI on "/" ``` このアプローチの応用範囲は無限大です。Web サイトのレイアウト崩れを直したいなら、スクリーンショットを撮って修正箇所を赤ペンでマークし、LLM に投げるだけで修正コードを出力してくれます。 さらに、Gemini CLI 向けの Nano Banana 拡張機能などを活用すれば、開発ワークフローの中から直接画像アセットを生成・編集し、モデルにとってより理想的な参照画像を即座に用意することも可能です。より本格的な UI リデザインを行いたいなら、Nano Banana Pro を含む Gemini ファミリーを活用した Google の [Stitch](https://stitch.withgoogle.com/) などのツールが、リッチなインターフェース設計環境を提供してくれます。 ## 適切なツールを選択する プロンプトの技術を磨くことは、戦いの半分にすぎません。残りの半分は、**「そのプロンプトをどのツールに投げるか」** の見極めです。今日、JavaScript フレームワークの増加スピードを上回るものが世界に一つだけあるとすれば、それは AI エージェントの数でしょう。ツールエコシステムが日進月歩で拡大し続ける中、最適なアシスタントを選ぶための「メンタルモデル」を持っておくことが極めて重要になります。 私は AI エージェントを、「操縦士(パイロット)の視点」から分類するのが分かりやすいと考えています。自らが完全に主導権を握り、オートコンプリートの提案を受け取りながら進めるのか? エージェントと対話しながら共同でコードを編集していくのか? それとも、指示だけを渡してバックグラウンドで完全自律実行させるのか? 人間が操縦席に座り、エージェントの手綱を握り続けるスタイルを、私は**「同期型(Synchronous)」**体験と呼んでいます。一方、タスクをまるごと委任してバックグラウンドで自律実行させるスタイルを**「非同期型(Asynchronous)」**体験と呼んでいます。代表的なツールの分類は以下の通りです。 * **同期型(Synchronous):** Gemini CLI、VS Code の Gemini Code Assist、Claude Code * **非同期型(Asynchronous):** Jules、Gemini CLI(YOLO モード)、GitHub Copilot Agent もちろん、あらゆる分類体系がそうであるように、この区分は理解を助けるための便宜的な整理にすぎません。同じツールであってもモードによって両方の振る舞いが可能ですし、まったく新しいパラダイム([Antigravity](https://antigravity.google/) のような存在)が次々と登場してくるからです。 それぞれのタスクにどのツールを割り当てるべきかを判断するため、私は「ビジネス価値(Business Value)」と「技術的確実性(Technical Certainty)」を軸にしたシンプルな **2x2 フレームワーク** を活用しています。 ![AI支援ワークフローフレームワーク](2x2-framework.png "私の AI 支援ワークフローフレームワーク") * **高価値 / 高確実性(High Value / High Certainty):** 同期型で進めます。Gemini CLI や IDE などのツールを使い、自らが常にループの中(in the loop)に留まり、キーボードから手を離さずに作業を進めます。 * **高価値 / 低確実性(High Value / Low Certainty):** 不確実性を減らすためのリサーチが必要です。非同期ツールやディープリサーチエージェント、プロトタイプを活用してソリューションを検証(spike)します。 * **低価値 / 高確実性(Low Value / High Certainty):** いわゆる「あると嬉しい(nice-to-have)」機能や定型タスクです。Jules や GitHub Copilot Agent のようなバックグラウンドエージェントに非同期で丸投げし、自分自身の時間はより高価値な作業のために確保します。 * **低価値 / 低確実性(Low Value / Low Certainty):** 基本的には**手を出すべきではありません**。どうしても取り組む必要があるなら、気休め程度にバックグラウンドエージェントに任せつつ、まずは技術的な確実性を高めることに注力してください。その過程で、価値そのものの再評価につながることもあります。 ## エージェントをカスタマイズする 生産的な仕事に集中したいときに、AI と不毛な押し問答を繰り広げたい人はいません。AI ツールに対する代表的な不満の一つが、「勝手に余計なことをする(過剰に積極的すぎる)」という点です。明確な指示もないのに勝手にファイルを削除したり、独りよがりな前提でコードを書き換えたりする現象です。AI を「自分の味方」として忠実に働かせるには、適切なカスタマイズが欠かせません。 エージェントをカスタマイズするアプローチには、大きく分けて 2 つの道筋があります。1 つ目は、エージェントがプロジェクト読み込み時に参照する `AGENTS.md` ファイルを配置する方法です。(※ `AGENTS.md` が標準規格として普及する以前は、`GEMINI.md` や `CLAUDE.md` といった独自のコンテキストファイルが使われていましたが、本質的な役割は同じです)。 2 つ目は、いわば究極の選択肢(核オプション)である**「エージェントのシステム指示(System Instructions)を直接書き換える」**方法です。すべてのエージェントがこの柔軟性を提供しているわけではありませんが、完全に自分好みの開発体験を構築するための最も強力なレバーとなります。以下、それぞれの詳細を見ていきましょう。 ### AGENTS.md このファイルは、AI エージェントに対する「従業員ハンドブック(就業規則)」のようなものだと考えてください。プロジェクトの目的、ディレクトリ構成、そして「中間ステップは必ずコミットすること」や「実装前に必ず確認を求めること」といった開発運用ルールを記述します。 ```markdown # Project Context This is a personal blog built with Hugo and the Blowfish theme. ## Code Style - Use idiomatic Go for backend tools. - Frontend customisations are done in `assets/css/custom.css`. - Content is written in Markdown with front matter. ## Rules - ALWAYS run `hugo server` to verify changes before committing. - Do NOT modify the theme files directly; use the override system. - When generating images, save them to `assets/images` and reference them with absolute paths. ``` > **プロのヒント(Pro Tip):** 自己改善ループを構築しましょう。コーディングセッションが終わったら、LLM に「今行ったセッションを振り返り、開発ワークフローを改善するための提案を出して」と依頼します。そこで得られた学びを `AGENTS.md` に追記していくことで、プロジェクトをこなすごとにエージェントが賢く成長していきます。 ### システム指示(System Instructions) `AGENTS.md` が「プロジェクト固有のルール」を規定するのに対し、システム指示は「エージェントのペルソナ(人格)と根本的な行動規範」を定義します。あらゆるエージェントには標準のシステム指示があらかじめ組み込まれていますが、それは万人向けの汎用的な振る舞いをするように設計されているため、個人の作業スタイルや特定の言語作法には必ずしもマッチしません。`AGENTS.md` に大量の指示を詰め込んで無理やりデフォルトの挙動を上書きしようとするのは逆効果になりがちです。最も確実なアプローチは、システムプロンプトそのものを目的に応じて書き換えることです。 システムプロンプトの上書きを許可しているエージェントは限られますが、Gemini CLI ではいくつかの環境変数を設定することでこれが可能です。私はこの機能を活用して、プロジェクトの技術スタックに応じた専門特化型の Gemini CLI エイリアスを作成しています。その目的は、単に「どんな言語もそこそこ書ける汎用アシスタント」ではなく、特定の技術エコシステムを熟知した「シニア〜プリンシパルエンジニア級のスペシャリスト知識」をエージェントに宿らせることにあります。例えば、私の [dotgemini](https://github.com/danicat/dotgemini) プロジェクトでは、Go や Python 開発専用のシステムプロンプトを用意し、標準の汎用アシスタントを「極めて強いこだわりを持ったエキスパートエンジニア」へと差し替えています。 以下は、私が Go 言語開発用に実際に使用しているシステムプロンプトの抜粋です。 ```markdown # Core Mandates (The "Tao of Go") You must embody the philosophy of Go. It is not just about syntax; it is about a mindset of simplicity, readability, and maintainability. - **Clear is better than clever:** Avoid "magic" code. Explicit code is preferred over implicit behaviour. - **Errors are Values:** Handle errors explicitly and locally. Do not ignore them. Use `defer` for cleanup but explicitly check for errors in critical `defer` calls (e.g., closing files). - **Concurrency:** "Share memory by communicating, don't communicate by sharing memory." - **Formatting:** All code **MUST** be formatted with `gofmt`. ``` これにより、ターミナルで `gemini-go` や `gemini-py` と打ち分けるだけで、対象言語のエコシステムやベストプラクティスを骨の髄まで理解した「専属エージェント」を呼び出せるようになります。 ### Model Context Protocol(MCP)でツールボックスを拡張する ここまでのカスタマイズはエージェントの「振る舞い」に関するものでしたが、もう一つ重要なのがエージェントの「拡張性」です。ここで主役となるのが [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) です。MCP 標準規格に準拠したサーバーを実装することで、開発者は多様な AI エージェントに対して自由自在に外部機能を接続できるようになります。 以前執筆した [Hello, MCP World!](https://danicat.dev/ja/posts/20250817-hello-mcp-world/) の記事でも解説した通り、MCP サーバーはエージェントに対して「Tools(外部ツール)」「Prompts(定型プロンプト)」「Resources(データリソース)」を提供します。中でも Tools は、API の呼び出しや Web 検索、ローカルファイルの操作など、エージェントが現実世界にアクションを起こすための架け橋となるため、最も大きな注目を集めています。 現在では多種多様な MCP サーバーが公開されており、その選択肢は日々爆発的に増え続けています。しかも自作するのも極めて容易であり、自分専用のサーバーを作ることを強くお勧めします。後ほどパーソナライズされたソフトウェアについて詳しく触れますが、「**AI を使って、AI の回答精度を高めるための専用ツールを自作する**」というアプローチこそ、私が今年身につけた最も強力なハックです。 実際、私自身も愛用している 2 つの MCP サーバー — Go のコーディング品質を向上させる [GoDoctor](https://github.com/danicat/godoctor) と、ブログの執筆・公開フローの退屈な作業を自動化する [Speedgrapher](https://github.com/danicat/speedgrapher) — を vibe coding で構築しました。どちらも私自身のリアルな開発ワークフローに合わせて完全に最適化されています。 これによって、素晴らしい好循環が生まれます。自分の生産性を高めるためのツールを作り、そのツールを使ってさらに高度なツールを開発していくのです。これこそが、私がこれまでのキャリアの中で最も「10x エンジニア」に近づけた瞬間だと確信しています。 ## バイブコーディングのワークフロー 私の vibe coding 体験は、感動的なひらめきに満ちていると同時に、時には苛立ちで頭を抱えるものでもありました。常に「感動的」な側に留まり続けるため、私はこの開発ワークフローを **「TDD(テスト駆動開発)を極限までブーストしたもの(TDD on steroids)」** として捉えています。 ここで、クラシックな TDD サイクルをおさらいしてみましょう。 1. **Red(失敗):** まずは失敗する小さな機能テストを作成する。 2. **Green(成功):** そのテストをパスさせることだけに全力を注ぐ。テストが失敗している間は、余計なコードに手を加えたり最適化を図ったりしない。 3. **Refactor(改善):** テストが通り正常に動作したら、コードの構造や品質を心置きなく洗練させる。 ![標準的なTDDサイクル](tdd-cycle.png "伝統的な Red-Green-Refactor ループ") バイブコーディングにおいても、根本的にやっていることはまったく同じです。ただし、生成されたコードが設計基準、コーディング規約、セキュリティ要件に厳格に適合しているかを検証する「リファクタリング(Refactor)」ステップの重要性が格段に高まります。 この適応型サイクルでは、各フェーズの焦点が次のように変化します。 * **Red(受け入れ基準の定義):** 手作業で失敗する単体テストを書く代わりに、プロンプト内で明確な「受け入れ基準(Acceptance Criteria)」を定義します。これが、モデルが満たすべき契約(コントラクト)となります。 * **Green(AI によるコード生成):** エージェントがソリューションを実装し、理想的にはそれが正しく機能することを証明するためのテストコードまで含めて生成します。 * **Refactor(規律の強制と品質保証):** ここが品質の防波堤(品質ゲート)です。コードレビューに AI の力を借りることは大いに推奨されますが、**コードを生成したのと同じセッションでレビューを行わせてはいけません**。自らの出力に対して強いバイアス(自己正当化の偏見)が働くからです。私が [GoDoctor](https://danicat.dev/ja/posts/20250729-how-to-build-an-mcp-server-with-gemini-cli-and-go/) に専用の「review」ツールを組み込んだのもまさにこのためです(のちに [エージェント時代におけるコードレビューのやり方](https://danicat.dev/ja/posts/20260303-code-reviews-in-2026/) で完全なレビューフレームワークとして体系化しました)。このステップで従来のリンターや自動テストを実行し、コードが基準を満たしているかを厳しく精査します。そして変更をコミットした上で、散らかったセッションによる混乱を防ぐためにエージェントのコンテキスト履歴を一度クリアします。 ![適応型バイブコーディングサイクル](vibe-coding-cycle.png "適応型バイブコーディングループ") 最も重要な原則は、**「検証を挟まずに LLM にコードを積み上げさせないこと」**です。エラーが連鎖・増幅(extrapolate)してしまうと、最終的に手遅れのがらくたが出来上がってしまいます。私が画面に向かって「元に戻せ(undo)!」と叫ぶ羽目になった回数は数え切れません。さらに恐ろしいことに、モデルは時に **度を超えたやり直し** をやらかします。例えば `git reset --hard` を勝手に実行して、4 時間分の作業成果が一瞬で虚空に消え去るような悲劇です。 「バイブスの崩壊(vibe collapse)」や「コンテキストの腐敗(context rot)」には常に警戒してください。セッションが長引きすぎたり、失敗した試行錯誤が履歴に蓄積しすぎると、モデルの思考能力は目に見えて劣化し、同じ間違いを堂々巡りで繰り返すようになります。モデルが 2 つの壊れた修正案の間を行ったり来たりする無限ループに陥ったら、即座に手を止めてください。最善の特効薬は、セッションを「一度電源を切って入れ直す(コンテキストをリセットして履歴を消去する)」ことです。 ハルシネーションを起こして暴走したセッションを対話で無理に修正しようとするよりも、頻繁にコミットを残しておき、安全なチェックポイントへいつでも巻き戻せるようにしておく方がはるかに健全です。一つのタスクが完了したら、**即座にコミット&プッシュし、コンテキストを完全にクリアしてから次のタスクに着手すること**を強く推奨します。 ## パーソナライズドソフトウェアの時代 単なる生産性の向上を超えて、vibe coding はさらに深遠な地平を切り拓きます。それは、**「パーソナライズされたソフトウェア(自分専用の特注ツール)」の経済的実現可能性**です。MCP のセクションでも触れましたが、これは使い捨ての小さな自動化スクリプトから、本格的なスタンドアロンアプリケーションに至るまで、あらゆるソフトウェアに当てはまります。 かつては、自分ひとりのためだけに特注のツールを開発することは、費用対効果の観点から割に合わない贅沢でした。しかし今や、**わずか 3〜4 回のプロンプトのやり取りだけで、完全に動作する実用アプリケーションが手に入ります**。 例えば最近、私は Markdown 記法で書かれたテキストを Google ドキュメントに綺麗に変換する方法に悩んでいました。従来のやり方なら、Google 検索で最適なツールを探すのに何時間も費やし、オープンソースから商用サービスまで無数のアプリやブラウザ拡張機能を比較検討していたはずです。機能一覧をもとに候補を絞り込み、レビューや評価を読み漁り、開発元が信頼できるかどうかセキュリティ上の安全性を必死に確認していたでしょう。 今日、そうした摩擦(フリクション)は完全に過去のものとなりました。ツールを探し回る代わりに、私はわずか数分でシンプルな Google ドキュメント専用拡張機能を vibe code し、ドキュメントにインストールして一度実行し、何事もなかったかのように次の仕事へと移りました。貴重な時間を大幅に節約できただけでなく、出所不明の拡張機能によって自分のマシンに見知らぬトロイの木馬が仕込まれる心配もなく、夜もぐっすり安心して眠ることができます。 このパラダイムシフトは、「作るか買うか(Build vs. Buy)」の損益計算を根本から覆します。私たちはもはや、中身のブラックボックスな汎用ソフトウェアを消費するだけの受動的なユーザーではありません。**自らの道具を自らの手で生み出す「道具のアーキテクト」**になれるのです。 ## まとめ Vibe coding とは、決してエンジニアが手を抜いて怠けるための手法ではありません。**より高い抽象度でシステムを思考し、操作するための新たなアプローチ**です。LLM が持つ圧倒的な創造的パワーと、ソフトウェアエンジニアリングが培ってきた規律ある実践 — 明確な要件定義、徹底したコンテキスト管理、そして妥協のないテストと検証 — を融合させることで、私たちはかつてないスピードと、何より純粋な喜びを持ってソフトウェアを創り出すことができます。 さあ、バイブスに身を任せましょう。ただし、その旅には必ず**「エンジニアとしての誇りと帽子(engineering hat)」**を忘れずに携えていってください。 --- # ADK、Ollama、SQLite で完全オフラインなエージェントを構築する方法 - **Markdown URL**: https://danicat.dev/ja/posts/building-aida-part-2/index.md - **HTML URL**: https://danicat.dev/ja/posts/building-aida-part-2/ - **Date**: 2025-11-03 - **Categories**: Agent Development - **Tags**: adk, gemini, ollama, python, rag, sqlite, tutorial - **Series**: Building the Diagnostic Agent - **Summary**: Google ADK、Ollama/LiteLLM 経由のローカル Qwen 2.5、sqlite-rag によるローカル SQLite ベクトル RAG を用いて、100% オフラインで動作する AI エージェントを構築するステップバイステップガイド。 [本シリーズのパート5](https://danicat.dev/ja/posts/20251031-building-aida/) では、エージェント用のカスタムクライアントインターフェースの構築に焦点を当てました。エージェントの使い勝手を高める大きな一歩となりましたが、まだ重要な機能が1つ欠けていました。「ネットワークがダウンしたときにどうするか」という問題です。 これはあらゆるエージェントにとって問題ですが、ここで重要なのは、私たちが構築しているのが「緊急診断エージェント」であるという点です。ネットワークがオフラインのときに使えない緊急診断エージェントなど、一体何の役に立つでしょうか? そこで私はフォールバックの仕組みについて考えました。ローカルの依存関係のみで診断を実行できたらどうでしょうか?これには、中核となるモデルを置き換えるだけでなく、新しい RAG 戦略を考案することも含まれます。 メリットは明らかです。接続されている間は最も高性能なオンラインモデルを使用できますが、性能低下や障害が発生したシナリオでは、正常な状態に戻るまでローカルモデルにフォールバックできます。それだけでなく、環境が隔離されている場合やプライバシーが懸念されるユースケースでも、このエージェントを利用できるようになります。 この記事では、ローカル診断エージェントを実現するために必要な機能に焦点を当てていきます。 ## クラウドモデルからローカルモデルへの切り替え ローカルモデルを実行する最も広く採用されている方法の1つが [**Ollama**](https://ollama.com/) です。Mac でコードを実行している場合は、[Homebrew](https://brew.sh/) を使って Ollama をインストールできます(Mac 以外の場合は、お使いの OS に合わせた公式のインストール手順を確認してください): ```bash brew install ollama ``` Ollama をインストールしたら、`ollama pull` を使ってモデルをダウンロードできます。例えば次の通りです: ```bash ollama pull qwen2.5 ``` モデル名だけで pull することもできますし(この場合は「デフォルト」バージョンがダウンロードされます)、特定のバージョンを指定するためにタグを使用することもできます。[`qwen2.5`](https://ollama.com/library/qwen2.5) のようなモデルファミリーでは、1B、2B、7B など様々なサイズのモデルや、特定のユースケース(テキスト処理、画像処理など)向けにファインチューニングされたバージョンが提供されているのが一般的です。 どのモデルが利用可能で、それぞれのサイズや性能がどうなっているかを確認するには、[Ollama ライブラリ](https://ollama.com/library) を確認してください。 今回のユースケースでは、当然モデルが賢いに越したことはありませんが、モデルが大きくなればそれだけ強力なハードウェアが必要になります。また、[**Osquery**](https://osquery.io/) や RAG ツールへの各種ツール呼び出しを連携させる必要があるため、選択するモデルがネイティブの tool calling(ツール呼び出し)機能を備えていることを確認する必要があります。 いくつかのモデルを評価した結果、Qwen 2.5 7B を使用することにしました。`ollama show` を実行すると、その機能や仕様を確認できます: ```bash $ ollama show qwen2.5 Model architecture qwen2 parameters 7.6B context length 32768 embedding length 3584 quantization Q4_K_M Capabilities completion tools ``` ### なぜ Qwen 2.5 なのか? AIDA のツール呼び出し要件に対応できるか、いくつかの選択肢をテストしました: * **GPT-OSS:** 表現力豊かな会話ができましたが、ツール呼び出しが非常にナイーブでした。例えば、`SELECT * FROM system_info`(およびそのバリエーション)を繰り返しリクエストしてループに陥り、一向に前進しないことが頻繁にありました。 * **Llama 3.1:** 会話の流れとツール呼び出しの両方で苦戦しました。 * **Qwen 2.5:** スムーズな会話の流れを維持しながら、ツール呼び出しを最も的確にこなすローカルモデルでした。 複雑なクエリの計画作成においては [**Gemini 2.5 Flash**](https://deepmind.google/technologies/gemini/flash/) のレベルには及びませんが、完全オフラインのモデルとしては十分な性能です。 ### LiteLLM によるローカルモデルの実行 Qwen をエージェントに接続するために、各 LLM プロバイダー向けの統一インターフェースを提供するライブラリ [**LiteLLM**](https://www.litellm.ai/) を使用します。これにより、わずか1行のコード変更でモデルを切り替えることができます: ```python # aida/agent.py from google.adk.models.lite_llm import LiteLlm # ... inside the agent definition ... # Instead of a hardcoded string like "gemini-2.5-flash", # we create a LiteLLM object with the model string MODEL = LiteLlm(model="ollama_chat/qwen2.5") # ... and pass MODEL to the root agent: root_agent = Agent( model=MODEL, name="aida", description="The emergency diagnostic agent", # ... instructions and tool definitions omitted ... ) ``` **注意:** モデル文字列の最初の部分は LiteLLM の「プロバイダー」名です(例: `ollama_chat/qwen2.5` の `ollama_chat`)。`ollama` も有効なプロバイダーですが、[より良い応答を得る](https://docs.litellm.ai/docs/providers/ollama) ためには `ollama_chat` を使用することが推奨されています。 ADK でローカルモデルを実行するために必要なのはこれだけです。エージェントをテストして、どのように応答するか確認してみてください。以前使用していた `gemini-2.5-flash` モデルと応答を比較してみるのもおすすめです。

最初に Gemini 2.5 Flash、次に Qwen 2.5 で実行した AIDA のデモ。Gemini の方が明らかに高速で、必要なツール呼び出しの回数も少なくなっています。Qwen の応答時間はローカルハードウェアに大きく左右されます(このデモは 48GB の RAM を搭載した Apple MacBook Pro M4 で動作しています)。

これでモデルがローカルで動作するようになりました!次は、もう1つのクラウド依存である [**Vertex AI RAG**](https://cloud.google.com/vertex-ai/docs/generative-ai/grounding/overview) に取り組みましょう。 ## SQLite RAG によるオフラインナレッジベースの構築 正直なところ、Vertex AI RAG を使ったことでプロジェクトの複雑な部分をうまく扱えるようになったものの、Vertex AI RAG は明らかにオーバースペックでした。Vertex AI RAG は、膨大なデータを扱う大規模なエンタープライズユースケース向けに設計されているからです。 今回のエージェントに必要なのは、基本的なスキーマ検索の仕組みだけです。また Osquery のスキーマは非常に安定しており、一度構築してしまえば後から手を加えることはほとんどありません。こうした特徴を考えると、Vertex AI RAG でホストすることを正当化するのは困難です……まさに「ハエを撃つのに大砲を使う」ようなものでした。 Osquery を使用している関係上、すでに [**SQLite**](https://www.sqlite.org/) エコシステムの中にいたため、SQLite をバックエンドにした RAG ソリューションを探すのは自然な流れでした。Google で検索したところ、非常に有望なプロジェクトを見つけました。**[`sqlite-rag`](https://github.com/sqliteai/sqlite-rag)** です。 もちろん、開発ではよくあることですが、そう一筋縄ではいきませんでした。 ### 課題: Python 3.14 の依存関係トラブル SQLite には機能を拡張するためのエクステンション(拡張機能)という概念があり、`sqlite-rag` もこれを前提に構築されています。 最初に `sqlite-rag` をテストした際の問題は、macOS のデフォルト Python に同梱されている SQLite パッケージが(セキュリティ上の理由から)拡張機能を無効化していたことでした。 この制限を回避するため、Homebrew を使って新しいバージョンの Python(3.14)をインストールすることにしました。システム標準の Python ではなく Homebrew 版の Python を確実に使用するために、`python3` コマンドのシンボリックリンクを少し調整する必要もありました。 同様の課題に直面した場合は、これら2つのコマンドの出力を比較して正しいバージョンの Python が使われているか確認してください(一致していない場合は PATH 環境変数を調整します): ```bash $ which python3 /opt/homebrew/opt/python@3.14/libexec/bin/python3 $ brew info python3 ==> python@3.14: stable 3.14.0 ... ==> Caveats Python is installed as /opt/homebrew/bin/python3 Unversioned symlinks `python`, `python-config`, `pip` etc. pointing to `python3`, `python3-config`, `pip3` etc., respectively, are installed into /opt/homebrew/opt/python@3.14/libexec/bin See: https://docs.brew.sh/Homebrew-and-Python ``` 3.14(別名 pi-thon)をインストールした状態で `sqlite-rag` をそのまま使おうとしたところ、依存関係の1つが 3.14 にまだ対応していなかったため失敗しました。`sqlite-rag` は [`markitdown`](https://github.com/microsoft/markitdown) に依存し、`markitdown` は [`magika`](https://google.github.io/magika/) に依存し、さらにそれが [`onnxruntime`](https://onnxruntime.ai/) に依存しています。しかし `onnxruntime` には macOS ARM64 向けの Python 3.14 用ビルド済み wheel がまだ提供されておらず、インストールが失敗してしまったのです。>.< 現時点で AIDA が取り込む必要があるのはプレーンテキストの `.table` ファイルだけなので、`markitdown` のドキュメントパース機能は実際には*不要*でした。Python 環境全体をダウングレードする代わりに、手っ取り早いハックを選びました。`sqlite-rag` がインポートを試みる前に、問題となっているモジュールをモック化してしまう方法です。 ```python import sys from unittest.mock import MagicMock # PRE-FLIGHT HACK: # 'markitdown' depends on 'onnxruntime', which fails to install/load # on Python 3.14 on macOS ARM64. # # Since we only use plain text ingestion, we mock it out to bypass the crash. sys.modules["markitdown"] = MagicMock() from sqlite_rag import SQLiteRag ``` お世辞にも綺麗とは言えませんが、しっかりと動作します。ずっとコードに残しておくべきものではありませんが、依存関係の問題が修正されるまでの暫定対応としては十分です。 ### Osquery スキーマの RAG データベースへの格納 これで `sqlite-rag` が動作するようになったので、次のステップは Osquery スキーマのインジェストです。これは `ingest_osquery.py` スクリプトを使って行います。スキーマディレクトリを走査し、各 `.table` ファイルを RAG データベースに追加していきます: ```python # ingest_osquery.py import os # ... markitdown hack omitted ... from sqlite_rag import SQLiteRag DB_PATH = os.path.abspath("schema.db") SPECS_DIR = os.path.abspath("osquery_data/specs") def ingest(rag: SQLiteRag, file_path: str): with open(file_path, "r", encoding="utf-8") as f: content = f.read() rel_path = os.path.relpath(file_path, SPECS_DIR) rag.add_text(content, uri=rel_path, metadata={"source": "osquery_specs"}) if __name__ == "__main__": if os.path.exists(DB_PATH): os.remove(DB_PATH) print(f"Initializing RAG database at {DB_PATH}...") rag = SQLiteRag.create(DB_PATH, settings={"quantize_scan": True}) print(f"Scanning {SPECS_DIR} for .table files...") files_to_ingest = [] for root, _, files in os.walk(SPECS_DIR): for file in files: if file.endswith(".table"): files_to_ingest.append(os.path.join(root, file)) total_files = len(files_to_ingest) print(f"Found {total_files} files to ingest.") for i, file_path in enumerate(files_to_ingest): ingest(rag, file_path) if (i + 1) % 50 == 0: print(f"Ingested {i + 1}/{total_files}...") print(f"Finished ingesting {total_files} files.") print("Quantizing vectors...") rag.quantize_vectors() print("Quantization complete.") rag.close() ``` インジェストの後には量子化(Quantization)のステップがあります。馴染みのない方のために説明すると、量子化とは高次元のベクトル埋め込み(Embedding)を圧縮する技術であり、大きな 32 ビット浮動小数点数をコンパクトな 8 ビット整数に変換します。 これはローカル環境において非常に重要です。量子化を行わないと、高次元ベクトルの保存によって SQLite データベースのサイズが肥大化し、一般的なノート PC 上での類似度検索がもたつく原因になります。量子化を行うことで、わずかな精度を犠牲にする代わりに、検索速度とストレージ効率の大幅な向上を得ることができます。 ### エージェントによるスキーマ RAG への問い合わせ 次に、`SQLiteRag` を使って `schema_discovery` ツールを実装します: ```python # aida/schema_rag.py import os # ... markitdown hack omitted ... from sqlite_rag import SQLiteRag from sqlite_rag.models.document_result import DocumentResult PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) SCHEMA_DB_PATH = os.path.join(PROJECT_ROOT, "schema.db") # open the RAG database schema_rag = SQLiteRag.create( SCHEMA_DB_PATH, require_existing=True ) def discover_schema(search_terms: str, top_k: int = 5) -> list[DocumentResult]: """ Queries the osquery schema documentation using RAG and returns all table candidates to support the provided search_terms. Arguments: search_terms Can be either a table name, like "system_info", or one or more search terms like "system information darwin". top_k Number of top results to search in both semantic and FTS search. Number of documents may be higher. Returns: One or more chunks of data containing the related table schemas. """ results = schema_rag.search(search_terms, top_k=top_k) return results ``` RAG を導入したことで、AIDA は自力でテーブル定義を調べられるようになりました。 ![AIDA のスクリーンショット](image-1.png "Qwen による 'run schema discovery for battery' の実行画面") スキーマの探索は機能するようになりましたが、まだ課題が残っています。 ## 専門知識の活用による能力差の克服 Qwen 2.5(パラメータ数 7B)のようなローカルモデル向けの開発は、Gemini 2.5 Flash のようなクラウドモデル向けの開発とは大きく異なります。 第一に、**コンテキストウィンドウ**の違いです。Gemini は 100 万トークンのコンテキストウィンドウを備えているため、ドキュメント全体をプロンプトに流し込んだり、非常に詳細な指示を与えたりすることができます。一方、Qwen 2.5 は 32k トークンと比較的小さいため、モデルに渡す情報をはるかに厳選する必要があります。 第二に、Qwen は Gemini 2.5 Flash のような **Thinking Model(思考型モデル)** ではないため、自律的に回答を推敲・洗練することが難しく、Gemini 2.5 Flash よりも丁寧な誘導を必要とする場面が多くなります。 このギャップを埋めるためには、エージェントのシステム指示とツールの構成をより工夫する必要があります。 ### シンプル化されたシステムプロンプト トークン数を節約するために、利用可能なテーブル名の一覧など、大量のトークンを消費していた要素を削ぎ落としてシンプルな指示文に変更します。最適なクエリを組み立てる処理は、完全にツール側に委ねる方針をとります。 ```python root_agent = Agent( model=MODEL, name="aida", description="The emergency diagnostic agent", instruction=""" [IDENTITY] You are AIDA, the Emergency Diagnostic Agent. You are a cute, friendly, and highly capable expert. Your mission is to help the user identify and resolve system issues efficiently. [OPERATIONAL WORKFLOW] 1. DISCOVER: Use `discover_schema` to find relevant tables and understand their columns. 2. EXECUTE: Use `run_osquery` to execute the chosen or constructed query. """, tools=[ discover_schema, run_osquery, ], ) ``` 検索キーワードが実際のテーブルスキーマに十分近ければ `discover_schema` ツールでもうまく機能しますが、既知のナレッジベースに基づいてクエリそのものを提供できるようにすれば、さらに精度を高められるのではないでしょうか? ### 定番クエリのための新しい RAG 幸いなことに、ゼロからすべてを学習させる必要はありません。Osquery コミュニティには、特定の診断にどのクエリが役立つかという素晴らしいナレッジベースが蓄積されています。さらに嬉しいことに、プロアクティブな監視のために任意の Osquery 環境へ導入できるオープンソースの「クエリパック(Query Packs)」としてそれらのクエリが提供されています。脅威検知やコンプライアンス監査など多種多様なクエリパックが存在し、まさに AIDA に持たせたい知識そのものです。 ただし、クエリパックは本来、バックグラウンドでシステムを監視する Osquery デーモンにインストールされることを想定しています。あらかじめ設定された頻度でクエリを実行し、監視ダッシュボードにアラートを通知する仕組みです。私たちがやりたいのは監視ツールとしてクエリをインストールすることではなく、AIDA が必要に応じてオンデマンドでそれらのクエリを利用できるようにすることです。そこで、通常の手順でパックをインストールするのではなく、テキストデータとして AIDA に渡す第2の RAG を構築することにしました。 Osquery リポジトリには、手始めに利用できる [サンプルパック](https://github.com/osquery/osquery/tree/master/packs) がいくつか用意されています。 以下が、クエリパックを処理するための新しいインジェストスクリプト `ingest_packs.py` です。先ほどのスクリプトと非常によく似ています: ```python # ingest_packs.py import json import os import glob import sys import re import sqlite3 from unittest.mock import MagicMock sys.modules["markitdown"] = MagicMock() from sqlite_rag import SQLiteRag DB_PATH = os.path.abspath("packs.db") PACKS_DIR = "osquery_data/packs" def ingest_pack(rag, pack_path): pack_name = os.path.basename(pack_path).replace(".conf", "").replace(".json", "") print(f"Ingesting pack: {pack_name}...") try: with open(pack_path, "r") as f: content = f.read() content = re.sub(r"\s*\n", " ", content) data = json.loads(content) pack_platform = data.get("platform", "all") queries = data.get("queries", {}) for query_name, query_data in queries.items(): sql = query_data.get("query") desc = query_data.get("description", "") val = query_data.get("value", "") platform = query_data.get("platform", pack_platform) text_to_embed = f"Platform: {platform}\nName: {query_name}\nDescription: {desc}\nRationale: {val}\nSQL: {sql}" metadata = { "name": query_name, "pack": pack_name, "query": sql, "description": desc, "value": val, "platform": platform, } try: rag.add_text(text_to_embed, metadata=metadata) except sqlite3.IntegrityError: pass # Skip duplicates except Exception as e: print(f" - ERROR: Failed to parse {pack_name}: {e}") def main(): if os.path.exists(DB_PATH): os.remove(DB_PATH) rag = SQLiteRag.create(DB_PATH, settings={"quantize_scan": True}) pack_files = glob.glob(os.path.join(PACKS_DIR, "*.conf")) + glob.glob( os.path.join(PACKS_DIR, "*.json") ) for pack_file in pack_files: ingest_pack(rag, pack_file) rag.quantize_vectors() rag.close() if __name__ == "__main__": main() ``` ツール定義も、スキーマ探索ツールとほぼ同様のパターンに従います: ```python # aida/queries_rag.py import os # ... markitdown hack omitted ... from sqlite_rag import SQLiteRag from sqlite_rag.models.document_result import DocumentResult PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) PACKS_DB_PATH = os.path.join(PROJECT_ROOT, "packs.db") queries_rag = SQLiteRag.create( PACKS_DB_PATH, require_existing=True ) def search_query_library(search_terms: str, platform: str = "all", top_k: int = 5) -> list[DocumentResult]: """ Search the query pack library to find relevant queries corresponding to the search terms. For better response quality, use the platform argument to specify which platform you are currently investigating (e.g. darwin) Arguments: search_terms Can be either a table name, like "system_info", or one or more search terms like "malware detection". platform One of "linux", "darwin", "windows" or "all" top_k Number of top results to search in both semantic and FTS search. Number of documents may be higher. Returns: One or more chunks of data containing the related queries. """ if platform == "all" or platform is None: search_terms += " windows linux darwin" else: search_terms += " " + platform results = queries_rag.search(search_terms, top_k=top_k) return results ``` 最後に、エージェントに新しいツールを認識させ、システム指示によっていつ使用すべきかを伝えます: ```python # aida/agent.py root_agent = Agent( # ... instruction=""" [OPERATIONAL WORKFLOW] Follow this sequence for most investigations to ensure efficiency and accuracy: 1. SEARCH: For high-level tasks (e.g., "check for rootkits"), FIRST use `search_query_library`. 2. DISCOVER: If no suitable pre-made query is found, use `discover_schema` to find relevant tables and understand their columns. 3. EXECUTE: Use `run_osquery` to execute the chosen or constructed query. """, tools=[ search_query_library, discover_schema, run_osquery, ], ) ``` 実際の動作画面がこちらです: ![AIDA のスクリーンショット](image-2.png "マルウェアチェックを実行中の AIDA。ログに表示されている通り、クエリライブラリから関連クエリが検索されていることがわかります。") 面白いのは、このツールが Qwen 2.5 の実用性を高めるだけでなく、Gemini 2.5 Flash にとっても大きなメリットになる点です。最小公約数(最も制約の厳しい環境)に向けて最適化を行うことで、結果的にシステム全体の品質が底上げされる好例と言えます。 ## まとめ これによって、インターネット接続がない状態でもコンピュータの問題を診断できる、本格的な緊急診断エージェントが完成しました。もっとも……モデルを実行できるだけの十分なマシンスペックがあれば、の話ですが!完璧なものなんてそうそうありませんよね :) この記事で紹介したのは、ここ数日で AIDA に加えた改善のほんの一部に過ぎません。プロジェクトの全容については、ぜひ [GitHub の AIDA リポジトリ](https://github.com/danicat/aida) をチェックしてみてください。 ## 参考リンク * [Gemini 2.5 Flash](https://deepmind.google/technologies/gemini/flash/) * [LiteLLM](https://www.litellm.ai/) * [Ollama](https://ollama.com/) * [Osquery](https://osquery.io/) * [Qwen 2.5 (Ollama Library)](https://ollama.com/library/qwen2.5) * [SQLite](https://www.sqlite.org/) * [sqlite-rag](https://github.com/sqliteai/sqlite-rag) * [Vertex AI RAG](https://cloud.google.com/vertex-ai/docs/generative-ai/grounding/overview) --- # Dev-UI の先へ:ADK エージェントのカスタムインターフェースを構築する - **Markdown URL**: https://danicat.dev/ja/posts/building-aida/index.md - **HTML URL**: https://danicat.dev/ja/posts/building-aida/ - **Date**: 2025-10-31 - **Categories**: Agent Development - **Tags**: adk, fastapi, gemini, javascript, python, tutorial - **Series**: Building the Diagnostic Agent - **Summary**: Google ADK の Dev-UI を脱却し、FastAPI バックエンド、Vanilla JS クライアント、Gemini CLI で生成したリアルタイムストリーミング同期アバターを構築する方法を解説します。 この半年間、Google での DevRel 業務の一環として、GenAI、バイブコーディング、エージェント開発、そしてその間にあるあらゆる領域を探求してきました。新しい技術を学ぶときは、実際に何かを作ってみるのが一番の近道だといつも感じています。この期間に私が情熱を注いできたプロジェクトのひとつが「診断エージェント」でした。これは、自然言語を使ってコンピューターのトラブルを診断できるソフトウェアです。 本シリーズの[パート4](https://danicat.dev/ja/posts/20251020-diagnostic-agent-with-adk/)では、Google の [ADK](https://google.github.io/adk-docs/) を使うように診断エージェントをリファクタリングしました。今回の記事では、プロジェクトにもう少し個性を吹き込むために、ADK エージェント向けのカスタムフロントエンドを作成する方法を探っていきます。 ## 新しいユーザーインターフェースを求めて これまでは、ADK、[Gemini](https://gemini.google.com/)、[osquery](https://osquery.io/)、[Vertex AI](https://cloud.google.com/vertex-ai/docs/start/introduction-unified-platform) といった主要コンポーネントを組み合わせて、小さな MVP を作り上げてきました。エージェントにはいくつかクセもありましたが、十分に面白かったため、ここ数か月のいくつかの登壇トークのネタとしても活用していました。 次のステップをどうしようかと少し行き詰まっていたとき、8月に見かけた [Sito さん](https://x.com/Sikino_Sito) のこのツイートを思い出しました: ![Sito-san's Avatar UI](image.png "https://x.com/Sikino_Sito/status/1957645002533925235") アニメやレトロゲームの大ファンである私にとって、このビジュアルはたまらなく魅力的でしたが、当時はまだ自分のプロジェクトと直接結びついてはいませんでした。それから数か月後、スペイン・マラガの BiznagaFest での登壇準備をしていたとき、ADK の開発用 UI を超えて、エージェントのための本格的なクライアントを作ってみたいと考えました。そのとき、ついに点と点がカチッと繋がったのです。 Sito さんは [Avatar UI Core](https://github.com/sito-sikino/avatar-ui-core) というオープンソースプロジェクトを公開されていますが、当時の私にはすぐに統合できるほどの知識がありませんでした。前述の通り、学ぶための最善の方法は「自分で作ること」です。そこで、Sito さんの作品にインスパイアされながら独自の UI を自作することにしました。また、レトロな世界観は大好きですが、8-bit というよりは 16-bit 時代のような、少しだけモダンさを残したテイストにしたいと考えました。 ## ADK ランタイムを探る 独自の UI を構築するための最初のステップは、エージェントのランタイムを作成することでした。ランタイムとは、エージェントを実際に実行し、ユーザーのリクエストをエージェントへルーティングするとともに、エージェントから返ってくるイベントをキャプチャしてモデルのレスポンスを処理するコンポーネントです。 これまでは `adk web` コマンドで起動できる ADK の開発用 UI に頼っていたため、ランタイムを自分で書く必要がありませんでした: ![ADK dev UI](https://danicat.dev/ja/posts/building-aida/image-1.png) 開発用 UI は非常に便利です。モデルへのリクエストやレスポンスのインスペクト、評価セットの作成といった多数のデバッグツールが揃っているほか、画像や双方向ストリーミングを扱うマルチモーダル機能も標準で備わっています。 ある意味、Dev-UI が最初から完璧に動いてくれたおかげで、私はエージェントの機能やツールの開発だけに専念でき、ランタイム自体の探索を後回しにしてしまっていたとも言えます。しかし、独自の本格的な UI を作るとなると、Dev-UI を自前の実装に置き換え、エージェントの Runner を自分で直接制御しなければなりません。 ソリューションの全体アーキテクチャは以下のようになります: ```mermaid flowchart LR frontend["`Frontend (HTML/CSS + JS) `"] runtime[Runtime] root["Root Agent"] osquery["osquery"] schema["schema"] rag[("Schema RAG")] os("Operating System") frontend -->|GET/POST| runtime subgraph be ["`Backend (FastAPI)`"] runtime -- query --> root root -- events --> runtime root --> osquery root --> schema subgraph tools osquery schema end end osquery --> os schema --> rag ``` HTML/CSS と JavaScript で書かれた軽量なフロントエンドが、Python の [FastAPI](https://fastapi.tiangolo.com/) で書かれたバックエンドに対してリクエストを送信します。バックエンド側では ADK の Runner を起動し、root agent とのやり取りを制御します。 root agent は AIDA の「頭脳」であり、リクエストの処理(LLM へのルーティング)と必要なツール呼び出しを担当します。root agent の処理が完了すると、ランタイムが処理するためのイベントが発行されます。 まずは最小限の実装(ベアボーン実装)から見ていきましょう。なお、root agent の定義自体は[前回の記事](https://danicat.dev/ja/posts/20251020-diagnostic-agent-with-adk/)で解説しているため、簡潔にするためにここでは割愛します。 ユーザーセッションを管理するために、`Runner` クラスとセッションサービスが必要になります。セッションサービスにはさまざまな実装がありますが、今回は単一ユーザー向けでありセッションも一時的なものであるため、シンプルにするために `InMemorySessionService` を使用し、ユーザー ID とセッション ID をコード内にハードコードします。 セッションと Runner の宣言は以下の通りです: ```py from fastapi import FastAPI from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from dotenv import load_dotenv load_dotenv() # --- Agent Definition --- from aida.agent import root_agent APP_NAME="aida" # --- Services and Runner Setup --- session_service = InMemorySessionService() runner = Runner( app_name=APP_NAME, agent=root_agent, session_service=session_service ) app = FastAPI() ``` 次に、エージェントにメッセージを送信するためのエンドポイントを実装します。名前は `chat` としましょう: ```py from fastapi import Request from fastapi.responses import JSONResponse from google.genai.types import Content, Part # --- API Endpoint for Chat Logic --- @app.post("/chat") async def chat_endpoint(request: Request): """Handles the chat logic, returning the agent's final response.""" body = await request.json() query = body.get("query") user_id = "demo_user" session_id = "demo_session" # Ensure a session exists session = await session_service.get_session(app_name=APP_NAME, user_id=user_id, session_id=session_id) if not session: session = await session_service.create_session(app_name=APP_NAME, user_id=user_id, session_id=session_id) response_text = "" async for event in runner.run_async( user_id=user_id, session_id=session_id, new_message=Content(role="user", parts=[Part.from_text(text=query)]), ): if event.is_final_response() and event.content and event.content.parts[0].text: response_text = event.content.parts[0].text return JSONResponse(content={"response": response_text}) ``` 冒頭の数行は典型的なリクエスト処理コードと、ハードコードされたセッション管理です。このコードの肝は `runner.run_async` の呼び出しで、root agent からイベントが発行されます。ここでは最終レスポンスのみに関心があるため、それを呼び出し元に JSON レスポンスとして返します。 以下のコマンドを実行して、この小さなアプリをテストできます: ```sh $ uvicorn main:app ... INFO: Started server process [86669] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` そして別のターミナルから `/chat` に POST リクエストを送ってみます: ```sh $ curl -X POST localhost:8000/chat -d '{"query":"hello"}' {"response":"Hello! Please state the nature of the diagnostic emergency."} ``` 今のところは UI が大きく進化したようには感じられないかもしれませんが、着実に前進しています。洗練されてきれいになる前には、まず無骨な姿を経験するものです! :) ## エージェントのフロントエンド UI をレンダリングする方法はたくさんありますが、私はフロントエンド開発が特別得意というわけではありません。そのため、デザイン周りはすべて Gemini CLI に任せることにしました。結果として、私が自分で作るよりもはるかに素晴らしいものに仕上げてくれました。 ここでは、PoC(概念実証)として最初の UI を表示するための非常にシンプルなアプローチを紹介します。ですがその後は、実際にフロントエンドに詳しい人に相談するか、私と同じように Gemini CLI を試してみることを強くおすすめします。 何らかの形で HTML を配信する必要があるため、手っ取り早い方法として FastAPI に新しいエンドポイントを作成します: ```py from fastapi.responses import HTMLResponse # see the full content below HTML_CONTENT=""" ... """ # --- Web UI Endpoint --- @app.get("/", response_class=HTMLResponse) async def get_chat_ui(): return HTML_CONTENT ``` もちろん、ここで重要なのは実際の HTML コンテンツです。チャットウィンドウ、入力ボックス、エージェントにメッセージを送信するボタンの3つの要素を定義します。 スニペットを短く保つため、以下のコードからはスタイル情報をすべて省いています。無骨ですが機能的です: ```html AIDA Chat

AIDA Chat

``` 再び `uvicorn main:app` を実行してホームページにアクセスすると、以下のような画面が表示されます。メッセージを送信してみてください。 ![Barebones chat page](image-2.png "This is how the world would look without designers") シンプルにするためにすべてを1つのファイルにまとめていますが、実際には HTML、CSS、JS、アセットファイル用のフォルダ(通常は `static`)に分けるのが適切です。正しい拡張子を使うことで、IDE がコードを理解しやすくなります。Gemini CLI にとってシンタックスハイライトは関係ありませんが、コードを手動で確認したり微調整したりするときに役立ちます。 ## デザインを洗練させる 正直なところ、ここからの作業は完全に Gemini CLI による魔法です。私の目標は、Sito さんが作ったようなレトロ・サイバーパンク・キュート・アニメなインターフェースを、自分好みのテイストで再現することでした。 そこで少しズルをして、Sito さんのツイートから保存したスクリーンショットのスタイルを再現するよう Gemini CLI に依頼しました。スクリーンショットを `image.png` として保存し、CLI に次のプロンプトを与えました: > I would like to update the UI in @demo.py to an aesthetic that resembles this interface @image.png > (@demo.py の UI を、このインターフェース @image.png に似たテイストにアップデートしてください) Gemini CLI では、`@` 文字をつけることで指定したリソース(ファイル)を読み込ませることができます。 ![Screenshot of the Gemini CLI showing the prompt used to generate the new UI.](https://danicat.dev/ja/posts/building-aida/image-4.png) そして、Gemini が作成してくれたのがこちらです: ![The new UI for the AIDA agent, generated by Gemini CLI, featuring a retro-cyberpunk aesthetic with a chat window and an avatar.](https://danicat.dev/ja/posts/building-aida/image-3.png) これが可能なのは、Gemini 2.5 がマルチモーダルであり、画像を実際に「理解」できるからです。言葉でうまく説明できないとき、私はよくこの手法を使ってモデルにやりたいことを伝えています。百聞は一見にしかず、ですよね? ### Nano Banana によるアセット生成 インターフェースは良くなりましたが、決定的なパーツが欠けています。それがアバターです。この問題を解決するために、もうひとつの CLI テクニックを使いました — Gemini CLI 向けの [Nano Banana](https://github.com/gemini-cli-extensions/nanobanana) 拡張機能をインストールしたのです。 この拡張機能を使うと、別のツールに切り替えることなく画像を生成できます。Nano Banana は新規の画像生成だけでなく編集にも優れているため、ベース画像から変更を加えて新しいフレームを生成し、アニメーションを作成するツールとして非常に効果的です。 私が使ったプロンプトは次の通りです: > create an avatar for the agent in @demo.py. the avatar should be a 2d anime girl in PC-98 style. make so it is looking at the "camera" in an idle pose > (@demo.py のエージェント用アバターを作成してください。PC-98 スタイルの 2D アニメ少女で、待機ポーズで「カメラ」を見ているようにしてください) Nano Banana 拡張機能がインストールされていれば、Gemini CLI がそれを呼び出して画像を生成します。拡張機能をインストールしたくない場合は、Gemini アプリや Web 上で同じプロンプトを実行しても構いません。 最初に出てきた結果がこの画像でした: ![Initial 2D anime girl avatar in PC-98 style, generated by the Nano Banana extension.](https://danicat.dev/ja/posts/building-aida/image-5.png) これに手動でトリミングを加え、顔だけにフォーカスさせました: ![Cropped version of the AIDA avatar, focusing on the face.](https://danicat.dev/ja/posts/building-aida/image-6.png) シンプルな会話アニメーション(口パク)を作るために、この画像をベースにして2枚目のフレームを生成させました: > modify static/assets/aida.png to create a new asset with the exact same pose but the character is with the mouth open, speaking > (static/assets/aida.png を修正し、まったく同じポーズで口を開けて話している新しいアセットを作成してください) その結果がこちらです: ![Second frame of the AIDA avatar with an open mouth, for the talking animation.](https://danicat.dev/ja/posts/building-aida/image-7.png) ファイルを整理するため、すべての `png` ファイルを保存する `static/assets` フォルダを作成しました。HTML コンテンツと同様に base64 でインライン化することもできましたが、可哀想な Python スクリプトが肥大化して(しかも散らかって)しまいます。 次に、これらのファイルを配信するコードが必要です: ```py # --- Static assets --- @app.get("/idle") async def idle(): return FileResponse("static/assets/idle.png") @app.get("/talk") async def talk(): return FileResponse("static/assets/talk.png") ``` そして、これらのエンドポイントのいずれかからの画像で `avatar-container` を埋めるように HTML を編集します: ```html
AIDA Avatar
AIDA
``` その結果がこちら: ![The AIDA chat interface with the idle avatar displayed.](https://danicat.dev/ja/posts/building-aida/image-8.png) 形が見えてきました! ### アニメーションの実装 アニメーションの追加はそれほど難しくありませんが、必要なすべてのポーズのフレームを用意する必要があります。すでに `talk` と `idle` を作成してあるので、これらのフレームを交互に切り替えることでシンプルな発話アニメーションを生成できます。 このロジックはシンプルな状態関数にカプセル化できます: ```js let talkInterval = null; function setAvatarState(state) { const avatarImg = document.getElementById('avatar-image'); if (state === 'talking') { if (!talkInterval) { talkInterval = setInterval(() => { // Toggle between talk and idle frames const isTalking = avatarImg.src.endsWith('/talk'); avatarImg.src = isTalking ? '/idle' : '/talk'; }, 150); } } else { // Stop animation and reset to idle if (talkInterval) { clearInterval(talkInterval); talkInterval = null; } avatarImg.src = '/idle'; } } ``` 発話アニメーション効果を実現するには、エージェントがデータの送信を開始したときに `setAvatarState('talking')` を呼び出し、終了したときに `setAvatarState('idle')` を呼び出すだけです。 ## ストリーミングでアバターに命を吹き込む パズルの最後のピースは、アバターの会話アニメーションをエージェントのストリーミング応答と同期させ、本当に生きているかのように見せることです。そのためには、リアルタイムデータを処理できるようにバックエンドとフロントエンドの両方を修正する必要があります。 ### バックエンドの変更:エージェントの応答をストリーミングする 標準の HTTP リクエストは、レスポンス全体が準備できるまで何も送り返さずに待ちます。LLM エージェントの場合、これはモデルが「思考」して段落全体を生成する間、静止した画面を見つめ続けることを意味します。アバターを生き生きと感じさせるには、その沈黙を打ち破る必要があります。 FastAPI の `/chat` エンドポイントを `StreamingResponse` を使用するように更新し、テキストチャンクが生成された瞬間に `runner.run_async` イベントから直接 yield します。 ```python from fastapi.responses import StreamingResponse from google.genai.types import Content, Part @app.post("/chat") async def chat_endpoint(request: Request): data = await request.json() user_query = data.get("query") # Hardcoded for demo simplicity user_id = "demo_user" session_id = "demo_session" # Ensure session exists if not await session_service.get_session(APP_NAME, user_id, session_id): await session_service.create_session(APP_NAME, user_id, session_id) async def response_stream(): """Generates text chunks from the agent's events.""" async for event in runner.run_async( user_id=user_id, session_id=session_id, new_message=Content(role="user", parts=[Part.from_text(text=user_query)]), ): # We only want the final text response for this simple UI if event.is_final_response() and event.content and event.content.parts: for part in event.content.parts: if hasattr(part, "text") and part.text: yield part.text return StreamingResponse(response_stream(), media_type="text/plain") ``` ### フロントエンドの変更:ストリームの受信とアニメーション連動 バックエンドがストリーミングを行うようになったので、フロントエンドの JavaScript もこのストリームを受信してアバターの会話アニメーションをトリガーするように更新する必要があります。`submit` イベントリスナーを変更して `ReadableStream` を使用し、テキストが到着するたびに追加していきます。 また、操作全体を `try/finally` ブロックで囲みます。これにより、ネットワークリクエストやストリーム処理中にエラーが発生した場合でも、常に `setAvatarState('idle')` が呼び出され、アバターが無限に話し続けるループに陥るのを防ぐことができます。 ```js // ... inside the submit handler ... // Prepare AIDA's message container const aidaMsg = appendMessage('AIDA> ', 'aida'); try { const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; setAvatarState('talking'); const chunk = decoder.decode(value, { stream: true }); // Typing effect for (const char of chunk) { aidaMsg.textContent += char; chatWindow.scrollTop = chatWindow.scrollHeight; // Tiny delay for retro feel await new Promise(r => setTimeout(r, 5)); } } } catch (err) { appendMessage(`SYSTEM> Error: ${err.message}`, 'system'); } finally { setAvatarState('idle'); } ``` ## 完成した結果 これらの変更により、私たちの AIDA エージェントは完全にインタラクティブで視覚的にも魅力的なインターフェースを手に入れました。アバターに命が吹き込まれ、ストリーミングされるレスポンスと同期して話すことで、はるかに没入感のある体験が生まれます。 ![AIDA in action](aida-demo.gif "It's alive!") ## おわりに・ソースコード 基本的な ADK 開発用 UI から、ずいぶんと遠くまでやってきました。Gemini CLI や Nano Banana 拡張機能のような生成 AI ツールを活用して、ユニークなレトロ・サイバーパンク・キュート・アニメな世界観を作り出しながら、カスタムフロントエンドを構築する方法を探ってきました。 この記事では ADK エージェント用フロントエンドの構築の基本を取り上げましたが、これはほんの出発点にすぎません。今回構築したデモの完全なソースコードは以下からダウンロードできます: * **[demo.py をダウンロード](https://danicat.dev/ja/posts/building-aida/demo.py)** エージェントのより発展的なバージョンに興味がある方は、私の GitHub でご覧いただけます:**[github.com/danicat/aida](https://github.com/danicat/aida)** ぜひリポジトリを探索し、自分で動かしてみて、よければコントリビュートもしてみてください!これらのビルディングブロックが実際のアプリケーションでどのように組み合わさるかを理解する素晴らしい方法になるはずです。 本シリーズの次回「[ADK、Ollama、SQLite で完全オフラインなエージェントを構築する方法](https://danicat.dev/ja/posts/20251103-building-aida-part-2/)」では、Ollama 経由のローカル Qwen 2.5 とローカル SQLite RAG を使用して、ネットワーク障害時でも完全に動作する AIDA の構築を掘り下げます。 ## 参考資料 * **[Agent Development Kit (ADK)](https://google.github.io/adk-docs/)**: Google ADK の公式ドキュメント。 * **[Gemini](https://gemini.google.com/)**: Google の AI アシスタント。 * **[osquery](https://osquery.io/)**: osquery の公式サイト。 * **[Vertex AI](https://cloud.google.com/vertex-ai/docs/start/introduction-unified-platform)**: Google Cloud の統合 AI プラットフォーム。 * **[FastAPI](https://fastapi.tiangolo.com/)**: Python Web フレームワーク FastAPI の公式サイト。 * **[Sito さんのツイート](https://x.com/Sikino_Sito/status/1957645002533925235)**: UI デザインの着想の元となったツイート。 * **[Avatar UI Core](https://github.com/sito-sikino/avatar-ui-core)**: Sito さんのオープンソースプロジェクト。 * **[Nano Banana 拡張機能](https://github.com/gemini-cli-extensions/nanobanana)**: 画像生成用の Gemini CLI 拡張機能。 --- # Google 入社 6 か月の振り返り:DevRel、AI、そしてマイルストーン - **Markdown URL**: https://danicat.dev/ja/posts/six-months-at-google/index.md - **HTML URL**: https://danicat.dev/ja/posts/six-months-at-google/ - **Date**: 2025-10-29 - **Categories**: Perspectives - **Tags**: career, community, devrel, reflection - **Summary**: Google Developer Relations Engineer としての最初の 6 か月間の振り返り。5 か国 15 イベント登壇、MCP や AI エージェントのリスキリング、個人マイルストーンを総括。 今日は私にとって、とても特別な節目です。Google に入社して正式に 6 か月が経ちました! 半年がもう過ぎたなんて信じられません。この会社に加わる絶好の機会を何年も何年も待ち望んできましたが、まさに期待していた通りの素晴らしい日々だったと胸を張って言えます。刺激的な挑戦の連続、素晴らしい仲間たち、そして目を見張るほどのスピードで進むイノベーションに満ちた、実に濃密な旅路でした。 この記事では、これまでの歩みの中からいくつかのハイライトを振り返ってみたいと思います。 ## 怒涛のスピード感 この 6 か月を一言で言い表すなら、間違いなく「*intense*(濃密、怒涛)」です。 ![ロンドンの 6PS で開催された Gain.pro ミートアップ](image02.jpg "ロンドンの 6PS で開催された Gain.pro ミートアップ") 光栄なことに、**5 か国 8 都市**にまたがる **15 のイベント**に参加・登壇する機会に恵まれました(気になる方のために挙げると、ロンドン、ベルリン、サンパウロ、クリチバ、ブレッチリー、フィレンツェ、バルセロナ、マラガです)。 開発者と直接顔を合わせ、彼らが何を開発しているのかを聞き、知識を共有することは常に私の情熱であり、これをこのスケールで体験できたのはまさに「唆る(exhilarating)」ものでした。(そう、AI が「exhilarating」という単語を提案してきたのですが、私は大の [Dr. STONE](https://www.nicovideo.jp/watch/sm36919684) ファンなので、今回は良しとしましょう。) ![I/O Connect Berlin での DevRel チーム](image03.jpg "I/O Connect Berlin での DevRel チーム") コンテンツ面では、このブログで **15 本の記事**を執筆し、[Gemini CLI と Go MCP の Codelab](https://codelabs.developers.google.com/cloud-gemini-cli-mcp-go?hl=ja#0) を作成しました。また、オープンソースや各種ドキュメントへの小さな貢献もいくつか行いました。 ### 自分自身のリファクタリング 世の中に JavaScript フレームワークの増殖スピードよりも速く増えているものが 1 つだけあるとすれば、それは AI ツールです。 私個人として最もエネルギーを費やしたのは、AI 開発の凄まじいスピードについていくことでした。エコシステムの進化は留まることを知りません。Vibe Coding や Agents から、Model Context Protocol (MCP)、そして絶え間なく登場する新しいツールやモデルに至るまで、そのすべてをキャッチアップし続けるのは並大抵のことではありません。 ![サンパウロの TDC での登壇風景](image07.jpeg "サンパウロの TDC での登壇風景") 4 月に Google での旅をスタートさせたとき、私は MCP が何なのかさえ知らず、自分自身で vibe coding を試した経験もほとんどありませんでした。それが 6 か月経った今では、それらの技術にどっぷりと浸かる毎日を送っています。 以前の私は、いつも変わらない「また新しい CRUD API を作る仕事か……」というルーティンに正直うんざりしていたので、自分自身をリスキリングするこの機会を得られたことは本当に幸運でした。そうした知識が今や時代遅れになったと言うつもりはありません(基礎的な知識は今でも極めて重要です)。しかし、すべてのコードを自分自身で手書きしなくてよくなったのは本当にありがたいことです。ワークフローに AI を導入したことは大きな転換点となり、私のエンジニアキャリアに新たな命を吹き込んでくれました。 ![GopherCon UK での基調講演](image04.jpg "GopherCon UK での基調講演") ## コミュニティとのつながり これほど急速に変化する環境の中では、圧倒されて途方に暮れてしまいがちです。そこで私の戦略は、自分が最も得意とすること、すなわち **Python** と **Go** のコミュニティに寄り添い続けることにフォーカスすることでした。 ![フィレンツェの GoLab で仲間の Gopher たちと](image09.jpeg "フィレンツェの GoLab で仲間の Gopher たちと") もちろん、すべての開発者コミュニティを心から大切に思っていますし、世界中のエンジニアと交流する楽しさは言葉では言い尽くせません。それでも、この 2 つのエコシステムにしっかりと軸足を置くことで、より明確な目的意識と集中力を持って AI 革命の波を乗りこなすことができました。 ![クリチバでの Women Who Go ミートアップ](image06.jpeg "クリチバでの Women Who Go ミートアップ") ADHD の気質を持つ私の脳は、あらゆる場所に首を突っ込みたがりますが、やはりフォーカスが不可欠だと実感しています。ひとりの人間にとって 2 つのコミュニティに関わることさえ多すぎる、と言う人もいるかもしれませんが、2017 年の初めにカンファレンス登壇を巡り始めて以来、Python と Go は私にとってずっと特別な存在であり続けています。 2018 年の Python Brazil でオープニング基調講演を務めたことは、2024 年の GopherCon(シカゴ)での基調講演と並び、今でも私のキャリアにおける最高のハイライトです。シカゴといえば、近いうちに Go のミューテーションテスト(mutation testing)のテーマを再訪する予定ですので、ぜひお楽しみに! ## もうひとつの個人的な勝利 仕事面での成果も素晴らしいものですが、Google 入社以来の最大の達成感は、実はプライベートなところにあります。なんと **16 kg の減量**に成功しました(そして現在も継続中です)! Google オフィスの充実した無料のカフェテリアやスナック、そして数々のイベント遠征を思えば、この減量ミッションは間違いなく「ハードモード」でした!これだけは言わせてください。**大好きな仕事に情熱を注ぐことは、メンタルヘルスに驚くほど素晴らしい効果をもたらす**ということです。 ![Google Cloud Day Barcelona にて](image10.jpeg "Google Cloud Day Barcelona にて") 互いを支え合える環境に身を置き、心から満たされる仕事をしていると、人生のあらゆる側面にポジティブな波及効果が連鎖していくのだと実感しています。 ## これからに向けて このような刺激的な時代に Google の一員であれることを、心から誇りに思っています。私たちは今まさに未来を創り出しており、その渦中に身を置けることは夢の実現そのものです。 次の 6 か月、そしてその先を見据えると、感謝とワクワク感で胸がいっぱいです。学ぶべきことはまだまだ山ほどあり、繋がるべきコミュニティも数多くあります。そしてもちろん、もっとたくさんの「vibe coding」を実践していくつもりです。 夢を見ることを決してやめないでください。私にできたのですから、あなたにも絶対にできます! *追記(P.S.):誤解のないように言っておくと、私は生成 AI が登場するずっと前からダッシュ(—)を使った文章を書いてきました。もっとも、この記事の推敲には Gemini の力を少し借りましたが :)* --- # Agent Development Kit で診断エージェントを作成する方法 - **Markdown URL**: https://danicat.dev/ja/posts/diagnostic-agent-with-adk/index.md - **HTML URL**: https://danicat.dev/ja/posts/diagnostic-agent-with-adk/ - **Date**: 2025-10-21 - **Categories**: Agent Development - **Tags**: adk, gemini, python, rag, tutorial, vertex-ai - **Series**: Building the Diagnostic Agent - **Summary**: Google ADK、osquery、Vertex AI RAG を使い、自動スキーマ探索とマルチレベルの健全性チェックを備えた自律型システム診断エージェントを Python で構築します。## はじめに 前回の記事からずいぶんと時間が空いてしまいました。ヨーロッパ各地のカンファレンスやミートアップで飛び回っており(途中で南米へのちょっとした寄り道もありました)、非常に忙しく過ごしていました。特にこの時期は、9月下旬から12月初旬にかけて多くのカンファレンスが集中するため、私たちデベロッパーリレーションズ(DevRel)にとって怒涛のシーズンとなります。 それでも、旅先で素晴らしい人たちと出会えるからこそブログへのインスピレーションが湧きますし、ブログ記事が新たな登壇のきっかけになることもよくあるので、どちらか一方だけでは成り立ちません。 今回は、本シリーズの[第3部「緊急診断エージェント」](https://danicat.dev/ja/posts/20250611-system-prompt/) をさらに発展させていきます。低レベルな [Vertex AI SDK](https://cloud.google.com/vertex-ai/docs/python-sdk/overview?utm_campaign=CDR_0x72884f69_default_b427567312&utm_medium=external&utm_source=blog) の代わりに、[Agent Development Kit (ADK)](https://github.com/google/agent-development-kit) フレームワークを使うようにエージェントをリファクタリングします。これにより、以前に手作業で書いていたボイラープレートコードの多くが最初から標準で提供されるなど、数多くのメリットが得られることが実感できるはずです。 だからといって、過去の記事で得た知識が無駄になるわけではありません。特に問題が発生して診断・トラブルシューティングが必要になったときには、内部で何が起きているのかを把握しておくことが大いに役立ちます。ADK は、エージェントを開発する際の生活をずっと楽にしてくれる上位の抽象化レイヤーだと考えてください。 ## 前回の振り返り ずいぶんと間が空いてしまったので、緊急診断エージェントがどのようなものだったかを簡単に思い出しておきましょう。私はこのエージェントを、『スタートレック』シリーズに登場する「コンピューター」に着想を得て開発しました。作中で登場人物たちは、(キーボードを叩く代わりに)コンピューターに話しかけて診断コマンドなどを指示します。私の目標は、現在の生成 AI 技術を使ってその体験を再現することでした。 コンピューターと対話して診断を実行するという目標を達成するために、私たちは2つの要素を活用しています。リクエストを解釈する生成 AI モデルと、OS の情報をモデルに公開するための [osquery](https://osquery.io/) というツールです。osquery を使うことで、モデルは自身のトレーニングデータとシステムに関する外部情報を組み合わせることができるようになります。 基本的に、このエージェントは以下のコンポーネントで構成されています: - 大規模言語モデル(Gemini) - Gemini の振る舞いを定義するシステムプロンプト - osquery のバイナリ - osquery をプログラムから呼び出すための Python ライブラリ - osquery を呼び出すツールとして Gemini に提供する Python ラッパー関数 osquery がマルチプラットフォーム対応であり、ホストシステムによってスキーマが異なる場合がある点を踏まえ、前回はシステムプロンプト内で osquery のテーブルスキーマを Gemini に渡すというちょっとした最適化も加えました。 前回の実装で対応できていなかった点としては、実行したい個々の診断手順に関する具体的な指示をモデルに一切与えていなかったことや、テーブル名以外のスキーマを完全には指定していなかったことなどが挙げられます。今回は、ADK の力、[Vertex AI RAG](https://cloud.google.com/vertex-ai/docs/generative-ai/rag?utm_campaign=CDR_0x72884f69_default_b427567312&utm_medium=external&utm_source=blog)、そしていくつかのテクニックを駆使して、これらの制限に取り組んでいきます。それではまず、リファクタリングから始めましょう! ## ADK へのエージェントのリファクタリング ADK へのエージェントのリファクタリングは、想像以上に簡単です。これまでに ADK エージェントを書いたことがなくても心配いりません。SDK をインストールし、ルートエージェントの仕様を定義して、付属の CLI(わかりやすく `adk` という名前になっています)で実行するだけです。 まずはシンプルな `hello world` エージェントから始めて、そこから段階的に構築していきましょう。最初にお好みのパッケージマネージャーを使ってマシンに ADK をインストールします。 macOS または Linux をお使いの場合は、以下のコマンドを使用できます: ```sh $ mkdir adk-tutorial && cd adk-tutorial $ python3 -m venv .venv $ source .venv/bin/activate (.venv) $ pip install google-adk ``` **Note:** 私はオールドスクールなので今でも `pip` と `virtualenv` を使っていますが、新しいパッケージマネージャーである [`uv`](https://github.com/astral-sh/uv) を好む方もいるでしょう: ```sh $ mkdir adk-tutorial && cd adk-tutorial $ uv init $ uv add google-adk ``` これら2つのアプローチの唯一の違いは、pip の場合は ADK CLI が `adk` コマンドとしてそのまま提供されるのに対し、`uv` の場合はデフォルトで `uv run adk` として呼び出す必要がある点です。 インストールが完了したら、`adk create [agent-name]`(または `uv adk create [agent-name]`)でテンプレートエージェントを作成できます: ```sh (.venv) $ adk create hello-agent ``` 作成ウィザードでは、モデルのバージョンとバックエンド(Gemini または [Vertex AI](https://cloud.google.com/vertex-ai?utm_campaign=CDR_0x72884f69_default_b427567312&utm_medium=external&utm_source=blog))の選択を求められます。ここでは Project ID とロケーションで認証できるように、`gemini-2.5-flash` と `Vertex AI` を使用します。 ```sh (.venv) $ adk create hello-agent Choose a model for the root agent: 1. gemini-2.5-flash 2. Other models (fill later) Choose model (1, 2): 1 1. Google AI 2. Vertex AI Choose a backend (1, 2): 2 ``` Vertex AI の場合、モデルが実行されるロケーションを気にせず使いたいときは、ロケーションを `global` に設定できます。特定のリージョンを指定したい場合は、`us-central1` などのアベイラビリティゾーンを選択してください。 ウィザードが完了すると、ファイルがディスクに書き込まれます: ```sh (...) Enter Google Cloud region [us-west1]: global Agent created in ~/adk-tutorial/hello-agent: - .env - __init__.py - agent.py ``` 重要なファイルは、環境設定を含み ADK 実行時に自動で読み込まれる `.env` と、エージェントのテンプレートコードが含まれる `agent.py` です。 生成された `agent.py` ファイルの内容は非常にシンプルです。全体像は以下の通りです: ``` from google.adk.agents.llm_agent import Agent root_agent = Agent( model='gemini-2.5-flash', name='root_agent', description='A helpful assistant for user questions.', instruction='Answer user questions to the best of your knowledge', ) ``` これは ADK の Dev-UI インターフェースを使ってテストできる完全なエージェントです。コマンドラインで `adk web` を実行するだけで、マシン上の `http://localhost:8000` で Web インターフェースが起動します。あっという間ですね! ## ADK による診断機能の実装 以前に Vertex AI SDK を使ったことがある方なら、コードがいかに簡潔になったかにすでに気づかれたはずです。完全に動作するエージェントを用意するために必要なのは、1つのエントリーポイントエージェント `root_agent` と少々の設定を定義することだけです。 それでは、この「hello world」エージェントに診断機能を追加して、次の段階に進めましょう。まずはお使いの OS 向けの[公式ドキュメント](https://osquery.readthedocs.io/en/stable/)の手順に従って、osquery バイナリをインストールします。 次に、Python バインディングをインストールします: ```sh (.venv) $ pip install osquery ``` ADK では、同じフォルダ構造の中に複数のエージェントを配置できる点に注目してください。先ほど `adk-tutorial` フォルダ内に `hello-agent` というエージェントを作成しました。ここで再度 `adk create` を実行すると、同じ構造内に2つ目のエージェントを作成できます: ```sh (.venv) $ adk create diag-agent ``` ADK の Web インターフェースはすべてのサブフォルダを個別のエージェントとして認識するため、複数存在する場合は画面右上のコンボボックスから切り替えることができます: ![agent selection combo](https://danicat.dev/ja/posts/diagnostic-agent-with-adk/image.png) それでは、`osquery` を呼び出すために必要なコードと適切なエージェント指示を含めて `agent.py` を更新しましょう: ```py from google.adk.agents.llm_agent import Agent from google.adk.tools import FunctionTool import platform import osquery import json def run_osquery(query: str) -> str: """Runs a query using osquery. Args: query: The osquery query to run. Returns: The query result as a JSON string. """ instance = osquery.SpawnInstance() instance.open() result = instance.client.query(query) return json.dumps(result.response) root_agent = Agent( model='gemini-2.5-flash', name='emergency_diagnostic_agent', description='A helpful assistant for diagnosing computer problems.', instruction=f"""This is an Emergency Diagnostic Agent. Your purpose is to support the user in diagnosing computer problems. You have access to the operating system's information via osquery. The current operating system is {platform.system()}. If the user doesn't give you an immediate command, ask the user 'What's the nature of your diagnostic emergency?'""", tools=[FunctionTool(run_osquery)], ) ``` `adk web` を実行して、いくつかのクエリを送信してエージェントをテストしてみましょう: !["ADK UI with the query 'show me this machine os, version and uptime'"](https://danicat.dev/ja/posts/diagnostic-agent-with-adk/image-1.png) ## システムプロンプトの再考 システムプロンプト(システム指示とも呼ばれます)は、すべてのエージェントの中核となる要素です。システムプロンプトはエージェントに使命と個性を与える最下層のプロンプトであり、エージェントが一貫した応答を返せるように優れたシステムプロンプトを構築することが極めて重要になります。 ADK において、システムプロンプトは以下の3つの要素で構成されます: - エージェントの内部名(`name`) - エージェントの説明(`description`) - エージェントへの指示(`instruction`) これらは、`root_agent` をインスタンス化する際に渡す引数に対応しています。 スタートレックのファンとしては、「レベル1診断手順(level 1 diagnostic procedure)」などの要求に対してエージェントが一貫して応答できるようにしたいところです。そこで、いくつかの診断レベルを定義してみましょう。改訂したより詳細なシステムプロンプトは以下の通りです: ```md This is an Emergency Diagnostic Agent. Your purpose is to support the user in diagnosing computer problems. You have access to the operating system's information via osquery. The current operating system is {platform.system()}. You can perform adhoc diagnostic queries based on the user's needs. For more structured and comprehensive analysis, you can execute one of the following predefined diagnostic procedures. Level 1: System Health Check Goal: A high-level overview of the system's current state and vital signs. * System Identity & Vitals: Gather hostname, operating system version, and system uptime. * CPU Status: Check overall CPU load and identify the top 5 processes by CPU consumption. * Memory Pressure: Report total, used, and free system memory. Identify the top 5 processes by memory consumption. * Disk Usage: List all mounted filesystems and their current disk space usage. * Running Processes: Provide a count of total running processes. Level 2: In-depth System & Network Analysis Goal: A detailed investigation including all of Level 1, plus network activity and recent system events. * (All Level 1 Checks) * Network Connectivity: List all active network interfaces and their configurations. * Listening Ports: Identify all open ports and the processes listening on them. * Active Network Connections: Report all established network connections. * System Log Review: Scan primary system logs for critical errors or warnings in the last 24 hours. Level 3: Comprehensive Security & Software Audit Goal: The most thorough analysis, including all of Level 2, plus a deep dive into software inventory and potential security vulnerabilities. * (All Level 2 Checks) * Installed Applications: Generate a complete list of all installed software packages. * Kernel & System Integrity: List all loaded kernel modules and drivers. * Startup & Scheduled Tasks: Enumerate all applications and services configured to run on startup or on a schedule. * User Account Review: List all local user accounts and identify which are currently logged in. If the user doesn't give you an immediate command, ask the user 'What is the nature of your diagnostic emergency?' ``` ここで再度エージェントを試してみると、「レベル1診断(level 1 diagnostic)」の意味を理解し、レポートを作成するために直ちに多数のツール呼び出しを実行する様子が確認できます: ![ADK UI showing agent running a level 1 diagnostic procedure](https://danicat.dev/ja/posts/diagnostic-agent-with-adk/image-2.png) ## Vertex AI RAG による回答品質の向上 上記のシステムプロンプトは手順を明確化しエージェントの存在意義を定義する上でうまく機能しますが、実際の実行段階になると、必ずしも期待通りの結果が得られないことに気づくかもしれません。 例えばテスト中、エージェントが私の OS(macOS を使用しています)上で空になっているテーブルに対してクエリを発行してしまう場面によく遭遇しました。これは、このデータとどのようにやり取りすべきかについて、モデルがより多くのコンテキスト知識を必要としている明確なサインです。 ![ADK window showing a query with empty results](image-5.png "A common problem: memory_info is empty on MacOS, but the model doesn't know that") 基盤モデルの能力を超えてエージェントの知識を補強する方法には、コンテキストエンジニアリング、ツール呼び出し、MCP リソース、検索拡張生成(RAG)、モデルの特化など、いくつかの選択肢があります。 今回の診断エージェントに関しては、osquery が長年にわたって公開されている広く知られたオープンソースプロジェクトであるため、オープンソースコードと Web 上の記事の双方を通じて LLM の学習データに含まれており、osquery の一般的な仕組みについての知識はすでに持っていると私は推測していました。 しかしモデルには、より具体的なシナリオでどのように振る舞うべきかという細かなニュアンスが欠けているようでした。システムプロンプトに動的にプラットフォーム名を追加することも多少は役立ちましたが、それだけでは不十分でした。そこで、RAG の仕組みを利用してエージェントに osquery の完全なスキーマ情報を把握させるアプローチを考えました。 RAG の背後にあるコンセプトは、モデルに対して情報を「必要に応じて(need-to-know basis)」供給することです。リアルタイムに取得したい情報をベクトルデータベースに保存しておき、ユーザー(またはエージェント)がクエリを発行した際に、ベクトル検索を使ってリクエストに最も類似したデータセグメントを見つけ出して取得し、モデルが処理する前にコンテキストを充実させます。 この診断エージェントでは、osquery の完全なスキーマをオンデマンドで取得できるようにします。例えば「memory」に関する情報をリクエストした場合、RAG 検索はベクトル空間内で「memory」に近いテーブルを探索し、リクエストを処理する前に関連テーブルとその完全なスキーマを取得します。これにより、モデルがより適切な osquery 呼び出しを選択できるようになります。 これを機能させるには、ベクトルデータベースに関連データを投入した上で、新しいツールを提供してそれを取得する方法をエージェントに「教える」必要があります。このツールを `discover_schema` と呼ぶことにしましょう。 ### Vertex AI RAG のセットアップ 最初に行う必要があるのは、Vertex AI RAG で新しいコーパス(corpus:データのコレクションを表す用語)を作成することです。 コーパスの情報源となるのは、[osquery の GitHub ページ](https://github.com/osquery/osquery) の [specs フォルダ](https://github.com/osquery/osquery/tree/master/specs) から取得できる osquery スキーマです。 コーパスを作成する非常に便利な方法は、[Google Cloud Storage](https://cloud.google.com/storage?utm_campaign=CDR_0x72884f69_default_b427567312&utm_medium=external&utm_source=blog) や Google ドライブからフォルダをアップロードすることですが、Slack や SharePoint などの他のデータソースも利用できます。Google Cloud Console のコーパス作成ウィザード(Vertex AI -> RAG Engine -> Create corpus)を使うか、Vertex AI SDK を使ってプログラムから作成することも可能です。 ![Create corpus wizard in Vertex AI RAG](https://danicat.dev/ja/posts/diagnostic-agent-with-adk/image-3.png) 今回のケースでは、osquery の GitHub リポジトリをローカルマシンにクローンし、`specs` フォルダのコピーを Google Cloud Storage バケットに作成した上で、Cloud Console を使ってバケットからコーパスを作成しました。1点注意が必要なのは、`specs` 内のテーブル定義ファイルの拡張子が `.table` になっているため、Vertex AI RAG が認識して処理できるようにすべてのファイルを `.txt` にリネームする必要がある点です。 シンプルなシェルコマンドを使って、この一括リネーム操作を実行できます: ```sh # In the directory with the .table files for f in *.table; do mv -- "$f" "${f%.table}.txt"; done ``` インポートが完了すると、以下のような画面が表示されるはずです: ![osquery schema corpus in Vertex AI RAG](https://danicat.dev/ja/posts/diagnostic-agent-with-adk/image-4.png) 次に、エージェントがこのコーパスにアクセスできるようにするためのツール定義を作成する必要があります。 ### Schema Discovery Tool このツールを動作させるには、作成したコーパスのリソース名が必要です。コンソールのコーパスの「Details」タブに表示されており、形式は `projects/[PROJECT-ID]/locations/[LOCATION]/ragCorpora/[CORPORA_ID]` のようになっています。 このパスを持つ環境変数を `.env` ファイルに作成します。名前は `RAG_CORPORA_URI` にしましょう。`.env` ファイルは以下のようになります: ```txt GOOGLE_GENAI_USE_VERTEXAI=1 GOOGLE_CLOUD_PROJECT=[PROJECT-ID] GOOGLE_CLOUD_LOCATION=[LOCATION] RAG_CORPORA_URI=projects/[PROJECT-ID]/locations/[LOCATION]/ragCorpora/[CORPORA_ID] ``` 続いて、以下のツール定義を `agent.py` ファイルに追加します。新しいインポート文もお忘れなく! ```py import os import vertexai from vertexai.preview import rag from google.protobuf.json_format import MessageToDict vertexai.init() def discover_schema(search_phrase: str) -> str: """Discovers osquery table names and schemas based on a descriptive search phrase. Args: search_phrase: A phrase describing the kind of information you're looking for. For example: 'user login events' or 'network traffic'. Returns: Table names and schema information for tables related to the search phrase. """ rag_corpora_uri = os.environ.get('RAG_CORPORA_URI') response = rag.retrieval_query( rag_resources=[ rag.RagResource( rag_corpus=rag_corpora_uri, ) ], text=search_phrase, ) return json.dumps(MessageToDict(response._pb)) ``` さらに、新しいツールが利用可能になったことをエージェントに知らせるために、エージェントの定義を更新します: ```py root_agent = Agent( model='gemini-2.5-flash', name='emergency_diagnostic_agent', description='A helpful assistant for diagnosing computer problems.', instruction=... # omitted for brevity tools=[ FunctionTool(run_osquery), FunctionTool(discover_schema), # new tool definition ], ``` 最後に、必須というわけではありませんが、私は自分のエージェントでスキーマ探索を徹底させたいため、指示(instruction)に以下の文言を追加しました: ```txt You MUST run schema discovery for all requests unless the schema is already known. ``` これは末尾に追加しても、診断レベルを定義する直前に追加しても構いません。 ここでエージェントを再起動し、`adk web` で再度実行すると、スキーマ探索が実際に動作する様子が確認できるようになります: ![Diagnostic agent with RAG schema discovery enabled](https://danicat.dev/ja/posts/diagnostic-agent-with-adk/image-6.png) スキーマ探索の有無による応答の違いをぜひ実際に動かして比較してみてください。私のテストでは、品質の差は非常に歴然としていました。 ## おわりに ずいぶんと長くなってしまいましたが、楽しんで読んでいただけたなら幸いです!もしご自身で診断エージェントをセットアップする際につまずいた点があれば、ぜひ教えてください。イベントで極端に忙しい時を除き、[LinkedIn](https://www.linkedin.com/in/petruzalek) でのご連絡には比較的早く返信しています。また、このエージェントをどのように拡張したかや、試してみた実験などについてもぜひお聞きしたいです。 本シリーズの次回作 [Dev-UIの先へ:ADKエージェントのインターフェースを構築する方法](https://danicat.dev/ja/posts/20251031-building-aida/) では、標準の `adk web` デバッグ画面から一歩踏み出し、FastAPI によるストリーミング対応のカスタムランタイムと、レトロ風のインタラクティブなアバター UI(AIDA)を構築します。 ## 参考リンク * [Agent Development Kit (ADK)](https://github.com/google/agent-development-kit) * [osquery](https://osquery.io/) * [osquery GitHub page](https://github.com/osquery/osquery) * [Vertex AI RAG](https://cloud.google.com/vertex-ai/docs/generative-ai/rag?utm_campaign=CDR_0x72884f69_default_b427567312&utm_medium=external&utm_source=blog) --- # Hello, MCP World!:Goで始めるModel Context Protocol入門 - **Markdown URL**: https://danicat.dev/ja/posts/hello-mcp-world/index.md - **HTML URL**: https://danicat.dev/ja/posts/hello-mcp-world/ - **Date**: 2025-08-17 - **Categories**: Agent Development - **Tags**: gemini, golang, keynote, mcp - **Summary**: GopherCon UK 2025 の登壇に基づく Model Context Protocol(MCP)の入門解説。Host、Client、Server のアーキテクチャと Tool/Resource/Prompt を解説。## はじめに この記事では、Anthropic が開発した、大規模言語モデル(LLM)とアプリケーション間の通信を標準化するプロトコル「Model Context Protocol(MCP)」について詳しく解説します。本稿は、先週 [GopherCon UK で発表した同名の基調講演(Keynote)](https://speakerdeck.com/danicat/hello-mcp-world) をベースに構成しています。 理解を深めるため、まずは基礎からスタートし、主要なアーキテクチャコンポーネント、トランスポート、そして 3 つのビルディングブロック(Tools、Prompts、Resources)について順を追って説明します。道中では、私が以前作成したサーバー(GoDoctor や Speedgrapher)の実践的なコード例も紹介します。最後に、Gemini CLI を使ったシンプルな「バイブコーディング(vibe coding)」の実例を通して、Go SDK for MCP を用いた独自サーバーの作成手順を見ていきます。 MCP について初めて耳にする方から、すでに自作サーバーをいくつか開発した経験のある方まで、さまざまなスキルレベルの開発者にとって役立つ知見をお届けします。 ## 新しい標準の誕生 標準(スタンダード)の話になると、いつも XKCD のこのコミックが真っ先に頭に浮かびます。 ![Standards](https://danicat.dev/ja/posts/hello-mcp-world/image.png) *出典: [xkcd.com](https://xkcd.com/927)* 面白いことに、この業界においてこのジョークが完全には当てはまらない稀有な事例が、今回の MCP かもしれません(少なくとも今のところは)。幸いなことに、業界全体が LLM にコンテキストを渡す標準として、急速に MCP へと収束していきました。 公式の仕様書では、MCP は次のように説明されています。 > MCP は、アプリケーションが大規模言語モデル(LLM)にコンテキストを提供する方法を標準化するオープンプロトコルです。MCP は、いわば AI アプリケーションにおける USB-C ポートのようなものだと考えてください。USB-C がデバイスとさまざまな周辺機器やアクセサリを接続する標準規格となっているのと同様に、MCP は AI モデルを多様なデータソースやツールと接続するための標準的な手段を提供します。MCP を使うことで、LLM 上にエージェントや複雑なワークフローを構築し、モデルを世界とつなぐことができます。 USB-C の例えもよく分かりますが、個人的には MCP を「新たな HTTP/REST」と捉える方がしっくりきます。HTTP が Web サービス同士が通信するための共通言語となったように、MCP は AI モデルが外部システムとやり取りするための共通フレームワークを提供します。エンジニアとして、私たちは過去 20 年ほどをかけてあらゆるものを「API ファースト」にし、ソフトウェアシステムを相互接続して新たな次元の自動化を実現してきました。この先 20 年とまではいかなくても、今後 5〜10 年の間、既存のシステムを AI 対応へと改修し(そして新たなシステムを立ち上げ)、莫大なエンジニアリングリソースを投じることになるはずです。そして MCP こそが、そのプロセスの要となるコンポーネントなのです。 ## MCP アーキテクチャ 以下の図を見ると、MCP のアーキテクチャは一見複雑そうに見えるかもしれません。 ![MCP Architecture](https://danicat.dev/ja/posts/hello-mcp-world/image-1.png) *出典: [MCP 仕様書](https://modelcontextprotocol.io/docs/learn/architecture)* MCP アーキテクチャの主要コンポーネントは以下のとおりです。 * **MCP Host(ホスト):** IDE やコーディングエージェントなどのメインとなる AI アプリケーション。 * **MCP Server(サーバー):** 特定の機能(ツールやプロンプトなど)へのアクセスを提供するプロセス。 * **MCP Client(クライアント):** ホストを個々のサーバーに接続するクライアント。 要するに、ホストアプリケーションが複数のクライアントを生成・管理し、各クライアントが特定のサーバーと 1:1 の関係で接続します。 ## MCP レイヤー 通信は 2 つのレイヤーで構成されています。 * **データレイヤー(Data layer)**: JSON-RPC ベースのプロトコルです。メッセージ形式の具体例は次のセクションで紹介します。 * **トランスポートレイヤー(Transport layer)**: 通信チャネルを定義します。主要なものは以下のとおりです。 - **Standard I/O(標準入出力 / stdio)**: ローカルサーバー向け - **Streamable HTTPS**: ネットワーク越しの通信向け(HTTPS+SSE を置き換えるもの) - **HTTPS+SSE**: セキュリティ上の懸念により、最新の仕様では非推奨(deprecated) データレイヤーは SDK 側で管理されるため、テスト目的を除けば手動でメッセージを組み立てる必要はありません。トランスポートの選択はユースケースによりますが、基本的にはまず stdio から始めて、必要に応じて後から HTTPS を追加するのがおすすめです。stdio の MCP サーバーを HTTPS に(あるいはその逆に)変換するオープンソースのアダプターも存在しますが、自作サーバーに HTTPS を組み込むこと自体非常に簡単なので、そうしたアダプターを使うのは自分でソースコードを管理できないサーバーの場合くらいでしょう。 ## 初期化フロー クライアントとサーバーは、接続確立時にハンドシェイクを行います。これには 3 つの主要なメッセージが関わります。 1. クライアントがサポートするプロトコルバージョンを指定して、サーバーへ `initialize` リクエストを送信する(サーバーはクライアントへ初期化レスポンスを返します)。 2. クライアントが `notifications/initialized` 通知を送信して初期化を確定する。 3. これにより、クライアントは `tools/list` などのリクエストを送信し、サーバーが提供する機能を取得できるようになります。 クライアント側から実際にやり取りされる初期化フロー(JSON-RPC 形式)は次のようになります。 ```json {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}} {"jsonrpc":"2.0","method":"notifications/initialized","params":{}} {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} ``` いきなり `tools/list` や `tools/call` メッセージを直接送信することはできない点に注意してください。ハンドシェイクを経ずに送信すると、「server not ready」といったエラーが返ってきます。 Gemini CLI などのコーディングエージェントを使って MCP サーバーを開発しているとき、私はよくシェル経由で以下のようにメッセージを流し込んでテストするようエージェントに指示します。 ```sh ( echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'; echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'; echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; ) | ./bin/godoctor ``` このフローを完全に把握する前は、コーディングエージェントが「サーバーの起動に時間がかかっているようなので、tool call の前に sleep を挟みます」といった見当違いな推測をしてしまうことがよくありました。そのため、実装が正しく動いているかを確実に検証するためにこの方法を好んで使っています。開発中の MCP サーバーと正しく通信する作法をエージェントへいち早く教え込むことが、開発をスムーズに進める秘訣です! ## MCP サーバーのビルディングブロック MCP サーバーの機能は、3 つの基本的なビルディングブロック(「プリミティブ」や「サーバーコンセプト」とも呼ばれます)を通じて公開されます。 | ビルディングブロック | 目的 | 制御主体 | 実世界の例 | | :------------- | :----------------------- | :---------------------- | :----------------------------------------------- | | **Tools(ツール)** | AI のアクション実行 | モデル主導(Model-controlled) | フライト検索、メッセージ送信、コードレビュー | | **Resources(リソース)** | コンテキストデータの提供 | アプリケーション主導(Application-controlled) | ドキュメント、カレンダー、メール、気象データ | | **Prompts(プロンプト)** | 対話テンプレート | ユーザー主導(User-controlled) | 「旅行の計画を立てる」「ミーティングを要約する」 | それぞれを詳しく見ていきましょう。 ### Tools(ツール) Tools は、AI モデルに特定のアクションを実行させるための関数です。たとえば、API やデータベース、コマンドラインツールなどを外部に公開します。 Tools の概念を実験するために私が作成したサーバーが GoDoctor です。これは、LLM が Go コードを書く際の支援能力を高めるためのツール群を提供します。GoDoctor という名前は、Go パッケージのドキュメントを表示する CLI ツール `go doc` にちなんだ言葉遊びです。 私の仮説は、「正確な公式ドキュメントを提供すれば、LLM のハルシネーション(幻覚)が減り、より品質の高いコードを書けるようになるのではないか」というものでした。あるいは少なくとも、自らの間違いを学習し、自己修正するための手掛かりが得られるはずだと考えました。 ツールの実装は、MCP サーバーへのツールの登録と、ハンドラーの実装という 2 つの主要コンポーネントで構成されます。 登録には `mcp.AddTool` 関数を使用します。 [View on GitHub: danicat/godoctor/internal/tools/get_documentation/get_documentation.go (lines 35-40)](https://github.com/danicat/godoctor/blob/main/internal/tools/get_documentation/get_documentation.go#L35-L40) ハンドラーは、API やコマンド、関数を呼び出して、プロトコルと互換性のある形式(`mcp.CallToolResult` 構造体)でレスポンスを返すアダプターの役割を果たします。 GoDoctor のドキュメント取得ツールのハンドラー実装は以下のようになっています。 [View on GitHub: danicat/godoctor/internal/tools/get_documentation/get_documentation.go (lines 49-86)](https://github.com/danicat/godoctor/blob/main/internal/tools/get_documentation/get_documentation.go#L49-L86) ### Prompts(プロンプト) Prompts は、パラメータを受け取ることができる再利用可能なユーザー主導のテンプレートを提供します。AI エージェント内ではスラッシュコマンドとして表示されることが多く、ユーザーはシンプルなコマンドひとつで複雑なワークフローをトリガーできます。 実際の動作例として、私が作成したもうひとつの MCP サーバー `speedgrapher` を見てみましょう。これはテクニカルライティングを支援するためのプロンプトとツールを集めたものです。 `speedgrapher` の中でも最もシンプルなプロンプトのひとつが `/haiku` です。Tools と同様に、プロンプトの定義とそのハンドラーの実装を行います。 [View on GitHub: danicat/speedgrapher/internal/prompts/haiku.go (lines 24-54)](https://github.com/danicat/speedgrapher/blob/main/internal/prompts/haiku.go#L24-L54) ### Resources(リソース) Resources はファイル、API、データベースなどのデータを公開し、AI がタスクを遂行するために必要なコンテキストを提供します。概念としては、**Tool が「アクションを実行するもの」**であるのに対し、**Resource は「情報を提供するもの」**です。 とはいえ、現実の開発シーンでは、ほとんどの開発者が(通常の API で GET リクエストを使うように)データの公開にも Tools を使っているため、Resources の優れた実装例にはまだあまりお目にかかれていません。これは仕様策定側が少し凝りすぎてしまった部分かもしれませんが、今後コミュニティが MCP に慣れていくにつれて、Resources の有効な活用パターンが見出されていくでしょう。 ## クライアントコンセプト(Client Concepts) サーバー側のビルディングブロックに加えて、MCP 仕様にはサーバーがクライアントに対して要求できる機能である **Client Concepts** も定義されています。具体的には以下のような機能があります。 * **Sampling(サンプリング):** サーバー側からクライアントのモデルへ LLM のテキスト生成(補完)を要求できるようにする機能。サーバー作成者がモデル呼び出しのために自前の API キーを用意する必要がなくなるため、セキュリティやコスト管理(課金)の観点から非常に有望です。 * **Roots(ルート境界):** クライアントがファイルシステムの操作境界を伝え、サーバーに対して操作が許可されているディレクトリを明示するメカニズム。 * **Elicitation(入力要求):** サーバーがユーザーに対して特定の情報を構造化された形式で要求し、必要な入力を得るまで処理を一時停止する仕組み。 これらもまた、私が調べた限りでは(サーバー・クライアント双方ともに)現実の実装が仕様に追いついていない領域のひとつです。これらの機能が広く普及するにはもう少し時間がかかるかもしれません。これぞ最先端(ブリーディングエッジ)技術を扱う課題でもあります……。たとえば、Gemini CLI に Roots サポートが追加されたのも、つい先週のことでした(https://github.com/google-gemini/gemini-cli/pull/5856 )。 ## ライブデモ: MCP サーバーのバイブコーディング お気に入りのコーディングエージェントに渡して「Hello World」サーバーを作らせるプロンプトの例を紹介します。現在のエージェントは非決定的(non-deterministic)なため、1 回で 100% 完璧にいかない場合もあります。その際は追加のプロンプトで微調整が必要になることもありますが、最初の足がかりとしては最適です。 ```text Your task is to create a Model Context Protocol (MCP) server to expose a "hello world" tool. For the MCP implementation, you should use the official Go SDK for MCP and use the stdio transport. Read these references to gather information about the technology and project structure before writing any code: - https://raw.githubusercontent.com/modelcontextprotocol/go-sdk/refs/heads/main/README.md - https://go.dev/doc/modules/layout To test the server, use shell commands like these: `( echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'; echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'; echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; ) | ./bin/hello` ``` エージェントがサーバーの作成に成功したら、新しく作ったツールに対して `tools/call` メソッドを実行させ、結果を確認してみましょう! ## 今後の展望 Go コミュニティは MCP エコシステムへ積極的に投資を行っています。特に注目すべき 2 つのプロジェクトを紹介します。 * **Go SDK for MCP:** デモで使用した公式 SDK で、Google と Anthropic のパートナーシップによって開発されています。現時点ではまだ実験的(執筆時点のバージョンは 0.20)ですが、十分に実用的であり活発に開発が進められています。[github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) で公開されています。 * **`gopls` の MCP サポート:** Go 公式の Language Server である `gopls` に MCP サポートを追加し、AI モデルによる Go のコーディング支援能力を大幅に強化する取り組みが進んでいます。プロジェクトはまだ初期段階ですが、[tip.golang.org/gopls/features/mcp](https://tip.golang.org/gopls/features/mcp) で進捗を追うことができます。 ## 注目の MCP サーバー コミュニティによって構築された注目すべきサーバーをいくつか紹介します。 * **Playwright:** Microsoft が開発・メンテナンスを行っているサーバーで、AI エージェントが Web ページを操作したり、スクリーンショットを撮影したり、ブラウザ操作を自動化できるようにします。[https://github.com/microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp) で入手できます。 * **Context7:** GoDoctor と同様に、モデルに公式ドキュメントを提供することでハルシネーションを抑制し、回答精度を高めるサーバーです。クラウドソーシングされたリポジトリからドキュメントを取得します。詳細は [https://context7.com/](https://context7.com/) をご覧ください。 ## 独自のサーバーを作ってみよう Model Context Protocol は、AI エージェントの機能を拡張するための標準化された手段を提供します。独自のサーバーを構築すれば、普段の開発ワークフローにぴったり合わせた、コンテキストを深く理解する専用アシスタントを作り出すことができます。 実際に手を動かしてみたい方のために、独自の MCP サーバーをゼロから構築するステップを解説した Google Codelab を作成しました。 [**How to Build a Coding Assistant with Gemini CLI, MCP and Go(Gemini CLI、MCP、Go でコーディングアシスタントを構築する)**](https://codelabs.developers.google.com/codelabs/gemini-cli-mcp-go) ## おわりに 最後までお読みいただきありがとうございました!ご質問や感想などがあれば、ページ下部のコメント欄や各種 SNS でぜひお気軽にお知らせください。 --- # 体感時間の数理モデル:なぜ年齢を重ねるほど時間は速く過ぎるのか - **Markdown URL**: https://danicat.dev/ja/posts/a-model-for-perceived-time/index.md - **HTML URL**: https://danicat.dev/ja/posts/a-model-for-perceived-time/ - **Date**: 2025-08-11 - **Categories**: Perspectives - **Tags**: reflection - **Summary**: 1/年齢の微分積分とPythonシミュレーションを用いた体感時間加速の数理モデリング。人生の知覚的な中間地点がなぜ9歳になるのかを解説します。 ## はじめに 大人になると、「年齢を重ねるほど時間の経過が速くなる」とよく実感します。ひとつの夏、1年、あるいは10年という歳月でさえ、かつて感じていた長さのほんのわずかな時間で過ぎ去っていくように感じられます。私の仮説はこうです。この現象は単なる気のせいではなく、私たちの脳が時間を知覚する仕組みから生じる、測定可能な必然であるということです。1年という時間は固定された経験の単位ではなく相対的なものであり、私たちがそれまで生きてきた総時間に対する割合として、その体感の長さが縮小していくのです。 この疑問はずっと私の頭から離れず、夜中の3時にふと目が冴えてしまったことが何度あったかわかりません。しかし、これまできちんとモデル化して考察するほどの数学的厳密さや気力はありませんでした。――今日、数学の計算を頼れるLLMインターンたちに委ねてみるまでは。これからご覧いただくのは、その探求の成果です。 本記事は、このアイデアに対する個人的かつ技術的な探求です。抽象的な感覚にとどまらず、時間の知覚的な加速を定量化するシンプルな数理モデルを構築することを目指しました。そしてこの探求は、私たちの人生の構造と、幼少期の経験が持つ極めて大きな意味についての、明快で深く考えさせられる結論へとたどり着きました。 ## 前提の整理:シンプルなモデル 知覚される時間の経過をモデル化するにあたり、まずは明快な関係性から出発します。「ある1年間の体感的な重み(知覚価値)は、その時点の年齢に反比例する」という関係です。数学的な観点から表すと、1年の価値は `1/age`(1/年齢)として表現できます。 この視点から見ると: - 人生の最初の1年間(1歳時)は、それまでに生きてきた全経験の100%に相当します(`1/1`)。 - 2年目(2歳時)は50%に相当します(`1/2`)。 - 42歳になる頃には、1年間が人生全体に占める割合はわずか約2.4%(`1/42`)にすぎません。 このモデルが示唆しているのは、私たちの時間認識が線形(リニア)ではなく対数的(ログスケール)であるということです。1年が経過するごとに、蓄積された総経験に寄与する割合は次第に小さくなり、その結果として「時間がどんどん加速している」という錯覚が生じます。これは簡略化したモデルではありますが、記憶や経験の構造を考察する上で非常に有用なフレームワークを提供してくれます。 ## 数理モデルによるアプローチ 純粋に数学的なアプローチを好む方に向けて、離散的な分数を個別に足し合わせることなく、人生の知覚的な中間地点(折り返し地点)を導き出す方法を見てみましょう。ある年齢 `t` までの累積体感時間は、関数 \[ f(x) = 1/x \] を `x=1` から `t` まで積分することでモデル化できます。 \[ f(x) = 1/x \implies \int_{1}^{t} f(x) = \ln(t) \] この積分の結果は、`t` の自然対数、すなわち `ln(t)` となります。 したがって、寿命 `L` を通じた総体感経験量は `ln(L)` で表されます。中間点 `M` は、累積体感時間が総量のちょうど半分になる年齢です。これにより、次の方程式が得られます。 \[ ln(M) = \frac{\ln(L)}{2} \] これを `M` について解くと、次のようになります。 \[ M = L^{1/2} \implies M = \sqrt{L} \] ここから直接的な結論が導かれます。**「人生の知覚的な中間地点は、寿命の平方根(ルート)である」** ということです。たとえば81年の寿命であれば、体感的な折り返し地点はちょうど **9歳** になります。この数学的な結果は、スクリプトによるモデルのベンチマークとなります。 当然ながら、より長く生きれば中間地点もそれに応じて移動しますが、このモデルの対数的な性質上、その変化はごくわずかです。たとえば寿命が100歳であっても、中間地点は10歳にしかなりません。 ## 探求のプロセス:体感時間の計算と可視化 ### 最初の試み:単純な離散和 このモデルが示唆する意味を探るため、最初のステップとして各年の離散値(`1/age`)を足し合わせて知覚的な中間地点を計算するPythonスクリプトを作成しました。これは、この概念をコードへと落とし込む最もストレートな方法に思えました。スクリプトの該当部分は以下の通りです。 [View on GitHub: danicat/danicat.dev/content/posts/20250811-a-model-for-perceived-time/time_perception_model_v1.py (lines 5-16)](https://github.com/danicat/danicat.dev/blob/main/content/posts/20250811-a-model-for-perceived-time/time_perception_model_v1.py#L5-L16) このスクリプトは、ある明確な結果を導き出しました。81年の寿命において、知覚的な中間地点は **7歳** という結果になったのです。 ![年齢に対する1年間の知覚価値のモデル(V1)](perceived_time_vs_age_v1.png "年単位のステップを用いた初期モデルの実行結果") しかし、この結果には問題がありました。数理モデルによる「9歳」という予測には近いものの、22%もの誤差は無視するには大きすぎました。この乖離が生じたのは、年単位の単純な総和が、積分によって描かれる滑らかで連続的な曲線の粗い近似にすぎないためです。特に `age=1` の最初の項の影響が極端に大きく出すぎてしまい、計算全体が歪んでしまったのです。 ### 第2の試み:洗練されたモデル より高精度なシミュレーションを作成するため、**月単位のタイムステップ**を用いるようスクリプトを改良しました。各月の知覚価値(`(1/12)/age_in_months`)を積算していくことで、連続的な時間の体験をよりきめ細かく近似できるようにしたのです。改良した計算のコア部分は以下の通りです。 [View on GitHub: danicat/danicat.dev/content/posts/20250811-a-model-for-perceived-time/time_perception_model_v2.py (lines 16-35)](https://github.com/danicat/danicat.dev/blob/main/content/posts/20250811-a-model-for-perceived-time/time_perception_model_v2.py#L16-L35) この新しいスクリプトは、数理モデルと極めて整合する結果を出しました。計算された知覚的な中間地点は **8.8歳** でした。 ![年齢に対する1年間の知覚価値のモデル(V2)](perceived_time_vs_age_v2.png "月単位のステップを用いた改良モデルにより、より正確な結果が得られた") このようにモデル化と改良を反復していくプロセスこそ、技術的な探求の中核です。最初の不完全な結果は失敗ではなく、モデルについてのより深い真実を浮き彫りにし、より堅牢で正確な結論へと導くために不可欠なステップだったのです。 ## 神経発達学との共通点 人生の体感時間が前半に偏重している(フロントロードされている)という結論は、単なる数学的な好奇心にとどまりません。これは神経発達生物学の基礎的な概念とも合致します。脳の学習や適応の能力は、**臨界期(critical periods)** と呼ばれる幼少期に最も高くなります。 小児期から青年期にかけて、脳では **シナプス刈り込み(synaptic pruning)** と呼ばれるプロセスが行われます。使われていない神経回路が排除される一方で、頻繁に使われる回路が強化されていくのです。このプロセスによって脳の効率は飛躍的に高まりますが、同時に時間の経過とともに可塑性(柔軟に適応する力)は低下していきます。言語の習得や社会行動の形成といった重要な発達段階には、脳が学習に対して例外的に受容的になる特定の時期(ウィンドウ)が存在します。 人生の知覚的な中間地点が9歳前後に位置するという今回のモデルの発見は、まさにこの生物学的な現実を反映しています。**脳の可塑性(brain plasticity)** がピークにあるこの時期の経験は、単に体感として重く感じられるだけでなく、その後の人生全体にわたって私たちの人格、スキル、世界観を支える神経回路のアーキテクチャを物理的に形成しているのです。したがって、このモデルは「私たちが何者であるかの基盤は、人生の極めて早い段階で不均衡なほど大きく築かれる」という生物学的な真理を数学的に表したものと見なすことができます。 ## モデルの限界 このモデルがあくまで単純化されたものであることを認識しておくことは重要です。その目的は内省のためのフレームワークを提供することであり、人間の意識に対する決定的な解明を提示することではありません。このモデルには、主に以下のような限界があります。 * **経験の均一性(Uniformity of Experience):** モデルではすべての年が同じ経験的重みを持つものとして扱われますが、現実はそうではありません。ルーティンワークばかりの1年は、新しい体験や旅行、人生の大きな変化に満ちた1年に比べて、知覚される人生経験への寄与度は低くなるはずです。 * **記憶の性質(The Nature of Memory):** モデルは体感時間が一定のペースで蓄積されると仮定しています。しかし、私たちが多くの経験を忘れてしまうことや、出来事の感情的な強度がその持続時間の認識を変化させることなど、記憶の複雑さは考慮されていません。 * **個人の主観性(Individual Subjectivity):** 時間の知覚は極めて個人的な体験です。注意力、気分、文化的背景といった要因が、時間の進み方がどれほど速く、あるいは遅く感じられるかに影響を与えます。`1/age` という関係はあくまで一般化であり、普遍的な法則ではありません。 ## おわりに この数理モデルは、神経発達に関する私たちの理解と突き合わせることで、時間との関係性を捉えるための有用な枠組みを提供してくれます。導き出された結果は明快です。私たちが知覚する人生は、驚くほど前半に偏重しています。最初の9年間は、その後の72年間と同じだけの知覚経験を人生全体にもたらしており、この結果は幼少期の高い脳の可塑性とも一致しています。 ただし、これはあくまでモデルであり、人生の完全な青写真ではありません。人間の経験が持つ豊かな複雑さを単純化し、すべての年を均一なものとして扱い、記憶の主観的な性質や新たな経験がもたらす影響を織り込んでいないからです。 こうした限界を踏まえた上で、ここから得られる最大の教訓は「諦念(運命論)」ではなく「自覚」です。このモデルは、私たちの人格形成期の経験が持つ深く永続的な影響を評価するための、定量的な視点を与えてくれます。私たちの世界観の土台は、生物学的な受容性がピークを迎えている極めて早い時期に不均衡なほど大きく作られることを示唆しています。そして年齢を重ねた私たちにとって、このモデルは強力なリマインダーとなります。過ぎ去る1年の体感的な重みが薄れていくことに対抗するには、新しく意味のある経験を追求することが不可欠であり、そうすることで残された体感的な人生を意識的に豊かにしていけるのです。 ## 参考リンク - **[NumPy](https://numpy.org/):** Pythonによる科学計算のための基本パッケージ。 - **[Matplotlib](https://matplotlib.org/):** Pythonで静的、アニメーション、インタラクティブな可視化を作成するための包括的なライブラリ。 --- # Speedgrapherの紹介:Vibe WritingのためのMCPサーバー - **Markdown URL**: https://danicat.dev/ja/posts/introducing-speedgrapher/index.md - **HTML URL**: https://danicat.dev/ja/posts/introducing-speedgrapher/ - **Date**: 2025-08-05 - **Categories**: Agentic Coding - **Tags**: ai, gemini-cli, golang, mcp, vibe-coding - **Summary**: Model Context Protocol(MCP)を活用してテクニカルライティング、Gunning Fog可読性スコアリング、校正を自動化するSpeedgrapherの設計と実装。 ## はじめに 正直に告白すると、私はモノづくりが大好きなのですが、それに伴う定型作業(ボイラープレート)まで好きかというと、必ずしもそうではありません。新しい記事のアイデアは常にたくさんあるものの、構成を組み立て、自分の編集基準を満たしているか確認し、適切なトーンに仕上げるというプロセスは、時に骨の折れる作業に感じられることがあります。この記事は、ある技術仕様を深く読み解いたことをきっかけに、執筆プロセスに心地よい構造化をもたらすMCPサーバー、[Speedgrapher](https://github.com/danicat/speedgrapher) を構築することになった物語です。 Speedgrapher への道のりは、前回の記事「[GoDoctorの構築:Gemini CLIとGoでMCPサーバーを作成する](https://danicat.dev/ja/posts/20250729-how-to-build-an-mcp-server-with-gemini-cli-and-go/)」を公開した直後に始まりました。その記事では、[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) によってAIエージェントがいかにツールを利用できるようになるかという点に特化して解説しました。公開後、私はMCP仕様を改めてじっくり読み直してみました。そこで、以前は見落としていた小さな記述が目に留まりました。プロトコルは `tools` だけでなく、`prompts` や `resources` も明示的に定義していたのです。ひらめきが走りました。メモやローカルファイル、GitHubリポジトリのあちこちに散らばっていたプロンプト集を、まったく同じプロトコルを使ってパッケージ化し、ポータブルに持ち運べるのではないかと気づいたのです。 実によい偶然でした。私がプロンプトサーバーの構想を練っていたその日、Gemini CLIチームが、MCPサーバーが公開するプロンプトを[ネイティブのスラッシュコマンド](https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/)として利用可能にする新機能を発表したのです。これにより、ポータブルなバックエンドツールキットという私のアイデアが、ターミナル上で直接動くファーストクラスで使いやすいUIを手に入れられることになりました。Speedgrapher のコンセプトは明確になりました。シンプルなスラッシュコマンドとして呼び出せる執筆ツールキットを束ねた、専用のMCPサーバーです。 ## Vibe Writing とは何か Speedgrapher を構築した技術的な道のりに飛び込む前に、「バイブライティング(vibe writing)」という言葉で私が何を意味しているのかを少し説明させてください。「バイブコーディング(vibe coding)」という言葉を耳にしたことがある方も多いでしょう。これは、自然言語のプロンプトを使ってAIを導きながらコードを生成させる、最近ますます一般的になってきた開発手法を指します。開発者が大所高所の方向性を定め、AIが定型コードや細かな実装を担うという、流れるような対話的アプローチです。 「Vibe writing」は、この概念を言葉の世界へと自然に拡張したものです。私にとってそれは、孤独な執筆作業を、AIパートナーとのダイナミックで協力的な対話へと変えることを意味します。文構造や文法、完璧な言葉探しといった機械的な作業に足を取られることなく、伝えたい中心メッセージ — 生み出したい「バイブ(空気感・熱量)」 — に集中できるのです。私が最初の火種 — 大まかなアイデア、個人的な体験談、直面した厄介な課題 — を提示し、AIがそれを構造的で筋の通った物語へと形作る手助けをしてくれます。 この言葉を使ったのは私が最初ではありませんが、まだ生まれたばかりの新しい概念です。これはコンテンツ制作へのアプローチにおける根本的なシフトであり、完全な手作業から人間とAIの協調関係への移行を表しています。 ## まずはシンプルに:俳句ジェネレーター どんな優れた技術探求も、まずは「Hello, World」から始まります。Speedgrapher における私の「Hello, World」は俳句(Haiku)でした。プロンプトをスラッシュコマンドとして公開できることを実証するための、手軽でリスクの少ない方法が必要だったのです。AIに詩を詠んでもらうこと以上にシンプルな検証があるでしょうか? 最初の試みは素朴なものでした。`--theme` 引数を取る `/haiku` プロンプトを作成し、プロンプト自体も `"generate a haiku based on the theme %s"` という単純なものでした。Gemini CLIを起動し、Speedgrapherプロジェクトをコンテキストとして読み込ませた状態で、こう入力しました。 `/haiku --theme=flowers` その結果返ってきたのは……詩ではありませんでした。モデルはプロジェクト内のGoコードを見て、私のリクエストを「*Speedgrapherに俳句機能を追加せよ*」という指示だと解釈してしまったのです。そしてGoファイルの編集計画を立て始めました。私はすぐに `ESC` キーを押して処理を中断し、作戦を練り直すことにしました。 この経験は、プロンプトエンジニアリングにおける極めて重要な原則を思い出させてくれました。「曖昧さ(ambiguity)」と「コンテキスト(context)」のバランスを取る必要性です。私が作成するプロンプトの多くでは、モデルが柔軟に推論して情報を補完できるように、あえてある程度の曖昧さを残しています。たとえば `/review` プロンプトは、単に「私たちが作業してきた記事をレビューしてください」とだけ指示しています。`DRAFT.md` のような具体的なファイル名は指定していません。対話型ワークフローにおいて、この曖昧さは強力な道具になります。モデルは厳密で固定的なファイルパスがなくても、直近のやり取りから対象のテキストを自律的に特定できるからです。 しかし俳句の場合、その曖昧さに適切な制約がありませんでした。メインのコンテキストがGoプロジェクトだったため、モデルは論理的ではあるものの意図とは異なる結論 — 「コードを変更したいのだろう」 — を導き出してしまったわけです。モデルが間違っていたわけではなく、妥当な推論をしたに過ぎません。今回はコードと無関係な極めて具体的な結果を求めていたため、意図を明確にするコンテキストを与えて曖昧さを減らす必要がありました。 何度か試行錯誤を重ねた後、最終的に以下のプロンプトに落ち着きました。 ```go // The final, working prompt for the haiku command. prompt = fmt.Sprintf("The user wants to have some fun and has requested a haiku about the following topic: %s", topic) ``` これが意図を伝える最善の表現かどうかはわかりませんが、私の目的には見事に合致し、モデルはその後一貫して俳句を詠んでくれるようになりました。コアコンセプトの実証が完了したため、より実践的なプロンプトの構築へと進む準備が整いました。 ## 執筆ツールキットの構築 俳句の実験によってコアコンセプトの正しさが確認できたので、より実践的な応用へと進みました。当時の私の `GEMINI.md` ファイルは、記事のレビュー、翻訳、アウトライン作成といった便利なプロンプトの集まりになっていたものの、ポータビリティがありませんでした。特定のプロジェクトに紐づいていたため、新しいプロジェクトを始めるたびにコピーし忘れることがよくあったのです。これらのツールをポータブルにするための次なる論理的一歩が、MCPサーバーでした。 まずは特によく使っていた3つのプロンプト — `interview`、`review`、`localize` — を Speedgrapher に移行することから始めました。これらのプロンプトの核となるのは「編集ガイドライン」です。たとえばローカリゼーションのガイドラインには「技術用語は翻訳しない」というルールを含めており、当ブログがサポートする3つの言語間での一貫性を確保しています。このような「コードとしての編集ガイドライン(editorial guidelines as code)」を作成するアプローチは、コードに対するリンターのように、一定の文体やクオリティを維持する構造化されたシステムを構築する方法です。 Speedgrapher のプロンプトはすべて Gemini の支援を受けて生成したものですが、`review` プロンプトについては少し異なるアプローチを取りました。モデルに私の過去の記事を分析させ、私の執筆スタイルに基づいた編集ガイドラインの生成を依頼したのです。生成された初稿はしっかりしたものでしたが、今でも常に洗練を重ねているプロンプトです。 以下は、GitHub上の Speedgrapher ソースコードから直接埋め込んだプロンプトの最新版です。 [View on GitHub: danicat/speedgrapher/internal/prompts/review.go (lines 18-28)](https://github.com/danicat/speedgrapher/blob/main/internal/prompts/review.go#L18-L28) 主要なプロンプトが揃ったところで、私の仕事の他の重要な部分の自動化に取り掛かる時が来ました。 ## リーダビリティの重要性 テクニカルライターとしての最大の課題は、明快さ(clarity)と複雑さ(complexity)の間のスイートスポットを見つけることです。文章が単純すぎると幼稚に感じられ、複雑すぎると読めなくなってしまいます。リーダビリティとは単に平易にすることだけではなく、読者を引き込み、知的な刺激を与えることでもあるのです。 リーダビリティに関する朗報は、それが測定可能であるという点です。完璧な指標は存在しないものの、[Gunning Fog Index(ガンニング・フォグ・インデックス)](https://en.wikipedia.org/wiki/Gunning_fog_index) は基準値を把握するための優れたツールです。Gunning Fog Index は、文章を初見で理解するために何年間の正規教育が必要かを推定する可読性テストです。たとえばスコア12は、米国の高校3年生(シニア)相当の読解レベルであることを意味します。 このインデックスは、以下のアルゴリズムに基づいて計算されます。 * 100語以上のテキストセクションを取り出す。 * 平均文長(1文あたりの平均単語数)を算出する。 * 「難解な単語(3音節以上の単語)」の数を数える。 * 平均文長に、難解な単語の割合を加算する。 * その結果に 0.4 を掛ける。 数学が好きな方向けに数式で表現すると、このアルゴリズムは以下のようになります。 \[ 0.4 \times \left[ \left( \frac{\text{words}}{\text{sentences}} \right) + 100 \left( \frac{\text{complex words}}{\text{words}} \right) \right] \] Fog Indexの本来の意図はテキストの理解に必要な「教育年数」を推定することですが、教育年数という枠組みで考えるのはあまり実用的ではないと感じたため、自分のニーズに合わせてカスタマイズすることにしました。まず、特殊な例外ケースを無視して計算を単純化しました。このアルゴリズムで最も複雑(complex)な部分の一つは、何をもって単語が「複雑(complex)」であるかを定義する処理です(しゃれのようですが)。基本ルールでは3音節以上の単語を複雑とみなしますが、`-ing`、`-ed`、`-es` などの特定の語尾を無視するといった例外ケースが生じます。 この例外処理は、実装時に思いのほか多くの問題を引き起こしました。私にとって厳密な精度は必要なく、シンプルさを優先して複雑さを多少過大評価する程度で十分でした。そこで、すべての特殊ケースを無視し、音節のカウントに関して2つの基本ルールのみを採用しました。1) 単語内の音節数は母音グループの数で推定する、2) 複雑な単語とは3音節以上の単語である(例外なし)。 また、教育年数から実用的なアプローチへと焦点を移した分類体系も作成しました。 | スコア | 分類 | 説明 | | :--- | :--- | :--- | | >= 22 | 難解すぎる(Unreadable) | ほとんどの読者にとって理解が困難。 | | 18-21 | 読みにくい(Hard to Read) | 専門家であってもかなりの労力を要する。 | | 13-17 | 専門家向け(Professional Audiences) | 専門知識を持つ読者に最適。 | | 9-12 | 一般向け(General Audiences) | 多くの読者にとって明快で親しみやすい。 | | < 9 | 平易すぎる(Simplistic) | 幼稚、あるいは単純すぎると受け取られる可能性がある。 | カスタマイズした Gunning Fog Index を `fog` ツールとして実装した後の最後のステップは、使いやすいインターフェースを用意することでした。`fog` ツールを呼び出して結果をわかりやすいフォーマットで提示する `/readability` プロンプトを作成しました。これは、「単一目的に絞ったツールを作り、それらを組み合わせてより強力で使いやすいワークフローを構築する」という Speedgrapher の設計指針に沿ったものです。 ## 執筆ワークフローの自動化 個々のプロンプトは有用でしたが、理想のワークフローを実現するには、まだ自動化すべきプロセスが多く残されていました。その後の数回のイテレーションを通じてプロンプトを実践で試し、プロセスの隙間を洗い出しながら、新しいプロンプトの追加や既存プロンプトの微調整を行いました。現在使用しているプロンプトは以下のとおりです。 **メインフロー** * `/interview`: 記事の素材を集めるために著者にインタビューを行います。通常、執筆セッションの起点となります。 * `/outline`: 現在のドラフト、コンセプト、またはインタビュー記録から構造化された構成案(アウトライン)を生成します。 * `/voice`: ユーザーの文体やトーンを分析し、生成される文章にそれを反映させます。 * `/expand`: 作業中の構成案やドラフトを、より詳細な記事へと拡張します。`hint` 引数を指定して、特定の段落やセクションを集中的に拡充することも可能です。 * `/review`: 執筆中の記事を編集ガイドラインに照らしてレビューします。 * `/readability`: Gunning Fog Index を用いて、直前に生成されたテキストの可読性を分析します。 * `/localize`: 執筆中の記事を対象の言語に翻訳します。 * `/publish`: 記事の最終版を公開します。 **オプション** * `/context`: 後続のコマンドのために、現在執筆中の記事をコンテキストに読み込みます。必要に応じてモデルに現在のドラフトを「思い出させる」ために使用し、全文を対象とする `/readability` や `/review` などのコマンドの前に実行することがよくあります。 * `/reflect`: 現在のセッションを分析し、執筆プロセスの改善点を提案します。プロンプトや編集ガイドラインを改善する際に役立ちます。 目指したのは、便利なコマンドの集まりから、シンプルなアイデアを洗練された多言語の公開記事へと導く、ひとつの合理化されたプロセスへと進化させることでした。 以下の図は、私のワークフローを簡略化して表したものです。 ```mermaid flowchart TD A[アイデア] -->|/interview| B[インタビュー記録] B -->|/outline & /voice| C[構造化された構成案] C -->|/expand| D[記事のドラフト] D -->|/review & /readability| E[推敲済みドラフト] E -->|/localize| F[ローカライズ版] F -->|/publish| G[公開された記事] ``` プロセスは、アイデアの核となるコンセプトを具体化する `/interview` から始まります。得られたインタビュー記録は `/outline` によって構造化された計画へと変換され、`/voice` によって私自身の執筆スタイルに合わせて調整されます。この土台ができあがったら、`/expand` でドラフトを肉付けし、`/review` と `/readability` で推敲を重ねる反復ループに入ります。 記事が承認されたら、`/localize` で他言語版を作成し、`/publish` で公開プロセスを完了します。さらにオプションの `/reflect` プロンプトを使えば、セッションを分析して将来の改善のためのメモを生成し、継続的な改良のサイクルを回すことができます。 ## まとめ リンターやテストを使ってコードに構造をもたらすのと同じように、クリエイティブなワークフローにも同様の原則を適用できます。執筆プロセスには、自動化できる反復作業が数多く存在します。自分専用のプロンプトツールキットを構築することで、定型作業をオフロードし、作品の核となるアイデアに集中できるようになります。 これこそが Speedgrapher のようなツールの真価です。「vibe writing」は書き手を置き換えるものではなく、執筆プロセスを拡張・強化するためのものです。そこにMCPサーバーを組み込むことで、混沌としがちなワークフローに役立つ構造化のレイヤーをもたらし、ベストプラクティスを確実に遵守させることができます。これはあらゆるAI支援プロセスに適用できます。自らのプロンプトを再利用可能でポータブルな資産として扱うことで、自らのプロセスとともに進化するシステムを構築し、プロンプトを1つずつ重ねながら、創作活動のクリエイティブな側面に集中できるようになるのです。 ## 今後の展望 Speedgrapher の旅はまだまだ終わりません。現在のツールキットはテキストに特化していますが、次の論理的なステップはマルチモーダルへの対応です。ヒーロー画像を生成したり、テキストからより高度な図を作成したり、レイアウトの最適化を提案するツールの統合を検討しています。執筆以外のタスクをより多く処理できる個人用ツールキットを構築し続け、自分自身がコンテンツそのものに集中できるようにすることが目標です。 ## 参考リソース * **[Speedgrapher プロジェクト](https://github.com/danicat/speedgrapher):** 本記事で紹介したMCPサーバーのソースコード。 * **[GoDoctorの構築:Gemini CLIとGoでMCPサーバーを作成する](https://danicat.dev/ja/posts/20250729-how-to-build-an-mcp-server-with-gemini-cli-and-go/):** この旅の着想となった前回の記事。 * **[Model Context Protocol (MCP)](https://modelcontextprotocol.io/):** プロトコルの公式ウェブサイト。 * **[Gemini CLI の公式アナウンス](https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/):** カスタムスラッシュコマンドのサポートを発表したブログ記事。 * **[Gunning Fog Index](https://en.wikipedia.org/wiki/Gunning_fog_index):** 可読性指標の詳細(Wikipedia記事)。 --- # GoDoctorの構築:Gemini CLIとGoでMCPサーバーを作成する - **Markdown URL**: https://danicat.dev/ja/posts/how-to-build-an-mcp-server-with-gemini-cli-and-go/index.md - **HTML URL**: https://danicat.dev/ja/posts/how-to-build-an-mcp-server-with-gemini-cli-and-go/ - **Date**: 2025-07-29 - **Categories**: Agent Development - **Tags**: ai, codelab, gemini-cli, golang, mcp, tutorial - **Summary**: Gemini CLI と Go 言語で GoDoctor MCP サーバーを構築するチュートリアル。stdio/HTTP トランスポート、ハンドシェイク、Cloud Run デプロイを詳解。 > **注記:** この記事は Gemini CLI を対象に書かれたものですが、Gemini CLI は非推奨となり **Google Antigravity 2.0** へと移行しました。新しい Antigravity CLI(`agy`)、SDK、および最新エコシステムの詳細については、[Antigravity 2.0 への銀河ヒッチハイク・ガイド](https://danicat.dev/ja/posts/20260521-the-hitchhikers-guide-to-antigravity-2-0/) をご覧ください。 ## はじめに 多くの皆さんと同様に、私も AI 支援開発の世界に深く没頭してきました。その道のりは、「おおっ!」と驚くような瞬間と、もどかしい壁にぶつかることの連続(まるでジェットコースター)です。これは、そんな道のりのひとつの記録です。あるものを作ろうと始めたはずが、もどかしいトラブルによって完全に脱線し、最終的には自分の AI 支援ワークフローを根本的に向上させるツールを手に入れることになった——そんなストーリーです。 私の当初の目標は、SQL を使ってマシンの状態をクエリできるツール [osquery](https://www.osquery.io/) のための Model Context Protocol(MCP)サーバーを構築することでした。Go コードを書くのを手伝ってもらうため、Gemini CLI を使うのをとても楽しみにしていました。しかし、すぐに大きな壁にぶつかりました。エージェントが生成する Go コードは、お世辞にもイディオマティック(Go らしい作法に則っている)とは言えなかったのです。初心者のようなミスを犯し、過剰な抽象化を作り込み、存在しない API をまるごと「ハルシネーション(幻覚)」で捏造することもしばしばありました。私の推測では、基盤モデルが新しい [Go SDK for MCP](https://github.com/modelcontextprotocol/go-sdk) で学習されておらず、「知らない」と認める代わりにそれっぽい答えをでっち上げるほうを選んでしまったのだと思います。 この経験から、ひとつの重要な気づきを得ました。ツールと戦うのではなく、ツールに*教える*ことができるのではないか、ということです。そこで私は osquery プロジェクトを一時中断し、「サイドクエスト」に乗り出すことにしました。その目的はただひとつ、Go 開発のエキスパートとなる専用 MCP サーバーを作ることです。最終的に [GoDoctor](https://github.com/danicat/godoctor) と名付けたこのサイドプロジェクトは、Gemini CLI がより良い Go コードを書くために必要なツール群を提供してくれます。 本記事では、GoDoctor を構築したプロセスを振り返ります。これは従来のチュートリアルというよりは、「プロンプト主導」の探求記です。プロジェクト要件をいかに効果的なプロンプトへと落とし込み、AI を実装へと導きながら、道中で避けられない失敗から何を学んでいくかに焦点を当てます。 ## 舞台設定:`GEMINI.md` コードを1行も書く前に、まず行うべきは基本ルールの策定でした。`GEMINI.md` は Gemini CLI 固有のファイルですが、コンテキストファイルを用意するプラクティスは多くの AI コーディングエージェントで一般的です(たとえば Jules では `AGENTS.md`、Claude では `CLAUDE.md` が使われます)。実際、これを [`AGENT.md`](https://ampcode.com/AGENT.md) というファイル名で標準化しようとする新たな動きも始まっています。このファイルは、AI に対してプロジェクトの規範や振る舞いへの期待値を正しく理解させるための土台となるため、極めて重要です。 新規プロジェクトだったため、この段階では具体的なアーキテクチャの詳細はまだありませんでした。そこで、まずは高品質でイディオマティックな Go コードを書くための汎用的なガイドラインから始めることにしました。プロジェクトが進むにつれて、ディレクトリ構造やビルドコマンド、主要ライブラリに関する具体的な指示を追加していくのが一般的です。よりプロジェクト固有の設定例としては、私の [`testquery` プロジェクト](https://github.com/danicat/testquery/blob/main/GEMINI.md) で使っている `GEMINI.md` をご覧ください。 以下が、今回の開発において AI の「憲法」として機能した初期の `GEMINI.md` です: ```markdown # Go Development Guidelines All code contributed to this project must adhere to the following principles. ## 1. Formatting All Go code **must** be formatted with `gofmt` before being submitted. ## 2. Naming Conventions - **Packages:** Use short, concise, all-lowercase names. - **Variables, Functions, and Methods:** Use `camelCase` for unexported identifiers and `PascalCase` for exported identifiers. - **Interfaces:** Name interfaces for what they do (e.g., `io.Reader`), not with a prefix like `I`. ## 3. Error Handling - Errors are values. Do not discard them. - Handle errors explicitly using the `if err != nil` pattern. - Provide context to errors using `fmt.Errorf("context: %w", err)`. ## 4. Simplicity and Clarity - "Clear is better than clever." Write code that is easy to understand. - Avoid unnecessary complexity and abstractions. - Prefer returning concrete types, not interfaces. ## 5. Documentation - All exported identifiers (`PascalCase`) **must** have a doc comment. - Comments should explain the *why*, not the *what*. # Agent Guidelines - **Reading URLs:** ALWAYS read URLs provided by the user. They are not optional. ``` このファイルによって、開発の最初期から品質とスタイルのベースラインを確立できます。 ## Model Context Protocol(MCP)を理解する このプロジェクトの核心にあるのが Model Context Protocol(MCP)です。MCP を「LLM ツールのための USB 標準」と例える人もいますが、私は別の見方をしています。**HTTP と REST が Web API の標準化にもたらした変革を、今まさに MCP が LLM ツールにもたらそうとしている**、と。REST が予測可能なアーキテクチャを提供して巨大な Web サービスのエコシステムを解き放ったように、MCP は AI エージェントの世界に待望の共通言語を提供しています。これは JSON-RPC ベースのプロトコルであり、MCP を「話す」エージェントであれば、個別の専用統合を作ることなく、仕様に準拠したあらゆるツールを発見して利用できるようになります。 プロトコルでは、エージェントとツールサーバーが通信するための方式(トランスポート)が定義されています。代表的なものは以下の2つです: * **HTTP:** おなじみの Request/Response モデルであり、[Cloud Run](https://cloud.google.com/run?utm_campaign=CDR_0x72884f69_default_b421852297&utm_medium=external&utm_source=blog) などにリモートサービスとしてデプロイされたツールに最適です。 * **stdio:** 標準入出力(stdin/stdout)を使用する堅牢なトランスポートであり、ローカルマシン上のプロセスとしてツールを実行するのに適しています。 `stdio` トランスポートでは、エージェントとサーバーが一連の JSON-RPC メッセージをやり取りします。接続を確立するために、まず重要な3ウェイハンドシェイクから始まります。クライアントがツール呼び出しを行えるのは、このハンドシェイクが完了した後のみです。 シーケンスは以下のようになります: ![MCP stdio sequence diagram](images/mcp-stdio-sequence-diagram.png) *

図1:公式MCPドキュメント(2025-06-18)の stdio トランスポート・シーケンス図。

* 公式仕様に基づいた、初期ハンドシェイクの JSON メッセージの例は以下のとおりです: **1. Client → Server: `initialize` リクエスト** クライアントは `initialize` リクエストを送信して対話を開始します。 ```json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "clientInfo": { "name": "Gemini CLI", "version": "1.0.0" } } } ``` **2. Server → Client: `initialize` レスポンス(結果)** サーバーはプロトコルバージョンを確認し、サポートする機能(capabilities)やサーバー情報を返す形で応答します。以下は `godoctor` バイナリからの実際のレスポンスです: ```json { "jsonrpc": "2.0", "id": 1, "result": { "capabilities": { "completions": {}, "logging": {}, "tools": { "listChanged": true } }, "protocolVersion": "2025-06-18", "serverInfo": { "name": "godoctor", "version": "0.2.0" } } } ``` **3. Client → Server: `initialized` 通知(Notification)** 最後に、クライアントが `initialized` 通知を送信して初期化の完了を伝えます。これは通知(notification)であるため `id` フィールドがなく、メソッド名が名前空間(`notifications/initialized`)で区切られている点に注意してください。 ```json { "jsonrpc": "2.0", "method": "notifications/initialized", "params": {} } ``` このやり取りが完了してセッションが確立されると、クライアントはツールの呼び出しに進むことができます。たとえばサーバーに利用可能なツール一覧を要求するには、`tools/list` リクエストを送信します。肝心なのは、このリクエストを送る前に、3つのハンドシェイクメッセージがすべて正しい順序で送信されていなければならない点です。 この一連のシーケンスは、以下のシェルスクリプトで実際に確認できます: ```bash #!/bin/bash ( echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"Manual Test Client","version":"1.0.0"}}}'; echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'; echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; ) | godoctor ``` このスクリプトを `godoctor` バイナリへパイプで流し込むと、まず `initialize` の結果が返され、続いて GoDoctor が提供する全ツールを正しく一覧表示する `tools/list` の結果が出力されます。この必須の3ステップによるフローを理解できたことが、最初の最大の難関を突破する鍵となりました(詳しくは次のセクションで説明します)。 MCP を初めて触る方には、公式ドキュメントを一読することを強くおすすめします。私にとって特に重要だったのは、[クライアント/サーバーのライフサイクル](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle) と [トランスポート層](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) に関するページでした。(あるいは読むのが面倒なら、それらの URL を CLI に渡して代わりに読ませてしまうのも手です =^_^=) ## 最初のブレークスルー:ドキュメントを読むエージェント 私の最初の目標は、API のハルシネーション問題を解決することでした。プロンプティングの初期段階によくあるように、最初のリクエストは素朴で少し曖昧なものでした: > 「`godoc` という名前のツールを1つ持つ Go 製の MCP サーバーを作成してください。このツールはパッケージ名と任意のシンボル名を受け取り、`go doc` コマンドを実行するものとします。」 しかし、結果は芳しいものではありませんでした。エージェントはどのツールを使い、どのプロトコルが最適なのかを判断するのに多くの時間を費やしてしまったのです。「MCP」という頭字語すら自明ではなく、私が「Model Context Protocol」のことだと明示するまでは、まったく別の概念だと解釈してしまうこともしばしばでした。Google 検索と WebFetch を呼び出すループに囚われ、手当たり次第に別の SDK を試しては動くコードを生成できずに別の SDK へピボットする……という試行錯誤を延々と繰り返しました。これこそが「バイブコーディング(vibe coding)」の現場で直面する現実です。指示を少しずつ磨き上げていく反復的なプロセスが求められます。数時間の試行錯誤の末、私ははるかに効果的なプロンプトにたどり着きました。具体的で質の高いリファレンスをあらかじめ与えておくことが何より重要だと学んだのです。 以下が、最終的に完成した改善版プロンプトです: > あなたのタスクは、go doc コマンドを公開し、LLM が Go のドキュメントを検索できるようにする Model Context Protocol(MCP)サーバーを作成することです。ツール名は go-doc とし、package_path(必須)と symbol_name(任意)の2つの引数を取ります。ドキュメントの取得には `go doc` シェルコマンドを使用してください。MCP 実装には公式の Go SDK for MCP を使用し、stdio トランスポート経由で通信するプロダクションレディな MCP サーバーを記述してください。また、サーバーの動作テストができるシンプルな CLI クライアントもあわせて作成してください。 > > コードを書き始める前に、技術仕様とプロジェクト構造を把握するため、必ず以下のリファレンスを読み込んでください: > - https://github.com/modelcontextprotocol/go-sdk > - https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle > - https://go.dev/doc/modules/layout このプロンプトが優れている理由はいくつかあります。使用すべき正確な SDK を指定し、トランスポート(`stdio`)を明確に定め、そして何よりエージェントに「読むべき資料リスト」を提示している点です。SDK のソースコードや MCP 仕様へのリンクをあらかじめ渡すことで、エージェントがハルシネーションを起こす確率を劇的に抑え込むことができました。 しかし、プロンプトを改善した後も道のりは平坦ではありませんでした。最大の壁となったのが `stdio` トランスポートの実装です。ツール呼び出しを行うと、決まって `server initialisation is not complete` という不可解なエラーで失敗してしまったのです。骨の折れるデバッグの末に判明したのは、問題がサーバー側のコードにあるわけではまったくない、ということでした。原因はまさに先述したとおり、MCP の `stdio` トランスポートが厳格な3ステップのハンドシェイクを要求する仕様だったためです。クライアントがハンドシェイク完了前にツールの呼び出しを試みてしまっていたのです。この経験から貴重な教訓を得ました。AI 向けのツールを構築する際、デバッグしているのは単なるプログラムコードではなく、「対話プロトコルそのもの」なのだということです。 サーバーが正常に起動し、プロトコルに従って正しく対話できるようになったら、次のステップはそれを Gemini CLI に認識させることでした。これにはプロジェクトルートの `.gemini/settings.json` ファイルを使用し、ロードすべきツールを CLI に指示します。以下の設定を追加しました: ```json { "mcpServers": { "godoctor": { "command": "./bin/godoctor" } } } ``` これを設定しておけば、このディレクトリで Gemini CLI を起動するたびに、バックグラウンドで `godoctor` サーバーが自動的に立ち上がり、エージェントからそのツールが利用可能になります。 ## AIコードレビュアーでフィードバックループを作る `godoc` ツールが動くようになったところで、次なるステップは、エージェントにドキュメントを読ませるだけでなく、コードの「品質」について考察(推論)させることでした。こうして誕生したのが `code_review` ツールです。これまでの土台があったおかげで、今回ははるかにスムーズに進みました。 プロンプトでは実装の詳細ではなく、「達成したいゴール」に焦点を当てました: > プロジェクトに code_review という新しいツールを追加したいです。このツールは Gemini API を使用して Go コードを解析し、Go コミュニティで広く受け入れられているベストプラクティスに沿った改善案のリストを json 形式で返すものとします。ツールへの入力として Go コードの文字列と、任意のヒント(hint)を受け取るようにしてください... > > Gemini の呼び出しには次の SDK を使用してください:https://github.com/googleapis/go-genai エージェントは依然として新しい `genai` Go SDK を学ぶ必要がありましたが、今回のツールボックスにはすでに自作の `godoc` ツールがありました。エージェントが自らツールを使って SDK の仕様を調べ、自分の間違いを修正しながらリアルタイムに学習していく様子を目の当たりにしました。試行錯誤のイテレーションは依然として必要でしたが、開発スピードと効率は以前と比べて格段に向上しました。 最大の収穫は、ツールそのもの以上に、それによって切り拓かれた「新たな可能性」でした。ツール自身を使ってツール自身のコードをレビューできるようになり、AI 支援開発が一段上のステージへと引き上げられたのです。**まさに正のフィードバックループが完成した瞬間でした。** 私のワークフローには、強力な新しいステップが加わりました。エージェントがコードを生成した直後に、自分自身の成果物を批評するよう指示できるようになったのです: > 「今書いたコードに対して `code_review` ツールを実行し、その提案を適用してリファクタリングしてください。」 するとエージェントは自身の出力を自ら分析し、AI が提示したフィードバックに基づいてコードをリファクタリングしてくれます。これこそが AI 向けツールを構築する真の醍醐味です。単にタスクを自動化するだけでなく、「自己改善できるシステム」そのものを構築しているのです。 ## 最終章:クラウドへのデプロイ > **Warning:** 独自の MCP サーバーを Cloud Run にデプロイする際は、必ず適切な認証を設定してください。特に Gemini API キーを使用している場合、**誰でもアクセスできる公開サーバーとしてデプロイしないでください**。公開エンドポイントが悪意ある第三者に悪用された場合、予期せぬ莫大なクラウド利用料が発生する恐れがあります。 `stdio` 経由で動作するローカルツールは個人の開発環境には最適ですが、Model Context Protocol の真の目標は、共有可能で発見しやすいツールのエコシステムを構築することにあります。この「サイドクエスト」の次なるフェーズは、GoDoctor をノートPC上のローカルバイナリから、[Google Cloud Run](https://cloud.google.com/run?utm_campaign=CDR_0x72884f69_default_b421852297&utm_medium=external&utm_source=blog) を使ったスケーラブルな Web サービスへと昇華させることでした。 そのためには、エージェントにクラウド開発における2つの新しいスキル——「アプリケーションのコンテナ化」と「クラウドへのデプロイ」——を教え込む必要がありました。 まず、トランスポートを `stdio` から `HTTP` へと切り替える必要がありました。前回の成果を踏まえ、プロンプトは簡潔かつ直接的に指示しました: > 「サーバーを Web に公開する準備をします。MCP サーバーを `stdio` トランスポートから `streamable HTTP` トランスポートを使用するようにリファクタリングしてください。」 サーバーが HTTP で通信できるようになったので、次はクラウド向けにパッケージングします。軽量で安全なコンテナイメージを構築する標準的な手法である、プロダクションレディなマルチステージ `Dockerfile` の作成をエージェントに依頼しました: > 「Go バイナリをコンパイルし、最小限の `golang:1.24-alpine` イメージにコピーするマルチステージ Dockerfile を作成してください。」 `Dockerfile` が完成したら、いよいよデプロイです。ローカルの概念実証(PoC)が、本物のクラウドインフラへと生まれ変わる瞬間です: > 「作成したイメージを Cloud Run にデプロイしてください。リージョンは `us-central1` を指定し、現在環境で設定されているプロジェクトを使用してください。完了したら、MCP ツールを呼び出すための URL を教えてください。」 エージェントは適切な `gcloud` コマンドを提示し、ほんの数分後には GoDoctor がインターネット上で稼働し始めました。セットアップを完了させるため、ローカルの Gemini CLI にリモートサーバーの存在を伝える必要があります。`.gemini/settings.json` ファイルを更新し、ローカルの `command` をリモートの `httpUrl` に差し替えました: ```json { "mcpServers": { "godoctor": { "httpUrl": "https://.run.app" } } } ``` これだけで、私の CLI はクラウド上にデプロイされて稼働しているリモートツールを直接利用できるようになりました。概念実証が真に完成したと感じた瞬間でした。エージェントを丁寧に誘導する反復的なプロセスが実を結び、ひとつのアイデアを現代のアプリケーションライフサイクル全体(ローカルの PoC からスケーラブルなクラウドネイティブサービスまで)へと導くことができたのです。 とは言え、普段の開発作業では現在も `stdio` 版を使っています。1人だけの開発環境にとって、Cloud Run へのデプロイはオーバースペック(やりすぎ)ですからね。 ## GoDoctor のバイブコーディングから得た教訓 今回の旅は、単にコードを書くこと以上に「AI といかに効果的に協働するか」を学ぶプロセスでした。最大の気づきは、自分のマインドセットを「コーダー」から「教師」あるいは「パイロット」へと切り替えることの重要性です。ここで得られた特に重要な教訓をいくつか挙げます: * **あなたがパイロットであること。** AI はときに納得のいかないアクションを提案してくることがあります。ためらわずに `ESC` キーを押して中断し、新たなプロンプトを与えて正しい方向へと誘導してください。 * **AI を常に「同じ認識(ループ)」に留めておくこと。** 基本的にはすべての作業をエージェントに任せるのがベストですが、ときに手動での修正が必要になることもあります。人間が手動でコードを変更すると、AI の持っているコンテキストが古くなってしまいます。コードベースの同期を保つために、どこを変更したのかを必ずエージェントに伝えてください。 * **困ったときは「再起動」に限る。** (決して珍しくない)AI が完全に行き詰まってしまった状況では、古典的な IT の対処法が驚くほど効きます。Gemini CLI であれば、`/compress` コマンドで会話履歴を要約するか、最悪の場合は CLI を再起動してクリーンなコンテキストから再開すると良いでしょう。 エージェントに適切なコンテキストとツールを与えることで、AI は格段に頼もしいパートナーへと成長しました。単にコードを書かせるだけの作業から、「自ら学び、自己改善できるシステム」を構築する旅へと変化したのです。 ## 今後の展望 GoDoctor をめぐる探求はまだまだ終わりではありません。現在も実験的なプロジェクトであり、新しいツールを追加したり対話を重ねたりするたびに、新たな発見があります。私の目標は、世界中の Go 開発者にとって真に役立つコーディングアシスタントへと育て続けることです。 もしこの旅をご自身でも体験し、ゼロから独自の MCP サーバーを作ってみたいと思われたなら、そのプロセスを体験できるハンズオンワークショップを用意しています。ぜひ **[Build an MCP Server with Go and Gemini CLI](https://codelabs.developers.google.com/cloud-gemini-cli-mcp-go)** のコードラボをチェックしてみてください。 また、こうした概念が公式の Go ツールチェーンにどのように適用されているか興味がある方には、同様の目標を掲げる `gopls` MCP サーバーについてのドキュメントを読むことを強くおすすめします。詳細は [Go 公式ドキュメント](https://tip.golang.org/gopls/features/mcp) を参照してください。 ## リソースと参考リンク 本記事で紹介した主なリソースをまとめました。皆さんにとっても役立つ情報となれば幸いです。 * **[GoDoctor プロジェクトリポジトリ](https://github.com/danicat/godoctor):** 今回作成したツールの完全なソースコード * **[Model Context Protocol 公式サイト](https://modelcontextprotocol.io/):** MCP を学ぶための公式ポータル * **[MCP 仕様書 (2025-06-18)](https://modelcontextprotocol.io/specification/2025-06-18):** 技術仕様の完全版 * **[MCP ライフサイクル仕様](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle):** クライアント・サーバー間のハンドシェイクを理解するための必読ドキュメント * **[MCP トランスポート仕様](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports):** `stdio` と `http` トランスポートの違いを理解するためのリファレンス --- # 「プロアクティブさ」は有害か?!Gemini CLI システムプロンプト深層解剖 - **Markdown URL**: https://danicat.dev/ja/posts/gemini-cli-system-prompt/index.md - **HTML URL**: https://danicat.dev/ja/posts/gemini-cli-system-prompt/ - **Date**: 2025-07-14 - **Categories**: Agentic Coding - **Tags**: gemini-cli, tutorial, vibe-coding - **Summary**: GEMINI.md コンテキストファイルと GEMINI_SYSTEM_MD 環境変数によるオーバーライドを駆使して、Gemini CLI のプロアクティブ度を制御するステップバイステップガイド。 > **注記:** この記事は Gemini CLI を対象に書かれたものですが、Gemini CLI は非推奨となり **Google Antigravity 2.0** へと移行しました。新しい Antigravity CLI(`agy`)、SDK、および最新エコシステムの詳細については、[Antigravity 2.0 への銀河ヒッチハイク・ガイド](https://danicat.dev/ja/posts/20260521-the-hitchhikers-guide-to-antigravity-2-0/) をご覧ください。 ## はじめに 多くの方はすでに [Gemini CLI](https://cloud.google.com/gemini/docs/codeassist/gemini-cli?utm_campaign=CDR_0x72884f69_default_b432031389&utm_medium=external&utm_source=blog) をご存じかと思いますが、まだ触れたことがない方は公式の [リリースブログ](https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/) で概要をチェックしてみてください。 前回の記事「[AI対応の世界のための現代的な開発者ワークフロー](https://danicat.dev/ja/posts/20250714-developer-workflow/)」では、日々のワークフローへの組み込み方について書きましたが、今回は少し違った切り口で掘り下げてみたいと思います。CLI をしばらく使っていると、非常に「プロアクティブ(先回りする挙動)」であることに気づくはずです。少し曖昧なプロンプトであっても次のステップを勝手に推測し、その推測に基づいてすぐさまアクションを実行しようとします。 この挙動の意図は、会話をより人間らしく自然なやりとりにすることにあります。たとえば、テストを書くように指示したものの、テストの実行手順を README に追記するのを忘れていたとしましょう。そこで「テストの実行手順を README に追記すべきじゃない?」と質問形式でフォローアップのプロンプトを投げると、CLI はその質問を反語(実質的な指示)と解釈し、即座に README ファイルを更新するコマンドを実行します。 一般的なユースケースでは、このレベルのプロアクティブさは無害どころか歓迎されることさえあるかもしれませんが、私の経験上、実際のワークフローでは邪魔になることのほうが圧倒的に多いです。典型的な例を挙げると、コードをひとしきり「バイブコーディング」した後に、CLI に「なぜこの `@x` ファイルを追加したの?」と意図を確認しようとしたときのことです。すると CLI は「そのファイルが存在してほしくないのだ」と勝手に解釈し、何の説明もなく*プロアクティブに*ファイルを削除してしまいました。私が純粋な疑問として質問し、回答を期待しているケースがほとんどであるため、このようなやりとりにはいつも酷くうんざりさせられます。 ツールを自分のニーズに合わせるためには、自分のコミュニケーションスタイルに合わせて CLI をパーソナライズすることが極めて重要です。生産的な作業を進めたいときに、AI と格闘したい人などいません。実際、AI ツールを敬遠して従来の IDE やオートコンプリートに戻ってしまう理由として、私が周囲の開発者から最もよく耳にするのがこの問題です。 Gemini CLI を自分にとってより生産的な挙動に仕上げるため、続く 2 つのセクションでは、CLI のレスポンスをカスタマイズする 2 つの方法―― `GEMINI.md` ファイルの活用と、システム指示(system instructions)のオーバーライド――について探っていきます。 ## GEMINI.md による Gemini CLI のカスタマイズ `GEMINI.md` は、CLI に追加のコンテキスト(前提知識)を提供するためのファイルです。ターミナルから CLI を起動すると、現在のフォルダーおよびその配下のすべてのサブフォルダーから `GEMINI.md` ファイルを探索します。これらのファイルはあらゆる用途に利用できますが、典型的な作業構造としては、ルートにある `GEMINI.md` ファイルを使って、プロジェクトの目的からフォルダー構成、ビルドやテストなどの主要な実行手順までを CLI に説明します。これは実質的に優れた `README.md` と非常によく似ていますが、唯一の違いは AI 向けに書かれているため、より「プロンプト的」な形式になっている点です。 トップレベルの `GEMINI.md` ファイルは、CLI の動作ルールを指示する場所としても最適です。「タスクの実装に入る前に計画を作成し、確認を求めること」や「Git を使って中間ステップを常にコミットすること」といった指示を追加することで、より一貫性のあるワークフローを確立できます。 以下は、CLI が従うべきプロセスを定義した好例です(共有してくれた Ryan J. Salva 氏に感謝します): [GitHub Gist: ryanjsalva/0a7f6782b8988e760b88f1635ea55f2e](https://gist.github.com/ryanjsalva/0a7f6782b8988e760b88f1635ea55f2e) 一方、ネストされた(サブフォルダー内の)`GEMINI.md` ファイルは、コードベースの特定部分を個別に説明するのに役立ちます。たとえば、フロントエンドとバックエンドのコードが同居するモノレポ構成の場合、各コンポーネントに特化した `GEMINI.md` を個別に配置できます。あるいは、多数の内部パッケージを持つ Go プログラムにおいて、各パッケージ固有の制約や前提条件を CLI に守らせたい場合にも有効です。ユースケースに関わらず、複数の `GEMINI.md` ファイルを活用することで、特定のタスクに応じたきめ細かいコンテキスト制御が可能になります。 注記: Gemini CLI がコンテキストファイルとして `GEMINI.md` を使用するのと同様に、[Claude](https://www.anthropic.com/product/claude) や [Jules](https://jules.google) などの他の AI ツールも独自の Markdown ファイル(それぞれ `CLAUDE.md` と `AGENTS.md`)を持っています。もし `GEMINI.md` というファイル名が気に入らない場合や、すべてのツールで同一のファイル名を使いたい場合は、`settings.json` の `contextFileName` プロパティでコンテキストファイル名をいつでも変更できます: ```json { "contextFileName": "AGENTS.md" } ``` ## GEMINI.md ファイルのメンテナンス `GEMINI.md` ファイルに関して最もよく耳にする不満は、「メンテナンスすべきファイルがまた一つ増えた」という点です。しかし嬉しいことに、これを自分自身の手で保守し続ける必要はありません。 私がコーディング作業で Gemini としばらく「ペアプロ」した後(特に問題や行き違いが多発したセッションの後)によく実践しているのは、将来同じ問題が再発しないように Gemini 自身に教訓を要約させ、その学びを新たな指示や修正ルールとして `GEMINI.md` ファイルに直接反映させることです。このアプローチをとれば、微調整に数日かかったとしても、モデルは開発者の経験や個人の好みに寄り添いながら自然と進化していきます。 ## 最終手段:システム指示(system instructions)のオーバーライド 通常は 1 つ以上の `GEMINI.md` ファイルを利用するのが CLI の推奨カスタマイズ方法ですが、[システム指示(system instructions)](https://cloud.google.com/vertex-ai/generative/docs/concepts/system-instructions?utm_campaign=CDR_0x72884f69_default_b432031389&utm_medium=external&utm_source=blog) と `GEMINI.md` の内容が競合してしまう場合、奥の手とも言える「最終手段(nuclear option)」が必要になることがあります。冒頭でも触れたとおり、モデルが「過剰にプロアクティブ」になり、プロンプトに書いてもいない意図を勝手に汲み取ろうとする挙動には特に悩まされてきました。消すつもりのないファイルを勝手に削除されたり、新規コミットを作るべき場面で直前のコミットを修正(amend)されたり、長時間を費やした未コミットの作業があるにもかかわらずリポジトリを「お掃除」されたり……これらは、ここ数週間の間に過剰なプロアクティブさによって私が実際に被った痛い実害のほんの一例です。 `GEMINI.md` を工夫して、プロンプトの文面を厳密に字義通り解釈させようと試行錯誤したものの、ほとんど効果はありませんでした。そして最終的に、システム指示の深淵(ラビットホール)へと潜り込むことになったのです。私の仮説は、「`GEMINI.md` よりも優先度の高い何かが邪魔をしているのではないか」というものでした。ありがたいことに Gemini CLI はオープンソースであるため、ソースコードを直接開き、組み込まれているプロンプトの中身を調査することができました。以下は、そこで見つけたコードのスニペットです: ```markdown You are an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools. ## Core Mandates - Conventions: Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first. - Libraries/Frameworks: NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it. - Style & Structure: Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project. - Idiomatic Changes: When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically. - Comments: Add code comments sparingly. Focus on why something is done, especially for complex logic, rather than what is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. NEVER talk to the user or describe your changes through comments. - Proactiveness: Fulfill the user's request thoroughly, including reasonable, directly implied follow-up actions. - Confirm Ambiguity/Expansion: Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked how to do something, explain first, don't just do it. - Explaining Changes: After completing a code modification or file operation do not provide summaries unless asked. - Path Construction: Before using any file system tool (e.g., ${ReadFileTool.Name}' or '${WriteFileTool.Name}'), you must construct the full absolute path for the `file_path` argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path. - Do Not revert changes: Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes. ``` システムプロンプトは非常に長大です。このプレビューでは最初の十数行ほどしか掲載していませんが、実際には一般的なユースケースに応じた推奨技術スタックの解説に至るまで延々と続いています(完全なプロンプトは上記のリンク先 GitHub で確認できます)。もちろん、これは多種多様なユースケースに対応しなければならない CLI としては理にかなっていますが、特定用途に特化した私たち自身のプロジェクトには適さない場合もあります。 私が最も問題視している箇所は、49行目にあります: ```markdown - Proactiveness: Fulfill the user's request thoroughly, including reasonable, directly implied follow-up actions. ``` 私が求めているのは「質問には純粋に質問として答えてほしい」ということだけなので、私の抱える悩みの 8 割はまさにこの 1 行が原因だと確信しています。では、どうすればこの指示を無効化できるでしょうか? 行を削除するプルリクエスト(PR)を送ることも考えられますが、この挙動が役立っているユーザーもいるはずです。あるいはプロジェクトをフォークして自分専用の「Daniela CLI」を作る手もありますが、それもあまり現実的ではありません。 幸いなことに、コードを読み解くなかで、このカスタマイズに極めて役立つ未ドキュメントの環境変数を偶然発見しました:`GEMINI_SYSTEM_MD` と `GEMINI_WRITE_SYSTEM_MD` です。 1. `GEMINI_SYSTEM_MD`:デフォルトのシステムプロンプトを任意のカスタム Markdown ファイルで上書き(オーバーライド)できるようにします。 1. `GEMINI_SYSTEM_MD=SYSTEM.md`:指定したカスタム `SYSTEM.md` ファイルからシステムプロンプトを読み込みます。 2. `GEMINI_SYSTEM_MD=1`:`~/.gemini/system.md` からシステムプロンプトを読み込みます。 3. `GEMINI_SYSTEM_MD=0` または `GEMINI_SYSTEM_MD=""`:システムプロンプトを[起動時に動的構築](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/prompts.ts)します(デフォルト)。 2. `GEMINI_WRITE_SYSTEM_MD`:システムプロンプトの内容を指定パスのディスクに出力(保存)します。 1. `GEMINI_WRITE_SYSTEM_MD=SYSTEM.md`:システムプロンプトの内容を `system.md` ファイルに書き出します(大文字小文字は保持されません)。 2. `GEMINI_WRITE_SYSTEM_MD=1`:システムプロンプトの内容を `~/.gemini/system.md`(または `GEMINI_SYSTEM_MD` が設定されている場合はその場所)に書き出します。 3. `GEMINI_WRITE_SYSTEM_MD=0` または `GEMINI_WRITE_SYSTEM_MD=""`:ディスクへの出力を無効化します(デフォルト)。 注記: これらの環境変数を公式ドキュメントに記載するための [機能リクエスト(Feature Request)](https://github.com/google-gemini/gemini-cli/issues/3923) が現在オープンされています。 新しいシステムプロンプトをゼロから作成する手間を省くため、私はローカルプロジェクトのフォルダーで `GEMINI_WRITE_SYSTEM_MD` を `SYSTEM.md` に設定し、Gemini CLI を一度起動しました。これにより、システムプロンプトがディスクへ書き出されます。なお、大文字小文字は保持されないため、この例のように指定しても実際にはすべて小文字の `system.md` として書き出されます。 ```sh $ export GEMINI_WRITE_SYSTEM_MD=SYSTEM.md $ gemini ``` いつもの Gemini 起動画面が表示されます: ![Gemini CLI 起動画面](https://danicat.dev/ja/posts/gemini-cli-system-prompt/image-4.png) CLI を終了するには、`/quit` と入力するか、Ctrl+D または Ctrl+C を 2 回押します。これで `system.md` ファイルがディスクに書き出されているはずです。 注記: ちなみに、ファイルへの書き出しは終了時ではなく起動(boot)時に実行されます。 ファイルが実際に生成されているか確認してみましょう: ```sh $ head -n 10 system.md ``` 私の環境で同じコマンドを実行した結果がこちらです: ![system.md に対する head コマンドの出力結果](https://danicat.dev/ja/posts/gemini-cli-system-prompt/image-2.png) これでシステムプロンプト全体のコピーが手に入ったので、あとは心ゆくまで自由に編集できます! たとえば、問題の 49 行目を削除したり、ゲーム開発向けのセクションなど不要な箇所を丸ごと削ることも可能です(もちろん、実際にゲームを開発しているなら残しておいて構いません)。プロンプトの調整が終わったら、`GEMINI_SYSTEM_MD` 環境変数にカスタムファイルを指定して起動します: ```sh $ export GEMINI_SYSTEM_MD=system.md $ gemini ``` カスタムシステムプロンプトを適用していると、画面の左下隅に赤いサングラスのアイコンが表示されます: ![カスタムシステムプロンプトが有効になった Gemini CLI](https://danicat.dev/ja/posts/gemini-cli-system-prompt/image-3.png) これは、あなたが地球上で最高にクールな開発者である証拠であると同時に、カスタムシステム指示ファイルが正しく使われているサインでもあります。 ## まとめ この記事では、通常の `GEMINI.md` による設定と、システムプロンプト自体を直接オーバーライドする最終手段の 2 つのアプローチで、Gemini CLI の挙動をカスタマイズする方法を紹介しました。どのような技術でも同様ですが、この知識は責任を持って活用してください。自分好みのスタイルに合わせて Gemini を微調整し、日々の開発体験をより快適なものにする助けになれば幸いです。 皆さんからのフィードバックもお待ちしています。特に「こんな `GEMINI.md` のルールやシステムプロンプトが効果的だった」という実践例があれば、ぜひ教えてください。 --- **注記:** 興味がある方は、このブログの開発で私が実際に使用している `system.md` ファイルを [GitHub リポジトリ](https://github.com/danicat/danicat.dev/blob/main/system.md) で公開していますので、参考にしてみてください。 --- # AI時代のモダンな開発者ワークフロー - **Markdown URL**: https://danicat.dev/ja/posts/developer-workflow-ai-world/index.md - **HTML URL**: https://danicat.dev/ja/posts/developer-workflow-ai-world/ - **Date**: 2025-07-11 - **Categories**: Agentic Coding - **Tags**: gemini-cli, jules, vibe-coding - **Summary**: 技術的確実性とビジネス価値の2軸で整理するAI時代の開発者ワークフロー。Gemini CLIとJulesを使い分ける優先順位付けフレームワークを提案。 ## はじめに ベルリンで開催された WeAreDevelopers World Congress 2025 から戻ってきたばかりですが、ヨーロッパ中や世界中から集まった多くの開発者と出会い、大いに刺激を受けました。もちろん、今年のメインテーマはAIでした —— AIはまさに至るところに存在しています!今やクラウドにも、PCにも、サングラスにも、トースターにも、キッチンの流し台にも、果てはトイレットペーパーにまでAIが搭載される時代です。誰もAIからは逃れられません……あのJavaScriptフレームワークの増殖スピードですら、AIの進化の速さには太刀打ちできないほどです!人類はもうおしまいです!!!>.< ……というのは冗談です(あるいは、あながち嘘でもないかもしれませんが!)。確かに今は少々不気味で先が見えない時代です。テクノロジー業界は大きく変容しています。「AIによって生産性が向上した」「AIが人員を代替できる」といった名目のもと、世界中のテック企業で大規模なレイオフが相次いでいます。これがレイオフの真の理由なのか、それとも別の思惑を隠すためのスケープゴート(口実)にAIが使われているだけなのかは、パブでビールでも飲みながら話すべきネタであって、このブログの本題ではありません。ただ確かなのは、今まさに大きな地殻変動が起きているということです。 私がこれまでの人生と経験から学んだ大切な教訓の一つに、「自分がコントロールできないものに思い悩んでも仕方がない」というものがあります。AIの台頭は不可避です。それなら、将来自分たちの仕事がどうなってしまうのかを不安がるのではなく、「今日、いまの自分の仕事を向上させるためにAIが何をしてくれるか」に目を向けてみませんか。ピンチをチャンスに変える、いわゆる「物事をポジティブに捉える(コップに水が半分も残っていると考える)」アプローチです。少し肩の力を抜いて、開発プロセスに「バイブス(vibe)」を取り入れた、現代の開発者ワークフローがどうあるべきかを一緒に考えてみましょう。 これは、私が過去4週間にわたって実際に実践し、非常に高い効果を実感しているワークフローです。もちろん、まだ発展途上で完璧なものではありませんので、あくまで参考程度にとどめてください。とはいえ、この領域の進化スピードは目覚ましく、今後さらに洗練されていくはずです。 ## 優先順位付けに対する2次元アプローチ 実際の「AIネイティブ」な働き方に踏み込む前に、私がこれまでのキャリアで7年以上にわたって使ってきた優先順位付けモデルを簡単にご紹介します。これは ThoughtWorks 在籍時にアジャイル変革(Agile transformation)プロジェクトで学んだ手法を、自分自身のニーズに合わせてカスタマイズしたものです。エンジニアとステークホルダー双方の関係者をひとつの部屋に集めて議論し、バックログを消化していく論理的な実行順序を導き出します。 このモデルは、`技術的確実性(technical certainty)`と`ビジネス価値(business value)`という、直交する2つの軸に基づいています。 ![優先順位付け演習の結果](image-3.png "演習結果に基づく実装の優先順位") 「技術的確実性」とは、その機能の実装手順や道筋がどれだけ明確になっているかを表します。技術的確実性が高い場合、実装に必要な全工程(またはほぼすべてのステップ)がすでに判明している状態です。逆に技術的確実性が低い場合は、どう実装すべきか見当がつかないか、最初の数ステップ程度しか見えていない状態を意味します。 「ビジネス価値」とは、チームの目標達成においてその機能がどれほど重要であるかを表します。ビジネス価値が高い機能はビジネスの成功に直結するクリティカルなものであり、ビジネス価値が低い機能は「あれば嬉しい(nice to have)」程度にとどまります。 この演習の最終的な目的は、**「すべてが最優先」**という膠着状態を打破することです。たとえ建前としてはすべてが重要だとしても、それらを2軸上に並べて可視化することで、最も頑ななステークホルダーであっても他の優先事項との相対的な位置関係を見直しやすくなります。また、ビジネス価値が同じでも技術的確実性が異なるタスクにも、実行すべき順番が存在します。まずは「手近な成果(low-hanging fruit)」を先に片付けることで、チームは残りの機能についてスパイク(技術調査)を行い、不確実性を減らす(=技術的確実性を高める)ための時間を稼ぐことができるのです。 ![優先順位別の作業モード](image-2.png "優先順位付けに基づく推奨作業モード") では、この優先順位付けモデルが「AIネイティブな働き方」とどう結びつくのでしょうか?私は自分自身を「何体ものAIを率いるボス(上司)」だと捉えています。自分自身のバックログに優先順位をつけ、どのタスクにどのツールを割り当てるかを差配するのです。深く関与してこだわりたい(「コミットしている」)機能であれば、同期的に対話しながら進めることを優先し、そうでなければバックグラウンドで動く非同期ワーカーにタスクを委任します。 ## 基本的な「AIネイティブ」ワークフロー システムの新しい機能を実装する場面を考えてみましょう。作業の進め方には、対話型モード(インタラクティブ)とバッチモード(非同期・投げっぱなし/fire-and-forget)という2つの主要な運用モードがあります。どちらを選択するかは、その機能をどう実装するかについての`技術的確実性`と、現時点でそれにどれだけ深くコミットしたいか(`ビジネス価値`)に大きく依存します。 ![優先順位付けの例](image.png "このブログにおける優先順位の例") たとえば前回の記事([Julesを使ってこのブログに特集記事を追加した方法](https://danicat.dev/ja/posts/20250703-jules-featured-post/))でも触れた、このブログのトップページに「おすすめ記事(featured post)」セクションを実装したときのケースを考えてみましょう。着手した当初、私は特集記事セクションの実装方法について何も知りませんでしたが、「実現したいゴール」のイメージだけは持っていました。どの技術を使うべきかも、コードのどこに手を加えればよいかも分からなかったため、これは典型的な「技術的確実性が低い」課題でした。一方で、「トップページが格段にプロフェッショナルになり、読者にとって魅力的になる」という明確な仮説があったため、自分にとっての「ビジネス価値」は非常に高いものでした。 このような状況では、フィードバックループを極力短くするのが自然な選択となります。AIが提案してくる変更を一つひとつ即座に確認し、正しい方向へと軌道修正しながら導いていく必要があるからです。逆に、優先度の低いタスクであればフィードバックループが長くても困らないため、バッチモード(非同期処理)に任せるのが適しています。 ## 低〜中程度の技術的確実性、または高いビジネス価値 = 対話型モード(同期) 技術的確実性が低い課題では、正しいゴールに到達するためにより手厚い監督が必要となるため、CLIツールを用いた対話型のプロセスを好んで使用しています。現在私がメインで使っているツールは、Google からリリースされてまだ2週間ほどながら、すでに開発者の世界で大きな話題を呼んでいる [Gemini CLI](https://cloud.google.com/gemini/docs/codeassist/gemini-cli?utm_campaign=CDR_0x72884f69_default_b431747616&utm_medium=external&utm_source=blog) です。 Gemini CLI は、AI を組み込んだ REPL(対話型実行環境)のようなコマンドラインアプリケーションです。プロンプトを入力すると CLI が即座に応答を返してくれます。Model Context Protocol(MCP)をサポートしているため、返ってくるのは単なるコードやテキストにとどまらず、実質的に何でも操作可能です。このサポートのおかげで、コーヒーの注文からデータベースの更新に至るまで、文字通りあらゆる用途に CLI を活用できます。もちろん、CLI の最も自然なユースケースはコーディングですが、人間の探究心はとどまることを知りません。:) Gemini CLI には自動化を想定した YOLO モードも用意されていますが、正直なところ、人間の監督なしにすべてを任せられるほどにはまだ信頼していません(これについては後述します)。そのため、解決策を決める前に問題領域をブレインストーミングし、探索したい場面で CLI を使うようにしています。機能の設計プランを作らせたり、実装の選択肢を調査させたり、あるいはとりあえず一気に実装させてみることもあります —— そして、その初期実装から得られた学びをもとに一度コードを破棄し、クリーンな状態から作り直すのです。これがいわゆる「プロトタイピング」です。 手動でコーディングして納得のいくプロトタイプを作るのに何度も試行錯誤するのと同様に、適切なプロンプトにたどり着くにも何度か試行が必要です。決定的な違いは、手作業なら1つのプロトタイプに1週間かかっていたところを、通常わずか30分から1時間程度で完了できる点です。1日で3〜4通りの異なる実装パターンを試すことができ、その日の終わりには十分な比較データをもとに、確信を持って1つの設計を採用できるようになります。 技術的確実性が低い課題に CLI を使う最大の理由は、フィードバックループがほぼ瞬時に回るためです。仮説を検証し、粗い部分を手直しして、すぐに次のイテレーションへ進めます。発生するタイムラグは、モデルがリクエストを処理するわずかな時間だけです。 ## 高い技術的確実性かつ低〜中程度のビジネス価値 = バッチモード(非同期) 前述のとおり、技術的確実性が高い課題とは、実装に必要なすべての(またはほぼすべての)手順がすでに見えている状態を指します。手順が分かっているのであれば、自分で手を動かす代わりに AI に実行を任せてしまえばよいため、作業が格段にスムーズになります。 このような場面こそ Gemini CLI の YOLO モードの出番と思えるかもしれませんが、実はこの用途にはさらに適した [Jules](https://jules.google/) というツールが存在します。Jules は今年の Google I/O で発表されたツールで、瞬く間に私の一番のお気に入りとなりました(Gemini CLI は僅差の2位です)。 Jules は GitHub と連携してバックグラウンドでタスクを実行してくれる非同期エージェントです。正直に告白すると、最初に Jules に触れたときは詳細をよく読まず、動作の遅さに少しやきもきしていました。しかししばらくして、「バックグラウンドにタスクを投げておき、自分はPCから離れて別の作業や私生活に集中できることこそが本質」なのだと気づいたのです。 Jules は GitHub に接続されているため、プロジェクト全体のコンテキストをすでに把握しています。そのため、「依存パッケージのバージョンアップ」「単体テストの実装」、さらには「特定のバグの修正」といったメンテナンスタスクを任せることができます。ここで重要なのは、フィードバックループが長い(すぐに結果が返ってくるわけではない)ため、手順やゴールがステップ・バイ・ステップで明確に見えているタスクに限定して利用するのが賢い使い方だということです。 ## 高い技術的確実性かつ高いビジネス価値 = 対話型モード(同期) 上記の分類を見て、「高い技術的確実性」と「高いビジネス価値」が組み合わさった領域を、なぜあえて同期的な対話型モードに指定しているのか疑問に思われたかもしれません。その理由は極めて単純で、私自身がその成果(高いビジネス価値)に強くこだわっており、自分の目で直接進捗を見届け、最短で完成させたいからです。パラメータの重みが同等であれば、技術的確実性よりもビジネス価値が常に優先されるべきだと私は考えています。 ## 低い技術的確実性かつ低いビジネス価値 = 本当にやるべきか? これらは普段、バックログの底に埋もれて忘れ去られてしまうようなタスクです。AI 以前の時代であれば放置一択でしたが、AI が使える現代なら、ふと思い出したタイミングで Jules にタスクを投げ、不確実性の低減やビジネス価値の発掘に向けた可能性を探らせてみます。投げる側の認知的負荷(認知コスト)はごくわずかなので、思いつく限りの簡単なプロンプトで Jules タスクを立ち上げても、失うものは文字通り何もありません。仮に大した成果が得られなかったとしても、どうせ自分では手をつけなかったタスクですから、少しでも有益な知見が得られればそれだけで儲けもの(プラス)です。 ## 例外 もちろん、どんな優れたプロセスにも例外はつきものです。高いビジネス価値を持つタスクであっても Jules に委任するケースがあり、それは主に「そうする以外に手がない状況」です。例えば以下のような場面です: 1. カンファレンスなどのイベントに参加中で、新しいアイデアを思いついてもPCを開いて実装できないが、手元にスマートフォンはあるとき 2. 移動中でネット回線が不安定、あるいは極端に遅いとき 3. スーパーのレジ待ちの列でやり残したタスクを思い出し、帰宅後すぐに取り掛かれるよう下準備(キックスタート)をしておきたいとき 要するに、「進捗ゼロ」か「Jules にタスクを投げる」かの2択であれば、迷わず Jules を起動して作業を進めてもらうということです。 また、ここまでの説明では従来の IDE がワークフローのどこに位置づけられるのかについて触れていませんでした。IDE を捨て去ったわけではありません。現に、このブログ記事も VS Code で執筆しています。私は IDE による手動編集を「ラストワンマイル」や「最終的なコードの仕上げ」のために残しています。注意点として、「バイブコーディング(vibe coding)」のセッションの真っ最中に手動で細かな編集を加えるのは避けてください。LLM が文脈を見失って脱線する原因になりやすいからです。しかし、プロセスの最終工程として手動調整を行うのであれば、何の問題もありません。 ## 番外編:不確実性を減らすためのTips あるトピックについて調査したいものの、自分のコードベースにどう組み込むかさえ見当がついていない段階もあるでしょう。Gemini CLI や Jules に無理やりコーディング以外の調査タスクをさせようとするのは、壁にネジを留めるのに金づちを使うようなものです。このような純粋なリサーチが必要な場面では、代わりに [Gemini Deep Research](https://gemini.google/overview/deep-research/?hl=en-GB) を使うのがおすすめです。Jules と同様、Gemini Deep Research も非同期で動作するため、バックグラウンドでリサーチを開始させておき、自分は普段の作業を続けられます。 もし PC の前にいて結果を待ちたくない(即座に対話したい)なら、Google 検索のグラウンディングを有効にした通常の Gemini を使うのも非常に効果的です。ただしどちらのツールも出力がやや冗長になりがちです。私のようにせっかちな(あまり待つのが得意でない)方は、得られたリサーチ結果をそのまま [NotebookLM](https://notebooklm.google/) に投入し、要約させたり、移動中に聴けるポッドキャスト(音声解説)を生成させたりすると、情報収集がさらに捗ります。 ## 結論 優先順位付け演習に基づく AI ツールの選定プロセスと作業モデルは、以下のように要約できます: ![優先順位別の推奨ツールの概要](image-1.png "推奨ツールと作業モードの概要") 1. **高い技術的確実性 + 高いビジネス価値** = Gemini CLI による同期プロセスまたはペアプログラミング。細かな仕様確認には検索グラウンディング付き Gemini を活用。 2. **低〜中程度の技術的確実性 + 高いビジネス価値** = Gemini CLI による同期プロセス + 技術的確実性を高めるための非同期リサーチ。 3. **高い技術的確実性 + 低〜中程度のビジネス価値** = Jules による非同期プロセス。必要に応じて Deep Research を併用。 4. **低い技術的確実性 + 低いビジネス価値** = 原則として着手しない(どうしても進めたい場合は、Jules や Deep Research を活用してどちらかのパラメータ向上を試みる)。 このワークフローについてどう思われますか?ぜひ下のコメント欄でご意見やご感想をお聞かせください! --- # Jules を使ってブログに特集記事セクションを追加した記録 - **Markdown URL**: https://danicat.dev/ja/posts/jules-featured-post/index.md - **HTML URL**: https://danicat.dev/ja/posts/jules-featured-post/ - **Date**: 2025-07-03 - **Categories**: Agentic Coding - **Tags**: jules, tutorial, vibe-coding - **Summary**: AIコーディングエージェント「Jules」を活用し、Hugo と Tailwind CSS でブログに特集記事セクションを反復的に実装したケーススタディ。プロンプトの試行錯誤、スタイリング、得られた学びを紹介。 > **著者注:** この記事の約90%はAIによって執筆されたものですが、読みやすく自然な文章になるよう私自身が校正・編集を行いました。Jules自身が自分の成果を誇らしげに語る傾向があったのは微笑ましいポイントでした。この最終結果に到達するまでに数多くのプロンプトで誘導する必要がありましたが、最後の微調整は手動で行ったほうがスムーズでした。編集の全履歴は [PRのコミット履歴](https://github.com/danicat/danicat.dev/pull/6) で確認できます。ちなみに、Julesは「翻訳機能は備えていない」と主張してこの記事のポルトガル語(ブラジル)翻訳を完全に拒絶したのですが、このブログ全体の翻訳は以前のやり取りでJules自身が行ったものでした。どうやらその時は気分が乗らなかったようです。:) ## はじめに 最近、最新のコンテンツをより効果的にハイライトするために、ブログのホームページを更新することにしました。バックエンドエンジニアである私にとって、フロントエンドの細かな実装に深く踏み込むことは普段の業務ではありません。そのため、あまり慣れていない領域ですべての変更を手作業でコーディングする代わりに、AIコーディングアシスタントである [Jules](https://jules.google) の力を借りることにしました。 この記事では、私たちの反復的な取り組みの過程、うまくいった点、(時には思わず笑ってしまうような)すれ違い、そしてWeb開発においてAIを効果的に活用し、特にスキルギャップを埋める方法について学んだことを詳しく紹介します。 ## 実装のゴール:特集記事(Featured Post)セクション Julesへの最初のリクエストはシンプルでした。 > 「メインページのレイアウトを変更して、最新のブログ記事を「最近の投稿」一覧に含めるのではなく、特集(ハイライト)として上部に目立たせて表示してください。「最近の投稿」リストには、その最新記事を除いた過去の記事を表示するようにします。なお、この挙動はブログのトップページ(Home)のみに適用してください。ユーザーが「Blog」メニューをクリックした際には、最新記事も含めすべての投稿が従来どおり新しい順(逆時系列)で表示される必要があります。」 Julesは意図を即座に汲み取り、Hugoのコードベースを探索して該当テンプレートを特定し、必要な修正を加える作業計画を提示してくれました。 ## 試行錯誤のハイライト:良かった点、苦戦した点、そしてAIの挙動 理想の表示を実現するまでに、いくつかのイテレーション(試行錯誤)を重ねました。 ### イテレーション1:初期セットアップ — まずは基本の骨組みから Julesは、Blowfishテーマのパーシャル(partial template)を的確に特定し、テーマのオーバーライド構造を整えてくれました。「Recent Posts(最近の投稿)」一覧から最新記事を切り離すロジックもスムーズに実装されました。 * **うまくいった点:** Hugoのコア構造の把握、記事リストの取得、基本的なテンプレートの改修。テーマやプロジェクト内のファイルを迅速に探索・把握するJulesの能力は、ここで大きな時間短縮につながりました。 ![タスクの実行結果を待機するJulesの画面](images/image001.png "各タスクの合間には、結果を待つ時間がそれなりにありました") ### イテレーション2:Tailwind CSS によるスタイリング — 試行錯誤のダンス 続いて、タイトル表示、カード幅、アイキャッチ画像のサイズなど、見た目のスタイリングに着手しました。ここではビジュアルを微調整するために、次のようなプロンプトを何度もやり取りすることになりました。 > 「特集記事のタイトルを『Featured Post』に変更してください。カード幅は画面幅の約80%に調整してください。画像が縦長すぎるので、4:3のアスペクト比を試してみましょう。……まだ少しイメージと違うので、もう少し横長(高さを低く)にしてください。」 ここで、視覚的な要素についてJulesと反復的に作業する難しさと特徴が如実に現れました。 * **Julesのアプローチ:** タイトルのためにi18nファイルを修正し、Tailwindの様々な幅ユーティリティクラス(`md:w-4/5`、`md:w-2/3`、`max-w-xl`、`max-w-2xl` など)を使い、画像のアスペクト比を調整するために `padding-bottom` を操作しました。 * **課題とフラストレーション:** 特に私のように普段バックエンドを中心に開発しているエンジニアにとって、AIという仲介者を挟んでTailwindのスタイリングを手探りで進める作業は大きな課題でした。Julesが適切だと考えたクラスを適用しても、レンダリング結果がこちらの思い描くイメージとすぐに一致するとは限りません。Tailwindのクラスを変更しても、一発で目に見える明確な差が出なかったり、意図した通りの効果が得られなかったりすることがありました。その結果、「このクラスを試してみて」「いや、もっと幅を狭く/広く/縦を低くして」というやり取りが何巡も続くことになりました。最終的にはうまくいったものの、時にはもどかしさを感じることもありました。AIを活用した非同期ワークフローにおいて、コードの変更と即座の視覚的フィードバックとの間に乖離があることが浮き彫りになりました。 * **得られた学び:** 指示が意図しない結果を生むことが多いため、見た目の微調整はAIとの共同作業の中で最も骨が折れる部分です。具体的でわかりやすいフィードバックを与えることが不可欠ですが、画面を直接指さしたりリアルタイムに自分で微調整ができない以上、多少の行き来は避けられないと認識することも大切です。それでもJulesはリクエストされた変更を一つひとつ愚直に適用してくれたため、フロントエンドに関する私の知識不足をしっかりと補ってくれました。 ### イテレーション3:カスタムCSS vs Tailwind — ちょっとした寄り道 ある時点で、カードの寸法をより細かくコントロールしたいと考え、次のようにプロンプトを出しました。 > 「Jules、既存のクラスを使おうとするのではなく、特集記事カード専用のユニークなスタイルクラスを新規に作成してください。このスタイルでは、コンテナに対して幅と高さを相対的に75%に設定してください……」 * **Julesの対応:** Julesは指示どおりにカスタムCSSルールを正しく作成し、カード用のパーシャルをリファクタリングしてそれらを適用しました。 * **結果と学び:** Julesはリクエスト通りに実装してくれましたが、Tailwindに大きく依存しているブログ全体のデザインの中で、その結果はどこか浮いた印象になってしまいました。カスタムCSSがうまく調和しなかったため、Tailwindとの一貫性を維持するほうが重要だとすぐに判断しました。これは、AIが生成したソリューションであっても、既存のデザイン言語や、確立されたフレームワークを尊重するという開発者の好みに合致させる必要があることを示す良い教訓となりました。Julesはリクエストに応じて再びTailwindへと戻してくれました。 > 「直前の変更を取り消して、Tailwindスタイルのフォーマットに戻してください。Tailwindのベストプラクティスに沿って同様のスタイルガイドラインを適用してください」 ### イテレーション4:「コメント」をめぐる大いなるすれ違い! 人間とAIのインタラクションにおいて、おそらく最も示唆に富んでいたのがこの場面です。私は次のように伝えました。 > 「特集記事の中にコメントがレンダリングされてしまっています。すべてのコメントを削除するか、非表示にしてください」 * **Julesの解釈:** Julesは、私がブログの *ユーザーコメントシステム*(UtterancesやGiscusなど)や、閲覧数・いいね数といったメタデータを指していると解釈しました。その結果、Julesは閲覧数・いいね数のメタデータを調査し、それらを条件付きで非表示にしようとする一連の作業を進めてしまいました。 * **私の補足説明:** これらの変更の後、具体例を挙げて誤解を解きました。 > 「違います、閲覧数やいいね数を消してほしいとは言っていません。画面に `{/* Adjusted padding ... */}` や `{/* Removed prose classes ... */}` のようなコードコメントがそのまま文字としてレンダリングされてしまっていることを指しています」 * **解決:** 私が言っていたのが、正しくない形式で書かれた *Goテンプレート/HTMLの文字通りのコードコメント*(Hugoの正しいコメント記法 `{{/* ... */}}` ではなく `{/*...*/}` と書いていたため、テキストとしてレンダリングされていた)だとJulesが理解すると、修正は一瞬でした。テンプレートから問題のテキストを削除して解決しました。 * **感心した点:** (誤解があったとはいえ)問題をデバッグしようとするJulesの粘り強さと体系的なアプローチは見事でした。 * **課題と学び:** これはAIとの対話における極めて重要な側面を浮き彫りにしました。それは「自然言語の曖昧さ」です。「コメント」という言葉には複数の意味があります。私の最初の指示は十分に厳密ではありませんでした。 ### イテレーション5:仕上げの微調整 目に見えていたテンプレートのコメントを解決した後、最後の仕上げを行いました。 > 「『Featured Post』という見出しタイトルを削除してください。カード幅を50%に変更し、タイトルとサマリーのフォントサイズを大きくしてください。アイキャッチ画像のアスペクト比は16:9にしてください。」 これにより、カード幅、フォントサイズ、画像のアスペクト比の最終調整が行われました。幅のパーセンテージ指定は効果がありませんでしたが、アスペクト比の変更は見事に狙い通りの効果を発揮しました。 ### ボーナス・イテレーション:Jules がこの記事のドラフトを執筆! > 「完璧です! これ以上のコード変更は必要ありません。では次に、今行った一連のイテレーションを振り返る新しいブログ記事のエントリを作成してください……」 そして完成したのがこの記事です! この記事自体、Julesとのやり取りログと私のガイダンスフィードバックをもとに、Julesの支援を受けてドラフトが執筆されました(まさに今あなたが読んでいるこの内容も含めてです)。 ## Jules との協調でうまくいった点 * **スキルギャップの解消:** バックエンドエンジニアである私にとって、Hugoのテンプレート処理やTailwind CSSなど、普段あまり触れないフロントエンドのタスクに取り組む上で、Julesは非常に心強い味方でした。Julesはフロントエンドに関する私の知識不足を補い、提示された解決策を私自身が誘導し、ブラッシュアップしていくことができました。 * **実装スピード:** 要件が明確に理解されている変更であれば、Julesは手作業でタイピングするよりもはるかに速く、コードの修正、ファイルの作成、構造のリファクタリングを行ってくれます。 * **複雑な指示への対応力:** 全体として、Julesは複数ステップにわたるリクエストや複雑なレイアウトの目標を的確に理解してくれました。 * **体系的な問題解決:** たとえ誤解が生じた場合でも、Julesは多くの場合、論理的なプロセスに沿って作業を進めてくれました。 * **反復的な微調整への適応性:** 微調整のリクエストに対しても、Julesは常にフィードバックを受け入れ、柔軟に対応してくれました。 ## 課題と学び * **言葉の厳密さ:** 「コメント」をめぐる出来事は、正確な言葉遣いがいかに重要であるかを如実に物語っています。人間にとっては当たり前の表現や略語であっても、AIにとっては曖昧に解釈される可能性があります。 * **視覚的なフィードバックループと Tailwind:** Tailwindでのスタイリングにおける試行錯誤は大きな課題でした。Julesは出力結果を直接「見る」ことができないため、理想の見た目を言葉で説明したり、なぜ特定のクラス指定が期待通りに動かないのかを伝えるには、根気と詳細な説明が必要でした。これは、視覚的なタスクをテキストベースでやり取りする以上、避けられない性質です。 * **誤解と軌道修正:** Julesがタスクを誤解した場合、そのまま間違った方向へと忠実に突き進んでしまいます。タスクの途中で処理を中断する方法がなかったため、現在の一連のアクションシーケンスが完了するのを待ってから、修正のフィードバックを与える必要がありました。 * **非同期ワークフローと開発ペース:** 作業はほとんどが非同期で進行します。各リクエストとJulesによる実装には、数分から、複雑なシーケンスの場合は30分近くかかることもありました。そのため、即座にフィードバックが得られる直接コーディングや、リアルタイムのペアプログラミングと比べると、イテレーションのサイクルは遅くなります。 ## おすすめのリソース Julesについて詳しく知りたい方は、以下の公式リソースを参照してください。 * [Jules 公式サイト](https://jules.google) * [Jules 公式ドキュメント](https://jules.google/docs) ## まとめ 全体として、ホームページのこの機能をJulesと一緒に実装した経験は非常に生産的なものでした。まさにAIを誘導しながら進める「バイブコーディング(vibe-coding)」そのものを実感できました。成功の鍵は、明確で反復的なコミュニケーション、すれ違いが起きたときの忍耐強さ、そして具体的で実行可能なフィードバックを進んで提供する姿勢にあります。 Tailwindの試行錯誤やAIによる時折の誤解といったフラストレーションは、AI支援開発の現状における避けられない一面です。しかし、非同期な性質や生成にかかる時間があったとしても、コーディングの機械的な部分を肩代わりしてもらい、自分の専門外の領域(Tailwindの具体的な実装やHugoの構造など)について提案をもらえたことは、最終的に確実なプラスとなりました。この単一機能のためだけに、フロントエンドのデザイン原則、Hugoの複雑な仕組み、Tailwind CSSの細かな仕様をゼロからすべて学ぼうとするよりも、はるかに速く、効果的でした。 JulesのようなAIアシスタントは極めて強力なツールです。人間の監督や設計意図そのものを完全に代替するわけではありませんが、適切なマインドセットとコミュニケーション戦略を持って臨めば、開発効率を飛躍的に高めてくれる頼もしい相棒になります。 --- # Gemini Cloud Assistでプロンプトからインフラを設計・構築する - **Markdown URL**: https://danicat.dev/ja/posts/gemini-cloud-assist/index.md - **HTML URL**: https://danicat.dev/ja/posts/gemini-cloud-assist/ - **Date**: 2025-07-02 - **Categories**: Applied GenAI - **Tags**: gemini, google-cloud, tutorial - **Summary**: Gemini Cloud Assist と Application Design Center を使い、自然言語プロンプトからGoogle Cloudのインフラ設計とTerraformコード生成を行う手順を解説。 ## はじめに 今回はいつものAIエージェントの話題から少し寄り道して、先週参加した「I/O Connect Berlin 2025」をきっかけに触ってみた製品について紹介します。 このイベントには、Google Developer Groups(GDG)のメンバーやコミュニティのエキスパートをはじめ、ヨーロッパ各地から1,000人以上の開発者が集まりました。4月にDevRelチームにジョインして以来、私にとって初めての公式Googleイベントでもあったため非常に感慨深いものでした(先週ブログの更新をお休みした理由もこれです!)。 私は会場で「Design and Deploy」というデモセッションを担当しました。これは [Application Design Center (ADC)](https://cloud.google.com/application-design-center/docs/overview?utm_campaign=CDR_0x72884f69_awareness_b428663487&utm_medium=external&utm_source=blog) と [Gemini Cloud Assist (GCA)](https://cloud.google.com/products/gemini/cloud-assist?utm_campaign=CDR_0x72884f69_awareness_b428663487&utm_medium=external&utm_source=blog) という2つのプロダクトを組み合わせたデモです。参加者の反応がとても好評だったため、現地に来られなかった皆さんにもこのテクノロジーを体験していただけるよう、ブログ記事としてまとめることにしました。 Application Design Centerは、アーキテクトや開発者がアプリケーションのインフラストラクチャを設計するのを支援するプロダクトです。画面上ではインフラの構成要素を視覚的に定義できる使いやすいUIが提供されていますが、内部(アンダー・ザ・フード)ではUI上のすべてがTerraformモジュールとして表現されているため、[Infrastructure as Code (IaC)](https://en.wikipedia.org/wiki/Infrastructure_as_code) のメリットもそのまま享受できます。 あらかじめお伝えしておくと、ADCは現在 [パブリックプレビュー(Public Preview)](https://cloud.google.com/products?e=48754805&hl=en#product-launch-stages&utm_campaign=CDR_0x72884f69_awareness_b428663487&utm_medium=external&utm_source=blog) の段階にあります。日々機能が進化しているため、過去のバージョンとの互換性が失われる可能性もあります。また、後述するように、一般提供(GA)までに改善が期待される粗削りな部分もいくつか見られます。 一方のGemini Cloud Assist(こちらもパブリックプレビュー中)は、Google CloudにおけるGemini支援機能の公式製品名です。単体の独立したプロダクトというよりも、大規模言語モデル(LLM)ベースの最新チャット体験を通じて、Google Cloud(GCP)のあらゆるリソースやサービスと自然言語で対話できるようにする「結合組織」のような役割を果たしています。 それでは、この2つのテクノロジーを組み合わせて、アプリケーションのインフラを素早く設計・構築していく流れを見ていきましょう。 ## アプリケーション設計セッションを開始する Application Design CenterはGoogle Cloudコンソールから手動で開くこともできますが、それではあまり面白くありません。新規設計でADCを起動する一番の方法は、任意のページでGeminiパネルを開くことです。たとえばここでは、プロジェクトのウェルカム(Welcome)ページを使用しています。 ![Google Cloudコンソールのウェルカム画面](images/image001.png "Google Cloudコンソールのウェルカム画面") 検索バー右側にある「星(スパークル)」アイコンのボタンをクリックすると、Gemini Cloud Assistのペインが開きます。 ![Geminiボタンの拡大図](images/image002.png "Geminiボタンの拡大図") すると、次のような画面が開きます。 ![Gemini Cloud Assistのウェルカム画面](images/image003.png "Gemini Cloud Assistのウェルカム画面") このパネルからGeminiと対話できます。「〇〇を行うアプリケーションを作成して」といったプロンプトとともに、アーキテクチャに関する詳細をできるだけ具体的に伝えてみましょう。例として、「猫の画像を生成するアプリケーション」を作成してみます。入力するプロンプトは以下のとおりです。 > Geminiを使って猫の画像を生成し、Cloud SQLデータベースに保存するアプリケーションを作成してください。ユーザーは生成サービス(generation service)を使って新しい画像をリクエストでき、画像サービス(pictures service)で生成された画像を閲覧できます。両方のサービスはフロントエンドサービスとグローバルロードバランサーを通じて公開されます。 プロンプトを入力すると、Geminiが少し推論を行い、数秒後に次のような構成案を出力してくれます。 ![アーキテクチャ図を含むGeminiの応答](images/image004.png "アーキテクチャ図を含むGeminiの応答") インラインで表示される図だけでも大まかな構成は把握できますが、[Edit app design(アプリデザインを編集)]ボタンをクリックすると、さらにデザインを直感的に操作・調整できます。拡張ビューで設計図が開き、構成をブラッシュアップできるようになります(※この記事の以降のセクションでは、[Edit app design]をクリックすると「Preview」ウィンドウが開く環境を前提に解説します。もし開かない場合は、記事末尾の注記をご確認ください)。 「Preview」ウィンドウの画面は以下のようになります。 ![Gemini Cloud AssistのPreviewウィンドウ](images/image005.png "Gemini Cloud AssistのPreviewウィンドウ") 命名規則や生成された各コンポーネントの設定内容を調整したい場合は、コンポーネントをクリックして設定パネル(configuration panel)を開けばいつでも変更できます。ここでは `frontend-service` の設定パネルを開いてみました。 ![コンポーネント詳細パネルのビュー](images/image006.png "コンポーネント詳細パネルのビュー") この画面には Cloud Run で起動されるコンテナイメージも表示されており、デフォルトではサンプル用の `hello` コンテナが指定されています。Gemini Cloud Assist は私たちがどのコンテナを実行したいかまでは把握していないためですが、イメージ情報を渡せば適切な値に差し替えてくれます。 ここで前提として強調しておきたいのは、**このツールはアプリケーションのコードそのものを実装するのではなく、それを支えるインフラストラクチャを設計・構成するためのツールである**という点です。フロントエンドやバックエンドの実際のコードを書くには、Gemini CLI や普段使っている IDE などの開発ツールを利用し、ビルドしたコンテナイメージを Artifact Registry などのコンテナレジストリにプッシュして Cloud Run から参照できるようにする必要があります。 Preview ウィンドウでは既存コンポーネントの編集は可能ですが、手動で新しいコンポーネントを追加することはできません。構成を段階的に改善(イテレーション)したい場合は、Gemini に修正指示を出します。たとえば、次のようなフォローアッププロンプトを投げてみます。 > 猫の画像が生成されるたびにイベントをキャプチャするストリーミングサービスを追加してください。ストリームの先にはコンシューマーサービスを配置し、GCS(Cloud Storage)でホストされている静的ページを更新して最新画像をフィードに追加できるようにします。 これがGeminiの応答です。 ![フォローアッププロンプトに対するGeminiの応答](images/image007.png "フォローアッププロンプトに対するGeminiの応答") すると Preview ウィンドウが新しい構成図に更新され、追加(緑)、変更(青)、削除(赤)がカラーハイライトで視覚的に表示されます。 ![提案された構成図の変更](images/image008.png "提案された構成図の変更") 画面下部に提案を採用するか(Accept)、破棄するか(Reject)の選択肢が表示されます。ですがその前に、内部で生成された Terraform コードをチェックしてみましょう。コードの差分を確認するには、[View diff(差分を表示)]をクリックします。 すると Code Diff ウィンドウが開き、変更前後のコードが左右に並んで表示されます。 ![Terraformコードの前後比較を示す差分レビューウィンドウ](images/image009.png "Terraformコードの前後比較を示す差分レビューウィンドウ") ご覧のとおり、ダイアグラム上の各ボックスがそれぞれ個別の Terraform モジュールに対応しています。下にスクロールすると、今回追加されたモジュールが緑色でハイライトされているのが確認できます。 生成された内容に問題がなければ提案を採用し、修正が必要であれば却下して Gemini に再指示を出せます。提案を受け入れたものの、`database-secrets` モジュールに少し違和感を覚えたので、Gemini に質問してみることにしました。 プロンプト:「Cloud SQLデータベースがIAM認証を使っているのに、なぜ database secret を追加したのですか?」 案の定、実際には不要だったようです。 ![IAM認証の質問に対するGeminiの応答](images/image010.png "IAM認証の質問に対するGeminiの応答") Preview ウィンドウでの表示: ![データベースシークレットを削除するGeminiの提案](images/image011.png "データベースシークレットを削除するGeminiの提案") これは非常に重要な教訓です。AIがどれほど進化しても、エンジニアが評価・判断を下す責任から解放されるわけではありません。最終的に本番環境の責任を負うのは私たち自身です(私たちの仕事がかかっています!)。すべてを自分の目で検証する姿勢を忘れないようにしましょう 🙂 検証という点では、もうひとつ気になったことがありました。Gemini が提案してきた Cloud SQL のインスタンスタイプが `db-perf-optimized-N-8` というかなりハイスペックなものだったのです。小さなプロトタイプには明らかに過剰スペックなので、プロンプトでコストを抑えるよう指示してみます。 > コスト効率を重視した構成にしてください(Make it cost effective) ![リージョンロードバランサーの提案とPostgreSQLからMySQLへの変更案](images/image012.png "リージョンロードバランサーの提案とPostgreSQLからMySQLへの変更案") うーん……これには少し考えさせられました。グローバルではなくリージョンロードバランサーにするという提案は理解できますが、なぜ MySQL のほうが PostgreSQL よりもコスト効率が高いと判断したのかは疑問です。私が気になっていたのはデータベースの種類ではなく、マシンスペック(マシンタイプ)でした。 しかも Gemini のチャット回答はすべてを伝えてくれていませんでした。コードの差分(diff)をよく確認してみると、実際にはマシンタイプ(属性 `tier`)も変更されていたのですが、テキストでの説明が抜けていたのです。 ![Geminiがマシンタイプ(tier)も変更したことを示すTerraform差分](images/image013.png "Geminiがマシンタイプ(tier)も変更したことを示すTerraform差分") これには納得がいかなかったので、理由を尋ねてみました。 > なぜ MySQL のほうが PostgreSQL よりもコスト効率が良いと考えるのですか?(Why do you consider MySQL more cost effective than PostgreSQL?) ![MySQLがPostgreSQLよりもコスト効率が良いと判断した理由を尋ねる](images/image014.png "MySQLがPostgreSQLよりもコスト効率が良いと判断した理由を尋ねる") Gemini からの回答では、MySQL が PostgreSQL よりコスト効率が良い理由として次の3点が挙げられていました。 1. ライセンスの違い(Licensing differences) 2. リソース消費量(Resource consumption) 3. マネージドサービスの価格設定(Managed service pricing) 残念ながら、この回答には同意できません。1点目については、両者ともオープンソースライセンスでありコスト差はありません。2点目については一理あるかもしれませんが、適切なベンチマーク検証が必要です。そして3点目は明確な誤りで、Google Cloud 上の Cloud SQL は PostgreSQL も MySQL も同じ料金体系です。ここは人間の勝ちということで、変更を差し戻しましょう。 > PostgreSQL から MySQL への変更は元に戻し、小さくしたマシンタイプ(tier)だけを維持してください。 最終確認:Cloud SQL を小さめの tier の PostgreSQL で動かす設定に落ち着きましたが、差分をもうひとつ見ると、Cloud Run の「ゼロスケール(scale to zero)」を有効にする重要な変更が加わっていることにも気づきました。 ![Cloud Runのゼロスケール設定(min_instance_count = 0)を示すTerraform差分](images/image015.png "Cloud Runのゼロスケール設定(min_instance_count = 0)を示すTerraform差分") この設定自体は非常に理にかなっていますが、やはりチャットの対話文では言及されていませんでした。これも「AI ツールの言うことは信頼しつつも、必ず検証せよ(Trust, but verify)」という教訓です。本番環境で予期せぬ挙動に見舞われるのは避けたいところです。 ## Terraform ファイルを取得する アーキテクチャの設計が固まったら、UI右上にある[<> Get Code(コードを取得)]ボタンをクリックします。これで、生成された Terraform コード一式が zip ファイルとしてパッケージングされ、ローカルマシンにダウンロードできるようになります。 本記事の執筆時点では、Application Design Center は GitHub や GitLab、Google Source、Bitbucket といったバージョン管理システムとの直接連携をサポートしていません。ツールからコードを取り出す手段は、現時点ではこの zip ダウンロードのみとなります。 組織(Organization)階層がセットアップされた企業アカウントであれば、この設計をそのまま AppHub 経由でデプロイできますが、個人アカウントを利用している場合は、現時点ではコードのダウンロードまでがツールの対応範囲となります。 ## Application Design Center UI に関する補足事項 [Edit app design]ボタンをクリックしたときの挙動は、Google Cloud コンソールの設定環境によって異なります。組織(Organization)に紐付いていない個人アカウントで試している場合、設計図の閲覧と Terraform コードのダウンロードができる「Preview」ウィンドウは開きますが、Application Design Center のフル機能 UI にはアクセスできません。 フル機能を利用するには組織配下のアカウントである必要があります。Application Design Center のセットアップには「ADC 有効化(app design center enabled)」フォルダーという特殊なフォルダー構成が必要となるためです。組織を持たないアカウントではフォルダーを作成できず、組織内であってもクラウド管理者によるフォルダー設定が必要になります。 そのため、組織に属していない個人開発者アカウントでは、現時点では ADC の全機能をフルに活用することはできません。 それでも本記事で紹介したように、Gemini を使ってアプリケーションアーキテクチャをプロトタイピングすることは十分に可能です。ただし、クラウド UI 上で設計の進捗を保存することはできないため、Terraform ファイルをローカルマシンにダウンロードし、手元の Terraform 環境からデプロイするワークフローになります。 ## まとめと今後の展望 新しい AI プロダクトが登場するたびに、トニー・スタークのように音声コマンドだけでソフトウェアを自在に組み上げていく未来を思い描いてワクワクします。完全な自動化にはまだ道半ばですが、Gemini Cloud Assist によって、自然言語でインフラの構成要素を指定・自動設計できるようになったのは大きな前進です。 UI や Gemini の提案精度にはまだ粗削りな部分が残っているものの、新しいアプリケーションを作るたびにゼロから手作業で Terraform コードを書き起こす手間が省けるだけでも、すでに大きな価値を感じています。 このツールは今後数ヶ月で急速に進化していくはずなので、この記事の内容も早晩アップデートされていくでしょう。最新情報をキャッチアップするには公式の [Application Design Center](https://cloud.google.com/application-design-center/docs/overview?utm_campaign=CDR_0x72884f69_awareness_b428663487&utm_medium=external&utm_source=blog) ドキュメントをチェックしてみてください。もちろん本ブログでも、面白い新機能やアップデートがあれば随時取り上げていきます。 プロンプトのアイデアとして、「make it cost effective(コスト効率を高めて)」「make it highly available(高可用性構成にして)」「explain why x instead of y(なぜ Y ではなく X を選んだのか説明して)」「replace x with y(X を Y に置き換えて)」「explain x to me like I’m 5(5歳児にもわかるように説明して)」など、クリエイティブなプロンプトをいろいろ試してみるのがおすすめです。 皆さんはどう思いましたか?こうしたツールにワクワクしますか、それとも怖さを感じますか?面白いプロンプトを見つけたら、ぜひ下のコメント欄で教えてください! --- # 大胆なプロンプト:システム指示とエージェントツールの実践ガイド - **Markdown URL**: https://danicat.dev/ja/posts/system-instructions-agent-tools/index.md - **HTML URL**: https://danicat.dev/ja/posts/system-instructions-agent-tools/ - **Date**: 2025-06-11 - **Categories**: Agent Development - **Tags**: gemini, python, tutorial, vertex-ai - **Series**: Building the Diagnostic Agent - **Summary**: システム指示、複数ターンのチャット履歴、osqueryエージェントツールを組み合わせ、Vertex AIで賢い自律型診断アシスタントを構築する実践ガイド。## はじめに 本ガイドでは、システムプロンプト(System Prompt / システム指示)とエージェントツール(Agent Tools)の設計を深掘りし、さらに洗練された診断エージェント体験を構築していきます。今回は [Vertex AI SDK for Python](https://cloud.google.com/vertex-ai/docs/python-sdk/use-vertex-ai-python-sdk?utm_campaign=CDR_0x72884f69_awareness_b424142426&utm_medium=external&utm_source=blog)、LangChain、Gemini、そして [Osquery](https://www.osquery.io/) を組み合わせて実装を進めます。 正直なところ、[シリーズ第1弾で作成した初期バージョンの診断エージェント](https://danicat.dev/ja/posts/20250531-diagnostic-agent/) はお世辞にもエンタープライズ(Enterprise)レディとは言えませんでした(もちろんエンタープライズ号の掛け言葉です)。内部で実際に何が行われているのか(本当にクエリを実行しているのか?)可視化できず、同じ「セッション」内で話した内容を記憶できず、時折ユーザーの指示を完全に無視することさえありました。 これでは本格的なエージェントに期待するユーザー体験とは程遠い状態です。理想的な診断エージェントは、自分の失敗を記憶し、一貫して指示を実行できる必要があります(例えば、特定のカラムが存在しないことを学習して別のアプローチをとるなど)。さらに、「言っている通りの処理を本当に実行しているのか?」という疑問も残ります。返される情報が正確かつ最新であることを確認するために、実行中のクエリをいつでも確認できるようにすべきです。 これらの課題を念頭に置きながら、さっそく手を動かして緊急用~~医療ホログラム~~診断エージェント(Emergency Diagnostic Agent)を作っていきましょう! ## 舞台を整える 前回は手軽さを優先して Jupyter Notebook 上でコードを書きましたが、今回は標準的な Python プログラムとして作成します。わずかな変更で Jupyter 上でも動作しますが、今回は診断エージェントに本格的なチャットインターフェースを持たせるために通常のスクリプトとして構築します。 Python プロジェクトを始める際は、依存関係を独立して管理するためにクリーンな仮想環境を作成することをおすすめします: ```bash $ mkdir -p ~/projects/diagnostic-agent $ cd ~/projects/diagnostic-agent $ python3 -m venv venv $ source venv/bin/activate $ pip install --upgrade google-cloud-aiplatform[agent_engines,langchain] ``` 前回の記事のエージェントを再現した初期バージョンの `main.py` は以下の通りです: ```py import vertexai from vertexai import agent_engines import osquery from rich.console import Console from rich.markdown import Markdown import os PROJECT_ID = os.environ.get("GCP_PROJECT") LOCATION = os.environ.get("GCP_REGION", "us-central1") STAGING_BUCKET = os.environ.get("STAGING_BUCKET_URI") vertexai.init( project=PROJECT_ID, location=LOCATION, staging_bucket=STAGING_BUCKET ) MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash") instance = osquery.SpawnInstance() def call_osquery(query: str): """Query the operating system using osquery This function is used to send a query to the osquery process to return information about the current machine, operating system and running processes. You can also use this function to query the underlying SQLite database to discover more information about the osquery instance by using system tables like sqlite_master, sqlite_temp_master and virtual tables. Args: query: str A SQL query to one of osquery tables (e.g. "select timestamp from time") Returns: ExtensionResponse: an osquery response with the status of the request and a response to the query if successful. """ if not instance.is_running(): instance.open() # This may raise an exception result = instance.client.query(query) return result def get_system_prompt(): if not instance.is_running(): instance.open() # This may raise an exception response = instance.client.query("select name from sqlite_temp_master").response tables = [ t["name"] for t in response ] return f""" Role: - You are the emergency diagnostic agent. - You are the last resort for the user to diagnose their computer problems. - Answer the user queries to the best of your capabilities. Tools: - you can call osquery using the call_osquery function. Context: - Only use tables from this list: {tables} - You can discover schemas using: PRAGMA table_info(table) Task: - Create a plan for which tables to query to fullfill the user request - Confirm the plan with the user before executing - If a query fails due a wrong column name, run schema discovery and try again - Query the required table(s) - Report the findings in a human readable way (table or list format) """ def main(): agent = agent_engines.LangchainAgent( model = MODEL, system_instruction=get_system_prompt(), tools=[ call_osquery, ], ) console = Console() print("Welcome to the Emergency Diagnostic Agent\n") print("What is the nature of your diagnostic emergency?") while True: try: query = input(">> ") except EOFError: query = "exit" if query == "exit" or query == "quit": break if query.strip() == "": continue response = agent.query(input=query) rendered_markdown = Markdown(response["output"]) console.print(rendered_markdown) print("Goodbye!") if __name__ == "__main__": main() ``` エージェントは `python main.py` で実行できます: ``` $ python main.py Welcome to the Emergency Diagnostic Agent What is the nature of your diagnostic emergency? >> ``` 元のコードと比較して、2つの細かな変更点があります。1点目は、ユーザーが「exit」または「quit」と入力するまでエージェントを動かし続けるメインループを追加した点です。これでチャットインターフェースが完成します。 2点目は、エージェントの一貫性を高めるためにシステムプロンプトを調整した点です。ここではエージェントの名前を「Emergency Diagnostic Agent(緊急診断エージェント)」と名付けました。この名前は素晴らしい[『スタートレック』のイースターエッグ](https://en.wikipedia.org/wiki/The_Doctor_(Star_Trek:_Voyager))(緊急用医療ホログラム)として機能するだけでなく、より重要な点として、緊急性のあるトーンを設定することで、[近年の研究論文](https://arxiv.org/pdf/2307.11760)で示されているようにモデルがリクエストにより忠実に従うよう促す効果が期待できます(こちらの[最近のインタビュー](https://www.reddit.com/r/singularity/comments/1kv7hm2/sergey_brin_we_dont_circulate_this_too_much_in/)も併せてご覧ください)。 もちろん哀れな緊急診断エージェントを脅迫するつもりはありませんし、本稿の作成にあたって傷つけられたエージェントは一切存在しないことを保証します。とはいえ、「緊急用(Emergency)」と銘打つことで、モデルが全力を尽くしてこちらの要求に応えようとする雰囲気を作り出すことができます。前バージョンのシステムプロンプトでは、エージェントが自分には能力がないと「思い込んだり」、どのテーブルを照会すればよいかわからずタスクを拒否するケースがありました。 もちろん、単に緊急エージェントと呼ぶだけでは期待通りの振る舞いを保証するのに不十分ですので、以下で見るようにモデルの行動を導くための指示をいくつか追加しています。 ## システム指示(System Instructions) システムプロンプト(システム指示)とは、会話全体を通じて LLM の振る舞いをガイドする一連の指示のことです。通常のチャット対話よりも高い優先度を持つ特別な指示であり、モデルに送信するすべてのプロンプトに常にシステム指示が添えて繰り返されているようなイメージです。 システムプロンプトがどうあるべきかについて、文献上の確固たる統一見解はまだありませんが、日々の実践からいくつかの実績あるパターンが生まれてきています。例えば、ほぼ共通の認識となっているのが、プロンプトの冒頭でエージェントに「役割(Role)」を割り当て、自身の目的を認識させて首尾一貫した回答を生成させるという手法です。 今回のエージェントでは、システムプロンプトに **Role(役割)**、**Tools(ツール)**、**Context(コンテキスト)**、**Task(タスク)** の4つのセクションを含める構成を採用しました。この構造はテスト段階で非常にうまく機能しましたが、これだけに固執する必要はありません。プロンプトを色々試して、より良い結果が得られるか実験してみてください。LLM で優れた成果を上げるには、実験を重ねることが何より重要です。 それでは、プロンプトの各セクションを順に見ていきましょう。 ### システムプロンプト:Role(役割) 役割とは、エージェントが存在する理由そのものです。「あなたはソフトウェアエンジニアです」や「あなたは診断エージェントです」といったシンプルなものから、詳細な説明、行動規範、制約事項などを含む手の込んだものまで様々です。 あらゆる種類のデータで訓練された大規模言語モデルにとって、役割を設定することは、質問に答えるためにアクセスすべき専門知識のドメインを定めるのに役立ちます。言い換えれば、質問に意味的(セマンティック)な文脈を与えるのです。例えば「クッキー(cookies)とは何ですか?」という質問を想像してみてください。食べるクッキーのことでしょうか、それともブラウザの Cookie のことでしょうか? エージェントの役割が未定義であればこの質問は完全に曖昧ですが、役割を「あなたはソフトウェアエンジニアです」といった技術的なものに設定した瞬間に、その曖昧さは解消されます。 今回のエージェントでは、役割を以下のように記述しました: ``` Role: - You are the emergency diagnostic agent. - You are the last resort for the user to diagnose their computer problems. - Answer the user queries to the best of your capabilities. ``` 単刀直入な定義(「あなたは緊急診断エージェントです」)に加え、モデルの振る舞いのトーンを決定づけ、こちらの要求を「真剣に」受け止めるよう促すための詳細な説明を追加しました。前述の通り、以前のバージョンのエージェントには要求を拒否してしまう悪い癖があったためです。 ### システムプロンプト:Tools(ツール) Tools は、コアモデルの枠を超えて外部システムと対話する能力をエージェントに説明するセクションです。ツールにはいくつかの種類がありますが、最も一般的な提供方法は Function Calling(関数呼び出し)です。 エージェントはツールを使って情報を取得し、タスクを実行し、データを操作できます。Vertex AI SDK for Python は、ユーザー定義関数だけでなく、Google 検索やコード実行などの組み込みツールもサポートしています。さらに、Model Context Protocol(MCP)インターフェースを介してコミュニティが管理する拡張機能を利用することも可能です。 今回構築するエージェントには、_osquery_ を呼び出せることを伝えます: ``` Tools: - you can call osquery using the call_osquery function. ``` ### システムプロンプト:Context(コンテキスト) 次は Context です。エージェントが動作する環境についての情報を伝えます。私はこのセクションを使って、以前のエージェントが陥りがちだった好ましくない挙動を明示的に指摘し、修正するようにしています。例えば開発のごく初期段階で、エージェントが存在するテーブルを「推測」して闇雲にクエリを送信し、エラー率が跳ね上がることがありました。コンテキストに利用可能なテーブル一覧を追加したことで、その問題は大幅に軽減されました。 同様に、テーブル内のカラム名を事前に確認せず推測しようとする傾向もありました。このケースでは、常に `SELECT *` を使うよう指示したくなる誘惑に駆られましたが、それは悪手(必要以上のデータを取得してしまう)なので思いとどまり、代わりに `PRAGMA` 文を使ってスキーマを探索する方法を「教え」ました。 これにより、エージェントがカラム名を推測して失敗することはあっても、人間の手を煩わせることなく自律的に軌道修正できるようになります。 改善されたシステムプロンプトの Context セクションは以下の通りです: ``` Context: - Only use tables from this list: {tables} - You can discover schemas using: PRAGMA table_info(table) ``` ここで `{tables}` は、モデルを起動する前に Osquery から取得したすべてのテーブル一覧を格納した変数です。 ### システムプロンプト:Task(タスク) 最後は Task です。このセクションでは、エージェントがユーザーのリクエストをどのように解釈し、実行すべきかを記述します。通常は、目の前のタスクを達成するために必要なステップを整理するために使用されます。 今回のケースでは、大まかな実行計画を提示しつつ、いくつかの条件付きディレクティブを追加しています: ``` Task: - Create a plan for which tables to query to fullfill the user request - Confirm the plan with the user before executing - If a query fails due a wrong column name, run schema discovery and try again - Query the required table(s) - Report the findings in a human readable way (table or list format) ``` 「実行前にユーザーに計画を確認する(Confirm the plan with the user before executing)」というステップは、エージェントがプロセスについてどう考えているかが把握できて興味深い反面、しばらくやり取りを続けていると少し煩わしく感じるかもしれません。実行計画はプロンプトで尋ねればいつでも教えてもらえるため、このステップを含めるかどうかは完全に任意です。 当初はこのステップをエージェントのデバッグ用として考えていましたが、次のセクションでは別の方法でデバッグを実現していきます。 これら4つのセクションを組み合わせることで、完全なシステムプロンプトが出来上がります。この改善されたプロンプトにより、本稿の執筆に向けたテスト期間中、より一貫した結果が得られるようになりました。また「人間にとって読みやすい」構造になっているため、新しいルールを追加する際のメンテナンスも容易です。 完成したシステムプロンプトの全体像は以下の通りです: ``` Role: - You are the emergency diagnostic agent. - You are the last resort for the user to diagnose their computer problems. - Answer the user queries to the best of your capabilities. Tools: - you can call osquery using the call_osquery function. Context: - Only use tables from this list: {tables} - You can discover schemas using: PRAGMA table_info(table) Task: - Create a plan for which tables to query to fullfill the user request - Confirm the plan with the user before executing - If a query fails due a wrong column name, run schema discovery and try again - Query the required table(s) - Report the findings in a human readable way (table or list format) ``` 余談ですが、プロンプトにはまだまだ改善の余地があると考えています。現在私が特に関心を持っている分野の1つが「自己改善型システムプロンプト(Self-improving System Prompt)」の実現です。セッション終了時に、モデル自身が得た学びを要約させて次回イテレーション用の新しいシステムプロンプトを生成させる、といったアプローチが考えられます。プロンプトをデータベースに保存し、次回のセッションで読み込む形です。もちろん、これにはシステムプロンプトの劣化(Degradation)や、さらに深刻なプロンプトインジェクション攻撃への懸念が生じるため、一筋縄ではいきません。それでも非常に面白い試みであり、近いうちに記事にするかもしれません。 ## デバッグモードを有効にする 初期設計におけるもう1つの懸念事項は、エージェントが水面下で何を行っているのかについての可観測性(オブザーバビリティ)の欠如でした。ここには2つのアプローチがあり、片方はもう一方よりも少し厄介です。1つ目は、LLM の「思考(Thoughts)」を覗き見してその中からツール呼び出しを探し出す方法(非常に面倒)、2つ目は、関数自体にデバッグ機能を追加して実行時に必要な情報を出力させる方法(大抵の場合、最もシンプルな解決策こそが正解です)です。 白状すると、私はアプローチ2に気づくまで、アプローチ1に不健全なほどの時間を費やしてしまいました。どうしても LLM の推論プロセスを追いたい場合は、[return_intermediate_steps](https://api.python.langchain.com/en/latest/agents/langchain.agents.agent.AgentExecutor.html#langchain.agents.agent.AgentExecutor.return_intermediate_steps) という設定を利用できます。学習の観点からは非常に興味深いのですが、出力フォーマットの解析に数時間費やした結果(ヒント:[実際には JSON ではありません](https://github.com/langchain-ai/langchain/issues/10099))、わざわざパースするほどの価値はないと判断しました。 では、シンプルな戦略はどのように機能するのでしょうか? デバッグフラグと、そのフラグをオン・オフするためのツールを追加します。この驚くほどシンプルな工夫によって、まったく新しい可能性の扉が開かれます。つまり、エージェントに自身の振る舞いを変更する権限を与えるのです! デバッグモードの実装は、グローバル変数とそれを設定する関数で構成されます: ```py debug = False def set_debug_mode(debug_mode: bool): """Toggle debug mode. Call this function to enable or disable debug mode. Args: debug_mode (bool): True to enable debug mode, False to disable it. Returns: None """ global debug debug = debug_mode ``` システムプロンプトにもこのツールについて記載します: ``` ... Tools: - you can call osquery using the call_osquery function. - you can enable or disable the debug mode using the set_debug_mode function. Context: ... ``` そして、エージェントのインスタンス化時にツールのリストに関数を追加します: ```py agent = agent_engines.LangchainAgent( model = model, system_instruction=get_system_prompt(), tools=[ call_osquery, set_debug_mode, ], ) ``` 最後に、新しい `debug` フラグを使用するように `call_osquery` を修正します: ```py def call_osquery(query: str): """Query the operating system using osquery This function is used to send a query to the osquery process to return information about the current machine, operating system and running processes. You can also use this function to query the underlying SQLite database to discover more information about the osquery instance by using system tables like sqlite_master, sqlite_temp_master and virtual tables. Args: query: str A SQL query to one of osquery tables (e.g. "select timestamp from time") Returns: ExtensionResponse: an osquery response with the status of the request and a response to the query if successful. """ if not instance.is_running(): instance.open() if debug: print("Executing query: ", query) result = instance.client.query(query) if debug: print("Query result: ", { "status": result.status.message if result.status else None, "response": result.response if result.response else None }) return result ``` これらの変更をすべて反映した上で、新しく実装したデバッグフラグを使ってエージェントがどのように _osquery_ を呼び出すか確認してみましょう: ``` $ python main.py Welcome to the Emergency Diagnostic Agent What is the nature of your diagnostic emergency? >> run a level 1 diagnostic procedure in debug mode Executing query: SELECT * FROM system_info Query result: {'status': 'OK', 'response': [{...}]} Executing query: SELECT pid, name, user, cpu_percent FROM processes ORDER BY cpu_percent DESC LIMIT 10 Query result: {'status': 'no such column: user', 'response': None} Executing query: SELECT pid, name, user, resident_size FROM processes ORDER BY resident_size DESC LIMIT 10 Query result: {'status': 'no such column: user', 'response': None} Executing query: PRAGMA table_info(processes) Query result: {'status': 'OK', 'response': [{'cid': '0', 'dflt_value': '', 'name': 'pid', 'notnull': '1', 'pk': '1', 'type': 'BIGINT'}, ...]} (...) System Information: • Hostname: workstation.localdomain • CPU Type: arm64e • Physical Memory: 51539607552 bytes Top 5 Processes by CPU Usage: PID Name CPU Usage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1127 mediaanalysisd 95627517 43062 mediaanalysisd-access 66441942 54099 Google Chrome 3005046 54115 Google Chrome Helper (GPU) 2092500 81270 Electron 1688335 Top 5 Processes by Memory Usage (Resident Size): PID Name Resident Size (Bytes) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 43062 mediaanalysisd-access 3933536256 54099 Google Chrome 1313669120 59194 Code Helper (Plugin) 1109508096 59025 Code Helper (Renderer) 915456000 19681 Google Chrome Helper (Renderer) 736329728 >> ``` 入力したコマンドが「run a level 1 diagnostic procedure **in debug mode**(デバッグモードでレベル1診断手順を実行せよ)」である点に注目してください。これはエージェントの興味深い能力である「マルチツール呼び出し(Multi-tool invocation)」を示しています。エージェントは必要と判断すれば、同じツールを複数回呼び出すだけでなく、異なるツールを同時に呼び出すこともできます。そのため、レポートを要求する前にあらかじめデバッグモードを有効にしておく必要はなく、エージェントが一度の処理ですべて実行してくれました。 また、エージェントが最初に `user` カラムを要求して失敗した後、`PRAGMA` 文を使って正しいスキーマを探索し、クエリを再試行して成功させている点にもご注目ください。これは、改善されたシステムプロンプトによってエージェントがエラーから自律的に復旧できる能力を完璧に証明しています。 ## チャット履歴の保持 本日の最後の課題は、調査の一貫した流れに沿って明確化のための質問をしたりシステムをさらに掘り下げて調査できるよう、エージェントが会話内容を確実に記憶できるようにすることです。 [前回の記事](https://danicat.dev/ja/posts/20250605-vertex-ai-sdk-python/) では、LLM がステートレス(状態を持たない)であること、そして「ターン(Turns)」を使って会話の現在の状態をモデルに「思い出させ」続ける必要があることを説明しました。幸いなことに、LangChain を使えばこれを手作業で行う必要はなく、[チャット履歴(Chat History)](https://python.langchain.com/api_reference/core/chat_history.html) という機能を活用できます。 チャット履歴の素晴らしいところは、[BaseChatMessageHistory](https://python.langchain.com/api_reference/core/chat_history/langchain_core.chat_history.BaseChatMessageHistory.html#langchain_core.chat_history.BaseChatMessageHistory) を実装しているものであれば何でも利用できる点です。自作のものを含め、あらゆる種類のデータストアを利用できます。例えば Vertex AI の公式ドキュメントには、[Firebase、Bigtable、Spanner](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/develop/langchain#chat-history?utm_campaign=CDR_0x72884f69_awareness_b424142426&utm_medium=external&utm_source=blog) を使用する例が掲載されています。 現時点では本格的なデータベースは必要ないため、名前の通りすべてをメモリ上に保存する `InMemoryChatMessageHistory` を採用します。 以下は典型的な実装例であり、ルックアップ用の `chats_by_session_id` 辞書を使用して複数のセッションをサポートしています(コードは [LangChain 公式ドキュメント](https://python.langchain.com/docs/versions/migrating_memory/chat_history/#chatmessagehistory) より引用): ```py chats_by_session_id = {} def get_chat_history(session_id: str) -> InMemoryChatMessageHistory: chat_history = chats_by_session_id.get(session_id) if chat_history is None: chat_history = InMemoryChatMessageHistory() chats_by_session_id[session_id] = chat_history return chat_history ``` そして、チャット履歴を有効にしてエージェントをインスタンス化する新しい `main` 関数は以下のようになります: ```py import uuid def main(): session_id = uuid.uuid4() agent = agent_engines.LangchainAgent( model = model, system_instruction=get_system_prompt(), tools=[ call_osquery, set_debug_mode, ], chat_history=get_chat_history, ) ``` 私と同じ過ちを犯さないための注意点として、`chat_history` 引数は `Callable` 型を想定しているため、ここで関数を実行(invoke)するのではなく、関数そのものを渡す必要があります。LangChain はここでファクトリパターンを採用しており、必要に応じて提供された関数(`get_chat_history`)を `session_id` 付きで呼び出して、適切な履歴オブジェクトを取得または作成します。この設計により、エージェントは複数の独立した会話を並行して管理できるようになっています。 関数のシグネチャには1つまたは2つの引数を含めることができます。引数が1つの場合は `session_id` とみなされ、2つの場合は `user_id` と `conversation_id` として解釈されます。詳細については [RunnableWithMessageHistory](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.history.RunnableWithMessageHistory.html) のドキュメントを参照してください。 最後のピースは、モデルの実行処理に `session_id` を渡すことです。これは以下のコードのように `config` 引数を介して行います: ```py # (...) while True: try: query = input(">> ") except EOFError: query = "exit" if query == "exit" or query == "quit": break if query.strip() == "": continue response = agent.query(input=query, config={"configurable": {"session_id": session_id}}) rendered_markdown = Markdown(response["output"]) console.print(rendered_markdown) ``` これで、セッションがアクティブである限り、会話内容はメモリ上に保存されるため、エージェントの「短期記憶」にある情報について質問できるようになります。これにより基本的な対話の多くがより自然に感じられるようになりますが、同時にさらに大きな問題への先例を作ることにもなります。セッション情報を保存できるようになったことで、イテレーションを重ねるごとに履歴は肥大化していきます。さらにクエリから自動生成されたデータを扱っていると、セッションコンテキストは急速に増大し、コンピューターのメモリ上限に達するはるか前に、モデルのトークン制限にぶつかってしまいます。 Gemini のようなモデルは[ロングコンテキストウィンドウ](https://ai.google.dev/gemini-api/docs/long-context)で知られていますが、コンテキストをデータで埋め尽くしてしまえば、100万トークンであってもあっという間に使い果たされてしまいます。また、コンテキストが長くなると情報の検索が次第に困難になるという問題(いわゆる[干し草の中の針(Needle in a Haystack)](https://cloud.google.com/blog/products/ai-machine-learning/the-needle-in-the-haystack-test-and-how-gemini-pro-solves-it?utm_campaign=CDR_0x72884f69_awareness_b424142426&utm_medium=external&utm_source=blog) 問題)が発生するモデルもあります。 増大するコンテキスト問題に対処する手法としては、圧縮(Compression)や要約(Summarisation)などがありますが、本稿のコンテキストを短く保つためにも(お分かりですね?)、それらは次回の記事に取っておくことにしましょう。 本稿で行ったすべての修正を含めた `main.py` の最終バージョンは以下のようになります: ```py import vertexai from vertexai import agent_engines import osquery from rich.console import Console from rich.markdown import Markdown from langchain_core.chat_history import InMemoryChatMessageHistory import os import uuid PROJECT_ID = os.environ.get("GCP_PROJECT") LOCATION = os.environ.get("GCP_REGION", "us-central1") STAGING_BUCKET = os.environ.get("STAGING_BUCKET_URI") vertexai.init( project=PROJECT_ID, location=LOCATION, staging_bucket=STAGING_BUCKET ) MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-pro-preview-05-06") instance = osquery.SpawnInstance() debug = False def set_debug_mode(debug_mode: bool): """Toggle debug mode. Call this function to enable or disable debug mode. Args: debug_mode (bool): True to enable debug mode, False to disable it. Returns: None """ global debug debug = debug_mode def call_osquery(query: str): """Query the operating system using osquery This function is used to send a query to the osquery process to return information about the current machine, operating system and running processes. You can also use this function to query the underlying SQLite database to discover more information about the osquery instance by using system tables like sqlite_master, sqlite_temp_master and virtual tables. Args: query: str A SQL query to one of osquery tables (e.g. "select timestamp from time") Returns: ExtensionResponse: an osquery response with the status of the request and a response to the query if successful. """ if not instance.is_running(): instance.open() # This may raise an exception if debug: print("Executing query: ", query) result = instance.client.query(query) if debug: print("Query result: ", { "status": result.status.message if result.status else None, "response": result.response if result.response else None }) return result def get_system_prompt(): if not instance.is_running(): instance.open() # This may raise an exception response = instance.client.query("select name from sqlite_temp_master").response tables = [ t["name"] for t in response ] return f""" Role: - You are the emergency diagnostic agent. - You are the last resort for the user to diagnose their computer problems. - Answer the user queries to the best of your capabilities. Tools: - you can call osquery using the call_osquery function. - you can use the set_debug_mode function to enable or disable debug mode. Context: - Only use tables from this list: {tables} - You can discover schemas using: PRAGMA table_info(table) Task: - Create a plan for which tables to query to fullfill the user request - Confirm the plan with the user before executing - If a query fails due a wrong column name, run schema discovery and try again - Query the required table(s) - Report the findings in a human readable way (table or list format) """ chats_by_session_id = {} def get_chat_history(session_id: str) -> InMemoryChatMessageHistory: chat_history = chats_by_session_id.get(session_id) if chat_history is None: chat_history = InMemoryChatMessageHistory() chats_by_session_id[session_id] = chat_history return chat_history def main(): session_id = uuid.uuid4() agent = agent_engines.LangchainAgent( model = MODEL, system_instruction=get_system_prompt(), tools=[ call_osquery, set_debug_mode ], chat_history=get_chat_history, ) console = Console() print("Welcome to the Emergency Diagnostic Agent\n") print("What is the nature of your diagnostic emergency?") while True: try: query = input(">> ") except EOFError: query = "exit" if query == "exit" or query == "quit": break if query.strip() == "": continue response = agent.query(input=query, config={"configurable": {"session_id": session_id}}) rendered_markdown = Markdown(response["output"]) console.print(rendered_markdown) print("Goodbye!") if __name__ == "__main__": main() ``` ## まとめ この記事では、エージェントから一貫した応答を引き出すためのシステムプロンプト微調整の重要性を学びました。また、マルチツール呼び出しが実際にどのように機能するか、ツールを使用してフィーチャーフラグを切り替えエージェントの挙動を動的に変更する方法も実践しました。さらに、インメモリのチャット履歴を使用してセッション状態を管理する方法についても解説しました。 本シリーズの次回以降の記事では、本物のデータベースを使用してセッション間の永続化を実現する方法を見ていき、トークンの概念を再確認しながらコンテキスト圧縮テクニックについて議論します。 ## 付録:試してみたい面白いプロンプト エージェントがより堅牢になったところで、新機能を実際にテストするための実践ガイドとしてこのセクションを活用してください。質問間でコンテキストがどのように記憶されているか、またエージェント自身に行動の理由を説明させることができるかを確認してみてください。 ``` >> run a level 1 diagnostic procedure >> run a level 2 diagnostic procedure >> explain the previous procedure step by step >> find any orphan processes >> show me the top resource consuming processes >> write a system prompt to transfer your current knowledge to another agent >> search the system for malware >> is this computer connected to the internet? >> why is my computer slow? >> take a snapshot of the current performance metrics >> compare the current perfomance metrics with the previous snapshot >> give me a step by step process to fix the issues you found >> how many osqueryd processes are in memory? >> give me a script to kill all osqueryd processes >> who am i? ``` 他にも面白いプロンプトを見つけたら、ぜひ下のコメント欄であなたの体験を共有してください。 シリーズの次回作 [Agent Development Kit (ADK) で診断エージェントを作成する](https://danicat.dev/ja/posts/20251020-diagnostic-agent-with-adk/) では、素の SDK によるボイラープレートから Google の Agent Development Kit (ADK) へのリファクタリングを行い、動的なテーブルスキーマ探索のための Vertex AI RAG を統合します。それでは、また次回お会いしましょう! --- # Vertex AI SDK for Python を深く掘り下げる - **Markdown URL**: https://danicat.dev/ja/posts/vertex-ai-sdk-python/index.md - **HTML URL**: https://danicat.dev/ja/posts/vertex-ai-sdk-python/ - **Date**: 2025-06-05 - **Categories**: Agent Development - **Tags**: gemini, python, tutorial, vertex-ai - **Series**: Building the Diagnostic Agent - **Summary**: Vertex AI SDK for Python におけるクライアントとGemini API間の通信モデルを解説。システム指示、Function Calling宣言、チャットセッションの構造を詳解。## はじめに この記事では、[Vertex AI SDK for Python](https://cloud.google.com/vertex-ai/docs/python-sdk/use-vertex-ai-python-sdk?utm_campaign=CDR_0x72884f69_awareness_b422727650&utm_medium=external&utm_source=blog) を使用したクライアントコードと Gemini API 間の通信モデルについて詳しく探ります。メッセージがどのように構造化されているのか、モデルがどのように質問のコンテキストを理解するのか、そして Function Calling(関数呼び出し)によってモデルの機能をどのように拡張するのか、といった概念を解説します。今回は Gemini を中心に取り上げますが、ここで紹介する概念は Gemma やその他の LLM にも同様に適用できます。 [本シリーズの第1弾「AIエージェントを使って自分のPCを「エンタープライズ号」にした話」](https://danicat.dev/ja/posts/20250531-diagnostic-agent/) では、ローカルマシンの診断に関する質問に応答する、シンプルでありながら驚くほど強力な AI エージェントの作成方法を解説しました。ごくわずかなコード行数(そして決して少なくはないコメント行)で、「マシンの CPU 使用率はどれくらいか」「マルウェアの兆候がないか確認して」といった問い合わせに応答するエージェントを実現できました。 これはもちろん、処理を大幅に簡略化してくれる Python SDK の恩恵によるものでした。例えば、関数をいつ呼び出すべきかをエージェントに自動判断させる [Automatic Function Calling(自動関数呼び出し)](https://ai.google.dev/gemini-api/docs/function-calling?example=weather#automatic_function_calling_python_only) 機能を活用しました。この機能のおかげで、通常の Python 関数を定義するだけで、SDK が関数のシグネチャと説明(docstring)を動的に自動解釈してくれたのです。ただ、この機能は残念ながら Python SDK 限定であるため、他のプログラミング言語で開発する場合はもう少し手順を踏む必要があります。 そこで今回の記事では少しアプローチを変え、Gemini API 自体の仕組みを解説します。これにより、Python だけでなく、利用可能な各種 SDK(JavaScript、Go、Java など)を使用する際にもスムーズに対応できるようになります。前回の記事と比較しやすいようコード例には引き続き Python を使用しますが、ここで取り上げる概念はすべての言語に共通するものです。 本記事で取り上げる主なトピックは以下の2つです: * クライアントとモデル間で対話(会話ターン)がどのように行われるか * 手動で Function Calling(関数呼び出し)を実装する方法 なお、Python 開発者にとっても得るものは決して少なくありません。対話の内部フローを理解しておくことは、Live API などのより高度な SDK 機能を活用したり、LLM 全般を扱ったりする上で非常に重要な基礎となるからです。 ## API の仕組みを理解する エージェントは一般的に、クライアント・サーバー型アプリケーションと同様の仕組みで動作します。リクエストの準備と送信を担うクライアントコンポーネントと、モデルランタイムをホストしてリクエストを処理するサーバープロセスが存在します。 Vertex AI には主に 2 つの API グループがあります。クライアントがリクエストを送信してレスポンスを待つ標準的なリクエスト/レスポンス型のコンテンツ生成を行う REST API と、WebSocket を使用してリアルタイム情報を処理する新しい [Live API](https://cloud.google.com/vertex-ai/generative-ai/docs/live-api?utm_campaign=CDR_0x72884f69_awareness_b422727650&utm_medium=external&utm_source=blog) です。Live API を適切に扱うにはもう少し準備が必要になるため、本記事ではまず REST API に焦点を当てます。 コンテンツ生成では通常、テキスト、画像、音声、動画のいずれかのモダリティを扱います。最新のモデルの多くはマルチモーダルであり、入力や出力において複数のモダリティを同時に処理できます。まずはシンプルに、テキストから始めましょう。 単発のリクエストを行う標準的なプロンプトアプリケーションは以下のようになります: ```python from google import genai client = genai.Client( vertexai=True, project="daniela-genai-sandbox", location="us-central1" ) response = client.models.generate_content( model="gemini-2.0-flash", contents="How are you today?" ) print(response.text) ``` 出力: ``` I am doing well, thank you for asking! As a large language model, I don't experience emotions like humans do, but I am functioning optimally and ready to assist you. How can I help you today? ``` 最初に行うべきことは、Vertex AI モード(`vertexai=True`)または Gemini API キーを使用してクライアントを初期化することです。今回の例では Vertex AI モードを使用しています。 クライアントが初期化されたら、`client.models.generate_content` メソッドを使ってプロンプトを送信できます。呼び出すモデル(ここでは `gemini-2.0-flash`)と、`contents` 引数にプロンプト(例: `"How are you today?"`)を指定します。 このコードを見ていると、Python が提供してくれる数多くの抽象化のおかげで、内部で何が起きているのかイメージしづらいかもしれません。ここで最も重要なポイントは、**コンテンツは単なる文字列ではない**ということです。 `contents` は実際にはコンテンツ構造体(`Content`)のリストであり、この構造体は **role(役割)** と 1 つ以上の **parts(パーツ)** で構成されています。この構造体の基底型は types ライブラリで定義されており、以下のようになっています: ```python from google.genai import types contents = [types.Content( role = "user", parts = [ types.Part.from_text("How are you today?") ] )] ``` つまり、私たちが `contents="How are you today?"` と記述するたびに、Python SDK が自動的に文字列から「文字列パートを持つコンテンツ」への変換を行ってくれているのです。 もう 1 つ注意すべき重要な点は、`generate_content` を呼び出すたびに、モデルは事前の記憶を持たずにゼロから推論を開始するということです。つまり、過去のメッセージのコンテキストを次のプロンプトに含めるのは**私たち(クライアント側)の責務**となります。モデルに対して「今日は何日ですか?(what day is today?)」と 2 回連続で質問する簡単なテストを行ってみましょう: ```python response = client.models.generate_content( model="gemini-2.0-flash", contents="what day is today?" ) print(response.text) response = client.models.generate_content( model="gemini-2.0-flash", contents="what day is today?" ) print(response.text) ``` 出力: ``` $ python3 main.py Today is Sunday, November 5th, 2023. Today is Saturday, November 2nd, 2024. ``` 上記の出力には 2 つの問題があります:1) モデルは実際の日付を知る術がないためハルシネーション(幻覚)を起こしている点、2) 同じ質問に対して 2 回異なる回答を返している点です。1) は日時の取得関数や Google 検索などのツールでグラウンディングすることで解決できますが、ここでは 2) に注目したいと思います。これはモデルが直前に自分が答えた内容すら覚えていないことを明確に示しており、モデルに会話の最新状態を伝え続けるのは**私たち**の責任であるという先ほどのポイントを実証しています。 コードに少し変更を加えてみましょう: ```python response = client.models.generate_content( model="gemini-2.0-flash", contents="what day is today?" ) print(response.text) # contents 配列の各要素は通常「ターン(turn)」と呼ばれます contents = [ { "role": "user", "parts": [{ "text": "what day is today?" }] }, { "role": "model", "parts": [{ "text": response.text }] }, { "role": "user", "parts": [{ "text": "what day is today?" }] }, ] response = client.models.generate_content( model="gemini-2.0-flash", contents=contents ) print(response.text) ``` 出力: ``` $ python3 main.py Today is Wednesday, November 15, 2023. Today is Wednesday, November 15, 2023. ``` モデルへの 2 回目の呼び出しでは、`contents` 属性に対話のコンテキスト全体を含めている点に注目してください。また、各パートの role が “user” から “model”、そして再び “user” へと変化していることにも注目してください(role に指定可能な値は “user” と “model” のみです)。モデルはこれによって会話のどの時点にいるのか、すなわち「ターン(turn)」を理解します。もし仮に、質問を繰り返す最後のパートを省略した場合、モデルは会話がすでに完了していると判断し、最後のターンが “user” ではなく “model” であるため新たなレスポンスを生成しません。 上記の `contents` 変数は「辞書(dictionary)」形式で記述されていますが、SDK には `types.UserContent`(role フィールドを自動的に “user” に設定)や `types.Part.from_text`(プレーンな文字列をパートに変換)など、便利なヘルパーメソッドも多数用意されています。 他の種類の入力や出力を扱う場合は、Function Calling(関数呼び出し)やバイナリデータなど、他の種類のパーツを使用できます。モデルがマルチモーダルの場合、同じメッセージ内に異なるコンテンツタイプのパーツを混在させることが可能です。 バイナリデータはインラインで渡すことも、URI から取得することもできます。`mime_type` フィールドを指定することで、異なるデータ形式を区別します。例えば、画像パーツは以下のように URI から取得できます: ```python from google.genai import types contents = types.Part.from_uri( file_uri='gs://generativeai-downloads/images/scones.jpg', mime_type='image/jpeg', ) ``` またはインラインで渡すことも可能です: ```python contents = types.Part.from_bytes( data=my_cat_picture, # バイナリデータ mime_type='image/jpeg', ) ``` 要約すると、会話のターンごとに、直前のモデルのレスポンスと新しいユーザーの質問の両方に対して、新しいコンテンツ行を追加していくことになります。 嬉しいことに、チャットボット体験は非常に重要なユースケースであるため、Vertex AI SDK にはこのフローの標準実装があらかじめ用意されています。`chat` 機能を使用すれば、上記の挙動をごくわずかなコード行数で再現できます: ```python chat = client.chats.create(model='gemini-2.0-flash') response = chat.send_message('what day is today?') print(response.text) response = chat.send_message('what day is today?') print(response.text) ``` 出力: ``` $ python3 main.py Today is Saturday, October 14th, 2023. Today is Saturday, October 14th, 2023. ``` 今回は、チャットインターフェースが会話履歴を自動的に管理してくれたため、モデルは日付を一貫して記憶できました。 ## 手動での Function Calling(関数呼び出し) クライアントメッセージの構築とコンテキスト管理の仕組みがわかったところで、次は API が Function Calling(関数呼び出し)をどのように処理しているかを見ていきましょう。基本的な流れとしては、利用可能な関数が存在することをモデルに伝え、モデルからの関数呼び出しリクエストを処理し、その実行結果をモデルに返却します。Function Calling はエージェントが外部システムや現実世界と対話し、単なるテキスト生成を超えてデータ取得や特定の処理のトリガーといったアクションを実行できるようにするために不可欠な機能です。 関数宣言(Function Declaration)は、モデルに対して何ができるかを伝えるものです。関数の名前、説明、そして引数をモデルに定義します。例えば、以下は `get_random_number` 関数の宣言です: ```python get_random_number_decl = { "name": "get_random_number", "description": "Returns a random number", } ``` モデルはこの宣言を把握することで、どの関数を呼び出すべきかを判断します。関数宣言には `name`、`description`、`parameters` の 3 つのフィールドがあります(今回の関数は引数を取らないため `parameters` フィールドは省略しています)。モデルは関数の説明と引数の説明を参照して、各関数をいつどのように呼び出すべきかを判断します。 前回の記事では、関数宣言を明示的に記述するのを省き、関数の docstring に基づいて SDK に自動推論させていました。今回は内部フローをより深く理解するために、関数を明示的に宣言するアプローチをとります。 関数とその宣言コードは以下のようになります: ```python def get_random_number(): return 4 # 公平なサイコロの出目によって決定 # 完全にランダムであることが保証されている (https://xkcd.com/221/) # 宣言によって関数に関する必要な情報をモデルに伝える get_random_number_decl = { "name": "get_random_number", "description": "Returns a random number", } ``` 関数宣言のその他の例は、[Vertex AI の Function Calling ドキュメント](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#schema-examples?utm_campaign=CDR_0x72884f69_awareness_b422727650&utm_medium=external&utm_source=blog) で確認できます。 次に、モデルがこの関数を利用できることを伝える必要があります。これはモデル設定(`GenerateContentConfig`)を通じて行い、関数をツール(Tool)として追加します。 ```python tools = types.Tool(function_declarations=[get_random_number_decl]) config = types.GenerateContentConfig(tools=[tools]) # 最初のプロンプト contents = [types.Part.from_text(text="what is my lucky number today?")] response = client.models.generate_content( model="gemini-2.0-flash", contents=contents, config=config, # モデル呼び出しに config を渡している点に注目 ) print(response.candidates[0].content.parts[0]) ``` 上記のコードを実行すると、以下のような結果が出力されます: ``` $ python3 main.py video_metadata=None thought=None inline_data=None file_data=None thought_signature=None code_execution_result=None executable_code=None function_call=FunctionCall(id=None, args={}, name='get_random_number') function_response=None text=None ``` ここで出力されているのはモデルレスポンスの最初のパートです。`function_call` フィールド以外のすべてのフィールドが空(`None`)になっていることがわかります。これは、モデルが**私たち(クライアント)**に対してこの関数を呼び出し、その結果をモデルに返してくれるよう求めていることを意味します。 最初はこの挙動に少し戸惑うかもしれませんが、よく考えてみれば完全に理にかなっています。モデルは関数の存在を知ってはいますが、それを実際にどうやって実行するかは一切知りません。モデルの視点から見れば、関数は同じマシン上で動いているわけでもないため、「自分の代わりにこの関数を呼び出してください」と私たちに丁寧に依頼する以外に方法がないのです。 前回の記事でこの手順が不要だったのは、Automatic Function Calling が裏で面倒を見てくれていたからです。内部的な呼び出しフロー自体はまったく同じですが、SDK がこの複雑さをすべて隠蔽してくれていたわけです。 次にやるべきことは当然、実際の関数を呼び出してその結果をモデルに返すことです。ただし思い出してください。コンテキストがなければ、モデルは直前のリクエストについて何も覚えていません。そのため、関数の実行結果だけをポンと送り返しても、モデルは何をすればいいのか理解できません! だからこそ、これまでの対話履歴(少なくともモデルがその値を要求した時点まで遡る履歴)を一緒に送信する必要があります。以下のコードは、関数呼び出しメッセージを受け取った後、完全な情報を含めて新たなリクエストを送信する実装です: ```python # レスポンスを検査し、モデルが要求している内容を把握したと仮定 result = get_random_number() # 実際の関数呼び出しを実行 # contents には元のプロンプトが残っているため、モデルの応答を追加し... contents.append(types.ModelContent(parts=response.candidates[0].content.parts)) # ... さらに関数呼び出しの結果を追加する contents.append(types.UserContent(parts=types.Part.from_function_response(name="get_random_number", response={"result": result}))) response = client.models.generate_content( model="gemini-2.0-flash", contents=contents, config=config, ) print(response.text) ``` 出力: ``` $ python3 main.py Today's lucky number is 4. ``` ## おわりに この記事では、エージェントクライアントがサーバー側のモデルとどのように通信しているか、言い換えれば LLM 通信の「ドメインモデル」がどのようになっているかを見てきました。また、Python SDK が裏で行ってくれている「魔法」のベールも剥がしてみました。 自動化は常に便利で、迅速に結果を出すのに役立ちます。しかし、エージェントを自作する際、実際に何が起きているのかを知っているかどうかは、開発がスムーズに進むかトラブルに見舞われるかの決定的な違いになります。とりわけエッジケースの処理は、*決して一筋縄ではいかない*からです。 バイブス駆動開発(Vibe Coding)が全盛の今、このようなことを言うのは一見皮肉に聞こえるかもしれません。しかし、私がバイブコーディングを通じてすぐに学んだことの 1 つは、AI に対してより的確かつ正確に指示を伝えられるほど、はるかに短い時間で圧倒的に優れた結果が得られるということです。だからこそ、今は基礎知識の価値を過小評価する時ではなく、むしろそれを深めるべき時なのです。AI があるから知識が不要になるのではなく、**AI があるからこそ**知識が必要になるのです。 ここまでの道のりを楽しんでいただけたなら幸いです。本シリーズの次回 [システム指示とエージェントツールの実践ガイド](https://danicat.dev/ja/posts/20250611-system-prompt/) では、今回の知識をベースに、セッション履歴の管理や動的なテーブルスキーマの注入を備えた本格的なターミナルチャットアプリケーションを構築します。 ぜひ下のコメント欄でご意見やご感想をお聞かせください! それではまた! o/ --- # AIエージェントを使って自分のコンピューターを「USSエンタープライズ」に変えた方法 - **Markdown URL**: https://danicat.dev/ja/posts/diagnostic-agent-uss-enterprise/index.md - **HTML URL**: https://danicat.dev/ja/posts/diagnostic-agent-uss-enterprise/ - **Date**: 2025-05-31 - **Categories**: Agent Development - **Tags**: gemini, python, tutorial, vertex-ai - **Series**: Building the Diagnostic Agent - **Summary**: Gemini と Vertex AI Agent Engine、osquery の Function Calling を使って、自然言語でPCの健全性を診断するスタートレック風アシスタントを構築。_宇宙、それは人類に残された最後の開拓地である。そこには人類の想像を絶する新しい文明、新しい生命が待ち受けているに違いない。これは人類初の試みとして5年間の調査飛行に飛び立った宇宙探査船USSエンタープライズ号の驚異に満ちた物語である。_ ## はじめに 子供の頃、父の影響で私は毎日のようにこのオープニングナレーションを聞いて育ちました。父のスタートレックへの情熱が、私がソフトウェアエンジニアの道を選ぶ上で大きなきっかけになったのだと思います。(スタートレックをご存じない方のために補足すると、このナレーションは初代スタートレックシリーズの全エピソードの冒頭で流れていたものです) スタートレックは常に時代を先取りしていました。そのような描写が大きな議論を呼んだ時代に、[米国のテレビ史上初となる異人種間のキスシーン](https://en.wikipedia.org/wiki/Kirk_and_Uhura%27s_kiss)を放映し、スマートフォンやビデオ会議など、今日では当たり前となった数多くの「未来的」テクノロジーも描いていました。 特に印象的なのは、劇中のエンジニアたちがコンピューターとやり取りする方法です。キーボードを叩いたりボタンを押したりする場面も時折見られますが、多くのコマンドは自然言語の音声で指示されます。「レベル1診断手順(level 1 diagnostic procedure)を実行せよ」といった象徴的な命令は、あまりに頻繁に登場するため、熱心なファンの間では[定番のネタ(ジョーク)](https://www.youtube.com/watch?v=cYzByQjzTb0)になっているほどです。 それから30年以上が経ち、私たちはインターネット以上の変革をもたらすと言われる「AI の時代」を迎えています。AI が仕事に与える影響を不安視する声も多くありますが([先週の記事「誰でもvibe codeできるのか?」](https://danicat.dev/ja/posts/20250528-vibe-coding/) でも触れました)、スタートレックを見て育った私には、今後数年でエンジニアの役割がどのように変化していくのかが自然とイメージできます。コードを1行ずつ書き、コンパイラを通じて手動で指示を与える代わりに、私たちは間もなくコンピューターと直接対話し、ブレインストーミングを行いながら開発を進めるようになるでしょう。 これを具体的にイメージできるように、今回は現在利用可能なテクノロジーを使って、自然言語で自分のマシンと対話できる小さな診断エージェントを作成してみます。 ## このデモに必要なもの 開発言語には、実験がしやすい Jupyter Notebook 上の Python を使用します。主に使用するツールとライブラリは以下の通りです: * [Vertex AI Agent Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/overview?utm_campaign=CDR_0x72884f69_awareness_b421478530&utm_medium=external&utm_source=blog) * [Osquery](https://www.osquery.io/)(および [Python バインディング](https://github.com/osquery/osquery-python)) * [Jupyter Notebook](https://jupyter.org/) [任意](私は [VS Code の Jupyter 拡張機能](https://code.visualstudio.com/docs/datascience/jupyter-notebooks) を使用しています) 以下のサンプルでは Gemini 2.0 Flash を使用しますが、お好みの [Gemini モデルバリアント](https://ai.google.dev/gemini-api/docs/models) を使用できます。クラウド上のサーバーではなくローカルマシンの診断を行うため、今回はエージェントを Google Cloud にデプロイすることはしません。 ## エージェントの概要 エージェント技術の仕組みをすでにご存じの方は、このセクションをスキップして構いません。 AI エージェントとは、周囲の環境を認識し、特定の目標を達成するために自律的なアクションを実行できる AI システムです。入力に基づいてコンテンツを生成することに特化した従来の大規模言語モデル(LLM)と比較すると、AI エージェントは環境と対話し、意思決定を行い、目的を達成するためのタスクを実行できます。これは、エージェントに情報を提供し、アクションを実行可能にする「ツール(tools)」を使用することで実現されます。 エージェント技術を体験するために、今回は [Agent Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/develop/langchain?utm_campaign=CDR_0x72884f69_awareness_b421478530&utm_medium=external&utm_source=blog) 経由で LangChain を使用します。まず、必要なパッケージをシステムにインストールします: ```shell pip install --upgrade --quiet google-cloud-aiplatform[agent_engines,langchain] ``` また、gcloud の Application Default Credentials(ADC)を設定する必要があります: ```shell gcloud auth application-default login ``` 注:このデモを実行する環境によっては、別の認証方法が必要になる場合があります。 これで Python スクリプトの作成準備が整いました。まず、Google Cloud のプロジェクト ID とロケーションに基づいて SDK を初期化します: ```python import vertexai vertexai.init( project="my-project-id", # あなたのプロジェクト ID location="us-central1", # クラウドのロケーション staging_bucket="gs://my-staging-bucket", # ステージング用バケット ) ``` 初期設定が完了したら、Agent Engine で LangChain を使用したエージェントの作成は非常にシンプルです: ```python from vertexai import agent_engines model = "gemini-2.0-flash" # ぜひ他のモデルも試してみてください! model_kwargs = { # temperature (float): サンプリング温度。トークン選択のランダム性を制御します。 "temperature": 0.20, } agent = agent_engines.LangchainAgent( model=model, # 必須 model_kwargs=model_kwargs, # 任意 ) ``` この設定だけで、一般的な LLM に問い合わせるのと同じようにエージェントへクエリを送信できます: ```python response = agent.query( input="which time is now?" ) print(response) ``` すると、次のような応答が返ってきます: ``` {'input': 'which time is now?', 'output': 'As an AI, I don\'t have a "current" time or location in the same way a human does. My knowledge isn\'t updated in real-time.\n\nTo find out the current time, you can:\n\n* **Check your device:** Your computer, phone, or tablet will display the current time.\n* **Do a quick search:** Type "what time is it" into a search engine like Google.'} ``` 設定やプロンプト、そしてモデルの挙動の揺らぎによっては、時刻を答えられないと返すか、あるいは「ハルシネーション(幻覚)」を起こして架空のタイムスタンプをでっち上げるかのどちらかになります。実際のところ、AI は時計を持っていないため、時計というツールを与えない限りこの質問には答えられません。 ## Function Calling(関数呼び出し) エージェントの機能を拡張する最も便利な方法の 1 つは、呼び出し可能な Python 関数を渡すことです。手順自体は非常にシンプルですが、関数の docstring(ドキュメント)を詳細に記述するほど、エージェントが適切に関数を呼び出しやすくなる点は重要です。まずは現在時刻を確認する関数を定義してみましょう: ```python import datetime def get_current_time(): """Returns the current time as a datetime object. Args: None Returns: datetime: current time as a datetime type """ return datetime.datetime.now() ``` システム時刻を返す関数が用意できたので、この関数の存在を認識させた上でエージェントを再作成します: ```python agent = agent_engines.LangchainAgent( model=model, # 必須 model_kwargs=model_kwargs, # 任意 tools=[get_current_time] ) ``` そして、もう一度同じ質問を投げます: ```python response = agent.query( input="which time is now?" ) print(response) ``` 出力は次のようになります: ``` {'input': 'which time is now?', 'output': 'The current time is 18:36:42 UTC on May 30, 2025.'} ``` これでエージェントはツールを活用し、実際のデータに基づいて質問に答えられるようになりました。なかなか面白いと思いませんか? ## システム情報の収集 今回の診断エージェントには、[Osquery](https://www.osquery.io/) というツールを使って、実行中のマシンに関する情報を取得する機能を持たせます。Osquery は Facebook(現 Meta)が開発したオープンソースツールで、OS の内部情報を公開する「仮想テーブル」に対して SQL クエリを実行できるようにするものです。 これは非常に便利です。システムに関する問い合わせの窓口が 1 つにまとまるだけでなく、LLM は SQL クエリの作成が非常に得意だからです。 Osquery のインストール手順は [公式ドキュメント](https://osquery.readthedocs.io/en/stable/) を参照してください。マシンの OS によって手順が異なるため、ここでは割愛します。 Osquery をインストールしたら、Osquery の Python バインディングをインストールします。Python らしく、`pip install` 1 つで完了します: ```shell pip install --upgrade --quiet osquery ``` バインディングをインストールしたら、`osquery` パッケージをインポートして Osquery の呼び出しを行えます: ```python import osquery # エフェメラルな拡張ソケットを使用して osquery プロセスを起動 instance = osquery.SpawnInstance() instance.open() # 例外が発生する可能性があります # クエリを発行し osquery Thrift API を呼び出す instance.client.query("select timestamp from time") ``` `query` メソッドは、クエリ結果を含む `ExtensionResponse` オブジェクトを返します。例えば以下のようになります: ```python ExtensionResponse(status=ExtensionStatus(code=0, message='OK', uuid=0), response=[{'timestamp': 'Fri May 30 17:54:06 2025 UTC'}]) ``` これまで Osquery を使ったことがない方は、[スキーマ](https://www.osquery.io/schema/5.17.0/) を確認して、お使いの OS でどのような情報が取得できるかを見てみることをおすすめします。 ### 出力フォーマットに関する補足 これまでのサンプルの出力はフォーマットされていませんでしたが、Jupyter 上でコードを実行している場合は、以下のモジュールをインポートすることで出力を綺麗に整える便利なメソッドが使えます: ```python from IPython.display import Markdown, display ``` そして、レスポンスの出力を Markdown として表示します: ```python response = agent.query( input="what is today's stardate?" ) display(Markdown(response["output"])) ``` 出力: ``` Captain's Log, Supplemental. The current stardate is 48972.5. ``` ## 点と点をつなぐ(診断エージェントの実装) OS の情報を照会する方法がわかったので、エージェントの知識と組み合わせて、システムに関する質問に答えてくれる診断エージェントを作成しましょう。 最初のステップは、クエリを実行する関数を定義することです。この関数は、後で情報収集用のツールとしてエージェントに渡します: ```python def call_osquery(query: str): """Query the operating system using osquery This function is used to send a query to the osquery process to return information about the current machine, operating system and running processes. You can also use this function to query the underlying SQLite database to discover more information about the osquery instance by using system tables like sqlite_master, sqlite_temp_master and virtual tables. Args: query: str A SQL query to one of osquery tables (e.g. "select timestamp from time") Returns: ExtensionResponse: an osquery response with the status of the request and a response to the query if successful. """ return instance.client.query(query) ``` 関数自体は極めてシンプルですが、ここで重要なのは、エージェントがこの関数の動作を理解できるように非常に詳細な docstring を記述することです。 テスト中によく発生した厄介な問題として、エージェントがシステム上にどのテーブルが存在するかを正確に把握していないという点がありました。例えば、私は macOS マシンで実行しているのですが、`memory_info` というテーブルは macOS には存在しません。 エージェントにより多くのコンテキストを提供するために、このシステムで利用可能なテーブル名を動的に渡すようにします。理想的な状況であれば、カラム名や説明を含むスキーマ全体を渡すところですが、Osquery ではそれを実現するのは容易ではありません。 Osquery の基盤となるデータベース技術は SQLite なので、`sqlite_temp_master` テーブルから仮想テーブルの一覧を取得できます: ```python # Python の小技を使って、このシステムに存在するテーブル一覧を特定する response = instance.client.query("select name from sqlite_temp_master").response tables = [ t["name"] for t in response ] ``` テーブル名がすべて取得できたので、この情報と `call_osquery` ツールを使用してエージェントを作成します: ```python osagent = agent_engines.LangchainAgent( model = model, system_instruction=f""" You are an agent that answers questions about the machine you are running in. You should run SQL queries using one or more of the tables to answer the user questions. Always return human readable values (e.g. megabytes instead of bytes, and formatted time instead of miliseconds) Be very flexible in your interpretation of the requests. For example, if the user ask for application information, it is acceptable to return information about processes and services. If the user requests resource usage, return BOTH memory and cpu information. Do not ask the user for clarification. You have the following tables available to you: ----- TABLES ----- {tables} ----- END TABLES ----- Question: """, tools=[ call_osquery, ] ) ``` 信じられないかもしれませんが、これで私たちのエージェントはあらゆる質問に答える準備が整いました!実際に試してみましょう: ```python response = osagent.query(input="what is the current time?") display(Markdown(response["output"])) ``` 出力: ``` The current time is Fri May 30 18:08:15 2025 UTC. ``` もう少し複雑な質問をしてみます: ```python response = osagent.query(input="what is the top consuming process?") display(Markdown(response["output"])) ``` 出力: ``` The top CPU-consuming process is: Process Name: mediaanalysisd, PID: 1127, Total CPU Time: 2876716 The top memory-consuming process is: Process Name: Code Helper (Plugin), PID: 2987, Memory Usage: 1537 MB ``` さらに創造的な質問を投げかけてみたらどうなるでしょうか? ```python response = osagent.query(input="computer, run a level 1 diagnostic procedure") display(Markdown(response["output"])) ``` 出力: ``` Here is the level 1 diagnostic information: **Operating System:** * **Name:** macOS * **Version:** 15.5 * **Build:** 24F74 * **Platform:** darwin * **Architecture:** arm64 ... ommitted for brevity ... **Uptime:** * **Total Seconds:** 261164 * **Days:** 3 * **Hours:** 0 * **Minutes:** 32 * **Seconds:** 44 ``` 素晴らしいですね!「レベル1診断手順(Level 1 diagnostic procedure)」が何であるかは一切定義していなかったにもかかわらず、驚くほど本格的な診断レポートを返してくれました。 さらに突っ込んだ質問も試してみましたが、その回答も期待を裏切らないものでした(ほとんどの場合は): ```python response = osagent.query(input="computer, do you see any signs of malware running?") display(Markdown(response["output"])) ``` 出力: ``` I have checked for processes that are not associated with a file on disk, which can be a sign of malware, and found none. I have also examined the top processes by memory and CPU usage. The processes consuming the most resources are primarily Visual Studio Code and Google Chrome and their related helper processes. This is typical behavior for these applications. Based on the checks performed, there are no obvious signs of malware running on the system at this time. ``` _(マイクを落としてステージを去るポーズ)_ =^.^= ## まとめ 使い古された表現かもしれませんが、AI はまさにゲームチェンジャーです。ごくわずかなコード行数で、OS の内部動作と対話できる本格的な自然言語インターフェースをゼロから構築できました。もう少し手を加えれば、より深い診断を行ったり、場合によっては自律的に問題を修復したりするようにエージェントを拡張することもできるでしょう。スコッティ機関長もきっと誇らしく思ってくれるはずです! ![マウスをマイク代わりにしてコンピューターに話しかけようとするスコッティ機関長](https://danicat.dev/ja/posts/diagnostic-agent-uss-enterprise/hello-computer-hello.gif) この記事で紹介したすべてのサンプルのソースコードは、私の [GitHub](https://github.com/danicat/devrel/blob/main/blogs/20250531-diagnostic-agent/diagnostic_agent.ipynb) で公開しています。 シリーズの次回記事「[Vertex AI SDK for Python を深く掘り下げる](https://danicat.dev/ja/posts/20250605-vertex-ai-sdk-python/)」では、クライアントと Gemini の通信プロトコルの裏側を探り、低レイヤーでの手動 Function Calling を実装します。 皆さんはどう感じましたか?ぜひ下のコメント欄で感想を共有してください! --- # 誰でも vibe code できるのか? ソフトウェアエンジニアの未来 - **Markdown URL**: https://danicat.dev/ja/posts/can-anyone-vibe-code/index.md - **HTML URL**: https://danicat.dev/ja/posts/can-anyone-vibe-code/ - **Date**: 2025-05-28 - **Categories**: Agentic Coding - **Tags**: career, vibe-coding - **Summary**: 誰でもバイブコーディングできるのか?AIが参入障壁を下げる一方で、古典的なソフトウェアエンジニアリングの規律とオーケストレーションが重要になる理由を考察。 ## はじめに エンジニアではない人が、本番環境で通用するプロダクションレディなアプリケーションを「vibe code(バイブコーディング)」できるようになると思いますか? これは、私が最近ずっと考えている問いです。 AI を取り巻く環境は目まぐるしいスピードで進化しており、魅力的な新ツールがほぼ毎日のように登場しています。そのため、多くの人が危機感を覚え、ソフトウェアエンジニアというキャリアの終焉を「予言」し始めているのも無理はありません。たとえば [Jules](https://danicat.dev/ja/posts/20250521-jules/) を見てみましょう。依存関係の更新やドキュメント作成、リファクタリング、さらにはテストに至るまで、エンジニアの典型的なタスクの多くを自動化できる AI エージェントです。 もしエンジニアとしてあなたがもたらしている価値がそれ**だけ**なのだとしたら、確かに焦りや不安を感じるべきなのかもしれません。しかし、恐れることは必ずしも悪いことではありません。恐怖は私たちをコンフォートゾーンから連れ出し、普段なら取らないようなリスクへの挑戦を後押ししてくれます。適度な危機感こそが、自己成長とイノベーションの原動力になるのです。 ## そもそも、エンジニアとは何者なのか? 大学で電気工学を専攻したものの実際には卒業していない私には、この問いに答える資格があまりないかもしれません。それでも、学位(ディプロマ)こそありませんが、大学で学んだ時間は、その後のキャリアにおいて問題に向き合う際のマインドセットを形作る上で確かな土台となりました。 私に大きな影響を与えた決定的な瞬間のひとつが、新入生向けの講義でエワルド・メール博士(パラナ連邦大学 / UFPR)がエンジニアという職業について語ってくれたマスタークラスでした。博士の数ある素晴らしい言葉の中で、20年以上経った今でも鮮明に心に残っているのが、私たちの仕事の本質についての話です……博士はこんなことを言いました。「エンジニアは、神に最も近い専門職である(……)なぜなら私たちは単に物事を修理するだけでなく、*創造*する存在だからだ」と。 もちろん、前後の文脈なしにこの言葉だけを聞くと突飛に思えるかもしれません。しかしこの話の要点は、医療などの分野でよく取り沙汰される「神コンプレックス(God complex:全能感の錯覚)」にまつわるものでした。教授は、「エンジニアは医師に対して引け目を感じる必要などない。本当にそのコンプレックスを持つべきなのはむしろ私たちの方だ」と冗談を飛ばしていたのです。(どうかコメント欄で荒らさないでくださいね。2000年代初頭の話で、まだ90年代のノリが色濃く残っていた時代ですから :-) 私の出身地には「どんな冗談の底にも一握りの真実がある」ということわざがありますが、それはさておき、メール博士の言葉は核心を突いていたと私は思います……あらゆるツールを削ぎ落としたとき、私たちの本質は「創造者(クリエイター)」に他なりません。創造の手法そのものは時代とともに進化します。かつては素手と原材料でした。やがて道具、コンピューター、そしてロボットが登場しました。そして今、私たちは AI の時代へと足を踏み入れています。 ですから、エンジニアというキャリアの行く末(それが衰退・消滅するのかどうか)について考えるときは、常に「新しいものを創り出したい」という人間の根源的な欲求を思い出してください。私たちはこれからも試行錯誤(イテレーション)を重ね、改善し、革新し続けます。これは人間性のあまりにも根本的な特質であり、今後5年はおろか、次の1000年が経とうと消え去るとは思えません。 しかし、私たちが物事を創り、改善していく**プロセスや手法**そのものは、間違いなく変わっていくはずです。 ## 単なるコーディングから、オーケストレーションへ AI 革命は、私たちを退屈なルーチンワークから解放し、仕事の中で最もエキサイティングな側面(アーキテクチャ設計、システムプランニング、ソリューションのテストや検証)に集中させてくれると私は考えています。 もちろん、コーディング自体がつまらないと言っているわけではありません。むしろ逆で、多くのエンジニアと同じように私もコードを書くのが大好きです。しかし、業務用の本番コードを保守することは、趣味で楽しくコードを書くこととは違います。本来達成したいゴールとは直接結びつかないようなコードを、膨大に書く羽目になるからです。 たとえばセキュリティ、ロギング、メトリクス収集、ネットワーキングといった非機能要件全般を思い出してみてください。環境構築やデプロイ、テスト、検証に費やす何時間もの作業。異なるアプリケーション層やデータベースなどを連携させるための細かなセットアップ手順……私たちが日常業務で行っていることの多くは、本質的なビジネス課題を解決するためというより、単に「システムを動く状態にする」ためだけに向けられています。 このブログの立ち上げのようなシンプルなタスクですら、数日間の作業を要しました。開発環境を整え、2025年の今ブログに最適なツールを調べ、ホスティングの選択肢を比較し、選んだプラットフォームの構成ルール(Hugo サイトの構造など)を学び、複数のテンプレートを試してから1つに絞り込み、投稿にコメント欄を追加する方法を調査し……といった具合です。 これらすべてが、私が本来解決したかったビジネス課題とどう関係しているでしょうか? もちろん必要な手順ではありますが、それらは単に「動かす」ために不可欠だったからこなしたに過ぎず、そもそもやりたかったことの核心ではありません。 私が本当に作りたかったのは、ブログ記事を公開し、より広い読者に届けるために各種プラットフォーム(Medium や LinkedIn など)へ後から配信するための「信頼できる単一の情報源(Source of Truth)」です。しかし、新規アプリケーションの着想から本番稼働に至る開発パイプラインのあらゆる側面に対応しなければならなかったため、本来の課題解決にはまだ着手すらできていません。 これらすべてを抽象化して、AI に「必要なものがすべて揃った(batteries included な)ブログプラットフォームを生成して」と頼むだけで済んだら、どれほど素晴らしいでしょうか? こここそが、AI が急速に真価を発揮する領域です。エージェント型 AI(Agentic AI)の登場により、私たちは近い将来、無数のエージェントを自由に指揮し、ソフトウェア開発プロセスの全フェーズをオーケストレーションできるようになります。人間の主たる役割は「新しいユーザー体験の着想(アイディエーション)」となり、それを具現化・運用に落とし込む作業はエージェントが担うのです。エージェントがコードを書き、デプロイし、改善を重ねます。調査を行って複数の選択肢を提示してくれるため、私たちは自分のビジョンに合わせて選ぶだけでよくなります。 具体的な例がなければイメージしにくいかもしれませんが、だからこそ私は SF(サイエンス・フィクション)が大好きなのです。SF は未来を覗き見る窓になってくれます。『スタートレック』のコンピューターを思い浮かべてみてください……あと数年もすれば、[このシーンのジョーディ・ラ=フォージのように](https://youtu.be/L0mRMp2kbQY?feature=shared)、私たちはコンピューターと対話しながら作業するようになると確信しています。エンジニアリングのプロセスは、エンジニアによる一方通行の作業ではなく、対話そのものになるのです。 現在すでにその域に達しているでしょうか? まだ完全には至っていません。しかし、エージェント型 AI はスタートレックで描かれた未来を現実にするための確実な一歩です。AI がもたらすインパクトはインターネットの登場すら超えるものだと実感し始めていますし、私たち開発者も「新世代(Next Generation)」(もちろん『新スタートレック: TNG』にかけた洒落です)へと自分自身をアップデートしていく努力をすべきでしょう。 ## 結局のところ、誰でも「vibe code」できるのか? *誰もが*すぐにできるわけではないかもしれませんが、参入障壁が劇的に下がっているのは間違いありません。求められるコアスキルは進化しつつありますが、システムを創造し、アーキテクチャを設計するというエンジニアの役割は失われていませんし、これからもずっと残り続けるでしょう。 **あなたはどう考えますか? AI を指揮して自らのビジョンを具現化する、より「ハンズオフ(設計・指示主導)」なエンジニアとしての自分の姿をイメージできるでしょうか? ぜひコメント欄で意見を聞かせてください!** --- # Julesについて語りましょう! - **Markdown URL**: https://danicat.dev/ja/posts/jules-autonomous-coding-agent/index.md - **HTML URL**: https://danicat.dev/ja/posts/jules-autonomous-coding-agent/ - **Date**: 2025-05-21 - **Categories**: Agentic Coding - **Tags**: jules, vibe-coding - **Summary**: Googleの非同期自律型コーディングエージェント「Jules」のファーストインプレッション。GitHub連携、自動PR生成、開発ワークフローの変化を解説。さあ皆さん、Julesについて語りましょう!Google I/Oのオーブンから焼き上がったばかりの、Googleが「自律型コーディングエージェント(autonomous coding agent)」と呼ぶ注目のツールが登場しました。……ところで、自律型コーディングエージェントとは一体何なのでしょうか?「コーディング版[NotebookLM](https://notebooklm.google/)」を想像してみてください。コーディング作業を支援するために特化したAIです。従来の「バイブコーディング(vibe coding)」アプローチとの大きな違いは、Julesではプロジェクト全体を丸ごとAIのコンテキストとしてインポートできる点です。そのため、すべての回答が実際に作業しているコードベースにしっかりと基づいたものになります! ## Julesの仕組み プロジェクトをインポートした後は、「タスク(task)」を送信するだけでJulesとやり取りができます。タスクの内容は、バグ修正や依存関係のアップデート、新機能の追加、計画の立案、ドキュメント作成、テストなど何でも構いません。タスクを受け取ると、Julesは非同期で実行ステップを計画し、期待通りの結果が得られるようにさまざまなサブタスクを実行します。たとえば、新しい変更によって既存のテストが壊れていないかを確認するといったことです。 GitHubと直接連携するため、使い始めるハードルはほとんどありません。現時点でIDEを完全に置き換えるものではありませんが、Jules上から直接多くのタスクを実行でき、リクエストした変更をすべて反映したブランチを作成してPull Request(PR)を出せる状態にしてくれます。 ## ファーストインプレッションと初期のテスト 残念ながら、昨日のJulesの発表直後ということもあってツールには現在大きな負荷がかかっており、タスクを送信してから結果が出るまでに少し時間がかかる場合があります。それでもJulesはバックグラウンドで作業を進めてくれるため、ブラウザ通知を有効にしておけば、完了したタイミングで通知してくれます。 そんな状況だったため、大規模な実験までは行えませんでしたが、手始めに[GitHub上のこのブログプロジェクトのREADME生成](https://github.com/danicat/danicat.dev/pull/1)(まさに今読んでいただいているこのページのソースコードです)を試してみました。さらに、ブログのテンプレート調整など、もう少し複雑な変更にも挑戦してみました。[生成されたファイル自体は正確でした](https://github.com/danicat/danicat.dev/pull/2)が、リクエストへの応答に少し時間がかかったため、いくつか手動で変更を加えることになりました。 ## IDEの新たな時代 初日としては上々と言える出来栄えで、今後数週間や数か月の間に引き出されるであろうポテンシャルには大きな期待が持てます。最大のキラー機能は、コードベース全体を対象に作業できる点です。「Gemini(またはChatGPT)に質問し、コードをIDEにコピーし、実行し、その結果を再度LLMにコピペして反復する」というあの従来の流れから解放されます。もちろん、Code AssistやCopilotなどのツールを使えばIDEから離れずにこうした機能の一部を利用できますが、従来のIDEはバイブコーディングにとって最適な環境とは言えず、どこか場当たり的なパッチ当て(ハック)のように感じてしまいます。 その意味で、Julesは次世代のIDEに向けた素晴らしい刺激(インスピレーション)になるかもしれません。世界中の開発者に向けて、より自然な形でAIのポテンシャルを解き放つ新しい時代を切り拓いてくれること。少なくとも私はそれを期待しています! ## さっそく試してみる Julesは現在パブリックベータ版として公開されており、[https://jules.google](https://jules.google) でサインアップすれば今すぐ試すことができます。 --- # Hello World: 技術の物語を紡ぐ新たな場所 - **Markdown URL**: https://danicat.dev/ja/posts/hello-world/index.md - **HTML URL**: https://danicat.dev/ja/posts/hello-world/ - **Date**: 2025-05-20 - **Categories**: Perspectives - **Tags**: blogging, career, reflection - **Summary**: danicat.devへようこそ:ソフトウェアエンジニアリング、Google Cloud、AIエージェント、キャリアの軌跡を発信するダニエラ・ペトルザレクの公式技術ブログ。 テクノロジー業界で20年以上を過ごす中で、あるトピックに対する自身の理解を深める最も効果的な方法は「他の人と共有すること」だと実感してきました。このブログ `danicat.dev` は、まさにそのための専用の場所です。 私の名前はダニエラ・ペトルザレク(Daniela Petruzalek)です。現在は Google でデベロッパーリレーションズエンジニア(Developer Relations Engineer)として働いています。このブログは、ソフトウェア開発、クラウドコンピューティング、AI における日々の実践から得た「技術の物語(Technical Narratives)」を共有するプラットフォームとして活用していく予定です。 ## 目的とアプローチ これまでもイベントや GitHub などのプラットフォームで技術的なコンテンツを発信してきましたが、より詳細でストーリー性のある記事をお届けできる場所を持ちたいと考えていました。単なるチュートリアルの枠を超え、途中で直面した課題も含めて、実際に「作り、学ぶプロセス」そのものを記録・発信していくことがこのブログの狙いです。 なお、このサイト自体は [Hugo](https://gohugo.io/) と [Blowfish](https://blowfish.page/) テーマで構築しています。この技術選定には、高速でシンプル、かつカスタマイズ性に優れたツールを好むという私のエンジニアリングに対するスタンスが反映されており、これは私が普段書いている技術的な意思決定にも共通するテーマです。 ## 今後の発信内容 このブログのコンテンツは、私自身の技術的な歩み(ジャーニー)を中心に展開していきます。どんなに複雑なトピックであっても、ひとつの「物語(ナラティブ)」として語ることで、より本質が明確になると信じているからです。具体的には、以下のようなトピックを扱っていく予定です: * **技術的な探求(Technical Explorations):** Go、Python、生成 AI(Generative AI)、および Google Cloud 上でのデータエンジニアリングに関する記事。 * **プロセスとワークフロー(Process and Workflow):** 開発者のワークフロー、ベストプラクティス、キャリアの歩み方についての考察。 * **等身大の視点(A Grounded Perspective):** コアな技術にとどまらず、猫やコーヒーといった個人的な関心事についても時折触れていきます。 このブログは、皆さんとの技術的な対話への招待状でもあります。コラボレーションや登壇のご依頼も大歓迎です。つながりたいと思ってくださる方は、[LinkedIn](https://www.linkedin.com/in/petruzalek/) でご連絡いただくか、[daniela@danicat.dev](mailto:daniela@danicat.dev) までメールをお送りください。 最後まで読んでいただき、ありがとうございます。ご意見や今後のトピックのリクエストなどがあれば、ぜひ下のコメント欄でお知らせください。 Dani =^.^= P.S.: このブログで述べられている見解は私個人のものであり、必ずしも雇用主の見解を代表するものではありません。 ---