TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

450

こんにちは。開発1部の村上です。 本記事は AIブログリレー 8本目 です。 エブリーではAIエージェントを社内のあらゆる業務で活用していくことを目指しています。そのAIエージェントたちを支えるのがデータ基盤です。そしてこの基盤を、AIにデータを「読ませる」ためのものから、AIエージェントを「動かす」ためのものへ進化させていきたいと考えています。本記事では、そのために必要なステップを整理します。 人のための基盤から、AIエージェントを "動かす" 基盤へ これまでのデータ基盤は、暗黙のうちに「人が使う」前提で設計されてきました。ダッシュボードを見るのも、SQLを書くのも人です。「この売上には手数料が含まれているんだっけ?」という定義の曖昧さも、人が文脈と経験で補完してきました。 利用者がAIエージェントに変わると、この暗黙の補完が効かなくなります。さらにエージェントが増えていくことを前提に立つと、基盤は特定の誰かの道具ではなく、すべてのエージェントが立つ共通の足場になります。足場が曖昧なままエージェントを増やすと、曖昧さの解釈がエージェントの数だけ生まれてしまいます。 さらにAIエージェント時代のデータ基盤は、データを参照して質問に答えるといった、データを「読める」ようにするだけでは不十分だと考えています。データにもとづいて業務の中で行動するエージェントを支える、つまりAIエージェントを「動かす」ところまでを、基盤の責務として考えていく必要があります。 このような基盤には、次の3つのステップがあると考えています。 参照できる : あらゆるデータを収集し、AIが参照可能な状態にする(メダリオンアーキテクチャ) 解釈を間違えない : 指標や用語の意味を一元定義する(セマンティックレイヤー) 行動できる : 業務のオブジェクト・関係・アクションを機械可読にする(オントロジー) 順に見ていきます。 あらゆるデータを集め、AIが参照できる状態にする 最初のステップは、構造化・非構造化を問わずデータを収集し、AIが参照できる状態にすることです。ここで採用しているのがメダリオンアーキテクチャです。 メダリオンアーキテクチャは、データをBronze(生データ)、Silver(クレンジング・整形済み)、Gold(ビジネス利用可能)という層に分けて、段階的に品質を昇格させていく設計です。 (出典: What is a Medallion Architecture? | Databricks ) ポイントは「きれいなデータを一発で作る」のではなく、生データを捨てずに保持したまま、信頼できる状態へ段階的に到達させることにあります。AIエージェント時代には、この層の分離がもう一つの意味を持ちます。エージェントにどの層を見せるかを制御できることです。品質保証されたGold層だけをエージェントの参照先にすることで、生データの揺らぎに引きずられた回答を構造的に防げます。 このステップまで整うと、Text to SQLが動き始めます。「先月のチャネル別売上は?」と聞けばエージェントがSQLを書いて答えてくれる。人が結果を確かめながら使う形であれば、AI活用はここから十分始められます。 ただし、「参照できる」と「正しく解釈できる」の間には溝があります。テーブルとカラムが見えることと、その数字が何を意味するかを理解していることは、別の問題だからです。 AIが解釈を間違えない状態にする 2つ目のステップは、セマンティックレイヤーの整備です。指標の定義、ディメンション、シノニム、用語といったビジネス上の意味を、BIツールやエージェントの側ではなくデータ層で一元的に定義します。 たとえばCAC(顧客獲得コスト)の定義がエージェント間で曖昧だったとします。すると、事業KPI分析エージェントとマーケティングエージェントが異なる計算式のCACを見て動いてしまう、といったことが起こります。しかもどちらのSQLも正しいため、この食い違いに気づくのは困難です。 Goldに計算済みのCACテーブルを置く手もありますが、CACのような比率指標は計算済みの値を再集計できません。「全チャネル合算では?」と粒度の違う質問が来た瞬間、エージェントはCACの平均を取って間違えます。だから必要なのは計算結果ではなく、計算式の定義です。 セマンティックレイヤーは、この定義をデータ層で管理します。Databricksの実装で言えば Unity Catalog Business Semantics がこれにあたります。中核となるmetric viewは、測度(CACの計算式そのもの)と、それを集計するディメンションを分離して定義する仕組みで、SQL・ダッシュボード・エージェントのどこから利用しても、同じ定義から同じ計算が決定的に実行されます。加えて glossaryやdomains といった用語・文脈を整備する機能の発表も続いており、この領域への投資はプラットフォーム側でも加速しています。 効果は数字にも表れています。dbtが2026年に再実施した ベンチマーク では、Text to SQL単体の精度は最新モデルで64.5%まで改善した一方、セマンティックレイヤー経由ではカバーされた質問に対してほぼ100%に到達しています。モデルの進化だけでは埋まらない差が、定義の一元化で埋まるということです。 ここまで整うと、エージェントは指標を正しく計算し、要因を正しく分解できるようになります。「読む」はほぼ完成です。しかし、分析だけではなく行動まで促そうとすると、まだ決定的に足りないものがあります。 AIが行動できる状態にする オントロジーとは何か 3つ目のステップがオントロジーの構築です。このブログリレーの 1本目でCTOも紹介 していましたが、オントロジーとは、業務を構成する オブジェクト (顧客、受注、商品...)、その 関係 、そしてそれぞれに実行できる アクション を、機械可読な形で定義したものです。 もともとオントロジーは知識工学の用語で、古典的にはオブジェクトと関係の記述を指します。エンタープライズの文脈ではPalantirがこれを拡張し、オブジェクト・プロパティ・関係というセマンティックな要素に加えて、アクションという「世界を変更する操作」までをオントロジーに含めています(参考: Palantir Ontology )。 業界の現在地 この領域は、まさに製品化が始まったところです。Databricksも2026年6月のData + AI Summitで独自のオントロジーである Genie Ontology を発表しました(現在プレビュー)。テーブル・クエリ・ダッシュボードから事業の文脈を自動抽出して生きたグラフを構築するもので、Unity Catalogで定義したセマンティクスがこのオントロジーに供給される構造になっています。アクション定義までは含まないかもしれませんが、オブジェクトと関係の自動構築という意味で同じ方向に進んでいます。 複数のエージェントが、同じ地図の上で動く 例として、定期購入型のEC(サブスクEC)でオントロジーの一部を考えてみます。 構成要素は次の4つです。 オブジェクト : 顧客、定期契約、定期受注(第n回の個別のお届け)、商品、在庫引当、配送 関係 : 顧客は定期契約を持つ。定期契約は定期受注を生成する。定期受注は商品を含み、在庫を引き当て、配送に紐づく 状態 : 定期受注は「受付中 → 出荷確定 → 発送済み」と遷移する。「お届け日の2日前に出荷確定へ移る」という締切の定義もここに属する アクション : 各オブジェクトに対する操作。前提条件・効果・権限がセットで定義される 配送先を変更する(前提: 状態が受付中) メニューを差し替える(前提: 状態が受付中、かつ、おまかせコース) 次回分の変更を予約する(対象: 定期契約。いつでも可) このオントロジーの上で、複数のエージェントが動くとどうなるか。同じサブスクECで動くCSエージェントと在庫オペレーションエージェントがいるケースを考えます。ここで見たいのは、互いに無関係な2つの出来事です。ある日、CSエージェントには顧客から「配送先を変えたい」という問い合わせが届きます。一方、在庫オペレーションエージェントの側では、入荷遅延によって木曜出荷分の在庫が不足していました。別々のトリガで、2体はそれぞれ独立に動き出します。それぞれの動きを並べるとこうなります。 CSエージェント 在庫オペレーションエージェント 起きたこと 顧客「配送先を変えたい」 入荷遅延で木曜出荷分の在庫が不足 関係の辿り方 顧客 → 定期契約 → 定期受注 商品 → 在庫引当 → 定期受注 受付中の受注への行動 「配送先を変更する」を実行 「メニューを差し替える」で欠品を吸収 出荷確定済みの受注への行動 「次回分の変更を予約する」に切り替え、「次回分から新しい住所にお届けします」と案内 引当済みのため対象外。手を付けない この2体は、会話もしていなければ、仕事の引き継ぎもしていません。起点も違います。片方は顧客から、もう片方は商品から関係を辿り、同じ定期受注オブジェクトに到達しているだけです。そして2体が共通して参照しているのは、定期受注の「状態」と、その状態を前提条件に持つアクションの定義です。顧客対応と欠品対応という別々の業務が矛盾しないのは、「出荷確定後は変更できない」というルールがアクションの前提条件として基盤に一つだけ存在して、両方がそれを参照しているからです。 ルールを各エージェントのプロンプトやMCPツールの説明文に書けば済む、と思うかもしれません。実際、エージェントが1〜2体のうちはそれで動きます。しかしそれは定義のコピーです。10体に定義がコピーされた世界では、ルール変更のたびに10箇所の改修が発生し、直し漏れたエージェントは旧ルールで動き続けます。API側で変更を拒否することはできますが、それで防げるのは誤った実行までで、エージェントは「変更できますよ」と案内してから実行に失敗する可能性もあるかもしれません。オントロジーはこの知識を参照に変えます。エージェントが何体いても、参照先は1つです。 この構造の強さは、ルールが変わる日にはっきり現れます。出荷締切を前日から2日前に早める、という業務判断が下りたとします。変更するのは基盤上の定義1箇所。その瞬間から、CSエージェントの案内も、在庫オペレーションエージェントの組み替え範囲も、将来作られる何体目かのエージェントの行動も、すべて同時に新しいルールに従います。 3層の上でエージェントを動かす ここまでの3ステップを一度に眺めると、こうなります。ステップ1でデータが見える。ステップ2で数字を正しく読める。ステップ3で「何ができて、実行すると何がどう変わるか」が分かる。 ここまではCSと在庫オペレーションの2体で見てきましたが、この土台の上には、マーケティングエージェント、予実管理エージェント、と業務ごとのエージェントを並べていけます。全員が同じ指標定義を読み、同じオブジェクトとアクション定義の上で動く。データ収集だけを進めてエージェントを増やすと、指標・フロー・ルールの定義が各エージェントのプロンプトへサイロ化していきますが、3層が整った基盤の上では、すべてのエージェントが同じ世界を見て行動します。 エブリーはどう取り組むか エブリーではステップ1にこれまで投資を続けてきており、メダリオンアーキテクチャによる基盤は整いつつあります。ステップ3のオントロジーは技術的にもまだ新しく、すぐに全面適用できる段階ではありません。そこで今期は、ステップ3を意識しながら、ステップ2のセマンティックレイヤーを整備していきます。 このステップ2と3は、アプリケーションのコードと人の暗黙知に散在している世界の定義を、基盤上の宣言的で機械可読な一箇所へ引き上げていく作業です。「CACの計算式」も「出荷確定後は変更できない」という定義も、今この瞬間もアプリケーションの実装の中に、そしてオペレーションを担う人の頭の中に存在しています。 そして、ここが重要なのですが、この作業はデータチームが外から観測するだけでは完遂できません。「締切を何時にするか」「CACに何を含めるか」は、データの中に答えがある問題ではなく、ビジネスの意思決定そのものだからです。セマンティックレイヤーもオントロジーも、技術の問題である以前に、ビジネスと一緒に定義を決めにいく組織の問題です。だからこそ、このリレーの 1本目でCTOが書いた 、エンジニアが事業の中に入ってその基盤を作っていくという話になるのだと思っています。 基盤を育てることと、ビジネスと共に定義を決めること。この両方が揃ったとき、データ基盤はAIにデータを読ませる基盤から、AIエージェントを動かす基盤になります。 さいごに 本記事では、AIエージェントを「動かす」ためのデータ基盤を、参照できる・解釈を間違えない・行動できる、という3つのステップで整理しました。エブリーではこの基盤づくりをこれから本格化させていきます。進捗や学びは、またこのブログで共有していく予定です。 エブリーでは一緒に働く仲間を募集中です! エンジニアブログをきっかけに少しでも興味も持っていただけたら、まずはカジュアルに面談しましょう!
AI活用をどう評価するか —— TokenMaxxing から TokenOptimization へ 株式会社エブリーでCTOを務めている今井( @imakei_ )です。 本記事は AIブログリレー 第7本目 です。自分はこれまで、第1本目で「 オントロジーと組織OS 」、第4本目で「 AIファーストな組織デザイン 」について書いてきました。今回はそれらと地続きのテーマとして、 AI活用をどう評価するか について書きます。 結論を先に言うと、AI活用の評価指標は固定するものではなく、フェーズに応じて TokenMaxxing → TokenOptimization と乗り換えていくもの、そして最終的には役目を終えて廃止すべきもの、だと考えています。 なぜAI活用の「評価」は難しいのか AI活用がどれくらい進んでいるかを測ろうとすると、すぐに壁にぶつかります。従来の生産性指標——コード行数、PR数、ベロシティ——は、AIを前提にするとうまく機能しないどころか、むしろ人をミスリードします。AIが書いた大量のコードを行数で評価すれば的外れですし、逆に「丁寧に少しずつ」を美徳とする指標は、AIに任せる動きにブレーキをかけてしまいます。 さらに厄介なのは、「そもそも、うちのメンバーはAIを使えているのか?」という一次情報すら、最初は曖昧だという点です。評価の設計は、この「まず現状が見えない」ところから始まります。 フェーズ1: TokenMaxxing —— まず「使い倒す」 弊社でCC(Claude Code)の利用ログを分析してまず見えてきたのは、 人間レビュー時代の習慣がAIの伸びしろに上限をかけている という構造でした。小さめのPR、慎重すぎる委譲、なんでも自分の目で最終確認する——CLIの利用時間が短いメンバーほど、この傾向が強く出ます。悪いことではないのですが、これは「人間が全部レビューできる範囲」に成果を縛る動き方で、AIを前提にすると足かせになります。 そこで導入したのが TokenMaxxing です。ざっくり言えば「トークン使用量を思いきり増やせ」という号令で、AIにどれだけ委ねたかを量で可視化し、行動そのものを変えにいく取り組みです。ここで大事なのは、これは あえて粗い指標 だということ。目的は精緻な測定ではなく、旧来の習慣を壊すための強制力(forcing function)です。いまはDatabricks上に利用ログを蓄積している段階で、個人単位での可視化はこれからですが、まずは「使っていい、むしろ使い倒せ」という空気を明確に作りました。 そしてもう一つ重要なのは、 これは半年程度の過渡期に限定した指標 であり、期末には不要になることを最初から織り込んでいる、という点です。TokenMaxxing は永続する評価軸ではありません。 TokenMaxxing の限界 —— 量は価値ではない TokenMaxxing には明確な限界があります。グッドハートの法則、すなわち「指標が目標になると、その指標は指標として機能しなくなる」という問題です。トークンを増やすこと自体が目的化すれば、無駄な生成や過剰な試行が増え、量が成果と乖離していきます。当然、コストも膨らみます。 つまり TokenMaxxing は、「使う」が当たり前になるまでの、期間を区切った指標です。使用が日常になった瞬間から、量を追うことは無駄を生む方向に反転します。だからこそ、次のフェーズへ乗り換える必要があります。 フェーズ2: TokenOptimization —— 「使いこなす」へ 次のフェーズが TokenOptimization です。ここでの軸は「量」から「質」へ、言い換えれば トークンあたりのアウトカム(value per token) へと移ります。同じ成果を、より少なく、より賢く出す方向です。 具体的には、タスクに応じたモデル選択(軽い作業に重いモデルを使わない)、コンテキストの効率化、harnessやスキル・サブエージェントの整備、そして「どこまでAIに委ね、どこは人が持つか」の見極め、といった話になります。 そしてここで効いてくるのが、これまでの連載で書いてきた土台です。第1本目のオントロジー・組織OSが整っているほど、AIは現実に正しく接地でき、少ないやり取りで的確に動きます——接地は、それ自体が最良の最適化です。また、有効だったプロンプトやスキルを個人に閉じず、第4本目で書いた「横糸」(勉強会・横断コミュニティ)で共有することで、TokenOptimization は個人技ではなく 組織の共有資産 になります。使いこなしは、横で流通させて初めて組織の力になります。 評価指標は「廃止する前提」で設計してもいい Token Maxxing から Token Optimization、アウトカムへの移行 ここまでを一段引いて眺めると、見えてくる原則があります。 過渡期の評価指標は、行動が定着したら廃止する前提で設計しておくと扱いやすい 、ということです。 TokenMaxxing は「使う」を、TokenOptimization は「使いこなす」を、組織に定着させるための指標です。定着すれば、その指標はもう見なくてよくなります。最終的に見るべきなのは、トークンのような手段の指標ではなく、事業に接続した アウトカム です。たとえば「ある事象の検知から、判断・実行・システム反映までが何分で回るか」といった、意思決定のループそのものの速さ。ここへ収斂させていくのが理想だと考えています。 裏を返せば、良い過渡期の指標には共通の条件があります。①行動をはっきり変えられること、②多少粗くてよいこと、そして③終了条件(いつ廃止するか)が決まっていること、です。廃止を前提にしない指標は、いつの間にか目的化して組織を縛ります。 おわりに TokenMaxxing で「使う」を、TokenOptimization で「使いこなす」を組織に定着させ、最後はトークンで測ること自体をやめて、事業のアウトカムで測る。評価指標を固定の正解として崇めるのではなく、フェーズに合わせて設計し、乗り換え、役目を終えたら廃止していく——これがAI活用の評価に対する、いまの自分の考え方です。 指標は、組織を導くための道具であって、目的ではありません。良い指標ほど、いずれ自らを不要にしていくものだと思います。
開発2部の内原です。 本記事は AIブログリレー 第6本目 です。 長く運用されてきたシステムというものは、様々な背景からいわゆるレガシー化していることも多いと思います。またそのシステムを担当していく際、多くの場合で似たような問題が発生します。 仕様書が古い or 存在しない 最初に実装したメンバーがいない テストがない このような開発・運用・保守しづらい状況だと、抜本的な対策をしたいが動かなくなるリスクを考慮して、都度継ぎ足し継ぎ足しの改修を繰り返すことになり、結果としてより開発・運用・保守が辛くなるという負のループに陥ることが多いと思います。 最近は生成AIがコードを書くのが当たり前の状態になっています。なので、AIに丸ごとリライトさせるということも不可能ではなくなっています。 しかし結論から言うと、レガシーシステムの刷新において単純なリライトでは機能しないことが多いと考えています。 この記事では、なぜそのような結論になるのかを整理したうえで、AIをレガシー刷新のための魔法としてではなく、理解と段階的移行を促進する道具として使う現実的な進め方を考えてみます。 なぜレガシー刷新は難しいのか そもそもなぜレガシーシステムの刷新は難しいと言えるのでしょうか。理由を分解すると、AIの使用未使用に関わらず共通している本質的な問題があります。 全体像を誰も把握していない=正しい仕様が不明 動いているコードだけが唯一の正であり、本来どう動くべきかを判断できる人がいない テストがない=正しさの基準がない リライト結果の動作が正しいものかどうかを判定する手段が存在しない 暗黙の依存・歴史的経緯が随所に残っている 一見特に重要ではないと思われるコードが、実は特別な処理のために必要だったりする ここで共通しているのは、これらがいずれも正しさを定義できていないことに起因している点です。 AIにリライトさせても、その結果が正しいかどうかを検証できなければ意味がありません。AIはそれっぽいコードを高速に大量生産することができますが、それが元の挙動を保っている保証はどこにもないためです。 つまりレガシー刷新においてまず行うべきことは、いきなりリライトすることではなく、正しさを定義できる状態に持っていくことです。 以下、その順序を3つのステップで見ていきます。 まず理解にAIを使う AIの最初の使いどころは、書くことではなく読むことです。レガシーコードの最大の問題は前述のとおり誰も全体を把握していないことなので、ここを埋めるのにAIは非常に向いています。 具体的には、次のような用途です。 コードの挙動説明 特定の関数やモジュールが何をしているのか、自然言語で要約させる 依存関係の可視化 どのモジュールがどこから呼ばれているか、影響範囲を洗い出させる デッドコード候補の洗い出し 到達不能なコードや、もはや使われていない分岐の候補を挙げさせる ポイントは、ドメイン知識を持つ人間と、横断的に大量のコードを読めるAIの分業が可能になる点です。 AIはコードが何をしているか(How)を高速に説明してくれますが、なぜそうなっているか(Why)は知りません。例えばある特殊処理が特定顧客向けの実装だった、といった背景です。AIに挙動を説明させ、人間がその背景知識と照らし合わせて確認していくことで、正しい仕様の確定を早めることができます。 ここで得た理解は、後のステップで何を保証すべきかを決める土台になります。 検証可能にする 理解が進んだら、次はリライト前の安全対策を実施します。テストがないコードをリライトするのはリスクが高いので、まず現状の挙動を固定するテストを用意します。 ここで有効なのが特性テスト(Characterization Test)という考え方です。これは正しい仕様をテストに書き起こすのではなく、現状のコードが今出している出力をそのまま期待値として固定するという手法です。レガシーコードでは正しい仕様が分からないので、ひとまず現状の挙動を基準にし、リライト後もそれが変わらないことを保証する、という発想です。 このようなテストの雛形づくりはAIが得意とするところです。入力と出力のパターンを大量に列挙させ、テストコードの雛形を量産させる。ただし、どのテストが妥当か、何を保証すべきかを判断するのは人間の仕事です。AIが生成したテストには、たまたま現状のバグまで正しい挙動として固定してしまうものも混ざります。それを取捨選択するのは背景を知る人間にしかできません。 この段階の本質は、正しさをAIや機械が判定できる形に問題を変換することです。一度テストという形で正しさが定義できれば、以降のリライトはテストが通るかという機械的に検証可能な問題に変えることができます。ここまで来て初めて、AIによるリライトが安全に回せるようになります。 段階的に移行する 安全対策が実施できたら、いよいよリライトです。ただしここでも一括リライトは避けます。大きな単位を一度に置き換えると、問題が起きたときに切り分けや切り戻しが難しくなるためです。 原則は、小さい単位に分解して、少しずつ置き換えることです。代表的なのがストラングラーフィグパターンで、新旧のコードを並行稼働させ、機能単位で徐々に新しい実装へ切り替えていく手法です。フィーチャーフラグで新旧を切り替えられるようにしておけば、問題が出てもすぐ元に戻せます。各ステップは小さく、テストを通し、差分をレビューできる大きさに保ちます。 このとき、変更を2種類に切り分けると見通しがよくなります。 機械的変換で済む部分 構文の置き換え、APIの一括リネーム、フォーマット統一など 決まったルールで変換できるものは、機械的ツール(codemodのような)やAIに任せやすい 意味的判断が必要な部分 データ構造の設計変更、業務ロジックの再構成など なぜそうするかの判断が伴うため、人間の関与が必要 AIは、前者を高速に処理し、後者においてはテスト補強や選択肢提示で人間を支援する。この両輪が機能することで、少しずつ移行を進めることが可能になります。 AIと人間の責任境界 ここまで見てきたとおり、レガシー刷新ではAIに任せていい部分と、人間が握り続けるべき部分とがあります。以下にその線引きを整理しておきます。 AIが苦手とするのは、次のような領域です。 暗黙の業務ルール コードには現れない、運用でカバーされている前提 なぜそうなっているかの歴史的背景 過去の障害対応や顧客対応の名残 影響範囲の見積もり 失敗したとき被害がどこまで及ぶか そして何より気をつけるべきは、一見もっともらしく動いているように見えて、実は元の挙動を微妙に変えてしまうような誤変換が発生し得ることです。テストが不十分なら、それも通り抜けてしまいます。これを見抜けるのは、背景を理解し、最終的に判断責任を持つ人間だけです。 自動化してよい判断と人間が最後に責任を持つ判断との線引きを意識する、AIを信用しすぎない、これがレガシー刷新でAIを安全に使うための重要なポイントと考えます。 参考:リファクタ実施例 ここまでの3ステップ(理解する → 検証可能にする → 段階的に移行する)を、実際に運用中のレガシーシステムで試してみたので、ステップに沿って紹介します。 ステップ1:まず理解する 最初に考えたのは、何を守れれば挙動が変わっていないと言えるのか、という点です。 今回のリライトで守りたかったのは、個々の関数やクラスの挙動というよりも、リクエストを受けてからレスポンスを返すまでの統合的な挙動でした。 Webサーバの設定やルーティング、フレームワークを経由した一連の処理は、単体テストの粒度では確認しきれません。単体テストを足していくやり方では抜け漏れが発生するリスクがありました。 そこで、アプリを一つの箱とみなし、HTTPリクエストという入力と、返ってくるレスポンスという出力だけで挙動を固定する方針にしました。これならWebサーバからフレームワークまでを通した実際の挙動をまるごと対象にできます。 これはまさに前述した特性テストの考え方そのものです。 ステップ2:検証可能にする 守るべき対象が決まったので、それを機械的に検証できる形にします。 ここで採用したのは、アプリをHTTPの外側から叩き、レスポンスを記録しておき、リライト後に同じ入力で照合して差分が出ないことを確認する Golden Master Testといわれる手法です。以下のような利点があると考えました。 アプリを改修せず、現状のまま丸ごと検証できる システム全体を一つのブラックボックスとして確認するので、テストのために内部を分割・差し替え可能にするような改修が不要になる 検証ツールがアプリの言語・フレームワークに依存しない HTTPさえ動けばよく、ツール側を別の言語で書いても成立する なおこの方式は記録したリクエストの分しか挙動を担保できない、という網羅性の限界があります。実際に叩いた経路しかカバーされないので、どのケースを記録対象に選ぶかという設計の判断が重要です。 実際にテストを実装し始めると安定しない要因が次々と出てきました。以下のようなものが実際にありました。 A/Bテストによる画面の出し分け そもそもA/Bテストの実装が入っていることを把握していなかった(テスト自体は当然終了しているはず) 出力がリクエストごとに変わるため、入力となるCookieを固定して結果を決定化した 日時に依存する挙動 時刻を凍結する拡張を導入し、日付に依存するページを再現可能にした 認証に依存する挙動 認証を前提とする箇所は、認証を突破するテスト用実装を追加(テスト環境でのみ有効) こうした調整を経て、公開ページや、認証前提の複数フローの導線、登録系ページについて自動的に検証できる状態まで到達できたと言えます。リライトの前後で、外部から見た挙動が変わっていないことを機械的に保証する安全機構を獲得できたことになります。 ステップ3:段階的に移行する(今後の予定) この時点では、一部のエンドポイントに安全機構を用意できたという段階で、ここから先こそが本題のリライトになります。 (というわけで現時点ではリライトできたという状況ではありません) 安全網ができたことで、小さい単位で置き換えてはテストで差分がないことを確認する、という段階的な移行が可能になります。 検証対象のエンドポイントを徐々に広げていくという、こうしたテストの横展開はまさにAIが得意とするところです。 まとめ レガシーシステムの刷新におけるAIの本質は、理解の加速と、検証可能な単位への分解の支援にあります。簡単にリライトしてくれる銀の弾丸ではないことがほとんどでしょう。 しかし、これまで仕様が分からない、テストがない、どこから手をつければいいか分からない、といった理由で腰が重かった刷新作業の速度を大きく上げられるのは確かです。 読んで理解し、安全対策を実施し、小さく置き換えていく、またその一連の流れをAIで加速する。そのような使い方をすることで、これまで手が出せなかったレガシーシステムに少しずつ手を入れていけるようになることがAIを利用する利点であると考えています。
こんにちは。開発1部の村上です。 本記事は AIブログリレー 5本目 です。 弊社ではすでに 全社共通ゲートウェイによる社内リモートMCPサーバー を構築したり、それをもとにMCPを社内で内製して多くの社員が使っていたりと、社外の公式MCPの利用も盛んになっています。本記事では、今後組織のMCP活用が拡大するとぶつかる課題を整理し、それを支える基盤としてのMCPゲートウェイというコンセプトについて書きます。 MCP is deadは本当か 基盤の話に入る前に、そもそもMCP自体必要でしょうか。今年2月末、 「MCP is dead. Long live the CLI」 という記事が反響を呼びました。LLMはCLIとドキュメントさえあれば勝手にやれる、認証は aws sso login や gh auth login という実績あるフローで済む、MCPサーバーは初期化が不安定で再認証が終わらない、権限の制御も粗いという主張です。日々Claude Codeを使っている人ほど、思い当たる内容だと思います。 一方で、GitHubのMCPサーバーを開発するエンジニアは、 MCP Dev Summit NA 2026の講演「MCP vs CLIs」 で、この論争が過熱していたまさにその時期にMCPの利用は急増しており、GitHub MCPサーバーは過去最大の週間利用量を記録したと語っています。「死んだ」と言われるものが、史上最も使われている状態なわけです。 CLIで十分か 改めてMCPとCLIの違いを整理してみます。 CLI MCP 設計の向き先 人間向けに設計済みのコマンド AI向けに設計されたツール・リソース モデルとの相性 学習データに豊富。パイプで合成できる スキーマで型が明確。素朴な全部載せはコンテキストを圧迫 認証・権限 トークン権限を全て委譲。 gh auth token などで露出し、環境変数から漏れやすい OAuth 2.1。ツール単位で操作面を絞れ、トークン権限をモデルから隔離できる 利用者・環境 シェルがある環境。主に開発者個人 リモート・非エンジニアにも届く 象徴的なのは認証です。CLIではトークンにできることはエージェントにもすべてできてしまう。しかも gh auth token 一発でトークン自体が露出します。一方でMCPのOAuthフローはトークンをモデルから隔離し、ツール単位で操作面を絞れます。 結局のところ、この論争は道具の優劣ではなく環境に依存してしまいます。CLIが成立するには、認証・権限・監査という関心事をOSの権限モデルと個人の責任が引き受けられる環境が必要です。数百人が複数のクライアントを使い、非エンジニアも含まれ、監査要件がある組織では、この前提が崩れます。個人は「使ってみる」で完結しますが、組織は、そこから「運用する」ことが求められます。そうなると組織がAIを活用していく上でMCPのメリットは十分にあり、引き続き有用な選択肢となっており、組織でどうMCPを管理していくかは重要なテーマだと思っています。 あらゆるシステムがMCPとしてエージェントと繋がる 増えるMCPサーバーとAIエージェント エブリーでは現在、外部MCPと社内MCPを合わせて数十程度のMCPが設定され、使われています。とはいえ、まだまだ発展途上だと思っており、エージェントに仕事を任せる範囲が広がるほど、CRM、チケット管理、社内Wiki、データ基盤と、社内外のあらゆるシステムがMCPを通してエージェントと接続されていきます。Uberでは、社内の1万を超えるサービスのエンドポイントをMCPツールとして公開できる状態まで到達しています( AAIF公式ブログ )。 そして増えるのはMCPサーバーの数だけではありません。呼び出し元も人間がClaudeを使うときにMCPを呼ぶだけでなく、社内で作られたエージェントが自らの判断でMCPを呼ぶようになります。定常業務を回す常駐エージェント、夜間にスケジュール起動するエージェント、コードを書き続けるバックグラウンドエージェントなど数百、数千の社内エージェントが、当たり前に稼働している未来が想像できます。 今はまだこのエージェントやMCPサーバー自体を増やしていき業務をエージェントに委譲することに注力していますが、同時にそれを推進することで新たに二つの課題に直面していくと考えています。 課題①: 統制が効かなくなる 一つ目は統制面です。誰がどのMCPサーバーに繋いでいるか分からない。各自のmcp.jsonに認証情報が散らばり、退職者のトークンがどこで生きているか追えない。エージェントに渡した権限が適正かを検証する仕組みがなく、何かが起きたときに「誰の委任で、どのエージェントが、何をしたか」を再構成できない。 エージェントに強い権限を渡せば、誤動作ひとつで本番データの削除や誤送信が起き、セキュリティ面ではエージェントそのものを禁止したくなります。かといって安全側に倒して権限を必要最小限まで削れば、今度は誰の役にも立たないエージェントになり、投資が回収できません。人間が誤操作で壊せる範囲には手間という上限がありますが、ループで動くエージェントにはそれがなく、簡単に大きな被害が出るインシデントを起こしかねません。だからこそ、それぞれのエージェントに何をさせられるかを管理できることが、組織のAI活用では大切になってきます。 課題②: 活用が進まなくなる 二つ目は活用面です。ここで言う活用とは、エージェントが正しくツールを選べること(精度)、社員が使い始められること(導入の手間)、そして改善が回ること(計測)の3つを指します。 まず精度という観点ではツール定義はそれ自体がコンテキストを消費します。数百のツール定義の読み込みだけで何万ものトークンを消費することもあります。ユーザーが何かを言う前に、この量が流れ込むのです。MCPが増えれば、モデルは進化しているとはいえ本題より先にツールの説明を読むことになり、選択を誤り、遅く、高く、不正確になります。 また、導入の手間という観点ではMCPが増えるほど、サーバーごと・ユーザーごとにOAuth認証を通したり、設定ファイルを書く作業は、非エンジニアも含めた全員の活用を考えると障壁になりえます。最後に計測という観点ではどのツールがどう誤選択され、どこで失敗しているかが見えなければ、ツール定義を改善するループが回りません。 MCP運用におけるゲートウェイというコンセプト 重要なのは、これらの課題がMCPというプロトコルの欠陥ではないことです。MCPが標準化したのは「どう話すか」だけであり、誰が・誰の権限で・何を・どこまで使い・誰がそれを見ているかという運用の関心事は、意図的に規定していません。だから薄く、速く普及しましたが、組織活用ではこの問題を解かないといけません。 その実装として業界が向かっているのが、MCPゲートウェイです。承認済みサーバーのカタログ(レジストリ)と、全トラフィックが通る中継点(ゲートウェイ)という構成です。レジストリが「何を使ってよいか」を定義し、ゲートウェイが「実際にそう使われているか」を実行時に強制する、という役割分担で、多くの製品はこの二つをセットで提供しています。2026年4月の MCP Dev Summit では多くの企業がMCPゲートウェイの上で社内のAI活用を推進しており、もはやそれを前提とした上でゲートウェイに関する機能の発表も多い印象です。Uberは 自社のエンジニアリングブログ でも、すべてのエージェントとサービスの間にMCP Gatewayを置き、呼び出し元の認証とツール実行の認可を通過したリクエストだけを下流に中継する構成を解説しています。 MCPゲートウェイが目指しているところ 何ができるのか ではゲートウェイは何ができるのか。業界で合意されているのは構成の型までで、機能の切り分けは各社各様です。今回は各社の実装を横断して、6つに整理します。 # 機能 統制面 活用面 1 単一経路化(集約) 野良MCPの遮断、統制点の一元化 1エンドポイントの追加で適切な権限範囲の全ツールに接続完了 2 認証・認可・シークレット管理 トークンの集中保管と即時失効 各自のOAuth設定が不要になる 3 ツール単位のポリシーとガードレール 削除系の禁止、引数の固定、レートリミット、PII検査 用途ごとに安全な形でツールを開放でき、自律的なエージェントやAI活用を促進 4 レジストリ(カタログ) 承認済みサーバーだけが流通する 「何が使えるか」を全員が発見できる 5 ツールの絞り込み・変換 モデルに見せる操作面そのものを縮小 必要なツールだけを動的にロードしコンテキストを削減 6 可観測性・監査 誰の委任で何が実行されたかの証跡 誤選択・失敗の計測とツール定義の改善 MCPゲートウェイはセキュリティ対策のみで切り取られがちですが、どの機能もAI活用自体を支える重要な役割を果たしていることがわかります。例えば認証仲介は、セキュリティ文脈ではトークン管理の話ですが、利用者から見れば効率的なオンボーディングの話です。ツール絞り込みは、統制文脈では操作面の縮小ですが、精度文脈ではコンテキスト削減そのものです。こうした安全な仕組みを作ってLLMに判断させない箇所を増やすことが、統制であると同時に信頼性の向上にもつながっていきます。 ユーザーの権限は「上限」であって「適正」ではない 各MCPサーバーがOAuthでユーザーを認証し、本人の権限の範囲で動く委任実行は正しい土台で、仕様もその方向に進んでいます。エージェント経由で本人以上の権限が使える、という権限昇格型の事故はこれで原理的に防げます。 しかし、ユーザーの権限は「上限」であって「適正」ではありません。自分の全メールを削除できますが、エージェントにそうさせたいわけではありません。エージェントは非決定的で、ハルシネーション1回の誤操作も「本人の正当な操作」として実行されます。必要なのは「このエージェントに許された操作」と「このユーザーの権限」の積集合を実行時に毎回評価することであり、自己規律に頼れない以上、この評価はエージェントの外側で行うしかありません。ゲートウェイとは、この評価を全リクエストに対して行う場所です。 現時点での技術的な選択肢 最後に、全てではないですが、このコンセプトを実装する際の現時点(2026年中期)でのゲートウェイを一部紹介します。 名前 特徴 agentgateway Linux Foundation傘下のOSS(Apache 2.0)。MCP/A2A対応のデータプレーンで、集約・ポリシー・可観測性が骨格の中立インフラ ToolHive Stacklok開発のOSS(Apache 2.0)。コンテナ隔離ランタイム・仮想MCPゲートウェイ・公式Registry API互換のレジストリサーバーまで揃うプラットフォーム型 Obot ゲートウェイ+カタログ+RBACを管理UI込みで提供する製品型OSS(MIT/オープンコア) AWS AgentCore Gateway AWSのマネージドサービス。セマンティック検索によるツール絞り込みが特徴で、既存API/LambdaのMCP化も担う Databricks Unity AI Gateway Databricksのマネージドサービス。Unity Catalogの権限モデルとエージェントトレーシングの延長でMCPを統治できる 選定の観点は既存スタックとの整合、運用体制で変わってくると思いますが、まだまだこの領域自体も発展途上なのでプレイヤーが大きく変わることも予想されます。 おわりに 今回はエージェントやMCPが組織で増えていった際にどういったことを考えないといけないのかを先行している企業の事例や技術的なトレンドから整理してみました。 エブリーでも今後エージェントを量産していきますが、その歩みを止めないためには、MCPゲートウェイのような基盤側の整備を同時並行で進めることが重要になります。エンジニア組織として、こうした基盤の構築を進めながら全社のAI活用を支えていきたいと思っています。 AIブログリレーは、このあとも開発部長陣が続けていきます。次回もぜひご覧ください。 エブリーでは一緒に働く仲間を募集中です! エンジニアブログをきっかけに少しでも興味も持っていただけたら、まずはカジュアルに面談しましょう!
AIファーストな組織デザイン 株式会社エブリーでCTOを務めている今井( @imakei_ )です。 本記事は AIブログリレー 第4本目 です。第1本目の「 オントロジーと組織OSとこれからのエンジニア 」(以下、前回)では、会社の現実を共通言語(オントロジー)として定義し、その上で人とAIが意思決定を回す「組織OS」を、エンジニア組織が主導して作っていく、という話を書きました。 今回はその続きとして、 AI時代の組織デザイン について書きたいと思います。組織OSという「仕組み」の話に対して、今回はそれを動かす「組織のかたち」の話です。結論から言うと、これからの組織は、 より少数で、事業にアラインし、その上で横のつながりを別途つくる ——という方向に向かうと考えています。 なぜ今、組織のかたちを問い直すのか 大前提として、AIによって一人あたりが生み出せる成果の量が大きく変わりました。これまでは「解きたい課題が増えたら、人を増やして対応する」のが基本の発想でしたが、AIを前提にすると、同じ課題をより少ない人数で解けるようになってきています。 この変化は、単に「生産性が上がった」で終わる話ではありません。 人を増やして解く時代の組織のかたちと、少数×AIで解く時代の組織のかたちは、そもそも設計思想が違う はずだ、というのが今回の出発点です。前回の組織OSが「AIが動ける土台」の話だったとすれば、今回はその土台を最大限に活かす「人の並べ方」の話だと捉えてください。 1. より少数な組織にする まず向かうべきは、少数化です。 人を増やすと成果が増える、という関係はAI以前から必ずしも成り立っていませんでした。人が増えれば増えるほど、コミュニケーションのコストや調整の手間が増え、意思決定は遅くなります。いわゆる「人を足したら余計に遅くなる」現象です。 AIを前提にすると、この構図はさらにはっきりします。これまで人手で埋めていた領域をAIに任せられるようになるほど、 人数を増やさずに対応できる範囲が広がる からです。だとすれば、やみくもに人を増やすのではなく、少数のメンバーがAIをレバレッジとして使い、一人あたりの担当範囲を広げていく方が、速くて強い組織になります。 ここで大事なのは、少数化は「人を減らすこと」が目的ではない、という点です。目的は、一人ひとりが事業の課題から実装までを見通せる状態をつくること。前回触れた社内FDE(現場に入り込み、課題定義から手を動かすエンジニア)が成立するのも、少数で担当範囲が広いからこそです。少数化とFDE的な動き方は、セットで効いてくると考えています。 2. 事業部にアラインした組織にする 次に、組織の切り方です。 エンジニア組織を「iOS」「バックエンド」「インフラ」といった職能で切る方法は、専門性を高めるうえでは合理的でした。ただこの切り方だと、一つの事業を前に進めるたびに複数の職能をまたいだ調整が必要になり、意思決定のたびに受け渡しが発生します。 弊社はデリッシュキッチン・トモニテ・retail HUBと複数ドメインのプロダクトを展開しており、職能ではなく 事業(ドメイン)にアラインした組織構造 をとっています。この組織へは1年ほど前に移行することを決め、すでに運用を始めています。1年前はあくまで仮説に基づくチャレンジ的な位置付けでしたが、1年動かしてきたうえでこの考え方が足元では正しかったと確信みたいなものを感じています。 この「事業(価値の流れ)にアラインした少数チーム」は、チームトポロジーで言う ストリームアラインド・チーム 、Spotifyモデルで言う スクワッド(Squad) にあたります。弊社の組織は、チームトポロジーをベースにしつつ、Spotifyモデルの教訓を取り込む形で設計しています。チームの人数を「ピザ2枚で足りる規模(5〜9人)」に抑えるのも、少数化と同じ狙いです。 なお、チームトポロジーにはこのほかに、共通基盤を担う プラットフォーム・チーム や、動画処理・機械学習などを扱う コンプリケイテッド・サブシステム・チーム もあります。本記事ではそこには踏み込まず、「縦=ストリームアラインド」と、後述する「横=イネイブリング/チャプター」の2つの軸に絞って話します。 この切り方には、AI時代に効く理由がいくつかあります。 一つは、意思決定のループが一つのチームの中で完結しやすいこと。「検知して、判断して、実行して、システムに反映する」という一連の流れが、職能をまたがずに閉じるので、少数でも速く回せます。 もう一つは、前回のオントロジーの話と地続きだという点です。組織OSの土台となるオントロジーは、「事業の現実(=名詞)」を中心にモデルを切ります。組織そのものが事業にアラインしていれば、チームの境界とオントロジーの境界が自然と揃う。エンジニアが事業のすぐ近くに座っているからこそ、現実に即した「名詞・動詞・ルール」を定義でき、社内FDE的な動きも構造的に成立します。 少数化と事業部アラインは、組み合わさって初めて力を持ちます。少数のチームが、一つの事業を丸ごと、課題定義から実装まで引き受ける。これがAI時代の基本ユニットになると考えています。 3. 横のつながりを、勉強会などで意図的につくる ここまでの「少数×事業部アライン」には、当然ながら弱点があります。 職能の知見が縦割りに分断されやすい という点です。 iOSエンジニアが各事業部に散らばると、iOS全体の知見をどう蓄積し、共有するのかが弱くなります。事業ごとに最適化が進む一方で、「あの事業部で解いた課題を、別の事業部がまた一から解いている」といった車輪の再発明が起きやすい。組織を縦(事業)で切ると、横(職能・技術)の糸が細くなるのは避けられません。 これを補うのが、 横のつながりを意図的につくる場 です。職能ごとの勉強会や、技術テーマごとの横断コミュニティといった、縦の組織図とは別のレイヤーの糸を通す。ここは自然発生に任せず、意図的に設計することが重要だと考えています。事業部アラインにすると横のつながりは「放っておくと消える」ものになるからです。 これは、Spotifyモデルが残した教訓とも重なります。スクワッドに自律性だけを与えた結果、サイロ化や車輪の再発明、全体最適の欠如といった問題が生まれた——という話です。「自律」だけでは組織は立ち行かず、意図的な横の連携という「ガードレール」が要る。Spotifyモデルの チャプターやギルド 、チームトポロジーの イネイブリング・チーム が担っていたのは、まさにこの横糸の役割です。弊社でも、縦(事業)に寄せた組織だからこそ、横糸を意図的に設計することを次の一手と位置づけています。 そしてAI時代には、この横糸を通す対象がもう一つ増えます。 AI活用の知見そのもの です。有効だったプロンプトや、業務を自動化するスキル、エージェントの使いこなし方——こうしたノウハウは、各事業部に閉じていると全社では育ちません。前回、組織OSの文脈で「便利ツールを共有資産へと育てる」という話をしましたが、その資産を横で流通させる場が、まさにこの横のつながりです。縦で事業を深掘りし、横でAI活用と技術知見を流通させる。この二層で組織を編むイメージです。 全体像:縦で深掘りし、横で編む 整理すると、こうなります。 少数化 で、一人あたりのレバレッジと担当範囲を広げる 事業部アライン で、少数のチームが事業を丸ごと、速く回す(縦の糸) 横のつながり で、職能の専門性とAI活用の知見を全社に流通させる(横の糸) 縦だけでは知見が分断され、横だけでは事業が進まない。両方を意図的に設計して初めて、少数でも強い組織になると考えています。そしてこの組織のかたちは、前回書いた組織OSと補い合う関係にあります。組織OSがAIに任せられる領域を広げるから少数化が成り立ち、事業部アラインだからオントロジーを現実に即して定義でき、横のつながりがAI活用の知見を共有資産へと育てる。仕組み(組織OS)と組織のかたちは、片方だけでは機能しません。 おわりに AI時代の組織デザインを、自分は「少数で、事業に張りつき、横で編む」という言葉で捉えています。人を増やして課題を解く発想から、少数のチームがAIをレバレッジに事業を丸ごと引き受け、横のつながりで知見を流通させる発想へ。これは一度作って終わりではなく、事業やAIの進化に合わせて組み替え続けていくものだと考えています。 前回の組織OSと合わせて、「仕組み」と「組織のかたち」の両輪で、AIファーストな組織づくりを進めていきたいと思います。 参考 Spotifyモデル: https://www.atlassian.com/ja/agile/agile-at-scale/spotify チームトポロジー: https://www.atlassian.com/ja/devops/frameworks/team-topologies
開発2部の内原です。 AIエージェントを使って開発していると、同じモデルを使っているはずなのにツールやエージェントによって賢さがまるで違う、という体験をすることがよくあります。モデルそのものは変わらなくても差が出る理由を考えてみます。 その差を生んでいるのが、モデルを取り巻く装備、いわゆるハーネスです。ハーネスという言葉はなんとなく使われがちですが、何を指しているのかは意外とふわっとしています(少なくとも自分の理解は割と曖昧でした)。 この記事では、この言葉を構成要素に分解して、何がエージェントの実力を決めているのかを整理してみます。 エブリーにおけるAIエージェントの活用状況とハーネスの必要性 先日の 村上からの投稿 全社のAIファーストを牽引するエンジニアの役割 でも言及がありましたが、エブリーでは今年から全社的にClaudeの活用を進めてきました。 それ以前からも特に開発部においてはCopilotやCursorなどのAIエージェントが利用されていましたが、当初は主にコーディング用途に使われていました。それが、開発部に閉じない業務改善、業務遂行のためにAIが用いられるようになってくると、エンジニアの役割も単なる実装者から、よりプロダクトや業務の理解を求められるようになり、業務フロー全体を考慮したオーケストレーションを実施する必要がでてきました。 その中で、より自律的なAI活用のためにはハーネスが重要になってくると考えています。AIに対しどのように振る舞うべきかを伝える必要があるためです。 なお、ハーネスの考え方自体はあらゆるAIエージェントに共通しますが、本記事では話を具体的にするため、特に開発で使うコーディングエージェントを念頭に置いて進めます。 モデル単体とハーネスの違い まずLLM単体は何をするものなのかを考えます。LLMは、突き詰めれば入力テキストに対して出力テキストを返すだけの関数であると言えます。それ自体ではファイルを読むことも、コマンドを実行することも、前回の作業を覚えていることすらできません。 一方実用的なAIエージェントは、そのモデルの周りに情報を渡す仕組みや道具、結果を確かめる仕組みを組み合わせ、それらを繰り返し動かしています。この一式をまとめてハーネスと呼びます。本記事では、ハーネスについてモデルを実世界におけるタスクに接続するための装備一式であると定義します。 ハーネスという言葉は、もともとは犬や馬のような対象に取り付けてその力を御し、目的の方向へ引き出すための装具を指します。ソフトウェアにおいても意味合いは同じく、テスト対象を動かすためのテストハーネス(スタブ、ドライバなど)のように、中心にある対象を制御して動かすための周辺の仕組みを指してきました。 AIエージェントのハーネスも同じで、モデルそのものではなく、その力を制御して実世界のタスクに向かわせるための装備だと捉えると分かりやすいです。 ハーネスを構成する4つの要素 ハーネスを、コンテキスト、ツール、権限・実行環境、検証ループの4つの要素に分けて見ていきます。 ただこれらの分類は絶対的なものではなく、目的や環境に応じて変化し得るものではあります。下記では汎用的に有効と思われるアプローチを考えます。 コンテキスト(何を渡すか) モデルに渡す情報の取捨選択です。モデルはコンテキストに入っている情報しか処理できないので、必要な情報をいかに過不足なく届けるかが重要です。 ただし、多ければよいというわけではありません。不要なものも大量に含めてしまうと、肝心の情報が埋もれて精度が落ちることになります。 関連するファイルやドキュメントのパスを渡す 長い履歴は要約して圧縮する 過去の経緯はメモリとして持たせ、必要なときに参照させる 過去の経緯を覚えておくメモリも、結局はその時々でモデルに渡すコンテキストの一部です。何を入れ、何を入れないかを設計することがコンテキストという要素の肝になります。 ツール(何ができるか) ファイルの読み書き、コマンド実行、検索、外部APIの呼び出しなど、モデルが現実の世界に働きかけるための道具です。 ツールがあって初めて、テキストを生成するだけのモデルが実際にコードを書き換えたり、テストを実行したりできるようになります。このとき、ツールの粒度や説明をモデルが正しく選択できる状態になっているかによって精度は大きく変動します。利用方法や目的が曖昧なツールだと、モデルに誤った使い方をされやすくなります。 権限・実行環境(どこまで許すか) ツールを与えるということは、モデルに実環境を触らせるということです。そのため、どこまでを自動で許すかの設計が必要になります。 サンドボックスで隔離する、権限モードによって実行範囲を絞る、破壊的な操作の前には確認を挟む、といった仕組みがこれに該当します。自動的に実行される範囲を広げるほど待機時間は減りますが、想定していない現象が発生したときの被害も大きくなります。 速さと安全のトレードオフの設計が重要です。 検証ループ(どう確かめるか) モデルの出力が正しいかどうかを、機械的なフィードバックとしてモデルに戻す仕組みです。テスト結果、型チェック、lint、実際に動かしたときの出力検証などがこれに該当します。 結果の検証が可能かどうかで成果は大きく変化します。検証ループの速さと確かさは、エージェントの賢さの大きな要因です。 4つの要素を動かすもの 4つの要素はいわば部品なので、これらを組み合わせて実際に仕事をさせるには、部品を方向づけ繰り返し動かす仕組みが必要です。 システムプロンプト モデルに対し、どのような役割、どのような方針で動いてほしいかを伝える指示です。 役割やルール、守ってほしい制約をあらかじめ与えるこの指示もハーネスの一部と考えることができます。ここではその場限りの指示を毎回書くのではなく、ハーネスの一部として作り込んでおく対象に変わったと言えます。 エージェントループ 部品をつなぎ、実際にタスクを前に進めるのがエージェントループです。考える→ツールを使う→結果を観測→考える、という繰り返しの制御フローを指します。 このループによって一度の応答で終わらず、失敗を踏まえて何度もやり直しながらゴールに近づくことができます。同時に、いつ止めるか、何回までやり直すかといった歯止めの設計も重要です。終了条件がなければエージェントは延々と動き続けてしまいます。 全体像と具体例 ここまでの要素を図にすると、おおよそ次のような関係になります。 Claude Code でいうと もう少し具体的に、コーディングエージェントのClaude Codeに当てはめてみます。 一般的なAIエージェントは自前でハーネスを構築していることが多いため、ユーザーが用意するファイル群はハーネスの一部にすぎず、ツールやループといった土台はClaude Code本体が提供しているという点に注意が必要です。 ユーザーが用意する層 コンテキスト CLAUDE.md、メモリ、作業中に渡すファイル システムプロンプト CLAUDE.md やスキルで与える役割・方針 権限・実行環境 settings.json でのツール許可やサンドボックスの設定 Claude Code本体が提供する層 ツール ファイルの読み書き、検索、コマンド実行、MCPなど 検証ループ hooks やテスト実行の仕組み エージェントループ 考える→ツール→観測→また考える、を回す中核 ただしこの線引きは必ずしも固定されていません。本体が提供する層であっても、検証ループは hooks やテスト実行の指示で何をいつ確かめるかを、エージェントループは進め方の指示で振る舞いを、ユーザー側からある程度調整することができます。 たとえばCLAUDE.mdには、方針(システムプロンプト)や参照(コンテキスト)に加えて、検証や進め方の指示も書けます。1つのファイルが複数の要素に対応していることになります。 # CLAUDE.md ## 方針(システムプロンプト) - コミットメッセージは英語、conventional commits 形式で書く - 既存コードのスタイルに合わせる ## 進め方(エージェントループ) - 大きな変更は、まず計画を示してから実装する ## 検証(検証ループ) - 変更後は npm test と npm run lint を必ず通す - テストが落ちたら、修正してから完了とする ## 参照(コンテキスト) - API仕様は docs/api.md を参照する 権限・実行環境のほうは .claude/settings.json などで設定します。 { " permissions ": { " allow ": [ " Read ", " Edit ", " Bash(npm test) " ] , " deny ": [ " Bash(rm:*) " ] } } これらはいずれもハーネスの一部です。ツールやループといった土台はClaude Code本体が担っており、両者を合わせて初めて一つのハーネスとして機能することになります。 AI-DLC との関係 最近では、AWSが提唱するAI-DLC(AI-Driven Development Life Cycle)のように、AIを中心に据えた開発方法論も出てきています。AgileやScrumに代わる開発プロセスそのものを再設計しようとするものです。 ポイントとしては、こうした方法論はハーネスの上に乗る上位のレイヤーであるということです。AI-DLCが掲げるAIが計画して人間が承認し実装に進むという流れも、成果物を蓄積して引き継ぐ仕組みも、各作業単位で良いハーネスが組まれていることが前提になっています。 このような方法論を導入してもうまくいかないとしたら、その下のハーネスが整っていない可能性があります。 まとめ AIエージェントの賢さとは、モデルとしての性能が全てではなく、コンテキスト、ツール、権限・実行環境、検証ループといった環境に左右されます。 AIエージェントを使いこなすうえでは、ハーネスの設計がエンジニアに求められる重要なタスクになると考えています。
こんにちは。開発1部の村上です。 本記事は、エブリーの開発部長陣がリレー形式で組織とAIについて書いていく「AIブログリレー」の第2本目です。 第1本目で、CTOの今井が 「オントロジーと組織OSとこれからのエンジニア」 という記事を書きました。会社の現実を人間とAIの共通言語として定義し、その上で意思決定が回り続ける「組織OS」を作る。そしてその組織OSづくりはエンジニア組織が主導すべきであり、最初の一歩として社内FDE(Forward Deployed Engineer)を立ち上げる、という内容です。 エブリーは 「AIファースト・カンパニー」 を掲げています。AIを単なる業務効率化ツールではなく、あらゆる領域で私たちの力を増幅してくれる存在と捉え、働き方もプロダクトもAIを前提に変えていく。この方針のもと、全職種でのAI活用を進めてきました。 本記事では、その現場で実際にどうAI活用がされているのか、AIファーストを実現するためにエンジニアは何をしていくべきかを書いていきます。全社のAIファーストを推進していくことは、自社プロダクトの開発と並行して、エンジニアが価値を出すべき領域になってきています。そしてその領域を牽引していくことこそが、エンジニアが業務ドメインに深く入り込み、社内FDEとして動くことだと考えています。 Claudeの全社導入で起きたこと エブリーでは今年の2月ごろから、段階的にClaudeの全社導入を進めてきました。対象はエンジニアに限りません。営業、マーケティング、CX、広報、クリエイターまで、全職種です。 導入後の変化は、数字にはっきり表れています。OpenTelemetryで計測している非エンジニア職のClaude Codeのアクティブユーザー数は、導入当初から約4倍に拡大しました。今ではエンジニア全体よりも多くの人がClaude Codeを活用しており、チャットでの利用にとどまらず、非エンジニアがClaude Codeを使いこなし、チームで作ったスキルをGitHubで管理する運用まで、エンジニア組織の外側で自然に立ち上がっています。 活用の中身も、当初想定していた文章作成やリサーチの域を越えています。 自然言語でデータに問い合わせ、自分の業務の分析を自分で回す レポート作成のような定型業務を仕組み化し、実際に事業成果へつなげる 手作業では年単位かかる規模の作業を、自動化ツールに置き換える コードを書いたことのないメンバーが、自分の業務に合わせた専用のダッシュボードや業務ツールを自分で作る こうした事例が、特定の部署に限らず全社のあちこちで同時多発的に生まれています。 そして、これらを横から見ていて感じるのは、 その業務に一番詳しい人が作るものが、一番使えるものになる ということです。 当たり前のことかもしれません。しかし、これまではそれが実現できていませんでした。業務に精通した人は、業務上の課題も、何が事業にとって重要なインパクトを持つのかもよく知っています。それでも自分では作れないため、エンジニアに依頼するしかなかった。そしてその過程で、やりたいことの意図や、言葉にしきれない業務上の肌感覚が抜け落ち、絶妙に求めているものと違うものが出来上がる。業務をやる人と作る人が分かれていたからこそ起きていた問題です。 AIによって「作ること」のコストが下がり、この壁は急速になくなりつつあります。分析ダッシュボードや業務効率化ツールの開発において、「エンジニアにしか作れないもの」は確実に減ってきています。価値の源泉が、実装できることから、業務を知っていることへ移り始めている。これが、全社導入から数ヶ月の現場で起きている変化です。 タスクの効率化から、業務フローの再設計へ この変化により、単一のタスクの効率化にとどまらず、業務の中で連続する複数のタスクをまとめて圧縮する動きも進み、業務時間そのものが削減されています。 そして、活用がここまで進んだからこそ、エブリーは次のステージも見えてきました。 これまで速くなってきたのは、既存の業務フローを構成する個々のタスクです。一方で業務フローそのものは、AIが存在しない前提で、多くの人間が関わることを前提に設計された当時のまま残っています。次は工程の並びと役割分担そのものをAI前提で組み直す番であり、エブリーの現状はタスクレベルの活用が中心だからこそ、ここには大きな伸びしろが残っています。 実際に、ここで大きな差が生まれることを示すデータがあります。 McKinseyの The State of AI(2025年11月版) :AIを利用する企業は 88% に達した一方、AIから大きな価値(EBIT=利払前・税引前利益の5%以上)を実現できている企業は 約6% にとどまる 同調査の2025年3月版 :検証した25の組織的要因のうち、生成AIのEBITインパクトに最も大きく効いたのは「ワークフローの根本的な再設計」だった つまり、AI活用の成果を分けるのはツールの導入量ではなく、業務フローそのものをAI前提で組み直したかどうかだ、ということです。 これは、冒頭で紹介したAIファースト・カンパニーの方針とも重なります。CEOの吉田が 社内に向けて語った のも、まさにこのことでした。 多くの人間が関わることを前提として設計された既存のオペレーションに対して少しずつAI対応を進めても、働き方の大きな変化を得る可能性は低いです。多くの場合は、1からAIを前提としたオペレーションを設計し直す必要があります。 タスクレベルの活用で得た成果は入口であって、本丸はその先にあります。エブリーが真の意味でAIファーストになっていくために越えるべきマイルストーンは、次の2つだと考えています。 AIを前提とした業務フロー全体の再構築 : 個々のタスクの効率化ではなく、どの工程をAIに任せ、どこで人が判断するのかという前提から業務を組み直すこと AI活用の、個人から組織への標準化 : 現状の活用は個人のスキルとツールに依存しています。これを組織として標準的に使え、学習し、育てていける基盤にすること そして、この2つを推進する役割こそが、冒頭で述べた社内FDEだと考えています。 FDEという役割 FDE(Forward Deployed Engineer)とは、顧客の現場に深く入り込み、その業務を理解したうえで、自社のプロダクトを軸にシステムを組み上げ、成果が出るまで伴走するエンジニアのことです。Palantirが確立した役割で、同社では一時期、FDEの数がプロダクト側のソフトウェアエンジニアを上回っていたほど、事業の中核を担ってきました。 なぜAI時代に求められるのか 生成AI時代に入り、OpenAIやGoogle、Microsoftといった代表的な企業が相次いでFDE組織の立ち上げや大規模な採用を進めるなど、この役割はいま世界的に再注目されています。 なぜこれほど求められているのか。モデル自体はAPIで誰でも呼べるようになった一方で、本当に難しいのはその先、つまりAIを企業ごとのデータ・権限・業務プロセスに接続し、信頼して任せられる状態まで持っていく部分だからです。そしてこの仕事には、業務を知っていること(何が正しく、どこが本当の課題か)と、システムを作れること(それをどう実装し、安全に動かすか)の両方が必要です。片方の知識だけでは成立しません。 さらに、前のセクションで見たとおり、AIで大きな価値を出すには既存の業務にAIを足すのではなく、業務フロー自体を再設計する必要があります。業務に深く入り込み、業務とシステムを一体で作り変えられるエンジニア。FDEへの需要の高まりは、AI活用の主戦場がモデルを作ることからAIモデルがフルに活用できるように業務や基盤を作り変えることへ移ったことの表れだと捉えています。 社内FDEという考え方 エブリーでは、このFDEの動きを社内にも向けます。事業部の業務に深く入り込み、AIを前提とした業務フローへの再設計を、業務とシステムの両面から進めていく。これが、冒頭から述べている社内FDEです。 もっとも、エンジニアが社内の業務に関わること自体は、これまでもやってきました。では何が違うのか。立ち位置と責任範囲が変わります。 これまでの社内向けの開発は、事業部が設計した業務フローを前提に、その中で使う一部の業務効率化ツールを作ることが中心でした。業務フロー自体は所与のもので、エンジニアが担うのはツールを完成させるところまでです。 社内FDEは、この立ち位置を変えます。Forward Deployedという名前のとおり、エンジニアが業務の現場に出ていき、課題の特定と業務の再設計から関わります。どの工程をAIに任せ、どこで人が判断し、そのためにデータや既存システムをどう作り変えるかまでを、業務側で携わるビジネスメンバーと一緒に決めていく。作る対象は業務フローの中の一部のツールではなく業務フロー全体であり、責任を持つ範囲はツールの完成ではなく、業務が実際に変わり、成果につながるところまでです。 なぜ社内FDEをエンジニアがやるのか 1本目で今井は、組織OSづくりをエンジニア組織が主導すべき理由を、組織の観点から4つ挙げていました。ここでは業務フローの再構築という現場の目線から、私の考えを重ねます。 タスクレベルの改善であれば、担い手はその業務に一番詳しい人です。それが一番使えるものになることは、いまエブリーの現場で起きている変化として紹介したとおりです。 しかし、業務フローの再設計となると、業務に詳しいだけでは踏み込めない領域が出てきます。業務側から見えているのは、画面と手順、つまり業務フローの表側です。表側だけを見ていると「この画面をこう改善する」までしか描けません。一方エンジニアは、社内基盤・社内ツール・自社プロダクトの裏側まで、システムの全体像を一気通貫で把握しています。裏側のデータ構造やシステム間の連携まで分かっていれば、「そもそもこの工程は要らない」「ここはAIに渡せる形にデータ構造から作り直そう」と、より広く、根本から設計できます。 実際、AI導入が最後につまずくのは決まって現場の業務・データ・既存システムであり、そこを日常的に握っているのもエンジニアです。業務の作り変えをいちばん深く、いちばん速く進められる位置に、すでに立っています。 一方で、これは「エンジニアだけでできる」という意味ではありません。業務フローの再設計にはその業務の解像度が不可欠で、それを持っているのは業務の当事者です。だからエブリーでは、社内FDEを、ビジネス側とエンジニア側でタッグを組んだプロジェクトとして進めます。 現在地と、これから FDEの活動としてではないですが、直近でも確かな手応えがすでにあります。 全社共通ゲートウェイによるセキュアな社内リモートMCPサーバー を整備したり、既存サービスの保有しているデータをMCPサーバーとして展開するだけでも、すでに現場でAIに任せられる業務は格段に増え、非エンジニアによるAIへの委譲が進んだ実感があります。 これらはいわば、業務の外側からAIとの接続を配線してきた取り組みです。だとすれば、業務の内側に入り込み、業務フローそのものと一体で配線し直せば、そのレバレッジはさらに大きくなるはずです。それを検証しにいくのが、社内FDEだと考えています。 必要に応じて今後は以下のような整備を横断して取り組んでいきます。 散らばったデータの意味を揃え、人もAIも同じ意味で扱える状態にするデータ基盤(セマンティックレイヤー) 社内のシステムやデータへ、安全に読み書きできる操作の層(MCP、API) 個人の環境に依存しない、権限と監査の効いたエージェントの実行基盤 AIに業務を渡せる形への、既存システムやデータ構造そのものの組み替え これらは一度作って完成するものではなく、業務に入り込むたびに磨き上げていく、社内FDEの道具であり成果物です。 ただし、どの業務から、どの粒度で手をつけるかを、机上で決めきるつもりはありません。どの工程が本当のボトルネックで、どのデータが現場で信用されているのかは、業務の中に入って初めて分かることだからです。ロードマップがあって社内FDEが動くのではなく、社内FDEが現場で掴んだものがロードマップになる。この順序で進めていきます。 社内FDEとして意識すること 最後に、社内FDEとして業務に入っていくうえで、大切にしたいと考えていることを3つ書きます。 1次情報を取りに行く 1つ目は、業務を1次情報で理解することです。 これまでのやり方の延長では、誰かが整理した資料や、又聞きの説明で業務を理解したつもりになってしまいます。しかし、整理された情報からは、業務フローを再設計するうえで本当に重要な情報やニュアンスが抜け落ちています。資料には書かれない例外処理、担当者が無意識にやっているリカバリー、数字に表れない現場の判断。再設計の勘所は、たいていそこにあります。 FDEの原点であるPalantirには、FDEがFBIの捜査官の隣に座り、捜査のプロセスをその場で観察しながらシステムを組み上げていったといった話もありますが、社内FDEも同じで、泥臭く現場に出て業務を観測し、向き合う業務を行っている人と同じ解像度になるところまでやる。ドメインに深く入り込むとは、具体的にはこういうことだと考えています。 御用聞きにはならない 2つ目は、現場に深く入り込みながらも、言われたものをそのまま作る存在にはならないことです。 現場の要望には必ず理由がありますが、要望の形をそのまま実装することが最適とは限りません。その裏にある本当の課題は何か、業務フロー全体で見たときにどこを変えるのが根本的か。業務の当事者と同じ解像度に立ったうえで、システムの全体像を知る者としての選択肢を出し、一緒に決めていく。 深く入り込むからこそ、頼まれたものを作る関係に流れやすくなります。そこで踏みとどまって業務フロー全体の再設計に立ち返れるかどうかが、社内FDEと従来の社内ツール開発の分かれ目だと考えています。 アウトプットではなく、アウトカムを追う 3つ目は、作ったものではなく、変わった業務で成果を測ることです。 MCPやエージェント、ダッシュボードを作ること自体は手段にすぎません。その先で業務はどれだけ圧縮されたのか、それによって事業の数値にどう貢献できたのか。そこまで追いかけて、初めて社内FDEの仕事は完了します。 作ること自体の満足に流れないために、社内FDEのプロジェクトには、アウトプットの完成ではなくアウトカムの指標を置きます。先に紹介したMcKinseyの調査が示すように、AIは導入しただけでは大きな価値につながりません。成果から逆算して業務に入り、成果で締める。この規律は、動きが自由なぶんだけ強く持つ必要があると考えています。 おわりに AIによって「作ること」のコストが下がり、エブリーの現場では、業務に一番詳しい人が自分の道具を自分で作る変化が起きています。実装が民主化されていくこれからの時代に、エンジニアの価値は、業務の裏側までを理解したうえで「何を、なぜ作り変えるのか」を描き、実現できることに移っていきます。 だからこそエンジニアが業務ドメインに深く入り込み、社内FDEとして業務フローの再構築と、AI活用の個人から組織への標準化を牽引していく。それがエブリーの目指すAIファーストへの道であり、これからのエンジニアの大きな価値の出しどころだと考えています。 この転換はまだ始まったばかりです。完成した仕組みの上で働くのではなく、業務とシステムをどう作り変えるかを描くところから関われる。エンジニアにとって、これほど面白いフェーズはなかなかないと思っています。興味を持っていただけた方は、ぜひ一度お話ししましょう。 AIブログリレーは、このあとも開発部長陣が続けていきます。次回もぜひご覧ください。 エブリーでは一緒に働く仲間を募集中です! エンジニアブログをきっかけに少しでも興味も持っていただけたら、まずはカジュアルに面談しましょう!
株式会社エブリーでCTOを務めている今井( @imakei_ )です。 本日からエブリーの開発部長陣が考える組織とAIについて 全10本にわたって開発部長陣とリレー形式で、ブログを投稿します! 今回はその AIブログリレー 第1本目 で、「オントロジー」と「組織OS」という考え方を入り口に、これからのエンジニア組織の役割について書きたいと思います。 結論から言うと、この組織OSづくりは、エンジニア組織が主導すべきだと考えています。 オントロジーと組織OS まず前提を簡単に整理します。 弊社エブリーはAIファーストカンパニーとして、プロダクトでの活用はもちろん、開発現場でもAIの実用的な活用を進めてきました。その延長で最近考えているのが、「組織そのものを、AIが動けるOSにしていく」というテーマです。 ここで鍵になるのが「オントロジー」という考え方です。簡単に言えば、会社の現実を、人間とAIの双方が同じ意味で扱える共通言語として定義したもの、と捉えてください。中身は3つの要素で考えると分かりやすいです。ここでは、商品を扱うようなサービスを例に挙げると以下のようになります。 名詞 :「顧客」「注文」「在庫」が、具体的にどのデータの何を指すのか 動詞 :それに対して「返金する」「担当を変更する」など、何ができるのか ルール :誰が閲覧・実行してよいか、変更履歴をどう残すか この共通言語の上で、人とAIエージェントがともに意思決定を回せる状態——それを本記事では「組織OS」と呼びます。データを溜める基盤ではなく、意思決定が回り続ける基盤を作る、というイメージです。 そして本題は、これを誰が主導するのか、です。経営企画なのか、DX推進部門なのか、データ基盤チームなのか。自分は、エンジニア組織が主導すべきだと考えています。 なぜエンジニア組織が主導すべきなのか 理由を4つに分けて述べます。 1. 「現実の散らかり」を最もよく知っているから 弊社はデリッシュキッチン・トモニテ・TIMELINEと複数ドメインのプロダクトを展開しており、「顧客」という言葉ひとつとっても、サービスやシステムごとに定義もIDも異なります。 この散らかりを、日々データとシステムに向き合う中で肌感として把握しているのはエンジニアです。どこに正となるデータがあり、どこが信用でき、どこをつなぐと何が壊れるか。この地図を持っているのは現場のエンジニアであり、オントロジーの「名詞」を本当の意味で定義できるのもここを知っている人だと考えています。 2. 「動詞」を安全に実装できるのはエンジニアだから オントロジーは名詞だけでは動きません。「返金する」「担当を変更する」といった動詞があって、初めて意思決定が現実に反映されます。 そしてこの動詞を、前提条件をチェックし、実行ログを残し、必要に応じて本番反映前にシミュレーションできる——そうした安全な形で実装するのは、まさにエンジニアリングの仕事です。ここはビジネス側がどれだけ要件を整理しても代替できない領域だと考えています。 3. AIを現実に接地させるのはエンジニアの仕事だから エージェントを業務に組み込むうえで最も重要なのは、現実のモノと操作に正しく接続されているかどうかです。MCPでツールを提供するにしても、権限を紐づけて監査可能にするにしても、これはすべてエンジニアリングの領域です。 優れたモデルを選定する話ではなく、そのモデルを現実に配線する話だと捉えています。配線できる担い手がいなければ、上位でどれだけ旗を振っても組織は動きません。 4. 抽象化とモデリングがエンジニアの本職だから 「現実を、扱いやすい構造に切り分けてモデルにする」という営みは、エンジニアが日々の設計で行っていることそのものです。オントロジーづくりは、対象がDBスキーマから「会社の現実」に変わっただけで、求められる思考の型は変わりません。むしろエンジニア組織が最も得意とする作業だと考えています。 オントロジーと組織OSとエンジニアの関係 これからのエンジニア組織が担う役割 「顧客とは何か」といった名詞の"意味"そのものを決めるのは、事業判断であり、ビジネスの仕事です。ここはエンジニアが閉じた場で勝手に定義してよいものではありません。 一方で、これからのエンジニア組織には、 その定義を「決めさせ、形にし、維持する」役割 が求められると考えています。具体的には、次のような動きです。 曖昧なまま放置されている定義に対し、「3つのシステムで意味が異なりますが、どれを正としますか」と 問いを立て、決断を促す 決まった定義を、ドキュメントやコード(データフローの定義、スキーマ、設定ファイル等)に 落とし込み、形にする それが時間とともに崩れないよう 守り、育てる 言い換えれば、Howを握りながらWhatを引き出す、ということです。定義そのものはビジネスと協働して決めますが、その場を設計し、形にし、維持する力学はエンジニア組織が担う。辞書を「コードとして」保持するからこそ、エンジニアが自然とその番人になれるのだと考えています。 従来のエンジニア組織が「決まった仕様を実装する」役割だったとすれば、これからは「組織が何を意思決定すべきかの土台そのものを整える」役割へと重心が移っていく。これがAI時代のエンジニア組織の役割になっていくと捉えています。 エンジニア組織として、ここからどう動くか 具体的な進め方はこれから詰めていく段階ですが、最初の一歩として、 社内FDE(Forward Deployed Engineer)の立ち上げ から着手しようとしています。 FDEは、もともとは顧客の現場に深く入り込み、その場の業務課題を解きながらシステムを組み上げていくエンジニアを指す役割です。これを社内に向ける、というのが「社内FDE」のイメージです。事業の現場に入り込み、業務とデータの実態を一次情報として捉えたうえで、前述の「名詞・動詞・ルール」を現実に即して定義し、形にしていく。オントロジーづくりは、机上の設計ではなく現場に張り付く作業だと考えており、それを担う人をはっきり置くことが、主導していくうえでの起点になると捉えています。 弊社は職能横断でビジネスにアラインした組織構造をとっており、エンジニアがもともと事業の近くに座っています。その意味で、社内FDE的な動きは構造的にも相性がよいと考えています。 まずは課題の大きい領域に社内FDEを置き、そこで得た知見をもとに、共通言語をどう全社の共有資産へと育てていくか——このあたりを走りながら具体化していく予定です。進め方や得られた学びは、また別の機会に書ければと思います。 主導するうえで意識していること 最後に、旗を振るうえで気をつけている点を3つ挙げます。 1点目は、 完璧な全社オントロジーを最初から作ろうとしないこと です。すべてを一度に進めようとすると必ず頓挫します。最も課題の大きい1ドメインから、小さく始めるのが現実的だと考えています。 2点目は、 エンジニアだけで定義を閉じないこと です。Howは握りますが、Whatはビジネスを巻き込んで決める。ここを取り違えると、技術的には正しいが誰も使わない辞書ができあがります。 3点目は、 「便利ツールづくり」と「基盤づくり」を混同しないこと です。目の前の自動化は成果が見えやすく魅力的ですが、それを共有資産の規律に乗せるところまで進めて初めて「組織OS」と呼べる状態だと考えています。 おわりに AI時代に入り、エンジニア組織の役割は「機能を作る」ことから、もう一段広がろうとしていると感じています。会社の現実をモデルに落とし込み、人とAIがともに動く土台——すなわち組織OSそのものを作る、という役割です。 そしてこの仕事は、現実のシステムに最も近く、動詞を安全に実装でき、AIを現実に配線でき、抽象化を本職とする、エンジニア組織が最もうまく担える領域だと考えています。経営や事業を巻き込みながらも、駆動するのは自分たちだと引き受けにいく価値のあるテーマです。 弊社でも、「個人のAI活用」から「組織のAI活用」へと軸足を移そうとしている今、ここはエンジニア組織が主導していきたいと考えています。 出典・参考 本記事における「オントロジー」の考え方は、以下のPalantirの記事を参考にしています。 Palantir, " Connecting AI to Decisions with the Palantir Ontology "
こんにちは、25卒エンジニアの 190 です。 2026年6月5日、弊社のオフィスを会場に「 26卒エンジニアの輪を広げNight 」というイベントを開催しました。さまざまな会社に入社した26卒エンジニアに集まってもらい、お互いの働き方やこれからの抱負を共有して交流してもらう、というイベントです。 このイベントは、弊社25卒の私と uho-wq 、 あかがわまさとも の3人で企画・運営しました。 本記事では、企画した背景・当日の内容・運営してみて気づいたことをまとめます。 背景 私たち25卒は、入社1年目に社外のエンジニアと知り合える機会に恵まれ、そこでできたつながりが日々の支えや刺激になっています。 一方で、入社して間もない時期は、他社の同期がどのように働いているのか、どのようなことを考えているのかを知る機会は意外と多くありません。 「後輩にも自社だけではなく、他社のエンジニアと交流してほしい」という思いから、26卒が他社のエンジニアと交流できる場を自分たちで用意しよう、と考えたのがこのイベントのきっかけです。 イベントの目的は、技術力の向上ではなく、次の点に置きました。 他社の同期が今どんなことに取り組み、何を考えているのかを知る そのうえで、気軽に相談し合える関係をつくる 一言でいえば「同期という仲間を増やしてもらうこと」がゴールです。上の世代を見て学ぶことも大切ですが、隣にいる同期の頑張りを知ることが、入社直後のこの時期にはいちばんの刺激になると考えました。 企画にあたって考えたこと コンテンツを設計するうえで、いくつか前提を置きました。 まず、懇親会を除いて 技術の詳細な話は扱わない 方針にしました。26卒はまだ研修中の会社も多く、業務の詳細を話しづらいケースがあると考えたためです。そのかわり、誰でも前向きに話せる「これからどう働いていきたいか」というテーマを共通の軸に据えました。 また、自己開示のハードルをできるだけ下げることを意識しました。初対面同士が大半になるので、いきなり話すのではなく、付箋に書き出してから共有する、という段取りにしています。 当日の内容 当日のタイムテーブルは以下のとおりです。 時刻 内容 19:15 開場 19:30 オープニング 19:35 会場スポンサーLT(弊社) 19:40 公募LT(3名) 20:00 グループワーク 20:20 クロージング 20:25 懇親会 21:30 終了 LT LTは2部構成です。まず、会場スポンサーである弊社から Hir_K がLTを行いました。続いて、公募で集まった3名の方に発表してもらいました。 LTのテーマは「今後の働き方や抱負」です。技術的な内容ではなく、これからどんなエンジニアになりたいか・何を大事にしたいか、といった切り口でお願いしました。 ニックネーム Sekibuuun3109さん HwaI12さん takkiiiiiiiiさん 抽象的なテーマなので集まるか少し不安もありましたが、ありがたいことに公募3枠とも埋まりました。どの発表も入社して数ヶ月のリアルな気持ちが反映されていて、聞いている側も自分ごととして受け止められる内容でした。 LTの様子 グループワーク グループワークのテーマは「今年度の抱負」です。流れは次のとおりで、各グループに分かれて実施しました。 付箋に自分の今年度の抱負を書き出す(5分) グループ内で共有する(10分) 抱負を書き出す際は、「できるようになりたいこと」「目標にしている先輩の特徴」などを例として提示しました。共有のパートでは、はじめに自己紹介(所属・職種・いま取り組んでいること)をしてから抱負を発表し、お互いに気になった点を質問し合ってもらう形にしました。 グループワークの様子 どのグループも活発に議論している様子が見られました。付箋を起点にしたことで、初対面同士でも会話のきっかけがつかみやすかったのではないかと思います。また、テーマを未来の話に寄せたことで、前向きな雰囲気だったのも良かったです。 懇親会 最後は立食形式の懇親会です。弊社が会場スポンサーとして、ケータリング、ドリンクを用意しました。 グループワークで一度話したことが会話のきっかけになっていて、グループの内外を問わず交流が広がっていました。26卒のコミュニティを作る動きも見られて良かったです。 運営してみて気づいたこと 私自身、初めての社外向けイベント運営だったので、終わってみるといくつか学びがありました。 テーマを「これからの話」に寄せたのは有効だった。 比較や反省ではなく抱負を軸にしたことで、入社直後でも話しやすい空気になりました。 付箋に書き出してから話すと、初対面でも話しやすい。 書き出す作業がワンクッションになり、会話のハードルが下がったと思います。 グループワークが懇親会の会話のネタづくりになった。 一度お互いのことを話しておくと、それがそのまま懇親会での会話のきっかけになります。懇親会を盛り上げるうえで、その前のグループワークが効いていたように感じました。 まとめ いろいろな会社・バックグラウンドの26卒エンジニアが集まってくれて、運営としても充実したイベントになりました。参加してくださったみなさん、ありがとうございました。 集合写真(公開許諾済み) 最後に 弊社では、ともに働く仲間を募集しています。 テックブログを読んで少しでも弊社に興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! https://corp.every.tv/recruits/engineer
— 実装、レビューからHarnessの整備、そしてHarnessを評価するEvalsへ 1. はじめに こんにちは、開発本部開発1部デリッシュキッチン開発部所属の西本( @daikon265 )です。 6月11日に開催されたCode w/ Claude: Extended Tokyoに参加してきました。参加して感じたのは、Claude Codeの活用が「どうコードを書かせるか」から、「Claudeが安全に働ける環境をどう作り、どう改善し続けるか」に移っていることでした。 これまでClaude Codeの活用では、 Harness を使ってClaudeをうまく制御する話が中心だったと思います。 一方で今回のカンファレンスでは、そのHarness自体をどう評価し、どう育てるかという視点も強く出ていました。 Anthropicのエンジニアとの1on1で、私は次のような質問をしました。 モデルが進化するにつれて、 今のHarnessはだんだん形骸化して、 むしろ邪魔になっていくのではないか? 返ってきた答えは印象的でした。 その直感は正しい。モデルが賢くなるにつれて、Harnessの重要性は下がっていく。Anthropic社内では、新しいモデルを定量評価し、その結果に基づいて、CLAUDE.mdを小さくするか、Skillsがノイズになっていないかを判断している。 Anthropicの中ではすでに、Evalsを使ってHarnessを評価し、必要なものを残し、不要になったものを削る運用が回っているということです。 本稿では、この考え方を抽象論に留めず、実際の開発フローに落とし込んで考えます。 大きくは、次の3つのフェーズで整理します。 1. 実装フェーズ 1-1. Plan:実装前に何を具体化するか 1-2. Implementation:サブエージェントにどう実装を分担させるか 2. レビュー・マージフェーズ 2-1. PRで何を見せるか 2-2. Claudeと人間でレビューをどう分担するか 3. 運用・改善フェーズ 3-1. マージ後に何を観察するか 3-2. 学びをどうHarnessに戻すか それぞれのフェーズで、カンファレンスで語られていた内容を紹介しつつ、自分たちの開発に取り入れるならどういう形になりそうかを整理していきます。 2. 実装フェーズ 実装フェーズは、さらに Plan と Implementation に分けて考えます。 ここでいうPlanは、実装前に仕様判断、実装判断、検証判断を具体化する工程です。自分も以前から、Claudeにいきなり実装させる前に、まず計画を作らせることはしていました。 ただ、その計画の立て方や粒度は曖昧になりがちです。 どこまでClaudeに質問させるのか。 どこまで仕様を固めるのか。 どの時点で検証条件を決めるのか。 どの情報を残しておけば、後続の実装やレビューで使えるのか。 今回のワークショップで分かりやすかったのは、まさにこの「Planをどう具体化するか」でした。 2-1. Plan:実装前に何を具体化するか 2-1-1. まずClaudeに質問させる 「How we Claude Code」ワークショップでは、エージェントが長時間自律稼働できるようになってきたことで、ミスのコストが上がっているという問題意識が語られていました。そこで紹介されていたのが、コードを書かせる前にClaudeへユーザーをインタビューさせ、曖昧さやエッジケースを引き出す方法です。 たとえば、次のような依頼は危険です。 友達用の割り勘アプリを作って この依頼だけでも、Claudeはそれらしいものを作れます。 しかし、保存方法はどうするのか、精算はアプリ内で行うのか、通貨は複数対応するのか、誰が誰にいくら払うべきかをどう最小化するのか、といった判断をClaudeが勝手に埋めることになります。 ワークショップで紹介されていたのは、次のようにClaudeに質問させる進め方です。 いくつか確信が持てない点があります。 まず私にインタビューして、仕様の曖昧なところを整理してください。 質問は一度に多くしすぎず、重要なものから聞いてください。 回答をもとに、実装前に合意すべき仕様をまとめてください。 デモでは、Claudeが用途、分割方法、保存方法、精算方法、通貨、共有方法などを質問し、回答に矛盾があれば追加で確認していました。その結果として、実装に入る前に詳細なspecが生成されていました。 ここで大事なのは、Claudeに質問させることで、実装前に判断が必要なことを表に出すことです。 2-1-2. 検証条件を先に決める Planで特に重要なのが、検証条件を先に決めることです。 検証に関するデモでは、実装前に Fixture、Invariant、Probe を決めることが強調されていました。 項目 意味 Fixture 検証時に再現する状態 Invariant 常に成り立っていてほしい性質 Probe 検証時に観測するポイント たとえばTo Doアプリなら、こうなります。 Fixture Invariant Probe 完了済みのTodoが1件ある 完了状態のデータと画面表示が一致している DOM上の状態属性、取り消し線の表示 非常に長いテキストのTodoがある レイアウトが崩れない 表示領域、折り返し、スクロール Todoが0件 空状態が表示される empty stateの文言、追加ボタンの表示 この考え方があると、Claudeも人間も「何をもって正しいと判断するのか」を共有できます。 2-1-3. Probeを実装上で観測できるようにする Fixture / Invariant / Probe を決めても、実装上でその状態を観測できなければ検証はしづらくなります。 このデモで特徴的だったのが、アプリの状態をDOMにスタンプする方法です。たとえばTodoが完了済みであれば、DOMに次のような属性を出します。 < li class = "todo-item todo-item--done" data -verified-done= "true" > oat milk </ li > 未完了であれば、 data-verified-done="false" になります。 ここで見たいのは、内部状態と画面表示の一致です。 data-verified-done="true" かつ 画面上も完了状態として表示されている Reactの内部状態を直接見に行く方法もありますが、実装依存になりやすく、検証のたびに扱いが面倒になります。そこで、検証したい状態をDOM属性として外に出しておくことで、Claudeや検証スクリプトが確認しやすい観測点を作ります。 つまりDOMスタンプは、Probeを成立させるための仕込みです。 これにより、内部状態としては完了済みなのに、画面上では完了済みに見えない、といったズレを検出しやすくなります。ワークショップでも、Reactのメモリ上ではTodoが完了済みなのに、画面に取り消し線が出ないケースを、この考え方で検出していました。 2-1-4. 自分たちのPlanでは何を残すか ここまでの内容を自分たちの開発に落とすなら、Planでは次の3つを残します。 ファイル 対応する考え方 書くこと spec.md インタビューで固めた仕様 作るもの、作らないもの、制約、未決事項 plan.md 実装の進め方 参照すべき既存実装、変更順序、実装方針 verification.md Fixture / Invariant / Probe / 観測点 再現する状態、守るべきルール、観測するポイント、コード上の観測点 verification.md には、単なる「テストする」で終わらせず、次の形で残します。 Fixture Invariant Probe 実装上の観測点(追加) 完了済みのTodoが1件ある 完了状態のデータと画面表示が一致している DOM上の状態属性、取り消し線の表示 data-verified-done 非常に長いテキストのTodoがある レイアウトが崩れない 表示領域、折り返し、スクロール Storybook / Playwright snapshot Todoが0件 空状態が表示される empty stateの文言、追加ボタンの表示 empty state selector Planのゴールは、Claudeが実装に入る前に、仕様判断・実装判断・検証判断を分離して、後から追える形にすることです。ドキュメントは、その判断を残すための手段として扱います。 2-1-5. Bunの実装ガイドを一般化する 「Rewriting Bun in Rust」では、ClaudeにBunをZigからRustへ書き直させ、約11日で巨大なPRをマージした事例が紹介されていました。 この事例で参考になるのは、実装に入る前に、複数のClaudeが同じ判断基準で動けるように、事前にルールを作っていた点です。 Bunでは、最初にClaudeと約3時間対話し、Zigの書き方をRustではどう表現するかを整理しました。その結果をポーティングガイドとしてまとめ、以後のClaudeインスタンスが必ず参照する資料にしています。さらに、Bunコードベース内の構造体やフィールドを調査し、ライフタイムに関する情報も共通資料として使っていました。 「ZigのパターンをRustにマッピングする」と言うと少し分かりにくいですが、要するにこれは 変換ルール集 です。 Bunの例 - Zigのこの書き方は、Rustではこう書く - メモリ管理はこの方針にする - crate分割はこの考え方にする - ライフタイムが複雑な箇所はこの資料を参照する これを自分たちの開発に置き換えると、次のようになります。 自分たちの例 - 古いAPI呼び出しは、新しいclientに置き換える - 独自UIは、design system componentに寄せる - 古いhooksは、新しいhooksに統一する - DB更新は、このmigration方針に従う - エラーハンドリングは、この形式に揃える 普段の開発では、これを implementation-guide.md として扱えばよさそうです。 変更の種類 作るもの 小さな修正 spec.md / plan.md / verification.md 複数箇所に同じ方針を適用する変更 implementation-guide.md を追加 複数のClaudeを並列に動かす変更 implementation-guide.md をほぼ必須にする 複数のClaudeを並列に動かす場合、全員が同じ判断基準を持っていないと、ファイルごとに実装スタイルがばらつきます。 implementation-guide.md は、そのばらつきを抑えるための共通ルールです。 コラム:HTMLプロトタイプはプロトタイプ開発で使える ワークショップでは、Planができた後にHTMLプロトタイプを作る方法も紹介されていました。 背景にあるのは、Anthropicの “Demos, not memos” という考え方です。Markdownの仕様書はClaudeには読みやすい一方で、人間が直感的に判断するには弱い。人間は、文字だけで仕様を読むより、触れるものを見たほうが「ここが違う」「この方向性は好き」と反応しやすい。そこで、ClaudeにHTMLで軽量なプロトタイプを作らせ、触りながら方向性を固める、という流れです。 自分でこれを取り入れるなら次のような感じがいいと考えています。 初期探索 - エンジニアがClaudeとHTMLプロトタイプを作る - 最低限触れる状態にして公開する - ユーザーや市場の反応を見る ブラッシュアップ - 反応が良ければ、Figmaでデザイナーに詳細を詰めてもらう - デザインシステムに載せる - 本格的に磨く この進め方は、今後増えていくと思います。 2-2. Implementation:サブエージェントに実装を分担し、失敗を再配布する Planができたら、実装に入ります。 ある程度の規模のタスクでは、複数のサブエージェントに分担させて並列に進めることが増えてきていると思います。 ここで参考になるのが、Bunの実装方法です。 Bunの事例では、並列実行時の制御も重要でした。重い操作や競合しやすい操作を各エージェントに任せきりにしないようにしていました。複数のClaudeがそれぞれbuildやtest、git操作を実行すると、同じ確認を重複して行ったり、同じworktree内で互いに干渉したり、全体の進行を遅くしてしまうからです。 そのため、build / test / git操作のような重い処理は、メイン側のワークフローで管理します。サブエージェントは実装や修正に集中し、メインエージェントが結果を集約して、失敗ログを分類し、担当するサブエージェントへ再配布します。 このループでは、役割を大きく3つに分けます。 役割 担うこと Main Agent spec.md / plan.md / verification.md を読み、作業を分割する。実装結果を集約し、build / test / lint / typecheck をまとめて実行する。失敗ログを分類し、必要なサブエージェントへ再配布する。 Sub Agents 割り当てられた範囲を実装する。必要に応じて、実装担当とレビュー担当を分ける。レビュー担当は diff や spec を読み、仕様違反、バグ、テスト不足を探す。 Human 仕様判断、設計判断、本番リスクを確認する。特にクリティカルな変更では、AIの検証結果だけでなくコードや動作も見る。 各サブエージェントに任せる範囲は絞ります。各サブエージェントがそれぞれ build や test を走らせ、個別に判断し始めると、重い処理が重複し、失敗ログの扱いもばらつきます。 そのため、検証と再配布は Main Agent 側に集約します。Main Agent が失敗ログを見て、次に誰へ戻すかを判断します。 失敗の種類 戻し先 build failure backend 担当の Sub Agent UI regression frontend 担当の Sub Agent test failure test 担当、または該当実装担当の Sub Agent migration error DB 担当の Sub Agent spec とのズレ 実装担当の Sub Agent 横断的な設計ミス Main Agent が plan を見直す このループの本質は、 検証結果を次のタスク分配の入力にする ことです。失敗を検出したら、原因ごとに担当へ戻します。さらに同じ失敗が繰り返されるなら、プロンプト、ルール、verification、implementation-guide 側に戻して、生成プロセス自体を直します。 3. レビュー・マージフェーズ レビュー・マージフェーズで重要なのは、検証結果を人間が判断しやすい形で見せることです。 3-1. PRでは検証結果を証拠として見せる PRには、変更概要や影響範囲だけでなく、動作確認や検証結果を載せます。 特にUIやワークフローが関わる変更では、スクリーンショットや動画が有効です。実際にどう動いたか、どの状態で確認したかが見えると、レビュアーは判断しやすくなります。 PRでは、最低限次の情報を揃えます。 PRに載せるもの 目的 変更概要 何を変えたかを短く把握する 影響範囲 どの画面・機能・APIに影響するかを見る 検証結果 どのFixture / Invariant / Probeを確認したかを見る スクリーンショット・動画 実際の動作やUIの崩れがないことを見る レビュアーに見てほしい箇所 判断が必要な点を明確にする 3-2. 検証ダッシュボードで状態と結果を見える化する 検証ダッシュボードのデモで特徴的だったのは、検証用のダッシュボードを作っていた点です。 通常のテストログでは、どのテストが通ったかは分かります。ただ、UIやワークフローの検証では、それだけだと「どの状態で、何を確認したのか」が見えにくい。そこで、検証ダッシュボード上に、どの画面・コンポーネントを、どのfixtureで、どのinvariantに対して検証したのかを表示します。 たとえば、次のような情報が見える状態です。 見えるもの 例 対象 画面、コンポーネント、ワークフロー Fixture 空状態、長いテキスト、大量データ、権限なしユーザー Invariant レイアウトが崩れない、状態と表示が一致する、権限外操作ができない Probe DOM属性、表示テキスト、APIレスポンス、エラー表示 結果 pass / fail、必要に応じてスクリーンショットや録画 これは、テストを増やす話というより、 検証結果をレビューできる形に変換する 話です。Claudeも人間も、同じ証拠を見ながら「何をもって正しいと判断したのか」を確認できます。 3-3. レビューはClaudeと人間で見る観点を分ける レビューでは、Claudeと人間の役割を分けます。 見る人 主に見ること Claude specとのズレ、テスト不足、エッジケース漏れ、権限・セキュリティの問題、既存挙動の破壊 Human 実際の動作、仕様解釈、設計判断、ユーザー影響、本番リスク Anthropicのエンジニアとの1on1では、社内ではすべてのPRに対してClaudeによる自動コードレビューが走り、その後に人間のレビュアーがハイレベルな観点で確認すると聞きました。 チーム内の運用イメージとしては、Claudeが細かいロジックやspecとのズレを拾い、人間が動作、設計、リスクを判断する形です。クリティカルな変更では、人間もコードを読みます。 4. 運用・改善フェーズ マージ後も観察と改善は続きます。 むしろ、マージ後に何を観察し、何をHarnessに戻すかが、Claude Codeをチーム開発に組み込むうえで重要になります。 4-1. マージ後も検証を増やす 「Rewriting Bun in Rust」では、テストスイートを通してマージした後も、品質強化が続いていました。マージ後にも別の角度から壊れ方を探す検証を増やしていたことが印象的でした。 たとえば、次のような検証です。 - セキュリティ上の問題を機械的に探す - ランダムな入力を大量に与えてクラッシュを探す - メモリリークのような実行時の問題を探す - テストでは通らなかったコード経路をさらに踏みに行く 講演で語られていた思想は、「動くことを信用したくない。動くことの実証的な根拠を持ちたい」というものでした。 マージ後に検証を増やすことは、次の開発ループを強くするための投資です。 4-2. EvalsでHarnessを育てる Evalsは、実装・レビュー・運用を回した後に、Harnessを改善するための仕組みとして効いてきます。 「Evals for taste: Hill-climbing a slide-generation agent」では、Evalsは「成功とは何か」を定義し、エージェントが改善しているかを確認するためのものだと説明されていました。最適化対象はプロンプトだけでなく、モデル選択、ツール設計、エージェントのアーキテクチャにも広がる、という話もありました。 「Tool, skill, or subagent? Decomposing an agent that outgrew its prompt」でも、Evalsを改善の絶対的な指針にすることが最大のメッセージとして語られていました。評価結果が上がることを確認できたからこそ、ツール、Skills、サブエージェント構成を改善できたという話です。 冒頭で触れた1on1の話に戻ると、モデルが賢くなれば、以前は必要だった細かい指示が不要になる。場合によっては、古いCLAUDE.mdやSkillsがノイズになる。だからこそ、Anthropic社内では新しいモデルを定量評価し、その結果に基づいて、CLAUDE.mdを小さくするか、Skillsがノイズになっていないかを判断しているそうです。 Harnessは作ったあとも、Evalsによって評価され、必要なら削られる。つまり、EvalsはClaudeの出力を採点しながら、Harnessを育てるための仕組みでもある。 自分たちも、Evalsによって「これはまだ効いているか」「もうノイズになっていないか」を見ていく必要があります。 4-3. 失敗をどこに戻すか マージ後に観察するものは、次のHarness更新の材料として見ます。 観察するもの Harnessに戻す先 本番で出たバグ eval / verification 繰り返し出るレビュー指摘 review skill / checklist 仕様の取り違え specの作り方 テスト漏れ verifier / contract test 不要になった指示 CLAUDE.md / Skill の削除候補 Skillsは、関連するタスクのときだけロードされる手順書として使うのが本質です。「Tool, skill, or subagent? Decomposing an agent that outgrew its prompt」でも、長いプロンプトをオンデマンドのSkillsに分解し、必要なときだけコンテキストをロードする考え方が紹介されていました。 Harnessを育てるほど、余計なものを削る視点も必要になります。 増やすもの 削るもの tests / evals / verifiers / CI / monitoring 古いCLAUDE.mdの指示 / ノイズになったSkill / 自明すぎるルール 「Evals for taste: Hill-climbing a slide-generation agent」では、evalスコアが飽和したらgrader自体を見直すべきだという話もありました。見た目は明らかに改善しているのにスコアが上がらないなら、評価側の解像度が足りない可能性があります。その場合は、graderの評価基準を具体化する価値があります。 Claudeの出力とあわせて、評価そのものも育てる。そのループがあると、Harnessの改善が感覚頼みから評価駆動に変わります。 5. 自分たちの開発に落とすなら ここまでの内容を実務に落とすなら、次の4段階で始めるのがよさそうです。 フェーズ やること 残すもの Plan 仕様、実装方針、検証条件を分けて整理する spec.md / plan.md / verification.md Implementation Main Agent が作業を分割し、Sub Agents が実装する。検証結果は Main Agent が分類して再配布する 失敗ログ、修正タスク Review / Merge PRでは検証結果や動作の証拠を見せる。Claudeは詳細、人間は仕様・設計・リスクを見る 検証ログ、スクリーンショット、レビュー結果 Post-merge 本番やレビューで出た失敗を、その場限りにせずHarnessへ戻す eval、verification、skill、checklist このフローでは、判断基準、検証可能性、レビュー可能な証拠、Harnessへのフィードバックを揃えることが大事です。 anthropic-dev-loop この流れは再利用できるように、Skillとしても整理しました。Claude CodeやCodexで同じ流れを使いたいときは、このSkillを読み込ませることで、Plan、実装分割、検証、レビュー、Harness更新までを一連の手順として扱えます。 https://github.com/nishimoto265/anthropic-dev-loop 6. おわりに 今回は、Code w/ Claude Tokyo / Extended Tokyoで紹介されていたAnthropicの開発手法を、自分たちの開発フローに落とし込むならどうなるのか、私なりの知見を交えながら整理しました。 Code w/ Claude Tokyo / Extended Tokyoの中で特に印象的だったのは、やはり Evals です。 私自身、評価関数があり、それに基づいてエージェントや開発フローが将来的には自動で改善されていくのではないかと考えていました。その考え方が Evals に現れており、Harnessを評価し、必要なものを残し、不要なものを削っていく手法は、今後さらに重要になっていくと感じています。 今後もAnthropicのエンジニアの手法を参考にしながら、Claude Codeを前提にした開発フローを自分たちなりに改善していきたいと考えています。
はじめに こんにちは。 開発本部 開発1部 デリッシュリサーチでデータエンジニアをしている吉田です。 今回は、Databricksのマネージドなデータ取り込み機能であるLakeflow Connectを使って、複数の広告プラットフォームからデータを取り込んだ事例を紹介します。 注意点 本記事で扱うLakeflow Connectのマネージドコネクタは、執筆時点でいずれもベータ版の機能です。 利用している機能とその結果は執筆時点のものです。 また、 リリースタイプの一覧 のとおり、ベータ版は本番環境での利用が推奨されておらず、仕様変更の可能性がある点にご注意ください。 背景 広告運用の分析には、各広告プラットフォームのデータ(キャンペーンやインサイトなど)が必須です。 これらをデータ基盤に取り込むには、プラットフォームごとに取り込みJobを作成する必要があります。 しかし、取り込みJobの作成には以下のような辛さがあります。 API仕様の調査、実装 認証情報の管理 APIのアップデート追従 特にAPIのアップデートに合わせた取り込みJobの追従などは都度工数が発生するため、運用における負担となります。 そこで、取り込みをマネージドに任せられるLakeflow Connectのマネージドコネクタを採用しました。 Lakeflow Connectとは Lakeflow Connectは、Databricksの統合データエンジニアリング機能群Lakeflowの取り込みコンポーネントです。 SaaSやデータベース向けのマネージドコネクタなどを提供し、取り込み処理をサーバーレスで実行します。取り込み先のテーブルはUnity Catalogで一元管理されます。 マネージド SaaS コネクタ コネクタには2種類あります。 マネージドコネクタ Salesforceなどのアプリやデータベース向け Lakeflow Spark Declarative Pipelinesの上に構築され、ソース固有の認証・CDC(増分取り込み)・スキーマ展開・自動リトライを提供する 標準コネクタ クラウドストレージやメッセージバス向けのカスタマイズ可能なコネクタ マネージドコネクタは、様々なSaaSに対応するコネクタが用意されており、今回はGoogle Ads、Meta Ads、TikTok Adsのコネクタを利用しました。 アーキテクチャ SaaS系コネクタの取り込みは、以下のシンプルな構成です。 Connection 各プラットフォームへの認証情報を保持するUnity Catalogのオブジェクト Ingestion pipeline 接続を通じてデータを取り込み、ストリーミングテーブルへ書き込む Destination Unity Catalog上のストリーミングテーブル パイプラインの作成 パイプラインはUI・Databricks Asset Bundles・REST APIなどから作成できますが、今回はPipelines API( /api/2.0/pipelines )をノートブックから呼び出して作成しました。 パイプライン定義を用意することで、容易に取り込みパイプラインを作成することができます。 パイプライン定義の例(Meta Ads) 以下のJsonはMeta Adsのパイプライン定義の例です。 { " name ": " <pipeline-name> ", " catalog ": " <catalog-name-for-event-logs> ", " schema ": " <schema-name-for-event-logs> ", " ingestion_definition ": { " connection_name ": " <connection-name> ", " objects ": [ { " table ": { " source_schema ": " act_<account-id> ", " source_table ": " campaigns ", -- キャンペーンの取り込み " destination_catalog ": " <save-to-catalog> ", " destination_schema ": " <save-to-schema> ", " table_configuration ": { " scd_type ": " SCD_TYPE_1 " } } } , { " table ": { " source_schema ": " act_<account-id> ", " source_table ": " ad_insights ", -- インサイトの取り込み " destination_catalog ": " <save-to-catalog> ", " destination_schema ": " <save-to-schema> ", " table_configuration ": { " scd_type ": " SCD_TYPE_1 ", " metamarketing_parameters ": { " level ": " campaign ", " start_date ": " 2026-01-01 ", " time_increment ": " 1 " } } } } ] } } table_configuration では取り込み時の挙動を指定します。 scd_type で履歴の保持方式( SCD_TYPE_1 は最新の状態のみを保持)を、 metamarketing_parameters でレポートの集計レベル( level )や粒度( time_increment )などコネクタ固有のパラメータを指定します。 UI上での管理 作成したLakeflow Connectのパイプラインは、Jobs & Pipelinesから確認できます。 取り込みは指定したテーブル単位で行われ、失敗したテーブルや特定のテーブルのみを再実行することも可能です。 現時点での使用感 現在、3つのコネクタを利用していますが、最終的にMeta Adsのコネクタは使用を停止しました。 Meta Adsコネクタでは、データ量の多いアカウントでは取り込みが安定しないケースがありました。 私たちの環境では主に ad_insights 、 ad_sets の取り込みで、以下のエラーが発生しました。 The Meta API failed since the amount of data requested exceeds the limit the Meta servers can process. 取得するカラムや期間を絞る対応を加えましたが解決せず、最終的には自前実装に切り替える選択をしました。 Tokenの更新の実装や、アカウントや取得したい項目が増えるたびの対応など、マネージドに任せることができない辛さがあり、コネクタの今後のアップデートに期待したいです。 まとめ Lakeflow Connectにより、Google Ads / TikTok Adsの広告データ取り込みは、API実装なしにパイプライン定義だけで作成できました。 複数アカウント・複数テーブルの定義をspecとしてコードで管理でき、増分取り込みも設定だけで実現できています。 一方でMeta Adsのように、データ量やコネクタの制約から現時点ではマネージドに任せきれず、自前実装で補ったケースもありました。 それでも、取り込みの作り込みの大部分から解放され、データ活用そのものに集中できるようになったメリットは大きいと感じています。ベータ版ゆえの制約は今後のアップデートでの改善に期待したいです。
目次 はじめに 背景と課題 方式の検討 全体アーキテクチャ 認証・認可フロー クライアントが認可サーバーを発見するまで トークンの取得 ゲートウェイでの JWT 検証 ゲートウェイの実装ポイント 設定ファイルによるバックエンド管理 検証済みユーザー情報の伝搬となりすまし防止 起動時の OIDC Discovery による fail-fast モック認可サーバーによるローカル開発 インフラ構成 社内での活用事例 今後の課題 おわりに はじめに こんにちは。 開発本部開発3部トモニテ開発部所属の庄司( @ktanonymous )です。 エブリーの開発組織では、日常業務から離れて新しい技術やアイデアに挑戦する「挑戦week」という取り組みを定期的に開催しています。 先日行われた挑戦weekの中で、私たちのチームは全社共通で利用できるリモート MCP ( Model Context Protocol ) サーバー向けの認証・認可ゲートウェイを設計・実装しました。 本記事では、その全体アーキテクチャや認証・認可フロー、実装のポイントなどを紹介したいと思います。 ※ 挑戦weekの詳細については過去の記事で紹介していますので、興味のある方は以下をご覧ください。 tech.every.tv 背景と課題 弊社では非エンジニア職にも Claude が配布され、職種を問わず AI 活用が盛んになってきています。 業務の中で Claude を利用するにあたり、社内情報の取得のために社内 API へ接続したいという需要が高まってきていると感じました。 今後、その手段として各チームがリモート MCP サーバーを立てるケースが増えていくと考えられます。 リモート MCP サーバーはインターネット経由でアクセスされるため、社外の人間が利用できないように認証・認可を考慮する必要がありました。 一方で、MCP サーバーを実装するたびに各チームが認証・認可を設計・実装するのはコスト面でも効率面でも避けたいものです。 そこで、共通で利用できる認証・認可基盤を作り、各チームの MCP サーバーはそれぞれのツールの実装に専念できるようにすることを目指しました。 今回検討した要件は以下の通りです。 Claude (Web / Desktop / Code) などの各種 MCP クライアントから、社内のリモート MCP サーバーのツールを利用できる 弊社の Google Workspace アカウントでログイン済みのユーザーのみがツールを利用できる 各チームが新しく MCP サーバーを追加するとき、認証・認可の実装を不要にする 方式の検討 認証・認可の共通化にあたり、大きく分けて以下 3 つのアプローチを検討しました。 ライブラリ方式: 認証・認可処理を共通ライブラリとして実装し、各 MCP サーバーに組み込む サーバー方式: 認可サーバーを立て、各 MCP サーバーがトークンを問い合わせることで検証する ゲートウェイ方式: ゲートウェイがリクエストを一括で受けて認証・認可を行い、検証済みのリクエストだけを後段の MCP サーバーへ転送する 3 つの内、ライブラリ方式は MCP サーバーの実装言語ごとにライブラリを用意する必要があり、 サーバー方式は、責務こそ分離できるものの、各 MCP サーバー側に認可サーバーへトークンを問い合わせる実装が必要になります。 一方、ゲートウェイ方式であれば、認証・認可をゲートウェイに一元化でき、後段の MCP サーバーは実装言語を問わず認証・認可も意識せずに済みます。 近年の MCP ゲートウェイ製品の設計とも方向性が近いことから、今回はゲートウェイ方式を採用しました。 なお、 mcp-context-forge や agentgateway といった既存の OSS MCP ゲートウェイも調査しましたが、 今回の要件に対して機能・ボリュームが大きすぎたため採用を見送り、必要最小限のゲートウェイを実装することにしました。 全体アーキテクチャ 全体のアーキテクチャは以下の通りです。 アーキテクチャ概要 今回実装したゲートウェイは大きく 4 つの要素から構成されています。 MCP クライアント: Claude Code / Claude Desktop など。OAuth 2.0 のパブリッククライアントの立ち位置 ゲートウェイ: ECS Fargate 上で稼働する Go (Echo) 製のリバースプロキシ。JWT の検証と後段 MCP サーバーへのルーティングを担う。クライアントから直接見える唯一の MCP サーバー。 Amazon Cognito: 認可サーバー兼 IdP。Google Workspace アカウントへのフェデレーションを行い、アクセストークン (JWT) を発行する MCP サーバー群: 各チームが実装するリモート MCP サーバー。private subnet に配置し、ゲートウェイ経由でのみアクセス可能であり、クライアントから直接的には見えていない 意識した点として、JWT を検証するのはゲートウェイだけという点があります。 後段の MCP サーバーは JWT 検証を実装せず、ゲートウェイが注入する検証済みのユーザー情報 (後述の X-Auth-* ヘッダー) を信頼します。 その前提を成立させるため、MCP サーバーはインターネットに直接公開せず、ゲートウェイからのみ到達できるネットワーク構成としました。 認証・認可フロー MCP の認可は MCP Authorization 仕様 で定義されており、 OAuth 2.1 をベースに、PRM (RFC 9728) などの仕様を組み合わせて構成されています。 今回のゲートウェイもこの仕様に沿って実装しています。 具体的には以下のようなフローとなっています。 認証・認可フロー クライアントが認可サーバーを発見するまで MCP クライアントには MCP サーバーの接続先を登録するため、認可サーバーがどこにあるかを予め知ることはできません。 クライアントがトークンなしでアクセスすると、ゲートウェイは 401 Unauthorized を返し、 WWW-Authenticate ヘッダーの resource_metadata パラメータで Protected Resource Metadata (PRM) の URL を通知します。 HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://mcp-gateway.example.com/.well-known/oauth-protected-resource" PRM は、保護されたリソース (今回はゲートウェイ) が「どの認可サーバーに保護されているか」「どのスコープをサポートするか」といった自身のメタデータを公開するための仕様です。 クライアントはこのメタデータを参照することで、トークンの取得先を機械的に発見できます。 クライアントがこの URL にアクセスすると、PRM の仕様で定義された以下のような JSON が返ります。 { " resource ": " https://mcp-gateway.example.com/ ", " authorization_servers ": [ " https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id> " ] , " scopes_supported ": [ " openid ", " email ", " profile " ] , " bearer_methods_supported ": [ " header " ] } クライアントは authorization_servers から認可サーバー (Cognito) を発見し、 さらに Cognito の Authorization Server Metadata ( /.well-known/openid-configuration ) を取得して、 認可エンドポイントやトークンエンドポイントを把握します。 Authorization Server Metadata は、認可サーバーが「どこで認可リクエストやトークン発行を受け付けるか」「どの機能をサポートするか」といった自身の設定情報を公開するための仕様です。 Cognito の場合、以下のような JSON が返ります (主要なフィールドのみ抜粋)。 { " issuer ": " https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id> ", " authorization_endpoint ": " https://<domain>.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize ", " token_endpoint ": " https://<domain>.auth.ap-northeast-1.amazoncognito.com/oauth2/token ", " jwks_uri ": " https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>/.well-known/jwks.json ", " scopes_supported ": [ " openid ", " email ", " phone ", " profile " ] } この 2 段階のメタデータ取得により、クライアント側に認可サーバーの情報を事前設定することなく、OAuth フローを開始することができます。 トークンの取得 認可サーバーの発見後は、通常の OAuth 2.0 Authorization Code フロー (PKCE 付き) です。 クライアントがブラウザを開いて Cognito の Hosted UI に遷移し、Cognito は Google へフェデレーションします。 ユーザーが自社の Google Workspace アカウントでログインすると、クライアントはアクセストークン (JWT) を取得します。 Cognito 側の設定のポイントは以下の通りです。 パブリッククライアント + PKCE 必須: Claude などの MCP クライアントは利用者の手元で動くため、クライアントシークレットを保持できません。そこでパブリッククライアントとして登録し、PKCE を利用するようにします。 アプリクライアントの事前登録: Cognito では接続元のアプリケーションを「アプリクライアント」として登録します。MCP クライアントごとにユーザープールを登録し、対応するコールバック URL (認可コードの返却先) を設定することで事前に利用するクライアントを登録します。 なお、MCP Authorization 仕様では、クライアントの登録方法として事前登録のほかに、 Client ID Metadata Documents (URL を client_id として扱い、認可サーバーがその URL からクライアント情報を取得する方式) や Dynamic Client Registration (RFC 7591) による動的登録も定義されています。 今回は、Cognito がこれらに対応していないことと、社内利用ではクライアントの種類が限られる (基本的に Claude 系のみ) ことから、仕様でも正規の選択肢とされている事前登録制を採用しました。 また、Cognito のアクセストークンには標準では email クレームが含まれないため、 Pre Token Generation Lambda トリガー を利用して、トークン生成時に email クレームを注入しています。 これにより、後段の MCP サーバーが「誰からのリクエストか」をメールアドレスで判定できるようになります。 ゲートウェイでの JWT 検証 ゲートウェイは、リクエストごとに JWT を検証します。 署名検証には github.com/coreos/go-oidc (v3.18.0) を利用し、 Cognito の JWKS (JSON Web Key Set) は初回取得後にキャッシュされます。 検証項目は以下の通りで、署名・有効期限といった基本的な検証に加えて、クレームベースのチェックを重ねています。 検証項目 内容 失敗時 署名 / iss / exp JWKS による署名検証、発行者・有効期限の確認 401 token_use "access" であること (ID トークンの誤用防止) 401 client_id 事前登録したアプリクライアントの許可リストに含まれること 403 email ドメイン エブリードメインであること 403 scope 必須スコープを満たすこと (設定時のみ) 403 token_use の検証は Cognito 固有のポイントです。 Cognito は ID トークン用とアクセストークン用にそれぞれ別の署名鍵を持ちますが 1 、両方の公開鍵が同一の JWKS で公開されます。 そのため、JWKS 内のいずれかの鍵で署名が検証できることだけを条件にすると、ID トークンも検証を通過してしまいます。 クライアントが誤って ID トークンを Authorization ヘッダーに載せてきた場合に備えて、 token_use クレームが "access" であることを確認しています。 なお、検証項目に aud (audience) クレームは含まれていません。 MCP Authorization 仕様ではリソースサーバーによるトークンの audience 検証が求められていますが、 Cognito のユーザープールが発行するアクセストークンには aud クレームが含まれず、代わりに client_id クレームが含まれます。 そのため、今回の実装では client_id の許可リスト検証によって、トークンが事前登録済みのクライアントに発行されたものであることを確認する形をとっています。 ゲートウェイの実装ポイント ゲートウェイ本体は Go (1.26.3) + Echo (v4.15.2) で実装しました。 ここでは設計上のポイントについて触れます。 設定ファイルによるバックエンド管理 ゲートウェイがバイパスする MCP サーバー (バックエンド) は YAML で宣言的に管理しています。 backends : - name : server-a url : http://server-a.internal:8081 - name : server-b url : http://server-b.internal:8082 ゲートウェイは起動時にこのファイルを読み込み、 /<name>/mcp というパスを各バックエンドの /mcp にマッピングします。 たとえば POST /server-a/mcp へのリクエストは http://server-a.internal:8081/mcp に転送されます。 プロキシ部分は Go 標準ライブラリのリバースプロキシをベースに、以下のように実装しています。 標準実装は転送先のホストを書き換えるだけでパスはそのまま転送するため、転送直前に呼ばれるリクエストの書き換え処理を拡張して、パスプレフィックスの除去を加えています。 func newBackendProxy(targetURL, stripPrefix string ) (*httputil.ReverseProxy, error ) { u, err := url.Parse(targetURL) if err != nil { return nil , fmt.Errorf( "parse %q: %w" , targetURL, err) } rp := httputil.NewSingleHostReverseProxy(u) originalDirector := rp.Director rp.Director = func (req *http.Request) { // 標準の書き換え処理 (転送先を u に向ける) を実行した上で、 // ゲートウェイ側のパスプレフィックスを除去 (/server-a/mcp → /mcp) originalDirector(req) req.Host = u.Host if stripPrefix != "" { req.URL.Path = strings.TrimPrefix(req.URL.Path, stripPrefix) if req.URL.Path == "" { req.URL.Path = "/" } } } // バックエンドに到達できない場合は 502 を返す rp.ErrorHandler = func (w http.ResponseWriter, r *http.Request, err error ) { log.Printf( "[proxy] upstream error %s %s: %v" , r.Method, r.URL.Path, err) http.Error(w, "bad gateway" , http.StatusBadGateway) } return rp, nil } 新しい MCP サーバーを追加したいチームは、サーバーをデプロイしてこの YAML に 1 エントリ追記するだけで、 認証・認可付きのリモート MCP サーバーを公開できます。 検証済みユーザー情報の伝搬となりすまし防止 ゲートウェイは JWT の検証後、 Authorization ヘッダーを除去し、検証済みのクレームを X-Auth-* ヘッダーとしてバックエンドへのリクエストに注入します。 X-Auth-Sub : ユーザー識別子 (sub クレーム) X-Auth-Email : メールアドレス (Pre Token Generation Lambda で注入した email クレーム) X-Auth-Client-Id : OAuth クライアント ID X-Auth-Scope : 許可されたスコープ一覧 このとき重要なのが、クライアントから送られてきた X-Auth-* ヘッダーを必ず削除してから再注入することです。 // 受信した Authorization / X-Auth-* を全て削除(なりすまし防止) stripIncomingAuthHeaders(req.Header) // 検証済みクレームから X-Auth-* を再注入 req.Header.Set( "X-Auth-Sub" , claims.Sub) req.Header.Set( "X-Auth-Email" , claims.Email) req.Header.Set( "X-Auth-Client-Id" , claims.ClientID) req.Header.Set( "X-Auth-Scope" , strings.Join(claims.Scopes, " " )) これにより、クライアントが偽の X-Auth-Sub を付けてリクエストすることで他人になりすますのを防ぎます。 ゲートウェイがクレームヘッダーの削除と再注入を強制するため、バックエンド側は常にゲートウェイで検証済みの X-Auth-* ヘッダーの利用を保証できます。 加えて、バックエンドにはゲートウェイ経由でしか到達できないようにネットワークを構成しています (後述)。 X-Auth-* を信頼できるのは「ゲートウェイを必ず通る」ことが前提なので、アプリケーション実装とネットワーク構成をセットで設計する必要があります。 起動時の OIDC Discovery による fail-fast ゲートウェイは起動時に Cognito へ OIDC Discovery ( /.well-known/openid-configuration の取得) を行います。 issuer の設定ミスなどがあればこの時点で起動エラーになるため、リクエストを受けてから認証エラーが多発する、という事態を防げます。 モック認可サーバーによるローカル開発 ローカル開発用に、OIDC Discovery・JWKS・トークン発行だけを備えた最小限のモック認可サーバーを用意しました。 ゲートウェイから見ると issuer の URL が違うだけなので、実際の Cognito と同じコードパスで JWT 検証まで通しでテストできます。 docker compose でゲートウェイ・サンプルバックエンド・モック認可サーバーを一括起動できるようにしており、AWS 環境なしで認証フロー全体を確認できます。 インフラ構成 インフラは Terraform で管理しています。要点は以下の通りです。 ECS Fargate: ゲートウェイは private subnet に配置し、外部への通信は NAT Gateway 経由 ALB: TLS を終端し、ゲートウェイへ転送。セキュリティグループでゲートウェイへの入力は ALB からのみに制限 Cognito User Pool: Google フェデレーション、アプリクライアント、Resource Server、Pre Token Generation Lambda を Terraform で定義 デプロイ: GitHub Actions から OIDC でロールを引き受けて ECR push と ECS デプロイを実行 MCP クライアント (Claude など) は固定 IP を持たないため、ALB は HTTPS (443) を全公開とし、アクセス制御は JWT 検証と WAF に任せます。 バックエンドの MCP サーバーは private subnet 内でゲートウェイからのみ到達できるようにし、ゲートウェイを経由しない場合はネットワーク的に到達不可能にしています。 社内での活用事例 実際に社内で Redash を操作するリモート MCP サーバーをゲートウェイを利用して社内向けにリリースされました。 Claude とのチャットだけで、クエリの実行やダッシュボードの操作といった Redash 上のほとんどの操作ができます。 ローカル版の Redash MCP サーバーについて、以前の挑戦weekの記事で紹介しています。 tech.every.tv Redash MCP サーバーをリモート化し、認証・認可をゲートウェイに集約することで、個人ごとに API Key などの配布や登録が不要になり、 ビジネスサイドのメンバーであっても、Redash アカウントを持っていれば Google アカウントにログインするだけで API 経由で Redash を操作できるようになりました。 Redash MCP を社内に公開しました 今後の課題 短期間での構築だったため、以下のような課題が残っています。 きめ細かなポリシー管理: 現状は「自社ドメインの社員であること」の確認までで、厳密に権限管理をする場合には、所属チームなどの属性に応じて利用できる MCP サーバーやリソースを制限する仕組みが必要になります。 VPC 間の接続: 各チームの MCP サーバーは別の VPC や AWS アカウントで稼働するケースもあります。そのため、今後さらに MCP サーバーの利用を展開していくためには、VPC Peering などによる VPC 間接続を検討する必要があります。 おわりに 本記事では、全社共通のリモート MCP サーバー向け認証・認可ゲートウェイの設計と実装を紹介しました。 ゲートウェイ方式を採用したことで、認証・認可の実装をゲートウェイに一元化でき、 各チームは MCP サーバーのツール実装に専念して、設定ファイルへの追記だけで認証付きのリモート MCP サーバーを公開できるようになりました。 MCP の認可まわりは仕様の整備が活発に進んでいる領域なので、今後も動向を追いながら基盤を育てていきたいと思います。 この記事が、社内での AI 活用の中で同じような課題感を持っている方の参考になれば幸いです。 最後まで読んでいただき、ありがとうございました。 Understanding user pool JSON web tokens (JWTs) - Amazon Cognito (2026年6月11日閲覧) 。「Amazon Cognito generates two pairs of RSA cryptographic keys for each user pool. One private key signs access tokens, and the other signs ID tokens.」と記載されています。 ↩
はじめに こんにちは、トモニテ開発部の吉田です。 今回は、私たちの部署で実施した「AI days」という取り組みについてレポートします。2日間かけて、普段の開発業務にAIを組み込むための土台を集中的につくる社内イベントです。 そもそものきっかけは、私が「トモニテでのAI活用をもっと加速させていきたい」と声を上げたことでした。私は役職者ではなく一開発メンバーですが、その思いを伝えたところ、具体的な形として今回のAI days開催に至りました。最初から「AI daysをやりたい」と考えていたわけではなく、「なんとかしたい」という思いが先にあって、それがチームを巻き込んだ取り組みに育っていった、という流れです。 「AIを業務に活かしたい」という話はあちこちで聞くものの、実際にチームとして何から手をつければいいのかは悩ましいところだと思います。私たちも手探りで進めたので、その過程と、やってみて分かったことを正直に共有します。 なぜ「AI days」をやったのか きっかけは、いくつかの課題感が重なっていたことでした。 AI活用が「点在」していた 業界全体、そして社内の他部署でもAIを業務に組み込み始める動きが広がっていました。一方で私たちトモニテ開発部は、メンバーそれぞれがAIを使ってはいるものの、スキルやノウハウが個人に点在しているだけで、業務フローに常設できていない状態でした。 組織として一段上のフェーズへ 個人の工夫に頼るのではなく、開発部全体のAI活用レベルを底上げして、組織として次のフェーズへ移行したい、という思いがありました。今後の新たな取り組みに着手するときにも、ここで構築した環境や型をそのまま流用できる状態をつくっておきたい、という実利的な狙いもありました。 これらを一気に前進させるために、まとまった時間を確保して集中的に取り組む形として「AI days」を企画しました。 進め方 AI daysは、次のような構成で実施しました。 Day0: 何にフォーカスしてどんなものを作るのかディスカッション Day1〜2: チームでskillを作成する 実施後: 全体での振り返り Day0で方針を固め、残りの時間はひたすら手を動かす、という流れです。それぞれ、どんなことを話して何をやったのかを掘り下げて紹介します。 なお、本記事に出てくる「skill」「agent」「オーケストレーター」は、いずれも Claude Code をベースにした仕組みです。AIに実行させる手順や命令群を、Claude Code の skill / agent として書き起こしていった、という前提で読んでいただけると分かりやすいと思います。使用したモデルは、AI days を実施した2026年6月第1週時点での Claude Opus 4.8 です。 Day0:AIにどこまで任せるかを決める Day0は、作業日とは別にMTGとして実施しました。本番に入る前に、まずチームで次のことを共有・議論する時間です。 なぜやるのか:背景と大義名分の目線合わせ 他部署での活用事例の共有:たとえば、Claude Code と Redash MCP(Databricks)を組み合わせることで、非エンジニア(PMM)でも高度な統計分析や横断分析ができるようになった、といった事例 現在地のヒアリングとアクション議論:各自のAI活用の現状の持ち寄りと、次のアクションの検討 そして、この場で中心になったのが、「そもそも何にフォーカスするのか」という問いでした。 論点はいくつもありました。 日常業務の自動化か、機能開発の効率化か:効果が大きいのはどちらか。日常業務なら複数の業務をMCPでつないで回せるか、機能開発ならUIの妥当性やデザインとの接続まで担保できるか。 どこまで巻き込むか:開発部に閉じてやるか、ビジネスサイドまで巻き込める領域でやるか。 議論の中で見えてきたのは、領域によってAIに任せきれる度合いが違うということでした。たとえばサーバーサイドは、私たちの開発基盤上、DBのスキーマからモデルを自動生成したり、OpenAPIでAPIの型を定義したりと、「定義」から実装をコード生成で結びつける仕組みが整っています。そのぶん生成物が定義からずれにくく、確からしさを担保しやすい。一方でUIには、こうした「正解」を機械的に突き合わせる仕組みがなく、AIのよしなな判断に委ねるとずれが出やすく、任せきりにしにくい領域です。 こうした議論を経て、今回は「開発部に閉じて、実装フェーズの自動化にフォーカスする」と決めました。対象にしたのは、 要件 → 設計 設計 → 実装 それを受けてのレビュー という実装まわりの一連の流れです。なお、要件→設計については、すでに個人が作ったものが共有されていたため、AI daysでは設計→実装のところから着手しました。 ここで目指したのは、AIの生成物をほぼノーレビューでapproveできる状態でした(結果がどうだったかは、後半の「振り返り」であらためて触れます)。そのために、行き当たりばったりではなく「こういう順番で・こういうルールで開発する」という型(ハーネス/ガードレール)をあらかじめ決めてしまおう、という方針を立てました。 進め方そのものも合わせて決めました。モブプロ/ペアプロ形式で手を動かし、話し合った内容をその場で誰かが作業者として形にしていく。そして、毎日PRを出してマージするところまで持っていく、という進め方です。 Day1〜2:チームでskillをつくる まずは「どんなルールがあるとよいか」を出し合う Day1は、対象とするAPIサーバーのリポジトリを1つ決め、「設計→実装の仕組みづくりにおいて、どんなルールがあるとよいか」をメンバーそれぞれが発表するところから始めました。 出てきた観点は多岐にわたりました。 アーキテクチャの明示:レイヤードアーキテクチャや命名規則といった既存の実装規約 実装フローの明示:要件→設計→テスト→実装という順序、Red→Greenサイクル テスト設計方針:テストテンプレート、テストデータ作成、テーブルテストの汎用化 レビュー観点:個人情報、環境依存、命名規則、再利用性、依存パッケージのバージョンなど PR作成:レビューしやすいよう小さく分割する セキュリティ:余計な権限をつけない、依存は最新・公式の使い方に準拠 パフォーマンス:EXPLAINなどで重くなりそうな箇所に事前に気付けるように この過程で出てきたのは、明確なルールだけではありませんでした。「普段はこう書く」「ここはこう判断している」といった、これまで明文化されてこなかったチームの共通認識も、あわせて表に出てきました。各自の頭の中にあった前提を口に出して並べたことが、思わぬ副産物につながりました。この点は、後ほど「振り返り」であらためて触れます。 「AIにできること/できないこと」で仕分ける 集まったアイデアは、次の3つに分類していきました。 人間はできているが、AIがまだできていないこと 人間もAIもできていないこと これまでできていなかったが、AIを使うことで効率よくできるようになること この仕分けが効きました。(1) はすでに人間が回せていることを言語化すればよいので、効果が出やすく着手しやすい。(2)(3) は新たに型をつくる必要があるぶん難度は上がりますが、うまくいけば伸びしろが大きいというように、何にどれだけ力を割くかの優先度を、チームの認識を揃えたうえで決められたからです。 そのうえで採ったのが、「普段のワークフローをskillに落とし込む」というアプローチです。いきなりAIエージェントの大掛かりな設計に踏み込むのではなく、すでに自分たちが回している業務の手順を、AIが実行できる形(skill)として書き起こしていきました。身近な業務から手をつけられるので、メンバー全員が手を動かしやすく、成果物がそのまま日々の開発に使える、という狙いです。初日は、この設計→実装のskillを形にするところまで進めました。 動かして検証し、レビューフローまでつくる 2日目は、前日につくったskillを実際のタスクに当てて動かし、その結果をチームで共有するところから始めました。出来上がったコードだけでなく、エージェントが実際にどう動いたかのログも一緒に見ながら、「ここは気になる」というポイントを洗い出していきます。たとえば、こんな気づきが挙がりました。 既存の実装を見つけているのに使わない:月齢計算のような複雑な処理で、本来は流用してほしい既存のユーティリティ関数があるのに、AIはそれを見つけたうえで使わず、独自に実装していました。「これを使ってね」という前提を事前に渡しておくべきだった、という気づきです。 ゼロから作るものは別PRに切り出したい:テストのfactoryのように、まだベースが存在しないものを実装しながら作らせると、意図しない作り方になりがちでした。先にそれだけを用意するPRを分けた方がよさそうです。あわせて「PRをどう分割するか」自体も、判断基準として持たせる必要があるかもしれません。 手順として明示すべき付随作業がある:DBを変更したらマイグレーションを生成する、API定義を変えたらドキュメント(swagger)を更新する、といった作業は、フローの中に手順として書いておかないと抜けてしまいます。 agentとskillの責務分担:「どこまでをagentに考えさせ、どこからをskillとして固定するか」の線引きには個人差があり、ここでも議論になりました(この問いは後述の「今後の展望」にもつながっていきます)。 そして、こうして見えてきた観点も踏まえながら、2日目の後半は、初日と同じくモブプロ形式でレビューフローの作成に取り組みました。実装する側だけでなく、生成物をチェックする側の型もそろえることで、「AIに実装を任せ、別系統でレビューする」という一連の流れを形にする、という狙いです。 つくったもの 2日間で、思っていた以上にたくさんの成果物が生まれました。大きく分けると、AIに渡すための「土台」と、それを使って実際に動かしたskill群です。 土台(AIに前提を渡すためのドキュメント類) リポジトリごとの約束ごとをまとめた CLAUDE.md テスト設計ガイドライン(テスト設計の指針をドキュメント化。 docs/test_design_guideline.md ) 実装ガイドラインの更新( docs/implementation_guide.md ) 実装フローを回すskill群 全体を束ねるオーケストレーターと、そこから呼び出される工程ごとのskillをセットで作りました。タスク分割書(実装タスクを粒度ごとに分割したドキュメント。前述のとおり要件→設計の工程は既存のskillが担っており、その出力物がこれにあたります)を入力に渡すと、テストコード・実装のdiff・ドラフトPRが出力される、というイメージです。具体的には、次の流れを一気通貫で回します。 コンテキスト収集 → テストケース定義 → テスト実装(Red)→ 機能実装(Green / Refactor)→ 動作確認(curlでの実機疎通)→ ドラフトPR作成 → レビュー 工夫したのは、各工程を独立したサブエージェント(別コンテキスト)として起動し、引き継ぎは会話ではなくファイル(収集したコンテキスト、テストコード、diff など)で行うようにした点です。テスト・実装・レビューが互いの思考を引きずらないので、後述する「別系統でのレビュー」も自然に組み込めました。 イメージしやすいよう、オーケストレーターskillの構成をもう少し具体的に紹介します。オーケストレーター自身は「薄く」保ち、ループ制御・サブエージェントの起動・成果物の引き継ぎ・失敗時の停止に徹して、実装の実体は工程ごとのskillに委譲する、という役割分担です。タスク分割書の依存関係からタスクの実装順を決め、1タスクごとに次の工程を回していきます。全体像を図にすると、次のような流れになります。 工程 委譲先skill 成果物 ① コンテキスト収集 collect-context context.md(当該タスク分) ② テストケース定義 write-testcases testcases.md ③a テスト実装(Red) implement-tests テストコード+Red確認 ③b 機能実装(Green / Refactor) implement-feature 本番コード+全テストGreen ③c 動作確認 verify-endpoint curlでの疎通確認結果(※エンドポイントを含むタスクのみ) ④ ドラフトPR作成 create-draft-pr 1タスク=1ドラフトPR ⑤ レビュー 社内のレビュー用プラグイン 指摘一覧 フロー全体のルールとしては、次のようなものを明文化しています。 工程は基本①〜⑤を順に回すが、③c 動作確認はエンドポイントの追加・変更を含むタスクのときだけ実行する(repository追加やバッチなど、叩く対象がないタスクではスキップする) PRは1タスク=1PRで小さく保ち、依存タスクがある場合は依存先のブランチから分岐したスタックPRにする レビュー(⑤)でCritical / Importantな指摘が出たら、機能実装とレビューを、指摘がゼロになるまで往復する(上限あり)。このとき、修正者の思考をレビューが引き継がないよう、レビュー担当のサブエージェントは毎回新規に起動する Greenにできない・要件と矛盾するなど解消できない問題に当たったら、推測で進めずに要確認事項として記録し、そのタスクで停止する もう一歩踏み込んで、各工程のskillに実際に書いている指示も紹介します。どのskillも、「品質に気をつける」のような心構えではなく、 「〜の場合は〜する」という状況と行動のペア で指示を書いています。 テストケース定義(②)には、こんな指示を書いています。 異常系は「エラーになること」ではなく、期待するエラー文言・型まで指定する 各テストケースに信頼性ラベルを付けさせる。「🔵 要件・既存実装に基づく / 🟡 合理的な推測 / 🔴 要確認」の3段階で、AIが推測で書いた部分を確定情報と混ぜさせない テスト実装(③a)のskillには、「テストを仕様として、本番実装とは独立したコンテキストで書く」という目的と、それを守るための指示を書いています。 本番ロジックは書かない。テストのコンパイルを通すための最小限のスタブ(中身は panic("not implemented") など)までは許可する Redを確認するときは、失敗の理由が「未実装による期待どおりの失敗」なのか「テストコード自体のバグ」なのかを切り分けてから返す 機能実装(③b)には、その逆を守らせる指示を書いています。 テストを弱めない。Greenにするためにアサーションを緩めたり削ったりしない。テスト側にバグがあると判断しても自分では直さず、要確認として報告する(テストは別工程の責務) リファクタは「このタスクで自分が書いた差分」に限定する。既存コードの規約違反に気づいても勝手に直さず、別PRの領分として報告に留める 動作確認(③c)には、何をもって「確認できた」とするかを指示しています。 「200が返った」だけでは正常系の確認にしない。検証用の既知の値をDBに投入し、それがレスポンスに正しく含まれることまで突き合わせる。あわせて、他ユーザーのデータや論理削除済みデータといった「含まれてはいけないデータが含まれない」ことも確認する 結果が期待と違ったら❌のまま正直に報告し、レポートを✅に寄せない。安易に「環境のせい」にしない こうした指示の多くは、最初から書けていたわけではなく、実行ログで意図しない動作を見つけるたびに書き足していったものです。たとえば「実行が英語で進んでいってしまう」という地味な問題は、サブエージェントを起動するプロンプトの冒頭に「すべての出力・成果物・ユーザーへの質問は日本語で行うこと」を必ず明記する、という指示で潰しました。 書いたルールが実際に判断として現れた場面もありました。テスト側のフィクスチャにバグがあり(デフォルト値を持つカラムにゼロ値を入れたところ、ORMの仕様でドロップされてDB側のデフォルト値で上書きされ、「非アクティブにしたいデータ」がアクティブのまま保存されていた)、実装が正しいのにテストがGreenにならない状況が起きたときのことです。このとき機能実装のサブエージェントは「テストを弱めない・テストは別工程の責務」というルールに従って、テストを書き換えてGreenにする近道を選ばず、オーケストレーターに差し戻していました。 そのときのオーケストレーターの実行ログがこちらです(抜粋・整形。日本語化の指示を入れる前の実行なので英語のままです)。 ⏺ Agent(Write testcases TASK-0003) ⎿ Done (13 tool uses · 72.1k tokens · 1m 56s) ⏺ 14 cases. Test subagent (Red) for TASK-0003: ⏺ Agent(Write Red tests TASK-0003) ⎿ Done (22 tool uses · 103.3k tokens · 3m 12s) ⏺ Red confirmed (14 cases). Feature subagent (Green→Refactor) for TASK-0003: ⏺ Agent(Implement TASK-0003 Green) ⎿ Done (20 tool uses · 52.3k tokens · 2m 40s) ⏺ The feature SA found a genuine test-fixture bug (not an impl bug): boil.Infer() drops is_active=false (zero-value of a with-default column), so the inactive fixture V3 persists as active. The impl is correct; per the skill the feature SA correctly refused to weaken the tests and stopped. Fixing fixtures is implement-tests' responsibility. I'll spawn a focused test-fix subagent (still the implement-tests role): ⏺ Agent(Fix TASK-0003 test fixture) ⎿ Done (14 tool uses · 38.2k tokens · 1m 22s) ⏺ 14/14 green now. テストケース定義→テスト実装(Red)→機能実装(Green)と工程ごとにサブエージェントが起動されていく様子と、機能実装のサブエージェントがフィクスチャのバグを見つけながらも自分では直さず("correctly refused to weaken the tests and stopped")、オーケストレーターがテスト修正専用のサブエージェントを改めて立ち上げて、フィクスチャだけをピンポイントで直させた様子が分かります。skillに書いた「テストを弱めない・テスト側のバグは自分で直さず報告する」が、そのまま判断として現れた例です。 一方で、現実的な落としどころを選んだ部分もあります。レビュー工程は、社内ですでにプラグイン化されているものを再利用しました(このレビュー用プラグインについては 過去のブログ記事 で紹介しています。その後もアップデートを重ねています)。また動作確認は、そもそも「何をもって動作確認とするか」という議論もしました。その過程で「リモートのテスト環境(社内のtestkit)経由で実行できる仕組み」という案も出たのですが、時間の兼ね合いで断念。今回はcurlでの実機疎通に落ち着いています。 そして、これらを使って実際にコードを書き、プルリクエストを出すところまで一気に進めることができました。人間が操作したのは、走り出す前にオーケストレーターから「どこまで自律で進めるか」「push・PR作成をどう扱うか」を確認されて回答した一度だけで、あとは全自動でできました。 振り返り:やってみて分かったこと 実施後、メンバー全員で振り返りを行いました。出てきた声を、よかった点と課題に分けて整理します。 よかったこと オーケストレーター+個別skillという構成 全体を指揮するオーケストレーターと、個別のskillに分けた構成にしたのですが、これがとてもよかったです。役割が分かれているので作るときも考えやすく、あとから育てていきやすい構成になりました。 暗黙知を言語化する作業そのものに価値があった skillをつくるには、普段は頭の中にある暗黙知を明文化する必要があります。この「暗黙知を落とし込む」作業は、skill作成の上で必須であると同時に、チームの知識を整理するうえでも意味のあるプロセスでした。 振り返りでも、「チームでの理解が揃った」「自分たちのコーディングやレビュー視点の棚卸しになった」「開発のスタイルや意思決定に対する、お互いの認識を合わせられた」という声が多く挙がりました。AIに渡すための土台(コンテキスト)を整える作業が、結果としてチーム内の共通認識を言語化する場にもなった、というのが大きな収穫でした。 モブプロ/ペアプロ形式が「やり方の共有」になった 普段の開発とはあえて違う形式で進めましたが、これも結果的によかった点です。話し合った内容をその場で誰かが形にしていくので、議論したことがそのまま成果物として残っていきました。また、skillやプロンプトを書く過程を互いに見ながら進める形式だったこともあり、振り返りでは「それぞれがどんなふうにskillを使い、作っているかという話ができたのはよかった」という声が挙がりました。AI活用のノウハウが個人に点在していた私たちにとって、やり方そのものを共有する機会になりました。 「まずは作ってみる」で十分ワークした ワークフローを落とし込むだけで、ある程度使えるものが出来上がりました。最初から作り込む必要はなく、まずトライしてみる、というスタンスで進めてよかったと感じています。 課題 「ツールで仕組み化すべき層」と「AIに任せる層」が混ざっていた 一方で反省点もありました。本来は静的解析(linterや型チェックなど)で機械的に防げるはずのところを、skill側でカバーしようとしていたケースがありました。 裏を返すと、暗黙知をわざわざskillに落とし込まなくても、静的解析で担保できた領域があったということです。何でもAIに言い聞かせるのではなく、コンパイラやリンターのレベルで制約をかけられるものはそちらに寄せる、という切り分けが必要だと気づかされました。 AIのセルフレビューには構造的な盲点があった もう1つ、AI駆動開発ならではの学びがありました。生成されたコードに対してAI自身にセルフレビューをさせると「重大な問題はなし」と返ってくる一方、別のAIコードレビューに通すと、バグやテストの不備が次々と見つかる、という場面が繰り返し起きたのです。 設計判断そのものは概ね妥当でも、AIに「自分の書いたものを自分でレビューさせる」だけだと、観点に構造的な抜けが残るということです。集約したデータを取りこぼしていないか、境界条件、並列実行時のテストの安定性など、人間でも見落としやすい観点ほど抜けやすい傾向がありました。 ここから得た教訓は、AIに任せきりにせず、静的解析・別系統のレビュー・人間のレビューを重ねる「多層のガードレール」を前提に組むべき、ということです。 さらに踏み込んで議論したこと 「よかった/課題」とは別に、振り返りでは少し踏み込んだ問いについても議論しました。AI駆動開発をチームで進めるうえで、同じことを考えている方の参考になりそうなので共有します。 通常業務の時間を削ってまで、この期間を確保した価値はあったか ここはおおむね「価値はあった」で一致しました。チームの理解が揃い、各自がどんなふうにskillを使い・作っているかを共有でき、AI駆動開発を進めるための基盤ができた、という点が評価されました。一方で「実際の開発フローへの導入まではもう一段ハードルが高い」という現実的な声もありました。 作ったものを、そのまま実務に流用できそうか ここは率直に「使える、ただし手放しでは無理」という評価でした。 使うことはできるが、劇的に速くなるわけではない 出来上がるのは「本実装とプロトタイプの中間」くらいの成果物で、そのまま扱うには少し悩ましい 「どこかにダメな部分がありそう」という前提でレビューするので、認知負荷込みで考えると「爆速」にはならない レビューの往復は当面避けられず、人間の手放しは遠い ただし、「要件を満たすPRが出来上がるまでのリードタイムは減らせそう」という手応えもありました。実装そのものは人間がやらなくてよい、けれどその分の穴を見つけて直す工程が残る、というのが現在地です。「せめて、AIレビューで定番的に指摘される箇所くらいは、AI自身が自律的に直せるようになってほしい」という要望も挙がりました。 なぜ「育てながら使う」という結論に落ち着いたか 「このままバッチリ使える」ではなく「各自で使いながら育てよう」という着地になった背景も振り返りました。 成果物の数は出たものの、全体を一気に「広く」設計してしまったため、一つひとつのskillの試行錯誤や深掘り(精度向上)に時間を割ききれなかった(開発フローの一部に絞って深める、というやり方もあり得たかも) 言語化しきれていない暗黙の指定が、まだ多く残っている skillへの指定だけでは限界があり、コンパイラやリンターのような強い制約はかけられない 「使ったあとにどう育てるか」までフローに組み込めていれば、もう少し「勝手に育つ」状態に近づけられたかもしれない 印象的だったのは、「これまで『よしなに実装』していたのが、『よしなにAIを育てる』に変わった感じ」という表現でした。人間がやることの重心が、実装そのものから「AIをどう育てるかの設計」へ移りつつある、という感覚です。 継続して運用するために、最低限なにが必要か 最後に、これを一過性で終わらせないために必要な仕組みを洗い出しました。 ガードレール(静的解析やテストのファクトリなど、機械的に品質を担保する仕組み) 各skillをもっと独立させ、一部から個別に導入・チューニングできるようにすること フィードバックを貯める場所と、それを改善に回すループ(理想は、改善が自然な運用フローに乗っている状態) 「どう育てていくか」の方針 今後の展望 今回は「ワークフローをskillに落とし込む」という形で進めましたが、振り返ってみると、その一歩手前にある「これはskillにすべきか、agentにすべきか」「skillの責務は必要十分か」という「AIの設計」のレベルまで踏み込めていれば、さらによかったかもしれません。 次の一手としては、実装前の手順も含めた全体フローを可視化し、どこからどこまでがAIの役割で、どこが人間の責務なのか(エンジニアだけでなく、PdMレベルも含めて)を整理していきたいと考えています。 あわせて、このskill群が実際にどれだけ利用されているかも計測していくつもりです。また、どの工程にどのモデルを充ててトークンコストをコントロールするかも、ハーネスエンジニアリングの一部として今後組み込んでいきたいと考えています。 まずは作ってみる、はじめから作り込まない、というアプローチ自体は正解だったと思っています。「改善は使った人が各自で進めてよし、大きな変更や相談したいことは歓迎」というゆるやかな運用方針のもと、ここで構築した環境や型を、今後の開発で継続的に育てながら活かしていく予定です。 まとめ 「AI days」は、点在していたAI活用のスキルを、組織の型として束ねていくための第一歩でした。 2日間という短い期間でも、ワークフローをskillに落とし込むだけで、ある程度使えるものを形にできました。同時に、「ツールで仕組み化すべき層との切り分け」「AIのセルフレビューの盲点」「育てる仕組みの必要性」といった、次に向き合うべき課題も具体的に見えてきました。 私たちの場合は、「まずは作ってみる。はじめから作り込まない」というスタンスで始めてみました。まだ手探りですが、得られた型を実務で磨きながら、次はもっと使えるものを作れるよう、引き続き思いを馳せていきます。
こんにちは、デリッシュキッチン開発部の 鈴木 です。 概要 Data Definition Language(DDL、データ定義言語)、つまり CREATE TABLE や ALTER TABLE でスキーマを変更する操作を、トランザクションの中で実行してあとからロールバックできるかは、データベースによって違います。MySQL ではロールバックできず、PostgreSQL ではロールバックできます。 ロールバックは、1 つのトランザクションの中で行った変更を、記録しておいた変更前の状態に戻す操作です。だからロールバックするには、その操作が 1 つのトランザクションに収まっていること、そしてその変更が記録されていることの 2 つが必要です。前者はどのデータベースでも同じで、1 つのトランザクションに収まらない操作(PostgreSQL の CREATE INDEX CONCURRENTLY など)は、そもそもロールバックの対象になりません。データベースによって分かれるのは、後者です。 ロールバックできるか、つまり変更が記録されるかは、メタデータの置き場所で決まります。変更前の状態は、ストレージエンジンの内部に記録されます。そのため、スキーマの定義情報であるメタデータも、その内部になければロールバックできません。古い MySQL はメタデータをエンジンの外側のファイルに置いていたため、ロールバックできません。PostgreSQL はメタデータを通常のテーブルとして内部で管理するため、ロールバックできます。MySQL 8.0 はメタデータを内部に移しましたが、互換性のために暗黙的なコミットを残したため、ロールバックできません。MySQL と PostgreSQL の差は、この置き場所の違いから生まれます。 図1:DDL をロールバックできるかの全体像。1 つのトランザクションに収まるか(DB 共通)→ 収まればロールバックできるか(DB で違う)の順に決まる。 前提:スキーマ変更をロールバックできるかは、データベースで違う CREATE TABLE や ALTER TABLE といった DDL を、トランザクションの中で実行してあとからロールバックできるかは、データベースによって違います。 MySQL :ロールバックできません。DDL には暗黙的なコミットが伴い、 ALTER TABLE を実行した時点でそれまでの変更が確定します。あとから ROLLBACK しても戻りません。 PostgreSQL :ロールバックできます。 BEGIN; ALTER TABLE ...; ROLLBACK; で変更前の状態に戻ります。 本記事はこの差を前提とし、 この違いを生んでいるものは何か を扱います。 DDL をロールバックできるかは、収まるか → ロールバックできるか の順で決まる ロールバックは、1 つのトランザクションの中で行った変更を、記録しておいた変更前の状態に戻す操作です。だからロールバックするには、その操作が 1 つのトランザクションに収まっていること、そしてその変更が記録されていること、の 2 つが必要です。前者を満たさなければ後者には進めないので、まず収まるか、次にロールバックできるか、の順に確かめれば十分です。 1 つのトランザクションに収まるか。 実行のしかたの問題で、データベースによらず一律に効きます。 収まるとして、その変更をロールバックできるか。 メタデータ(テーブルや列などスキーマの定義情報)の置き場所で決まり、データベースによって変わります。 ロールバックできるかを問えるのは、1 つのトランザクションに収まる操作だけです。前提で見た MySQL と PostgreSQL の差は、ロールバックできるかで決まります。 1 つのトランザクションに収まるか(DB によらず一律) ほとんどの DDL は 1 つのトランザクションに収まります。ただし、収まらない操作もあります。これはデータベースによりません。 代表例は PostgreSQL の CREATE INDEX CONCURRENTLY です。通常の CREATE INDEX はテーブルをロックして一気に索引を作ります。一方 CONCURRENTLY は、書き込みを止めずに索引を作るため、複数の段階に分けて実行します。各段階はそれぞれ別のトランザクションになるので、全体を 1 つのトランザクションブロックに収められません。 DDL をロールバックできる PostgreSQL でも、 CREATE INDEX CONCURRENTLY はトランザクションの中では実行できません。ロールバックできるかどうか以前に、1 つのトランザクションに入らないからです。この操作はここで止まり、ロールバックできるかはそもそも問えません。 収まるとして、ロールバックできるか(ここで DB が分かれる) 1 つのトランザクションに収まる操作、つまり普通の DDL について、ロールバックできるかを考えます。これがデータベースによって違います。 行の更新や削除といった Data Manipulation Language(DML、データ操作言語)をロールバックできるのは、MVCC(変更前の古い行を残しておく仕組み)が古い行を保持しているからです。この記録の仕組みはストレージエンジンの内部にあります。だから DDL をロールバックするには、スキーマを記述するメタデータも、ストレージエンジンの内部、つまりこの仕組みの管理下になければなりません。 メタデータがこの管理下にあるかどうかが、データベースごとの差を生みます。 古い MySQL(5.7 以前) :メタデータは .frm というファイルにあり、ストレージエンジンの外側にありました。ロールバックの仕組みの管理下にないので、トランザクションに参加できません。だから暗黙的にコミットされ、ロールバックできません。 MySQL 8.0 :メタデータを InnoDB の内側に取り込み、データディクショナリとして管理するようにしました。これで DDL がアトミックになりました(途中で中断しても中途半端なスキーマが残りません)。ただし、この変更が狙ったのはクラッシュ時の安全性であって、ユーザーがロールバックできるようにすることではありません。従来の挙動との互換性から、暗黙的なコミットはそのまま残されました。 アトミック(壊れない)と、ユーザーが好きなときにロールバックできることは別物です。 内側に入れても、まだロールバックできません。 PostgreSQL :スキーマ情報をシステムカタログ( pg_class など)として、通常のテーブルと同じく MVCC で管理します。 CREATE TABLE はシステムカタログに 1 行追加するだけの操作になります。行の追加をロールバックするのと同じ仕組みがそのまま効くので、ロールバックできます。 3 つは別々のケースではなく、メタデータの置き場所が、外側(古い MySQL)から、内側だが暗黙コミット(MySQL 8.0)、内側で完全管理(PostgreSQL)へと段階的に変わっているだけです。前提で見たとおり、MySQL はロールバックできず、PostgreSQL はロールバックできます。これは、このメタデータの置き場所による結果です。 図2(詳細):DDL をロールバックできるかは、1 つのトランザクションに収まるか(DB 共通)→ 収まるとしてロールバックできるか(メタデータの置き場所・DB で違う)の順に決まる。 結論:DDL かどうかではなく、収まるか → ロールバックできるか で見る DDL をロールバックできるかは、収まるか、次にロールバックできるか、を順に確かめれば分かります。DDL かどうかで一律に判断すると、PostgreSQL ではロールバックできることや、PostgreSQL でも CREATE INDEX CONCURRENTLY はロールバックできないことを見落とします。この順で見れば、そうした例外も正しく判断できます。 1 つのトランザクションに収まるか。 収まらなければ( CREATE INDEX CONCURRENTLY など)、どのデータベースでもロールバックできません。 収まるとして、メタデータがロールバックの仕組みの管理下にあるか。 使っているデータベースによって、ロールバックできるかが決まります。 最後に、具体的な落とし穴を一つ挙げます。MySQL では DDL と DML を同じトランザクションに混ぜてはいけません。これはロールバックできるか(メタデータの置き場所)に関わる問題です。MySQL では DDL のメタデータがロールバックの仕組みの外側にあるため、 ALTER の暗黙的なコミットによって、直前の INSERT などが意図せず確定してしまいます。この挙動は、DDL がロールバックの仕組みの外側にあることから説明できます。 まとめ 今回は、なぜ MySQL では DDL をロールバックできず PostgreSQL ではできるのかについて考えました。MySQL は DDL をロールバックできない、PostgreSQL はできるということを覚えてもいいとは思うのですが、それではツールの使い方に閉じてしまいます。なぜそのような設計になっているのかを理解することで、MySQL や PostgreSQL という個別の知識にとどまらず、初めて触れる操作やデータベースでも、1 つのトランザクションに収まるか、そしてメタデータがロールバックの仕組みの管理下にあるか、という同じ見方で自分で判断できるようになります。実際、同じ PostgreSQL でも CREATE INDEX CONCURRENTLY はロールバックできない、といった例外にも自分で気づけるでしょう。
はじめに こんにちは、デリッシュキッチンでiOSエンジニアをしている谷口恭一です。 デリッシュキッチンの iOS アプリでは、プッシュ通知やディープリンクから特定の画面・特定のセクションへ直接遷移する導線を数多く提供しています。 delish://scroll_to_any_section このリンクの例で意図しているのは、次の一連の動作です。 アプリを起動し、下部タブバーで特定のタブに切り替え目的の画面に遷移し、その中の特定のセクションまでスクロールし、必要ならそのセクションのモーダルを提示する。 一見ただの画面遷移に見えます。ところが指示された通りに実装すると、アプリが閉じている状態や他画面にいる状態で起動したときスクロールが効かない、という現象に突き当たります。「スクロールせよ」という命令を出した瞬間には、スクロール先のセクションがまだ画面に存在しない可能性があるからです。セクションはネットワーク通信が返ってきて初めて描画されますが、その完了を待たずに命令が走ってしまいます。 なぜ難しいのか この難しさの根は、SwiftUI が宣言的 UI フレームワークであることにあります。SwiftUI では画面は状態から導出され、状態が変われば画面が再構築されます。一方「特定の瞬間に一度だけスクロールする」「モーダルを開く」といった操作は、状態とは無関係に一度だけ走らせたい 一過性の命令 です。この一過性の命令を宣言的な UI のなかで確実に実行するには、ただ命令を書くだけでは足りず、宣言的な仕組みに乗せる工夫が要ります。 しかもディープリンクの場合、命令を出す側(起動)が、命令を受け取る画面よりも先に存在します。受け手がまだ生まれていないところへ命令が飛んでくる、という時間的なずれも重なります。 この記事の目的 この記事では、この問題を 命令を直接実行しようとせず、SwiftUI が扱える「状態」に置き換えて解く というアプローチで整理します。プッシュ通知やディープリンク、外部サービスからのコールバックなど、画面のライフサイクルの外から飛んでくるトリガーを扱う場面で広く応用できる考え方だと思います。 リンク起動からのスクロールを題材に、次の順で、単純な実装が壊れるところから一つずつ組み立てていきます。それぞれの仕組みがなぜ必要になるのかが、順を追って見えてくるはずです。 なぜ単純な実装では動かないのか スクロール要求を、消えずに残る「状態」として保持する 前提条件が揃った瞬間に、何度起きても安全に一度だけ実行する 起動情報を、画面が生まれる前から保持し続ける置き場所を用意する 一つの起動情報に、複数の画面要素がそれぞれ反応できるようにする なお、ディープリンク処理は「受け取った URL を解析してどの画面へ遷移するか判定する」前半と、「遷移先の画面で要求された処理を実行する」後半に分けられます。この記事で扱うのは後半だけで、前半の URL 解析・画面判定には触れません。 1. なぜ単純な実装では動かないのか スクロールという処理を実行するには、次の前提条件がすべて揃っている必要があります。 対象画面の View 階層が構築済みであること セクションの描画に必要なデータがネットワークから取得済みであること そのデータをもとにスクロール先の View が描画済みであること 問題は、 要求が発生した時点でこれらがどこまで揃っているかが、起動状態に依存して変わる ことです。 コールドスタート (終了状態から起動):要求発生時、前提条件は一つも揃っていません。 ウォーム/ホットスタート (起動済みで対象画面も表示中):要求発生時、前提条件がすべて揃っていることもあります。 そして、揃っていく順序やタイミングは制御できず、要求側から知ることもできません。最も極端なコールドスタートでは、要求の発生時刻 t = 0 と実行可能になる時刻 t = T が大きく乖離し、 T はネットワークレイテンシ次第で毎回変動します。 要求を受けた時点で scrollTo を直接呼ぶ単純な実装を考えてみます。コールドスタートでは t = 0 に実行され、前提条件が揃う t = T まで待たないため、何も起こりません。一方ウォームスタートでは前提条件が揃っているため正しく動作します。すなわち、 同じコードが起動状態によって成功したり失敗したりする わけです。この実装は、正しさを実行時点のタイミングという制御不能な要因に依存させています。 この命令的アプローチは、暗黙に「要求時点 = 実行可能時点」を前提しています。同期的なボタンタップなら成立しますが、ディープリンク経由では成立しません。求めるのは、起動状態によらず常に正しく動く実装です。そのために、以降では次の三つを順に組み立てていきます。 要求を、実行可能になるまで失われない 状態 として保持する 前提条件が揃ったタイミングで実行する 冪等なトリガー を配置する 起動情報を、 View / ViewModel より生存期間の長いオブジェクト に保持させる 2. スクロール要求を、消えずに残る「状態」として保持する まず、要求を「即座に実行する命令」ではなく「未実行の要求」という状態として保持します。実行可能になるまで要求が失われないようにするためです。最小の表現は二値の列挙型で足ります。 enum ScrollRequest { case none case requested } @Published private ( set ) var scrollRequest : ScrollRequest = .none 要求の記録とクリアを、状態遷移として定義します。 func requestScroll () { scrollRequest = .requested } func resolveScroll () { scrollRequest = .none } 要求の記録は、いつ実行されるかを規定しません。要求側は実行すべき内容だけを記録し、いつ実行可能かの判断は次節の実行側が担います。これにより、要求の発生と実行が分離されます。 3. 前提条件が揃った瞬間に、何度起きても安全に一度だけ実行する 要求が状態として残るようになったので、次は実行側を組みます。実行してよい条件は、要求と前提条件の論理積です。 この条件は二つの変数(要求と前提条件)の論理積であり、二つが真になる順序は起動状態によって変わります。どちらが先かで二通りに分かれます。 ケース A:要求が先に立ち、前提条件が後から揃う(コールドスタートに典型的) ケース B:前提条件が先に揃っていて、要求が後から立つ(すでに対象画面が表示されている場合) どちらのケースでも、後から真になった変数が出そろった瞬間に論理積が成立します。前提条件が要求のはるか前から揃っていても、その瞬間が要求の成立と同時になるだけで、構造は変わりません。順序が一定しない以上、実行を単一のタイミングに固定することはできません。そこで、条件が満たされうる タイミングごと に、同一のガードと同一の処理を配置します。SwiftUI では次の3つが該当します。 トリガー(1) : .onAppear ── 対象セクションが View 階層に出現した瞬間 トリガー(2) : scrollRequest の .onChange ── 要求が後から立った瞬間(セクションは描画済み) トリガー(3) :データ有無の .onChange ── データが後から到着した瞬間(要求は先に立っていた) .onAppear { if scrollRequest == .requested { proxy.scrollTo(targetSection); resolveScroll(); runFollowUp() } } .onChange(of : scrollRequest ) { newValue in if newValue == .requested, data != nil { proxy.scrollTo(targetSection); resolveScroll(); runFollowUp() } } .onChange(of : data != nil ) { hasData in if hasData, scrollRequest == .requested { proxy.scrollTo(targetSection); resolveScroll(); runFollowUp() } } 3つのトリガーは、いずれも同一のガード条件と同一の処理を持ちます。どちらのケースも、少なくとも一つのトリガーで捕捉されます。 ケース 条件が満たされる瞬間 捕捉するトリガー A:要求が先、前提条件が後 前提条件が後から揃う (3) または (1) B:前提条件が先、要求が後 要求が後から立つ (2) ここでケース B が、トリガー (2) を不可欠にします。前提条件がすでに揃っているところへ要求が立つと、要求の変化を見るトリガー (2) がその瞬間に成立を検知し、遅延なく実行します。コールドスタート(ケース A)だけ想定すると (2) は冗長に見えますが、すでに対象画面が表示されている状態を正しく扱うために必要です。「遅延実行」と呼んでいますが、要求の時点で前提条件が揃っていれば即座に実行され、遅延は前提条件が未充足のときだけ発生します。 複数のトリガーを置く以上、一つが実行した後に別のトリガーが発火しえます。これを安全にするのが、実行直後の resolveScroll() による状態消費です。 最初に条件を満たしたトリガーが状態を消費するため、後続のトリガーはガードで弾かれて副作用を持ちません。結果として、 どのトリガーが何回発火しても実行は必ず一度 となります。命令的アプローチが単一の実行時点を予測して失敗するのに対し、この設計は実行可能になりうる全時点を網羅し、最初に成立した一度だけを通します。ガード条件が不変である限り、タイミングがどう変動しても正しく動きます。 これは「いつスクロールするか」という命令的なタイミング制御を、「スクロールしてよい状態か」という宣言的な条件評価へ置き換えたものにほかなりません。 .onAppear や .onChange は SwiftUI が状態変化のたびに評価する仕組みであり、一過性の命令を、UI 更新と同じ評価サイクルに乗る宣言的な判定として表現しています。 4. 起動情報を、画面が生まれる前から保持し続ける置き場所を用意する ここまでで単一画面内の遅延実行は解けました。残る課題は、ディープリンクが運んできた起動情報(遷移元やコールバック先など)をどこに保持するかです。 コールドスタートでは、ディープリンクの受信そのものがアプリ起動のきっかけになります。つまり 起動イベントが先に発生し、それを受け取るべき View / ViewModel はその後に生成される わけです。イベントの時点では対象画面の ViewModel の init すら呼ばれておらず、View / ViewModel は起動をきっかけにこれから作られます。イベントが、受け手の生成に時間的に先行しています。 この非対称性が、状態の保持先を決定します。起動情報を @State や @Published のような View / ViewModel のライフサイクルに束縛された状態として持とうとすると、それらを宣言するインスタンスがイベントより後に生成されるため、イベントを受け取れません。したがって起動情報の保持先は、 View / ViewModel より生存期間の長いオブジェクト でなければなりません。起動元に近い側でこのオブジェクトを生成し、後から生成される ViewModel へ注入します。 この保持先に Combine の CurrentValueSubject を用います。 PassthroughSubject は送出時点で購読者が存在しなければイベントを破棄しますが、起動情報確定の時点で購読者となるべき ViewModel はまだ生成されていません。 CurrentValueSubject は最新値を保持し続けるため、ViewModel が後から生成・購読しても最新の起動情報を取得できます。 実装上は、起動元に近い側で Subject を生成し、ViewModel の init に注入します。ViewModel は Subject の所有者ではなく購読者です。 final class TargetViewModel { private let intentSubject : CurrentValueSubject < LaunchIntent , Never > init (intentSubject : CurrentValueSubject < LaunchIntent , Never > ) { self .intentSubject = intentSubject } } ViewModel は注入された Subject に対し、 init 時に .value を読めば起動時に確定した起動情報を同期的に取得でき、以降の新規起動には sink で反応できます。こうして CurrentValueSubject は、 イベント駆動(送出時に反応する)と状態保持(後から参照しても残っている)の双方を、ViewModel のライフサイクルを超えて 満たします。これは第2節の「要求を状態として保持する」を、起動情報という、より広い寿命のレベルで実現したものにあたります。 なお、ここで保持する起動情報は型のない URL ではなく、型付きの構造体にしておきます。後段のコードが URL の文法を意識せず、必要な値だけを参照できるようにするためです。 struct LaunchIntent : Equatable { let from : String? let callbackURL : String? let shouldScrollToSection : Bool } 5. 一つの起動情報に、複数の画面要素がそれぞれ反応できるようにする 一つのディープリンク起動が指示する内容は、しばしば複数の画面要素にまたがります。たとえば「特定のタブに切り替える」要素と「そのタブ内である処理を実行する」要素のように、担当の異なる別々の要素が、同じ一つの起動情報を必要とする場面があります。 起動情報を Subject で配信しておくと、これらの要素が 同じ一つの起動情報を、それぞれ独立に購読できます 。重要なのは、要素のあいだに上下関係や呼び出し関係がない点です。互いの存在を知らないまま、同じ Subject から各自に必要な情報だけを取り出します。 タブ切り替えを司る要素は、次のように購読します。 intentSubject .receive(on : DispatchQueue.main ) .sink { [ weak self ] intent in if intent.callbackURL != nil { self ?.selectedTab = .targetTab } } .store( in : & cancellables) 機能処理を司る要素は、同じ Subject を別に購読します。 intentSubject .sink { [ weak self ] intent in if let url = intent.callbackURL { Task { await self ?.handleCallback(url : url ) } } } .store( in : & cancellables) 二つの受け手は対等で、互いを知りません。新たな反応を足したければ .sink を一つ加えるだけでよく、既存の購読者には影響しません。命令の連鎖(「タブを切り替えてから処理を呼ぶ」)ではなく、一つの起動情報に複数の受け手が疎結合に反応する構造になっています。これが、起動情報を共有された状態として配信することの効果です。 6. 組み上がった構造 組み立ててきた要素を統合すると、次の構造になります。 この構造は、各部分を局所的に追加するだけで拡張できます。 新たな起動挙動の追加は、 Intent へのフィールド追加と、対応する要求状態・トリガーの追加で完結します。既存フローを変更しません。 新たな反応の追加は、 Subject への .sink 追加で完結します。他の購読者に影響しません。 実行タイミングを予測せず網羅するため、レイテンシや遷移順序が変動しても、ガード条件が不変である限り正しさが保たれます。 7. おわりに ディープリンク経由フローの難しさは、ルーティングではなく、「処理を要求された時点」と「処理を実行できる時点」がずれることにあります。命令的な実装は、要求した時点ですぐ実行できると暗黙に前提します。ですが実際に実行できるのは前提条件が揃ってからで、その時点は起動状態次第で後ろにずれます。この前提が崩れたときに動かなくなります。 この記事で組み立てた実装は、「いつ実行するか」を予測する命令的な発想から、「実行してよい状態か」を評価し続ける宣言的な発想へと転換するものです。要求をクリア可能な状態として保持し、前提条件が揃いうる全タイミングに冪等なトリガーを配置し、起動情報を ViewModel より生存期間の長いオブジェクトに保持させて配信します。いずれも、一過性の命令的要求を SwiftUI が扱える宣言的な状態へ翻訳する、という同じ発想の現れです。命令を宣言的な状態に落とし込めば、その状態は SwiftUI の評価サイクルに自然に組み込まれ、UI の更新と同じ仕組みで実行タイミングが解決されます。 これらを組み合わせることで、発生元もタイミングも制御できない外部起動に対し、レイテンシや遷移順序の変動があっても正しく動作し、かつ拡張容易な実装が得られます。これはプッシュ通知・ディープリンク・外部認証コールバックといった非同期な外部トリガーを宣言的 UI の上で扱う、あらゆる場面に適用しうるパターンです。
はじめに こんにちは。開発本部でデリッシュキッチンの Android アプリ開発を担当している岡田です。 アプリを運用していると、リリースのたびに Crashlytics とにらめっこする時間が必ず発生します。クラッシュの一覧を眺めて、優先度を決めて、スタックトレースを読んで、該当コードを探して、原因を考えて、直す。やること自体は明確なのですが、ダッシュボードとエディタを行き来する手作業が地味に重く、件数が増えると後回しになりがちでした。 本記事では、公式の Firebase MCP サーバー が提供する Crashlytics ツールを Claude Code から使い、この一連の流れ(トリアージ → 原因分析 → 該当コード特定 → 修正 PR のドラフト作成)を半自動化した話を紹介します。特定の社内事例に依存しない、誰でも再現できる手順としてまとめました。Android / iOS で Crashlytics を使っていて、AI コーディングツールを業務に取り込みたい方の参考になればうれしいです。 目次 はじめに 背景:クラッシュ対応のどこが手間か Firebase Crashlytics MCP とは セットアップ 前提 Claude Code への MCP 登録 クラッシュ対応フロー:半自動化の中身 1. トリアージ:上位クラッシュを一覧する 2. 深掘り:スタックトレースと発生状況を取得 3. 突き合わせ:原因仮説と該当コードの特定 4. 修正と PR ドラフト作成 運用ノウハウ / つまづきポイント ツールは --only crashlytics で絞る CI / サービスアカウント認証では 404 に注意 プラットフォームによってツールが出ないことがある Experimental ゆえのバージョン固定 どこまで任せるか(“半” の線引き) まとめ 参考リンク 背景:クラッシュ対応のどこが手間か クラッシュ対応そのものは難しい作業ばかりではありません。むしろ「手間」の多くは、判断と判断のあいだにある移動コストでした。具体的には次のようなところです。 ツールの往復 :Crashlytics(ブラウザ)でクラッシュを確認し、エディタに戻って該当箇所を探し、また Crashlytics に戻って影響範囲を見る、という往復が多い。 スタックトレースとコードの突き合わせ :難読化されたスタックトレースから当たりをつけ、リポジトリの該当コードまで辿るのは、慣れていても地味に時間がかかる。 トリアージの属人性 :「どのクラッシュから手を付けるか」の判断が、見る人によってブレる。影響端末・OS バージョン・発生数を毎回手で確認するのも面倒。 この「往復」と「突き合わせ」を AI に任せられれば、人間は 原因の判断と最終レビュー に集中できます。そこで MCP の出番です。 Firebase Crashlytics MCP とは MCP(Model Context Protocol) は、AI クライアントに外部ツールやデータソースを接続するための共通規格です。Firebase は公式に Firebase MCP サーバー を提供しており、その中に Crashlytics 用のツール群が含まれています。 このサーバーを AI クライアントに繋ぐと、ダッシュボードを開かなくても、AI との対話の中でクラッシュデータを取得・更新できるようになります。対応クライアントは Claude Code / Claude Desktop / Gemini CLI / Cursor / VS Code Copilot など幅広く、本記事では Claude Code を使います。 公式ドキュメント時点で提供されている主な Crashlytics ツールは以下のとおりです。 ツール名 役割 crashlytics_get_report 期間や条件を指定してクラッシュレポート(上位 issue など)を取得 crashlytics_get_issue 特定の issue の詳細を取得 crashlytics_list_events issue に紐づくイベント(スタックトレース等)を一覧取得 crashlytics_batch_get_events 複数イベントをまとめて取得 crashlytics_update_issue issue の状態(クローズ等)を更新 crashlytics_list_notes / crashlytics_create_note / crashlytics_delete_note issue へのメモの参照・追加・削除 加えて、 crashlytics:connect という対話的なガイド付きワークフローも用意されており、「どのクラッシュを優先すべきか」といった相談ベースの使い方もできます。Google I/O 2026 では、これらを束ねた Crashlytics の Agent Skills も発表され、IDE から離れずにデバッグ支援を受けられる方向に進んでいます。 ⚠ 注意 :Crashlytics の MCP 機能は Experimental(実験的) です。SLA や非推奨ポリシーの対象外で、ツール名や挙動が変わる可能性があります。本記事の内容も執筆時点(2026 年 6 月)のものです。 セットアップ 前提 Node.js / npm(Firebase CLI のインストールに使用) Firebase CLI をインストールし、認証済みであること。Firebase MCP サーバーは Firebase CLI と同じ認証情報 を使います。未導入なら以下を実行します。 # Firebase CLI をインストール(未導入の場合) npm install -g firebase-tools # バージョン確認 firebase --version # ログイン(MCP サーバーはこの認証情報を使う) firebase login Claude Code への MCP 登録 インストール済みの Firebase CLI なら、MCP サーバーは次のコマンドで起動します。 firebase mcp --dir /path/to/your/project Claude Code には .mcp.json (プロジェクト直下)で登録するのが手軽です。クラッシュ対応用途であれば、後述の理由から --only crashlytics でツールを絞る のがおすすめです。 { "mcpServers": { "firebase": { "command": "firebase", "args": [ "mcp", "--only", "crashlytics", "--dir", "/path/to/your/project" ] } } } CLI から登録する場合は次の形でも同じことができます。 claude mcp add firebase -- firebase mcp --only crashlytics グローバルインストールを避けたい場合は、公式ドキュメントのとおり npx -y firebase-tools@latest mcp ... ( command を npx 、先頭の args に -y firebase-tools@latest )でも起動できます。 登録後、Claude Code で /mcp を実行し、 firebase サーバーと crashlytics_* ツールが認識されていれば準備完了です。 クラッシュ対応フロー:半自動化の中身 ここからが本題です。Claude Code に自然言語で依頼するだけで、ツール呼び出し → 分析 → コード修正 → PR ドラフトまでを通しでやってもらいます。フロー全体は次のイメージです。 1. トリアージ:上位クラッシュを一覧する まずは「今いちばん効くクラッシュ」を洗い出します。 直近 7 日間で発生数が多いクラッシュを上位 5 件、影響ユーザー数と OS バージョン分布つきで一覧にして。優先度の高い順に並べて。 Claude は crashlytics_get_report を呼び、結果を表にまとめてくれます。発生数だけでなく影響ユーザー数や端末分布まで一度に並ぶので、「まず何から直すか」をその場で判断できます。 2. 深掘り:スタックトレースと発生状況を取得 優先度の高い issue を 1 件選び、詳細を取りに行きます。 1 件目の issue の詳細を取得して、代表的なスタックトレースと、発生している端末・OS・アプリバージョンの傾向を教えて。 ここでは crashlytics_get_issue と crashlytics_list_events (必要に応じて crashlytics_batch_get_events )が呼ばれます。「特定 OS バージョンだけで起きている」「特定の画面遷移直後に多い」といった、原因の当たりをつけるための材料が揃います。 3. 突き合わせ:原因仮説と該当コードの特定 ここが手作業でいちばん面倒だった部分です。Claude Code はリポジトリのコードも読めるので、スタックトレースとソースを突き合わせてもらいます。 このスタックトレースに対応するコードをリポジトリから特定して、クラッシュの原因仮説を立てて。null 安全や lifecycle 周りの問題があれば指摘して。 スタックトレースの該当クラス・メソッドからソースの行まで辿り、「ここで nullable な値を !! で参照していて、特定条件で null になりうる」といった粒度で原因仮説を出してくれます。 4. 修正と PR ドラフト作成 原因に納得できたら、修正と PR まで一気に依頼します。 原因仮説に沿って修正案を作って。修正したらブランチを切って、変更内容と原因・対応を説明した PR をドラフトで作成して。 Claude Code がコードを修正し、 gh pr create --draft でドラフト PR まで作ってくれます。PR 本文には「どのクラッシュに対応したか」「原因」「修正内容」が整理された状態で入るので、レビュー依頼の手前まで一気に進みます。 運用ノウハウ / つまづきポイント 実際に動かすと、いくつか引っかかりやすい点があります。先に共有しておきます。 ツールは --only crashlytics で絞る Firebase MCP サーバーは Crashlytics 以外にも多くのツールを持っています。プロジェクトのデータ量が多いと、起動時のツール一覧取得( tools/list )でタイムアウトやメモリ不足が起きることが報告されています( firebase-tools #9663 )。クラッシュ対応に用途を絞るなら、最初から --only crashlytics を付けてツールを限定しておくのが安全です。動作も軽くなります。 CI / サービスアカウント認証では 404 に注意 ローカルの firebase login では問題なく動くのに、CI 環境でサービスアカウントや Workload Identity Federation(WIF)を使うと、 crashlytics_get_report が「HTTP 404, Method not found」で失敗するという報告があります( #10310 / #10004 )。同じ環境でも firebase login:ci で取得したトークンなら通る、という回避策が共有されています。CI に組み込む前提なら、認証方式の検証は早めにやっておくとよいです。 プラットフォームによってツールが出ないことがある 過去には iOS プロジェクトで Crashlytics ツールが認識されないケースも報告されています( #9495 )。ツールが見当たらないときは、 --dir で正しいプロジェクトを指していること、firebase-tools が最新であることをまず確認してください。 Experimental ゆえのバージョン固定 前述のとおり Crashlytics MCP は Experimental です。常に最新の firebase-tools を使っていると、ある日ツール名や引数が変わって手順が動かなくなる可能性があります。チームで共有するなら、 npm install -g firebase-tools@<version> のように firebase-tools のバージョンを固定して運用するほうが安定します。 どこまで任せるか(“半” の線引き) あくまで「半」自動化にとどめているのには理由があります。クラッシュの原因仮説は説得力があっても間違っていることがありますし、修正がほかの挙動に影響しないかの最終判断は人間の責任です。 今回の運用では、 Claude は提案者、人間は承認者 という役割分担を崩していません。PR をドラフトで作るところまでを AI に任せ、レビューとマージは必ず人が行います。トリアージと突き合わせという「重いけれど判断の少ない作業」を肩代わりしてもらい、人は判断に集中する——この線引きが、いまのところいちばん心地よく回っています。 まとめ 公式の Firebase MCP サーバーと Claude Code を組み合わせることで、クラッシュ対応のトリアージから原因分析、該当コードの特定、修正 PR ドラフト作成までを通しで半自動化できました。ダッシュボードとエディタの往復が減り、後回しにしがちだったクラッシュ対応の腰が軽くなったのが一番の効果です。 まだ Experimental な機能なので過信は禁物ですが、上位クラッシュの定期トリアージを仕組み化するなど、伸ばせる余地はまだまだありそうです。同じように Crashlytics 運用に手間を感じている方は、まず --only crashlytics で繋いでみるところから試してみてください。 最後まで読んでいただきありがとうございました! 参考リンク firebase.google.com firebase.google.com firebase.blog firebase.blog modelcontextprotocol.io
Goの import cycle not allowed を出しているのはコンパイラじゃない こんにちは、 uho-wq です。 早速ですが、以下のソースコードをビルドするとどのような結果になるでしょうか? # ディレクトリ構成 $ tree . . ├── a │ └── a.go ├── b │ └── b.go └── go.mod 3 directories, 3 files // ./a/a.go package a import ( "example.com/cycle/b" ) func A() string { return b.B() } // ./b/b.go package b import ( "example.com/cycle/a" ) func B() string { return a.A() } 答えは下記のようにパッケージの循環参照によるエラー ( import cycle not allowed )が出ます。 $ go build ./... package example.com/cycle/a imports example.com/cycle/b from a.go imports example.com/cycle/a from b.go: import cycle not allowed ビルドのログを見てもわかる通り、 a.go がパッケージ b を、 b.go がパッケージ a をimportしているため、依存関係が循環してしまっていることがわかると思います。 Go言語では同一パッケージ内ではファイル間の循環依存が許容されていますが、パッケージ間の循環依存は禁止されています。 では次に、Goにおけるどの処理がこのエラーを吐いていると思いますか? 答えはコンパイラ ( cmd/compile )、ではなくgoコマンド ( cmd/go )なのです。 実際にエディタで確認して見ると、LSPのdiagnosticsがエラーを吐いていたので、コンパイルする前の段階で検知されていることが確かにわかります。 LSP diagnostics 2026年3月にThe Go Blogで公開された Type Construction and Cycle Detection では、Goのコンパイル時における型チェックについて言及されていました。 ただし、この型チェックはGoコンパイラ内で行われているため今回言及したいパッケージの循環参照とは別の仕組みであるということがわかります。 この記事ではGo 1.26.0のソースコードを実際に辿りながら、 cmd/go でどのように import cycle not allowed を検出しているのかを追っていきます。 importパスを取得する パッケージが循環参照しているかを検証するために、まず import "..." の宣言内容を取得する必要があります。 このimportの宣言取得はAST (抽象構文木)によって行われます。 import 宣言は構文解析されると ast.ImportSpec という AST ノードになり、パス文字列は (*ast.ImportSpec).Path.Value に入ります。 go/build はこの AST を走査して import パスのスライスを集めます。 // src/go/build/read.go at readGoInfo info.parsed, _ = parser.ParseFile(info.fset, info.name, info.header, parser.ImportsOnly|parser.ParseComments) for _, decl := range info.parsed.Decls { d, ok := decl.(*ast.GenDecl) if !ok { continue } for _, dspec := range d.Specs { spec, ok := dspec.(*ast.ImportSpec) // ← import 宣言の AST ノード if !ok { continue } path, _ := strconv.Unquote(spec.Path.Value) info.imports = append (info.imports, fileImport{path, spec.Pos(), doc}) } } ちなみに (*ast.ImportSpec).Path.Value の importパスにはダブルクォーテーション ( " )が含まれていますが、 strconv.Unquote で削除しています。 依存を探索する importパスのスライスが取得できたので、ここからは実際にimportパスを起点に探索を行います。 コアとなるロジックは cmd/go/internal/load/pkg.go の以下の関数、メソッドです。 loadImport : import を1つ解決して、その先のパッケージを返す関数。初めて見るパッケージなら (*Package).load を呼ぶ (*Package).load : パッケージを1つ読み込む関数。そのパッケージの全 import を順に loadImport で解決し、最後に依存リストを確定する これらが再帰的に呼び合うことによって、importパスの依存関係を深さ優先探索 (Depth-First Search: DFS)で辿っていきます。 とは言ったものの、少し複雑で分かりづらいので実際のソースコードから追っておきます。 下記は (*Package).load のソースコードの抜粋です。 // src/cmd/go/internal/load/pkg.go at (*Package).load imports := make ([]*Package, 0 , len (p.Imports)) for i, path := range importPaths { p1, _ := loadImport(..., path, ...) imports = append (imports, p1) ... } p.Internal.Imports = imports importパスごとに loadImport を呼び、解決し終えたimportsのスライスを最終的に p.Internal.Imports へ格納していることがわかります。 では次に loadImport を見てみましょう。 // src/cmd/go/internal/load/pkg.go at loadImport func loadImport(..., path string , ..., stk *ImportStack, ...) (*Package, *PackageError) { bp, _, err := loadPackageData(...) importPath := bp.ImportPath p := packageCache[importPath] // キャッシュを引く if p != nil { stk.Push(...); p = reusePackage(p, stk); stk.Pop() // 循環依存判定 } else { p = new (Package); p.ImportPath = importPath packageCache[importPath] = p // キャッシュ登録 p.load(..., bp, err) // 再度 (*Package).load を呼ぶ } ... return p, nil } ここではキャッシュ ( packageCache )が用いられていることがわかります。 このキャッシュはメモ化と循環依存の判定という2つの用途を持ちます。 メモ化: ロード済みパッケージの再ロードを防ぐ 循環依存判定 ( reusePackage ): キャッシュヒットしたパスがスタックしたパスのいずれかに戻ってきたかを判定する キャッシュヒットしなかった場合には再度 (*Package).load を呼ぶことによって再帰的な探索を行っています。 循環依存判定 キャッシュヒットしたパスに対して reusePackage によって循環依存判定を行います。 // src/cmd/go/internal/load/pkg.go (reusePackage) func reusePackage(p *Package, stk *ImportStack) *Package { // We use p.Internal.Imports==nil to detect a package that // is in the midst of its own loadPackage call ... if p.Internal.Imports == nil { // この部分 if p.Error == nil { p.Error = &PackageError{ ImportStack: stk.Copy(), Err: errors.New( "import cycle not allowed" ), IsImportCycle: true , } } p.Incomplete = true } return p } p.Internal.Imports は loadImport と (*Package).load が相互再帰的に呼び出している都合上、起点となるimportパスに対して最も深い依存関係まで探索して初めて非nilになります。 今回例にあげた循環依存は a → b → a の経路を辿るので、最初の a に対しての探索が解決する前 ( p.Internal.Imports == nil )に再度 a がキャッシュヒットしたため import cycle not allowed として考えることができるわけです。 ちなみに、辿った経路はすべて ImportStack にスタックとして保持されています。 // src/cmd/go/internal/load/pkg.go type ImportStack []ImportInfo func (s *ImportStack) Push(p ImportInfo) { *s = append (*s, p) } func (s *ImportStack) Pop() { *s = (*s)[ 0 : len (*s)- 1 ] } func (s *ImportStack) Copy() ImportStack { return slices.Clone(*s) } 循環依存のエラーを返す際に、 ImportStack をPackageErrorに含めて返すことによって、ビルドのエラーログに経路を含めて表示する事が可能となっています。 ❯ go build ./... package example.com/cycle/a imports example.com/cycle/b from a.go imports example.com/cycle/a from b.go: import cycle not allowed おわりに ここまで import cycle not allowed の出どころについて追ってきました。エラーを吐いていたのはコンパイラ ( cmd/compile ) ではなく go コマンド ( cmd/go ) でした。 仕組みは、 go/build が AST から import パスを取り出し、 loadImport と (*Package).load が相互再帰で深さ優先探索し、探索中に同一パッケージがキャッシュヒットした場合に循環参照として判定していることがわかりました。
はじめに こんにちは、開発1部で食事管理アプリヘルシカの開発をしている新谷です。 ダイエット・食事管理・体重管理・カロリー計算 - ヘルシカ every, Inc. ヘルスケア/フィットネス 無料 apps.apple.com 今回は、iOSアプリの広告効果がどう計測されているのかを、アトリビューションツール Adjust を通して調べてみました。 iOS開発を始めて半年ほど経ちましたが、Adjustは広告効果を測定するためのツール程度の認識しかなく、改めて仕組みを理解してみようと思ったのがきっかけです。正直、すべてを細かく追えたわけではないのですが、調べて分かった範囲を整理してみます。 広告アトリビューションの全体像 そもそもアトリビューションとは、ユーザーがどの広告経由でアプリをインストール・利用したかを特定することです。どの広告が効果的だったかを知るために使われます。 Adjustで広告効果を計測するとき、計測は大きく2つの系統に分かれます。 Adjust自身のアトリビューション。クリックやインプレッションとインストールを突き合わせて判定します。ATT(App Tracking Transparency)の同意状況やネットワークの種類によって、判定手法が変わります。 SKAdNetworkによる計測。OSが匿名で計測する仕組みで、ユーザーの同意がなくても広告経由のインストールを計測できます。Adjustでは、自前のアトリビューションとは別のデータとして扱われます( Adjust: SKAdNetwork )。 この2つは別の経路です。前者はユーザー単位の識別子であるIDFA(Identifier for Advertisers)を使えるかどうかが鍵になり、後者はOSが匿名で計測するため同意がなくても動きます。以降、まず前提となるIDFAとATTを確認してから、それぞれの計測を見ていきます。 IDFAとATTについて iOSの広告計測の中心にあるのが IDFA です。これはデバイスごとの広告用IDで、これを使うと、どの広告を見たユーザーがインストールしたかをユーザー単位で追跡できます。 iOS 14.5以降、このIDFAへのアクセスには ATT による明示的な同意が必須になりました。ATTは、アプリがユーザーを追跡する前に同意ダイアログで許可を求めることを義務づける、Appleの仕組みです。 同意状況は ATTrackingManager.trackingAuthorizationStatus から取得でき、値は次の4つです( Apple: ATTrackingManager.AuthorizationStatus )。 notDetermined : まだ同意ダイアログを出していない状態 authorized : ユーザーが許可した状態 denied : ユーザーが拒否した状態 restricted : ユーザーが同意・拒否を選べない状態(スクリーンタイムなどで制限されている場合) 許可が得られない場合、IDFAはすべてゼロのダミー値になり、ユーザー単位の追跡はできません。 1. Adjustのアトリビューション手法 Adjustのようなアトリビューションツール(MMP: Mobile Measurement Partner)は、広告のクリックやインプレッションとインストールを突き合わせて、どの広告が効いたかを判定します。この判定の手法が、ATTの同意状況とネットワークの種類によって変わります。 1-1. ATTの同意がある場合 ATTの同意が得られていてIDFAが読める場合、Adjustは確定的アトリビューションを使います。広告のクリックやインプレッションの時点と、インストールの時点でそれぞれ一意のIDを収集し、両者が一致すればそのインストールを広告に結びつける、というデバイスマッチングの手法です。Adjustの説明では、クリックベースのものは「精度が100%で、最も信頼性の高いアトリビューション」とされています( Adjust: アトリビューション手法 )。 Adjustが使う識別子は主に次のものです。 広告ID(iOSのIDFA): ユーザーがリセット・制限できる、広告用のID デバイスID(iOSのIDFV、Identifier for Vendor): デバイスに永続的に紐づくID。アトリビューション目的でのみ使われる Adjustのリファレンスタグ: クリックやインプレッションごとに生成される一意のID SANと非SANで仕組みが違う 同じ確定的アトリビューションでも、ネットワークの種類によって、誰が突き合わせを行うかが変わります。広告ネットワークは、Google・Meta・TikTokなどの大手で自前でアトリビューションを行う SAN(Self-Attributing Network、自己アトリビューションネットワーク)と、それ以外の通常のネットワーク(非SAN)の2種類に分かれます( Adjust: Self-Attributing Network )。 SAN: クリックやインプレッションのデータを自分からは共有しません。代わりに、AdjustがSDK経由で取得した広告IDを送ってくるのを待ち、自社が持つデータと突き合わせて判定します。 非SAN(通常のネットワーク): 広告IDを含むクリックやインプレッションのデータを、Adjustのようなアトリビューションパートナーに送ります。Adjustがそれをインストールと突き合わせて判定します。 つまり、SANはAdjustからネットワークへ、非SANはネットワークからAdjustへと、データを送る向きが逆になります。突き合わせを行うのも、SANはネットワーク側、非SANはAdjustです。 図にすると、それぞれこうなります。 SAN(ネットワークが突き合わせる) 非SAN(Adjustが突き合わせる) 1-2. ATTの同意がない場合 ATTで同意が得られないとIDFAはゼロ値になるため、IDFAを使った確定的アトリビューションはできません。この場合の扱いは、ネットワークの種類で分かれます。 非SAN(通常のネットワーク)の場合 非SANでは、その代わりに確率的モデリング(デバイスIDを使わず統計的に推定する手法)やSKAdNetworkが使われると、Adjustのドキュメントで説明されています( Adjust: ATTフレームワーク )。 SANの場合(Appleを除く) SAN(Appleを除く)では、ネットワーク側がクリックなどのデータをAdjustに渡さないため、非SANのような確率的モデリングは使えません。Adjustのドキュメントでも「Apple Search Adsを除き、ユーザーの流入元としてAPI連携パートナーに紐付けることに制限がある」とされています( Adjust: ATTフレームワーク )。また、IDFAの共有にオプトインしていないiOSデバイスのインストールについては、SKAdNetworkの集計データを使って計測することになると説明されています( Adjust: Self-Attributing Network )。 1-3. Apple広告の場合 Apple広告(Apple Search Ads)だけは、他のネットワークとは別の経路で計測されます。AppleのAdServices frameworkと、サーバー側から呼ぶAPIを組み合わせた Apple Ads Attribution API という仕組みです( Apple: AdServices )。 Appleのドキュメントでは、アトリビューション取得の流れが次のように説明されています。 アプリが AdServices framework にトークンをリクエストする framework がトークンを生成して返す そのトークンを使って、APIでAppleのアトリビューションサーバーにリクエストする キャンペーンに対応するアトリビューション情報( campaignId や adGroupId 、 keywordId など)が返る Adjust SDKを使う場合は、この一連のやり取りをSDKが担います。 レスポンスは、ATTの同意状況によって2つのタイプに分かれます( Adjust: Apple Adsアトリビューションの仕組み )。 詳細(Detailed): ATTに同意したユーザー向け。エンゲージメントの時刻(分単位)まで含む 標準(Standard): 同意しなかったユーザー向け。時刻は含まれない このように、Apple広告はATTに同意がなくても標準レスポンスで計測でき、同意があれば時刻まで含む詳細なデータが得られます。 2. SKAdNetworkでの計測 ここまではAdjust自身が行う計測でした。もう1つの系統が、OSが提供するSKAdNetworkです。 SKAdNetworkはiOS 11.3(2018年)から提供されています。ユーザー個人を特定せずに広告の成果を計測できるよう、Appleがインストール計測をOS側で引き受ける仕組みとして導入しました。iOS 14.5のATTでIDFAの利用が制限されてからは、同意がなくても成果を計測できる手段として重視されるようになったようです。 SKAdNetworkの計測には、Adjust自身のアトリビューションとは違ういくつかの特徴があります( Adjust: SKAdNetwork )。 ユーザーの同意がなくても動く(OSが匿名で計測するため) Adjustが記録するデバイスレベルのデータとは別物として扱われる リアルタイムではなく、最短でも24時間の遅延がある インストール後の行動はコンバージョン値(0〜63の小さな数値)として表現される おおまかな流れは、Appleが最終的なコンバージョン値を確定し、広告ネットワークに通知し、ポストバック(成果を知らせる通知)が送られる、というものです。 なお、SKAdNetworkで計測するには、アプリ開発者が、利用する広告ネットワークのIDを Info.plist の SKAdNetworkItems に登録しておく必要があります( Apple: SKAdNetworkItems )。Adjust SDK自体にはSKAdNetworkのサポートが含まれており、コンバージョン値はAdjustの管理画面で設定したマッピングに従って処理されます( Adjust: SKAdNetwork )。 そのうえで、Adjustがこのポストバックを受け取る経路には、2つのパターンがあります。 2-1. ネットワーク経由のポストバック 1つ目は、広告ネットワークを経由する経路です。Appleが最終的なコンバージョン値を確定すると、まず広告ネットワークに通知が届きます。その通知を受けた広告ネットワークが、Adjustにデータを含むポストバックを送ります( Adjust: SKAdNetwork )。 この経路でAdjustが受け取れるデータは、広告ネットワークが提供する内容に左右されます。Adjustは複数の広告ネットワークから届いたSKAdNetworkのデータを集約してレポートします。 2-2. SKAdNetworkから直接届くポストバック 2つ目は、SKAdNetworkからAdjustに直接ポストバックが届く経路です。iOS 15以降、SKAdNetworkは獲得したポストバックを、広告ネットワークのエンドポイントに加えて、アプリ側で指定した追加のURLにも直接送れるようになりました( Adjust: SKAdNetwork ダイレクトインストール )。このURLにAdjustのエンドポイントを指定しておくと、端末のSKAdNetworkからAdjustへ直接ポストバックが届きます。 この経路を使うには、アプリ開発者が Info.plist にAdjustのコールバックURLを追加する必要があります。 この経路の利点は、コンバージョン値・トランザクションID・アトリビューション署名などを含む完全なデータセットがそのまま届くことです。一方で、FacebookやGoogle広告など一部のパートナーからはトランザクションIDや署名が得られないことがあり、その場合は詳細なデータの比較ができず、ポストバック数を比べるにとどまります。 まとめ iOSアプリの広告効果がどう計測されているのかを、Adjustを通して調べました。 調べてみると、ATTの同意状況とネットワークの種類で手法が細かく分岐していて、想像よりずっと込み入っていました。 広告計測はマーケティング寄りの領域で、普段エンジニアはあまり関わらないかもしれませんが、仕組みを知っておくと役立つ場面がありそうだと感じています。同じように広告計測まわりを調べている方の参考になればうれしいです。
目次 はじめに セッション「業務に残された良くない型で考える TypeScript の限界」 自分のプロジェクトを点検してみた まずは網羅的に数える 「どう対策するか」で分類する 実際に直してみた ユニオン配列の絞り込み 点検してわかったこと おまけ: TSKaigi 2026 を楽しんだ話 おわりに はじめに こんにちは、デリッシュキッチンでフロントエンドの開発をしている惟高です! 先日、TypeScript のカンファレンス「TSKaigi 2026」に参加してきました。 イベント全体の様子やブースの企画、各セッションの紹介は弊社の最速レポートにまとめているので、当日の雰囲気が気になる方はこちらもあわせてご覧ください。 tech.every.tv 本記事では、その中でも特に印象に残った sajikix さんのセッション「業務に残された良くない型で考える TypeScript の限界」を取り上げます。 speakerdeck.com 実務のコードに残ってしまう「良くない型」との向き合い方を扱ったセッションでした。聞きながら「自分が普段書いているコードにも、こういう型はたくさんあるな」と思い、気になったので、同じやり方で自分のプロジェクトを点検してみました。 セッション「業務に残された良くない型で考える TypeScript の限界」 セッションは、業務で書いてしまった「良くない型」や「妥協した型」を収集・分類し、パターン化・分析することで、TypeScript の難しさの一端に迫る、という内容でした。 セッションでは、「良くない型」の探し方も具体的に示されていました。AI を使って次の観点を網羅的に洗い出し、分類したうえで、分類ごとに実際にコードを見て判断する、という進め方です。 @ts-ignore / @ts-expect-error (コンパイルエラー自体を黙らせる指定) as キャスト(型アサーション。値を「この型だ」と言い切る。 as const や as unknown as は除外) 型ガード関数 value is X (値の型を絞り込む関数) セッションを聞いて、普段触っているプロジェクトに当てはめるとどんな結果になるのか気になり、実際にやってみました。 自分のプロジェクトを点検してみた 対象は、私が普段関わっているフロントエンドのコードベースです。TypeScript 4.5.5 / Next.js 12 / React 17 という構成です。 まずは網羅的に数える セッションの探し方にならい、3つの観点を網羅的に抽出しました。結果は次のとおりです。 観点 件数 @ts-ignore / @ts-expect-error 0 件 型アサーション as ( as const / as unknown as を除外) 104 件 型ガード関数 value is X 1 件 この分布を見ると、型まわりで使われている手段が as にかなり偏っているのがわかります。 @ts-ignore は使われておらず、型ガード(型述語)も1件だけでした。 こうした as は、誰か特定の人の問題ではなく、日々の開発や当時の TypeScript の制約のなかで自然に積み重なっていくものだと思います。 なお、単純に as を文字列検索すると、 export { default as Foo } のような 再エクスポート (型キャストではない構文)まで引っかかってしまいました。こうした型キャストではないものを AI に分類させて除き、「型アサーションとしての as は104件」という数字を出しました。 「どう対策するか」で分類する 次に、見つけた104件をどう分類するかです。セッションでは「どう対策するか」を起点に、次の2つの軸が紹介されていました。 境界で起きているのか / 内部で起きているのか : 外部(ライブラリ・外部データ・DOM など)との境界に由来するものか、自分たちのコード内部で起きているものか 安全圏に押し戻せるか : 型ガードやヘルパーで型安全に直せるか、それとも難しいか この2軸で104件を仕分けすると、次のようになりました。 押し戻せる 難しい 境界 約76件(外部データ・フォームライブラリ・DOM など) 1件 内部 約22件(配列の絞り込み・冗長な as など) 数件(動的なキーアクセス) Note : ライブラリの型を扱う箇所など境界と内部をまたぐパターンもあり、件数は概数です。 7割超が「境界 × 押し戻せる」に集中しました。つまり、 外部との境界で型が緩くなった箇所を as で握っていて、しかもその多くは型ガードやヘルパーで直せる 、ということです。ここからは、内部起因の as として型ガードで押し戻せる例を見ていきます。 実際に直してみた ここで紹介するコードは、社外公開のため型名や処理を一般化しています。また TypeScript 4.5.5 で型チェックが通ることを確認しています。 型ガードを追加することで TypeScript の型追跡が効くようになる例を見ていきます。 ユニオン配列の絞り込み string と number が混在しうる配列を、「全部 string の配列」として型を絞って扱いたい箇所がありました。 if (typeof answers[0] === 'string') { return (answers as string[]).every((a) => target.includes(a)); } as string[] はコンパイル時に型を string[] として扱わせる指定で、実行時には何も起きません。残りの要素に number が混ざっていても検出できず、TypeScript もこのチェックから配列全体の型を絞ることができません。 直し方は2つあります。 型述語(type predicate) : 型ガード関数を追加するだけで、既存のデータ構造を変えずに as を外せます。 const isStringArray = (a: (string | number)[]): a is string[] => a.every((x) => typeof x === 'string'); if (isStringArray(answers)) { return answers.every((a) => target.includes(a)); // a は string に絞られる } isStringArray が全要素を確認してから a is string[] を返すので、TypeScript は if ブロック内で answers が string[] だと追跡できます。 discriminated union(判別可能なユニオン) : データ構造にタグを持たせる設計変更が必要ですが、TypeScript が kind の値を見るだけで型を絞れます。 discriminated union: 共通のタグ(ここでは kind )を持たせたユニオン型。タグの値を見るだけで、どのメンバーかを型レベルで絞り込めます。 type Answers = | { kind: 'string'; values: string[] } | { kind: 'number'; values: number[] }; if (answers.kind === 'string') { return answers.values.every((v) => target.includes(v)); // v は string に絞られる } as string[] は TypeScript に型を宣言するだけで実行時の保証はありませんでしたが、型述語は全要素の確認、discriminated union はデータ構造の設計によって、TypeScript が型を正しく追跡できる状態になります。 点検してわかったこと 104件を一通り見て、いくつかのことがわかりました。 まず、セッションスライドで具体的に紹介されていたパターン(DOM Event / catch(e) / unknown / Branded型 / Array標準メソッド)は、このリポジトリにはほとんど存在しませんでした。代わりに多かったのは as string が71件で、大半はオプショナルプロパティに対して undefined の可能性を as で除いているものでした。 型ガードを追加することで押し戻せる例(union 配列の絞り込み)も見つかりました。一方、フィールド名が外部データで動的に決まるフォームのように、現在の設計では型安全にしきれない箇所も残っています。そうした as については理由と改善の見通しをコメントで残しておく、という方針が考えられます。 おまけ: TSKaigi 2026 を楽しんだ話 セッションや技術的な学び以外にも、今回は純粋にイベントを楽しめました! 各社のスポンサーブースを巡って担当者の方から直接お話を聞けたのも良かったです。エブリーのブースでは、足を運んでいただいた方々から AI の活用事例をいろいろ聞かせていただき、各現場での取り組みがとても刺激的でした。 お弁当も美味しく、ノベルティもたくさんいただいて大満足な2日間でした!TypeScript についてじっくり向き合える機会はなかなかないので、こういったカンファレンスに定期的に参加することの大切さを改めて実感しました。 ノベルティ一覧 おわりに セッションで学んだ探し方を実際に自分のコードに当てはめることで、漠然と as を使っていた箇所が可視化されました。どこに型ガードが必要なのかが具体的に見え、型アサーションを型システムと向き合うきっかけにしていきたいと思います。 最後までお読みいただき、ありがとうございました!
はじめに こんにちは。リテールハブ開発部小売アプリチームの池です。 業務で Laravel Octane のメモリが残る挙動について調査する機会がありました。 Laravel Octane は、長時間稼働するプロセス上で Laravel アプリケーションを動かして高速化するツールです。便利な一方で、プロセスが長く生きるためメモリが残り続け、書き方次第ではリクエスト間で状態が引き継がれてしまうという、従来の Nginx + PHP-FPM 構成の Laravel では発生しにくい特性を持っています。この特性を理解せずに使うと予期しない事故につながる可能性があると感じました。 そこで本記事では、Octane + Swoole の仕組みを整理した上で、サンプルプログラムで挙動を検証し、Worker プロセスが常駐することに起因して気をつけるべきポイントについて整理したいと思います。 Laravel および Octane について多少の知識がある方を前提に書いており、Laravel 本体の解説等には触れません。 なお、本記事の内容は一次情報から確認するように努めていますが、私の理解違いや Octane / Swoole のバージョン差による挙動の違いが含まれている可能性があります。誤って実装すると事故につながり得る領域でもあるため、最終的にはご自身でソースコードや公式ドキュメントをご確認の上で適用ください。 仕組みの整理 この章では、Octane + Swoole で前のリクエストの情報が次のリクエストに残る仕組みを、次の 4 つの観点から整理します。 Octane + Swoole では Worker プロセスが常駐するため、PHP プロセス内のメモリがリクエストごとに初期化されない Octane はリクエストごとに $this->app を clone して $sandbox 上で処理することで、ベースのアプリインスタンスを直接書き換えないようにしている ただし PHP の clone はシャローコピーなので、共有されたオブジェクトの内部状態はリクエスト間で残り得る Octane は RequestReceived イベントに紐づくリスナー群でフレームワーク側の状態をリセットしている 本記事に登場するアーキテクチャ図やライフサイクル図は必要な要素に絞って簡略化しています。 1. Worker プロセスが常駐するため、メモリは初期化されない Octane + Swoole は、長時間生きる PHP プロセスを立ち上げる仕組みです。 Swoole のプロセスアーキテクチャ Worker は OS プロセスです。Manager から複数生成され、一定数のリクエストを処理するか停止シグナルを受信するまで走り続けます。 Worker のライフサイクル 長時間生き続ける Worker プロセスは次のように動きます。 注目したいのは、1 つの Worker プロセスが生き続けたまま複数のリクエストを順に処理し続ける構造です。Worker 起動時に 1 回だけ Laravel アプリケーションを組み立てて $this->app に保持し、その後はリクエストのたびにこの $this->app を clone して $sandbox として使い回します。この「同じ $this->app が複数リクエストにまたがって使われる」点が、後で見る「前のリクエストの情報が次のリクエストに残ってしまう」仕組みの一部になっています。 Worker 起動時に Laravel アプリケーションを組み立てている処理 Worker::boot() の実装は以下の通りです。 <?php // vendor/laravel/octane/src/Worker.php public function boot ( array $ initialInstances = []) : void { // ベースとなる Laravel アプリインスタンスを1つ生成 // 以降、リクエストのたびにここから clone する $ this -> app = $ app = $ this -> appFactory -> createApplication ( ... ) ; $ this -> dispatchEvent ( $ app , new WorkerStarting ( $ app )) ; } なぜメモリが残るのか Swoole + Octane では以下の仕組みでメモリが残ります。 Worker プロセスが生き続けるため、リクエスト終了時に変数・オブジェクトに割り当てられたメモリが解放されない 結果として、 $this->app を含む変数・オブジェクトがリクエストをまたいでメモリに残り続ける ここから先は、この「メモリに残り続ける $this->app をリクエストごとにどう扱うか」ということに焦点を当てます。 2. Octane はリクエストごとに $this->app を clone する Worker が常駐すれば $this->app も残ります。ただ、リクエスト処理の中で $this->app を直接書き換えてしまうと、その変更が次のリクエストにそのまま残ります。Octane はこれを避けるために、リクエストのたびに $this->app を clone して $sandbox を作り、その上でリクエスト処理を回す設計になっています。 <?php // vendor/laravel/octane/src/Worker.php public function handle ( Request $ request , RequestContext $ context ) : void { // アプリインスタンスを clone してリクエスト用の sandbox を作る CurrentApplication :: set ( $ sandbox = clone $ this -> app ) ; $ gateway = new ApplicationGateway ( $ this -> app, $ sandbox ) ; try { $ response = $ gateway -> handle ( $ request ) ; // ... レスポンス返却 ... } finally { $ sandbox -> flush () ; // sandbox 側の bindings クリア unset ( $ gateway , $ sandbox , ... ) ; CurrentApplication :: set ( $ this -> app ) ; // 元のアプリインスタンスに戻す } } ここでの clone の役割は、リクエスト処理を $sandbox 側に隔離して、ベースの $this->app を直接書き換えないようにすることです。 $this->app 自体は Worker 寿命までずっと生き続けますが、毎リクエストの処理が $this->app を直接書き換えなければ、結果としてベースを変更せずに使い回せる、という設計になっています。 3. clone はシャローコピーなので、内部状態はリクエスト間で残り得る ただし、PHP の clone はシャローコピーであるため、前節の「ベースを書き換えない」が成り立つ範囲には限界があります。 Application オブジェクト自体は新規(リクエストごとの器) 配列プロパティ( bindings / instances など)はコピーされる(sandbox 側で書き換えても元には反映されない) 配列の中身(実際のオブジェクト)は元のアプリ $this->app と $sandbox で共有される これにより、以下のような挙動になります。 例えばリクエスト中に app()->instance('request', $req) のように差し替えても、 $sandbox 側にのみ反映され、ベースの $this->app には反映されない 一方で、ベースの $this->app に登録された解決済み singleton インスタンスは両者で共有されたまま 「singleton にリクエスト固有データを入れると次のリクエストにも残る」という現象は、この「clone してもオブジェクト自体は共有される」ことが要因と考えられます。 clone はコンテナの配列レベルでの隔離は提供するものの、配列の中に入っているオブジェクトの内部状態までは守ってくれない、というのがポイントです。 4. Octane の RequestReceived リスナーが一部の状態をリセットする clone だけでは配列の中に入っているオブジェクトのプロパティ書き換えを防げないため、Octane はそこを、リクエストごとに発火するイベントとそれに紐づくリスナーで、明示的に状態をリセットすることで補っています。 Worker のメインループ Worker のメインループの中では RequestReceived イベントが発火し、デフォルトで 8 個のリスナーを順に実行してから HTTP Kernel に処理を渡します。 Worker 起動 ↓ WorkerStarting イベント ↓ ┌── メインループ ─────────────────────────────────────────┐ │ RequestReceived イベント ─→ [8 listeners] │ │ ├─ FlushLocaleState │ │ ├─ FlushQueuedCookies │ │ ├─ FlushSessionState │ │ ├─ FlushAuthenticationState │ │ ├─ EnforceRequestScheme │ │ ├─ EnsureRequestServerPortMatchesScheme │ │ ├─ GiveNewRequestInstanceToApplication │ │ └─ GiveNewRequestInstanceToPaginator │ │ ↓ │ │ HTTP Kernel (Middleware → Controller) │ │ ↓ │ │ リクエスト終了 │ └─────────────────────────────────────────────────────────┘ ↓ max_requests に達したら Worker 再起動 これら 8 個のリスナーは、Locale / Cookie / Session / Auth といったフレームワーク状態のリセットや、 app('request') の差し替え、HTTPS スキームやポートの整合性チェックなどを担います。 実装の中でも、特に挙動を把握しておきたい 2 つを確認します。 まず、認証ガードを毎リクエスト破棄する FlushAuthenticationState を見てみます。 Laravel の AuthManager は内部で Guard インスタンスを $guards 配列にキャッシュし、各 Guard はさらに認証済みユーザーをプロパティに保持します。Octane でこのインスタンスをクリアしないと、 singleton で起きる現象と同じ構造で、前のリクエストの認証ユーザーが次のリクエストに引き継がれてしまいます。 FlushAuthenticationState は、このキャッシュを毎リクエスト破棄することで、前のリクエストの情報が次のリクエストに残らないようにしています。 実装は以下の通りです。 <?php // vendor/laravel/octane/src/Listeners/FlushAuthenticationState.php class FlushAuthenticationState { public function handle ( $ event ) : void { if ( $ event -> sandbox -> resolved ( 'auth.driver' )) { $ event -> sandbox -> forgetInstance ( 'auth.driver' ) ; } if ( $ event -> sandbox -> resolved ( 'auth' )) { with ( $ event -> sandbox -> make ( 'auth' ) , function ( $ auth ) use ( $ event ) { $ auth -> setApplication ( $ event -> sandbox ) ; $ auth -> forgetGuards () ; }) ; } } } auth がコンテナで解決済みの場合に forgetGuards() を呼びだし、Guards のキャッシュをクリアしていることがわかります。 次に、Request インスタンスを差し替える GiveNewRequestInstanceToApplication の実装は以下の通りです。 <?php // vendor/laravel/octane/src/Listeners/GiveNewRequestInstanceToApplication.php class GiveNewRequestInstanceToApplication { public function handle ( $ event ) : void { $ event -> app -> instance ( 'request' , $ event -> request ) ; $ event -> sandbox -> instance ( 'request' , $ event -> request ) ; } } app('request') を新しい Request インスタンスに差し替えます。 app('request') を呼ぶコードが常に最新の Request を見られるのは、このリスナーの働きによるものと理解できます。 何が残って、何が消えるのか 2 つのレイヤーに分けて整理します レイヤー 何が常駐するか リセット手段 Worker プロセス全体 Worker プロセス自体 + 関数テーブル / クラステーブル / static 変数 / グローバル変数 octane:reload / max_request 到達による Worker 再起動 Laravel Application ( $this->app ) bindings、singleton インスタンス、boot 済み ServiceProvider RequestReceived リスナー(部分的) なお、各 Worker は独立した OS プロセスであるため、Worker 間のメモリは分離されています。コルーチン無効時には同じ Worker 内のリクエストも順次処理されるため、気にすべきは「同じ Worker の中で、前のリクエストのデータが次のリクエストに引き継がれてしまわないか」という点に絞られると考えられます。 そのうえで、Laravel アプリコード上でよく使う状態保持の方法ごとに、同じ Worker・別リクエストでどう振る舞うかを整理すると次のようになります。 状態保持の方法 同 Worker・別リクエスト static 変数 残る グローバル変数 / $GLOBALS 残る Worker boot 時点などで解決済みの singleton インスタンスのプロパティ 残る 通常 bind (毎回新規) 毎回新規 scoped バインディング 次のライフサイクル開始時に flush $request->attributes Request 自体が新規生成 「残る」となっている static / グローバル変数 / singleton プロパティは、リクエスト固有のデータや、リクエストごとに増え続けるデータを置くと事故につながり得る点に注意が必要です。アプリ起動時に 1 度だけ初期化されるような不変なデータや、Worker 内で意図的に共有したいデータを置く分には問題ないと考えています。 ここまでは仕様上こうなっているはず、という整理でした。次は簡易的なプログラムで動作を検証します。 検証 仕組みの整理で示した観点を、実機で順に確認していきます。 static 変数 / グローバル変数がリクエスト間で残ること $this->app の内部状態(singleton インスタンスのプロパティ)がリクエスト間で残ること RequestReceived リスナーが一部の状態を実際にリセットすること 加えて、これらの検証が成立する前提として「同 Worker 内ではリクエストが順次処理される」ことを最初に確認します。 検証環境 検証は macOS 上のローカル Docker コンテナで、以下のバージョン構成で行います。 ソフトウェア バージョン Laravel 12.59.0 Octane 2.17.3 Swoole 6.2.1 Octane は次のコマンドで起動します。 php artisan octane:start --server=swoole --workers=10 --max-requests=500 このとき内部で適用される主な Swoole オプションは次の通りです。 設定 値 由来 enable_coroutine false Octane デフォルト(Laravel 本体がコルーチンセーフでないため意図的に無効化) worker_num 10 --workers=10 で指定 max_request 500 --max-requests=500 で指定(メモリリーク対策) 特別な設定はしておらず、Octane / Swoole のデフォルトのままです。 検証用ルートは routes/web.php に用意し、curl で叩いて結果を観察します。 前提の確認: 同 Worker 内でリクエストが順次処理されること まず大前提として、1 Worker 内では複数リクエストが並行処理されず、1 つずつ順番に処理されることを確認します。これ以降の検証はすべて「同じ Worker に来た複数リクエストが順番に処理される」という前提で議論を組み立てるため、確認します。 確認用コード リクエストを受け取ったら 2 秒スリープして PID とコルーチン ID を返すだけの単純なルートを用意します。 <?php Route :: get ( '/test-coroutine' , function () { $ pid = getmypid () ; $ cid = \Swoole\Coroutine :: getCid () ; sleep ( 2 ) ; return [ 'pid' => $ pid , 'cid' => $ cid , 'is_coroutine' => $ cid !== - 1 , ] ; }) ; 実行 Worker 数を 1 に絞った状態( --workers=1 )で、3 リクエストを並列で投げます。もし並行処理されるなら合計 2 秒前後で完了するはずです。 start= $( date +%s ) curl -s http://localhost:8001/test-coroutine > /tmp/r1 & curl -s http://localhost:8001/test-coroutine > /tmp/r2 & curl -s http://localhost:8001/test-coroutine > /tmp/r3 & wait echo " Total: $(( $ ( date +%s ) - start )) s " 結果 { " pid ": 14 ," cid ": -1 ," is_coroutine ": false } { " pid ": 14 ," cid ": -1 ," is_coroutine ": false } { " pid ": 14 ," cid ": -1 ," is_coroutine ": false } Total : 6s 3 リクエストの PID が一致(=14)し、 cid = -1 でコルーチン外、合計時間が 2 秒×3 = 6 秒となっていることから、1 Worker 内ではリクエストが並行ではなく順次処理されることが確認できました。 検証 1: static 変数 / グローバル変数がリクエスト間で残ること 仕組みの整理で「static 変数は同 Worker 内では持続する」と書きました。これを実際に確かめます。 確認用コード <?php Route :: get ( '/test-static' , function () { static $ counter = 0 ; $ counter ++ ; $ GLOBALS [ 'global_counter' ] = ( $ GLOBALS [ 'global_counter' ] ?? 0 ) + 1 ; return [ 'pid' => getmypid () , 'static_counter' => $ counter , 'global_counter' => $ GLOBALS [ 'global_counter' ] , ] ; }) ; static 変数とグローバル変数の両方をインクリメントするシンプルなコードです。 実行 まず、同 Worker 内挙動を見るため Worker 数 1( --workers=1 )で順次 10 リクエストを投げます。 for i in { 1 .. 10 } ; do curl -s http://localhost:8001/test-static echo done 次に、Worker 数 10( --workers=10 )で並列 20 リクエストを投げ、Worker 間の独立性を確認します。 for i in { 1 .. 20 } ; do curl -s http://localhost:8001/test-static > /tmp/s_ $i & done wait for i in { 1 .. 20 } ; do cat /tmp/s_ $i ; echo; done | sort 結果 順次 10 リクエスト( --workers=1 ): { " pid ": 14 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 14 ," static_counter ": 2 ," global_counter ": 2 } { " pid ": 14 ," static_counter ": 3 ," global_counter ": 3 } ... { " pid ": 14 ," static_counter ": 10 ," global_counter ": 10 } 並列 20 リクエスト( --workers=10 ): { " pid ": 22 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 22 ," static_counter ": 2 ," global_counter ": 2 } { " pid ": 23 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 23 ," static_counter ": 2 ," global_counter ": 2 } { " pid ": 24 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 24 ," static_counter ": 2 ," global_counter ": 2 } ... { " pid ": 31 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 31 ," static_counter ": 2 ," global_counter ": 2 } 順次 10 リクエストでは全て同じ PID(=14)で、 static_counter と global_counter が 1 から 10 まで連続して増加しています。同 Worker 内では static / グローバル変数が持続していることが確認できます 並列 20 リクエストでは 10 個の異なる PID(22 〜 31)にリクエストが分散し、それぞれの Worker でカウンタが独立に 1 から始まっています。Worker 間でメモリが分離されていることが分かります 以上から、 static 変数とグローバル変数は同 Worker 内のリクエスト間で持続し、Worker 間では分離されることが確認できました。 これは、1 リクエスト目の値が 2 リクエスト目に意図せず見えてしまう可能性を意味します。前のリクエストの情報が次のリクエストに残るパターンと考えられるため、利用には注意が必要そうです。 検証 2: $this->app の内部状態がリクエスト間で残ること singleton の中にリクエスト固有のデータを保持して、リクエスト間で値が残ることを確認します。 確認用コード <?php // app/Services/UserContextSingletonService.php class UserContextSingletonService { private ? string $ currentUserName = null ; public function setCurrentUser ( string $ name ) : void { $ this -> currentUserName = $ name ; } public function getCurrentUser () : ? string { return $ this -> currentUserName; } } <?php // app/Providers/AppServiceProvider.php public function register () : void { $ this -> app -> singleton ( UserContextSingletonService :: class ) ; } public function boot () : void { // ベースの $this->app->instances にインスタンスを格納するため、boot 時点で resolve する $ this -> app -> make ( UserContextSingletonService :: class ) ; } 通常の singleton() だけでは、初回 app(...) 解決時にインスタンスが sandbox 側に入り、リクエスト終了時の $sandbox->flush() で破棄されます。リクエスト間で持続する状態を再現するため、サンプルでは boot() で make() を呼んでベースの $this->app->instances にインスタンスを積んでいます。 <?php use App\Services\UserContextSingletonService; Route :: get ( '/test-singleton-set/{name}' , function ( string $ name ) { app ( UserContextSingletonService :: class ) -> setCurrentUser ( $ name ) ; return [ 'pid' => getmypid () , 'action' => 'SET' , 'value' => app ( UserContextSingletonService :: class ) -> getCurrentUser () , ] ; }) ; Route :: get ( '/test-singleton-get' , function () { return [ 'pid' => getmypid () , 'action' => 'GET' , 'value' => app ( UserContextSingletonService :: class ) -> getCurrentUser () , ] ; }) ; 実行 curl http://localhost:8001/test-singleton-set/Alice curl http://localhost:8001/test-singleton-get curl http://localhost:8001/test-singleton-get 結果 { " pid ": 14 ," action ":" SET "," value ":" Alice " } { " pid ": 14 ," action ":" GET "," value ":" Alice " } ← 別リクエストなのに残っている { " pid ": 14 ," action ":" GET "," value ":" Alice " } ← まだ残っている 別のリクエストにもかかわらず、Alice という値がそのまま見えています。 singleton バインディングのインスタンスプロパティに格納した値が残ることが確認できました。 検証 3: RequestReceived リスナーが一部の状態をリセットすること FlushAuthenticationState リスナーが Guard キャッシュを破棄していることを確かめます。 確認用コード <?php use App\Models\User; use Illuminate\Support\Facades\Auth; Route :: get ( '/test-auth-set/{name}' , function ( string $ name ) { $ user = new User ([ 'name' => $ name ]) ; Auth :: setUser ( $ user ) ; return [ 'pid' => getmypid () , 'action' => 'SET' , 'user_name' => Auth :: user () ?-> name , ] ; }) ; Route :: get ( '/test-auth-get' , function () { return [ 'pid' => getmypid () , 'action' => 'GET' , 'user_name' => Auth :: user () ?-> name , ] ; }) ; Auth::setUser() を利用してデフォルトの Guardの $user プロパティに直接 User インスタンスを入れ、Guard キャッシュの状態を作って検証します。 実行 (1) デフォルト構成( FlushAuthenticationState 有効) curl http://localhost:8001/test-auth-set/Alice curl http://localhost:8001/test-auth-get 結果は以下の通りです。 { " pid ": 14 ," action ":" SET "," user_name ":" Alice " } { " pid ": 14 ," action ":" GET "," user_name ": null } ← flush で消えている 実行 (2) FlushAuthenticationState を外した場合 検証のため、 RequestReceived リスナーから FlushAuthenticationState を除外します。 <?php // config/octane.php (listeners 部分のみ抜粋) use Laravel\Octane\Events\RequestReceived; use Laravel\Octane\Listeners\FlushAuthenticationState; use Laravel\Octane\Octane; return [ // ... 'listeners' => [ // ... RequestReceived :: class => array_values ( array_filter ( Octane :: prepareApplicationForNextRequest () , fn ( $ listener ) => $ listener !== FlushAuthenticationState :: class , )) , // ... ] , ] ; 同じ curl を実行します。 { " pid ": 15 ," action ":" SET "," user_name ":" Alice " } { " pid ": 15 ," action ":" GET "," user_name ":" Alice " } ← 前のリクエストの情報が残っている リクエストをまたいで Alice という値が残っています。 検証結果から、 FlushAuthenticationState を外すと認証状態がリクエスト間で残り、有効な場合は Guard キャッシュが毎リクエスト破棄されることが確認できました。フレームワーク標準の Auth が Octane でも安全に使えるのは、このリスナーが裏で動いているからこそだと言えます。 おわりに 本記事では、Octane + Swoole で前のリクエストの情報が次のリクエストに残る仕組みを整理し、サンプルコードで動作を検証しました。その結果、 clone とリスナーによって Auth などのフレームワーク側の状態はリクエストごとにクリアされる一方、 static 変数 / グローバル変数や、Worker boot 時に解決済みの singleton インスタンスのプロパティは自動ではクリアされないことが確認できました。 持続させてはいけない場所に状態を置かないこと、また、もし独自のグローバル状態を持たせる場合には RequestReceived などのイベントに自前のリスナーを追加してクリアすることを意識できればと思います。 最後まで読んでいただきありがとうございました。 参考 Deep Dive into Laravel Octane(Albert Chen)