株式会社豆蔵のブログ - TECH PLAY

TECH PLAY

株式会社豆蔵

株式会社豆蔵 の技術ブログ

120

はじめに # GFXBenchというGPUベンチマークソフトを取り上げたシリーズの2回目です。 前回でGitHub公開されたGFXBenchのビルドが行えました。 今回は実際に実行してベンチマークスコアを確認してみます。 ベンチマーク紹介 # GFXBenchは様々なベンチマークから構成されており、起動時に選択して実行する様になっています。 カテゴリは大きく 高レベルテスト と 低レベルテスト に分かれています。 今回取り上げるのは 高レベルテスト の方で、その中からいくつか抜粋して紹介します。 --> Information 以降画像は筆者のスマートフォンのGFXBench実行時スクリーンショットより引用 スマートフォン/タブレットによってはレイアウト等見え方が異なる場合があります T-Rex # Manhattan # Manhattan 3.1 # Car Chase # Aztec Ruins # 色々説明されていますが、まずは "Required minimum API" の部分だけに注目してみます。 以下の様にベンチマークが OpenGL ESバージョン に伴って進化してきた事がわかります。 (GFXBenchのバージョンアップで各ベンチマークが追加されるという見え方) ベンチマーク OpenGL ES version T-Rex 2.0 Manhattan 3.0 Manhattan 3.1 3.1 Car Chase 3.1 + AEP (Android Extension Pack) そのため、そのAndroid端末が対応している OpenGL ESバージョン によって実行出来るベンチマークが変わります。実行できない場合は後のベンチマーク選択画面で選択出来ない状態となります。 ※ Aztec Ruins は路線が変わった様なので省略(APIのバージョンではなくマルチAPIでのベンチマークというニュアンスになった模様) ベンチマークインストール # 前回ビルドを行ったapkファイルをAndroid端末にインストールします。 Android端末側は開発者モードになっていてビルド用PCとadb接続出来ているとします。 なお、adbコマンド実行時は Git Bash でなく Windowsターミナル を使用した方が(何かと)トラブルが起きずに済みます。 adb install gfxbench-5.1.5+corporate.apk ベンチマーク実行 # Android端末上からGFXBenchのアイコンをクリックし起動します。 初回起動時は「Pushed data not found」という表示が出ます。 「OK」を選択してapkバンドルのデータをアプリデータ領域にコピーをします。 --> Information 前回も触れましたが初回起動時にこのコピー分だけアプリサイズが増える事になります。 手動でデータを配置する手順も上記表示の様に案内されます。 起動すると、ベンチマークの初期画面が表示されます。 そこで "テスト選択" ボタンを押すと テスト選択 の画面になります。 実行するベンチマークとしては筆者の思い入れより以下の2つとします。 T-Rex Manhattan また、それぞれについて、オンスクリーン版とオフスクリーン版があります。 オンスクリーン版では実際に画面にベンチマークが描画されて実行されます。そのためAndroid端末の実際の画面サイズにベンチマークスコアが依存します。 オフスクリーン版では画面に描画されず(進行がわかる程度の描画はあり)オフスクリーンサイズ固定で実行されます。そのためAndroid端末の実際の画面サイズに依存せずベンチマークスコアを得る事が出来ます。 Android端末を横断的に(画面サイズに依存せず)GPU性能を比較したい場合はこちらを選択します。 最初起動すると全ベンチマークがチェックされた状態になっているので、実行したいベンチマークのみをチェックし "開始" ボタンを押します。 (高レベルテスト、低レベルテスト といったカテゴリでチェックを外すと一括でチェックを外せるので便利です) ベンチマーク実行結果 # 実際にオフスクリーン版を実行してみます。 (画面描画を見たい方はオンスクリーン版でお楽しみください) 実行するAndroid端末はAndroidバージョンが(比較的)新しいものを手元のコレクションから見繕ってみます。 なお、今回対象とするAndroid端末のスペック帯はベンチマークスコアが最大でも 100台半ば 程度のfpsのものとします。 (ハイスペックな端末にとってはいまや T-Rex/Manhattan ベンチマークは軽い部類となってしまいましたので...。 その場合はもっと重いベンチマークが適していると思われます) 以下、実測した結果例です。ベンチマークスコアはオフスクリーン版のfps値です。 --> Caution あくまで 「筆者の環境および測定時点における一例」 であり、同様のGUP、手順で測定された場合でも、環境によって異なる結果となる可能性がある点をご了承ください。 GPU SoC Driver version Android version T-Rex score Manhattan score ARM Mali G57 MC1 Allwinner A537 OpenGL ES 3.2 v1.r51p0-00eac0.26a7a06524af59d6533aad5e5bab3098 15 19 13 ARM Mali G57 MC2 Mediatek Helio G99 OpenGL ES 3.2 v1.r32p1-01eac0.394145956bc7cd8e697b330aba11e3d3 13 57 37 ARM Mali G57 MC3 Mediatek Dimensity 800U OpenGL ES 3.2 v1.r32p1-01eac0.461cd25a1c7796cc6d3ad05234c053ac 12 85 54 偶然にも:-) core数(MC)違いのGPUですが、core数が多いもの程いい結果となりました(それはそう)。 もう少し比較をするためにベンチマークスコアをcore数で割ってみます。 GPU T-Rex/core score Manhattan/core score ARM Mali G57 MC1 19 13 ARM Mali G57 MC2 28.5 18.5 ARM Mali G57 MC3 28.3 18 今回のAndroid端末のMC2, MC3のものは同じ傾向(同等MC1想定値からの比例関係)である事がわかります。 その値からすると今回のAndroid端末のMC1のものは控えめな値である事もわかります。 低スペック帯なのでそこまで周波数が高くない?等の理由で控えめなのかもしれません。 おわりに # 今回はGFXBenchの内容紹介と、前回ビルドしたGFXBenchを実際に実行してベンチマークスコアを確認しました。 次回は今回実行したAndroid端末よりも古いAndroidバージョンで実行したい場合の手順を取り上げます。 ライセンスおよび免責事項 # 本記事に掲載しているスクリーンショット、検証結果は、BSD 3-Clause Licenseのもとで公開されている Kishonti-Opensource/gfxbench のソフトウェアおよびアセットを利用・引用したものです。 Original Copyright: (c) 2005–2025 Kishonti Ltd. License: BSD 3-Clause License 画像等の権利について: 記事内で引用しているGFXBenchのベンチマーク実行画面およびUIの著作権は、原著作者であるKishonti Ltd.に帰属します。 【免責事項】 本記事に掲載している手順、ベンチマークスコア等の測定結果は、特定の検証環境における現状のまま(AS IS)のものであり、その正確性、安全性、再現性を保証するものではありません。 本情報の利用や検証の実行により生じた直接的・間接的な損害について、筆者および株式会社豆蔵は一切の責任を負いません。内容を十分にご確認の上、ご自身の責任においてご利用ください。
アジャイルグループの中村です。 生成AIカンファレンス2026 に参加しました。生成AIの社会実装をテーマに、産業・開発・知能の3つの視点から、AIエージェントやフィジカルAIなど幅広いテーマを扱うイベントです。 AIで速くなった、その先への不安 # 2026年に入り、AIエージェントによるコーディングの高速化は一気に現実味を増しました。自身も個人開発アプリのメンテでPoC程度に触れましたが、その程度の経験でも「10倍の生産性」といった表現が、以前ほど大げさには聞こえなくなりました。エンジニアとしては、素直に興奮します。 一方、アジャイルコーチとしての自分は、ふたつの不安を覚えました。 ひとつは、成果物を大量に作れるようになれば、テストからリリースまでの後工程が渋滞するのではないか。 もうひとつは、製造コストの制約が薄くなれば、「どんどん作って、後から考えよう」と要件定義が雑になるのではないか。 今回のカンファレンスでは、このふたつの懸念が国内の現場でどう現れているのかを確かめたいと考えていました。百聞は一見に如かず、です。 結論から言えば、懸念していた構造は、すでに現場の課題として現れていました。ただし、それを前提に仕事のあり方を変えようとする実践も、すでに始まっています。 どのセッションも、いわゆる「キラキラ事例」とは一線を画す、生々しく骨太な内容でした。 そのなかから、ふたつの不安にそれぞれ重なったセッションを紹介します。 不安その1:速くなった仕事は、後工程で渋滞するのではないのか # 『生成AIの現在地:最前線の研究を社会実装する』 # <Sakana AI株式会社 大村壮太氏> AIと科学をテーマに、AIによる研究活動の高速化と、その先で顕在化している課題が紹介されました。いくつか提示された課題の中で、いちばん印象に残ったのが次のキーワードです。 「頭と終わりは人間」 頭とは「問題空間の定義」です。 科学者としてのAIの性能は上がり続け、研究のループは高速化しました。一方で、AIの介入によって、科学の扱う問題空間そのものが狭まってしまう懸念もあります。 終わりとは「理解・解釈」です。 思考の一部をAIに任せられても、成果を理解し、意味を解釈する人間の仕事までは消えません。AIが成果を生み出す速度に、人間側の処理能力が追いつかなくなる。研究助成の審査や論文査読は、その具体例として挙げられていました。 また、「理解がないと、知識と創造が阻害される。フィードバックループがない」との言葉にも、深くうなずかされました。 ソフトウェア開発のレビューや検収に当てはめれば、開発リーダーや顧客に負荷が集中することは容易に想像できます。 不安その2:作るのが安くなったら、要件定義が雑になるのではないか # 『“PoC止まり”を超える、小売AXの最前線』 # <株式会社アンドエスティHD 甲斐裕樹氏 / 株式会社Algomatic 鴨居啓人氏> アパレル小売店舗の店長業務を支援する「店長AIエージェント」の事例です。 売上分析や在庫検索などをAIエージェントから利用できる仕組みで、在庫検索業務を75%削減したと紹介されていました。 「まず現場で使ってもらえるもの」 この言葉こそ、ふたつ目の不安に対するひとつの答えに見えました。 製造コストが下がったから、とりあえず作るのではない。誰の何を変えたいのかを先に置き、使ってもらいながら確かめる。作ることが容易になるほど、むしろ問題設定とフィードバックの質が重要になる。 そして驚いたのは、大企業へAIを浸透させていく実行力です。 店長の業務を変えるという明確なニーズから出発し、数店舗からスモールスタートする。現場のフィードバックに基づいて改善しながら対象店舗を増やし、スコープも広げていく。非IT部門も含む部門横断チームが方向性を示し、オーナーシップをもって主導する。 活字に書き出してしまうと、ユースケースや進め方に目新しさはありません。 当たり前のことをやり抜く。その積み重ねが、75%という数字につながったのだと受け止めました。 まとめ # 私なりに整理すると、問いはふたつです。 その仕事は正しいか。 顧客価値を正しく定義し、本当に取り組むべき問題を選べているか。 仕事は正しく行われているか。 一部だけを速くするのではなく、価値を届けるまでのフロー全体が流れているか。 生成AIによって仕事は速くなります。だからこそ、こうした昔からの問いに向き合うことが、より重要になると考えます。 要求工学を価値の柱としてきた弊社こそ、ここに出番があるのでは――と、小さくガッツポーズしつつ、このふたつの問いを自分自身の宿題としても持ち帰りたいと思います。
この記事を読んでいただきありがとうございます。アジャイルグループに所属する、藤井智弘です。 質問 #  うちのチームは既存システムの保守運用が中心です。不具合対応や改善要望への対応がメインで、大きな新機能をスプリントで計画的に作っていくような仕事ではありません。研修で習ったスクラムは新規開発が前提のように見えますし、うちは割り込みも多い。 うちにはスクラムは合わないのではないでしょうか? 回答 #  合わないのは、スクラムではなく「スクラム解説の前提」のほうです。視点を変えると、保守運用はアジャイルにとってかなり自然な環境です。ウォーターフォール文化が身に染み付いた開発チームは、「最初にすべてを決めない」という考え方に慣れるのに苦労します。一方、保守運用の世界では「最初からすべて決めない」のは“当たり前”です。  さらに言えば、保守運用が向いているのはスクラムの「仕事の管理」の側面だけではありません。スクラムが定義していない、しかしアジャイルに不可欠なもう半分——テストや設計、リリースといった“モノづくりの技術”——を鍛える場として、保守運用はおそらく最良の舞台です。本稿の後半はそこに重心を置きます。  まずは、合う合わないを拙速に結論づける前に、「新規開発と保守運用は、本当に別物なのか」から考え直してみましょう。 「新規開発」と「保守運用」は、本当に別物か #  この質問の土台には、「新規機能の開発」と「保守運用の不具合対応・改善要望対応」はまったく違う仕事だ、という感覚があります。  スクラムを前提に、まずはここを疑ってみます。以下、前者を「新規開発系」、後者を「保守系」と便宜上呼び分けます。  バックログの視点で両者を並べてみると、こうなります。 新規開発系: 作りたい機能が並ぶ → 価値やリスクで優先順位をつける → 小さく完成させて届ける → フィードバックを得て次を決める 保守系: 不具合報告や改善要望が流れ込む → 影響度や緊急度で優先順位をつける → 直して届ける → 利用者の反応や再発状況を見て次を決める   新規かどうかにかかわらず、価値のある作業項目が流入し、それらに優先順位をつけ、小さく完成させて届け、結果を見て次を判断する。一歩引いてみると、両者は構造的には同じです。スクラムが回そうとしているループそのものを、どちらの「系」も回しています。そしてどちらのバックログも、項目が後から流れ込むことを前提にしたリストです。全件が最初に揃っているべきというリストではありません。  では、何も違わないのかというと、差はあります。ただしそれは「種類の差」ではなく「程度の差」です。 項目のサイズと独立性 : 保守系の項目は小粒で、互いに独立していることが多い。新規開発系は大きく、項目間の依存が強くなりがち 流入の予測可能性 : 保守系は突発の流入が多く、来週何をやるかを今週決めきれない。新規開発系は(保守系と比べて)計画が立てやすい ステークホルダーの期待の形 : 新規開発系には「いつ、何が出るのか」が問われ、保守系には「どれだけ早く、確実に直るのか」が問われる  これらはスプリントの長さ、ゴールの立て方、キャパシティの配分といった「運用パラメータ(第1回参照)」を変える理由にはなりますが、「スクラムをやる/やらない」を分ける理由にはなりません。同じ考え方の上で、パラメータ=調整のしかたが違うだけです。 スクラムが定義していない「もう半分」 #  もう一歩進めます。ここまでの話は、バックログと優先順位づけ、つまりスクラムが定義する「仕事の管理」の側面でした。しかしスクラムガイドを読み返すと、気づくことがあります。 スクラムは「どう作るか」を一切定義していません。 テストをどう書くか、設計をどう保つか、どの頻度で統合しリリースするか…これらはスクラムの外にあります。  アジャイルのこの「もう半分」を担ってきたのが、XP(エクストリーム・プログラミング)に代表される構築プラクティスです。自動テスト(そしてテストを先に書いてから実装するテスト駆動開発)、リファクタリング(動きを変えずに内部構造を整えること)、継続的インテグレーション(変更を頻繁に統合し自動でビルド・テストすること)、小さなリリース、シンプルな設計。スクラムだけを導入して「スプリントごとに動くものが出てこない」「スピードが落ち続ける」と悩むチームの多くは、この半分が欠けています。管理の枠組みだけあって、その中で回すモノづくりの足腰がない状態です。 保守運用は、その「もう半分」を鍛える最良の舞台だ #  ウォーターフォール文化に染まった開発チームがアジャイルに移行するとき、最大の障害はたいてい「全部決めてから作りたい」という習慣(いや欲求)です。線表を引き、要件を固め、設計を固め、実装へ…この習慣を捨てるのに、多くのチームが何ヶ月も、ときに何年も苦しみます。  ところが保守運用の現場を見てください。  来月どんな不具合が報告されるか、誰も知りません。半年先の対応計画を精緻に立てることに意味がないことを、全員が体で知っています。つまり、 「最初にすべて決めない」状態が、意図せずすでに達成されている のです。開発チームが苦労して手放す習慣が、環境としてそもそも成立しません。  そして、ここからが本題です。保守運用の日常業務は、XPの技術プラクティスの練習問題そのものです。 自動テスト : 不具合を直すとき、「まず再現するテストを書き、それを通す」のは最も自然な手順です。直した箇所が次の改修で壊れないよう回帰テストの網を増やしていくことも、保守の現場では「当然のこと」として受け入れられます。多くの未熟なアジャイルプロジェクトで「テストはリリース間際にやればよい」という誤解(開きなおり?)が平然と通っているのとは大きな違いです。 リファクタリング : 保守系の変更は小粒です。「触ったところを少しだけきれいにして戻す」を毎回やる習慣が、大規模な作り直しをせずにコードを健全に保つ唯一の現実的な方法であり、保守運用は開発者に練習する機会を多く与えてくれます。 継続的インテグレーションと小さなリリース : 項目が独立しているため、1件ずつ統合し、1件ずつ届けられます。「小さく完成させて届ける」サイクルが短く、ビルド・テスト・デプロイの自動化に投資する動機も見返りも、新規開発より明確です。 完成の定義 (何をもって「終わった」とするかのチームの合意): スプリントごとに何度も「完成」を経験できるので、見積り・分割・「完成とは何か」の合意といった基本動作の練習回数を、新規開発案件よりはるかに多く稼げます。検査と適応のリズムも、障害の振り返りや再発防止という形で日常業務と地続きです。  このように見ていくと、「保守運用にスクラムは合わない」のではなく、むしろ「スクラムの管理の枠組みと、XP的なモノづくりの技術の両方を身につけるなら、保守運用は良い舞台である」という見方が、十分に成り立ちます。 でも限界も知っておこう #  ただし、限界もあります。保守運用で鍛えやすいのはモノづくりの側であって、スクラムの管理の側には練習しにくいことが2つあります。 スプリントゴールで仕事を「束ねる」力 。小粒で独立した項目をこなすだけなら、ゴールは「今スプリントの分を終わらせる」に退化しがちです。バラバラの作業を1つの目的に束ねる練習は、意識しないと発生しません。 価値の優先順位を巡る判断 。「直すのが当然」の不具合が相手では、「もたらす価値に基づいて何を作り、何を作らないか優先順位をつける」という、プロダクトオーナーの筋トレの機会が乏しくなります。  ほぼアジャイル初学者ばかりのチームが、いきなり本番開発に突撃して失敗している現場を少なからず見ていると、「まず保守運用でモノづくりの技術を身につける」→「新規開発でゴールやPOの筋トレに取り組みながら開発を進める」というアプローチは、十分に検討に値すると思います。  とりわけ日本では、この「モノづくりの技術」への投資が構造的に不足しがちです。経営層の中には、アジャイルを「早い、安い」という文脈で捉えている方が少なくありません。その結果、体制の編成は単価優先になり、育成への投資が省かれ、設計からコーディング、テストに至る“モノづくり”の能力がチームとして整わなくなります。スクラムの研修は受けたが、テストの自動化もリファクタリングも誰もやったことがない——そんなチームが「スクラムを始めた」と称して本番開発に突撃する光景は、珍しくありません。責めるべきは個々のエンジニアではなく、この編成と投資の意思決定です。  もっともこの意思決定も、コスト削減の制約の下では、その場その場では合理的な選択ではあります。合理的な選択の積み重ねが望まぬ結果に行き着く——この構造は、次回からの更改案件の話でも繰り返し登場します。  私見では、これがアジャイル開発の失敗の大きな要因のひとつです。保守運用を鍛錬の舞台にする提案は、この足腰を日常業務の中で鍛え直す提案でもあります。  経営層の方に、「早い、安い」の代わりに期待していただきたいのは、「変更1件のリードタイムが縮む」「切替やリリースの事故が減る」「見積の外れ幅が狭まる」という、測れる変化です。これらは、本連載の今後の回で採り上げようかと考えています。  そしてもちろん、開発チームを「価値の創造エンジン」という目で見ていただきたい。コストセンターとして単価を削る対象ではなく、 投資して育てる対象 として。 それでも現場で困ることへの処方 #  保守運用へ適用するとして、よく起こるだろう困りごとへの対処法もいくつか挙げておきましょう。 スプリントゴールが立てられない  前述したようにゴール設定はなかなか難しいかもしれません。スプリントを回し始めて数ヶ月のチームなら、ここでいっそのこと割り切って、ムリにゴール設定せず、まず「自動テストとリファクタリングを日常の手順に組み込む」といったモノづくりの基本動作に集中してもいいんじゃないかと思います。  もちろん、「ゴール決めを禁止する」意図はありません。ゴールを「機能のまとまり」で立てようとすると詰まります。改善・削減・安定化の観点で考えると立てやすくなるでしょう。「今月のアラート件数を半減する」「この手順書を廃止できる自動化を完成させる」「問い合わせ上位3件をFAQ化して問い合わせを減らす」。これらは立派なスプリントゴールであり、前述の「束ねる力」の練習にもなります。 割り込みで計画が壊れる  まず1〜2スプリント、割り込みの件数・時間・発生元を記録し、感覚ではなく実績の比率で「計画枠」と「割り込み枠」を分けてキャパシティを設計してください。詳しい運用(記録のとり方、当番制、受付の一本化など)は、第12回あたりでまとめて扱う予定です。 そもそもスプリントという区切りが実態に合わない  割り込みが恒常的に過半を占めるようなら、スクラムにこだわらず、カンバン(スプリントを区切らず、流れで仕事を管理する手法)の併用・移行を正面から検討してください。流れてくる仕事を順に引き取り、流れの速さと詰まりを改善するカンバンは、フロー中心の現場の実態に素直に合います。  スクラムは目的ではなく手段です。「スクラムをやめる」ことは「アジャイルをやめる」ことではありません。 なぜこの質問は30年繰り返されるのか #  理由は単純で、アジャイルの解説・研修・成功事例のほとんどが「新機能を開発するチーム」を暗黙の主人公にして書かれてきたからです。読者の多数派が保守運用に携わっているにもかかわらず、教材の主人公は常に新規開発チームでした。加えて日本では、新入社員研修でウォーターフォール型の開発はしっかり教えられる一方、アジャイルはほとんど扱われません。「教材がない」状態は、キャリアの入口から始まっているのです。  だから保守運用の実践者は、世代が替わるたびに「自分たちは例外なのではないか」「新しいアプローチに触れる機会が奪われているのではないか」という同じ不安を抱きます。  しかし本稿で見たように、例外なのは現場ではなく教材の前提のほうです。「先が読めない中で、小さく完成させ、学びながら進む」というスクラムの舞台装置は整っている。そのうえ、スクラムが定義しないモノづくりの技術を鍛える練習問題が、毎日流れ込んでくる。  保守運用は アジャイルの周縁ではなく 、むしろ その足腰を作る場所 だと考えてはいかがでしょうか? 次回: 「アップグレード案件の『現行機能保証』にどう向き合う?」
連載をはじめるにあたって #  この記事を読んでいただきありがとうございます。アジャイルグループに所属する、藤井智弘です。  「豆蔵最後のアルバイト」という謎の肩書でお客様を煙に巻きながら、日々研修やご支援に従事している中で、同じような質問を何年にも渡って繰り返しいただいてきました。アジャイル開発が世に出て30年近い年月が経ち、実践者の世代交代が進んでいるので、かつて議論し尽くされた(かに見えた)質問が、打者一巡よろしく繰り返し話題にのぼるのも当然とも思います。  そうしたよくいただく質問を起点に、その背景にある構造的な問題や現場で使える対処法を、FAQの形で明文化しておこう、というのが、これから始める連載の趣旨です。  対象となる読者像は、以下のように設定しました。 研修や専門書での学習によって、「スプリントってなに?」という基本的な用語レベルは知っている 新年度になってスプリントを回し始めて数ヶ月、「これでいいんだっけ?」という引っかかりが溜まりはじめた 基礎を学んだ方がもう一歩…「知っているのに、現場でうまくいかないのはなぜか」…掘り下げて考える機会を作ろうということがねらいです。  実際にいただいた質問を起点にしているので、タイトルを見ると「開発者視点」と思われそうですが、「アジャイルに関わる人であれば知っておいたほうが良い内容」を意識しました。ですので、チームを率いるリーダーや、発注側・経営側としてスクラムのチームと向き合う立場の方もお付き合いください。  本題に入る前に、お断りを3ついれさせていただきます。 「正解」ではなく「選択肢」。 …課題への対処法は、プロジェクトが置かれた文脈(コンテキスト)に応じて、異なるアプローチを採ることも珍しくありません。本稿でも課題への対処法を提示することがありますが、それらは「正解」というよりも、「皆さんが採りうる選択肢」と理解してください。他の書籍やブログで提案されているアプローチとは異なることもあるでしょうが、「選択肢が増えた」くらいに捉えていただき、ご自身のチームの文脈で何をすればいいかを考えるきっかけとしてください。 「AIツールの具体的な使い方」の開設ではありません。 …昨今開発現場でAI利用に関する関心が高まっていますが、本連載でAIツールの具体的な使い方を解説することはありません。きっと別の人が別の記事で採り上げてくれるでしょう。 餅は餅屋、法的なことは専門家にまかせる。 …請負契約等の法的・契約実務上の論点も守備範囲外とさせていただきます。  少々前置きが長くなりました。初回のFAQには、いままさに研修や導入支援の場で受けることが増えている、この時代ならではの質問を選びました。 質問 #  AIがコードを書き、テストを書き、設計案まで出してくれる時代です。開発のスピードは桁違いに上がりました。正直なところ、いまさらスクラムの基礎を学ぶ意味はあるのでしょうか? スプリントで区切って人間が見積りや振り返りをするのは、AIのスピードの前では、“儀式”に見えます。 回答 #  学ぶ意味はあるし、重要性はむしろ増しているとさえ思います。なぜなら、スクラムが管理しているのは「作るスピード」ではなく「進むべき方向」だからです。  AIが劇的に速くしたのは「どのように作るか」であって、「何を作るか」「作った結果から何を学ぶか」は人が主体的に行う活動であり、AIが助けにはなっても、AIに丸投げすることはできません。作るスピードが上がるほど、間違ったものも速く作れるようになる——この単純な事実が、検査と適応(作ったものとやり方を頻繁に点検し、次を調整し続けること)という地味な基礎活動の価値を、かつてなく高めています。 AIが速くしたもの、しなかったもの #  まず、冷静に仕分けをしましょう。生成AIが確かに変えたのは、実装・テストコード作成・定型設計・調査といった「どのように作るか」の領域です。ここが数倍(いやもっとかな?)速くなったという主張に異論はありませんし、私自身実感してもいます。  では、次の問いはどうでしょうか。 この機能は誰のどんな業務課題を解決するのか。 リリースした結果、狙った効果は出たのか。 出なかったとして、次に何を変えるのか。 チームの今のやり方は機能しているか…。  これらは、人が主体的に意思決定するテーマであり、AIは答えを持っていません。もっともらしい答えの文章は生成してくれますが、その検証責任は人に残ります。そしてお気づきの通り、「何を作るか」「作った結果から何を学ぶか」「チームでどう検査と適応を回すか」は、アジャイルがこの30年間一貫して問うてきたことそのものです。AIは、アジャイルが担ってきたこれらの問いを何ひとつ引き受けてくれていない。むしろ、問いに向き合わないまま作れてしまうスピードだけが上がったのです(少なくともこれまでのところは)。 “AIいいなり開発”という新しくて古い病気 #  その帰結として、現場で目にするようになった光景があります。AIの生成物を、十分な検証もせず、その意図を理解もせずただ丸呑みして積み上げていく開発…「それじゃぁ『 AIいいなり開発(AI-Dictated-Development) 』じゃないか!」と叫びたくなる姿です。本来の「 AI駆動開発(AI-Driven-Development) 」とは似て非なるものです。 イラスト左:AI駆動   イラスト右:AIいいなり  AIいいなり開発の特徴は、一つひとつの成果物がもっともらしいことです。コードは動き、テストは通り、ドキュメントは整っている。問題は、その全体が誰の何のために作られたのか、誰も満足に説明できないことです。検証されていない仮定の上に、検証されていない生成物が高速に積み上がっていく。従来なら数ヶ月かかった「間違った方向への行軍」が、数週間で再現できるようになりました。  しかし、これは新しい病気ではありません。「要件定義書に書いてあったから作りました」「言われた通りに作りました」——意図を問わない開発は昔からありました。AIはそれを高速化し、かつ「作った感」で覆い隠すのがうまくなっただけです。  診断が同じなら、処方も同じ系統です。 スクラムの基礎は、AIの出力に対してこそ効く #  この連載で扱う基礎を、AIの文脈に置き直してみます。 宛先と目的を問う習慣 は、「このコードは誰の何のためか」(fig002)をAIの出力に対しても問わせます。生成物がもっともらしいほど、この問いだけが人の仕事として残ります 小さく完成させて検査するリズム は、生成されたものを鵜呑みにせず、動かして・使わせて・学ぶ機会を強制的に確保します。スプリントは「人間が遅いから区切る」のではなく、「学習のリズムを設ける」ための仕組みなので、生成が速くなっても不要にはなりません。むしろ検証すべき生成物が増えた分、検査のリズムの価値は上がります 計測と記録の規律 は、「AIで速くなった気がする」を検証可能な事実に変えます。リードタイム(着手から届くまでの時間)は本当に縮んだのか、手戻りは増えていないか。道具の評価もまた、検査と適応の対象です  逆に、変わってよいものもあります。スプリントの長さ、見積りのやり方、ドキュメントの作り方…これらはスクラムの進行を調整する“運用パラメータ”であり、AIによって最適値が変わり得ます(この「基本」と「運用パラメータ」の区別は、次回以降の各論でも繰り返し登場します)。生成が速いなら、スプリントはもっと短くできるかもしれない。見積りの議論は、作って確かめる方が速い場面が増えるかもしれない。パラメータは大胆に見直してください。この技術の大変革の時代に、「批判精神なき前例踏襲は、“罪”」です。 これらパラメータの変更もチームで議論し合意し、その効果を検査する対象です。周囲を無視して現場で好き勝手やっていい話ではありません。  そして、ここまで説明した検査と適応のループそのものを外した瞬間、残るのは「高速ないいなり」です。 なぜ、新しい技術が生まれるたびに、似たような質問が繰り返されるのか #  「新しい道具が来たから、プロセスの基礎はもう不要では?」という問いは、実はAIが初めてではありません。CASEツール(設計図からコードを自動生成する1980〜90年代のツール群)が自動生成する、RAD(試作を繰り返して短期間で作る1990年代の開発手法)で開発が桁違いに速くなる、ローコードでプログラマーが不要になる…新しい道具が現れるたびに、同じ問いが再演されてきました。打者一巡どころか、これはもう何巡目かの打席です。  そして毎回、結論も同じでした。道具は「どのように作るか」を変え、ときに劇的に改善する。しかし「何を作るべきか」「作った結果から何を学ぶか」は道具の外に残り続け、そこを疎かにしたプロジェクトは、速い道具で速く失敗した…。  AIが過去の道具と規模の違う変化であることは認めます。だからこそ、と言うべきでしょう。道具が強力になるほど、ハンドルを握る側の基礎が問われます。本連載がこれから扱うのは、そのハンドルの握り方です。 今後の予定 #  次回以降の予定ですが、タイトルと順序を以下に列挙しておきます。 ただ、これらは仮のもので、断りなく変更される可能性があることをご承知おきください。 第2回: 保守運用チームにスクラムは合わないのでは? 第3回: アップグレード案件の「現行機能保証」にどう向き合う? 第4回: 更改案件の見積を、全機能を確定せずにどう出す? 第5回: ドキュメントが信用できない現行システム、何を『仕様』にして検証する? 第6回: デイリースクラムが進捗報告会になっています 第7回: レトロスペクティブが機能しません…同じ改善案の再放送と、ToDoの列挙会 第8回: スプリントレビューにステークホルダーが来なくなりました 第9回: ストーリーポイントの人日換算を上司に求められます 第10回: ベロシティが安定しません。計画に使っていいの? 第11回: スプリント内に終わらないタスクが毎回出ます 第12回: 割り込み作業はどう計画に入れる? 第13回: POが多忙でチームに来ません 第14回: スクラムマスターは何をする人? 暇そうに見えます 第15回: 自己組織化と言われても、結局リーダーが全部決めています 第16回: 技術的負債の返済をPOが優先してくれません 第17回: ドキュメントはどこまで書けばいい? 第18回: 最初は改善が進んだのに、最近チームが停滞している気がします 第19回(最終回): 連載の総括  長い文章にお付き合いいただきありがとうございました。 次回: 「保守運用チームにスクラムは合わないのでは?」
はじめに # 大まかな構成は把握していても、細かい実装には手が回らずぼんやりとしか理解してなかったので、勉強を兼ねてRAG(Retrieval-Augmented Generation)を実装してみました。 今回は、RAGの核となる「ベクトル化」「類似度検索」「コンテキスト注入」を理解することを目的に、フレームワークやVectorDBをあえて使わず、最小構成で動かしてみます。 これらは実運用ではライブラリやVectorDBに置き換える選択肢がありますが、まずは本質を素の形で体感します。 そのうえで、ミステリーというわかりやすい題材を使って、RAGの動作をイメージしやすい形で見ていきます。 本記事で使用している用語の定義 Instruction: 役割や振る舞いの指定 ドキュメント: 原文である文書のこと プロンプト: 質問や依頼文の原文 クエリ:指すモノはプロンプトと同じですが、検索に使用する質問を指す場合、呼び変えています チャンク: 原文をもとに分割された状態 コンテキスト: LLMに渡す背景情報 本記事で使用している図、小説 Geminiで生成したものを用いています。 技術スタック # node 26 Google AI Studio gemini-embedding-2(Embeddingモデル) gemini-3.5-flash-lite(テキスト出力モデル) typescript 7 @google/genai 2.17.1 --> Google AI Studio 無料枠でも十分に試せて、導入ハードルが低いことから、今回利用しました。 事前準備として Google AI Studio の無料枠を使ってAPI Keyを取得します。 無料枠はクレジットカード登録不要で試せます。 サンプルアプリ # まずRAGの基本構造を見えるようにするため、今回のサンプルではあえてフレームワークやVectorDBを使わずに、最小構成で動かしてみました。 このアプリでは、RAGの本質である「ドキュメントをどのように扱うか」「どこを類似検索するか」「どの情報をLLMに渡すか」を、最小限のコードで確認できます。 本記事で掲載しているコードは こちら で公開しています。 スペック # Instruction 空行で分割する。 起動後、ユーザーに選択を促し、選択した内容をLLMに渡すコンテキストの一部に含める。 ドキュメント 空行で分割する。 プロンプト 空行で分割する 起動後、ユーザーに選択を促し、選択した内容をLLMに渡すコンテキストの一部に含める。 類似度検索は、Top1のスコアを基準に相対評価で決定する。 処理フロー # 今回作成したアプリの処理フローは下図の通りです。 sequenceDiagram autonumber actor cli participant app as 今回のアプリ participant llm as Google AI Studio cli --> app: 起動 app -->> cli: Instructionの選択確認 cli --> app: Instruction選択 app --> llm: ドキュメントの組み込み llm -->> app: ベクトル化されたドキュメント app -->> cli: Promptの選択確認 cli --> app: Prompt選択 app --> llm: プロンプトの組み込み llm -->> app: ベクトル化されたプロンプト app --> app: コサイン類似度計算 app --> app: 類似度検索 app --> llm: 回答生成 llm -->> app: 回答 app -->> cli: 回答 ベクトル化(Embedding) # 自然言語や画像などを人が使うデータを、AIが理解できるように意味を表す数値のリスト(ベクトル)に変換する処理です。 LLMやEmbeddingモデルは、文字をトークン単位で処理し、学習を通じて言葉どうしの関係を扱います。 ただし、文章どうしの意味の近さを検索で直接比較するには、数値として扱える形にする必要があります。 たとえば「有休」と「有給休暇」のような近い意味の言葉を、近い位置に配置するのがEmbeddingです。 そこで、テキストをEmbeddingモデルを使って「意味の位置関係を表す数字のリスト」に変換します。これがベクトル化です。 具体例で考える(イメージ)たとえば言葉を「ジャンル」「フォーマット」などの軸(次元)で数値化してみます。 言葉 軸①:仕事っぽさ 軸②:休みっぽさ ベクトル(座標) 有給休暇 0.9 0.9 [0.9, 0.9] 有休 0.9 0.9 [0.9, 0.9] リモートワーク 0.8 0.2 [0.8, 0.2] ラーメン 0.0 0.0 [0.0, 0.0] ※Embeddingモデルは、この軸を膨大な数(今回は3072個)使って、複雑な言葉の意味を「数字のリスト」として表現しています。 ベクトル化することで、文字がまったく違っても「有給休暇」と「有休」はグラフ上で近い位置にあるということをAIが理解できるようになります。 const vectorizedDoc: number[][] = await embedAndVectorizedContents(LLM_MODEL_EMBEDDED, documents); async function embedContent(model: string, content: string): Promise<EmbedContentResponse> { // モデルに渡したcontentsが数値化されて返される return await ai.models.embedContent({ model, contents: content }); } async function embedAndVectorizedContent(model: string, content: string): Promise<number[]> { // モデルから返された値を後続のコサイン類似度算出で使いやすいように数値配列に変換 return extractValues(await embedContent(model, content)); } async function embedAndVectorizedContents(model: string, contents: string[]): Promise<number[][]> { const res = await Promise.all( contents.map(async (doc) => { return await embedAndVectorizedContent(model, doc); }), ); return res; } --> Gemini Embedding 2のクエリとドキュメント指定について 検索用途の場合、クエリとドキュメントを役割に応じた形式でEmbeddingすることが推奨されています。 たとえば、クエリは task: question answering | query: ... 、ドキュメントは title: ... | text: ... と付与します。 サンプルでは、ベクトル化とコサイン類似度の仕組みに焦点を絞るため、クエリとドキュメントを同じ形式でEmbeddingしています。 実運用では 公式ドキュメント に沿って用途別の形式を使うことで、検索精度の改善が期待できます。 コサイン類似度の考え方(Cosine Similarity) # 2つの文章(ベクトル)の「意味がどれくらい似ているか」を0〜1(または -1 〜1)の数値で判定すること。 テキストがすべて「数字のリスト(矢印)」になったので、プロンプトのベクトルと、ドキュメント側のベクトルの「向きの近さ」を計算できます。 なぜ「距離」ではなく「向き(角度)」なのか 距離で比べた場合: 長文の「有休についての詳しい説明」と、短文の「有休はいつ」というプロンプトは、文章の長さに対応するベクトルの大きさも含めて比べるため、「離れている」と判定される場合があります。 角度で比べた場合: 長さが違っても、テーマが同じなら矢印の向いている「方向」がほぼ一緒になります。 コサイン類似度は、ベクトルの大きさの影響を除き、向きだけを比べる計算です。 1.0に近ければ意味が近く、0.0に近ければ関係ないコンテキストと言えます。 function cosineSimilarity(v1: number[], v2: number[]): number { const dotProduct = v1.reduce((sum, a, idx) => sum + a * v2[idx], 0); const mag1 = Math.sqrt(v1.reduce((sum, a) => sum + a * a, 0)); const mag2 = Math.sqrt(v2.reduce((sum, b) => sum + b * b, 0)); if (!mag1 || !mag2) return 0; return dotProduct / (mag1 * mag2); } 類似度検索で関連情報を絞り込む(Vector Search) # プロンプトのベクトルとドキュメントのベクトルを「コサイン類似度」で比較し、関連度の高いテキストを抽出する処理です。 プロンプトに近しいドキュメントを使って回答生成するため、チャンク粒度によっては、複数のチャンクを扱う必要があります。 「単純にTop n固定で取得する」と、関係のない情報(ノイズ)までLLMに渡してしまい、ハルシネーション(誤情報)や精度の低下に繋がります。 今回は「Top 1のスコアに対する閾値を設けて、その範囲内のn件を取得する」方法でコンテキストを特定する方法を採りました。 これにより、**「関連する情報が複数ある時はまとめて取得し、関連情報が1つしかない時はムダなノイズを混ぜない」**制御ができました。 function selectRelevantContents( scoredContents: ScoredContent[], topN: number, relativeScoreMargin: number, ): ScoredContent[] { const topContents = [...scoredContents] .sort((a, b) => b.score - a.score) .slice(0, topN); // Top1のスコアから相対的にみて関連の深いドキュメントを抽出 const filteredContents = topContents .filter((item) => item.score >= topContents[0].score - relativeScoreMargin); return filteredContents; } 回答生成 # 抽出したテキストを「コンテキスト(背景情報)」としてプロンプトに埋め込み、LLMに回答させる処理です。 const prompt = ` 以下のInstructionおよびコンテキストに基づいて回答してください。 Instruction: ${instruction} コンテキスト(背景情報): ${retrievedContext} 質問: ${query}`; // LLMにコンテキストと質問を投げて、回答を生成 const response = await ai.models.generateContent({ model: LLM_MODEL_GENERATED, contents: prompt, }); console.log(`【LLMの回答】:\n${response.text}`); ミステリーを使ってRAGの挙動を確認してみる # ここからは、サンプルアプリで解説した内容をミステリー小説を題材にして確認します。 RAGのポイントは「何を選ぶか」と「どの情報を渡すか」であり、ミステリーはこの検証に適した題材です。 ミステリー小説(ドキュメント) # 第1章:吹雪の夜 標高1,500メートルに位置する観測所「雪山荘」。12月24日夜、猛烈な吹雪により館は完全な孤立状態となった。当時館内にいたのは、館主の神楽坂、助手の有馬、研究員の黒田、家政婦の白石の4名のみである。 第2章:開かずの密室 翌朝8時、敷地内の鐘楼2階で神楽坂が遺体となって発見された。現場の扉は内側から重厚なボルトで施錠されており、窓もすべて内側から木枠が打ち付けられていた。室内に鍵はなく、神楽坂のポケットに入っていた1本のマスターキーが唯一の鍵だった。 第3章:証言とアリバイ 事件当夜の午前0時(推定死亡時刻)、全員の行動ログが残されている。有馬は1階の無線室で外部との交信を試みており、その通信記録はログサーバーに刻まれていた。家政婦の白石は食堂でずっと明日の朝食の仕込みをしており、防犯カメラにその姿が映っていた。一方、黒田は「自分の部屋で寝ていた」と主張するが、彼を証明する客観的な証拠は何もない。 第4章:現場に残された違和感 鐘楼2階の窓枠には、極めて細い「ピアノ線」の摩擦痕と、わずかな「氷の破片」が残されていた。また、鐘楼の外壁を伝う外排水管には、上から吊り下げられたような擦れ傷が存在していた。 第5章:決定的な証拠 警察の鑑識により、外排水管の擦れ傷から黒田のコートの繊維が検出された。また、黒田の自室のゴミ箱からは、細かく切断された大量のピアノ線の端切れと、外からの吹雪で削られた氷を固めるために使われた保温ボトルの残痕が発見された。これにより、黒田が氷で作った重りとピアノ線、排水管を利用し、屋外から窓の木枠越しに内側のボルトを閉めて密室を作り上げたトリックと、彼が犯人であることが確定した。 探偵の設定(Instruction) # 探偵1人目 探偵の検埼索斗 口調が軽い感じの探偵です。 探偵2人目 ロンドン出身の調査ジャーナリストのEleanor Reed 「Case noted. Moving on.」が決まり文句。 3人目 新米刑事の小室慎一 推理はまだまだだけど、好感の持てるキャラクターです。 実践:質問と回答 # 同じミステリーでも、渡すコンテキストの数や、Instructionの違いによって回答の質や方向性が変わります。 ここでは、その変化を具体例で確認します。 1. 単一コンテキストを使う必要がある質問 # コンテキストを使って回答できるか検証します。 Instruction: 探偵の検埼索斗 質問: 怪しい人物は LLMの回答とコンテキスト利用ログ 実行時、どのように数値化され、どのチャンクを選択したのか見えるように debug info として参考掲載しています。 やあ、検埼索斗だ。ちょいと今回の事件のログを洗わせてもらったぜ。 推理を進めると、状況はこうだな。 まず、有馬は1階の無線室で外部と交信していて、その通信記録がしっかりログに残ってる。お次は家政婦の白石さん、彼女は食堂で朝食の仕込みをしていて、防犯カメラにバッチリその姿が映っていた。 さて、問題はもう一人の黒田クンさ。「自分の部屋で寝てた」なんて主張してるけど、それを裏付ける客観的な証拠は――ゼロ、ナシ、何もなし、と。 つまり、現時点で**アリバイがない人物は黒田**だな。いやはや、怪しい影がプンプンするじゃねえか。 --- debug info --- embedding model: gemini-embedding-2 generated model: gemini-3.5-flash-lite retrieval: topN=2, top1 score margin=0.05 instruction: resources/novel.inspection.txt document: C:\...\developer-site-article-examples\rag-sample\resources\novel.context.txt prompt: C:\...\developer-site-article-examples\rag-sample\resources\novel.prompt.txt query: アリバイがない人物は誰ですか? chunk scores: 1. 第3章:証言とアリバイ: score=0.7098 selected 2. 第1章:吹雪の夜: score=0.6258 excluded 3. 第5章:決定的な証拠: score=0.6219 excluded 4. 第4章:現場に残された違和感: score=0.5941 excluded 5. 第2章:開かずの密室: score=0.5929 excluded アリバイのない「黒田」をコンテキストから導き出して、しっかり回答してくれました。 2. 複数コンテキストを使う必要がある質問 # 関連するコンテキストを使って回答できるか検証します。 Instruction: 探偵の検埼索斗 質問: 密室トリックを解き明かす(伏線が存在する) LLMの回答とコンテキスト利用ログ やあ、検埼索斗だ。ちょいと軽い感じに見えるかもしれないが、事件の推理にかけては任せてくれよな。 さて、今回の「開かずの密室」のからくりさっきまでじっくり洗い出してみたんだけどさ、なかなかよくできたトリックだったわけよ。じゃあ、証拠に基づいてサクッと解説してやるよ。 ヒントと答えをまとめると、ポイントは**「氷」「ピアノ線」「外排水管」**の3つだ。 1. **氷の重り** 外からの吹雪で削られた氷を固めて「重り」を作ったんだ。これがトリックのキモになる部分ね。 2. **ピアノ線** 黒田の部屋のゴミ箱から、細かく切断された大量のピアノ線の端切れが見つかっている。こいつが遠隔操作の糸代わりってわけだ。 3. **外排水管と窓の木枠** 外排水管からは黒田のコートの繊維がバッチリ検出されてる。犯人は屋外から窓の木枠越しにピアノ線と氷の重りを使って、内側のボルトを操作したのさ。 つまりどういうことか? 犯人は、外からピアノ線と氷で作った重りを使って巧妙に内側のボルトを閉め、完璧な密室を作り上げたってわけ。ちなみに、使った氷の重りは時間が経てば勝手に溶けて証拠隠滅できるって寸法よ。いやはや、頭は使うもんだよねぇ。 ま、そういうわけで、今回の事件もスッキリ解決って感じだな! --- debug info --- embedding model: gemini-embedding-2 generated model: gemini-3.5-flash-lite retrieval: topN=2, top1 score margin=0.05 instruction: resources/novel.inspection.txt document: C:\...\developer-site-article-examples\rag-sample\resources\novel.context.txt prompt: C:\...\developer-site-article-examples\rag-sample\resources\novel.prompt.txt query: 密室はどのように作られましたか?仕掛けのヒントを教えてください。 chunk scores: 1. 第5章:決定的な証拠: score=0.7352 selected 2. 第2章:開かずの密室: score=0.7212 selected 3. 第4章:現場に残された違和感: score=0.6732 excluded 4. 第3章:証言とアリバイ: score=0.6551 excluded 5. 第1章:吹雪の夜: score=0.6432 excluded 関連性が高いとして導出された2, 5章の内容をもとに回答が返ってきました。 使い古されたようなトリックですが、しっかり回答してくれました。 3. コンテキストの存在しない質問 # コンテキストが存在しない質問をして、ハルシネーションの発生を検証します。 Instruction: 探偵の検埼索斗 質問: 犯人の動機は(情報が一切ない = RAGのガードレール検証) LLMの回答とコンテキスト利用ログ やあ、検埼索斗だ。軽そうに見えて頭のキレは抜群って評判の、あの探偵さ。 さて、今回の事件、なかなか厄介な「開かずの密室」だな。鐘楼の2階で神楽坂さんが遺体で見つかって、扉も窓も内側からがっちり施錠されていた。部屋には鍵がなくて、神楽坂さんのポケットにあったマスターキーが唯一の鍵だったってわけだ。 ……で、君からの質問は「犯人が神楽坂さんを殺害した『動機』は何ですか?」だな。 ふむ。証拠(コンテキスト)をよーく読んでみたんだけどさ……。 実はこの提供された情報の中には、**犯人がなぜ神楽坂さんを殺害したのかという「動機」については一言も書かれていない**んだよね。 わかるのは、現場が標高1,500メートルの孤立した「雪山荘」で、容疑者が助手の有馬、研究員の黒田、家政婦の白石の3人に絞られているってことと、密室のトリックの状況証拠くらいだ。 動機についてのピースがまだ揃ってない。これじゃあ、いくら名探偵の俺でも推理のしようがないってわけ! もう少し事件の背景や人間関係がわかる証拠を持ってきてくれよ。そしたらこの華麗な頭脳で、犯人の下心まで丸裸にしてあげるからさ! --- debug info --- embedding model: gemini-embedding-2 generated model: gemini-3.5-flash-lite retrieval: topN=2, top1 score margin=0.05 instruction: resources/novel.inspection.txt document: C:\...\developer-site-article-examples\rag-sample\resources\novel.context.txt prompt: C:\...\developer-site-article-examples\rag-sample\resources\novel.prompt.txt query: 犯人が神楽坂さんを殺害した『動機』は何ですか? chunk scores: 1. 第2章:開かずの密室: score=0.7569 selected 2. 第1章:吹雪の夜: score=0.7171 selected 3. 第5章:決定的な証拠: score=0.6835 excluded 4. 第3章:証言とアリバイ: score=0.6819 excluded 5. 第4章:現場に残された違和感: score=0.6435 excluded 今回の小説には「動機」を書いていないため、分からないと回答していますね。 ハルシネーションが防げたように見えますが、これは「証拠以外から推測しないよう指定したInstruction」が効いているのではないかと思われます。 RAGは関連性の低いチャンクを取得する可能性もあるため、根拠のない回答を完全には防げません。 実運用では最低スコアの検索結果棄却や、根拠不足時の回答方針も考慮する必要がありそうです。 コンテキストに関する文章を外して再検証 LLMに渡すコンテキストから、コンテキストを読むように言及する記述を除去してハルシネーションが発生するか検証します。 LLMの回答とコンテキスト利用ログ ふむ……事件の真相に迫る重要な質問だな。 だが、提示されたデータ(コンテキスト)を隅々まで検証したが、神楽坂が殺害された具体的な**『動機』**に関する記述はどこにも見当たらなかった。 分かるのは以下の客観的事実だけだ: * 犯行現場は完全な密室(内側からのボルト施錠と木枠の打ち付け)。 * 被害者である神楽坂のポケットから発見されたマスターキーが唯一の鍵。 * 容疑者は館内にいた有馬、黒田、白石の3名に絞られている。 動機を暴くには、彼らの人間関係や過去の秘密、あるいは遺留品のさらなる調査が必要不可欠だな。……まだ見えていないピースを探すとするか。 --- debug info --- embedding model: gemini-embedding-2 generated model: gemini-3.5-flash-lite retrieval: topN=2, top1 score margin=0.05 instruction: resources/novel.inspection.txt document: C:\...\developer-site-article-examples\rag-sample\resources\novel.context.txt prompt: C:\...\developer-site-article-examples\rag-sample\resources\novel.prompt.txt query: 犯人が神楽坂さんを殺害した『動機』は何ですか? chunk scores: 1. 第2章:開かずの密室: score=0.7569 selected 2. 第1章:吹雪の夜: score=0.7171 selected 3. 第5章:決定的な証拠: score=0.6835 excluded 4. 第3章:証言とアリバイ: score=0.6819 excluded 5. 第4章:現場に残された違和感: score=0.6435 excluded 今回はInstructionおよび共通プロンプトからコンテキスト参照の指示を外した状態で複数回実行しても、動機を創作する回答は発生しませんでした。 モデルが、与えられた情報に動機がないことを認識して回答を保留したと考えられます。 今回確認した範囲では発生しなかっただけであり、ハルシネーションが発生しないことを保証できるわけではありません。 4. 同じ条件で再度質問してみる # 同じInstructionおよび質問を指定した挙動を確認してみます。 Instruction: 探偵の検埼索斗 質問: 密室トリックを解き明かす(伏線が存在する) LLMの回答とコンテキスト利用ログ やあ、検埼索斗だ。ちょいと軽い感じに見えるかもしれないが、謎解きにかけては一級品だぜ。それじゃあ、今回の事件の「開かずの密室」のからくりをサクッと暴いていこうか。 証拠品を並べて論理的に組み立てれば、答えは一つしかないのさ。仕掛けのヒントは以下の通りだ。 1. **氷の重りとピアノ線** 外からの吹雪で削られた氷を固めて「重り」を作り、それに「ピアノ線」を結びつけて使っていたのさ。細かく切断されたピアノ線の端切れや、保温ボトルの残痕が黒田の部屋からしっかり見つかっている。 2. **外排水管と窓の木枠** 外排水管には黒田のコートの繊維がしっかり残っていた。犯人は屋外から、この排水管を利用して窓の木枠越しにアプローチしたんだ。 3. **トリックの全貌** つまりこういうことだ。犯人は**「氷で作った重りとピアノ線、そして外排水管」**を巧妙に利用して、屋外にいながら窓の木枠越しに内側のボルトを閉めたのさ。時間が経てば氷の重りは自然に溶けて証拠も消える……はずが、鑑識の執念と俺の推理の前には隠し通せなかったってわけだ。 完璧なロジックだろ? 事件の真相は、いつだって証拠のなかに眠っているのさ。さて、次の依頼はどこかな? --- debug info --- embedding model: gemini-embedding-2 generated model: gemini-3.5-flash-lite retrieval: topN=2, top1 score margin=0.05 instruction: resources/novel.inspection.txt document: C:\...\developer-site-article-examples\rag-sample\resources\novel.context.txt prompt: C:\...\developer-site-article-examples\rag-sample\resources\novel.prompt.txt query: 密室はどのように作られましたか?仕掛けのヒントを教えてください。 chunk scores: 1. 第5章:決定的な証拠: score=0.7352 selected 2. 第2章:開かずの密室: score=0.7212 selected 3. 第4章:現場に残された違和感: score=0.6732 excluded 4. 第3章:証言とアリバイ: score=0.6551 excluded 5. 第1章:吹雪の夜: score=0.6432 excluded 口調は変わってますが、内容は概ね同じでした。 ただし、この回答に含まれている「氷の重りは自然に溶けて証拠も消える」は、実は小説本文にはない補足です。 根拠に基づく回答を厳密に求める場合は、事実と推測を区別する必要があります。 これも厳密にいえばハルシネーションに該当するのでしょうね。 5. Instructionによる変化を確認 # 今回のサンプルアプリではドキュメントとは別にInstructionを作成し、検索したコンテキストとあわせてLLMに渡しているので、これによる変化を確認してみます。 ロンドン出身の調査ジャーナリストのEleanor Reed LLMの回答とコンテキスト利用ログ Based on the evidence provided in the case files, here is the factual verification regarding the construction of the locked room, clearly separated from any speculation. ### Facts (Based on Evidence) * **The Mechanism:** The suspect, Kuroda, utilized an ice-weight, piano wire, and an exterior drainage pipe to manipulate and secure the heavy interior bolt from the outside through the window frame. * **Physical Evidence (Drainage Pipe):** Forensic analysis by the police detected fibers from Kuroda's coat on the abrasions of the exterior drainage pipe, placing him in direct physical contact with the mechanism used. * **Physical Evidence (Kuroda's Room):** A search of Kuroda's personal waste bin yielded a large quantity of finely cut piano wire ends, alongside residue from a thermal bottle. The latter was used to compact ice, which served as the temporary weight for the trick before melting away. * **Resulting Illusion:** Through this setup, the room—featuring a door bolted from the inside and windows secured by nailed wooden frames—was successfully converted into a closed room ("locked room") from the outside. ### Speculation * Any hypotheses regarding Kuroda's exact motive, his precise psychological state during the execution of the crime, or the timeline of events prior to the discovery of Kagurazaka's body at 8:00 AM remain outside the direct scope of the current forensic findings. Case noted. Moving on. --- debug info --- embedding model: gemini-embedding-2 generated model: gemini-3.5-flash-lite retrieval: topN=2, top1 score margin=0.05 instruction: resources/novel.inspection.txt document: C:\...\developer-site-article-examples\rag-sample\resources\novel.context.txt prompt: C:\...\developer-site-article-examples\rag-sample\resources\novel.prompt.txt query: 密室はどのように作られましたか?仕掛けのヒントを教えてください。 chunk scores: 1. 第5章:決定的な証拠: score=0.7352 selected 2. 第2章:開かずの密室: score=0.7212 selected 3. 第4章:現場に残された違和感: score=0.6732 excluded 4. 第3章:証言とアリバイ: score=0.6551 excluded 5. 第1章:吹雪の夜: score=0.6432 excluded Instruction通り、英語で整然と回答してくれました。 新米刑事の小室慎一 LLMの回答とコンテキスト利用ログ あ、あの……!事件のことで頭がいっぱいで、つい考え込んじゃっていました。僕、新米の小室慎一って言います。どうぞよろしくお願いしますね。 密室の謎についてですね……。みなさんすごく深刻に考えていらっしゃいますけど、僕、黒田さんとお話ししていて、なんだかすごく心が温かくなったんです。あんなに優しくて、丁寧にお茶を入れてくださる方が、冷たいトリックなんて使えるわけがないって、どうしても思ってしまうんですよね。 それに、黒田さんって本当に気配り上手で、僕のくだらない質問にも優しく耳を傾けてくださるんです。「寒いから風邪をひかないようにね」なんて声をかけてもらっちゃって……あ、すみません、つい脱線しちゃいました! ええと、仕掛けのヒントですよね? 鑑識の方たちの話だと、外の排水管のあたりに擦れ傷があって、黒田さんのコートの繊維がちょっとだけ付いていたみたいなんです。それに、お部屋のゴミ箱からピアノ線の細かい切れ端とか、なんだか冷たそうな保温ボトルが見つかったとか……。 でも、きっとこれって、黒田さんが音楽の趣味でピアノ線を大切に使っていただけですし、寒い日に温かい飲み物を持ち歩いていた優しい証拠だと思うんです。 外から窓の木枠越しに、その……氷の重りとピアノ線を使って、内側のボルトを器用にカチャッと閉めちゃったのかなぁなんて、ちょっとだけ推理してみたりして……。 うーん、でもやっぱり、あんなに穏やかな黒田さんが犯人だなんて、僕にはどうしても信じられないんです。もう少しだけ、みんなであたたかいコーヒーでも飲みながら、ゆっくりお話しして確かめてみませんか? きっと何か、別の温かい真相があるはずですよ。 --- debug info --- embedding model: gemini-embedding-2 generated model: gemini-3.5-flash-lite retrieval: topN=2, top1 score margin=0.05 instruction: resources/novel.inspection.txt document: C:\...\developer-site-article-examples\rag-sample\resources\novel.context.txt prompt: C:\...\developer-site-article-examples\rag-sample\resources\novel.prompt.txt query: 密室はどのように作られましたか?仕掛けのヒントを教えてください。 chunk scores: 1. 第5章:決定的な証拠: score=0.7352 selected 2. 第2章:開かずの密室: score=0.7212 selected 3. 第4章:現場に残された違和感: score=0.6732 excluded 4. 第3章:証言とアリバイ: score=0.6551 excluded 5. 第1章:吹雪の夜: score=0.6432 excluded Instruction通り、推理が迷走しました。 まとめ # 自前でRAGを組むことで、以下の原理が明確になりました。 RAGはLLMに知識を記憶させているのではなく、「関連するコンテキストを動的に作って渡しているだけ」。 今回は最小構成でRAGの仕組みを理解することに集中しましたが、実運用ではPostgreSQLのpgvectorやQdrantなどの仕組みを使うことで、より大規模なデータに対応できます。 実装が複雑化しても基本原理は変わらない。 今回は「本質を見えやすくする」ことを目的に、最小構成で作ってみました。 実際にシステムに落とし込むときには、適切なライブラリやストレージを選ぶことが重要です。 まずは素のコードでブラックボックスを排除し、RAGの基本構造を体感してみてはいかがでしょうか。
私はC#が好きなのですが、デリゲートを使ったことがないと気付きました。 昔やったプロジェクトでデリゲートを見たことはありますし、先輩が使っているのを見たこともあります。しかし自分自身が使ったことはなくて、食わず嫌いしていたかもと思えてきました。 そこでデリゲートをコードを書きながら学ぼうと考えたのが今回の記事です。私が書いて動かしたコードで解説していきます。 デリゲート(delegate)とは # デリゲートとは委譲という意味です。 委譲とはコトバンクによると「権利・権限などを他の人・機関に譲って任せること」となっています。つまり自分がやることを他人に任せることです。例えば上司が部下に仕事を振ることなどはいい例でしょう。 https://kotobank.jp/word/%E5%A7%94%E8%AD%B2-432326 ならばプログラミングにおいてはやることは処理です。処理の実行を他の機能に任せるということになります。 しかしこれだけだとなんのこっちゃか分かりません。そこでもう少し具体的に言うと、処理(メソッド)の参照を代入して、値ではなく処理(メソッド)を渡すことで、他の機能(クラス)から処理(メソッド)を実行できるようにするとなります。 例えばある処理が完了したらコールバック処理を呼び出してもらうときに使えます。 事前準備 # この記事のサンプルコードを動かすにはVisual StudioまたはVisual Studio CodeでC#開発ができるようセットアップを行ってください。Visual Studio Codeでのセットアップはこちらの記事を参照してください。 VS Codeで始める!わかる&できるC#開発環境の構築【2025年版マニュアル】 IDEのセットアップができたら、次はコンソールアプリのプロジェクトを作成してください。この記事のサンプルコードは手軽に動作確認するためにコンソールアプリで作っています。 デリゲートの基本をサンプルコードで解説 # デリゲートの基本的な書き方 # 早速ですがコードを書いて動きを確認していきます。 動かせるサンプルコードを掲載します。 DelegateSample というクラスを作り、以下のコードを書いてください。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSample { // デリゲートを宣言 // デリゲート型のメソッドを宣言しているようなもの delegate void SampleDelegate(string message); // サンプルの実行 public void ExecSample() { // 宣言したデリゲートを型としてメソッドを引数に渡す // 違和感ある書き方だが、メソッド型のインスタンスを作ってメソッドを代入しているようなイメージ SampleDelegate sampleDelegate = new SampleDelegate(Console.WriteLine); // 試しにメッセージを出してみる // Console.WriteLineを代入したので、デリゲートを実行するとConsole.WriteLineが実行される sampleDelegate("テストメッセージです"); // 次は独自に作ったメソッド(コンソールに文字列を出すだけだが)を代入してみる sampleDelegate = TestMessage; sampleDelegate("カレーとサラダ"); Console.WriteLine(); } void TestMessage(string message) { Console.WriteLine($"今夜の夕食は{message}の予定です。"); } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); DelegateSample delegateSample = new DelegateSample(); delegateSample.ExecSample(); このコードを実行すると、次のようにメッセージが表示されます。 デリゲート初心者には分かりづらいとか違和感があると感じたかもしれません。 デリゲートの宣言は delegate 型のメソッドを定義することです。そして次に宣言したメソッドを型としたインスタンスを作成します。型がメソッドだなんて一瞬意味が分からなく感じますよね。 そして delegate 型のインスタンスにはメソッドを代入できます。メソッドを実行するための仕組みなので、このようになります。メソッドを代入するということ自体が慣れない行為ですよね。 でもここまで見てみると、引数さえ合っていれば、色んなメソッドを代入して実行可能だろうと分かりますね。 複数のメソッドをまとめるマルチキャストデリゲートというものもあります。例えば先ほどの DelegateSample クラスに次のようにコードを追加してみましょう。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSample { // デリゲートを宣言 // デリゲート型のメソッドを宣言しているようなもの delegate void SampleDelegate(string message); // サンプルの実行 public void ExecSample() { // 宣言したデリゲートを型としてメソッドを引数に渡す // 違和感ある書き方だが、メソッド型のインスタンスを作ってメソッドを代入しているようなイメージ SampleDelegate sampleDelegate = new SampleDelegate(Console.WriteLine); // 試しにメッセージを出してみる // Console.WriteLineを代入したので、デリゲートを実行するとConsole.WriteLineが実行される sampleDelegate("テストメッセージです"); // 次は独自に作ったメソッド(コンソールに文字列を出すだけだが)を代入してみる sampleDelegate = TestMessage; sampleDelegate("カレーとサラダ"); Console.WriteLine(); // ここから追加 // 実はメソッドの追加が可能 sampleDelegate += TestMessageLunch; sampleDelegate("カレーとサラダ"); Console.WriteLine(); // 追加済みメソッドの削除も可能 sampleDelegate -= TestMessage; sampleDelegate("カレーとサラダ"); // ここまで } void TestMessage(string message) { Console.WriteLine($"今夜の夕食は{message}の予定です。"); } // 昼食用のメソッドを追加 void TestMessageLunch(string message) { Console.WriteLine($"今夜の昼食は{message}でした。"); } } } Program.cs を実行すると以下のようになります。 このコードにはデリゲートに対してメソッドを加算及び減算しています。 なんと delegate 型のインスタンスには複数のメソッドの追加や追加したメソッドの削除ができるのです。 これをマルチキャストデリゲートと呼びます。 デリゲートの進化の歴史を知る # 実は先ほど書いたデリゲートのやり方は昔ながらのやり方でした。それがもう少し進化し、C# 2.0の時代には匿名メソッドが使えるようになりました。 匿名メソッドのサンプルコードを掲載します。 DelegateSampleAnonymous というクラスを作り、以下のコードを書いてください。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSampleAnonymous { // デリゲートを宣言 // デリゲート型のメソッドを宣言しているようなもの delegate void SampleDelegate(string message); // サンプルの実行 public void ExecSample() { // 次は独自に作ったメソッド(コンソールに文字列を出すだけだが)を代入してみる // 定義されたメソッドだけでなく、匿名メソッドも代入可能 SampleDelegate sampleDelegate = delegate(string message) { Console.WriteLine($"今夜の夕食は{message}の予定です。"); }; sampleDelegate("カレーとサラダ"); Console.WriteLine(); // 匿名メソッドを追加してみる sampleDelegate += delegate(string message) { Console.WriteLine($"今夜の昼食は{message}でした。"); }; sampleDelegate("カレーとサラダ"); Console.WriteLine(); // ちなみに匿名メソッドで追加すると削除できない // このように一度別のインスタンスに入れてから追加する必要がある SampleDelegate workDelegate = delegate(string message) { Console.WriteLine($"今夜の朝食は{message}でした。"); }; sampleDelegate += workDelegate; sampleDelegate("カレーとサラダ"); Console.WriteLine(); sampleDelegate -= workDelegate; sampleDelegate("カレーとサラダ"); } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); Console.WriteLine(); Console.WriteLine("匿名メソッドのサンプルです"); Console.WriteLine(); DelegateSampleAnonymous delegateSampleAnonymous = new DelegateSampleAnonymous(); delegateSampleAnonymous.ExecSample(); 実行すると以下のようになります。 ここでちょっと厄介なことは、匿名メソッドを使った場合は追加したメソッドの削除ができないことです。どうしても削除したければ、一度インスタンスに入れてから追加する必要があるのです。 上記のサンプルで言うと workDelegate が該当します。 現在はラムダ式を使うのが主流となっています。ラムダ式を使うサンプルコードを掲載します。 DelegateSampleLambda というクラスを作り、以下のコードを書いてください。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSampleLambda { // デリゲートを宣言 // デリゲート型のメソッドを宣言しているようなもの delegate void SampleDelegate(string message); // サンプルの実行 public void ExecSample() { // 次は独自に作ったメソッド(コンソールに文字列を出すだけだが)を代入してみる // 定義されたメソッドだけでなく、ラムダ式も代入可能 SampleDelegate sampleDelegate = (string message) => { Console.WriteLine($"今夜の夕食は{message}の予定です。"); }; sampleDelegate("カレーとサラダ"); Console.WriteLine(); // ラムダ式を追加してみる sampleDelegate += (string message) => { Console.WriteLine($"今夜の昼食は{message}でした。"); }; sampleDelegate("カレーとサラダ"); Console.WriteLine(); // ちなみにラムダ式で追加すると削除できない // このように一度別のインスタンスに入れてから追加する必要がある SampleDelegate workDelegate = (string message) => { Console.WriteLine($"今夜の朝食は{message}でした。"); }; sampleDelegate += workDelegate; sampleDelegate("カレーとサラダ"); Console.WriteLine(); sampleDelegate -= workDelegate; sampleDelegate("カレーとサラダ"); } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); Console.WriteLine(); Console.WriteLine("ラムダ式のサンプルです"); Console.WriteLine(); DelegateSampleLambda delegateSampleLambda = new DelegateSampleLambda(); delegateSampleLambda.ExecSample(); 実行すると以下のようになります。 ラムダ式になるともはや delegate という記述すらなくなります。一番シンプルな書き方です。 FuncとActionを使えば宣言不要 # 戻り値がない処理ならAction # ここまでのやり方は delegate 型のメソッドを宣言して、そのインスタンスにメソッドを代入するというものでした。わざわざ宣言しているので、用途別にデリゲートを作成する必要がありました。 そこで汎用型として戻り値がない場合は Action 、戻り値がある場合は Func というものが登場しました。 Action を使ったサンプルコードを掲載します。 DelegateSampleAction というクラスを作り、以下のコードを書いてください。 このようにジェネリックで引数の型を指定します。わざわざ delegate 型のメソッドを宣言する必要がないので、コードもシンプルになります。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSampleAction { public void ExecSample() { // Actionデリゲートを使用するサンプル // Actionデリゲートは戻り値がvoidで、引数の型を指定できる Action<string> actionDelegate = (message) => { Console.WriteLine($"今夜の夕食は{message}の予定です。"); }; actionDelegate("カレーとサラダ"); } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); Console.WriteLine(); Console.WriteLine("Actionのサンプルです"); Console.WriteLine(); DelegateSampleAction delegateSampleAction = new DelegateSampleAction(); delegateSampleAction.ExecSample(); 実行すると以下のようになります。 ちなみにメソッドの追加や削除については delegate を使う場合と同様に+や-などの演算子を使ってください。また匿名メソッドやラムダ式では削除ができないことも同様ですので、削除したいときは一度インスタンスに入れてから追加してください。 戻り値がある処理ならFunc # Func を使ったサンプルコードを掲載します。 DelegateSampleFunc というクラスを作り、以下のコードを書いてください。ジェネリックの型指定で、<引数の型, 戻り値の型>というように指定します。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSampleFunc { public void ExecSample() { // Actionデリゲートを使用するサンプル // Actionデリゲートは戻り値がvoidで、引数の型を指定できる Func<int, string> funcDelegate = (num) => { return $"入力された値は{num}です。"; }; Console.WriteLine(funcDelegate(5)); } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); Console.WriteLine(); Console.WriteLine("Funcのサンプルです"); Console.WriteLine(); DelegateSampleFunc delegateSampleFunc = new DelegateSampleFunc(); delegateSampleFunc.ExecSample(); 実行すると以下のようになります。 Func もメソッドの追加や削除については delegate を使う場合と同様です。 デリゲートの本当の使い道 # コールバック処理 # コールバック処理のサンプルコードを掲載します。 DelegateSampleAction というクラスを作り、以下のコードを書いてください。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class CallbackSample { // コールバックのサンプル // ここでは、処理が終わった後に呼び出されるメソッドをデリゲートとして渡す // デリゲートを使うことで、処理が終わった後に呼び出されるメソッドを自由に変更できる public void ExecSample(string message, Action<string> callback) { Console.WriteLine($"処理中: {message}"); // 処理が終わった後にコールバックを呼び出す callback?.Invoke($"処理が完了しました: {message}"); } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); Console.WriteLine(); Console.WriteLine("コールバックのサンプルです"); Console.WriteLine(); CallbackSample callbackSample = new CallbackSample(); callbackSample.ExecSample("カレーとサラダ", (result) => { Console.WriteLine(result); }); 実行すると以下のようになります。 このようにメソッドの引数に Func や Action を定義することで処理を受け取ります。そして Invoke メソッドで処理を実行します。 条件に応じた処理の差し替え # 実はLINQの Where は引数が Func になっています。つまり Where メソッドの中ではデリゲートを使っているのです。 では Func の内容次第で結果が異なる例を作ってみましょう。 DelegateSampleLinq というクラスを作り、以下のコードを書いてください。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSampleLinq { public void ExecSample() { List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // まずは偶数だけを抽出してみる var evenNumbers = FilterNumbers(numbers, (num) => num % 2 == 0); Console.WriteLine("偶数のリスト:"); Console.WriteLine(evenNumbers.Count > 0 ? string.Join(", ", evenNumbers) : "該当する値はありません。"); // 続いて奇数を抽出してみる var oddNumbers = FilterNumbers(numbers, (num) => num % 2 != 0); Console.WriteLine("奇数のリスト:"); Console.WriteLine(oddNumbers.Count > 0 ? string.Join(", ", oddNumbers) : "該当する値はありません。"); } /// <summary> /// 引数として渡されたFuncを実行し、条件に一致する値だけを返す /// </summary> /// <param name="numbers"></param> /// <param name="predicate"></param> /// <returns></returns> static List<int> FilterNumbers(List<int> numbers, Func<int, bool> predicate) { List<int> resultList = new List<int>(); foreach (var number in numbers) { if (predicate(number)) { resultList.Add(number); } } return resultList; } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); Console.WriteLine(); Console.WriteLine("条件を切り替えるサンプルです"); Console.WriteLine(); DelegateSampleLinq delegateSampleLinq = new DelegateSampleLinq(); delegateSampleLinq.ExecSample(); 実行すると以下のようになります。 デリゲートの脆弱性を補うためのevent # ここまでデリゲートの使い方について色々と見てきました。 しかし便利な反面、危険も伴います。何せ代入次第で何とでも処理内容を変えられます。また Invoke メソッドによって好きなところで実行が可能です。脆弱な面があることは否めません。 下手すると不具合の元になるのはもちろん、セキュリティ面での脆弱性も怖いところです。 そこで event というキーワードを付けることで、外部クラスからの代入や実行ができなくなります。 event を使ったサンプルコードを掲載します。 DelegateSampleEvent というクラスを作り、以下のコードを書いてください。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp { internal class DelegateSampleEvent { // パブリックだから他のクラスからアクセス可能 public event Func<int, string> SampleEvent; } internal class DelegateSampleExternal { public void ExecSample() { DelegateSampleEvent sample = new DelegateSampleEvent(); // 外部クラスからのデリゲートの代入は不可 sample.SampleEvent = (int x) => $"入力された値は{x}です。"; // 外部クラスからのデリゲートの追加と削除は可能 sample.SampleEvent += (int x) => $"入力された値は{x}です。"; Func<int, string> func = (int x) => $"今日はコーヒーを{x}杯飲みました。"; sample.SampleEvent += func; sample.SampleEvent -= func; // 外部クラスからの実行は不可 sample.SampleEvent.Invoke(99); } } } 上記のようにコードを書くと、以下のスクリーンショットのように代入と Invoke メソッドのところでコンパイルエラーが発生します。 ひとまず先ほどのコードでコンパイルエラーが出ている個所はコメントアウトしてください。 フードコートでの注文を例にデリゲートを試す # デリゲートのやり方が分かったところで、現実世界をイメージして練習してみたいと思います。そこでフードコードのスイーツ屋をテーマに扱います。ただ文字の表示や計算をするよりは楽しい内容になるし、委譲をイメージしやすいと考えたためです。 業務フローは以下の図のようになります。 ここではコードをかなり端折って解説します。本当は注文内容だって明細を持てるようにしたり、注文内容も動的に変えられるようにしたりする必要がありますが、今回はそこが主題でないため簡易的なメッセージで済ませます。 それではソースコードを掲載します。 Models というフォルダに作って、その中に以下の5つのクラスを作成してください。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp.Models { internal class Buzzer { public int No { get; set; } public Buzzer(int no) { No = no; } public void Ring() { Console.WriteLine($"ピー!ピー!ピー!{No}番のブザーが鳴っています!"); } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp.Models { internal class Cook { public void CookOrder(Order order) { Console.WriteLine($"料理人が注文を受け取りました: {order.OrderContent}"); Console.WriteLine("料理を作っています..."); Console.WriteLine("料理が完成しました!"); } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp.Models { internal class Customer { private Buzzer Buzzer { get; set; } public Order MakeOrder() { Console.WriteLine("お客様が注文しました。"); return new Order("パンケーキとコーヒーのセット"); } public void Pay() { Console.WriteLine("お会計を済ませました。"); } public void PickUpBuzzer(Buzzer buzzer) { Buzzer = buzzer; Console.WriteLine($"お客様が{buzzer.No}番のブザーを受け取りました。"); } public void PickUpFood() { Console.WriteLine($"お客様が{Buzzer.No}番のブザーを渡しました。"); Console.WriteLine("お客様が料理を受け取りました。"); } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp.Models { internal class Order { public string? OrderContent { get; set; } public Order(string? orderContent) { OrderContent = orderContent; } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DelegateApp.Models { internal class Staff { private delegate void BuzzerRingHandler(); private IDictionary<int, BuzzerRingHandler> _buzzerHandlers = new Dictionary<int, BuzzerRingHandler>(); public void TakeOrder(Order order) { Console.WriteLine($"スタッフが注文を受け取りました: {order.OrderContent}"); Console.WriteLine("お会計は1,500円になります。"); } public Buzzer HandOverBuzzer(int no) { Buzzer buzzer = new Buzzer(no); Console.WriteLine($"スタッフが{no}番のブザーを渡しました。"); _buzzerHandlers.Add(no, buzzer.Ring); return buzzer; } public void RingBuzzer(int no) { if (_buzzerHandlers.TryGetValue(no, out var buzzer)) { buzzer.Invoke(); _buzzerHandlers.Remove(no); } } } } そして Program.cs には以下のように書いてください。 using DelegateApp; using DelegateApp.Models; Console.WriteLine("デリゲートを開始します"); Console.WriteLine(); Console.WriteLine("フードコートで料理が出来たらブザーを鳴らすサンプルです"); Console.WriteLine(); Customer customer = new Customer(); Staff staff = new Staff(); Cook cook = new Cook(); Order order = customer.MakeOrder(); // 注文とお会計 staff.TakeOrder(order); customer.Pay(); // ブザーを渡す int no = 1; Buzzer buzzer = staff.HandOverBuzzer(no); customer.PickUpBuzzer(buzzer); // 調理 cook.CookOrder(order); // ブザーを鳴らして料理を渡す staff.RingBuzzer(no); customer.PickUpFood(); 実行すると以下のようになります。 ポイントとしては店員がどのお客さんに何番のブザーを渡したかを管理しているということです。 プログラムとしては Staff クラスが Dictionary で番号とブザーを鳴らすメソッドの組み合わせを持っています。 こうすることで、料理ができたときに店員が顧客の手元にあるブザーを鳴らせるようにできます。 おわりに # コードを書いて動かしてみることで、デリゲートとは何かが少し分かりました。やっぱり書いて動かしてみるのが一番ですね。 デリゲートが分からないという方はこの記事のフードコートみたいなものを作ってみてください。
はじめに # GFXBenchというGPUベンチマークソフトをご存じでしょうか。 Windows, Linux, Android, iPhone等のマルチプラットフォームで実行出来るkishonti社のGPUベンチマークソフトです。 GFXBench - unified graphics benchmark based on DXBenchmark (DirectX) and GLBenchmark (OpenGL ES) 今まで新しいAndroid端末を買ってはAndroidのストアからダウンロードしてベンチマークを行ったものでした。 今回取り上げるきっかけとなった流れです。 久々に新しいAndroidタブレットを購入 ストアからGFXBenchを探すと見つからない? 過去Android端末にインストール済のGFXBenchもサーバに接続出来ず起動しない? たまたまメンテナンス等でタイミングが悪いのかと1か月位待っても状況変わらず 流石におかしいと思い調べたところサービス終了、GitHubでオープンソース化されていた事が発覚! オープンソース化は2025年末の出来事の様です。 KISHONTI Milestones - our History GFXBench and CompuBench are shutting down after 21 years, source code and toplist database move to GitHub 単なるベンチマークソフト以上に筆者には思い入れがありました。 無料のダウンロード版ではなく商用のソースコード版を自社製品の基板(not Android)に移植しベンチマークを行う、という業務を行った事があったからです。 GFXBenchの中にはいくつかベンチマークの種類がありますが、T-Rex/Manhattanあたりの時代の事でした。 (ベンチマーク種類については次回の実行編で触れます) アーリーアクセスのソースコードだと移植性も良くなく、かなり手を入れて移植しデバッグしたものでした...。 それがGitHubでオープンソース化です。 GitHub - Kishonti-Opensource-gfxbench 時代の流れを感じたり当時の事を思い出したりでしんみりしてしまいますが、今までの活動やオープンソース化された事に感謝しつつビルドを行い実行してみようと思います。 今回の目標としては以下とします。 GFXBenchをソースコードからビルドしてAndroid端末で実行出来るノウハウを修得する ビルド環境:Windows11 ビルド対象:Android用、かつ筆者のAndroid端末コレクションの関係上古いAndroidバージョンでも実行出来る事が望ましい それにより、既存の実行出来なくなった実行バイナリの代替としてだけではなく、実験でソースコードを修正したくなった時も気軽にローカルで試せる様にする 対象者は以下とします。 Androidアプリ開発経験あり Android Studio、sdkmanager、adbコマンドを扱える人 Android端末を開発者モードに出来る人 コマンドライン操作、パッチ、環境変数に戸惑わない人 Graphics API (今回は OpenGL ES) に詳しいとより楽しむ事が可能 ビルド環境準備 # GitHub GFXBench公式のAndroid用ビルド手順は以下にあります(以下"公式手順"として参照します)。 doc/gfxbench_gl_android_build.txt この内容に従ってビルド環境を構築していきます。 以下、公式手順を引用し、項目ごと見ていきます。 ./sdkmanager "ndk;28.0.12674087" "build-tools;35.0.0" "platforms;android-35" "platform-tools" "cmdline-tools;latest" Path Version Description Location build-tools;35.0.0 35.0.0 Android SDK Build-Tools 35 build-tools/35.0.0 cmdline-tools;latest 16.0 Android SDK Command-line Tools (latest) cmdline-tools/latest ndk;28.0.12674087 28.0.12674087 rc2 NDK (Side by side) 28.0.12674087 ndk/28.0.12674087 platform-tools 35.0.2 Android SDK Platform-Tools platform-tools platforms;android-35 1 Android SDK Platform 35 platforms/android-35 - Android NDK 28.0.12674087 (tested version), GCC is no longer supported http://developer.android.com/sdk/ndk/index.html - Android SDK, API Level 35 (recommended versions, store version built with API 35) http://developer.android.com/sdk/index.html この内容に従って、sdkmanagerで上記をダウンロードします。 今回はAndroidStudio上からsdkmanagerのGUIを起動して各バージョンを指定してダウンロードしました。 各インストール場所を後に環境変数で指定する事になります。 - On Windows suggested shell is Git Bash (Cygwin nor WSL is not supported) https://git-scm.com/downloads Git Bashをインストールします。ビルドの際のコマンドライン操作もGit Bash上で行います。 - CMake 3.5.0 or newer (tested with CMake 3.21) http://www.cmake.org/cmake/resources/software.html - Swig 3.0.12 or 4.0.2 (tested with 3.0.12 and 4.0.2) http://swig.org/download.html - Java Development Kit (e.g.: openjdk17) それぞれインストールします。今回使用したバージョンはそれぞれ以下です。3.21, 4.0.2, openjdk17 JDKはインストール場所を後に環境変数で指定する事になります。 - Add cmake and swigwin applications to `PATH` (on Windows) cmakeとswigwinは指定通り環境変数 PATH に加えます。 ここから 公式手順へのプラス分 です。 - pythonインストール 公式手順には記載ありませんが、pythonがビルドに必要となりますのでインストールします。 pythonへ言及のある他プラットフォーム用インストールにおいてもversionについては記載はなく不明ですが、今回は version 3.14.5 を使用しました。 また、Windows11ではpythonコマンドからWindowsストアのインストーラにエイリアスが貼られているため、それも外しておきます。 Windows11の  設定 > アプリ > アプリの詳細設定 > アプリ実行エイリアス から以下の2つをオフにします。 アプリインストーラー python.exe アプリインストーラー python3.exe - CUDA Toolkitインストール 同じく記載ありませんが、CUDA Toolkitがビルドに必要となりますのでインストールします。 Android版なのにCUDA?となりますが、 frameworks/cudaw というフレームワークでプラットフォーム共通に使用されているからの様です。 使われ方もベンチマーク本体ではなく情報収集のためだけの様です。dlopenでCUDAライブラリがロードされ直接リンクされない仕組みのため、Android版であってもビルドエラーや不正な実行になる事もありません(ヘッダファイルがあればいいレベル)。 であればビルド環境やソースコードを修正して外してしまってもいいのですが、今回は修正点を少なくしたかったのでそのままにしています。 (後述の様に若干の修正は必要にはなります) 今回はビルドに使用したPCインストール済の v10.2 を使用しました。 ソースコード準備 # 以降Git Bashにて操作します。 入手 # GitHubのリポジトリからgit cloneします。 git clone https://github.com/Kishonti-Opensource/gfxbench.git 必要なストレージサイズの目安は筆者の環境では以下でした。 ビルド前:6.5GB台 ビルド後:18GB台 修正 # 公式手順へのプラス分 です。 以下の様にCUDAヘッダパスの設定を修正します。 GitHub workflow手順 .github/workflows/main.yml ではビルド環境はLinuxの様で、今回のWindowsビルド環境のための修正です。 またWindows環境でコピーコマンドの影響なしにコピー出来る様にも修正しています。 CUDAインストールディレクトリ名は各自の環境に合わせて適宜変更してください。 --> Warning 以下のパッチは筆者の環境における一例であり、適用は 自己責任 にてお願いいたします。(ライセンス等の詳細は 記事末尾 に記載しています) diff --git a/frameworks/cudaw/CMakeLists.txt b/frameworks/cudaw/CMakeLists.txt index 0cc4a30e..710b4519 100644 --- a/frameworks/cudaw/CMakeLists.txt +++ b/frameworks/cudaw/CMakeLists.txt @@ -13,7 +13,9 @@ add_library(cudaw STATIC if(ANDROID) execute_process( - COMMAND cp -rL /usr/local/cuda/include ${CMAKE_CURRENT_SOURCE_DIR}/include/nvidia + COMMAND ${CMAKE_COMMAND} -E copy_directory + "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.2/include" + ${CMAKE_CURRENT_SOURCE_DIR}/include/nvidia RESULT_VARIABLE COPY_RESULT ) if(NOT COPY_RESULT EQUAL 0) ビルド実行 # 再び公式手順に戻りビルドを行います。 環境変数設定 # git cloneしたソースのトップディレクトリで以下の環境変数をセットします。 公式手順へのプラス/修正分 分は以下です。 JAVA_HOMEの追加 NDKバージョンを前述の通り28.0.12674087に修正 各ディレクトリ名は各自の環境に合わせて適宜変更してください。 (sdkmanagerでインストールした分はデフォルトなら以下のディレクトリ名になっていると思います) export JAVA_HOME="/c/Tools/jdk-17.0.18+8" export ANDROID_NDK="$HOME/AppData/Local/Android/Sdk/ndk/28.0.12674087/" export ANDROID_HOME="$HOME/AppData/Local/Android/Sdk" export NG_CMAKE_TOOLCHAIN_FILE="$HOME/AppData/Local/Android/Sdk/ndk/28.0.12674087/build/cmake/android.toolchain.cmake" export CMAKE_MAKE_PROGRAM="$HOME/AppData/Local/Android/Sdk/ndk/28.0.12674087/prebuilt/windows-x86_64/bin/make.exe" export WORKSPACE=$PWD export PLATFORM=android-arm64-v8a export CONFIG=Release export APPLICATION_TYPE=gui なお、筆者の環境 Git BashでUTF-8使用 + openjdk17 の場合、JDKのコマンド出力が文字化けしたのですが、以下の設定で回避出来ました。 export JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 ビルドスクリプト実行 # 公式手順通り以下のスクリプトを実行します。 scripts/build-3rdparty.sh scripts/build.sh ビルドが成功すると、ビルドのコンソールログで表示されるディレクトリに gfxbench-5.1.5+corporate.apk ファイルが出来上がります。 サイズは筆者の環境では以下でした。 apkサイズ:1.2GB台 Android端末で起動後のサイズ:3.4GB台 ※インストール、起動方法については次回の実行編にて ※起動後のサイズが増えるのは、初回起動時apk内にバンドルされたデータがアプリデータ領域に展開されるため --> Information apk内にデータをバンドルさせない手順もあります(環境変数 BUNDLE_DATA=false)。 この場合のapkサイズは 21MB台 となります。 ただしその場合は手動でデータをアプリデータ領域に配置する必要があるのでバンドルさせる通常手順のままが便利です。 出来上がったapkファイルの各バージョン情報は以下です。 platformBuildVersionCode='35' compileSdkVersion='35' minSdkVersion:'21' targetSdkVersion:'35' minSdkVersion:'21' (Android5.0) なので古いAndroid端末でもインストール出来、動作する事も期待されます。 --> Information このminSdkVersionはGFXBenchが要求する一番リッチなGraphics、OpenGL ES 3.1 + AEP に対応が始まったversionとなります。 なのでこのminSdkVersionからフルにGFXBenchを楽しむ事が出来ると期待されます。 (このminSdkVersionの値の理由はこれでは?とも思います) 他のビルドバリエーション # 今回は一番少ない修正案、および公式の記載準拠でビルドを行いましたが、以下のビルドのバリエーションも考えられます。 次々回以降で取り上げたいと思います。 (筆者の目標としてはここからが本番とも言えます) ユニバーサルAPKビルド # 今回は PLATFORM=android-arm64-v8a でAndroidプラットフォームを固定する手順でしたが、複数プラットフォームを含むユニバーサルAPKを作成する手順も存在する様です。 scripts/build-multiarch-apk.sh 特に古いAndroid端末での実行を考えると android-armv7a も必要になってくるので両対応させたい場合に便利と思われます。 古いAndroid端末用ビルド # minSdkVersion:'21' (Android5.0) なので古いAndroid端末でもインストール出来、動作する事も期待されます。 と書いたのですが、実際に試すと修正が必要となりました。 実行時にAndroid端末上でエラーが発生します。 おそらくAndroid8より古い場合にNGと思われます。 developer版ビルド # APPLICATION_TYPE=gui としてビルドしましたが、 developer としてビルドする手順もあります。ただこれも実際に試すと修正が必要となりました。 ビルド時にエラーが発生します。 developer版らしく以下の恩恵があります。 apk内にデータをバンドルさせない形式必須で、データは別途adb pushする形式となる adb push分データのサイズは 2.2GB台 程度。更に必要ベンチマーク分(ディレクトリ単位)で絞れる可能性もあり アプリのGUIが gui 版とは変わりapkファイル自体も 4MB台 と小さくなる アプリを修正してはインストールし直すというサイクルが楽になる 思わぬ発見もあるかもしれません :-) なお、公式手順の最後に Legacy install process for developer app (not supported) から始まる特殊な手順があります。 その中で 4.4.4_r1(SdkVersion:'19')と更に古いAndroidバージョンに対応出来る様に見える旨があるのですが、これは対象外とします。 おわりに # 今回はGFXBenchというベンチマークについて取り上げるきっかけのお話とビルドまでを行いました。 次回は実際に実行しベンチマークスコアを測ってみます。 ライセンスおよび免責事項 # 本記事に掲載しているビルド修正パッチ、引用しているビルド手順は、BSD 3-Clause Licenseのもとで公開されている Kishonti-Opensource/gfxbench のソースコードおよびドキュメント( doc/gfxbench_gl_android_build.txt )を利用・引用したものです。 Original Copyright: (c) 2005–2025 Kishonti Ltd. License: BSD 3-Clause License ドキュメントの権利について: 記事内で引用している公式ビルド手順のテキストの著作権は、原著作者であるKishonti Ltd.に帰属します。 【免責事項】 本記事に掲載しているパッチ、手順は、特定の検証環境における現状のまま(AS IS)のものであり、その正確性や安全性を保証するものではありません。 パッチの適用やビルドの実行により生じた直接的・間接的な損害について、筆者および株式会社豆蔵は一切の責任を負いません。内容を十分にご確認の上、ご自身の責任においてご利用ください。
はじめに # 本ページは「AIエージェントとシステムをつなぐMCP入門」の続編です。 今回は、認証/認可について説明します。 MCP仕様では認可機能は任意扱いですが、MCPを実運用する場合は必須機能と言えます。 本記事では、以下の内容を扱います。 Streamable HTTPにおけるMCPの認証・認可の流れ PRMと認可サーバーの発見 Keycloakとトークンイントロスペクションを使ったMCPサーバー側の検証 本記事で掲載しているコードは こちら で公開しています。 --> シリーズ目次 連載:AIエージェントとシステムをつなぐMCP入門 イントロダクション stdio実装編 StreamableHTTPステートレス実装編 StreamableHTTPステートフル実装編 プロンプト編 リソース編 認証/認可編(本ページ) 今回使用するライブラリなど # npm@12.0.2 node@26.6.0 @modelcontextprotocol/express@2.0.0 @modelcontextprotocol/node@2.0.0 @modelcontextprotocol/server@2.0.0 typescript@7.0.2 zod@4.4.3 Keycloak 26.3.4 Docker compose --> 今回使用するライブラリについて @modelcontextprotocolのv2がリリースされたので、本記事からはv2を使用します。 併せてNodeおよびTypeScriptも最新のものを使用します。 MCPモジュールについて v1では @modelcontextprotocol/sdk を使用していました。 v2でモジュールが分割されたため、 @modelcontextprotocol/server, express, node を使用します。 MCP仕様:認証/認可 # トランスポート別の認証アプローチ # トランスポート(通信方式)により、認証の実装指針が異なります。 StreamableHTTP 認可は任意(OPTIONAL)となっていますが、実装する場合はMCPの認可仕様に準拠しなければなりません。 stdio(標準入出力) MCP仕様で定義されている認証仕様の対象外です。 認証情報(APIキーなど)はサブプロセス起動時に実行環境から直接取得して利用する形になります。 その他 そのプロトコルで確立されたベストプラクティスに準拠します。 準拠規格 # セキュリティと相互運用性を確保しつつ簡素化を図るため、以下の標準規格のサブセットが採用されています。 OAuth 2.1 (IETF Draft) OAuth 2.0 Bearer Token Usage (RFC 6750) OAuth 2.0 Authorization Server Metadata (RFC 8414) OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591) Resource Indicators for OAuth 2.0 (RFC 8707) OAuth 2.0 Protected Resource Metadata (RFC 9728) OAuth Client ID Metadata Documents (CIMD) ロール # 以下のロールが定義されています。 MCPサーバー(OAuth 2.1リソースサーバー): アクセストークンを受け入れ、保護されたリソースへのリクエストに応答する。 MCPクライアント(OAuth 2.1クライアント): リソース所有者に代わって保護されたリソースへリクエストを行う。 認可サーバー: ユーザーと対話し、アクセストークンを発行する。MCPサーバーと同一でも、独立した形で構成してもよい。 認証フロー # MCPの公式ドキュメントに掲載されている「Authorization Flow Steps」を一部和訳して転載。 --> Information MCP仕様では、クライアントがPRMと認可サーバーメタデータを取得し、PKCEを利用した認可コードフロー(Authorization Code Flow)でトークンを取得します。 今回の検証コードでは主にリソースサーバーとしての動作検証を目的とし、クライアント側のブラウザ認証フローは省略しています。 Keycloakで事前に取得したアクセストークンを使い、MCPサーバーがBearerトークンを受け取る。 Keycloakの Introspection endpoint でトークンの有効性、Audience、Scopeを検証する。 sequenceDiagram autonumber participant B as User-Agent (Browser) participant C as MCPクライアント participant M as MCPサーバー participant A as 認可サーバー Note over C: 認可サーバーの発見 C->>M: MCPリクエスト(トークンなし) M->>C: 401 Unauthorized(WWW-Authenticate header) Note over C: リソースメタデータの発見 C->>M: PRM(Protected Resource Metadata)リクエスト M->>C: PRM C->>A: 認可サーバーメタデータ取得 A-->>C: 認可サーバーメタデータ Note over C: クライアント登録 alt Client ID Metadata Documents A->>C: Fetch metadata from client_id URL C-->>A: JSON metadata document else Dynamic client registration C->>A: POST /register A->>C: Client Credentials else Pre-registered client Note over C: Use existing client_id end C->>B: Open browser with authorization URL + code_challenge + resource B->>A: Authorization request with resource parameter A->>B: Redirect to callback with authorization code + iss B->>C: Authorization code callback C->>A: Token request + code_verifier + resource A->>C: Access token (+ refresh token) C->>M: MCPへリクエスト(トークンあり) M-->>C: MCPからレスポンス 認可サーバーの発見 # MCPクライアントは、以下の手順で認可サーバーの場所と機能を特定する。 リソースメタデータの発見(RFC 9728) クライアントは 401 Unauthorized レスポンスを受け取った際、 resource_metadata (リソースメタデータ)を取得しなければなりません。 優先順位 WWW-Authenticate ヘッダーの resource_metadata パラメーターに示されたURL MCPサーバーエンドポイント配下のWell-Known URI(e.g. /.well-known/oauth-protected-resource/path/to/mcp ) ドメインルートのWell-Known URI(e.g. /.well-known/oauth-protected-resource ) 認可サーバーメタデータの取得(RFC 8414) クライアントは、リソースメタデータから得られたIssuer(発行者)URLの形式に応じて認可サーバーのエンドポイントを特定します。 優先順位 パスコンポーネントがある場合(例:/tenant1) OAuth 2.0方式(e.g. /.well-known/oauth-authorization-server/tenant1 ) OIDC方式パス挿入(e.g. /.well-known/openid-configuration/tenant1 ) OIDC方式パス付与(e.g. tenant1/.well-known/openid-configuration ) パスコンポーネントがない場合 OAuth 2.0方式(e.g. /.well-known/oauth-authorization-server ) OIDC方式(e.g. /.well-known/openid-configuration ) 厳格な検証ルール メタデータドキュメント内のissuer値は、Well-Known URLの構築に使用した発行者識別子と完全に一致しなければならない。 不一致の場合は、クライアントはそのメタデータを使用不可とする。 --> Information 検証コードでは、「パスコンポーネントあり」の「OAuth 2.0方式」を使用しています。 クライアント登録 # 認証フローを開始する前に、クライアントはクライアントIDを取得する必要があります。 登録メカニズムと優先順位 事前登録(Pre-registration): クライアントとサーバー間に既存の関係がある場合、静的な認証情報を使用する。 Client ID Metadata Documents (CIMD): クライアントとサーバー間に事前関係がない場合、HTTPS URLをクライアントIDとして使用する。 動的クライアント登録 (Dynamic Client Registration): CIMD未対応サーバーとの後方互換性のために維持されていますが非推奨なので、基本的に選択しない。 ユーザー入力: 他の手段がない場合、ユーザーに情報を入力させる。 --> Information 検証コードでは、Keycloakに事前登録した静的な認証情報を使用しています。(Pre-registrationに相当) 検証コードの解説 # 検証コード仕様 # 下記のような検証コードを使って説明します。 Keycloakに依存する処理は、keycloak.tsに集約する形で構成しています。 項目 内容 備考 トランスポート Streamable HTTP(Stateless) 認可サーバー Keycloak 8081ポートで公開 MCPエンドポイント POST /mcp Bearer認証必須 トークン検証 トークンイントロスペクション(Keycloakの Introspection endpoint ) 認証エラー 401 Unauthorized + WWW-Authenticate トークン無効または期待するAudienceと不一致の場合にスローします 認可エラー 403 Forbidden + WWW-Authenticate トークンは有効でも、スコープが不足している場合にスローします PRM GET /.well-known/oauth-protected-resource/mcp 今回の検証ではやらないこと ブラウザの認可画面 PKCEパラメーターの生成 認可コードを受け取るcallback 認可コードからトークンへの交換 CIMD Dynamic Client Registration Keycloakの設定 # Docker composeで起動する際、同梱しているRealm設定を使って自動生成しています。 これにより、後述する動作検証に必要なRealmはすべて設定された状態でKeycloakが起動されます。 realm: mcp-demo Client Scope: mcp:tools : aud= http://localhost:3000/mcp mcp:no-scope : aud= http://localhost:3000/mcp mcp:diff-audience : aud= http://localhost:3000/mcp-diff Client Introspection用: mcp-server 正常系トークン取得用: mcp-demo-client ( mcp:tools を付与) Scope不足検証用: mcp-demo-no-scope-client ( mcp:no-scope を付与) Audienceなし検証用: mcp-demo-no-audience-client Audience不一致検証用: mcp-demo-diff-audience-client ( mcp:diff-audience を付与) --> Information Audience: どのリソースサーバー向けのトークンか Scope: リソースサーバーで何を実行できるか PRMの公開 # MCPクライアントが認可サーバーを発見するためのPRMの公開。公開は mcpAuthMetadataRouter が担います。 クライアントが認証なしでMCPエンドポイントへアクセスした場合、 WWW-Authenticate ヘッダーにもPRMのURLが含まれます。クライアントはそのURLからPRMを取得し、認可サーバーの場所を知ることができます。 app.use( mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpServerUrl, scopesSupported: [REQUIRED_SCOPE], resourceName: "MCP Auth Streamable HTTP", }), ); PRM例 { "resource": "http://localhost:3000/mcp", "authorization_servers": ["http://localhost:8081/realms/mcp-demo"], "scopes_supported": ["mcp:tools"], "resource_name": "MCP Auth Streamable HTTP" } resource : 保護対象であるMCPサーバーの識別子 authorization_servers : 認可サーバーのIssuer URL scopes_supported : MCPサーバーが利用するScopeの候補 resource_name : リソースの表示名 Bearer認証ミドルウェア # MCPエンドポイントの前段でBearer認証しています。 requireBearerAuth は、MCPエンドポイント開始前に実行されるミドルウェアです。 authMiddleware でエラーが検出された場合は、そこで処理を中断するため、以降の処理は実行されません。 index.ts const authMiddleware = requireBearerAuth({ // ※1 verifier: tokenVerifier, // ※2 requiredScopes: [REQUIRED_SCOPE], resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl), }); //omit app.post(MCP_PATH, authMiddleware, async (req, res) => { const server = createServer(); // authMiddlewareでエラーが発生した場合、ここは実行されません // omit ※1: トークン検証(詳細な検証内容は後述します) ※2: スコープ検証 トークンが指定されたスコープを持っているか検証します。 トークンが持つスコープと、requiredScopesに設定したスコープを requireBearerAuth が比較します。 Keycloak Introspectionによるトークン検証 # MCPサーバーが受け取ったアクセストークンは、Keycloakの Introspection endpoint で検証しています。 ここで重要なのは、HTTP 200だけではトークンが有効だと判断できない点です。 keycloak.ts export function createTokenVerifier(mcpServerUrl: URL): OAuthTokenVerifier { return { verifyAccessToken: async (token) => { // Keycloakのイントロスペクションでトークン検証 ※1 const params = new URLSearchParams({token, client_id: OAUTH_CLIENT_ID}); if (OAUTH_CLIENT_SECRET) params.set("client_secret", OAUTH_CLIENT_SECRET); const response = await fetch(KEYCLOAK_INTROSPECTION_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString(), }); // 200 以外を返した場合 ※2 if (!response.ok) { const text = await response.text().catch(() => ""); throw new OAuthError(OAuthErrorCode.InvalidToken, `Invalid or expired token: ${text}`); } // omit // トークンが無効または期限切れの場合 ※3 if (!data.active) throw new OAuthError(OAuthErrorCode.InvalidToken, "Inactive token"); ※1 token: 検証対象のトークン OAUTH_CLIENT_ID , OAUTH_CLIENT_SECRET : MCPクライアントの情報ではなく、MCPサーバーがKeycloakにIntrospectionを依頼するための認証情報 ※2, 3 200以外が返された場合は、無効または期限切れとみなして InvalidToken を返します。 200が返されても active が false の場合、トークンが無効なため、同様に InvalidToken を返します。 Audienceの検証 # トークンに設定されている aud は「トークンが、どのリソースサーバー向けに発行されたか」を表します。 トークンが有効でも、別のリソース向けに発行されたトークンを受け入れないようにAudienceを検証しています。 keycloak.ts // Audience (aud) クレームの検証 if (OAUTH_STRICT) { // OAUTH_STRICTを有効にしている場合、検証を実行します // ※1 if (!data.aud) throw new OAuthError(OAuthErrorCode.InvalidToken, "Resource indicator (aud) missing"); const audiences = Array.isArray(data.aud) ? data.aud : [data.aud]; const allowed = audiences.some((audience) => checkResourceAllowed({ requestedResource: audience, configuredResource: mcpServerUrl }), ); // ※2 if (!allowed) throw new OAuthError(OAuthErrorCode.InvalidToken, `Expected audience compatible with ${mcpServerUrl}, got: ${audiences.join(",")}`); } ※1 audが存在しない場合は、リソースインジケーターが欠落している旨を返します ※2 audが許可されているものと一致しない場合は、期待するAudienceと一致しない旨を返します 検証コードを使った動作検証 # 基本フロー(認可フローの流れを検証します) Authorizationヘッダー未設定でリクエスト(MCP Inspector) Authorizationヘッダー未設定でリクエスト(curl) PRM(Protected Resource Metadata)リクエスト 有効なトークンを使って正常アクセス 例外フロー Authorizationヘッダーに形式違いの値を設定してリクエスト Authorizationヘッダーに無効なトークンを設定してリクエスト スコープ不足のクライアントでリクエスト Audience不足のクライアントでリクエスト 異なるAudienceが設定されているクライアントでリクエスト 1. Authorizationヘッダー未設定でリクエスト(MCP Inspector) # Authorizationヘッダー未設定の状態で、MCP InspectorからMCPサーバーへ接続します。 UI上のポップアップメッセージ OAuth Authorization Failed Policy 'Allowed Client Scopes' rejected request to client-registration service. Details: Not Permitted to use specified clientScope コンソールに出力されるログ Error from MCP server: StreamableHTTPError: Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_token","error_description":"Missing Authorization header"} at StreamableHTTPClientTransport.send (file:///xxx/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js:364:23) at process.processTicksAndRejections (node:internal/process/task_queues:104:5) { code: 401 } レスポンスヘッダーが確認できなかったので、curlで再検証。 2. Authorizationヘッダー未設定でリクエスト(curlで再検証) # 先ほどと同じくAuthorizationヘッダー未設定の状態で、curlを使って tools/list を呼び出します。 curl -i -X POST http://localhost:3000/mcp \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", error_description="Missing Authorization header", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp" 結果 HTTPステータスとして 401 Unauthorized が返されました。 error には、「トークン無効」と示されています。 error_description には、「Authorizationヘッダーが見つからない」と示されています。 resource_metadata には、PRMエンドポイントのURLが設定されています。 3. PRM(Protected Resource Metadata)の確認 # resource_metadata に設定されていたURLにアクセスし、認証に必要なメタデータを確認します。 curl -s http://localhost:3000/.well-known/oauth-protected-resource/mcp { "resource":"http://localhost:3000/mcp", "authorization_servers":["http://localhost:8081/realms/mcp-demo"], "scopes_supported":["mcp:tools"], "resource_name":"MCP Auth Streamable HTTP" } MCP Inspectorの場合 結果 PRMが返されました。 resource: 保護対象リソースを識別するURL authorization_servers: 認可サーバーのIssuer URL scopes_supported: 利用可能な認可スコープ 4. 有効なトークンを使って正常アクセス # 認証して有効なトークンを取得してから、 tools/list を呼び出します。 トークン取得 Keycloakからアクセストークンを取得します。 curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=mcp-demo-client" \ -d "client_secret=mcp-demo-client-secret" { "access_token":"<valid_token>", "expires_in":300, "refresh_expires_in":0, "token_type":"Bearer", "not-before-policy":0, "scope":"mcp:tools" } MCPサーバーへアクセス Keycloakから取得した access_token を使って、MCPへアクセスします。 curl -s -X POST http://localhost:3000/mcp \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer <valid_token>" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' event: message data: {"result":{"tools":[{"name":"sum_numbers","title":"sum_numbers","description":"Sum two numbers","inputSchema":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"a":{"type":"number","description":"first number"},"b":{"type":"number","description":"second number"}},"required":["a","b"]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"type":"number","description":"sum result"}},"required":["result"],"additionalProperties":false}},{"name":"get_server_policy","title":"get_server_policy","description":"Return simple authorization policy for demo","inputSchema":{"type":"object","properties":{}},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"resource":{"type":"string","description":"resource server url"},"requiredScope":{"type":"string","description":"required scope"}},"required":["resource","requiredScope"],"additionalProperties":false}}]},"jsonrpc":"2.0","id":1} 結果 正常にアクセスでき、 tools/list の結果が確認できました。 --> Information このレスポンスには 2026-07-28 RC で対応したJSONスキーマ(2020-12)の $schema も含まれています。 RCの変更内容については、 MCP 2026-07-28 RC解説 で説明しています。 MCP Inspectorで接続した場合 5.1. Authorizationヘッダーに形式違いの値を設定してリクエスト # Authorizationヘッダーに「形式不正のトークン( Bearer から始まらない値)」を設定してリクエストしてきたケースを想定した例外処理の確認。 curl -i -X POST http://localhost:3000/mcp \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -H "Authorization: bad_format_token" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", error_description="Invalid Authorization header format, expected 'Bearer TOKEN'", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp" { "error":"invalid_token", "error_description":"Invalid Authorization header format, expected 'Bearer TOKEN'" } 結果 HTTPステータスとして 401 Unauthorized が返されました。 error_description には、「無効な形式である」と示されています。 5.2. Authorizationヘッダーに無効なトークンを設定してリクエスト # Authorizationヘッダーに形式は正しいが「存在しないトークン」を指定してリクエストしてきたケースを想定した例外処理の確認。 curl -i -X POST http://localhost:3000/mcp \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unknown_token" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", error_description="Inactive token", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp" { "error":"invalid_token", "error_description":"Inactive token" } 結果 HTTPステータスとして 401 Unauthorized が返されました。 error_description には、「トークンが無効である」と示されています。 5.3. スコープ不足のクライアントでリクエスト # 「権限不足のトークン(必要なクライアントスコープが設定されていないクライアントで認証)」を使ってリクエストしていたケースを想定した例外処理の確認。 トークン取得 curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=mcp-demo-no-scope-client" \ -d "client_secret=mcp-demo-no-scope-client-secret" {"access_token":"<valid_token>","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"mcp:no-scope"} MCPサーバーへアクセス curl -i -X POST http://localhost:3000/mcp \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer <valid_token>" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer error="insufficient_scope", error_description="Insufficient scope", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp" {"error":"insufficient_scope","error_description":"Insufficient scope"} 結果 HTTPステータスとして 403 Forbidden が返されました。 error には、「スコープ不足」と示されています。 error_description には、「スコープ不足」と示されています。 5.4. Audience設定なしのクライアントでリクエスト # 「期待するAudienceが設定されてないトークン(必要なAudienceが設定されていないクライアントで認証)」を使ってリクエストしてきたケースを想定した例外処理の確認。 トークン取得 curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=mcp-demo-no-audience-client" \ -d "client_secret=mcp-demo-no-audience-client-secret" {"access_token":"<valid_token>","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":""} MCPサーバーへアクセス curl -i -X POST http://localhost:3000/mcp \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer <valid_token>" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", error_description="Resource indicator (aud) missing", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp" {"error":"invalid_token","error_description":"Resource indicator (aud) missing"} 結果 HTTPステータスとして 401 Unauthorized が返されました。 error_description には、「リソースインジケーターが見つからない」旨が示されています。 5.5. 異なるAudienceが設定されているクライアントでリクエスト # 「別リソース向けのトークン(期待されるAudienceとは異なる値を設定したクライアントで認証)」を使ってリクエストしてきたケースを想定した例外処理の確認。 トークン取得 curl -s -X POST http://localhost:8081/realms/mcp-demo/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=mcp-demo-diff-audience-client" \ -d "client_secret=mcp-demo-diff-audience-client-secret" {"access_token":"<valid_token>","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"mcp:diff-audience"} MCPサーバーへアクセス curl -i -X POST http://localhost:3000/mcp \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer <valid_token>" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", error_description="Expected audience compatible with http://localhost:3000/mcp, got: http://localhost:3000/mcp-diff", scope="mcp:tools", resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource/mcp" {"error":"invalid_token","error_description":"Expected audience compatible with http://localhost:3000/mcp, got: http://localhost:3000/mcp-diff"} 結果 HTTPステータスとして 401 Unauthorized が返されました。 error_description には、「期待しているAudienceが異なる」旨が示されています。 まとめ # Streamable HTTPでは、MCP仕様に基づく認証・認可の仕組みを利用できます。 MCPクライアントは、 401 レスポンスの WWW-Authenticate ヘッダーやPRMから認可サーバーを発見し、アクセストークンを取得します。 MCPサーバーは、受け取ったBearerトークンを検証し、保護対象リソースへのアクセスを許可または拒否します。 今回の検証コードでは、Keycloakとトークンイントロスペクションを使い、MCPサーバー側の認証・認可処理を確認しました。 Authorizationヘッダーの未設定や形式不正、無効なトークン、Audienceの不備を401として拒否し、Scope不足を403として拒否できることも確認しました。 実際にMCPサーバーを公開する際は、利用者やクライアントごとに必要なScopeを定義し、保護するリソースに応じたAudienceを検証することが重要です。
はじめに # 先月、GitHub のスタック PRs (Stacked pull requests) がパブリックプレビューになりました。 https://github.blog/changelog/2026-07-30-stacked-pull-requests-are-now-in-public-preview/ これまでも PR のブランチからブランチを生やして、PR スタックさせることは可能でした。小さい PR をスタックすることで、1つの PR は小さくレビューしやすくなりますが、スタックのメンテナンスにコストがかかります。 スタック PRs でどこまで効率がアップするのか、見ていきたいと思います。 概念 # PR を作るとき、ある修正 A を前提に 修正 B を入れたいケースで、未マージの A ブランチから B ブランチを作成して作業するということはよくあります。 1つ目の PR は base を main へ 2つ目以降は、直前の PR ブランチを base にする gitGraph commit id: "Initial commit" branch feature-1/setup commit id: "Add skeleton" branch feature-2/logic commit id: "Add logic" branch feature-3/docs commit id: "Add docs" checkout main merge feature-1/setup merge feature-2/logic merge feature-3/docs 小さい PR をスタックさせることで、1つ1つの PR の規模が小さくなり、レビューも楽になるというメリットがあります。しかし、PR をスタックさせると、PR 説明にベースブランチを書いたり、中間のスタックで発生した変更を上位のスタックのブランチにリベースして反映するという作業が頻発します。 スタック PRs は「特定のブランチを起点にした一連の PR スタック」に纏わる煩雑な作業を大幅に削減してくれます。 検証環境とドキュメント # 本記事では、以下のバージョンで試しています。 GitHub CLI (gh): 2.98.0 gh-stack プラグイン: 0.1.0 GitHub CLI をインストールし、 gh auth login は済ませておきましょう。以下のコマンドでスタック PRs のプラグインがインストールされます。 gh extension install github/gh-stack 公式ドキュメントの概要説明とクイックスタートは以下のページです。 https://docs.github.com/en/pull-requests/get-started/about-stacked-prs https://docs.github.com/en/pull-requests/get-started/stacked-prs-quickstart スタック PR のコマンドリファレンスは以下のページにあります。 https://docs.github.com/en/pull-requests/reference/stacked-prs-cli-commands サンプルのリポジトリ構成 # 簡単なリポジトリを作って検証していきます。 main ブランチには README.md だけを置きました。この時点でリモートに push してしまって OK です。 . └── README.md feature/01-setup ブランチ。ソースコードやドキュメントを追加しました。 . ├── README.md ├── docs │ └── notes.md └── src └── hello.py feature/02-add-logic ブランチ。ドキュメントとソースコードを追加しています。 ├── README.md ├── docs │ ├── notes.md │ └── stacked-pr.md └── src ├── calculator.py └── hello.py feature/03-add-docs ブランチ。ドキュメントを追加しています。 . ├── README.md ├── docs │ ├── notes.md │ ├── stacked-pr.md │ └── usage.md └── src ├── calculator.py └── hello.py スタックの初期化 # 以下を実行して、3 本のブランチを 1 つのスタックとして初期化しました。 $ gh stack init --base main feature/01-setup feature/02-add-logic feature/03-add-docs スタックが作成されました。 ? Enable git rerere to remember conflict resolutions? (Y/n) ✓ Adopted 3 branches: main ← feature/01-setup ← feature/02-add-logic ← feature/03-add-docs You're on feature/03-add-docs (top of stack). What's next: • see the full stack: gh stack view • move between branches: gh stack switch • link these PRs into a Stack on GitHub: gh stack submit ローカルのブランチ関係は main を起点にした 3 段のスタックとして認識されました。 スタックを確認します。 gh stack view スタックを構成する各ブランチと変更内容が一覧できます。 ▶ feature/03-add-docs (current) +61 -12 │ ▾ 2 files changed │ README.md +58 -12 │ docs/usage.md +3 -0 │ ▸ 2 commits │ ├ feature/02-add-logic +6 -0 │ ▸ 2 files changed │ ▸ 1 commit │ ├ feature/01-setup +4 -0 │ ▸ 2 files changed │ ▸ 1 commit │ └ main --> Information この例では、3つのブランチを init で一括追加しましたが、単独のブランチを追加する場合は、 gh stack add でブランチをスタックに追加可能です。ただし、これはブランチ作成とスタックへの追加を同時にやるコマンドです。既存のブランチを追加する方法は現時点では提供されてないようです。 gh stack add <branch> --> Information 今回、まだ GitHub のリポジトリを作っていなかったので、作成後に以下のコマンドで、origin を設定しました。初回は、スタックの土台になる main ブランチを先に push しておくのがポイントです。既存のリポジトリからクローンしている場合は不要です。 git remote add origin https://github.com/kondoumh/stacked-pr-demo.git gh stack submit を使って、リモートにスタックの情報を反映していきます。TUI 画面が開いて各 PR の title と description を編集できます。あわせて ready for Review と Draft のどちらの状態で作成するのかも選べます。 スタックの最上位を選択すると、画面右下に SUBMIT 3 PRs というボタンが表示されます。 このボタンは、マウスでクリック可能です。クリックするとリモートリポジトリに対して反映が開始されます。 gh stack submit Checking stack state... Pushing to origin... ✓ Created PR #5 for feature/01-setup ✓ Created PR #6 for feature/02-add-logic ✓ Created PR #7 for feature/03-add-docs ✓ Stack created on GitHub with 3 PRs (stack #8) ✓ Pushed and synced 3 branches Web UI で確認すると、3つのブランチとそれに対応する PR が作られていることが分かります。 --> Information PR が #5 から始まっていますが、これは一度スタックが不整合になってやり直したからです。 stack のクリーンアップは以下のように unstack で可能です。 # ローカル gh stack unstack --local # origin 側 gh stack unstack スタックの2番目の PR の画面です。マージの UI が通常の PR と異なり、スタック構造が表示されています。 ブランチのリベース # PR をスタックして作業しているときよくあるのが、途中のブランチに変更を入れ、上位のブランチをリベースするという作業です。スタック内のブランチで作業してリベースを試してみましょう。 feature/02-add-logic で calculator.py にロジックを追加しました。(既存の add に multiply を追加) @@ -1,2 +1,5 @@ def add(a: int, b: int) -> int: return a + b + +def multiply(a: int, b: int) -> int: + return a * b git status On branch feature/02-add-logic Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: src/calculator.py コミットします。 git commit -m 'add logic to calculator.py' スタックの上位である feature/03-add-docs にはこの変更がまだ反映されていません。従来ですと、feature/03-add-docs をチェックアウトし、git コマンドでリベースする必要がありました。スタック PRs の場合、作業したブランチの上位スタックに変更を反映させるには、以下のようにするだけです。 gh stack rebase --upstack feature/02-add-logic の内容が、リベースの連鎖により波及していきます。 ✓ Fetched latest main from origin ✓ Trunk main is already up to date Stack detected: (main) <- feature/01-setup <- feature/02-add-logic <- feature/03-add-docs Rebasing branches in order, starting from feature/02-add-logic to feature/03-add-docs ✓ Rebased feature/02-add-logic onto feature/01-setup ✓ Rebased feature/03-add-docs onto feature/02-add-logic All upstack branches from feature/02-add-logic rebased locally with main (c137e85) To push up your changes, run `gh stack push` feature/03-add-docs をチェックアウトして git log を見ると、先ほどの calculator.py の変更コミットの上に、変更がリベースされているのが分かります。 commit 8279783de1fb932c4754b59cb01ad69cf7f7c3da (HEAD -> feature/03-add-docs) Author: Copilot <copilot@example.com> Date: Sun Aug 23 12:51:14 2026 +0900 Add workflow commit 3b0f02f3523811819e0eaffda2c28f6443f8ff28 Author: Copilot <copilot@example.com> Date: Wed Aug 5 23:00:29 2026 +0900 Document stacked PR workflow commit 177241ca3fdd9c99462dc90b40f1072600e8fd3f (feature/02-add-logic) Author: Copilot <copilot@example.com> Date: Sun Aug 23 15:45:01 2026 +0900 add logic to calculator.py # 計算ロジック追加のコミット --> Information --upstack オプションをつけなければ、main からすべてのブランチに必要なリベースをやってくれます。 gh stack rebase また、波及の途中でコンフリクトが発生すると処理が一時停止します。対象のファイルを修正して git add した後、gh stack rebase --continue を打てば再開できます。gh stack rebase --abort でスタック全体を元の状態に巻き戻せます。 リベースを含む変更をリモートに反映するには、 gh stack push を実行します。 $ gh stack push Pushing 3 branches to origin... ✓ Pushed 3 branches Run `gh stack view` to see your stack of PRs PR にもちゃんと反映されています。 スタック PRs のマージ # スタックのマージは、任意の階層で可能です。2番目のスタックの PR 画面でマージボタンを押してみます。 スタックの順にマージされました。 最上位のスタックの PR 画面です。自動的にベースブランチが main に切り替わっていました。 --> Information main への切り替えは少しラグがあるようです。マージ済みのブランチのままマージボタンが押せる状態でした。試してはいませんが、万が一マージしてしまっても、revert して、ベースブランチを手動で main にすれば OK だと思います。 事故らないためには、マージ後にヘッドブランチを自動削除する(Automatically delete head branches)」を有効にしておいた方がいいでしょう。 マージキューとの組み合わせ # 今回試せていませんが、マージキューと組み合わせると、マージ作業自体からも解放されます。スタック PR(main ← PR1 ← PR2 ← PR3)で一番怖い事故は、順序の逆転や依存関係が壊れることです。 マージキューなし: 「まず PR1 が承認されてマージされたか確認して、次に PR2 の CI が通るのを待ってからマージボタンを押して…」と、人間が順番を見張る必要があります。 マージキューあり: 承認された PR をキューに放り込んでおけば、システム側が依存関係の順序を守って、自動的に検証・マージを順番に処理してくれます。 これにより、「途中の PR で main が壊れるリスク」を完全にゼロにできます。 開発体験としては、レビュー完了=即「手放し」ができ、レビューアから Approve をもらったら、「とりあえずキューに追加して自分は次のタスクに行く」というムーブが可能になります。 --> Information かなり前ですが、マージキューの紹介記事もあります。 /blogs/2023/02/15/github-pr-merge-queue/ さいごに # AI がコードを書いてくれることにより、PR 作成は爆速になり、1つの PR に含まれるファイルも多くなっています。人のレビューが滞留してしまうという現象があちこちの現場で起きていると思います。 PR が巨大になる理由として、開発者の心理的な側面もあります。あるタスクをやっていると、タスクに関係ない変更も入れちゃったりしがちです。スタックさせることは可能ですが、スタックに纏わる作業が面倒なので、まとめて作ってしまうためです。 スタック PRs が使える環境であれば、「あ、この修正は元の PR のスコープ超えてるな。」と思ったら gh stack add で新しいブランチをスタックに積めばいいのです。 システムがスタックの管理を肩代わりすることで、「小さく作って、小さくレビューしてもらう」というベストプラクティスを自然に維持できるようになります。 人力によるスタック PR と 公式スタック PRs の違いをまとめておきます。 比較項目 従来の手動スタック (main ← PR1 ← PR2) 公式 Stacked PR サポート スタックの可視化 説明欄に手動で「#100 に依存」などと書く必要があった PR 画面上でスタックの全体像・前後関係が UI として表示される 親 PR マージ時の挙動 親がマージされた後、子のベースブランチを手動で main に向け直す必要があった 親 PR がマージされると、子 PR のベースブランチが自動的に更新される 親 PR 修正時の追従 親 PR にレビュー指摘でコミット追加や rebase が入ると、子 PR での rebase --onto が地獄 スタック内の変更伝搬や差分管理がGitHub 上の導線で扱いやすくサポートされる レビューアの負担 「どこからどこまでがこの PR 自体の差分か」を見失いやすい スタックの文脈を保ったまま、PR ごとの純粋な差分だけに集中してレビューできる GA になって、いろんな現場に普及するのが待ち遠しい機能ですね。
はじめに # アジャイルグループの石田です。 以前掲載した スクラムマスターのAI活用を考える の連載で、透明性・検査・適応の三本柱をAIで強化するアプローチを紹介してきました。 今回はその実践編として、私が現在のプロジェクトで実践している、AIの発信を起点にデイリースクラムを進める取り組みを紹介します。 ここまで紹介してきたAI活用は、あくまで人間が「AIを使いに行く」ものでした。 分析してほしいデータがあるときにAIに相談する、文字起こしを使ってレトロスペクティブの評価をするといった使い方では、発信の起点は常に人間側にあります。 しかし私が目指しているのは、その関係を逆転させた世界です。 AIがチームの状態を検査して発信し、人間がその結果を起点にスクラムイベントを進めていく。 本記事では、その第一歩としてデイリースクラムで実践している取り組みを具体的に紹介します。 進捗報告になりがちなデイリースクラム # 多くのチームで、デイリースクラムは形骸化しがちです。 「昨日やったこと、今日やること、困っていること」を順番に共有するだけの進捗報告会になり、15分をなんとなく消化して終わってしまうチームも少なくないでしょう。 本来デイリースクラムは、単なる作業報告の場ではありません。 スプリントゴールに対して自分たちが順調に進んでいるかを検査し、必要であれば計画を適応させる場であるべきです。 しかし、この「スプリントゴールへの検査と適応」を毎日きちんと行うのは簡単ではありません。 昨日から今日にかけてチケットの状態がどう変わったのか、その変化がスプリントゴール達成にとって良い兆候なのか悪い兆候なのかを、人間が毎朝正確に把握するのは負荷が高いからです。 そこでAIの出番です。 チケットの日々の変化を蓄積・分析し、スプリントゴールとの関係を客観的に評価します。 実践:GASでチケット履歴を蓄積し、Geminiで分析する # 私が実践している仕組みは、大きく2つのステップで構成されています。 Google Apps Script (GAS) で、現在のスプリントにあるJiraチケットとそれに紐づくサブタスクの状態を毎日取得し、Googleドキュメントに時系列で蓄積する 蓄積された履歴をGeminiに分析させ、スプリントゴールへの検査結果を出力させる Geminiによる分析結果を情報源としてデイリースクラムを実施する ステップ1:GASでチケットの日次履歴を蓄積する # まず、Jira APIを通じてGoogleドキュメントにJiraのデータを記録していくGASを用意します。GASコードの作成自体も、以前の記事と同様に生成AIを使用します。 ドキュメントの内容は、まず冒頭に現在進行中のスプリントのスプリントゴールを書き、そのあとはスプリントに含まれるストーリーやタスクなどのチケットと、それに紐づくサブタスクの変更履歴を毎日追記していきます。 サブタスクの変化も追うことでデータ量は増えてしまいますが、実際の作業の進み具合や滞留を捉えることができます。 実際にJiraから取得している内容は、おおむね以下のようなものです。 === 2026-08-05 のスナップショット === sprint_name: スプリント 21 sprint_goal: ユーザー通知機能をリリース可能な状態にする target_date: 2026-08-14 id: prj-1421 - 通知設定のデータモデル定義 status: Done assignee: 田中 story_points: 2 change_log: 2026-08-13 15:23 status(InProgress -> Done) id: prj-1422 - 設定更新APIエンドポイントの実装 status: In Progress assignee: 田中 story_points: 1 change_log: 2026-08-13 15:45 status(To Do -> InProgress) ステップ2:Geminiで検査・適応の観点から分析する # 蓄積したドキュメントを、デイリースクラム前にGeminiに分析させます。 システムプロンプトをカスタム指示としてGemに登録しておくと、毎朝同じ観点で安定した分析を得られて便利です。 分析結果をデイリースクラムで利用するため、特に下記の内容が重要になります。 スプリントゴールの現在地 :チケットやサブタスクの変更履歴とスプリントゴールをAIが分析することで、スプリントゴールの達成に対して開発が今どのような状態にあるかを検査します。 メンバー別・本日のアップデート&問いかけ :チケットにアサインされているメンバーごとに直近の作業状況を整理し、その状況を元に、スプリントゴールの達成をブロックしている要素がないかなどをAIが具体的に問いかけます。 実際のデイリースクラムでは、これらの情報を元に各メンバーの作業状況を確認し、AIが行ったスプリントゴールに対する検査の妥当性を確認したうえで、必要であれば適応を行います。 これにより、デイリースクラムをただの進捗報告に終わらせず、メンバーが自然とスプリントゴールに向かって一丸となれます。 作成したGemのプロンプトの一部を紹介します。 # 役割 あなたは優秀なアジャイル・スクラムマスターであり、デイリースクラムのファシリテーターです。 提示されるJiraのログを読み解き、チームがスプリントゴールを達成するために、 本日のデイリースクラムで確認すべきポイントを整理して、会議の起点となる発言を生成してください。 # 入力データの特性・運用ルール (中略) # 思考プロセス・指示 (中略) # 出力フォーマット 以下の構成で出力してください。 デイリースクラムの場でそのまま上から順に気持ちよく読み進められる、 プロフェッショナルかつ親しみやすいトーンを徹底してください。 皆さんおはようございます。本日のデイリースクラムを始めます。 ## スプリントゴールの現在地 [ゴール項目]:(進捗感や残りのタスク量・作業タイプを踏まえた分析を1〜2行でコンパクトに記述) ## メンバー別・本日のアップデート&問いかけ [メンバー名 / またはペアプロ等の場合は連名] 直近の状況:(直近の稼働日における、担当チケットの動きやchangelogの要約) AIからの問いかけ:(状況や作業タイプを踏まえ、ブロックしている要素がないかなど具体的に問いかける文章) AIの発信からスクラムが動く # この仕組みの本質は、デイリースクラムの起点がAIの発信になることにあります。 従来であれば、メンバーが一人ずつ状況を報告し、その内容を聞いて誰かが問題に気づく、という流れでした。 しかしこの実践では、デイリースクラムが始まる前にGeminiがすでにスプリントゴールへの検査を終えています。 チームはAIが提示した「今日議論すべき論点」を出発点にデイリースクラムを始められます。 「AIが問いかけている、田中さんが担当するAPI実装のブロッカーについて、まず話しましょう」というように、最も重要な検査と適応から会話がスタートするのです。 これは、私が目指す「AIを使いに行くのではなく、AIの発信からスクラムが動いていく世界」の始まりです。 人間はAIに問い合わせる手間から解放され、AIが差し出した気づきに対して「どう適応するか」という、人間にしかできない意思決定に集中できます。 スクラムマスターとしての展開 # 私はこの仕組みを作り、チームに展開しました。最初の2週間ほどは、データとプロンプトを調整しながら私自身がGeminiで分析を回し、その結果をSlackで共有するテスト運用を行いました。 ある程度安定して運用できるようになった段階で、継続するかどうかをチームに委ね、現在はチームが自律的にこの仕組みを使っています。 スクラムマスターとしては、デイリースクラムの検査と適応を強化する選択肢をチームに示したうえで、実際に使うかどうかはチームに任せることが大切だと思います。 AI起点のデイリースクラムによるチームの変化 # 実際のデイリースクラムは、下記のような流れで運用しています。なお、チームは全メンバーがフルリモートで稼働しています。 当日のファシリテート担当がデイリースクラム前にGeminiを動かし、結果をSlackに投稿する。 分析結果をもとに、まずスプリントゴールの現在地を把握し、続いてAIが示したメンバー別の問いかけを確認する。 今のままでスプリントゴールを達成できそうかを、「はい」「いいえ」「微妙」の3つのいずれかでZoomのリアクションとして示す。 「いいえ」や「微妙」があった場合は、その内容を確認し、必要に応じて作業計画を変更する。 デイリースクラムのプラクティスとして「スプリントゴールチェックイン」というものがあります。 これは、デイリースクラムの冒頭で「このままいけばスプリントゴールを達成できそうか?」を開発者全員に問いかけることで、スプリントゴールへの検査を強化するというものです。 上記の手法は、これにAIによる補助を加えることで、スプリントゴールへの検査と適応をさらに強化した形になっています。 これまでのチームでは、デイリースクラムでそれぞれの作業状況の共有はできていたものの、その作業がスプリントゴールに対して正しく向いているのかを検査できていない、という問題がありました。 それがAIの分析によって作業状況の透明性が高まり、さらにスプリントゴールへの検査までAIが担うことで、その結果を踏まえた適応の議論がチーム内で自然と生まれるようになりました。 想定される疑問 # Geminiを手動で動かしているのなら、AI起点とは言えないのでは? # AIの発信を起点にすると言いながら、結局Geminiを人間が動かしているのであれば、起点はやはり人間なのではないかという疑問です。 これはその通りで、本来であればAPIなどを使って分析を自動化し、デイリースクラム前に毎日Slackなどへ検査結果が投稿される形が理想だと思います。 しかし今回の手法は、私が携わっているプロジェクトではAIに関するAPIが使用できない、という制約条件を加味したうえでのものでした。 本記事を参考に実践してみたい方は、ぜひより高度な自動化に挑戦してみてください。 Jiraに組み込まれたAIでチケットの分析は可能なのでは? # Jiraには現在、Rovoと呼ばれるAIエージェントが組み込まれています。これを使えば、ここまでの手間をかけずともスプリントの分析は可能でしょう。 しかしプロンプトの自由度や出力結果の安定性という面を考えると、今回のようなシステムプロンプトを用いた方法は十分に有用だと考えています。 実践する際の注意点 # AIの検査は「たたき台」である # Geminiの検査結果は非常に有用ですが、あくまでチケットのデータから読み取れる範囲の分析です。 チケットには表れないコンテキスト、たとえば「このチケットが止まっているのは意図的に優先度を下げたから」といった背景までは把握できません。 AIの発信を出発点にしつつ、その指摘が的を射ているかをチームが判断する。この対話こそがデイリースクラムの価値であり、AIの分析を鵜呑みにすることではありません。 ドキュメントの肥大化には注意 # JiraチケットのChangelogを含むデータを毎日書き込んでいると、ドキュメントのデータ量、つまりAIへの入力量が肥大化していきます。 スプリントごとに新しいドキュメントに切り替える、前日に変更があったものだけを書き込むといった、データ量を抑える工夫が必要です。 仕組み自体もふりかえる # この仕組みがチームの役に立っているかどうかも、定期的にふりかえる対象です。 AIの発信が的外れだったり、かえって議論をミスリードしたりするようであれば、プロンプトや蓄積するデータの内容を見直します。 またスクラムマスターとしては仕組みを提供するまでが役割です。チームにとって意味をなさない、運用負荷が高すぎるといった結論に至ったのであれば、利用を停止する判断も必要です。 まとめ # 本記事では、スクラムマスターのAI活用を考える連載の実践編として、デイリースクラムをAI起点で回す取り組みを紹介しました。 GASでJiraチケットとサブタスクの日次履歴をGoogleドキュメントに蓄積する Geminiのカスタム指示(Gem)で、スプリントゴールの現在地とメンバー別の問いかけを毎朝生成する その分析結果を起点にデイリースクラムを始め、スプリントゴールチェックインと組み合わせて検査と適応を強化する この仕組みによって、これまで作業報告に終始しがちだったデイリースクラムが、スプリントゴールへの検査と、それを踏まえた適応を話し合う場へと変わりました。 作業状況の透明性、スプリントゴールへの検査、ゴール達成に向けた適応という、スクラムの三本柱をAIによって着実に強化することができます。 まだ道半ばの取り組みですが、AIがチームの状態を検査し続け、人間はその気づきをもとに適応へ集中する。 そんなAI起点のスクラムの世界を、これからも実践を通じて探求していきたいと思います。
はじめに # 私が4年間取り組んできた ETロボコンの活動の軌跡 を紹介します。 昨年度はベーシッククラス(当時の名称:プライマリークラス)に挑戦し、AI活用を取り入れた新しい開発スタイルにも踏み込みました。 これまでの経験がどのように積み重なり、昨年度の成果につながったのかをまとめています。 最後までお付き合いいただければ幸いです。 4年間の活動をひとことで紹介すると # 「設計(UML)→ 実装 → 実機制御 → チーム運営 → AI活用」 と、毎年新しい挑戦を積み重ねてきました。 1年目 :エントリークラスで基礎を学ぶ 2年目 :モデル評価がB→Aへ成長 3年目 :後輩サポートで理解が深化 4年目 :ベーシッククラス挑戦+AI活用という新フェーズへ ETロボコン 4年間の歩み この4年間で、技術面もチーム面も大きく成長できたと感じています。 1. 参加のきっかけ:ロボットが動く"あの瞬間"をもう一度 # 私がETロボコンに参加し始めた理由は、高専時代のロボコン経験にあります。 試行錯誤して作ったロボットが初めて思い通りに動いた瞬間の高揚感。 「技術で仲間と何かを成し遂げる楽しさ」を、社会人になっても味わいたいと思い、参加しました。 2. ETロボコンとは:ソフトウェア重視の"教育型ロボコン" # ETロボコンは、 組込みソフトウェア技術の教育を目的としたロボットコンテスト です。 一般的なロボコンがハードウェアの工夫や機構設計を競うのに対し、ETロボコンは 「ソフトウェア設計(UML)と走行結果の両方を評価する」 という点が大きな特徴です。 詳しくはETロボコン公式サイトをご覧ください。 https://www.etrobo.jp/ --> Information ETロボコンは2002年から開催されており、2026年で25年目を迎える長寿コンテストです。組込み技術者育成への貢献が評価され、 経済産業大臣賞を受賞 した実績もあります。 ETロボコンの目的 # 組込みソフトウェア技術者の育成 分析・設計・制御モデリングを含む PBL(Project-Based Learning) の実践 初心者からベテランまでが学び合える教育の場の提供 全国規模の大会構成 # ETロボコンは全国12地区で地区大会が開催され、 上位チームは全国大会(チャンピオンシップ大会)へ進出します。 レベルに合わせて選べる3つのクラス # エントリークラス(シミュレータ部門) 初心者向け。シミュレーターで参加でき、設計と実装のつながりを学べる。 ベーシッククラス(フィジカル部門) 旧プライマリークラス 初級〜中級者向け。モデル表現力やチーム開発を学ぶ。 アプライドクラス(フィジカル部門) 旧アドバンスドクラス 実務経験者・上級者向け。制御技術やAIなど高度な要素を取り入れ、システム全体を可視化しながら開発を学ぶ。 ソフトウェアで勝負するロボコン # ハードウェアは共通の市販キットを使用し、 「設計(モデルシート)+走行結果」 の総合成績で評価されます。 モデルシートの信頼性・一貫性・表現力が重要で、 走行タイムや難所クリアによるボーナスタイムと合わせて順位が決まります。 技術教育イベントが充実 # 技術教育(オンライン/オフライン) モデリングワークショップ 試走会 地区大会での交流会 チャンピオンシップ大会での成果発表 企業・学生・教育者が同じテーマで議論できる貴重な場となっています。 参加者数と実績 # 全国12地区で大会を開催 累計 4,095チーム / 22,700名が参加 (公式発表) 教育効果の高さから、企業研修や大学の授業にも広く活用されています。 大会との関わり # 豆蔵は大会に参加するだけでなく、地区ゴールドスポンサーとして大会を支援しており、 技術教育への取り組みを企業として積極的に行っています。 3. これまでの参加年ごとの振り返り # 1年目:エントリークラス挑戦(+アドバンスドクラスサポート) # 初参加で手探り状態ながら、モデル作成と走行調整に取り組みました。 アドバンスドクラスのサポートも経験し、上位クラスのレベルの高さを実感しました。 2年目:モデル評価がB → Aへ成長 # モデリングに重点を置いて取り組んだ結果、評価がBからAへ向上。 設計の重要性を強く感じた年でした。 3年目:後輩サポートで理解がさらに深まる # 自分がA評価を受けたモデルをベースに、後輩へ「どうすればモデルが良くなるか」を伝える立場に。 教えることで自分の理解も深まり、チーム全体の底上げにつながりました。 4. 昨年度(4年目)の取り組み:ベーシッククラス挑戦と"AI活用"という新しい武器 # 昨年度は大きな転換点でした。 エントリークラスから一歩進み、 ベーシッククラスに挑戦 。 さらに今年度は、 AIを積極的に活用した開発スタイル にも踏み込みました。 モデルのレベルアップ # 複雑な制御に対応するため、状態遷移やクラス構造の精度を高め、設計段階での思考が大幅に深化しました。 AIを活用したモデルのブラッシュアップ # AIを"設計レビューの相棒"として活用し、 モデル改善案の生成 抜け漏れチェック UML表現の最適化 など、設計品質の向上に役立てました。 実機を使ったロボット制御の習得 # センサー値の取得、PID制御、ライン追従など、実機ならではの学びが多く、組込み制御の理解が深まりました。 AIを用いたコーディング # コード生成やリファクタリングの提案など、AIを活用した開発スタイルにも挑戦しました。 結果 # モデルは シルバーモデル(2位) を獲得。 走行は課題が残ったものの、技術的な挑戦の幅は過去最大で、次年度につながる大きな経験となりました。 5. 今後の展望:UML × 実機 × 制御 × AI の4本柱をさらに深化 # 今年度は昨年度の経験を踏まえ、 UMLによる設計力、実機制御、制御技術、そしてAI活用 という4本柱をさらに強化していく予定です。 その取り組みの一環として、さらなるモデル作成のレベルアップを目標に アプライドクラスへ挑戦 しています。 昨年度は走行に課題が残りましたが、今年度はコース環境を整備し、 制御技術の底上げにも本格的に取り組んでいます。 AI活用については、 モデル改善 コード生成 設計レビュー など、開発プロセス全体を支える重要な技術として位置づけています。 最後に # ETロボコンは、モデリング、コーディング、実機調整、AI活用など、幅広い技術を実践的に学べる場です。 ロボットが動く瞬間のワクワクと、技術が積み重なっていく手応えを、これからも大切にしていきたいと思います。
はじめに # Model Context Protocol (MCP) は、2024年11月の初版リリース以来、AIモデルと外部データ・機能を接続する標準プロトコルとして急速に進化してきました。 今回の2026-07-28 Release Candidate(以下、RC)は、単なる機能追加ではなく、プロトコルの実運用前提を大きく変える改訂です。 とくに注目すべきは、プロトコル層のステートレス化です。 セッション管理を外し、ロードバランシング・再試行・観測性・キャッシュ運用をHTTP標準に近い形へ寄せることで、エンタープライズ環境でも扱いやすい構成へ移行しようとしています。 本ページでは、この改訂内容を仕様差分と実装視点で、RCの要点、仕様の変遷、具体的な変更点という流れで説明します。 公式ブログ: The 2026-07-28 MCP Specification Release Candidate RCの要点 # 今回のRCは「機能追加」より、「実運用前提の再設計」が主眼になります。 とくに、プロトコル層セッションを廃止してステートレスにシフトすることで、ロードバランシング、再試行、観測性、キャッシュ運用がHTTP標準の作法に近づきました。 さらに、Extensions(MCP Apps/Tasks)、認可強化、非推奨ライフサイクルが同時に導入され、仕様の進化プロセスも整備されています。 最大のBreaking Changeは、プロトコル層セッションを廃止し、 ステートレスファースト へ移行 Streamable HTTPは、 自己完結リクエスト+必須ヘッダー へ移行 サーバー起点の入力要求は、SSE維持型から InputRequiredResult+再送 のマルチラウンドトリップへ移行 Dynamic Client RegistrationはClient ID Metadata Documentsへの移行が推奨される MCP AppsとTasksは、Extensionとして正式化 入出力スキーマがJSONスキーマ(2020-12)に準拠 MCP仕様の変遷 # timeline 2024-11-05 初版リリース : コアアーキテクチャの定義 (Tools, Resources, Prompts) : 周辺機能の定義 (Roots, Sampling, Logging, HTTP+SSE) 2025-03-26 通信と認証の強化 : StreamableHTTP導入 : OAuth2による認証整備 2025-06-18 プロトコルの簡素化、構造化 : JSON-RPCバッチ処理の廃止 : 構造化出力 (structuredContent)対応 2025-11-25 UXと大規模運用の布石 : 段階的スコープ同意 (認証強化) : アイコン表示対応 : Tasksの実験的サポート 2026-07-28 ステートレスへの転換 : ステートレス化 : キャッシュ管理の導入 : ルーティング最適化 : 初期機能の非推奨化 : MCP Apps : Tasksを拡張へ移動 2026-05-21: RC確定 2026-07-28: 最終仕様の公開予定 非推奨確定~最低12カ月以内: 非推奨機能の削除禁止期間 仕様変更の詳細 # 今回予定されている仕様変更は下記の通りです。 🚨はBreaking Changeを示します。 プロトコルのステートレス化: initialize /セッション依存を廃止し、自己完結リクエストへ移行 🚨 リクエストモデルの変更: 必須ヘッダーと _meta を毎リクエストで送信 🚨 レスポンスモデルの変更: resultType が必須化 🚨 ハンドシェイクとセッションの廃止: initialize , initialized , Mcp-Session-Id を廃止 ステートレスプロトコルにおけるステートフルアプリケーション: Explicit Handle方式で状態を引き回す 🚨 サーバーからクライアントへのリクエストを再構築: input_required , requestState を用いた再送モデルへ 🚨 ルーティング(必須HTTPヘッダーによるトラフィック制御): ボディ解析なしでルーティング可能にし、検証を厳格化 キャッシング機構の導入: ttlMs , cacheScope で鮮度と共有可否を明示 観測性(W3C Trace Contextの伝搬を標準化): traceparent などで分散トレース相関を標準化 🚨 変更通知の再編: subscriptions/listen 中心に再編し、旧購読APIを整理 MCP Apps: サーバーレンダリングUIを公式Extensionとして扱う 🚨 Tasks APIをエクステンションへ移行: コア実験機能からExtensionへ再設計 認可の強化: OAuth2.0/OIDC運用に寄せた要件を強化 🚨 機能の非推奨化: Roots、Sampling、LoggingをDeprecated化 入出力スキーマがJSONスキーマ(2020-12)に準拠: 表現力を拡張し、バリデーション要件を明確化 ガバナンスの変更: Lifecycle、Extensions TrackおよびConformance連動を明文化 1. プロトコルのステートレス化 # これまではStreamable HTTP通信において、まず initialize リクエストでセッションを確立し、以降のリクエストには Mcp-Session-Id ヘッダーを付与する必要がありました。 2026-07-28では この接続確立フロー全体がプロトコル層から除去されます。 リクエストモデルの変更 リクエストモデルに以下の変更が入ります。 リクエストごとに _meta へプロトコルバージョンや機能宣言( capabilities )を内包します。 ヘッダーおよび _meta で指定したプロトコルバージョンが一致していること。不一致の場合は HeaderMismatchError ( -32020 )で拒否されます。 要求したバージョン自体がサーバー非対応の場合は UnsupportedProtocolVersionError ( -32022 )が返されます。 2025-11-25仕様 初回 { "jsonrpc":"2.0","id":1,"method":"initialize", "params":{ "protocolVersion":"2025-11-25", "capabilities":{}, "clientInfo":{"name":"sample-client","version":"1.0.0"} } } 2回目以降 Mcp-Session-Id: 1a2b3c4d-5e6f-7g8h { "jsonrpc": "2.0","id": 2,"method": "tools/call", "params": { "name": "get_user", "arguments":{"user_id":"u123"} } } 2026-07-28仕様 MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: get_user { "jsonrpc": "2.0","id": 1,"method": "tools/call", "params": { "name": "get_user", "arguments": { "user_id": "u123" }, "_meta": { "io.modelcontextprotocol/protocolVersion":"2026-07-28", "io.modelcontextprotocol/clientInfo":{"name":"sample-client","version":"1.0.0"}, "io.modelcontextprotocol/clientCapabilities":{} } } } レスポンスモデルの変更 レスポンスモデルに以下の変更が入ります。 resultType が必須化(取りうる値: complete , input_required ) { "resultType": "input_required", "inputRequests": { "confirm": { "type": "elicitation", "message": "3ファイルを削除しますか?", "schema": { "type": "boolean" } } }, "requestState": "1a2b3c4d5e6f7g8h9i0j..." } ハンドシェイクとセッションの廃止 通信の開始と維持に使用していたセッション管理が変更されます。 これにより、スティッキールーティングやセッション管理が不要になるため、水平スケーリングが容易になります。 項目 2025-11-25 2026-07-28 initialize , initialized ハンドシェイク 必須 廃止( SEP-2575 ) Mcp-Session-Id ヘッダー 2回目以降のリクエストに必須 廃止( SEP-2567 ) プロトコルバージョン、クライアント情報 初期化時に1回交換 リクエストごとに _meta に含める サーバーケーパビリティの取得 初期化レスポンスで受け取る 新設の server/discover メソッドで都度取得 2025-11-25仕様 sequenceDiagram actor c as MCP Client participant s as MCP Server c->>+s: initialize(セッション確立) s-->>-c: c->>+s: tools/call(<br>Mcp-Session-Id, <br>name: get_user) s-->>-c: 2026-07-28仕様 sequenceDiagram actor c as MCP Client participant s as MCP Server c->>+s: tools/call(<br>MCP-Protocol-Version: 2026-07-28, <br>Mcp-Method: tools/call, <br>Mcp-Name: get_user) s-->>-c: ステートレスプロトコルにおけるステートフルアプリケーション プロトコルがステートレスになっても、アプリケーションが状態を持つことはできます。 ステートレス化に伴い、状態管理の責任はトランスポート層からアプリケーション層(モデルとの対話)へと移動します。 セッションへ依存する替わりに、サーバーは明示的なハンドルをツールの戻り値として発行して、次回のツール呼び出し時に引き渡す設計が推奨されます。 これにより、状態がモデルの推論コンテキストに統合され、より柔軟な推論制御が可能になります。 sequenceDiagram autonumber actor client as Model participant server as MCP Server client ->> server: create_basket server -->> client: {"basket_id": "b001"} client ->> server: add_item("basket_id": "b001", arg0: xyz) 2のレスポンス: basket_id や context_token のような明示的なハンドル(Explicit Handle)をツールの戻り値として発行する必要があります。 サーバーからクライアントへのリクエストを再構築 サーバーが処理中にクライアントへ追加入力を求めるエリシエーションは、接続を維持する替わりに InputRequiredResult を返却し、それを再度リクエストで投げ直す仕組みに再設計されます。 sequenceDiagram autonumber actor client as MCP Client participant server as MCP Server client ->> server: tools/call server -->> client: 追加入力の要求(InputRequiredResult) client ->> server: 同じtools/callをinputResponses+requestStateで再送 server -->> client: 最終的な結果 サーバーからの入力要求は「アクティブなクライアント要求の処理中」に限定されます。「突然のプロンプト」は禁止され、すべてのエリシテーションはユーザーが開始したアクションに紐付く必要があります。 2のレスポンス { "resultType": "input_required", "inputRequests": { "confirm": { "type": "elicitation", "message": "3ファイルを削除しますか?", "schema": { "type": "boolean" } } }, "requestState": "1a2b3c4d5e6f7g8h9i0j..." } InputRequiredResult には requestState が必要。 関連SEP マルチラウンドトリップ: SEP-2322 Multi Round-Trip Requests サーバー発の入力要求の制約: SEP-2260 Server-initiated requests constraints ルーティング(必須HTTPヘッダーによるトラフィック制御) Streamable HTTPで下記の3ヘッダーが必須になります。 これにより、ロードバランサー、ゲートウェイ、レートリミットがリクエストボディを解析(DPI)せずルーティングできるようになります。 ヘッダーとボディの内容が一致しない場合、サーバーはリクエストを拒否します。 ヘッダー 概要 用途 MCP-Protocol-Version プロトコルバージョン(例: 2026-07-28) バージョン整合性の検証 Mcp-Method JSONメソッド名(例: tools/call ) メソッド単位のルーティング、レート制限 Mcp-Name ツール名やリソース名(例: search ) ツールおよびリソース単位のルーティング 関連SEP ルーティングヘッダー: SEP-2243 キャッシング機構の導入 リスト応答(tools/listなど)にキャッシュ機構が導入されます。 これにより、MCPクライアントはレスポンスの有効期間(ttlMs)とスコープ(cacheScope)を認知できます。 リストの変更を知る場合、これまではSSEストリームを維持して検知するのが唯一の手段でしたが、これに代替する手段として活用できます。 ttlMs: ミリ秒単位の有効期間。 cacheScope: public(共有可)またはprivate(個別ユーザー限定)。 関連SEP キャッシュ機構: SEP-2549 観測性(W3C Trace Contextの伝搬を標準化) 分散トレーシングをサポートするため、 _meta フィールドにW3C Trace Context( traceparent , tracestate , baggage )の伝播が標準化されました。 これにより、OpenTelemetry互換のバックエンドで、ホストからサーバー、さらにその先のバックエンドまでを一貫したスパンツリーとして可視化できます。 関連SEP 観測性: SEP-414 変更通知の再編 変更通知もステートレス化に合わせて再編されています。 これまでの「接続やセッションに連なる通知」から、明示的なリクエストに紐づく通知へ再編されます。 主な変更点 変更通知は subscriptions/listen のレスポンスストリームで受信する形に統一されます。 旧来の購読API( resources/subscribe , resources/unsubscribe )は整理されます。 旧来のGETベースのSSE受信および再開機構( Last-Event-ID )は対象外です。 notifications/progress および notifications/message は、 subscriptions/listen ではなく「そのリクエスト自身のレスポンスストリーム」に流れます。 クライアント実装は「変更通知ストリーム」と「個別リクエストの進捗通知」を分離して扱う必要があります。 接続断の復旧時は、過去ストリーム再開ではなく、必要なリクエストを再送する設計が前提になります。 2. MCP Apps # サーバーがHTML UIテンプレートを提供し、クライアントがサンドボックス化されたiframe内で描画する機能です。 データの可視化や複雑なフォーム入力を可能にします。 UI内のアクションはすべてJSON-RPCプロトコルを通じて伝達されるため、監査ログや同意フローの対象として統合管理が可能です。 特徴 単なるテキスト応答ではなく、MCPサーバー主導のUI体験を組み込める ツール定義時にUIテンプレートを宣言する設計が必要 セキュリティレビュー対象が「ツール実装 + UIテンプレート」に広がる 関連SEP MCP Apps: SEP-1865 3. Tasks APIをエクステンションへ移行 # 2025-11-25でコア機能として実験的に導入されたTasksが エクステンションとして再設計されます。 2025-11-25のTasks APIを実装している場合は、移行が必要です。 項目 2025-11-25 2026-07-28 位置づけ コア機能(実験的) エクステンション(正式) タスク作成 クライアント主導 サーバー主導( tools/call へのレスポンスとして返す) タスク取得 tasks/result (ブロッキング) tasks/get (ポーリング) タスク入力 tools/call の再送で対応 tasks/update で入力応答を送信 タスクキャンセル tasks/cancel tasks/cancel tasks/list あり 廃止(セッションなしでは安全なスコープを設定できないため) 関連SEP Tasks API: SEP-2663 4. 認可の強化 # 認可仕様がOAuth2.0+OIDC(OpenID Connect)の実運用に即した形で強化されます。 とくに( RFC9207 ) iss は、MCPの多サーバー接続構成で起きやすいmix-up系リスクへの対策として重要です。 Authorization Responseの iss 検証 認可レスポンス(Authorization Response)に含まれる iss パラメーターの検証が、MCPクライアント側で必須になります。 これにより、1つのクライアントが複数のサーバーと接続するMCP特有の構成において発生しやすい「ミックスアップ攻撃(認可コードを悪意あるサーバーにだまし取られる攻撃)」を低コストで対策できます。 動的クライアント登録(Dynamic Client Registration)時の application_type 宣言 動的クライアント登録する際、クライアントは適切なapplication_typeを宣言することが必須になります。 これにより、認可サーバーがデスクトップやCLIを誤ってWebアプリと判定し、セキュリティ上の理由からlocalhostのリダイレクトURIを拒否してしまうような、実装上の競合を回避します。 クライアント資格情報(Credentials)の認可サーバー(issuer)の紐づけ強化 クライアント資格情報は、それを発行した認可サーバーに厳密に紐づけされなければなりません。他の認可サーバーへの使い回しは禁止され、リソースサーバーの認可サーバーが変更された場合は再登録を義務付けています。 リフレッシュトークンを要求する方法の明文化 ステップアップにおけるスコープ蓄積動作の定義 権限の累積によって段階的に権限付与する動作(e.g. 最初にread権限を付与し、次いでwrite権限を追加。累積する形で権限を構成する) .well-known discovery suffixの定義 関連SEP Authorization Responseの iss 検証: SEP-2468 Dynamic Client Registrationの application_type 宣言: SEP-837 クライアント資格情報のissuer紐づけ: SEP-2352 リフレッシュトークン要求: SEP-2207 ステップアップのスコープ蓄積: SEP-2350 .well-known discovery suffix: SEP-2351 5. 機能の非推奨化 # 3機能が非推奨(Deprecated)になります。 非推奨から削除までは12カ月間の猶予があるため、その間に移行が必要です。 廃止機能 理由 推奨される移行先 Roots ステートフルな設計と不整合 ツールの入力パラメーター(inputSchema) 、リソースURI Sampling 責任境界の明確化 クライアント側の制御、プロバイダーAPI直接連携 Logging 業界標準の観測性ツール推奨 stdio: stderr, 構造化ログ: OpenTelemetry(W3C Trace Context) 関連SEP 機能の非推奨化: SEP-2577 6. 入出力スキーマがJSONスキーマ(2020-12)に準拠 # ツールの入出力スキーマ( inputSchema , outputSchema )がJSON Schema(2020-12)に対応しました。 これにより、 oneOf , anyOf , allOf および $ref による高度なスキーマ定義が可能になり、ツールの型安全性が向上します。 出力スキーマはオブジェクトだけでなく任意のJSON値を使用できます。 また、リソース未検出のエラーコードがMCPカスタム(-32002)から、JSON-RPC標準(-32602: 無効なパラメーター)に変更されます。 関連SEP JSON Schema 2020-12: SEP-2106 リソース未検出エラーコード変更: SEP-2164 7. ガバナンスの変更 # 今後は同規模の破壊的変更を常態化しないための仕組みも同時に導入されています。 機能ライフサイクル(Feature Lifecycle)(Active/Deprecated/Removed) 全機能にActive、DeprecatedおよびRemovedの状態を定義し、DeprecatedからRemovedまで最低12カ月の猶予を義務化します。 これにより、将来改訂での破壊的変更が予測可能になります。 Extensions Trackの正式化 新機能はまず拡張として提案および検証し、成熟したもののみコアへ取り込む運用に整理されました。 公式拡張は独立リポジトリ( ext-* )で管理され、コア仕様と独立して進化できます。 SEPとConformance Suiteの連動強化 標準化トラックのSEPは、対応シナリオがConformance Suiteに入るまでFinalに到達できません。 仕様策定と実装検証の乖離を抑え、SDK実装の相互運用性を高めます。 関連SEP Feature Lifecycle: SEP-2596 Extensions Track: SEP-2133 Conformance Suite連動: SEP-2484 SDK tier system: SEP-1777 実装者向けアクション # 実装者向けに、確認したい項目を簡単に整理します。 プロトコルのステートレス化: トランスポート層の移行 initialize , initialized 前提コードの棚卸し Mcp-Session-Id 依存コードの洗い出し リクエストヘッダーに Mcp-Method , Mcp-Name , MCP-Protocol-Version を設定 _meta に必須キー( protocolVersion , clientInfo , clientCapabilities )をリクエストごとに設定 HTTPヘッダーと _meta の一致検証失敗( -32020 )および非対応バージョン( -32022 )をハンドリング server/discover 呼び出しを実装し、対応バージョンとcapabilitiesを事前確認 レスポンスに resultType を設定 変更通知の受信設計を subscriptions/listen 前提へ移行 認可の強化: 認可運用の見直し Authorizationの iss 検証対応を確認 Dynamic Client Registrationの移行方針を整理 トレースコンテキストの伝播確認とログ収集設計を見直す 拡張機能と非推奨機能の整理 MCP Appsの導入有無を判断 Tasks APIの旧API利用箇所を抽出 Roots, Sampling, Loggingの移行計画を策定 エラーコードの変更: -32002 から -32602 SDKの更新 各言語のTier 1 SDKを最新版にアップデートし、ステートレス実装へ移行する まとめ # 今回の改訂は、単なる仕様の更新ではなく、MCPを「ステートレスなHTTP基盤上で運用するプロトコル」へ再設計するものです。 ステートレス化によるスケーラビリティ、W3C Trace Contextなどによる運用ガバナンス強化は、エンタープライズ導入における技術的障壁を下げる効果が期待できます。
はじめに # Deno 2.9 リリースおめでとうございます。 Deno 2.9 | Deno Electron 大好きな自分としても気になるのはやはり Deno Desktop です。 Tauri と同様 WebView をバックエンドにする構成と Electron と同様 Chromium ベースの構成を選べるとのことで、これは試すしかないと思いました。 公式ドキュメントは以下にあります。 https://docs.deno.com/runtime/desktop/ --> Caution Deno ブログには以下のように書かれており、2.9 時点ではデスクトップ機能は実験的段階です。 deno desktop is experimental in 2.9. The surface described here is stabilizing and some platform features are still landing. 使ってみる # まずは 2.9 にアップグレードしておきます [1] 。 deno upgrade main.ts に Deno.serve を使って普通にサーバープログラムを書きます。 main.ts Deno.serve(() => new Response( "<!DOCTYPE html><h1>Hello from Deno desktop </h1>", { headers: { "content-type": "text/html" } }, ) ); 同じディレクトリで deno desktop main.ts を実行します。 $ deno desktop main.ts ⚠ deno desktop is experimental and subject to change Check main.ts Compile main.ts to hello.dylib Embedded Files hello.dylib └── main.ts (430B) Files: 1.91KB Metadata: 1.38KB Remote modules: 12B Downloading laufey webview backend for aarch64-apple-darwin (v0.4.0) Download laufey-webview-aarch64-apple-darwin.tar.gz 97.44KiB/97.44KiB Codesigning bundle with identity "-" hello.app/Contents/MacOS/laufey_webview: replacing existing signature hello.app/Contents/MacOS/hello.dylib: replacing existing signature Bundle hello.app 最後の出力で、ルートに hello.app (macOS のアプリ実行ファイル)が生成されており、起動できます(Windows の場合は、hello.exe が生成されます)。 Deno のコンセプト通り、追加のモジュールや設定なし(Out of the box)でデスクトップアプリが生成されました。 Deno Desktop の開発体験 # HMR (Hot Module Replacement) オプション付きで起動することで、ローカルの開発サーバを立ち上げ、コード変更を検知してアプリ内容を即時更新してくれます。 deno desktop --hmr main.ts ⚠ deno desktop is experimental and subject to change Compile main.ts to file:///Users/kondoumh/Library/Caches/deno/desktop/5f4a00908e99d886/hello.dylib Embedded Files hello.dylib └── main.ts (422B) Files: 1.9KB Metadata: 1.38KB Remote modules: 12B Running desktop app with HMR (watching /Users/kondoumh/dev/deno-study/desktop/hello) Runtime loaded successfully from: /Users/kondoumh/Library/Caches/deno/desktop/5f4a00908e99d886/hello.dylib Runtime started [desktop] dylib path: "/Users/kondoumh/Library/Caches/deno/desktop/5f4a00908e99d886/hello.dylib" Listening on http://127.0.0.1:52958/ main.ts のコードを書き換えると、保存後すぐに画面へ反映されます。 --> Information Electron では HMR は標準では利用できず、別途 Forge などで開発サーバーを起動する必要があります。 https://developer.mamezou-tech.com/blogs/2024/01/29/electron-forge-introduction/ UI の ローカル HTTP サービスによる実現 # Electron (Forge など) ではローカルサーバーの利用は開発時が中心で、配布後は file:// でアセットを読む構成が一般的です。 これに対し Deno Desktop は、配布後のバイナリでもローカル HTTP サーバーを内部起動し、空きポートを自動割り当てして UI を描画します。サーバーはプロセス内で閉じており、外部公開はされません。ポート衝突を意識せずに済むのも良い点です。 この「開発時もビルド済みバイナリでも、同じ HTTP 実行モデルで UI を提供する」設計により、 開発時とデプロイ時の挙動に差がない コンテンツはブラウザとデスクトップで同じ動きをする Next.js などのフレームワークがそのままデスクトップアプリの中で動く といったメリットが得られます。 https://docs.deno.com/runtime/desktop/serving/ DevTools の起動 # Electron や Tauri と同様、DevTools によるデバッグが可能です。BrowserWindow を起動し、 openDevtools メソッドを呼ぶだけです。 const win = new Deno.BrowserWindow({ title: "My Deno Desktop App", width: 800, height: 600, }); win.openDevtools(); https://docs.deno.com/runtime/desktop/devtools/ --> Information いまのところ、DevTools のフルサポートはバックエンドを cef にしている時のみです。 以下のように、指定して起動する必要があります。 deno desktop --hmr --backend=cef main.ts バックエンドとフロントエンドの通信(Bindings) # Electron の IPC 通信は render.js と main.js を preload.js 経由でブリッジする必要があり、かなり面倒です。Deno デスクトップでは BrowserWindow にバインドした関数を bindings というグローバルオブジェクトにより簡単に呼び出すことができます。 Deno ランタイムとレンダリングバックエンドはスレッドやプロセスとして動作し、呼び出しはプロセス内チャネルを介して行われます。このサンプル構成ではソケットベースの IPC を直接扱わずに済むため、Electron の ipcMain / ipcRenderer、Tauri の invoke と比べて見通しよく書けるのが利点です。 実際のコードで見てみましょう。 const win = new Deno.BrowserWindow({ title: "Bindingsのテスト", width: 800, height: 600, }); // ========================================== // 1. バックエンド側:フロントから呼ばれる関数を登録 // ========================================== win.bind("getSystemInfo", async (userName) => { console.log(`[Deno側] フロントエンドから呼ばれました! 引数: ${userName}`); // Denoの機能を使ってOSの情報を取得 const denoVersion = Deno.version.deno; const os = Deno.build.os; // 少し重い処理をシミュレート(0.5秒待つ) await new Promise(resolve => setTimeout(resolve, 500)); // フロントエンドに返すデータ(JSON化できるものなら何でもOK) return { message: `こんにちは、${userName}さん!`, os: os, denoVersion: denoVersion }; }); // ========================================== // 2. フロントエンド側:画面のHTMLを返す // ========================================== Deno.serve(() => { const html = ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Bindings Test</title> </head> <body> <h1>Deno Desktop Bindings</h1> <button id="btn">システム情報を取得</button> <pre id="result">ここに結果が出ます</pre> <script> // ボタンが押された時の処理 document.querySelector('#btn').addEventListener('click', async () => { const resultArea = document.getElementById('result'); resultArea.textContent = "取得中..."; try { // 💡 bindings を使ってバックエンドの関数を呼び出す const data = await bindings.getSystemInfo("kondoumh"); // 結果を画面に表示 resultArea.textContent = JSON.stringify(data, null, 2); } catch (error) { resultArea.textContent = "error: " + error.message; } }); </script> </body> </html> `; return new Response(html, { headers: { "content-type": "text/html" }, }); }); アプリ画面です。 システム情報を取得 ボタンをクリックするとしばらく呼び出し中になり、結果が表示されます。 結果が表示された状態。 アプリを起動しているバックエンドでは次のようにログが出ています。 [Deno側] フロントエンドから呼ばれました! 引数: kondoumh すごくシンプルです。OS のネイティブ機能と Web UI を簡単に連携できるのがいいですね。 https://docs.deno.com/runtime/desktop/bindings/ メニュー の利用 # アプリケーションメニューの実装。 BrowserWindow の setApplicationMenu メソッド内でメニューオブジェクトを定義して渡します。 BrowserWindow にイベントリスナーを登録してメニューがクリックされた時の振る舞いを実装します。 role などは Electron と同じですね。 win.setApplicationMenu([ { submenu: { label: "File", items: [ { item: { label: "New", id: "new", accelerator: "CmdOrCtrl+N", enabled: true, }, }, { item: { label: "Open…", id: "open", accelerator: "CmdOrCtrl+O", enabled: true, }, }, "separator", { item: { label: "Save", id: "save", accelerator: "CmdOrCtrl+S", enabled: true, }, }, { role: { role: "quit" } }, ], }, }, { submenu: { label: "Edit", items: [ { role: { role: "undo" } }, { role: { role: "redo" } }, "separator", { role: { role: "cut" } }, { role: { role: "copy" } }, { role: { role: "paste" } }, ], }, }, ]); win.addEventListener("menuclick", (e) => { const detail = (e as CustomEvent).detail; switch (detail.id) { case "new": console.log("New clicked"); break; case "open": console.log("Open clicked"); break; case "save": console.log("Save clicked"); break; } }); コンテキストメニューの実装。 Deno.MenuItem の配列を作成して、BrowserWindow の showContextMenu に座標とともに渡します。 const contextMenu: Deno.MenuItem[] = [ { item: { label: "Copy", id: "copy", enabled: true } }, { item: { label: "Paste", id: "paste", enabled: true } }, "separator", { item: { label: "Properties…", id: "props", enabled: true } }, ]; // Trigger from a right-click. The webview may not forward the browser // `contextmenu` event, so handle the secondary mouse button on the window. win.addEventListener("mousedown", (e) => { if (e.button === 2) { win.showContextMenu(e.clientX, e.clientY, contextMenu); } }); win.addEventListener("contextmenuclick", (e) => { if (e.detail.id === "copy") { console.log("Copy clicked"); } if (e.detail.id === "paste") { console.log("Paste clicked"); } if (e.detail.id === "props") { console.log("Properties clicked"); } }); --> Information ここではメニューのクリックイベントでログを出力していますが、ログ自体はアプリを起動しているターミナル側に出ますのでご注意ください。 https://docs.deno.com/runtime/desktop/menus/ フレームワークを利用した開発 # Deno.serve() を利用したサンプルを見てきましたが、Deno デスクトップでは、以下のフレームワークとともに利用可能です。これらのプロジェクトのディレクトリで deno desktop を起動すると、フレームワークを自動検出してアプリを構成します。多くのモダンフレームワークがサポートされています。 Next.js Astro Fresh Remix Nuxt SvelteKit SolidStart TanStack Start Vite ローカルで動いてるのに SSR を使うというのがなんとも不思議な感じですが、ちゃんと動いてセキュアであればヨシ!という感じでしょうか。 https://docs.deno.com/examples/next_tutorial/ Next.js のアプリを作成します。 deno run -A npm:create-next-app@latest 作成したプロジェクトディレクトリへ移動して実行します。 cd <project-dir> deno desktop -A Next.js のアプリが、外部サーバーなしでまるっとデスクトップ内で動いてるのは不思議な感じです。 https://docs.deno.com/runtime/desktop/frameworks/ バックエンドの選択について # デスクトップアプリは配布するバイナリのサイズも重要です。小さいに越したことはありません。 Electron は Chromium を内包するため、インストールされたバイナリサイズは300MBぐらいの大きさになったりします。 Deno Desktop の場合、OS にプリインストールされている WebView を使えば70MB程度です。CEF(Chromium) だとやはり300MB程度になります。 OS 依存の WebView だと、Windows と Mac で微妙に CSS や JS の挙動が変わるクロスブラウザ問題が発生するため、そのための対応やテストも必要になります。機能が少ないうちは WebView でもいいかもしれませんが、機能が増えてくるとテストの手間も何倍にもなっていきます。 Deno Desktop の場合、最初は軽量な WebView でスタートし、クロスブラウザが重荷になってきたら、ちょっと配布サイズは大きくなるけど、CEF にスイッチできるのがいいかなと思います。 https://docs.deno.com/runtime/desktop/backends/ --> Information Tauri だとこの辺、Servo ベースの自前 WebView プロジェクト Verso 待ちですが、Deno は既存の Chromium を選択可能にしているあたり、現時点での割り切りが感じられますね。 Electron との比較 # 既存 Web アプリをデスクトップ化したいユースケースでは、Deno Desktop はかなり有力です。 一方で、Electron の WebContentsView のような高度なマルチビュー構成を前提にしている場合は、現時点では Electron のほうが適しています。たとえば VS Code や Figma のように、複数ビューを細かく制御するタイプのアプリです。 ざっくり整理すると次のような感触です。 単一ウィンドウ中心 + 既存 Web 資産活用: Deno Desktop はかなり良い 複雑なウィンドウ/ビュー管理: Electron が依然強い --> Information マルチビュー構成の対応の弱さは Tauri も同様です。 https://developer.mamezou-tech.com/blogs/2025/12/01/porting-an-electron-app-to-tauri2/ Electron の WebContensView 構成については以下の記事をご参照ください。 /blogs/2024/08/28/electron-webcontentsview-app-structure/ https://docs.deno.com/runtime/desktop/comparison/ さいごに # 以上、Deno Desktop 機能を一通り試しました。 Out of the box でここまでデスクトップ開発体験が整っているのは率直に驚きです。タスクトレイやメニュー、Bindings など、アプリらしさを出すための API が最初から揃っているのも好印象でした。 Tauri と違ってアプリ側をすべて TypeScript で書けるため、既存 Web アプリをベースに「メニューやタスクトレイを追加し、OS 機能と連携する」用途ではかなり相性がよいと感じます。最小構成なら、デスクトップアプリ化自体は1時間もかからないはずです。 --> Information Tauri も JS の API を生やして、Rust 知らない勢を取り込もうとしてはいます。 Deno のキラー機能になる可能性もありますね。experimental から安定版へ向けて、今後の熟成がとても楽しみな機能です。 2026年7月6日現在の最新は 2.9.1 です。 ↩︎
はじめに # AIテクニカルセクターの藤堂です。セクターが独立組織となって以降、全社でAI活用を広げる取り組みが本格化しています。そのような中、6月には 第40回 JSAI2026 に参加しました。 前半ではポスター発表をした、士業資格試験を対象としたLLMベンチマークShigyoBenchについて簡単に触れ、AIの実用化についての所感を述べます。 後半では、人工知能学会で印象に残った発表と、今後のAI研究について思ったことを記します。 どういう論文か # ShigyoBenchは、 日本の士業資格試験を対象にしたLLMベンチマークデータセット です。宅建・行政書士・弁理士・司法書士・司法試験(予備試験含む)・不動産鑑定士・公認会計士の8試験、短答式8,979問を統一フォーマットで整備し、複数のLLMで評価実験を行いました。データセットは Hugging Face で公開しています。 論文の位置づけとしては、専門領域の知識を試験問題で定量評価する研究です。士業領域(行政書士・弁理士・宅建・不動産鑑定士・司法書士など)とCPA全科目について、これまで再現可能な共通ベンチがなかった空白を埋める、というのが動機です。 評価の結果、モデルや試験によって差は大きく、Gemini-3-Proは一部の試験で合格ラインを上回る一方、多くのモデル・科目ではまだ届かない、というのが本研究の結論です。 たとえば次のような問題があります。以下は、 google/gemma-4-26B-A4B-it で推論させて不正解となった一例です。 司法試験・令和2年・民法第8問 # 問題 即時取得に関する次のアからオまでの各記述のうち、判例の趣旨に照らし誤っているものを組み合わせたものは、後記1から5までのうちどれか。 ア. Aは、自己所有の宝石をBに売却して現実の引渡しをした。その後、Bは宝石をCに売却して現実の引渡しをした。さらにその後、AはAB間の売買契約をBの強迫を理由として取り消した。この場合、Cは即時取得により宝石の所有権を取得することはない。 イ. 未成年者Aは、自己所有の宝石をBに売却して現実の引渡しをした。その後、AはAB間の売買契約を未成年であることを理由として取り消した。この場合、Bが即時取得により宝石の所有権を取得することはない。 ウ. Aは、B所有の宝石をBから賃借して引渡しを受けた上、宝石をCに預けていたが、宝石をDに売却し、Cに対し宝石を今後Dのために占有するよう命じ、Dがこれを承諾した。この場合、Dは宝石がA所有であると信じ、かつそのことに過失がなかったとしても、即時取得により宝石の所有権を取得することはない。 エ. Aは、Bが置き忘れた宝石を、自己所有物であると過失なく信じて持ち帰った。この場合、Aが即時取得により宝石の所有権を取得することはない。 オ. Aは、BがCから賃借していた宝石を盗み、Dに贈与した。Dが宝石をAの所有物であると過失なく信じて現実の引渡しを受けた場合、Bは宝石の盗難時から2年間は、Dに宝石の回復を請求することができる。 ア ウ 2. ア エ 3. イ ウ 4. イ オ 5. エ オ google/gemma-4-26B-A4B-it の回答 この問題は、民法192条の「即時取得」に関する記述の正誤を判断し、誤っているものの組み合わせを選ぶ問題です。 ### 各記述の検討 **ア. 正しい** 即時取得が成立するためには、「取引行為」が必要です。Bの強迫による取消しの場合、取消しによって契約は遡及的に無効となりますが、判例によれば、取消しによって権利を失った者(A)からさらに譲り受けた第三者(C)が即時取得を行うためには、**「取消し前」に取引が行われていなければなりません**。しかし、本問ではAが取消しをした「後」にCが譲り受けているわけではなく、時系列としては「A→B(売買)→C(売買)」の後に「Aが取消し」をしています。 ここで重要なのは、強迫による取消しは「詐欺」とは異なり、取消しによって権利が遡及的に失われますが、即時取得の要件である「有効な取引」の観点から、強迫による取消しがなされた場合、Bは無権利者となります。しかし、判例の趣旨(および通説的な考え方)では、強迫による取消しは、善意無過失の第三者(C)に対してもその効力を主張できる(Cは即時取得できない)とされています。したがって、「Cは即時取得により所有権を取得することはない」という記述は正しいです。 **イ. 誤り** 即時取得が成立するためには、占有者の「権利」が欠けていること(無権利者からの取得)が必要ですが、取引行為自体は有効である必要があります。未成年者が単独で行った契約の取消しは、行為能力の瑕疵によるものですが、これは即時取得を妨げる理由にはなりません。Bが善意無過失であれば、即時取得により所有権を取得できます。したがって、「Bが即時取得により所有権を取得することはない」という記述は誤りです。 **ウ. 正しい** 即時取得が成立するためには、「占有の譲渡」などの直接的な占有の移転が必要です。本問のように、AがCに預けていたものを、Dに対して「占有改定(CがDのために占有するよう命じる)」によって占有を移転させた場合、判例は**占有改定による即時取得の成立を否定**しています。したがって、「Dは……即時取得により宝石の所有権を取得することはない」という記述は正しいです。 **エ. 正しい** 即時取得は「取引行為(売買、贈与など)」によって占有を取得する場合に成立します。本問のように、他人の物を勝手に持ち帰る行為(占有の事実上の取得)は「取引行為」には当たらないため、即時取得は成立しません。したがって、記述は正しいです。 **オ. 誤り** 民法193条により、盗品または遺失物の占有者は、盗難または遺失の時から**2年間**、即時取得者に対してその物の回復を請求できます。しかし、本問のDは、Aが「盗んだ物」であることを知らない(善意無過失)ため、即時取得が成立します。即時取得が成立した場合、Dは有効に所有権を取得します。たとえ民法193条の期間内であっても、即時取得によって権利が確定している以上、元の所有者BはDに対して返還を請求することはできません(即時取得は、193条の規定にかかわらず、有効な取得を認めるものです)。したがって、記述は誤りです。 ### まとめ - ア:正しい - イ:誤り - ウ:正しい - エ:正しい - オ:誤り 誤っているものは **イ** と **オ** です。 選択肢を確認すると、イとオの組み合わせは「4」となります。 <answer>4</answer> 正解 : 1 手法や結果の詳細については 論文 をご覧ください。 実用化への道のり # LLMが登場し、ShigyoBenchのような人間と同じ試験問題を使ってLLMの専門知識を測ることが、AI評価の標準として定着しました。それにより、国際数学オリンピックや東京大学の入試試験、医師国家試験など、いくつもの試験がフロンティアLLMで解かれることが知られています。RAGなどを組み合わせれば士業試験で合格ラインに届かせることも、十分あり得ると思います。 ポスター発表時のディスカッションやいくつかのセッションでも、人間の仕事はなくなっていくのでは、という議論が盛んにありました。 一方で、試験問題が解けることと、実用化できることは、別問題だということが現在、議論されています。 最も進んでいる領域の1つと考えられるのが医療分野です。Gong ら(2025)のシステマティックレビューでは、39の医療LLMベンチマークを整理しました。 国家試験(USMLE)型の知識ベース評価では、先行モデルが84〜90%の正答率に達します。 対照的に、診療現場に近い実践ベースの評価では45〜69%程度にとどまります(いわゆる knowledge-practice gap)。 「試験問題が解けることと、患者さんに直接LLMの出力を出せることは別」ということを、知り合いのお医者さんにも聞いたことがあります。 これは医療だけの問題ではなく、実用化にはいくつかのステップがあるのだろうと思います。自由記述での推論、不確実性の管理、マルチターンの対話、文脈の統合、安全性といった実践的なベンチマークに何が求められるかが、議論の焦点となっています。 ソフトウェア開発では、少し様子が違います。こうした「試験と現場のギャップ」を十分に議論する前に、コーディング支援やエージェントが現場に入り始めている印象があります。おそらく、コードは試して直しやすく、フィードバックも速いからでしょう。医療や法律のように、一度の誤りから取り返しのつかない結果が生じにくい、という違いもあるのかもしれません。ただ、1つのシステムにかかわる複雑なコードやドキュメント類の品質をどのように保つのか、テストの合格率だけでは見えないリスクもあります。私自身、実案件でLLMを使いながら、その便利さと危うさの両方を感じています。 フィジカルAIへの期待 # JSAI2026では、ロボティクスやフィジカルAI(Physical AI)に関する発表が数多く見られました。業務都合でその中の1つのセッションしか聞けなかったのですが、日本でも研究者や学生の関心が集まっていることを実感しました。 最も印象に残った発表は、「基盤モデル時代におけるPhysical AI」セッションでの「 自動運転向けの多視点の動画生成における世界ハンドオフ整合性指標の設計 」です。 拡散モデルによる映像生成モデルが登場し、自動運転用の合成データ作成などに役立てられています。そのようなモデルで複数の視点からカメラで写したような映像が取得できますが、それらカメラの間で物理的な不整合(あるカメラでセダンだった車が別のカメラで別の車種になるなど)が発生します。そのような不整合を定量的に評価しようという論文です。 画像におけるハルシネーションという言い方をされていて、原理的には確かにそういったことが起こるなと思いますし、そのことを定量化したという点で価値があります。 松尾豊教授が、「JSAIの3分の1は国際学会で発表できるレベルにある」と言及されていました。個人的な体感として、論文数が増えた現在、そこまで高い割合ではないかなと思いますが、本発表は国際レベルの内容で、このように優秀な若者がフィジカルAIの問題をどんどん解いていくのだろうなと思いました。 生成AIがAI研究自体を変え始めている # 特定のセッションというより、学会全体を通して感じたのは、生成AIがAI研究の進め方そのものを変え始めている、ということでした。 文献の当たり方、プログラミング、実験の設計、データの整理、モデルの学習といった、これまで研究者が時間をかけていた作業の多くを、LLMが肩代わりし始めています。負荷の中心は「実装する」「情報を集める」から、「何を問うか」「どう測るか」へ移っているように見えます。試行が短くなるほど次の成果が出やすくなり、それがまた試行を短くする、というサイクルになっているように見えます。 個人的には、ひとつの論題を深掘りするより、こうしたサイクルを回しやすくする環境づくりに時間を使うようになってきました。 Cursor や Codex のようなコーディングエージェントがあります。 各種 AI API と接続したアプリケーション、Google Colaboratory や Modal のような GPU 基盤も利用しています。 いま利用しているツールを図にすると、おおむね次の構成です。 モデルの使い分け、先行研究の整理の仕方、どこで人間が判断を挟むか、どのGPU をどの実験に回すか。このようにメタ視点で、実験環境全体にリソースを割くことにより、研究が加速していきます。 明日には似た研究が発表されているかもしれない。研究が加速する中でいちばん難しいのは、何を題材にするかだと思います。 おわりに # 現地には最終日しか行けませんでしたが、ポスター発表では名だたるLLMベンダーの方々とお話ができて大変充実していました。よもや国内にAIモデルベンダーと呼ばれる人々が現れるとは5年前には想像もつきませんでしたが、彼らにはどんどん世界にチャレンジしていってもらいたいです。 また、以前お仕事を一緒にさせていただいた懐かしい人にも出会えて、様々なお話ができました。このようなことはリアルの醍醐味ですし、AIコミュニティというのは広そうで狭いコミュニティなのだなと実感しました。 次は長崎です。 参考文献 # 本研究(ShigyoBench) 藤堂真登, 石川真之介. ShigyoBench: 日本の士業資格試験を対象としたLLMベンチマークデータセットの構築と評価. JSAI2026 論文集 , 2026. https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/5Yin-A-16 / データセット: https://huggingface.co/datasets/todo1111/shigyobench 実用化・医療 Gong EJ, Bang CS, Lee JJ, Baik GH. Knowledge-Practice Performance Gap in Clinical LLMs: Systematic Review of 39 Benchmarks. J Med Internet Res . 2025;27:e84120. https://doi.org/10.2196/84120 学会で印象に残った発表 キム ボンジュン, et al. 自動運転向けの多視点の動画生成における世界ハンドオフ整合性指標の設計. JSAI2026 論文集 (基盤モデル時代におけるPhysical AI), 2026. https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/2G4-OS-47a-02 大会 人工知能学会. 2026年度 JSAI(第40回)
今年も半分が過ぎました。2026年4-6月のサマリーです。 記事数・執筆者数 # この3ヶ月で13本の記事が投稿され、記事数は889になりました。 連載 # AIエージェントとシステムをつなぐMCP入門 # MCP(Model Context Protocol) は AI エージェントが外部サービスと通信するための仕様で、2024年に Anthropic 社によって初版がリリースされました。MCP を使用することで、AI エージェントは外部サービスの機能を効果的に利用できます。MCP の基本から実装まで段階を分けて解説するシリーズです。 /blogs/2026/04/24/mcp-impl_introduction/ 現在は以下の6記事が公開されています。 イントロダクション stdio実装編 StreamableHTTPステートレス実装編 StreamableHTTPステートフル実装編 プロンプト編 リソース編 テーマ別記事 # 認定資格 # /blogs/2026/04/13/google_cloud_all_certified_revenge/ /blogs/2026/04/20/aws_certified_generative_ai_developer/ ペアレンタルコントロール # 上記の AWS・Google Cloud 認定を“W全冠”したエンジニアが、クラウドから降りて自宅のネットワークと格闘した異色の記事です。夜中に学校用タブレットでゲームをする子供との「イタチごっこ」に終止符を打つべく、手持ちの家庭用ルータとRaspberry Piを活用して、MACアドレス制限やサブネット分離、Pi-holeによる独自DNS構築まで、本気の「ガチ構成」を徹夜で組み上げる様子を赤裸々に綴っています。DoH(暗号化DNS)対策などのリアルな課題にも触れられており、ネットワークの基礎を学び直したい方や、同じ悩みを持つITエンジニアの親御さん必見の泥臭くも愛に溢れた実践録です。 /blogs/2026/04/09/home_network_control/ スクラムマスターと AI # チームの対話を支えるスクラムマスターにとって、視覚的な資料作成は欠かせませんが、一方で多大な時間がかかるのが共通の悩み。本記事では、そのボトルネックを AI で突破する実践的な手法を解説しています。ChatGPT を思考のパートナーとして構成を練り上げ、最新の生成 AI ツールでスライドを一気に形にする――単なる「時短術」に留まらず、AI との対話を通じてアイデアを磨き、本来注力すべきファシリテーションやコーチングの質を高めるための「共創のプロセス」を紹介しています。資料作成の重圧から解放され、チームの価値最大化に向き合いたいリーダー・マネージャーにとっても役立つ内容となっています。 /blogs/2026/04/27/ai-presentation/ GitHub # GitHub の Organization 運用において、「機密リポジトリを作りたいが、Basic Permission の設定変更による管理コスト爆発は避けたい」というジレンマ。この記事では、高価なEnterprise プランを契約せずとも、Teams プランの制限下で安全かつ効率的にアクセス権を管理する「ホワイトリスト方式」の戦略を解説しています。全メンバー用の統合チーム作成によるセキュリティ境界の構築から、GitHub API と Actions を組み合わせた「チーム追加漏れを防ぐ自動化スクリプト」の実装まで、管理者の負担を増やさない現場目線のハックを紹介しています。 /blogs/2026/06/24/github-manage-organization-access/ CI/CD 環境で広く使われる GitHub Actions に新たに追加された、単一ワークフロー内でのステップ並行実行機能をいち早く検証した記事です。background 属性を使った非同期実行や、parallel ブロックを使った同時実行の基本構文を解説するだけでなく、Go 言語のクロスコンパイルを用いた実践的なパフォーマンス検証も実施。vCPU コア数やコンテキストスイッチの観点から「期待したほど速くならなかった理由」と、「これまでの Matrix ビルドとどう使い分けるべきか」まで深く考察しており、現場の CI 改善に直結する生きた知見が得られます。はてなブックマークでも注目され、公開直後からアクセスが上昇しました。 /blogs/2026/06/27/github-actions-parallel-steps/ さいごに # 以上、2026年度第1四半期のサマリーでした。投稿数が少なかったため、個別の記事紹介を厚めにしてみました。 よかったら フィード の購読、 X や Bluesky でのフォローもお願いします。 Facebook でも本サイトの注目記事をはじめ豆蔵に関するイベントを紹介しています。 note にも時々本サイト関連の記事が掲載されています。
はじめに # 本ページは「AIエージェントとシステムをつなぐMCP入門」の続編です。 今回は、リソースについて説明します。 MCPのリソースは、AIモデルが回答を生成する際に参照するコンテキスト情報(ファイル、ガイド、仕様など)を提供する機能です。 ツールが「AIモデルの判断」で実行され、プロンプトが「ユーザーの意思」で選択されるのに対し、リソースは「アプリケーション主導(Application-driven)」でその組み込みを決定します。 本記事で掲載しているコードは こちら で公開しています。 --> シリーズ目次 連載:AIエージェントとシステムをつなぐMCP入門 イントロダクション stdio実装編 StreamableHTTPステートレス実装編 StreamableHTTPステートフル実装編 プロンプト編 リソース編(本ページ) 今回使用するライブラリなど # npm@11.11.1 node@22.22.0 typescript@6.0.3 @modelcontextprotocol/sdk@1.29.0 zod@4.3.6 使用例 # 開発ガイドの共有: コーディング規約や設計方針をリソース化して、AI生成時の前提情報として渡します 仕様断片の参照: API仕様、エラーコード定義、入力制約などの抜粋をリソースとして公開し、コード生成の精度を高めます 動的パラメーターによって切り替える情報の提供: URIテンプレートを使い、可変パラメーターで内容が変わるリソースを提供します ツール、プロンプト、リソースの違い # これまでの特集でツール、プロンプト、リソースの主要要素に触れてきたので、ここで役割を振り返ります。 要素名 主な役割 制御主体 識別方法 リソース データ・コンテキスト(静的な知識)の提供 アプリケーション主導 リソースのURI プロンプト メッセージ・ワークフローのテンプレート ユーザー主導 プロンプト名 ツール 具体的な関数の実行(能動的なアクション) AIモデルによる実行 ツール名 リソースの種類 # MCPのリソースは、URIの与え方によって2種類に分かれます。 種類 説明 用途例 静的リソース 固定URIで一意に特定されるリソース ガイド、固定仕様、設定情報など リソーステンプレート URIテンプレート(RFC 6570)でパラメーター化されたリソース IDや名前などの可変要素で内容が変わる情報 プロトコルメッセージ # MCPのリソースに関するメッセージは6種類あります。 メッセージ 方向 説明 resources/list クライアント → サーバー 利用可能なリソース一覧を取得する(ページネーション対応) resources/templates/list クライアント → サーバー リソーステンプレート一覧を取得する resources/read クライアント → サーバー URIを指定してリソースのコンテンツを取得する resources/subscribe クライアント → サーバー 指定URIのリソース変更通知を購読する(ケーパビリティ subscribe: true が必要) notifications/resources/updated クライアント ← サーバー 購読中のリソースが変更されたことを通知する notifications/resources/list_changed クライアント ← サーバー リソース一覧が変化したことを通知する(ケーパビリティ listChanged: true が必要) ※通知を検証するためには、リソースの源泉を別に設け かつ 源泉側の変更検知も必要なため、サンプルには含めていません。 sequenceDiagram actor client as MCP Client participant server as MCP Server client ->> server: リソース一覧取得(resources/list) server -->> client: リソース一覧 client ->> server: リソース取得(resources/read) server -->> client: リソース データ構造(Data Types) # リソースコンテンツ uri : リソースを一意に識別可能なURI name : リソース名 title : 表示用のタイトル description (任意): リソースの説明 icons (任意): アイコンリスト mimeType (任意): コンテンツのMIMEタイプ。テキストまたはバイナリ(Base64エンコードが必要)を指定可。 size (任意): バイト数 アノテーション(Annotations): クライアントへのヒント audience : 誰向けのリソースか示す情報。 user , assistant または両方を指定可。 priority : 0.0~1.0の数値で示される重要度(1.0が最重要) lastModified : 最終更新日時(ISO8601形式のタイムスタンプ) URIスキーム # リソースのURIには目的に応じたスキームを選びます。 スキーム 用途 備考 https:// Web上のリソースを参照 クライアント自身がアクセスできる場合に使用する。サーバー経由で取得する場合は独自スキームの検討が推奨されます file:// ファイルシステムのような構造を表現したいリソース ファイルシステムとのマッピングは必須ではなく、値の取得先は自由(DBや外部APIなど) git:// Gitリソース コミット、ブランチ、パスなどのGit固有の構造を表現 カスタム 独自スキームによる任意のリソース RFC3986準拠。本サンプルの memory:// 、 orders:// もこれに該当 --> Information サーバーはすべてのリソースURIを検証する サンプルでは省略していますが、パストラバーサルなど意図しないアクセスを防止しなければいけません。 fileでディレクトリを表現する場合 mimeTypeに inode/directory (XDG規格)を指定することが推奨されます。 実装サンプル # コードの全体は こちら をご覧ください。 静的リソースの登録 # 固定URIでリソースを公開する例です。 registerResource の第2引数にURI文字列を渡すと静的リソースになります。 // リソース: アノテーション付きの静的リソース(テキストコンテンツ) const testGuideUri = "memory://guides/testcase-prompt-playbook"; server.registerResource( "testcase-prompt-playbook", testGuideUri, { mimeType: "text/markdown", description: "テスト観点生成のガイド", annotations: { audience: ["assistant"], // AIへの参照情報として位置づける priority: 0.8, // 重要度(高め) lastModified: "2026-06-28T00:00:00Z", }, }, async () => ({ contents: [{ uri: testGuideUri, mimeType: "text/markdown", text: [ // 実務ではもっと細かい指示が必要になりますが、行数を抑えるため最小限に留めています "# APIテストケース作成ガイド", "- 仕様の観点: 正常系、代替系、異常系、境界値、認可、冪等性", "- 基本フローの正常系を中心に、代替系、異常系、その他の観点を付加する形にまとめる。", "- ユースケースの検証を主眼とし、入力値検証などは含めない(単体テストで担保する)", "- 必要に応じて分類しながら、箇条書きで簡潔にまとめる", ].join("\n") }] }), ); 動作確認: resources/read が確認できるPostmanで確認 バイナリコンテンツの指定例 バイナリコンテンツ(画像など)は blob にBase64エンコード文字列を設定します。 // リソース: バイナリコンテンツの例(画像) server.registerResource( "company-logo", "file://assets/logo.png", { mimeType: "image/png", description: "企業ロゴ画像" }, async () => ({ contents: [{ uri: "file://assets/logo.png", mimeType: "image/png", blob: "<Base64エンコードされたデータ>" }], }), ); リソーステンプレートの登録 # ResourceTemplate クラスを使うと、URIテンプレート(RFC 6570)でパラメーター化されたリソースを公開できます。 {orderId} のようなプレースホルダーがリクエスト時に展開されます。 list コールバックは resources/list で返す候補一覧を定義します(省略不可、不要なら undefined を渡します)。 resources/read リクエスト時、クライアントは orders://O00001/detail のようにテンプレートを展開したURIを指定します。 サーバー側では {orderId} 部分が変数として分解され、ハンドラーに渡されます。 // リソーステンプレート: URIのパラメータで内容が変わる動的リソース server.registerResource( "order-detail", new ResourceTemplate("orders://{orderId}/detail", { list: async () => ({ resources: [ { uri: "orders://O00001/detail", name: "O00001", mimeType: "application/json" }, { uri: "orders://O00002/detail", name: "O00002", mimeType: "application/json" }, ], }), }), { mimeType: "application/json", description: "受注詳細" }, async (uri, { orderId }) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ orderId, status: "pending" }) }], }), ); 動作確認 リソーステンプレートの確認: resources/templates/list が確認できるMCP Inspectorで確認 リソーステンプレートの実行確認: resources/read が確認できるPostmanで確認 まとめ # リソースは、AIへの参照情報を明示的に管理したい場合に有効です。 静的リソースは固定情報(ガイド、仕様など)、リソーステンプレートは可変情報(IDベースの詳細など)に向いています。 アノテーションを設定することで、クライアントがリソースを適切に選択し、優先順位付けできます プロンプトと組み合わせると、生成指示と参照情報の両方を標準化できます
はじめに # 先日、GitHub Actions ワークフローで background 機能が実装され、単一のワークフロー内でステップを並行で実行可能になったことが発表されました。 https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/ これまでも、Strategy Matrix を使うと複数のランナーを使って並行処理させることはできましたが、今回の機能で、単一のランナーでの並行処理がサポートされたことになります。 公式ドキュメント には、バックエンドとフロントエンドのビルドを並行で実行するサンプルが掲載されています。 steps: - name: Build frontend id: build-frontend run: npm run build:frontend background: true - name: Build backend id: build-backend run: npm run build:backend background: true - name: Run linter while builds run run: npm run lint - name: Wait for both builds to finish wait: [build-frontend, build-backend] - name: Run tests run: npm test 並行実行を試す (background 版) # ステップ毎に background: true を指定する方法です。この属性を付与したステップは、実行開始後即座にフォアグラウンドに処理を戻します。 name: Background Hello World on: workflow_dispatch: jobs: hello-background: runs-on: ubuntu-latest steps: - name: Background hello 1 #1 id: hello1 run: | echo "hello1 start: $(date -u +%H:%M:%S)" sleep 4 echo "hello1 end: $(date -u +%H:%M:%S)" background: true - name: Background hello 2 #2 id: hello2 run: | echo "hello2 start: $(date -u +%H:%M:%S)" sleep 3 echo "hello2 end: $(date -u +%H:%M:%S)" background: true - name: Foreground step (runs while background steps are active) #3 run: | echo "foreground start: $(date -u +%H:%M:%S)" sleep 1 echo "foreground end: $(date -u +%H:%M:%S)" - name: Wait for background steps #4 wait: [hello1, hello2] - name: Done run: echo "Both background steps have completed." バックグラウンド実行するステップ。4秒間のスリープの開始と終了で時刻を表示します。 background: true を指定します。 バックグラウンド実行するステップ2個目。1個目と同様の処理です。スリープは3秒にしてます。 バックグラウンド実行しながら、フォアグラウンドで実行されるステップ。1秒間スリープします。 バックグラウンドの2つのステップを待ち受けるステップです。 wait でステップの ID を配列で指定するだけです。 実行結果です。 hello1 と hello2 が同一時刻に開始され、1秒違いで終了しています。 フォアグラウンドステップも2つのバックグラウンドステップと同時刻に開始されています。 wait ステップで2つのバックグラウンドステップの完了を待っています。 --> Information 上記のサンプルでは単純な sleep と echo をバックグラウンドで実行し、wait で待機しているだけですが、実際の CI/CD パイプラインでは「バックグラウンドで起動したサーバーやプロセスの結果を後から使いたい」というユースケースがよくあります。このようなケースでは、出力は一時ファイルに書き出しておき、wait後のステップでそれを読み込むといった工夫が必要になります。 並行実行を試す (parallel 版) # parallel キーワード配下にステップを並べるだけで並列化できます。parallel ブロックを抜けると完了するため、wait で待つ必要はありません。 name: Parallel Hello World on: workflow_dispatch: jobs: hello-parallel: runs-on: ubuntu-latest steps: - parallel: #1 - name: Parallel hello 1 run: | echo "parallel-1 start: $(date -u +%H:%M:%S)" sleep 4 echo "parallel-1 end: $(date -u +%H:%M:%S)" - name: Parallel hello 2 run: | echo "parallel-2 start: $(date -u +%H:%M:%S)" sleep 3 echo "parallel-2 end: $(date -u +%H:%M:%S)" - name: Parallel hello 3 run: | echo "parallel-3 start: $(date -u +%H:%M:%S)" sleep 2 echo "parallel-3 end: $(date -u +%H:%M:%S)" - name: Done after all parallel steps #2 run: | echo "done step start: $(date -u +%H:%M:%S)" echo "All parallel steps have completed." echo "done step end: $(date -u +%H:%M:%S)" parallel 配下に3つのステップを配置します。sleep は4秒、3秒、2秒とバリエーションを持たせています。 通常のステップです。parallel ステップ完了後に実行されます。 実行結果です。 hello1, hello2, hello3 が同時刻に開始されています。それぞれ指定通り、4秒、3秒、2秒実行にかかっています。 最後のステップは、parallel ステップ完了後の時刻から開始されていることがわかります。 クロスコンパイルで使ってみる # 応用として、すぐに思いつくのは、クロスコンパイルで複数プラットフォーム向けのバイナリ生成を並行で実行することです。例えば、Go 言語では Linux / macOS / Windows の向けのバイナリをクロスコンパイルできます。 - name: Build run: | GOOS=linux GOARCH=amd64 go build -o build/linux-amd64/sb2md main.go GOOS=linux GOARCH=arm64 go build -o build/linux-arm64/sb2md main.go GOOS=windows GOARCH=amd64 go build -o build/windows/sb2md.exe main.go GOOS=darwin GOARCH=amd64 go build -o build/macos/sb2md main.go GOOS=darwin GOARCH=arm64 go build -o build/macos_arm/sb2md main.go 並列化する以前のビルド結果です。5つのバイナリを生成するのに44秒かかっています。 並列化を適用しました。run で複数行書いてましたが、個別のステップに分けて、parallel 配下に置きました。 - parallel: - name: Build linux amd64 run: GOOS=linux GOARCH=amd64 go build -o build/linux-amd64/sb2md main.go - name: Build linux arm64 run: GOOS=linux GOARCH=arm64 go build -o build/linux-arm64/sb2md main.go - name: Build windows amd64 run: GOOS=windows GOARCH=amd64 go build -o build/windows/sb2md.exe main.go - name: Build darwin amd64 run: GOOS=darwin GOARCH=amd64 go build -o build/macos/sb2md main.go - name: Build darwin arm64 run: GOOS=darwin GOARCH=arm64 go build -o build/macos_arm/sb2md main.go トータルは40秒でした、Linux amd64 のビルドは3秒で終わってますが、他のプラットフォーム用のビルドはそれぞれで40秒かかっています。思ったより短縮されませんでした。考えられる原因としては、やはりランナーの CPU コア数でしょうか。 ランナー(ubuntu-latest)の vCPU が2コアであることから多重度が上がらなかった 5つのプロセスが2つのコアを奪い合ってコンテキストスイッチが大きかった ランナーのアーキテクチャ自体が Linux amd64 なので、ネイティブのバイナリ生成は瞬時に終わった CPU コアの多い Larger Runner にすればもっと短縮できそうですが、40秒が3秒程度に短縮されるだけなら、コストパフォーマンスはイマイチですね。今回のユースケースには合わない感じがします。 さいごに # 以上、GitHub Actions ワークフローでの並行ステップ実行を試してみました。 今回試したGoのクロスコンパイルのように、CPUヘビーで互いに独立したタスクであれば、これまで通りStrategy Matrixを使って別々のランナーを立ち上げた方が高速に処理できる可能性が高いです(課金は高くなりますが)。 一方で、今回の並行ステップ実行は公式ドキュメントのサンプルにあるような「バックエンドとフロントエンドのビルド」や「テスト実行と並行しての Lint 実行」など、単一ランナーの空きリソース(I/O待ちの時間など)を効率よく活用したい場面で輝く機能だと言えるのではないかと思います。
はじめに # GitHub Organization を運用していると、セキュリティと利便性のバランスを取る必要があります。先日、「機密性の高いデータを保持するリポジトリを作りたいので、オーガニゼーションの Basic Permission を write から no permission に変更してほしい」という相談を受けました。 この要望は妥当ですが、そのまま実装すると新規リポジトリ作成のたびにメンバーのアクセス権を個別設定しなければならず、管理者の負担が大幅に増えてしまいます。 本記事では、Enterprise プランではなく Teams プランの制限下で実装できるアクセス権管理戦略をご紹介します。 問題の整理 # 環境の制約 # 大前提として以下があります。 GitHub Enterprise ではなく Team プラン Team ベースのアクセス制御は可能だが、細かい設定に限界がある 元々の課題 # Basic Permission が write に設定されている 機密性の高いリポジトリに対して、デフォルトでアクセス権が与えられてしまう これを防ぐため、Basic Permission を no permission にしたい その先の課題 # Basic Permission を no permission に変更すると、全ての新規リポジトリでメンバーのアクセス設定が必要 新規メンバー追加時にも、リポジトリごとのアクセス設定が必要 管理作業が爆発的に増加する 今回の解決策 # GitHub のことは GitHub Copilot に聞こうということで、Web 版の Copilot くんに相談し、オーガニゼーション内のチームによるセキュリティ境界を構築することにしました。 アクセス権管理の全体像 # 以下に構築したアクセス権管理の全体像を示します。 graph TB Org["Organization<br/>(mamezou-tech)"] Members["Organization Members<br/>(全従業員)"] AllMembers["all-members Team"] NonAllMembers["all-members 未所属メンバー"] SecurityTeam["security-team<br/>(限定メンバー)"] GenRepo["一般リポジトリ<br/>📁 public, docs, tools, etc."] ConfRepo["機密リポジトリ<br/>🔒 secrets, finance, compliance, etc."] Org --> Members Members -->|標準運用| AllMembers Members -->|例外運用| NonAllMembers AllMembers -->|Read/Write アクセス| GenRepo AllMembers -.->|アクセス不可| ConfRepo Members -->|選定メンバーのみ| SecurityTeam SecurityTeam -->|Read/Write アクセス| ConfRepo NonAllMembers -.->|アクセス不可| GenRepo NonAllMembers -.->|アクセス不可| ConfRepo style Org fill:#e1f5ff style Members fill:#f3e5f5 style AllMembers fill:#c8e6c9 style NonAllMembers fill:#eceff1 style SecurityTeam fill:#ffccbc style GenRepo fill:#fff9c4 style ConfRepo fill:#ffcdd2 linkStyle 6,7 stroke:#d32f2f,stroke-width:2px,stroke-dasharray: 6 4 この構成では、 all-members への所属がアクセス可否を決めるセキュリティ境界となり、未所属の Organization メンバーは一般リポジトリ・機密リポジトリのいずれにもアクセスできません。 基本戦略:「ホワイトリスト方式」への転換 # 以下の方針でアクセス権を整理します。 1. 全メンバーが所属する統合チームの作成 all-members という全メンバーが所属するチームを作成します。このチームには、ほぼ全員がアクセスすべき一般的なリポジトリへのアクセス権を付与します。 利点: 新規メンバー追加時は、このチームに追加するだけで大多数のリポジトリにアクセス可能 リポジトリ作成後のデフォルトアクセス設定が不要 2. 機密リポジトリの隔離 機密性の高いリポジトリ(例:構成情報、個人情報、営業秘密など)は、 all-members チームからは見えないようにします。 その代わり、必要なメンバーのみで構成した別チーム(例: security-team 、 finance-team )を作成し、そのチームにのみアクセス権を付与します。 3. リポジトリごとの権限設定 リポジトリA(一般向け) └─ all-members チーム: read リポジトリB(一般向け) └─ all-members チーム: write リポジトリC(機密:セキュリティ) └─ security-team チーム: write └─ 特定ユーザー: admin リポジトリD(機密:財務) └─ finance-team チーム: write └─ 特定ユーザー: admin 実装手順 # Step1: all-members チームの作成 # Organization Settings → Teams 「Create a team」から all-members チームを作成 新規メンバー追加時に画面から本チームを選択して追加 Step2: リポジトリアクセス権の設定 # リポジトリの Settings → Collaborators and teams all-members チームを追加し、適切な権限(Read/Write)を設定 Step3: 機密リポジトリの個別設定 # 機密リポジトリは all-members チームを追加しない 専用チーム(例: security-team )を作成し追加 または、特定ユーザーのみを直接追加 自動化でリポジトリへのチーム追加を効率化 # 前述の戦略を実装する際、課題となるのは 既存・新規リポジトリへの all-members チームの追加を漏れなく行うこと です。GitHub Actions を使ってこのプロセスを自動化できます。 実装の課題 # 新規リポジトリ作成後、 all-members チームの追加を忘れることがある 既存リポジトリ数が多い場合、手動で一括追加するのは手間 リポジトリ管理者により追加漏れが生じる可能性 自動化の仕組み # GitHub API と GitHub CLI を使用し、以下の処理を自動実行します。 スクリプト例 #!/bin/bash set -e ORG="mamezou-tech" TEAM="all-members" DRY_RUN=${DRY_RUN:-false} if [ "$DRY_RUN" = "true" ]; then echo "🔍 DRY RUN MODE - No actual changes will be made" fi # 除外リポジトリの設定(機密リポジトリなど) EXCLUDE_REPOS=( "secret-repo-1" "confidential-data" ) # Organization のリポジトリ一覧を取得 repos=$(gh api --paginate /orgs/$ORG/repos --jq '.[].name') success_count=0 skip_count=0 fail_count=0 for repo in $repos; do # 除外リポジトリをスキップ if [[ " ${EXCLUDE_REPOS[@]} " =~ " ${repo} " ]]; then echo "⊘ Skipping: $repo (excluded)" ((skip_count++)) || true continue fi echo "Adding $TEAM to $ORG/$repo..." if [ "$DRY_RUN" = "true" ]; then echo "✓ (dry-run)" ((success_count++)) || true else if gh api --method PUT \ /orgs/$ORG/teams/$TEAM/repos/$ORG/$repo \ -f permission=push \ --silent; then echo "✓" ((success_count++)) || true else echo "✗ Failed" ((fail_count++)) fi fi done echo "" echo "================================" echo "Repository Sync Summary:" echo " Success: $success_count" echo " Skipped: $skip_count" echo " Failed: $fail_count" echo "================================" スクリプトのポイント EXCLUDE_REPOS : 除外リポジトリを明示的に管理(機密リポジトリはここに列挙) permission=push : 書き込み権限を付与( permission=pull なら読み込み権限のみ) DRY_RUN : true 時はシミュレーション実行 API 呼び出しで一括処理し、人手を削減 --> Information 最初 permission=push のところを Copilot くんが write にしていて、実行時にハマりました。GitHub API Copilot くんでも間違えるほど対称性がない部分があるので注意が必要です。 GitHub Actions ワークフロー # name: Sync all-members Team on: workflow_dispatch: schedule: - cron: '0 2 1 * *' # 毎月1日 2:00 UTC に実行 pull_request: paths: - '.github/workflows/sync-all-members-team.yml' - 'scripts/sync-all-members-repos.sh' jobs: sync-team: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Add all-members team to repositories run: bash scripts/sync-all-members-repos.sh env: GH_TOKEN: ${{ secrets.ORG_MEMBER_PAT }} DRY_RUN: ${{ github.event_name == 'pull_request' }} ワークフロー構成のポイント workflow_dispatch : 手動実行で即座に同期漏れをチェック可能 schedule : 定期実行(例:毎月1日)で同期漏れを自動検出 pull_request : スクリプト更新時に自動テスト実行 DRY_RUN : PR では true で変更をシミュレーション 必要なシークレット設定 ORG_MEMBER_PAT : Organization リポジトリ管理権限を持つ Personal Access Token スコープ: admin:org , repo など 運用のコツ # ✅ スクリプト更新時は PR から自動実行で確認してからマージ ✅ 除外リポジトリは明示的にコメント付きで管理 ✅ 定期実行により同期漏れを定期的に検出・修正 ✅ 手動実行(workflow_dispatch)でオンデマンド同期も可能 ✅ 新規リポジトリ作成直後の手動実行で即座にチーム追加可能 疎通確認 # うまく構築できているはずですが、アクセス制御が意図どおり機能しているかを確認するため、Organization メンバー(非限定メンバー)に実際に機密リポジトリのリンクを開いてもらい、挙動を確認しました。 --> Slack の会話 👦 kondoh 16:16 ちょっとお願いがあるんですが、このリポジトリを開いたらどうなるか教えてください。 https://github.com/mamezou-tech/[private-repo-for-restricted-team] 👧 nakamura 16:27 404になります。 👦 kondoh 16:28 ありがとうございます!機密性の高いリポジトリなので、404で大丈夫です。💯 ポイントと注意点 # 今回の構成には、以下のメリットと注意点があります。 メリット # ✅ Basic Permission を変更せずにセキュリティを向上 ✅ 新規メンバー追加時の作業が最小限で済む ✅ ほとんどのリポジトリへのアクセスが自動的に付与される ✅ GitHub Enterprise に比べて低コスト デメリット・注意事項 # ⚠️ 機密リポジトリの管理は手動作業 ⚠️ チーム構成の変更時にも対応が必要 ⚠️ 定期的なアクセス権の監査が必要 さいごに # GitHub Organization のアクセス権管理は、セキュリティと運用性のバランスが重要です。Enterprise プランがなくても、チーム機能を活用することで、ある程度の制御は可能です。 重要なのは、「全員がアクセスすべきリポジトリ」と「限定的にアクセスすべきリポジトリ」を明確に分類することです。この分類が甘いと、セキュリティリスクが発生したり、逆に運用が複雑になったりしますので注意が必要です。
はじめに # 本ページは「AIエージェントとシステムをつなぐMCP入門」の続編です。 今回は、プロンプトについて説明します。 MCPのプロンプトは、MCPクライアント向けにテンプレート化されたメッセージやワークフローを提供する機能です。 生成指示やツールの利用順序を定義したワークフローなど、テンプレート化して再利用したい場合に有効です。 本記事で掲載しているコードは こちら で公開しています。 --> シリーズ目次 連載:AIエージェントとシステムをつなぐMCP入門 イントロダクション stdio実装編 StreamableHTTPステートレス実装編 StreamableHTTPステートフル実装編 プロンプト編(本ページ) 今回使用するライブラリなど # npm@11.11.1 node@22.22.0 typescript@6.0.3 @modelcontextprotocol/sdk@1.29.0 zod@4.3.6 使用例 # 定形タスクのテンプレート化: コード生成やレビューなど利用頻度の高い指示をテンプレート化することで、クライアント側の指示作成を省略できます ペルソナやルールの適用: 役割や出力形式の制約をコンテキストとして適用します ワークフローのテンプレート化: ツールの利用順序などを定義して一連の流れをガイドします 実装サンプル # コードの全体は こちら をご覧ください。 定形タスクのテンプレート化 # 定形タスクであるレビュー指示をテンプレート化した実装例です。 最近は省略されがちですが、サンプルなので役割も明示しています。 argsSchema(引数スキーマ): プロンプトの変数部分を定義するもの messages: roleとcontentで構成されるメッセージ配列 role: メッセージのスピーカーを示す識別子で、user/assistantのいずれかを設定します user(ユーザー): 人間からの入力を表します。ここにAIへの指示(プロンプト本文)を記述します。 assistant(アシスタント): AIからの返答を表します。 --> プロンプトの指示だけではレスポンスの形式がブレてしまう場合 出力形式の「見本(Few-shot)」をあらかじめAI自身の返答として配置しておくことで、精度の高い結果を引き出せます。 content: メッセージ本体。サンプルはテキストのみですが、バイナリ(画像、音声、埋め込みリソース)も扱えます。 text(テキスト): プレーンテキストのメッセージで、自然言語のやりとりで一般的に使用されるタイプ image(画像): 視覚的な情報を含むメッセージ audio(音声): 音声情報を含むメッセージ resource(埋め込みリソース): サーバーのリソース(ドキュメント、コードサンプルなど)を会話に組み込む。 --> バイナリを扱う場合 Base64でエンコードし、適切なMIMEタイプの設定が必要です。 // プロンプト:コードレビュー指示をテンプレート化 server.registerPrompt( "code-review-prompt", { title: "code-review-prompt", description: "コードレビュー指示", // 引数スキーマの定義:MCP InspectorなどのMCPクライアントのUIで入力フォームとして表示され、サーバー側の関数に渡されます。 argsSchema: { specPath: z.string().describe("仕様書パス"), codePath: z.string().describe("対象コードパス"), }, }, async ({ specPath, codePath }) => { return { messages: [ // user: プロンプト本文 { role: "user", content: { type: "text", // 実務ではもっと細かい指示が必要になりますが、行数を抑えるため最小限に留めています text: [ "コードレビュー指示", "あなたはNode.js/TypeScriptのバックエンド開発スペシャリストです。", "* 優先観点: 仕様整合性、例外設計、認可制御、トランザクション制御、性能、保守性、セキュリティ。", "* 禁止: 推測で仕様補完しない。根拠のない断定をしない。", "* 指摘の重大度: High(必須)/Medium(基本的に対応)/Low(できるだけ対応)を付ける。", "* 指摘は「問題」「対象個所」「修正案」を1行で示す。", "* 出力は最大10件、重複指摘は統合する。", "* 改善コードが必要なら最小差分で提案する。", `* 仕様書パス: ${specPath}`, `* 対象コードパス: ${codePath}`, ].join("\n"), }, }, // assistant: 見本 { role: "assistant", content: { type: "text", text: ["(コードレビュー指摘の出力例)", "1. High: [問題] 仕様に記載のないAPIエンドポイントが存在する。 [対象個所] src/api/user.tsのgetUser関数。 [修正案] 不要なエンドポイントなら削除、仕様が古いなら更新して整合させる。", // omit ].join("\n")}, }, ], }; }, ); 動作確認(MCP Inspector) ワークフローのテンプレート化 # ツールの実行順序をテンプレート化した実装例です。 // プロンプト: 指示やワークフローをテンプレート化。 server.registerPrompt( "inventory-check-workflow", { title: "inventory-check-workflow", description: "在庫確認ワークフロー", argsSchema: { orderNo: z.string().describe("受注番号"), }, }, async ({ orderNo }) => { const prompt = [ "在庫確認ワークフロー", `1. tools/callでget-orderを実行してください。(引数 orderNo: "${orderNo}")`, "2. tools/callでcheck-attached-inventoryを実行してください。(引数: orderId: 1のレスポンスのorderId, quantity: 1のレスポンスのquantityの合計)", "3. 2のレスポンスのavailableがtrueなら「在庫引当済み」、falseなら「在庫不足」と返してください。", ].join("\n") return {messages: [{role: "user", content: {type: "text", text: prompt,}}]}; }, ); server.registerTool( "get-order", // omit: 今回は固定値を返すだけの実装にしています ); server.registerTool( "check-attached-inventory", // omit: 今回は固定値を返すだけの実装にしています ); 動作確認(MCP Inspector) プロンプトの利用(VS Code) 実際にプロンプトを使ってツールの実行を指示して動作を確認。 プロンプトで指示した順にツールを実行し、結果も指示した通りに返されました。 まとめ # プロンプトは、定形タスクを標準化したい場面で効果を発揮します。 プロンプト内でツールの呼び出し順序を指示し、同じサーバー内にそのツールを registerTool で用意しておくと、LLMが指示にしたがって自律的にツールを呼び出し、一連の作業を自動完結できます。 次編ではリソースを扱います。
はじめに # 本ページは「AIエージェントとシステムをつなぐMCP入門」の続編です。 今回は、StreamableHTTPで通信するMCPサーバーのステートフル実装について説明します。 ステートフル構成は、同じ利用者の連続操作を同一セッションとして扱いたい場合に有効です。 たとえば「ツールの呼び出し結果を次の呼び出しに引き継ぐ」「セッション単位で一時状態を保持する」「接続中の文脈を維持する」といった用途で使います。 本記事では、ステートレス実装との違いに焦点を当て、ステートフル構成で押さえるポイントを整理します。 掲載コードは こちら で公開しています。 --> シリーズ目次 連載:AIエージェントとシステムをつなぐMCP入門 イントロダクション stdio実装編 StreamableHTTPステートレス実装編 StreamableHTTPステートフル実装編(本ページ) 今回使用するライブラリなど # npm@11.11.1 node@22.22.0 typescript@6.0.3 @modelcontextprotocol/sdk@1.29.0 zod@4.3.6 ステートレスとの違い # まず、実装方針の差を先に整理します。 観点 ステートレス ステートフル サーバーおよびトランスポートのライフサイクル リクエストごとに生成・破棄 セッションごとの接続コンテキストとして生成し再利用 セッションID 基本は使わない sessionIdGenerator で各トランスポートに紐づくIDを決定して管理 接続処理 リクエストごと セッション初回リクエスト時に1回 終了処理 レスポンス後 SIGINTなどですべてのセッションをまとめて --> サーバー、トランスポート、セッションの関係 「1つのサーバーが、1つのトランスポートを使って、複数のセッションを管理する」形を想像しているとセッション管理の実装に困惑します。 今回使用したバージョンでは McpServer が同時に接続できる transport は1つで、 StreamableHTTPServerTransport も単一の sessionId しか保持できません。 そのため、今回のサンプルはセッションごとにサーバーとトランスポートを管理する形を採っています。 --> SIGINTとは SIGINT は「割り込みシグナル」です。 ターミナルで Ctrl + C を押したときにプロセスへ通知され、Node.jsでは process.on("SIGINT", ...) で終了前処理を実装できます。 今回は、サーバーで保持していた接続や状態の破棄に利用しています。 サーバーの実装 # 簡単にMCPサーバーを実装してステートレスとの差分を説明します。 コードの全体は こちら をご覧ください。 sequenceDiagram participant Client as MCPクライアント participant Server as MCPサーバー Client->>+Server: POST /mcp ※初回リクエスト Server->>Server: 新規セッション作成 Server->>Server: トランスポート接続 Server->>Server: ツール実行 Server-->>Client: 200 OK + MCP-Session-Id alt 2回目以降 Client->>+Server: POST /mcp ※MCP-Session-Idあり Server->>Server: 既存セッション取得(sessionId) Server->>Server: ツール実行 Server-->>Client: 200 OK end par プロセス終了時 Server->>Server: 全セッションをクローズ end 差分1: セッションコンテキストを保持する # ステートレスでは、リクエスト単位で都度生成していました。 ステートフルでは、セッションIDをキーにして接続コンテキストを保持します。 ここでの SessionContext は「MCPサーバープロセスそのもの」ではなく、SDKの接続モデルに合わせたセッション単位の実装コンテキストです。 type SessionContext = { server: McpServer; transport: StreamableHTTPServerTransport; }; const sessions = new Map<string, SessionContext>(); --> 実運用で状態を管理する場合 今回は簡潔さを優先してメモリで状態を管理しています。 実際に運用する場合は、メモリリーク防止や分散環境下の運用を考えるとNoSQLなどの外部ストアを使うことを検討したほうが安全です。 差分2: リクエストをセッション単位で振り分ける # 既存セッションIDがあれば対応するコンテキストを使い、なければ新しいセッションコンテキストを作成します。 sessionIdGenerator 名称から勘違いしてしまいがちですが、汎用的な採番戦略ではありません。 これはトランスポートに紐づくセッションIDを初期化時に決めるためのコールバックです。 MCP-Session-Id 初回のリクエストで振り出され、クライアントが受け取ります。 2回目以降のリクエストでは、 MCP-Session-Id ヘッダーとして付与して再利用します。 async function createSessionContext() { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), }); const server = createServer(() => transport.sessionId); await server.connect(refineTransport(server, transport)); return { server, transport }; } app.post("/mcp", async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; let context: SessionContext | undefined; if (sessionId) { context = sessions.get(sessionId); } else { context = await createSessionContext(); } await context.transport.handleRequest(req, res, req.body); // initialize 後に transport 自身へ設定された sessionId をキーに保持する。 const issuedSessionId = context.transport.sessionId; if (issuedSessionId) { sessions.set(issuedSessionId, context); } }); 差分3: プロセス終了時に全セッションをまとめてクローズする # セッションごとにサーバーとトランスポートを保持しているため、終了時にすべてのセッションを明示的にクローズします。 process.on("SIGINT", async () => { for (const context of sessions.values()) { await context.transport.close(); await context.server.close(); } process.exit(0); }); --> 実運用におけるセッションの終了処理 今回のサンプルは、プロセス終了時に全セッションをまとめてクローズする形にしています。 実運用では、一定期間操作がないセッションを自動的に破棄したり、クライアントが明示的にセッションを終了できる仕組みを用意することも重要です。 セッション管理を確認 # セッションごとに値を保持する counter ツールを追加実装して、セッションごとに値が保持されていることを確認します。 確認には複数セッションで操作が必要なので、MCP InspectorとPostmanを併用します。 MCP Inspectorの実行結果 Postmanの実行結果 MCP Inspectorから3回、Postmanから2回ツールを実行した結果です。 図で確認できる通り、それぞれ別のセッションIDが振り出され、セッションごとにcounterでインクリメントする値が管理されていることが確認できました。 --> PostmanでMCPを追加する場所 ひさしぶりにPostmanを使い、MCPのCollection追加に迷ったので場所を掲載しておきます。 セッションが切り替わることを確認 # MCPサーバーを再起動し、セッションが切り替わることを確認します。 先ほどとは異なるセッションIDが割り振られ、countが1に戻っていることが確認できました。 まとめ # ステートフルは、同一セッション内で、リクエストをまたいだ状態を保持できます。 一方、ステートフルにしたことで、終了時のクローズやセッション管理など運用面の責務が増えます。ツールの呼び出しに順序を求めるような場合はステートフルにしたいところですが、単純さを優先するならステートレスにするというのが現実的な判断になると思います。