キャディ株式会社のブログ - TECH PLAY

TECH PLAY

キャディ株式会社

キャディ株式会社 の技術ブログ

237

はじめに 本記事は、2026年3月10日に開催された Elastic{ON} Tokyo での発表「 『定型』を許さない製造業データへの挑戦 」の内容をもとにした連載の最終回です。 第1回:ビジュアル情報を活かす 第2回:分断されたデータを繋ぐ 第3回(本記事):止めずに進化させる 第1回ではビジュアル情報を使った類似図面検索、第2回ではデータソース横断検索の設計について書きました。最終回の本記事では、製造業データの「非定型性」にどう向き合っているか、そして進化し続けるインデックスをどうやって無停止で運用しているかについて書きます。 「正解のデータモデル」が存在しない 製造業のデータにおける3つ目の壁は、データモデルの多様性です。 データ構造がバラバラ 図面の「表題欄」ひとつとっても、会社ごとに記載項目やフォーマットは驚くほど違います。しかもこの違いは会社単位に留まりません。部品の種類によっても、設計者の書き方によっても、データの持たせ方は変わります。 たとえば同じ「部品データ」でも、機械部品の「部品図」と電気制御の「回路図」と仕様を書いた「仕様書」ではデータ種そのものが違う。同じデータ種の中でも、図番や品名のような共通項目以外は「材質」だったり「熱処理」だったり「表面処理」だったりと、作るモノ次第で必要な情報が変わる。極端な話、ネジ一本と大型エンジン筐体では管理すべき項目が全く違います。 普通のシステムなら共通スキーマを事前に定義するところですが、製造業データではそれが通用しないのが厄介なところです。 検索ニーズもバラバラ データ構造がバラバラであるように、検索の切り口もバラバラであるということも重要です。 同じ部品を探したいという目的でも、立場が変われば検索の切り口は全く違います。設計は「形状」から入りたいし、調達は「発注実績」から入りたいし、品質管理は「不具合履歴」から入りたい。 もっと厄介なのは、一人のユーザーの中でもこのコンテキストが動的に切り替わるということです。最初は「似た形の図面」を漠然と探していた設計者が、良い事例を見つけた瞬間に「このサプライヤーで作った実績はあるか?」と調達の視点に切り替わる。実績を見たら今度は「この加工方法での不具合率は?」と品質の視点に潜っていく。 固定の検索項目では、この思考の連鎖には追従できません。どんな切り口から入ってきても、コンテキストがどう遷移しても、データ間の関係性を提示し続けられる柔軟さが必要です。 進化し続けるインデックス、止められない制約 ここまで書いてきた多様性は、データ構造や検索の切り口が日々変わり続けた結果として生まれたものです。新しい製品カテゴリが増えれば管理すべき属性が変わるし、探し方も変わる。そしてこの変化は今後も止まりません。 こうした変化に追従するには、データや検索の切り口に合わせてインデックス設定を切り替えていくことが重要です。すなわち、マッピングの更新、アナライザーの調整、フィールドの追加や型変更など、インデックス定義を継続的にアップデートする必要があります。 ただ、ここに大きな制約があります。 CADDi Drawer は今この瞬間の意思決定を支えるアプリケーションなので、インデックス構造の変更でユーザーの業務が止まるのは許容できません。一方、Elasticsearch の reindex 処理ではデータの一貫性を保つために書き込みを止めるのが一般的な手法です。 「常に最新のインデックスに進化させたい」と「24時間365日、更新も検索も止めてはいけない」。この2つの要求は正面からぶつかります。 ゼロダウンタイム reindex の仕組み 標準的な reindex の問題 Elasticsearch でインデックス定義を変更する通常の手順はこうです。 新しいマッピングでインデックスを作成する 旧インデックスから新インデックスへデータをコピーする(reindex) エイリアスを切り替えて新インデックスを参照させる 旧インデックスを削除する 問題はステップ2です。reindex 中に旧インデックスへ書き込まれたデータは新インデックスに反映されない。書き込みを止めれば一貫性は保てますが、ユーザーの更新がブロックされる。止めなければ、切り替え時にデータが欠損する。どちらも受け入れられません。 私たちのアプローチ そこで、ゼロダウンタイムでインデックスを切り替えられる仕組みを自前で実装しました。 書き込みと読み込みのパスを抽象化し、新旧のインデックスを並行稼働させながら、一貫性を保った状態でトラフィックを切り替えます。 具体的には、reindex を開始したら新旧両方のインデックスに書き込むことで、新インデックスにも最新データが反映されるようにしています。reindex が完了したら、読み取りパスを新インデックスに切り替える。この切り替えは瞬時に行われるので、ユーザーの検索は中断しません。書き込み順序の保証やデータ整合性のチェックも組み込んでいて、データが欠損しない仕組みになっています。 ユーザーからはメンテナンスの存在が見えず、常に最新のインデックス構造で検索できる状態を維持しています。 スキーマ変更を恐れずに回せる 個人的には、このゼロダウンタイムの仕組みで一番大きいのは、スキーマの変更を躊躇なくやれるようになったことだと思っています。 製造業のデータモデルに正解がない以上、スキーマは仮説です。現場のフィードバックを受けて継続的に直していくしかない。ユーザーから既存スキーマでは表現できない属性の要望が出てきたらフィールドを追加するし、検索精度を上げたければアナライザーの設定を見直してインデックスを再構築するし、新しいデータソースが接続されたら非正規化構造ごと変える場合もあります。 こうした変更のたびにサービスを止めていたら、改善のサイクルは回りません。ゼロダウンタイム reindex があることで、本番環境のインデックスを継続的に進化させ続けられています。 連載のまとめ 3回にわたって、CADDi Drawer の検索基盤について書いてきました。 第1回では、図面をベクトル化して kNN 検索で類似図面検索を作った話。第2回では、10兆規模の組み合わせを用途別インデックスと動的制御で捌いている話。そして今回は、スキーマが変わり続ける中でゼロダウンタイム reindex でインデックスを進化させ続けている話。 振り返ると、3つの壁はそれぞれ独立した問題のように見えて、根っこでは繋がっています。ビジュアル検索がなければ探索は始まらないし、データソース横断がなければ探索は完結しないし、スキーマを柔軟に変え続けられなければ探索は陳腐化する。どれか一つが欠けても「探索」にはならない。 3つの壁を個別の技術課題として解くのではなく、「探索」という一つの設計思想で貫けたことが、振り返ってみると一番大きかったと思っています。 製造業のデータはこれからも変わり続けます。私たちの検索基盤も、それに合わせて進化し続けていきます。 本連載の内容は Elastic{ON} Tokyo での発表をもとにしています。発表スライドは こちら からご覧いただけます。
はじめに 本記事は、2026年3月10日に開催された Elastic{ON} Tokyo での発表「 『定型』を許さない製造業データへの挑戦 」の内容をもとにした連載の第2回です。 第1回:ビジュアル情報を活かす 第2回(本記事):分断されたデータを繋ぐ 第3回:止めずに進化させる 前回は、製造業の検索に「ビジュアル」が外せないこと、Elasticsearch の kNN 検索を使った類似図面検索について書きました。 今回は、次に立ちはだかる「データ規模と結合」の壁について掘り下げます。複数のデータソースを横断した検索を、どうやって高速に実現しているかという話です。 「探索」に必要なデータ横断 前回も触れた通り、私たちが実現したいのは「探索」です。たとえば調達部門が「より良い発注」を目指す場合、こういうプロセスを一気通貫で回したい。 発注予定の図面に対して、視覚的に似た過去の図面を検索する 見つかった図面に紐づく発注実績をシステム横断で確認する 集まった情報をもとに今回の発注額を適正化する この②「システム横断での実績確認」が、技術的にはかなり重い問題を含んでいます。 製造業データの「分断」 製造業の現場には、PLM、EDI、SCM、CRM といった業務ドメインごとのシステムが並んでいます。それぞれの業務には最適化されていますが、システム間でデータが分断されているのが常です。 この分断を解決するために ERP が導入されてきました。ただ、ERP はあくまで組織の「管理」が目的のシステムです。前回書いた「ビジュアルな情報をビジュアルなまま扱う」という発想にはなっていないので、ERP に実績データが集まっても、現場が判断の拠り所にする「モノの形」とは切り離されたまま。分断は形を変えて残り続けます。 CADDi Drawer は、こうしたバラバラのデータソースを統合し、一つの画面から横断的に探索できるようにするプロダクトです。 データ規模と「結合」の壁 日本の製造業は数十年、時には100年を超える歴史があり、蓄積されたデータは膨大です。図面だけで100万枚以上、受発注実績は1,000万レコード以上。仕様書やトラブル記録などの非定型な文書も含めるとさらに増えます。 これらを統合して扱うとき、RDB のようにテーブルを繋ぎ合わせる素朴な結合をやると、組み合わせは10兆件規模に達します。インデックスがなければ全件走査になるので、対話的な速度は到底出ません。 では、図面と実績をデータ種別ごとに別のインデックスに入れればいいかというと、それだけでは不十分です。片方のインデックスで検索した結果をもう片方と突き合わせる必要があり、突き合わせ側はインデックスが効かず全件走査に落ちるケースが出てきます。クエリの絞り込みが弱い場合や結合キーの値の種類が少なく多対多の組み合わせが膨らむ場合、この突き合わせのコストが支配的になって応答速度が崩れます。 この10兆通りの組み合わせを、検索エンジンが得意な形にどう落とし込むか。ここがアーキテクチャ上の大きな問題でした。 複数インデックス構造による解決 私たちがとったのは、複数のデータソースを統合したうえで、用途別のインデックス群を構築するアプローチです。 万能なインデックスは作れない 私たちも最初は「すべての要件を満たすインデックスをどう設計するか」と考えていましたが、うまくいきませんでした。 データソースごとにインデックスを分けると、前述の通りクエリ時の突き合わせで全件走査が走って遅いという課題があります。その一方で非正規化して複合インデックスにまとめれば検索は速くなりますが、あるデータソースの1レコードを変えただけで紐づく全ドキュメントに波及して大量更新が走る。検索を速くすると更新が重くなるし、その逆もまた然りで、一つのインデックスでは両立しません。 なので発想を変えて、万能なインデックスを作ろうとするのをやめました。代わりに要件を分解して、用途ごとに別のインデックスを持たせています。例えば、データソースごとにオンライン更新が走るインデックスと、データ鮮度には多少の遅延があるもののデータソースを横断して検索できるインデックスを分けて持つ、といった具合です。 動的なインデックス選択 ここで鍵になるのが、クエリ実行時の制御層です。ユーザーから投げられたクエリの内容をシステムが解析し、そのとき最も効率的なインデックスを自動で選択します。 単一データソース内の検索ならそのデータソースのインデックスを使い、複数データソースにまたがる検索なら横断用のインデックスを使う。こうした切り替えが裏側で動いています。 ユーザーから見ると、データがどこにあるか、どのインデックスが使われているかは一切見えません。透過的な検索体験を維持したまま、裏では用途に応じた最適化が走っている、という構成です。 類似検索 × フィルタリングの両立 データソース横断検索の難しさは、「類似検索」と「フィルタリング」を組み合わせたときに顕著になります。 そして現場では、この掛け合わせがないと使い物になりません。「この図面に似た形のもの」を探すだけでなく、「その中で、今取引のある会社に発注したことがあるものだけ」に瞬時に絞り込めなければ、実際の比較検討には使えないからです。 なぜ難しいのか 類似検索(kNN)はベクトル空間上の距離計算、フィルタリングは転置インデックスによる構造化データの絞り込みで、それぞれ背後のデータ構造もアルゴリズムも違います。素朴に組み合わせると、どちらかの速度が犠牲になります。 さらに、フィルタリング対象のデータが別のデータソースに由来する場合(発注実績は ERP、図面はファイルサーバーなど)、結合処理のコストも乗ってきます。 どう解決したか kNN 検索とフィルター条件を同一クエリ内で組み合わせることで、ベクトル検索の結果に対して構造化データの絞り込みをかけています。 加えて、前述の横断検索用インデックスで図面と実績の関連を事前に非正規化しているため、検索時のデータソース間結合は発生しません。 Elasticsearch が kNN とフィルタを同一クエリ内で同時実行できることと、事前結合による非正規化インデックスの組み合わせで、類似検索とデータソース横断のフィルタリングをサブセカンドで安定して返せるようになりました。 アーキテクチャの全体像 データが取り込まれてからユーザーに届くまでの流れを整理します。 PLM、ERP、ファイルサーバーなど複数のデータソースからデータを収集する AI による図面解析、メタデータの抽出、ベクトル化などの処理を行う 用途別のインデックスにデータを振り分ける ユーザーのクエリを解析し、最適なインデックスを動的に選択する サブセカンドで結果を返し、ファセットやハイライトとともに表示する 図面と実績は更新頻度も構造も違うデータですが、探索に最適な形に統合・変換してインデックスし続けることで、ユーザーはデータの所在を意識せずにあらゆる情報を横断できます。 まとめ 製造業のデータを素朴に結合すると10兆件規模の組み合わせになる。これをどう捌くかが今回のテーマでした。 私たちの答えは、用途別に最適化した複数のインデックスと、クエリに応じてそれらを動的に選択する制御層です。特に、非正規化による事前結合で検索時の結合コストをなくしたことと、Elasticsearch が kNN とフィルターをネイティブに統合できることが、類似検索とフィルタリングの両立を可能にしています。 ただ、ここまで作り込んだインデックス構造も、現場の変化に追従できなければ意味がありません。次回の最終回では、このインデックスを「サービスを止めずに進化させ続ける」ための仕組みについて書きます。
はじめに 本記事は、2026年3月10日に開催された Elastic{ON} Tokyo での発表「 『定型』を許さない製造業データへの挑戦 」の内容をもとに、3本の連載記事として再構成したものです。 第1回(本記事):ビジュアル情報を活かす 第2回:分断されたデータを繋ぐ 第3回:止めずに進化させる キャディ株式会社で、AIデータプラットフォーム「CADDi Drawer」の検索基盤の技術戦略と開発チームのマネジメントを担当している橋本です。 この記事では、製造業の検索で「ビジュアル情報」がなぜ外せないのか、それを Elasticsearch でどう実現したかについて書きます。 CADDi Drawer とは CADDi Drawer は、製造業のお客様が持つデータを「検索・探索」を通じて活用できるようにするプラットフォームです。 CADDi というプロダクトは3層構造になっています。最下層で ERP やファイルサーバーなどに散在するデータを統合・構造化し、中間層の CADDi Drawer で構造化されたデータを結びつけて現場の意思決定を支援し、最上層で業界課題に特化したソリューションを提供しています。 CADDi Drawer は、下層で生まれた生のデータを現場が使える知見に変換する、いわば情報のハブです。 「検索」から「探索」へ 従来の業務システムでは、検索体験は特定の業務フローに合わせて作られていました。固定の検索フィールドに図番や品名を入力し、ERP から返ってくる文字情報を確認する。「何を探しているか、どう探せばいいかが最初から決まっている」前提の検索です。 でも、現場で本当に必要なのは、こういう決まりきった検索ではありません。たとえば調達部門が「より良い発注」を目指す場合、こういうプロセスを回したい。 発注予定の図面に対して、視覚的に似た過去の図面を検索する 見つかった図面に紐づく発注実績をシステム横断で確認する 集まった情報をもとに今回の発注額を適正化する 最初から「何を探すか」が決まっているわけではなく、データに触れながら手探りで解像度を上げていくプロセスです。これが私たちが「探索」と呼んでいるもので、CADDi Drawer が支えるべき対象です。 ビジュアルの壁 この「探索」を製造業で実現するうえで、最初に立ちはだかるのがビジュアルの壁です。 製造業はどこまでいっても「モノづくり」の世界です。部品の価格や納期を確認するとき、データベース上の数値だけで判断できるかというと、まず無理です。背後にある「形」を図面や 3D モデルで見て、大きさ、重さ、加工の複雑さといった物理的な実体を把握して初めて、数字に意味が出てくる。文字情報だけでなく形状情報とセットで扱えることが前提になります。 つまり、探索の起点は図面や CAD というビジュアル情報です。ここで求められるのは大きく2つあります。 ビジュアルな検索結果が次の探索行動を生むこと 形状そのものを検索条件にできる手段 順に説明します。 ビジュアルな検索結果が探索を駆動する 「探索」は、データを見て、気づき、次の条件を入れるという思考のループです。ここで「見て」の部分が決定的に重要です。 SQL ビューアーのようにテキストと数値が並ぶだけの検索結果では、このループは回りません。図面のサムネイルが検索結果に並んでいて、形状を一覧でパッと比較できるからこそ、「あ、この形状のほうが近いな」「この系統をもう少し掘ってみよう」という次のアクションが生まれます。ビジュアルな情報がユーザーの思考を刺激し、探索行動を惹起する。これがただのデータ検索と「探索」の違いです。 CADDi Drawer では、検索結果の一覧で図面のサムネイルと属性情報をセットで表示しています。いちいちファイルを開かなくても形状を確認しながら絞り込みを進められる。この体験を支えるために、どんなに複雑な条件でも1秒を切る応答速度を維持しています。私たちの検索基盤では Elasticsearch を使っていますが、ここで活きてくるのは低レイテンシの分散検索、ファセット、ハイライトといった基本機能で、これらを組み合わせることでビジュアルな結果を瞬時に返し続けるインターフェースの土台を作っています。 形状で探す ― kNN 検索による類似図面検索 UI の工夫だけでなく、「形状そのものをクエリにできる」という技術的なアプローチも取っています。 なぜ形状検索が要るのか 製造業の現場では「この図面に似た部品を過去に作ったことがあるか?」という問いが日常的に発生します。ただ、この「似ている」を言葉で表現するのは難しい。テキスト検索で「丸い」「穴が3つ」と入れたところで、形状全体の類似性は捉えられません。 ベクトル化と kNN 検索 私たちは、図面の幾何学的な特徴を捉える学習モデルを使って全図面をベクトル化しています。このベクトルデータを Elasticsearch の kNN 検索にかけることで、「この図面に似たもの」という言語化しにくい条件での検索を実現しました。 kNN(k-Nearest Neighbors)検索は、ベクトル空間上で距離が近い、つまり特徴が似ているドキュメントを取得する手法です。私たちは Elasticsearch の kNN 検索機能を使っています。 テキスト検索やファセットによる絞り込みと類似図面検索を同じクエリの中で組み合わせられるのと、100万枚を超える図面に対しても分散インデックスで速度が出るのが、この構成を選んだ理由です。kNN と構造化データのフィルタリングを同一クエリで実行できることが決め手で、全文検索と同居させる構成にしています。 ビジュアル × 形状クエリで探索の入口をつくる ビジュアルな検索結果と形状クエリが揃うことで、「検索窓に何を打てばいいかわからない」という状態からでも探索を始められるようになります。ただし、ビジュアルだけでは探索は完結しません。見つけた図面の先にある発注実績や品質データにたどり着くには、データソースの横断が必要です。これは次回の記事で扱います。 まとめ 製造業の検索においてビジュアル情報は前提です。文字だけの検索結果を眺めて部品の良し悪しを判断できる人はいません。 この記事では、ビジュアルな検索結果が次の探索行動を生むこと、そして形状をクエリにできる kNN 検索の2つについて書きました。どちらも「探索」の入口をつくる技術です。 ただ、ビジュアルの問題を解いただけでは探索は完結しません。先ほどの調達の例でいえば、①似た図面を見つけた後に、②発注実績確認や③発注適正化まで一気通貫で回す必要があります。次回は、このデータソース横断検索をどう実現しているか、10兆通りの組み合わせをどう捌いているかについて書きます。
Introduction Every AI session starts blank. You open a new chat, and you're back at square one: your preferred approach to error handling, the architectural constraint you settled on three weeks ago, the trade-off you already reasoned through and documented. None of it is there. So you explain it again. And again tomorrow. This is the baseline problem, and most engineers solve it eventually. But how they solve it is where things go sideways. The first failure mode is overreaching. Someone reads about vector databases, knowledge graphs, or shared memory layers and jumps straight to building that. Five hours of setup, a new dependency in the stack, and three weeks of maintenance later, they realize they just needed to paste some instructions into a config file. The problem was simple. The solution was not. That mismatch costs real time. The second failure mode runs in the opposite direction. A team builds a production agent system and stitches together memory as an afterthought: some context stuffed into a prompt, a few hardcoded facts, no real persistence strategy. It works at first. Five months later, the system needs to track state across users, or share knowledge between agents, or handle ten times the volume. Fixing it now means redesigning the memory layer from scratch, around code that was never built to accommodate it. Both failures have the same root: choosing a memory approach without a clear model of what "memory" actually means across different complexity levels. This blog is a map. It covers five levels of AI memory, from the session context that every tool ships by default up to memory-as-a-service infrastructure for multi-agent platforms. Higher level does not mean better. It means more capable in specific ways, and more complex to build and operate. The goal is to match the level to the actual continuity need, not to the most interesting solution in the space. Two groups of engineers will find this useful: Track A is engineers customizing a personal AI coding assistant: tuning Claude Code, Cursor, or similar tools to maintain context about their projects, preferences, and workflows across sessions. Track B is engineers designing memory for a production agent system: something customer-facing, multi-user, or running autonomously at scale. The five levels apply equally to both. Where the right stopping point differs between tracks, the post calls it out explicitly. What this article means by memory Memory is one of those terms that sounds obvious until you try to define it. Depending on who you ask, "AI memory" might mean conversation history, vector databases, user profiles, knowledge graphs, long-term storage, learned behaviors, agent state, or all of the above. Different tools and frameworks use the same word to describe very different things. For the purposes of this article, memory means information that survives beyond the immediate interaction and can influence future interactions. That includes things like: Past conversations and decisions User preferences and project context Facts extracted from interactions Documentation, notes, and research Entities, relationships, and historical changes In other words, memory is about preserving and retrieving information over time. There is another category worth mentioning: procedural memory. Procedural memory is memory about how to do things rather than what is true. Examples include deployment playbooks, investigation workflows, tool usage patterns, reusable execution plans, and strategies that improve through repeated use. Many modern agent systems rely heavily on this kind of memory. The reason procedural memory does not appear as its own level in this framework is that it cuts across several levels rather than fitting neatly into one of them. A deployment playbook might start life as a simple instruction file, evolve through observations gathered from previous executions, and eventually become part of a structured knowledge resource. The same procedural knowledge can exist in multiple forms depending on how mature the system becomes. This article therefore focuses primarily on information memory: conversations, facts, knowledge, decisions, entities, and relationships. Not because procedural memory is less important, but because it follows a different set of design concerns and would deserve its own discussion. With that scope established, we can look at the dimensions that distinguish one memory system from another. Memory has three dimensions Before we walk through the five levels, it helps to understand what makes them different from each other. The levels aren't a ranking. They're positions on a map, and the map has three axes. Depth The first is depth: how rich and structured is the thing being stored? At the shallow end, you have raw conversation (the literal back-and-forth that happened in a session). Move further along and you get extracted facts and summaries, where some processing has already been done to make the content useful. At the deep end sit interconnected knowledge graphs, where entities, relationships, and temporal changes are all modeled explicitly. Higher depth means more work was done to make stored information usable, and more complexity to build and maintain. It's not automatically better. A summary is faster to retrieve and cheaper to store than a graph traversal, and for many use cases that's exactly the right trade. Persistence The second dimension is persistence: how long does stored memory survive? Some systems hold context only for the duration of the current session. Close the window and it's gone. Others persist across sessions within a project, so you can pick up a conversation thread days later. The most durable approaches store memory permanently, independently of any session or tool: the information outlives whatever generated it. Scope The third is scope: who can access what's stored? At one end, memory is personal and sometimes locked to a single tool (only you, only in this editor). Some systems share memory across a team, so knowledge one person captured becomes available to everyone. At the widest scope, memory is exposed via an open protocol so any compliant AI tool can read from and write to the same store, regardless of vendor. The important thing is that these three dimensions are independent of each other. A system can have high depth but be session-only: a sophisticated knowledge graph that evaporates when the process exits. Another can be permanent but strictly personal, surviving indefinitely while staying locked to one user and one tool. Depth doesn't imply persistence, and persistence doesn't imply scope. The levels we're about to cover are anchors at specific positions on this three-dimensional map, not rungs on a ladder where the goal is to climb as high as possible. Memory level positioning Level Depth Persistence Scope L1: Session Minimal Session Personal L2: Instruction Low Permanent (manual) Personal → Team L3: Episodic Medium Project / Long-term Personal L4: Semantic High Permanent Personal → Team L5: Infrastructure High Permanent Team / Platform A note on levels versus architectures Before continuing, it's worth clarifying something that often causes confusion when people discuss AI memory. The five levels are not a strict hierarchy where each level replaces the previous one. They describe different combinations of memory representation, persistence, and deployment scope. The lower levels primarily differ in what is remembered: L1 stores recent context. L2 stores instructions and preferences. L3 stores experiences and interactions. L4 stores synthesized knowledge. L5 is slightly different. It does not describe a new form of memory so much as a new ownership model for memory. The memory becomes infrastructure that can be shared across tools, agents, and users. This means the levels are not mutually exclusive. An L5 memory platform might expose simple L3-style conversation summaries, a compiled knowledge base from L4, a knowledge graph, or all three simultaneously. Think of L3 and L4 as describing the shape of memory. Think of L5 as describing where that memory lives and who can access it. L1 and L2: The foundations L1: Session memory Every AI tool ships with some form of memory out of the box. Conversation history, project workspaces, the sliding context window keeping your recent messages available: it's all there without any configuration. This is L1. It solves exactly one problem well: continuity within a single session. Ask a follow-up, reference something you said ten minutes ago, iterate on a design: it all works. Zero setup, zero maintenance. The wall appears the moment you close the conversation. Start a new session the next day and everything is gone: no memory of the architecture decision you landed on yesterday, no awareness of the other three projects you're juggling, no accumulated sense of how you prefer to work. The slate is blank. For Track A , L1 is fine for one-off tasks and exploratory sessions where continuity genuinely doesn't matter. The mistake is staying here for work that spans days or weeks and then wondering why the AI keeps asking questions you've already answered. For Track B , L1 is always present as the foundation and is rarely sufficient on its own once continuity matters. Every agent system starts here. Most production systems eventually need more than this. Tools at this level (as of mid-2026) See the tools reference table at the end of this article for a fuller view. L2: Instruction memory The first meaningful upgrade is also the simplest: you write things down and make sure the AI reads them at the start of every session. This is L2. System prompts, rules files, project context documents, conventions lists: anything structured that loads automatically when a conversation begins. Some tools support hooks that inject these files without you thinking about it. Others require pasting them in or configuring a project workspace. The mechanism varies; the idea is the same. What L2 actually solves is the re-explanation tax. Every engineer who uses AI tools regularly has felt it: the first ten minutes of a session spent re-establishing who you are, what project you're on, how you name things, what you've already tried. L2 eliminates most of that. Write your preferences once, point the tool at them, and the next session starts with that context already in place. It scales to teams in a way that surprises people. A shared conventions file checked into a repository means everyone's AI assistant follows the same patterns (consistent naming, consistent code style, consistent architectural constraints) without anyone having to configure their tool individually. The file becomes a single source of truth that both humans and AI tools read. The appeal of L2 is that it asks almost nothing from you technically. No infrastructure, no databases, no vector embeddings, no background processes. Just files and text, working with mechanisms every AI tool already supports. For most engineers, this is where 80% of the "my AI doesn't know me" friction disappears. An important pattern appears once teams start using L2 and L3 together. Most decisions begin life as history. A discussion happens, alternatives are evaluated, a tradeoff is accepted, and a conclusion is reached. Initially that information belongs in L3 because it reflects something that happened. Over time, some of those decisions stop being history and become policy. "We chose SQS instead of Kafka" eventually becomes "Use SQS for asynchronous processing." At that point the information has effectively been promoted from L3 into L2. Healthy memory systems tend to do this continuously. Episodic memory captures what happened. Stable learnings are promoted into instructions. This keeps the memory layer compact while ensuring important decisions become durable guidance rather than forgotten conversations. The limitation is just as simple to understand: the instructions are static. They reflect what should happen, not what did happen. If the project evolves and nobody updates the file, the AI keeps following outdated rules. If a key decision was made in a session yesterday, it doesn't automatically appear in the rules tomorrow. L2 captures intent; it doesn't capture history. The moment you need the AI to remember what actually occurred across sessions, you've outgrown L2 and are looking at the next level. For Track A , this is the first upgrade almost every engineer should make. A project context file and a personal preferences file together cover the majority of continuity friction with minimal ongoing effort. For Track B , L2 is present in almost every production system, whether explicitly recognized or not. Every agent needs a system prompt defining its behavior, persona, and constraints. It's always present, even in the most sophisticated systems: other levels build on top of it, not in place of it. Tools at this level (as of mid-2026) See the tools reference table at the end of this article for a fuller view. L3: Episodic memory The problem that L2 doesn't solve is accumulation. You can write excellent instructions for how the AI should behave, but it still has no idea what you actually worked on last week. You re-explain the same project context. You re-share the same decisions. After a few months of this, the appeal of "the AI remembers what we've discussed" starts to feel less like a feature and more like a basic expectation. L3 is where that expectation gets met. The core pattern is simple: at session end, a background process captures what happened, processes and stores the content, and on the next session automatically injects whatever past context seems most relevant. No manual maintenance. The AI just gradually accumulates working knowledge of your projects, preferences, and decisions over time. The appeal is real. After a few weeks, the AI starts referencing things without being prompted: the architectural decision you made last Tuesday, the library you ruled out and why, the naming convention your team settled on. That kind of continuity changes how the tool feels to work with. The lossy versus lossless spectrum Before picking a tool at this level, there is one distinction worth understanding clearly: what gets stored, and what gets thrown away. On the lossy end, when a session ends an LLM reads the conversation and extracts key facts, decisions, and preferences into a compact summary. The raw transcript is discarded. This approach is token-efficient and scales well over months, since the stored memory stays compact regardless of how many sessions accumulate. The tradeoff is that you lose the ability to reconstruct what was actually said. You get a distilled view of the past, not the past itself. This works well for long-running projects where accumulated context matters more than exact recall. On the lossless end, the raw conversation is preserved and a searchable index is built on top. Full-text search, vector embeddings, or both. This enables exact retrieval: you can find a specific exchange from five weeks ago, reconstruct the reasoning behind a decision, or debug why an agent behaved a certain way. The cost is higher storage and more infrastructure. It suits audit trails and situations where "what was actually said" carries weight. Most mature tools today sit somewhere between these extremes. They may store verbatim content but surface LLM-generated summaries as the primary retrieval interface. When you are evaluating a tool here, it is worth checking both sides separately: what gets stored (the raw conversation, a summary, structured facts) and what gets surfaced at retrieval time (matched chunks, a synthesized answer, a flat list of facts). Those are often different things, and the gap between them is where retrieval quality problems tend to hide. How retrieval typically works At session end, the background process captures the conversation and splits it into chunks. Each chunk is converted into a vector embedding and written to a store. At the next session start, the current context (what you just typed, the project you opened) is used to query the store, and the top-matching chunks are injected into the context window automatically. Some tools add keyword search or hybrid retrieval on top for better precision when semantic similarity alone isn't enough. The quality of this whole pipeline depends heavily on chunking strategy and retrieval tuning, which is why two tools using the same underlying model can feel very different in practice. Where this level breaks L3 is primarily personal and local. Most tools at this level store memory for one user, on one machine, for one AI tool. Sharing that accumulated context across teammates, or making it accessible from a different AI tool, requires moving to L5. This is a meaningful constraint if you are thinking about team-level continuity. Language support is also worth checking before committing. Much of the tooling here was built with English-first assumptions, and retrieval quality for Japanese content varies considerably between tools. For Track A engineers, this is the main upgrade worth making. Once the setup is done it runs in the background. The value accumulates gradually and quietly, which means you probably won't notice it working until you realize you stopped explaining yourself so much. For Track B , this level maps to what agent frameworks call episodic memory: the record of what an agent has done in past interactions. The lossy-versus-lossless decision has clearer stakes here. A user-facing agent that makes specific commitments ("I'll follow up by Friday," "we agreed to disable that feature") may need accurate recall of those commitments, which pushes toward the lossless end. Getting this wrong in a production system tends to surface in support tickets rather than evaluation runs. Tools at this level (as of mid-2026) See the tools reference table at the end of this article for a fuller view. L4: Semantic memory Everything below this level is about remembering. L4 is about compiling. The levels before it capture what happened: which files were read, what preferences were stated, which decisions were logged. Useful, but passive. L4 takes raw material and builds something from it: a structured, interconnected knowledge resource that gets more valuable the longer you use it. The goal shifts from "remember this conversation" to "build something I can reason over." There are two distinct architectural models at this level. They solve different problems, require different infrastructure, and represent genuinely different design choices rather than variations on the same idea. The compilation model Think of this as an AI-maintained encyclopedia. Raw source material (research papers, documentation, articles, internal notes) arrives in an immutable input layer. An LLM reads that material and writes structured articles: encyclopedia-style entries with cross-references, organized by concept, updated as new sources arrive. The knowledge resource is the compiled output, not the raw sources themselves. The contrast with standard retrieval-augmented generation is worth being precise about. In a typical RAG setup, you embed raw documents and retrieve chunks at query time. The retrieved knowledge does not accumulate into a maintained representation. Every query starts from the underlying source documents, even if the retrieval layer itself becomes richer over time. In the compilation model, synthesis happens upfront. Articles build on each other, contradictions get flagged, and the index evolves as the domain does. The trade-off is scale. A flat index works well up to a few hundred articles, where the full index still fits in a context window and the LLM can check for gaps and overlap. Beyond that, hybrid retrieval becomes necessary, and the operational complexity grows with it. But for sustained research within a bounded domain, this model performs in a way raw retrieval simply cannot. This is the right choice for engineers doing months of sustained research in a specific technical area: a new language runtime, a framework you're adopting, a domain like security or distributed consensus where foundational concepts keep resurfacing. If you find yourself re-establishing the same background context you've already read through and summarized, L4 with a compilation model is the upgrade you're looking for. The graph model Where the compilation model produces readable articles, the graph model produces a knowledge graph: entities (people, products, decisions, concepts) and the relationships between them, with temporal indexing on every fact. The distinctive capability is temporal reasoning and multi-hop queries. Every fact in the graph carries a validity period. When something changes, the old fact is marked as superseded rather than deleted. This means you can ask "what was the customer's infrastructure configuration at the end of Q3?" and get the right answer, even if that configuration has since changed significantly. You can also traverse relationships across entity types: "which users are affected by this architectural decision?" or "what decisions did this team make during the incident period?" This requires a graph database backend, which adds real operational overhead compared to the compilation model. There is also a latency characteristic that matters for system design: entity extraction and relationship building happen in background processing, so newly ingested information is not immediately queryable. Write-then-read consistency is not a given. This is an architectural tradeoff, not a bug, but teams need to design around it explicitly. The graph model is the right foundation for agent platforms that need to track evolving entities across time: customer accounts, organizational structure, system configurations, incident timelines. Any use case where "what was true at time T" is a real query belongs here. On retrieval at this level Both models benefit from hybrid retrieval, combining semantic vector search, keyword matching (BM25), and graph traversal. Community benchmarks consistently show hybrid approaches outperforming any single method at scale, and the gap widens as the knowledge base grows. This is an implementation choice within L4 rather than a reason to prefer one model over the other. For Track A engineers: L4 is worth the investment when you are doing sustained research in a specific technical domain over weeks or months. Most people do not need it, and adding it prematurely just creates maintenance overhead. The signal that you are ready for it is noticing you keep re-reading the same foundational material because you cannot trust your notes to reflect the current state of your understanding. For Track B teams: this is the knowledge retrieval layer of any serious agent platform. The compilation versus graph decision is an early architectural choice with long-term consequences, because retrofitting temporal and relational reasoning onto a compilation-based system later is painful. Compilation is simpler to operate and the right default when relationship traversal is not a core requirement. Graph unlocks capabilities that are genuinely difficult to approximate otherwise. Tools at this level (as of mid-2026) See the tools reference table at the end of this article for a fuller view. L5: Memory infrastructure At this level, memory stops being a feature of a specific AI tool and becomes infrastructure. A dedicated memory store is exposed via an API or open protocol, and any compliant client can read from and write to it. The AI assistant no longer owns the memory; the memory layer does. This shift in ownership matters more than it might initially seem. At lower levels, memory follows the tool. If you switch tools, you start over. At L5, memory is independent of any tool. What one AI learned in one session, a different AI can access in the next. What one agent discovered about a user, another agent serving that same user already knows. The memory layer becomes the stable part of the system, and the AI tools sitting on top become interchangeable. The open standard increasingly used for this is MCP (Model Context Protocol), which defines a common interface for connecting AI clients to external services. A memory store exposed over MCP can in principle be queried by any MCP-compatible client, which gives you real portability between tools. Two variants worth distinguishing The first is shared personal memory: one person, multiple AI tools, one central store. Set up a database, expose it over MCP, and any AI client you use can contribute to and draw from the same pool of context. Your preferences, project history, past decisions: accessible regardless of which tool you open that day. If you use Claude Code for implementation, Cursor for exploratory edits, and a chat interface for design conversations, they all draw from the same memory. This is still relatively rare in personal setups, mostly because the setup cost is non-trivial and the benefits only become visible once you actually use multiple tools regularly. The second is shared platform memory: multiple users, multiple agents, one central layer with per-user isolation. This is the architecture for a production multi-user system. Memory is namespaced by user ID or organization ID. Each agent retrieves the right user's context at the start of an interaction and writes new context at the end. The memory store becomes a service with the same operational expectations as any other stateful service in your stack. Concerns that appear only at this level Several problems don't exist at lower levels and become real the moment memory moves to a network service. Latency is the first one to check. At lower levels, memory retrieval is local. At L5 it's a network call, and memory retrieval involves both a query embedding step and a vector search, which takes time. As of writing, some memory systems have retrieval latency in the tens of seconds at the 95th percentile under load. Putting that on the synchronous path of a user-facing agent produces visible delays that users notice immediately. Verify latency characteristics before committing to a system, and where possible design retrieval to run asynchronously or speculatively before it's needed. Multi-tenancy isolation is the application's responsibility, not the memory store's. The standard pattern is to namespace every memory entry by user ID or organization ID and enforce that boundary on every read and write at the application layer. This sounds simple and is worth getting exactly right. A failure here doesn't just produce wrong answers; it exposes one user's context to another, which is a trust failure that tends to get noticed. Data residency and ownership become real operational questions. Running an L5 memory layer means operating a database containing user context: where it lives, who has access to it, what happens when a user deletes their account, how long data is retained and why. These questions have answers at smaller scale too, but at platform scale they need written policies and enforcement. Self-hosted deployments give full control at the cost of operational burden. Managed cloud services trade that control for simplicity. Neither choice is wrong; it's worth making it explicitly rather than by default. Cost compounds at scale. Storage, embedding computation, and retrieval operations all carry per-operation costs that are easy to ignore during development and harder to ignore once real users arrive. Where it breaks The hardest memory bugs in AI systems tend to live here. Stale memory that contradicts current reality, with no clear way for the agent to know which source to trust. Contradictory facts written by different agents into the same store, with no reconciliation. Isolation failures that are silent until they cause a visible problem. None of these are unsolvable, but they require deliberate design rather than optimism. L5 is also real infrastructure with real overhead. For personal use, the setup cost rarely justifies the benefit unless you genuinely use multiple AI tools and want one shared context layer across all of them. Most individual continuity needs are covered by L3 at a fraction of the complexity. For Track A , this is probably not where to invest time right now. If you use only one AI tool, or your tools don't yet support MCP-based memory retrieval, the abstraction adds cost without benefit. It's worth keeping an eye on as tooling matures. For Track B , this is where a new architectural conversation begins: memory is no longer just something agents consume, but infrastructure that other systems depend on. The decisions made here (what protocol to use, what database to run, how to handle isolation, how to manage retrieval latency) shape everything built on top. The mistake to avoid is designing agents first and memory last. Memory architecture is a prerequisite, not an afterthought, and retrofitting it into a system that wasn't designed for it is significantly harder than getting it right from the start. Tools at this level (as of mid-2026) See the tools reference table at the end of this article for a fuller view. How to pick your level Routing guide The framework is best thought of as a map rather than a maturity ladder. L1-L4 describe different forms of memory. L5 describes when memory becomes shared infrastructure Personal use (Track A) Start at L2. A well-written system prompt and a project instructions file cover most of the friction engineers describe as "the AI doesn't know me" or "I have to re-explain my setup every time." Most people underestimate how far this gets them before anything more complex is needed. Add L3 or L4 when losing context across sessions becomes genuinely painful: re-explaining a decision you made last week, re-establishing which branch of a design you landed on, re-orienting the AI to where you left off on a project. That's the signal. Not "this might be useful someday" but "this is costing me real time right now." Consider L4 if you're doing sustained research in a specific domain over months and you want knowledge to accumulate and stay findable rather than scattering across session summaries. Not every engineer reaches this point, and that's fine. L5 is rarely worth the setup cost for personal use. The main case where it makes sense is if you actively use multiple AI tools and want one shared context layer across all of them. If you use one tool consistently, L3 handles continuity at much lower complexity. Agent platforms and production systems (Track B) L2 is mandatory. Every agent needs a system prompt, behavioral guardrails, and clear instructions about what it should and shouldn't do. Treating this as optional produces agents that behave inconsistently under novel inputs. L3 gives agents memory of past interactions. The lossy vs lossless choice matters more here than in personal use: too much context and you hit cost and latency ceilings; too little and the agent feels stateless in ways users notice. Getting that balance right requires profiling real usage, not guessing at design time. L4 is where agents go from retrieving chunks of text to reasoning over structured knowledge. The compilation model and the graph model serve genuinely different use cases, and the architectural consequences of that choice compound over time. It's worth thinking through carefully before committing. If L5 is likely to be part of the future architecture, it is worth recognizing that early. Retrofitting shared memory infrastructure into an established platform is usually more expensive than planning for it upfront. Quick routing "I just want it to remember my preferences": L2 "I want it to remember what we worked on last week": L3 "I want to stop re-explaining domain background": L4, compilation model "I need it to track how something changed over time": L4, graph model "Multiple agents or users need shared context": L5 Choosing the right level solves the architectural question of where memory should live and how it should be represented. In practice, however, selecting a level is only part of the challenge. As memory systems become more capable and accumulate more information, a different set of concerns begins to dominate: memory quality, lifecycle management, governance, and trust. Cross-cutting concerns The levels in this framework describe where memory lives, how long it survives, how structured it becomes, and how broadly it is shared. Some challenges, however, appear regardless of which level you choose or combine. In practice, these concerns often have a larger impact on system quality than the choice of storage technology or retrieval algorithm. They emerge as soon as a system starts remembering information, and they become more important as memory accumulates over time. Memory consolidation: how information moves between levels The framework can make the levels appear static. Real memory systems are usually not. Information tends to move between levels as its importance becomes clearer. A conversation starts as an interaction. Parts of it may later be summarized. Important facts may be extracted from those summaries. Over time, stable facts may become structured knowledge, and repeated lessons may eventually become instructions or policies. Conversation → Summary → Fact → Knowledge → Instruction This process is often called memory consolidation . Human memory behaves similarly. Individual experiences become generalized knowledge. Repeated observations become habits and rules. Most long-term knowledge does not enter memory in its final form; it evolves through successive stages of refinement. Modern agent systems increasingly follow the same pattern. An agent may initially record every interaction, but storing everything forever is rarely useful. As memory grows, systems need mechanisms that transform information into more compact, durable, and reusable forms. The goal is not simply to remember more. The goal is to remember the right things. The challenge is deciding what deserves promotion. Promote information too aggressively and important nuance disappears. Promote too little and memory grows without bound, making retrieval slower, more expensive, and less reliable. As memory systems mature, consolidation often becomes more important than storage. The question shifts from: How do we remember more? to: How do we remember what matters? Memory quality becomes the real problem Most discussions about memory focus on storage and retrieval. Which vector database should you use? How should chunks be split? What embedding model performs best? Those questions matter, but they are rarely the first thing that breaks. The moment a system begins writing memory automatically, a different problem appears: memory quality. The agent may misunderstand a conversation and extract an incorrect fact. A user may change their mind while an outdated preference remains stored. Two agents may write contradictory information about the same entity. A hallucinated conclusion may be captured and later retrieved as if it were established truth. The risk exists at every level beyond simple instructions, but the consequences become more severe as memory becomes more structured and more widely shared. At L3, an incorrect memory may simply cause a future conversation to start from the wrong assumption. At L4, that same incorrect memory may become part of a structured knowledge resource, influencing future reasoning and synthesis. At L5, the problem expands further. Incorrect or contradictory memories can propagate across multiple agents, tools, or users, turning a local mistake into a system-wide one. This is sometimes described as memory poisoning, but the underlying issue is broader: a memory system is only as trustworthy as the process that decides what enters it, and what stays in. As systems mature, questions like these become increasingly important: Who is allowed to write memory? Can memories be edited or deleted? How are contradictory memories reconciled? Should memories expire automatically? How much confidence should be attached to a stored fact? Should users be able to inspect and correct their own memory? The retrieval pipeline matters. The quality of what enters the pipeline often matters more. What the levels don't capture The framework is useful for making architectural decisions. It's less useful for a few problems that cut across levels. Self-improving agents (systems that rewrite their own behavioral instructions based on experience, blurring the boundary between L2 and L3) are a real design pattern that doesn't fit cleanly into any single level. The tooling around this is still maturing. It's worth following, but not worth building on top of today unless you have a specific reason to. Team memory governance is a policy problem, not a technology problem. Once memory is shared at L5, questions emerge that no level framework answers: who owns a memory entry, who can delete it, what happens when someone leaves the team, how long things are retained and under what legal obligations. The levels tell you what to build. They don't tell you how to govern it. The ecosystem moves faster than the framework does. Specific tools and platforms will look different in a year. The architectural decision (which level) is more durable than the implementation choice (which specific tool). When something changes in the ecosystem, revisit the tools. Revisit the framework only if the underlying problem has actually changed. Closing The most expensive mistake in building AI-assisted systems isn't picking the wrong tool. It's picking the wrong level and spending months building on top of it. If you're customizing a personal setup, starting low is genuinely the right call. The path from L2 to L3 is easy, and the friction tells you when you're ready to move up. If you're building a production agent system, the advice is different. You can implement incrementally — you don't need everything on day one — but you should think about your target level upfront. L5 shapes the entire memory architecture. Discovering mid-build that you need multi-tenancy or cross-tool access isn't an upgrade; it's a redesign. Know where you're heading before writing the first agent, even if you build toward it gradually. Tools reference Examples as of mid-2026. This space moves fast — treat this as a starting point for evaluation, not a definitive list. Many of the tools listed below span multiple layers and do not fit cleanly into a single category. Their placement in this table is intended to be indicative of their primary focus, not a definitive classification. Level Example tools Pattern they represent Watch out for L1: Session memory Claude Projects, ChatGPT, Gemini Workspace, Cursor, GitHub Copilot Platform-native session memory, no additional setup Memory resets at conversation end; nothing persists across sessions L2: Instruction memory CLAUDE.md / system prompts (any tool), Claude Code memory hooks, Cursor .cursorrules, GitHub Copilot instructions Structured instructions loaded at session start — human-written, static Instructions go stale if not maintained as the project evolves; doesn't capture what happened, only what should happen L3: Episodic memory Mem0, MemSearch, MemPalace, Hermes Agent, claude-mem Automatic session capture with retrieval injection — lossy (summaries) to lossless (verbatim + index) spectrum Language bias: many tools have English-first retrieval quality; verify before committing for non-English content L4: Semantic memory Compilation : LLM Wiki (Karpathy pattern), ktundwal/librarian Graph : Zep / Graphiti, Cognee, LangMem, Hindsight (local) Compilation model compiles raw sources into a maintained encyclopedia; graph model extracts entities and relationships with temporal indexing Compilation: scale ceiling around 200-500 docs before hybrid retrieval is needed. Graph: write-then-read latency due to background processing. LangMem: 59s p95 search latency — background use only L5: Memory infrastructure OpenBrain, AWS AgentCore Memory, Google Memory Bank, Any of the L3/L4 solution you would self-host and share or go with their cloud offer if any Memory as infrastructure exposed via MCP or REST — persists across tools and users Retrieval latency is now a network call (verify p95 before using on sync path); multi-tenancy isolation is the application's responsibility; data residency requires explicit policy decisions
こんにちは、2026年5月に入社し、AI for applicationチームにジョインしました村上です。 キャディ株式会社では、プロダクトが解くべき課題を正しく捉えるには現場業務への解像度が不可欠だという考えから、ドメイン知識をキャッチアップするための仕組みが社内に用意されています。その一つが「調達もくもくワーク」という体験型トレーニングです。 私自身、前職では製造業とは全く関係のない業界でMLエンジニアをしていました。そのため製造業のバックグラウンドはゼロです。 今回、ドメイン知識を身につける機会として上長にお勧めされ、「調達もくもくワーク」に参加しました。 この記事では、製造業未経験のMLエンジニアである筆者がこのワークに参加し、調達業務のペインを体験したうえで感じたことを書きます。 用語の整理 調達業務の概要 調達もくもくワークとは ワークでの体験 1.見積先の選定 手作業 CADDi Quote 2.見積依頼 手作業 CADDi Quote 3.見積結果の査定 手作業 CADDi Quote 体験してどう変わったか AIやシステムが引き受けるべき仕事 製造業全体の最適化の難しさ We are hiring 用語の整理 この記事で使う主要な製造業用語をまとめます。その他の用語は本文中で適宜補足します。 調達 : 自社製品に必要な部品や材料を外部から購入・手配する業務全般を指します。購買とほぼ同義ですが、サプライヤーの選定や価格交渉まで含む広い意味で使われます。 サプライヤー : 部品を製造・納品する外部の加工会社のことです。「協力会社」「外注先」とも呼ばれます。 バイヤー : 調達担当者のことです。サプライヤーに見積を依頼し、発注先を決定する側を指します。 図面 : 製造する部品の設計書です。形状・寸法・材質・表面処理などを記載した技術文書で、調達業務ではPDFや紙でやり取りされます。 見積 : サプライヤーが図面をもとに提示する価格・納期の回答です。複数社から見積を取ることを「相見積」と呼びます。 QCD : Quality(品質)・Cost(コスト)・Delivery(納期)の頭文字です。調達判断の際に考慮すべきパラメータです。 CADDi Quote : キャディが提供する製造業AI見積クラウドです。図面のアップロードから見積依頼・回答比較までをカバーします。 CADDi Drawer : キャディが提供する製造業データ活用クラウドです。図面からの情報の構造化や図面の形状認識が強みの一つになっており、類似した図面の検索が可能になっています。 調達業務の概要 調達担当者の仕事は、設計部門から受け取った図面をもとに外部のサプライヤーに部品の製造を依頼し、QCDを守って納品してもらうことです。大まかな流れは以下のとおりです。 図面受領・確認 : 設計部門から部品表と図面を受け取る。 カテゴリ分け : 図面を見て、切削・板金・購入品などに仕分ける(サプライヤーは加工種別ごとに専門が分かれているため、ここでの振り分けが必要)。 見積先選定 : サプライヤーリストから対応可能なサプライヤーを選ぶ。過去の発注実績と突合し、新規品かリピート品かを判定して加味する必要がある。 見積依頼の送付 : 見積依頼書を作成し、図面を添付してメールで送付する。 見積回答の査定 : サプライヤーからの回答を比較し、QCD観点で発注先を決める。 発注 : 発注書を作成してサプライヤーに送る 調達もくもくワークとは 調達もくもくワークは、CADDi Quoteのユーザーである調達担当者の業務を、サンプルの図面や発注実績などを使って実際に手を動かしながら追体験するトレーニングです。調達業務の一部についてまずCADDi Quoteなし(手作業)で苦しんでから、CADDi Quoteありでやり直します。ビフォー・アフターを自分の手で体験する設計です。ワークでは上記フローのうち、見積先選定・見積依頼の送付・見積回答の査定を体験しました。調達業務にどのようなペインがあり、それをCADDi Quoteがどう解決しているのかを肌で知ることが目的です。実際にはCADDi Quoteなしで一通り体験した後にCADDi Quoteを体験したのですが、読みやすさのため各フェーズの中で両方の感想を書きます。 調達もくもくワークの様子。 硬い雰囲気に見えますが、実際には気軽に質問したり手作業の煩雑さに愚痴を言いながら進めていました。 ワークでの体験 1.見積先の選定 ワークでは「どのような部品をどれだけいつまでに調達する必要がある」というような依頼が調達部門にきている状態からスタートします。それぞれの部品をどこのサプライヤーから調達するかを考える必要があります。どのサプライヤーがどんな加工に対応でき、過去にどんな品質・納期実績があったかというような情報が体系的にまとまっていることは稀です。結果として現場ではベテランの経験則に依存していることもよくあるようです。また、過去にいくらの金額で発注したかという情報はあったとしても、発注には至らなかった見積結果についてまとまっていることはほとんどないようでした。 研修で使用したダミーの取引実績とサプライヤー情報。このような情報がまとまっていること自体ほとんどないそうです。 手作業 手作業ワークでは過去の発注履歴スプレッドシートが用意されていたのでそれを頼りに選定しました。しかし注文実績があっても過去の品質や価格の妥当性はわかりませんでした。経験則がないこともあり自分は見積をもらってから考えれば良いかとなり、多めのサプライヤーに見積を出すことにしました。 今回はスプレッドシートに過去の情報がまとまっていたのでまだマシでしたが、実際にはどこにもまとまっていないことも多いとのことで、効率的な調達先の選定はかなり難しそうだと感じました。適切でないサプライヤーへの見積依頼はサプライヤー側への負担になりますし、見積に対する発注比率も下がることで、信頼関係を損ねることにもなります。 CADDi Quote CADDi Quoteを使ったワークでは過去実績の比較が非常にやりやすくなっていました。過去の発注履歴や図面の情報をCADDiに貯めてもらっていることが前提とはなっていますが、図面IDと必要な加工情報を渡せば過去の発注実績を元にサプライヤーの候補が得られます。発注価格・見積履歴・過去のトラブルなど、見積依頼先を決めるために必要な情報がまとまっていました。 少しだけ過去と異なるような部品を発注する場合、図面IDだけを見ていると新規品であるため発注先の選定を機械的にしづらいという問題があります。しかしCADDi Quoteは今回発注する図面だけではなく、その図面に類似した図面に対する発注実績も加味してくれます。この機能は図面の形状解析が高精度でできるCADDiならではの強みです。 2.見積依頼 見積先を決めた後、各サプライヤーにメールで見積依頼を送ります。図面をPDFとしてメールに添付して送ることも多いようです。サプライヤーごとに見積依頼書の中身や宛先名を変更する必要がありますし、依頼する図面が違うので正しい図面をメールに添付する必要があります。作業自体は単純で機械的に進めていけば良いのですが、ミスを誘発する要素が多いです。 手作業 ワークでは見積先として選定した宛先(実際には研修を担当していただいた先輩のメールアドレス)に見積依頼を送る作業を行いました。各サプライヤーごとに見積依頼書の作成を行い、図面を添付してメールを送るだけの作業です。単純に見える作業ですが実際には時間とストレスがかなりかかります。他社宛の依頼書を送ったり、図面を添付し忘れたりする人が当然続出しました。自分は見積先を多く選定してしまっていたのもあり、結局途中から作業が嫌になり見積依頼先をこっそり減らしてしまいました。ワークでは実務と比べると少量の部品やサプライヤーでの体験だったのですが、本当に真面目にミスなくやろうと思うと1時間以上は平気でかかりそうでした。機械的な作業に感じられ個人的には一番の苦痛でした。 見積書と大量の図面添付の例。一つ一つ図番を確認しながら添付していくのはかなり注意力を消費します。 CADDi Quote CADDi Quoteを使ったワークでは劇的に楽になっていました。上流工程である見積先選定の結果から数クリックするだけで見積依頼書の作成からメールの送信まで自動で行ってくれました。1時間はかかるであろう作業が数秒になるのには流石に感動しました。 3.見積結果の査定 見積依頼を出した後、それぞれのサプライヤーから見積結果が返ってくるのを待ちます。見積結果が出揃ってきたらその結果を取りまとめてどこに発注を出すかを決めることになります。その際にはQCD(品質、コスト、納期)全体を加味して発注先を決める必要があります。 見積にかかる時間はサプライヤーによって当然異なるので見落としがないように気を付ける必要があります。また、一つのプロジェクトの部品の調達だけをしていれば良いわけではないため、見積結果を待っている間には他の部品の調達も並行して進めなければなりません。待ち時間がある分、情報の整理や管理が必要とされるフェーズです。 手作業 ワークでは時間が決まっているので見積結果は用意されている状態でこのフェーズが始まりました。見積結果を見ていると各社フォーマットが異なっており、比較可能な状態に整理すること自体にまず時間がかかりました。 見積結果の例。カラム名が異なり機械的に統合できなかったり、条件付きでの見積結果だったりと単純に比較することが難しいです。 見積金額には条件がついていることもあり、特定の加工をする部分は除いての金額であったり、見積依頼をした部品全てをまとめて一括発注することを条件としたケースでの金額が記載されていることもあり、比較が大変でした。見積結果をスプレッドシートにまとめた後も、その判断は難しいです。結局私はシンプルに加工費を見て一番安い組み合わせを探してそこに発注するという判断をしました。実際には定性的な要素も多く絡み、トラブル対応が柔軟なサプライヤーであるかどうかや、工場の位置が近く輸送に関わる問題があるかどうかなど、複合的に考える必要があるとのことでした。 CADDi Quote CADDi Quoteから送られた見積依頼のメールには専用のフォームが用意されており、サプライヤーはそこから見積結果を返答します。その結果はCADDi Quoteに自動で連携されます。どこに見積依頼を送ったのか、どこから結果が返ってきてどこがまだなのか、見積結果はいくらなのか。こうした情報が一元的に管理されていきます。 金額による比較は当然できますし、サプライヤーごとの注意事項などもまとめておき表示できるため、定性的な評価もある程度できるようです。現状ではサプライヤー側からの条件付きの見積結果については対応できる幅に限度がありそうなため、CADDi Quoteだけで業務を完全に代替できるわけではありません。しかし、人手でやっていた面倒な作業部分をサポートして楽にするという形での貢献になっていそうでした。 体験してどう変わったか AIやシステムが引き受けるべき仕事 今回のワークでは、調達業務における「機械的作業」と「判断業務」の混在が鮮明になりました。頻繁な情報の紐付けや転記、照合といった定型作業は、画面やファイルを行き来しながらミスなく行う必要があり、想像以上に大きな負荷となります。本来、システムが担うべきなのはこうした作業です。調達担当者が真に時間を割くべきなのは、新規サプライヤーの開拓やコスト削減、サプライチェーン全体のリスク戦略といった、人の判断力や創造性が求められる本質的な仕事です。しかし、定型作業に忙殺されている限り、そこに時間は割けません。 MLエンジニアとしての視点では、複雑なアルゴリズムによる解決策に目が行きがちですが、現場ではデータの紐付け自体が手作業で行われており、それが最大のボトルネックになっています。ロジックの精度以前に、データを構造化して蓄積し、活用できる状態を作ることが最優先でした。CADDi Quoteは、こうした泥臭い紐付け作業をシステム化することで、人が本来の業務に集中できる環境を提供しています。 さらに、CADDi Quoteには蓄積されたデータを分析し、調達をより効率化するインサイトを導き出す機能も備わっています。売上の50%以上を占めることもある調達・購買において、これまでベテランの勘に頼っていた決定を、整理されたデータに基づいて行えるようになります。データの蓄積と分析によって、単なる作業効率化を超えた、より戦略的で価値のある調達の実現を目指しています。 製造業全体の最適化の難しさ 調達金額の最適化について考えを深めていくと、次第に「サプライヤー(加工会社)側の大変さ」も気になり始めました。サプライヤーは、基本的に見積依頼に無料で対応します。安すぎる金額を提示すれば受注できても赤字になり、逆に高すぎれば発注をもらえず、見積に費やした工数がタダ働きになってしまいます。図面だけでは条件が曖昧な場合、発注側とのすり合わせにかかるコミュニケーションコストも無視できません。調達業務の負担はバイヤー側だけのものではなく、製造業全体の効率化を考えるには、多数のステークホルダーの目線に立つ必要があると痛感しました。 また、「片側だけの効率化が、必ずしも全体最適につながらない可能性」も感じています。CADDi Quoteによって調達側の作業が楽になり、扱える案件数が増えれば、バイヤーがサプライヤーに送る見積依頼の総数も増えるかもしれません。もちろんシステムの精度が上がれば的外れな依頼は減るはずですが、バイヤー側の処理能力が爆発的に向上することで、結果的にサプライヤー側の対応負荷が変わらないどころか、むしろ増えてしまうというパラドックスも起き得ます。 これは確信のある主張というよりも、ワークで感じた問いです。調達プラットフォームを考えるとき、バイヤー側の効率化だけでなく、サプライヤー側にも見積対応の効率化や受注機会の可視化といった価値をセットで提供していく視点が必要なのだろうと感じました。製造業全体のエコシステムを良くするのは、一方向の最適化では済まない難しい問題です。 「モノづくり産業のポテンシャルを解放する」という大きなミッションを達成するには、このようなステークホルダーが多く複雑な問題も避けては通れません。実際CADDi Quoteもこの大きな問題を解決するための変革が現在進行形で進んでいます。1日の体験ワークでこうした問いが自然に浮かんできたこと自体が、このワークショップの設計のうまさだと感じました。 We are hiring ミクロに実際の現場の問題に向き合いながらも、長期的にはマクロに製造業全体の効率改善を目指していける環境です。 製造業が未経験であっても、このようなワークを通じてドメイン理解を深めることができます。 このような立ち位置で働けることにワクワクを感じる方がいれば、製造業未経験でもぜひお話をしましょう! tech.caddi.com
Data&Analysis部の稲葉です。 キャディは2026年6月10日(水)〜12日(金)にパシフィコ横浜で開催されたSSII2026(第32回 画像センシングシンポジウム)にプラチナスポンサーとして協賛しました。また、キャディから3名が登壇しましたので、当日の様子をレポートします。 オーガナイズドセッション:産業界における生成AIの利活用 技術動向解説セッション:CADにおけるAI分野の動向と製造業への実適用 インタラクティブセッション 終わりに オーガナイズドセッション:産業界における生成AIの利活用 生成AIが産業界でどのように価値を生み、どのような条件で実運用に乗るのかを議論するセッション です。 キャディからは、オーガナイザーとしてシニアリサーチエンジニアの福原( @gatheluck )、また講演者として私、稲葉( @mi_spindel )が登壇しました。会場には約450名と、非常に多くの方にお越しいただきました。 登壇資料はこちらです。 speakerdeck.com speakerdeck.com 私からは、Data&Analysis部として取り組んでいる生成AI活用事例として製造業RAGを紹介しました。 (本当は、他にも様々な取り組みがあるのですが、また機会を見て紹介したいと思います。) また、この中で直面した課題の一つが「各LLM/VLMモデルが我々のドメイン特有のタスクをどこまで実行できているかが不明」というものでした。 そこで、製造業LLM/VLMベンチマークとしてManuDraw-Benchを独自構築し、比較評価したお話しをしました。 皆様からのたくさんの質問をいただきまして、ありがとうございます。当日は時間の関係で回答しきれませんでしたので、ここで回答したいと思います。 Question: CADでのDXFはパースせずに図面の見た目で拾ってしまった方が総合的なコストが安いという判断でしょうか? Answer:DXFなどベクター図面のまま処理した方が、QualityやCostの観点で良いタスクは存在していると思います。ただ、結局はimage encoderが必要なタスクはありますし、処理プロセスの種類が増えることによって運用負荷も増えますので、QCDを総合的に判断して技術選定をしています。 Question:会社の固有知識については基本的にはRAGで対応となると思うのですが、RAGでは対応しきれないのではないか、モデルの Fine Tuning が必要ではないかという意見も社内であり、意見が分かれています。 Fine Tuning まで実施したというような事例はあるでしょうか? Answer:Fine Tuningまで実施した事例があるかないかの詳細はお答えできないのですが、Fine Tuningを考える前に基本はナレッジ管理とGroundingで対応できないかを先に考えるべきだと思っています。また、Fine Tuningと言えど一定のデータ量は確保しなければならないですが、機密情報の漏洩を考えると個社ごとに調整をすることになりがちです。確保できるデータ量とその労力に対する効果を検討する必要があります。 Question:過去の故障事例などは、設計改善に使用可能なレベルで、原因情報が現場から上がってくるモノでしょうか?実は、原因をLLMが把握するところに課題はないでしょうか。 Answer:おっしゃる通りで原因とその対応が上がってくる仕組みを作ることが重要ですし、そこは課題です。その解決のためにも、キャディが提供しているような部横断のデータ基盤を構築しデータを紐づけることが必要になります。 Question:P15のVLMによる3次元点群予測は1点1点を文字列として出力しているわけではないのだと思いますが、Pythonスクリプト出力などでCADモデリングさせて、表面に点を生やしているのでしょうか? Answer:1点1点を点群を文字列として出力させているのではなく、メッシュモデルとして形状を面の組み合わせで出力させてから点群サンプリングをしています。おっしゃるようにPythonスクリプト出力などでCADモデリングする方法もありますが、コーディング能力や各LLM/VLMが扱える開発環境にも依存するため、このような方法を取っています。 Question:位置やサイズなど幾何的な理解の性能は、モデルの進化を待てばある程度上がってきそうなベンチの結果になっているのでしょうか? Answer:そのあたりは今後の傾向を見てみないと何とも言えないところですので、今後もトラッキングしていきます。ただ、自然画像や文字列で表現されている位置やサイズの理解は進化が見られます。 Question:3面図とアイソメ図でVLMからみた理解度にどれだけ差があるのでしょうか?VLMの訓練にメインで使われているであろう写真データには三面図的な見え方は珍しそうに思います。写真に近そうなアイソメ図の方が理解してくれたりするのでしょうか? Answer:とても良いご指摘です。三面図をそのまま与えるよりは、斜めから見たアイソメ図の方が形状の理解力は高い印象です。ただ、アイソメ図には寸法など必要な情報が抜けているので、処理プロセスのどこかでアイソメ図にリフトするとかデータを組み合わせて利用するとか工夫は必要にはなると考えています。 共に登壇いただいたエクサウィザーズの加藤様、LayerXの松村様、素晴らしい発表をありがとうございました。エクサウィザーズ様はやはり取り組まれていることの幅が広いですし、生成AIの性能だけでなくUI/UXやガバナンスといった活用推進の重要なポイントを俯瞰して抑えられているなと感じました。LayerX様はAgentありきの業務フローに変革していく強い意思を伝えながらも、結局は当たり前をやりきれるかという話に共感しました。 登壇の様子 技術動向解説セッション:CADにおけるAI分野の動向と製造業への実適用 昨今の3D基盤モデルなどの潮流を概観し、CAD解析技術の実プロダクト適用における「研究と実践」のリアルを解説するセッション です。 キャディから、Data Platform本部長の今井( @imaimai0 )が登壇しました。こちらも500名近い方に聴講いただくことができました。 speakerdeck.com なぜ製造業においてCADが重要なのか、CADの活用において何がボトルネックになっているのか、最新研究の動向はどうなっているのかをお話ししています。 詳細は資料をご確認いただけると幸いですが、私からは特に、CAD(Geometric系)やMechanical Engineering系の研究開発をもっと盛り上げたいと思っていることをお伝えしたいです。 資料の中でもCVPRにおいてGeometric系の論文は5%程度というお話しがありましたが、製造業のGDPが日本の2割程度を占める巨大産業にも関わらず日本においても類似の研究開発が少ないように思います。我々もResearchチームの規模を拡大中ですし、共同開発や共同研究の機会も探しているところです。一緒に盛り上げていただける仲間を募集中です。 インタラクティブセッション 6/11(木)のブース出展では、多くの方にキャディブースへお越しいただきました。セッションを聴講して「話を聞いてみたい」と立ち寄ってくださった方もいらっしゃいました。 ポスター発表では、現在取り組んでいる研究開発テーマを紹介しました。注力しているテーマとして、以下の3点をご紹介しました。 設計資産全体を横断して検索するための、2D図面と3D CADモデルの埋め込み空間の統合 3D CADデータ内の加工に必要な特徴の認識 製造業ドメインに特化した独自の視覚言語モデル(VLM)評価ベンチマーク ManuDraw-Bench ManuDraw-Benchは公開されているのか?されないのか?といった質問をたくさんいただきましたが、権利の関係上公開は難しいです。ただ、業界の研究開発を促進したいと思っていますので、何かしら対応を考えていきたいとは思っています。 お話いただいた皆様、本当にありがとうございました! 終わりに SSII2026への協賛・参加を通じて、製造業領域におけるAIの研究開発の現状や、その中でのキャディのチャレンジについて知っていただく貴重な機会となりました。また、来場者の方との交流から、日々取り組む課題に対してもヒントを得られる非常に有意義な場でした。 運営の皆様、素晴らしい会をありがとうございました。 今後もキャディは技術的な挑戦を続け、画像センシング・コンピュータビジョン研究コミュニティの発展と、モノづくり産業のポテンシャル解放の両面に貢献していきたいと思っています。 また皆さんとお会いできることを楽しみにしています!
こんにちは!Data&Analysis部の今野です。 キャディは2026年6月8日(月)〜12日(金)にGメッセ群馬で開催されたJSAI2026(人工知能学会全国大会 第40回)にプラチナスポンサーとして協賛させていただきました。 この記事では5日間にわたる開催期間中の当社ブースの出展報告や、聴講した技術セッションのレポートをお伝えします。 会場紹介 ブース紹介 聴講したセッションの感想 非構造データからの情報抽出 画像音声メディア処理:視覚言語理解とマルチモーダル生成 終わりに 会場紹介 まずは会場の様子を紹介します! 場内では複数のセッション・企業展示・ポスター発表が同時並行で実施されていました。企業展示では、多様な業界に跨る159社の企業が、生成AIに限らない幅広い機械学習技術の産業応用に関して紹介をしていました。私も色々なブースを訪れ、エンジニアの皆さんと意見交換をさせていただきました。 場内の案内図 大規模言語モデル等の生成AIとともにフィジカルAIも大きなテーマの一つとなっていて、実際に入口付近ではGMOインターネット株式会社のブースの前でヒューマノイドロボットが来場者を迎えていました。 入口から見た会場の様子 ブース紹介 次にキャディのブースについて紹介します! ブースでは、主に私たちが提供している「製造業AIデータプラットフォームCADDi」の中で使用されている機械学習技術や現在取り組んでいる研究開発テーマの紹介をしました。また本サービスのデモを通して、実際に製造業の現場の課題解決にどのように貢献しているかを実感してもらえるようにしました。     注力してR&Dを行っているテーマの中でも、設計資産全体を横断して検索するための2D図面と3D CADモデルの埋め込み空間の統合・3D CADデータ内の加工に必要な特徴の認識・製造業ドメインに特化した独自の視覚言語モデル(VLM)評価ベンチマーク ManuDraw-Benchの3点について紹介させていただきました。   ノベルティとしては、キャディが各所で出展する度に大人気の「データ活用カツ」や製造業をイメージしたマイクロファイバークロスをお配りしました。   図面が文書としての特徴と幾何学的な形状が融合した複雑なデータであることやその中に潜む活用しきれていない情報の価値であったり、テキストや画像と比べるとあまり一般的でない題材の3D CADに対してどのようなアプローチを課題に応じて選択しているかについて関心を持って頂くことができました。 イベント期間中、キャディのブースに立ち寄ってくださった皆さん、本当にありがとうございました! 聴講したセッションの感想 次に、聴講させていただいたセッションの感想を簡潔にまとめます。 非構造データからの情報抽出 株式会社リクルートの中田さんとSansan株式会社の山内さんがオーガナイザとして実施されたセッションです。 複数ページの請求書 ・医薬品データ・技術マニュアル ・有価証券報告書 など、多種多様な産業の実務に溢れる「非構造データ」から、いかに正確かつ網羅的に情報を抽出するかという共通の課題に対する検証結果が発表されました 。 問題設定及びそれに対して提案された手法は様々ですが、ほとんどの発表においてVLMが手法の中心に据えられていたことが印象的でした。引き続き物体認識やOCRに特化したモデルの有効性は変わらないものの、大規模言語モデルの推論能力を生かした情報の構造化はこの領域において避けては通れないものになっていると感じました。その中で、プロンプトの工夫・画像の前処理・ステップの分割・検索システムの統合など能力を最大限に引き出すための多岐に渡る手法が検証されていました。 図面という非構造化データからの情報抽出は我々の中心にある課題の一つで、日頃の取り組みと密接に関連したセッションであり、これから解析対象のカバレッジを拡張していくに当たり非常に示唆に富んだ内容でした。 https://pub.confit.atlas.jp/ja/event/jsai2026/session/acHLauKQ 画像音声メディア処理:視覚言語理解とマルチモーダル生成 本セッションでは、画像・音声・対話データといったマルチモーダル情報を対象に、人間の認知特性や数理的アプローチを組み合わせて処理のブラックボックスを解き明かし、生成・推論の「制御性と効率性」をいかに向上させるかという共通の課題が議論されました 。 特に印象的だったのは、東京大学の大坂さんによるVLMにおける視覚アクセス境界の存在を実証した研究です 。この研究では、思考のプロセスを言語化させるChain-of-ThoughtプロンプティングをVLMで行う際、モデルが「長く考えること」で視覚的な特徴抽出を拡張しているわけではなく、実際には言語空間内での記号的推論を延長しているに過ぎないという機構を示すものでした。 過去のテックブログ でも紹介しているように、図面からより高度な情報の獲得を目指していくに当たり、VLMのメカニズムに起因する視覚情報に基づく推論の限界は我々も課題に感じており、この問題を新たな視点で捉えられる研究内容であるように感じました。 https://pub.confit.atlas.jp/ja/event/jsai2026/session/fQHM84pJ 終わりに 5日間にわたるJSAI2026への協賛・参加を通じて、AIに関するアカデミックな着目点や産業応用における最新の機械学習技術について幅広い知見を得られる貴重な機会となりました。また、会場での多くの交流や専門的なセッションから、VLMの可能性など我々が日々取り組んでいる課題に対してもヒントを得られる非常に有意義な時間でした。 スピーカーの皆さん、参加者の皆さん、運営の皆さん、素晴らしい会をありがとうございました。 今後もキャディは技術的な挑戦や現場の課題解決に向けた取り組み、そして研究開発の内容を積極的に発信し、エンジニアコミュニティの発展の一助となれるような貢献を続けていきたいと思っています。 次回、また皆さんとお会いできることを心より楽しみにしています!
TL;DR Dependabotの脆弱性アラートをSlackに流し、Devin(AIコーディングエージェント)でトリアージ・修正を行うスキームを構築しました CVE脆弱性アラートを、チームで効率的に対応できるようになりました AIの活用で「セキュリティよくわからないから放置...」が「なるほど、そういう脆弱性なのか。これなら対応できそう」に変わりました はじめに 製造業データ活用クラウド CADDi Drawer開発チームの大道です。 突然ですが、みなさんのリポジトリにDependabotのアラート、溜まっていませんか? 「あ、また増えてる...まあ今は忙しいし、後で見よう」って思っちゃいません? npm install をした際のCriticalやHighの件数を見て、「嫌なものを見たな〜」という気持ちになりませんか? こうして「後で」が永遠に来ないまま、気づけばアラートが大量に溜まっている。そんな経験、ありませんか? この記事では、DependabotとDevin(AIコーディングエージェント)を組み合わせて、CVE対応が「回り続ける」仕組みを作った話を紹介します。セキュリティの専門知識がなくても「とりあえずやってみよう」と思えるような内容を目指しているので、気軽に読んでいただけたらうれしいです。 背景:なぜスキームを作ろうと思ったか 「気づいた人がやる」運用の限界 もともと私たちのチームでは、CVE脆弱性アラートへの対応は「気づいた人がやる」スタイルとなってしまっていました。 Dependabotからのメール通知は飛んでくるのですが、日々の開発メールに埋もれてしまい、気づかないことがほとんどです。たまたま気づいた誰かが善意で対応してくれる。でも、その「誰か」がいない日は、当然誰もやりません。「誰か」がやってくれたことに対する感謝も埋もれがちです。 放置するリスクは一定ある 実際、多くの脆弱性は到達可能性(Reachability)がなく、自分たちのコードから該当の関数を呼んでいないケースがほとんどです。この経験が積み重なると、「今回も大丈夫か」と思ってしまうのは、ある意味エンジニアとしての経験則でもあります。 ただ、最近はリスクを巡る状況も変わってきています。 AIの進化によって、攻撃用のコードを生成するハードルが大きく下がりました。以前であれば高度な知識が必要だったエクスプロイトの作成も、今ではAIを使えば比較的容易にできてしまう時代です。その結果、ゼロデイ攻撃のスピードは加速し、公開される脆弱性の件数自体も増加傾向にあります。 そして、もしセキュリティインシデントが起きてしまった場合、事業へのインパクトは計り知れません。顧客データの漏洩、サービスの停止、信用の失墜...。「大丈夫だろう」の一言では済まされない世界です。 だからこそ、エンジニアとしては微妙なストレスを抱えているのも事実ではないでしょうか。「たぶん大丈夫」と思いつつも、心のどこかで「もしこれがインシデントにつながったら...」とビクビクしている。そのストレス、放置しているアラートの数に比例して大きくなっていませんか? DependabotのPR自動作成機能に対する要望 Dependabotには検知した脆弱性に対して、自動でPRを作成してくれる便利な機能がありますね。 ですが、「もっとこうなったらよいな」と思うものもありました。 変更の妥当性を判断する情報が乏しい : 「このパッケージをx.x.xからy.y.yに上げます」としか書かれていない。破壊的変更があるのか、自分たちのコードに影響があるのか、PRを見ただけでは判断しづらい プロジェクト固有のルールを守ってくれない : 私たちのチームでは「lockfileの直接編集禁止」「overridesは原則使わない」といったガイドラインがありますが、Dependabotにそれを強制する術がない (もしかしたらRenovateなら設定を駆使すればこのあたりは回避する方法があるかもしれませんね) 結局、PRが来ても「これ、マージして大丈夫...?」と不安になり、確認コストが高くなりがちでした。 セキュリティの知見、チームで底上げしたい もうひとつ、スキームを作りたかった理由があります。 脆弱性対応は、最終的には人間が判断する必要があります。「この脆弱性は本当に自分たちのアプリに影響があるのか?」「Hotfixを当てるべきか、通常リリースでいいのか?」。こうした判断は、AIに丸投げするわけにはいきません。 そもそも、「どのような脆弱性なのか」がわからないという問題もあります。SQLインジェクション、XSS、プロトタイプ汚染...。名前は聞いたことがあっても、具体的にどういう攻撃手法で、どうガードすればいいのか。アプリケーションエンジニアとして、こうした知識は本来持っておいたほうがいいはずです。 そういった知見は、自分たちのアプリケーションに対してコードを書いたり、AIが書いたコードをレビューする際にも役に立つはずです。 ただ現実には、こうしたセキュリティの知識を、アプリケーションエンジニアが体系的に学ぶ機会はそう多くありません。業務の中で自然と身につく類のものでもない。だからこそ、CVE対応のプロセス自体を「学びの機会」にできないか、という思いもありました。 実際に組んでみたスキーム 全体のフローは以下のようになっています。 graph TD A[Dependabot検知] --> B{Severityがチームの既定値以上?} B -- No --> C[無視] B -- Yes --> D[Slack通知] D --> E[Code Ownerがアサイン] E --> F[担当者がトリアージ] F --> G{Hotfix必要?} G -- Yes --> H[即時対応・特殊フロー] G -- No --> I[通常修正] I --> J[AIレビュー実施] J --> K{Humanレビュー必要?} K -- Yes --> L[Human Review] K -- No --> M[Merge & Slack報告] L --> M それぞれのステップを見ていきましょう。 Step 1. まずSlackに流す ― 「気づく」の自動化 最初にやったのは、Dependabotのアラートを Slackに流すこと でした。 GitHub Actionsで1時間ごとに新規アラートをチェックし、Incoming Webhookで専用のSlackチャンネルに通知します。チームリーダーへのメンション付きなので、見逃しにくい仕組みです。 やっていること自体はシンプルですが、 「普段見ているインターフェースに情報を届ける」 というのが大事なポイントです。メールは埋もれますが、Slackは(良くも悪くも)みんな見ています。通知先を変えるだけで、「気づかなかった」という問題がほぼなくなりました。 Step 2. Devinにトリアージを依頼 ― 「理解する」のハードル低下 アラートに気づいたら、次はトリアージです。 ここで活躍するのがDevinです。Slackのスレッドから、こう打つだけです。 @Devin !triage_cve CVE-XXXX-XXXXX Devinは事前に設定したPlaybookに従って、以下のことを調べてくれます。 脆弱性の概要と深刻度 : CVEの内容を平易な日本語でまとめてくれる 到達可能性(Reachability) : 脆弱性のある関数を自分たちのコードが実際に呼んでいるかを調査 devDependenciesかどうか : 開発環境のみの影響なら緊急度が下がる Hotfixの必要性 : 上記を踏まえて、通常リリースで良いか、Hotfixが必要かの判断材料を提示 SlackにDevinがトリアージ結果を投稿した様子 これが本当にありがたいのです。以前は「CVEの英語ドキュメントを頑張って読み解く」必要がありましたが、今はDevinが要点をまとめてくれるので、エンジニアは 「判断する」ことに集中 できます。 セキュリティに詳しくないメンバーでも、Devinのレポートを読めば「なるほど、これはdevDependenciesだけだから緊急度は低いな」「これは実際にリクエスト処理で使っているライブラリだから早めに対応しよう」と判断できるようになりました。 ただし、最終的な判断は必ず人間が行います。 これはエンジニア組織のルールとして、キャディ株式会社 共同創業者兼最高技術責任者 CTOの小橋から明確なメッセージが発信されています。AIは優秀ですが「責任」はとってくれません。 Slackのスレッドでやりとりされるので、迷ったらリーダーや有識者に相談するのもやりやすいです。 Step 3. Devinに修正も依頼 ― 「直す」の効率化 トリアージが終わったら、修正です。ここでもDevinに頼ります。 @Devin !Patch_cve DevinはPlaybookに定義された修正ガイドラインに沿って、PRを作成してくれます。 私たちのチームでは、以下のような修正ルールを定めています。 セマンティックバージョニングの範囲内で更新する : メジャーバージョンを勝手に上げない。 overridesは原則使わない : 親パッケージが指定した範囲内で一時的に引き上げるために使うのであればよいが、レンジ外のバージョンへ無理やり上書きすると、テストされていないパッケージの組み合わせになるリスクがある。 Dependabotにはこうしたプロジェクト固有のルールを守らせることが難しかったのですが、DevinならPlaybookに定義しておくことで遵守させることができます。これが、修正をDevinに任せている大きな理由です。 PRが作成されると、AIレビュー(Devin ReviewまたはCopilot Review)が自動で実施されます。もちろん、Humanのレビュアーも必ずアサインします。 Step 4. 週次定例でカバー ― 仕組みの穴を塞ぐ どんな仕組みにも漏れはあります。リーダーが休みでアサインが遅れた、Devinの修正がうまくいかなかった、親パッケージが未対応でそもそも直せない...。 こうした漏れを拾うために、週次の定例MTGで滞留しているアラートをチェックしています。 直接対応できないケース(間接依存のパッケージに修正パッチが出ていない場合など)は、チケットを起票して管理します。フィルターで一覧化し、定例MTGにて状況を確認しています。 そしてもうひとつ、この場で 対応してくれたメンバーをちゃんとレポートに載せる ようにしています。脆弱性対応は地味な作業です。やっても新機能のリリースのように派手に褒められることは少ない。だからこそ、「あなたが対応してくれたおかげで安全になりました」と可視化することが大切だと考えています。 振り返って重要だと思ったこと このスキームを運用してみて、いくつか大事だと感じたことがあります。 普段見る場所に通知する、それだけで変わる 正直、これが一番効果がありました。 メールからSlackに通知先を変えただけで、アラートへのリアクション速度が段違いに変わりました。人は「目に入るもの」には反応するんですよね。「気づきの導線設計」は、ツールの選定以上に大事かもしれません。 AIがセキュリティのラーニングコストを下げてくれた CVEの内容を理解し、影響範囲を調査し、修正方針を考える。以前はこの一連の作業が重くて、「できる人」に偏りがちでした。 AIが一次調査を肩代わりしてくれることで、セキュリティに詳しくないメンバーでも対応できるようになりました。AIは「判断するための情報収集コスト」を下げてくれる存在です。 「セキュリティの知見がないから自分には無理」というハードルが、チーム全体で下がったのは大きな変化でした。 でも、最後は人間が判断する AIが便利だからといって、すべてを任せるわけにはいきません。 Hotfixを当てるかどうかの判断 破壊的変更を含むアップデートの影響範囲の最終確認 本当にマージして問題ないかの判断 これらは人間の責任です。そして、AIのおかげで「判断に必要な材料を集めるコスト」が下がっているからこそ、人間が判断そのものに集中できるようになったとも言えます。 やった人をちゃんと認知する 繰り返しになりますが、脆弱性対応は報われにくい作業です。ユーザーに見える新機能を作るのとは違って、「何も起きなかった」が成果になります。 だからこそ、週次レポートで対応者を可視化する仕組みは大事にしています。「ありがとう」が自然に言える仕組みは、運用を続けるための燃料です。 おわりに 「脆弱性のアラート、見なかったことにしよう...」 この記事を読んでくださった方の中にも、そんな経験がある方がいるかもしれません。 でも、仕組みを作ってみて思ったのは、 負債はちゃんと細かく返していったほうが、精神衛生上いい ということです。溜めれば溜めるほど重くなるし、見ないフリをしている間のストレスは意外と大きい。 完璧な運用じゃなくていいんです。「回り続ける仕組み」をまず作ること。そしてAIの力を借りれば、セキュリティという重たいテーマのハードルも、思ったよりずっと下がります。 この記事が、みなさんの脆弱性対応の第一歩になれば幸いです。
はじめに こんにちは。Reliabilityグループ QAエンジニアのmeguroです。 最近、開発現場でAI Agentがコードを書く場面が増えてきました。Claude CodeやGitHub Copilotを使えば、APIサーバーの実装が数分で完成します。テストコードも自動生成される。開発スピードは確かに上がっています。 しかし、QAエンジニアとして、あることが気になりました。 「AI Agentが生成したテストコードの品質は、どうやって担保すればいいのか?」 同じAI Agentなのに、時には網羅的なテストを書き、時には重要なケースが抜け落ちている。この品質のばらつきは、一体何が原因なのか。 QAとして、AI時代のテスト品質をどう管理すべきか。その答えを探すため、実験を行いました。同一のタスク管理APIを3パターンの仕様書で実装し、生成されたテストコードの品質を定量的に比較してみました。 結果は明確でした。 仕様書の書き方次第で、AI生成テストの品質が2〜3倍変わる。 AI Agentは「書かれたことは確実にテストする」が「書かれていないことはテストしない」という特性があります。 つまり、 仕様の完全性が品質を決定する のです。 この記事では、その実験の過程と、品質を担保するための具体的な知見をお伝えします。 実験設計:同一APIを3パターンの仕様で実装 実験の概要 今回の実験は、特定の仮説を検証するのではなく、「AI Agentの特性から逆算して、品質を担保するために必要な要素を導き出す」という演繹的なアプローチをとりました。 AI Agentには「指示されたことは忠実に実行するが、行間は読まない」という特性があります。この前提(公理)から、「人間が暗黙的に行っている思考」をどの段階まで具体化して与えれば、納得する品質に届くのかを検証するため、以下の3段階のPoCを設計しました。 3つのPoC poc-1-minimal(最小限仕様) エンティティ定義 エンドポイント一覧 エラーマッピング表 認証・認可ルール poc-2-tdd(TDD手法追加) poc-1の仕様 + TDD手法の指示 実装順序の指定 具体的なテストコード例 poc-3-strategy(テスト戦略追加) poc-1の仕様 + テスト戦略ドキュメント 5つの品質指標の定義 検証すべき仕様項目の具体例 評価指標(5つの品質メトリクス) テストコードの品質を測るため、過去のバグを分析し、実際に見落とされがちな5つの指標を設計しました。 Schema Coverage(スキーマカバレッジ) 定義:レスポンスの全フィールドが検証されているか 計測方法:検証済みフィールド数 / 全フィールド数 目標:100% Post-Condition Coverage(事後条件カバレッジ) 定義:書き込み操作で「対象レコード・関連レコード・不変条件」の3点が検証されているか 計測方法:検証済み事後条件数 / 全事後条件数 目標:100% Error Mapping Consistency(エラーマッピング一貫性) 定義:HTTPステータスコードが全エンドポイントで一貫しているか 計測方法:実装済みエラーパターン数 / 全エラーパターン数 目標:100% Authorization Coverage(認可カバレッジ) 定義:全ロール×全エンドポイントの組み合わせがテストされているか 計測方法:テスト済み組み合わせ数 / 全組み合わせ数 目標:100% Abnormal Test Ratio(異常系テスト比率) 定義:4xx/5xxテストの割合 計測方法:異常系テスト数 / 全テスト数 目標:60%以上 実験結果:仕様の質が品質を決める さて、実際にAI Agentに実装させ、その実験結果を見ていきましょう。 定量結果サマリー 指標 poc-1 poc-2 poc-3 戦略 改善率 Schema Coverage 43% 61% 100% 2.3倍 Post-Condition Coverage 22% 39% 100% 4.5倍 Error Mapping Consistency 31% 90% 97% 3.1倍 Authorization Coverage 48% 80% 100% 2.1倍 Abnormal Test Ratio 53% 58% 48% 0.9倍 注目すべき3つの発見 結果を分析していて、特に興味深かった点を3つ紹介します。 発見1:TDD手法はError Mapping Consistencyで劇的改善(31%→90%) poc-2では、 CLAUDE.md に具体的なテストコード例を含めました。 その結果、AI Agentはこのパターンを全8エンドポイントに自動適用し、401テストが網羅的に生成されました。一方、poc-1では「認証なしは401を返す」とテキストで書いていましたが、テストは1エンドポイントにしか書かれませんでした。 学び:抽象的な指示(「全エンドポイントで認証テストを書く」)よりも、具体的なコード例の方がはるかに効果的です。 発見2:テスト戦略はスキーマ・認可で100%達成 poc-3の docs/test-strategy.md には、全32通りの検証パターンをマトリクスで指示しました。 結果、AI Agentはその表を見て「全8エンドポイント × 4ロール = 32パターン」のテストを自動生成してくれました。さらに、Schema Coverageについても、Protoの全フィールドを1つずつ検証するテストコードを生成しました。 学び:「何を検証すべきか」が具体的に列挙されていれば、AI Agentは漏れなく実装してくれます。 発見3:poc-1でも53%の異常系テストは書かれる 意外だったのは、最小限の仕様でも、異常系テストは過半数書かれていたことです。これは、エラーマッピング表(404/400/403/500の定義)を明示していたおかげでした。 ただし、一貫性に欠けていました。例えば POST /projects には401テストあり PATCH /tasks には401テストなし 学び:「何をテストすべきか」の明示的な指示がなければ、AI Agentは恣意的にテストを選んでしまいます。 なぜAI Agentには異なる仕様が必要なのか? ここまでの結果を見て、「なぜこんなに差が出るのか?」と疑問に思われた方もいるかもしれません。 その理由は、人間とAI Agentの根本的な違いにあります。 人間とAI Agentの根本的な違い 人間の開発者: 仕様(80%完成) + 暗黙知 + 経験 + コミュニケーション → 完全な実装 AI Agent: 仕様(100%必須) → 実装品質 品質を担保するために意識すべき6つの観点 この実験を通じて明らかになったのは、「頭の中で考えている品質の視点」を、明示的に定義することの重要性です。 従来、エンジニアは「暗黙知」を持っていました。 人間の開発者なら、レビューやコミュニケーションで補えました。 しかし、AI Agent時代では、思考プロセスを仕様書に明記する必要があります。 以下、品質を担保するために意識すべき6つの観点と、実験で得られた知見を紹介します。 品質観点1: エラーハンドリングの一貫性 なぜ重要か エンドポイントごとにエラーハンドリングがバラバラだと、以下の問題が起きます ユーザー体験の不整合:同じ「認証なし」なのに、401と500が混在 監視設計の困難:どのステータスコードを監視すべきか曖昧 障害対応の遅延:本番で「これは404か500か?」の切り分けに時間がかかる 「全エンドポイントで一貫したエラーマッピング」を担保する必要があります。 従来の検証方法と課題 実装後にテストコードをレビュー 手動テストで各エンドポイントのエラーレスポンスを確認 → 8エンドポイント × 3エラーパターン = 24ケースを人手で確認 → 実装後に気づいたときは既に手遅れ 品質を設計で担保する 実験では、以下を事前に定義しました: ## エラーマッピング(全エンドポイント共通) | エラー種別 | HTTPステータス | 内部エラー | |-----------|----------------|-----------| | 認証なし | 401 | JWT検証失敗 | | リソース不存在 | 404 | sql.ErrNoRows | | DB障害 | 500 | その他すべて | ## テスト要件 - 全8エンドポイントで401テストを実装 - 全8エンドポイントで404テストを実装 - 全8エンドポイントで500テストを実装 実験結果: Error Mapping Consistencyが31%→97%に改善 エラーハンドリングの一貫性は、品質の基本です。実装前に表として定義することで、レビュー工数を削減し、品質を担保できます。 品質観点2: 事後条件の検証 — 副作用を見逃さない なぜ重要か 書き込み操作(POST/PATCH/DELETE)では、対象レコードだけでなく、関連レコードや不変条件も変化します。 典型的な見落とし - POST /projects でProjectは作成されるが、作成者がMemberに追加されない - DELETE /projects でProjectは削除されるが、関連するTaskが残ってしまう - 不変条件の破壊(例:オーナーが0人のプロジェクトが存在する) 「対象レコード」「関連レコード」「不変条件」の3点セットで検証する必要があります。 従来の検証方法と課題 テストコードで「レスポンスが200 OK」だけ確認 → DBの状態は検証されていない → 本番で「データが不整合」のバグ報告 → レスポンスが正しくても、DB状態が壊れている 品質を設計で担保する 実験では、事後条件を3点セットで定義しました ## 事後条件(POST /projects) 以下の3点をIntegration Testで検証すること: 1. 対象レコード: Project行が1件作成される(status=active) 検証SQL: SELECT status FROM projects WHERE id = $1 2. 関連レコード: Member行が1件作成される(role=owner, user_id=作成者ID) 検証SQL: SELECT COUNT(*) FROM members WHERE project_id = $1 AND role = 'owner' 期待値: 1 3. レスポンス: Proto全フィールドが含まれる 実験結果: Post-Condition Coverageが22%→100%に改善(4.5倍) 書き込み操作の品質は「レスポンスが正しい」だけでは担保できません。DB状態を直接検証する設計が必要です。 品質観点3: スキーマの完全性 — フィールド欠落を防ぐ なぜ重要か APIレスポンスで「主要なフィールドは返るが、一部のフィールドが欠落」というバグは頻発します。 典型例 - Protoにstatusフィールドを追加したが、ハンドラで返し忘れ - フロントエンドでundefinedエラー - 本番リリース後に発覚 「Protoの全フィールドがレスポンスに含まれる」を担保する必要があります。 従来の検証方法と課題 テストで主要フィールド(id, name)だけ検証 → 追加したフィールドは検証されない → 「主要な機能は動く」が「追加機能が壊れている」 品質を設計で担保する 実験では、全フィールドを明示的に列挙しました ## Contract Test要件 GET /projects/:id のレスポンスで、以下の全フィールドが非ゼロ値で返ることを検証: - id (string) - name (string) - description (string) - status (enum: active/archived) - owner_id (string) - created_at (timestamp) - updated_at (timestamp) テストコード例: assert.NotEmpty(t, project.ID) assert.NotEmpty(t, project.Name) assert.NotEmpty(t, project.Status) // ... 全7フィールドを検証 実験結果: Schema Coverageが43%→100%に改善(2.3倍) フィールド欠落は、型チェックでは検出できません。全フィールドを明示的に検証する設計が必要です。 品質観点4: 認可の網羅性 — 権限チェック漏れを防ぐ なぜ重要か 認可のバグは、セキュリティインシデントに直結します。 典型例 - オーナーだけが実行できるDELETE /projects/:idを、メンバーも実行できてしまう - 本番で「他人のプロジェクトを削除できる」バグ報告 - → セキュリティインシデント 「全ロール × 全エンドポイントの組み合わせ」を検証する必要があります。 従来の検証方法と課題 「オーナーは実行できる」だけテスト → 「メンバーは実行できない」はテストされない → 正常系だけでは、セキュリティホールを防げない 品質を設計で担保する 実験では、全組み合わせを表で定義しました: ## Authorization Coverage: 100% 全8エンドポイント × 4ロール = 32パターンを検証 | エンドポイント | 認証なし | 非メンバー | メンバー | オーナー | |--------------|---------|----------|---------|---------| | GET /projects | 401 | 200 | 200 | 200 | | POST /projects | 401 | 201 | 201 | 201 | | PATCH /projects/:id | 401 | 403 | 403 | 200 | | DELETE /projects/:id | 401 | 403 | 403 | 204 | (以下、Task/Memberエンドポイントも同様) 実験結果: Authorization Coverageが48%→100%に改善(2.1倍) 認可の品質は「正常系が動く」だけでは担保できません。全組み合わせを明示的に検証する設計が必要です。 品質観点5: リスクベースのテスト優先度 なぜ重要か すべてのバグが同じ重要度ではありません。 - 金銭的損失につながるバグ(決済、課金) - セキュリティインシデントにつながるバグ(認可、認証) - ユーザー体験を大きく損なうバグ(データ削除、不整合) 「リスクの高い領域ほど、異常系テストを厚くする」必要があります。 品質を設計で担保する 実験では、リスク評価を異常系テストのパターン数に変換しました ## リスク評価とテスト戦略 ### 高リスク領域: DELETE /projects(データ削除) 異常系テスト5パターン必須: 1. 他人のプロジェクトを削除 → 403 2. 存在しないプロジェクトを削除 → 404 3. DB障害時の挙動 → 500 4. トランザクション途中でタイムアウト → ロールバック確認 5. 外部キー制約違反 → 適切なエラーメッセージ 「重大度:高」という抽象的な表現ではなく、具体的なテストパターン数で優先度を表現します。 品質観点6: 暗黙知の明示化 なぜ重要か 「ベストプラクティス」「適切に実装」といった抽象的な要求は、人によって解釈が異なります。 典型例 - 「削除は適切に実装」→ 物理削除を実装 - → 監査ログ要件を満たせない - → 「適切」の定義が曖昧だった 「なぜそうするのか」を明示する必要があります。 品質を設計で担保する 実験では、「〜しない理由」も記載しました ## 削除の実装方針 ソフトデリートを使用すること 理由: - 監査ログ要件:誰が、いつ削除したかの記録が必要 - 誤削除時の復旧:ユーザーからの「間違って消した」問い合わせに対応 - データ分析:削除されたプロジェクトの傾向分析が必要 物理削除しない理由を明記することで、実装時の判断ミスを防ぐ 暗黙知を明示することで、「なぜこの仕様なのか」が後から読んでも分かる仕様書になります。 品質を担保するために必要なこと ここまで紹介した「6つの品質観点」を実務で体現するために、AI Agent時代には以下の「5つの力」が求められます。 ①品質観点を言語化する力 頭の中にある「これはテストすべき」という直感を、仕様書に落とし込む。 ❌「適切にテストすること」 ✅「全8エンドポイント × 3エラーパターン = 24テスト必須」 ②網羅性を設計する力 「全エンドポイント」「全フィールド」という曖昧な表現を、数値化・具体化する。 ❌「全エンドポイントで一貫」 ✅「8エンドポイント × 4ロール = 32パターン」 ③リスクを評価する力 どこを厚くテストすべきかを判断し、異常系のパターン数で表現する。 高リスク領域(DELETE操作)→ 異常系5パターン 低リスク領域(GET操作)→ 異常系3パターン ④副作用を見抜く力 対象レコードだけでなく、関連レコードや不変条件も検証対象として定義する。 対象レコード + 関連レコード + 不変条件 = 3点セット ⑤暗黙知を明示化する力 「なぜそうするのか」を書くことで、実装時の判断ミスを防ぐ。 「ソフトデリートを使う(理由:監査ログ要件、誤削除時の復旧)」 これらは、AI Agentには判断できません。 まとめ AI Agent時代でも、品質を担保するのはエンジニアの役割です。 ただし、その方法が変わりました。 従来:実装後にテストを書き、レビューでバグを見つける AI時代:実装前に「何を検証すべきか」を仕様として定義する 「暗黙知」として持っている品質観点を、明示的に仕様書に落とし込むこと。 これがAI時代の品質保証になってくるのではないでしょうか。 質問やフィードバックがあれば、ぜひコメントで教えてください!
はじめに 10年以上前、単語を単なる記号ではなく、意味の近さを「距離」で表現する高次元ベクトルへと数値化する技術「Word2Vec」が登場しました。これが、AIの重要技術の1つである「埋め込み(Embedding)技術」が研究界で大きな注目を集める契機となりました。 その後、この技術は単語から文章、さらには画像へと適用の幅を広げ、AIは次第にそれらの「意味」を理解し始めます。類似する文章や画像を検索する精度は飛躍的に向上しましたが、当時はまだ、文章と画像を同じ尺度(共通のベクトル空間)で扱うことは困難でした。 その壁を打破したのが、2021年にOpenAIが発表した「CLIP」です。CLIPは文章と画像で共通の埋め込みを作成することに成功し、いわば両者の意味を繋ぐ「共通言語」を誕生させました。このマルチモーダル埋め込みの成功を皮切りに、現在は音声や3Dデータなど、あらゆる情報を共通の埋め込み空間で扱う研究が加速しています。 現在、製造業においてもこれらの技術は浸透しつつあります。画像埋め込みを用いた「図面類似検索」や、テキスト埋め込みを応用した「RAG(検索拡張生成)」などがその代表例です。しかし、現状では単一モーダルの活用が主流であり、マルチモーダル埋め込みの真の社会実装はまだ始まったばかりと言えるでしょう。 マルチモーダル埋め込みの研究分野での盛り上がりを鑑みれば、今後この領域が製造現場に社会実装されていくのは間違いありません。本記事では、マルチモーダル埋め込みの概要から最新の研究事例、そして製造業における活用の未来について詳しく解説します。 「埋め込み」とは? この節では、そもそも「埋め込み」「マルチモーダル埋め込み」とは何かを説明します。 「埋め込み」とは、一言で言えば「現実世界のあらゆる情報を、AIが計算しやすい『多次元空間上の座標(ベクトル)』に変換する技術」のことです。人間が言葉の意味を理解するように、コンピュータに「概念の近さ」を数値として教え込むプロセスだと考えると分かりやすいでしょう。 埋め込みを使うことで、次の3つのことを実現します。 1. 「記号」を「座標」に変える コンピュータは本来、数字しか扱えません。かつて、AIに「リンゴ」という言葉を教える際は、単に「101番」という背番号(ID)を割り振るだけでした。しかし、これでは「リンゴ」と「ナシ」が似ているという意味のつながりを計算できません。 埋め込み技術は、エンコーダーを使い、例えば「リンゴ」を [0.12, -0.54, 0.88, ...] という数百次元の数値の並び(ベクトル)に変換します。この数値は、その対象が持つ「甘み」「赤い」「果物」といった無数の特徴を凝縮した地図上の座標のようなものです。 2. 「近さ」で意味を測る データが座標(ベクトル)になると、データ同士の「距離」を計算できるようになります。 意味が似ているもの: 空間上で近くに配置される(例:「犬」と「猫」) 意味が異なるもの: 空間上で遠くに配置される(例:「犬」と「数学」) 3. 多様なデータを一箇所に集める(マルチモーダル) 最新の埋め込み技術の凄さは、テキストだけでなく、画像、音声、センサーデータまでも同じ空間に並べられる点にあります。 例えば図1のように、「犬が走っている」というテキスト、ゴールデンレトリバーの画像、「ワンワン!」という鳴き声、そして走る犬の動画。これらは入り口こそバラバラですが、それぞれの専用エンコーダーによって「ベクトル(数値の並び)」へと変換されます。 変換されたデータは、多次元の「埋め込み空間」の中に配置されます。この空間の最大の特徴は、「意味が似ているものほど、近くに配置される」という性質です。この例では、データ形式が違っていてもすべて「犬」という同一のコンセプトに関連しているため、AIの頭の中(空間)では同じエリアにギュッと集まります。 これにより、AIはデータ形式の壁を越えて、「これは『犬』という本質的な概念だ」と一貫して捉えることが可能になるのです。 図1 マルチモーダル組み込み 犬の例 埋め込みの歴史 埋め込み技術は、わずか10年余りの間に驚くべき進化を遂げてきました。ここでは、その進化の道筋を象徴する3つのマイルストーンを紹介します。「単語だけ」から始まった技術が、「画像と言葉の融合」を経て、「3Dシーンの丸ごとアライメント」へと到達する物語です。 Word2Vec ── 言葉に「座標」を与えた原点 Mikolov et al., 2013 2013年、Googleの研究チームが発表したWord2Vecは、「言葉の意味を計算できるようにした」という点で、自然言語処理の歴史を塗り替えました。 それまでの限界:One-hotベクトル Word2Vec以前、コンピュータに単語を教える方法は非常に素朴でした。「One-hotベクトル」と呼ばれる方式では、語彙数と同じ長さのベクトルを用意し、該当する単語の位置だけを「1」、残りをすべて「0」にします。 例えば語彙が5万語あれば、「リンゴ」は5万次元のベクトルの中で、たった1箇所だけ「1」が立っている状態です。この方式には2つの致命的な欠陥がありました。 次元の呪い: 語彙が増えるほどベクトルが膨大かつ疎(スカスカ)になり、計算効率が極めて悪い。 意味の不在: 「リンゴ」と「ナシ」のベクトルは完全に直交しており、距離を測っても「近い」とも「遠い」とも言えない。つまり、意味の類似度が一切計算できない。 Word2Vecの発想の転換 Word2Vecは、この問題をシンプルかつ強力なアイデアで解決しました。「ある単語の意味は、その周囲に出現する単語によって決まる」という言語学の仮説(分布仮説)に基づき、大量のテキストから単語の「使われ方のパターン」を学習したのです。 具体的には、ニューラルネットワークに以下の2つのタスクのいずれかを解かせます。 CBOW: 「昨日 ___ を食べた」→ 空欄に入る単語を、周囲の文脈から予測する。 Skip-gram: 「リンゴ」という単語から、その周囲に出現しやすい単語(「食べた」「赤い」「果物」など)を予測する。 このタスクを何億もの文章で繰り返すことで、各単語は数百次元の密なベクトル(分散表現)へと圧縮されます。「リンゴ」と「ナシ」は似た文脈で使われるため、自然と空間上で近くに配置されるのです。 なぜ衝撃的だったのか Word2Vecが世界を驚かせたのは、学習されたベクトルが「意味の足し算・引き算」を可能にしたことです。 「王様」−「男」+「女」=「女王」 この計算が成立するということは、ベクトル空間の中に「性別」「階層」といった意味の軸が自然に形成されていることを意味します。人間が明示的に教えたわけではなく、AIが大量のテキストから自ら意味構造を発見した ── この事実は、後の埋め込み技術すべての出発点となりました。 CLIP ── 画像と言葉の壁を壊した転換点 Radford et al., 2021 Word2Vecが「単語同士の距離」を測れるようにしたのに対し、2021年にOpenAIが発表したCLIP(Contrastive Language-Image Pre-training)は、さらに大胆な問いに挑みました。「画像と言葉の距離を測ることはできないか?」── CLIPの答えは、驚くほどシンプルでした。 4億枚の「画像+キャプション」で学ぶ CLIPは、画像用とテキスト用の2つの独立したエンコーダーを備えています。学習に使ったのは、インターネットから収集した4億セットの「画像とキャプションのペア」。この膨大なデータを使い、以下のルールでベクトル空間を鍛え上げます。 正しいペアは近づける: 犬の画像と「走る犬」というキャプション → ベクトルを空間上で近くに配置。 無関係なペアは遠ざける: 犬の画像と「青い車」というキャプション → ベクトルを空間上で引き離す。 これが「対照学習(Contrastive Learning)」です。正解と不正解を対比させながら空間を整えていくことで、画像とテキストが同じ座標系で「意味的に比較可能」になります。 「見たことがないもの」を理解できる CLIPの最も画期的な特徴は、「Zero-shot性能」と呼ばれる能力です。従来の画像認識モデルは、「犬」「猫」「車」などのラベルを事前に教え込む必要がありました。学習していないカテゴリは認識できません。 CLIPは違います。ラベルではなく「言語の概念そのもの」を学習しているため、一度も見たことがない物体でも、テキストで説明すれば識別できます。例えば「三角形の赤い標識」と入力すれば、学習データに含まれていなくても該当する画像を見つけ出せるのです。 この「データ形式を超えた意味の理解」こそ、マルチモーダル埋め込みの幕開けでした。 CrossOver ── 3Dシーンを丸ごと対応づける最前線 Sarkar et al., 2025 (CVPR 2025 Highlight) CLIPが画像とテキストの2つのモダリティを結びつけたのに対し、2025年にコンピュータビジョンの最高峰学会CVPR 2025でハイライト論文に選ばれたCrossOverは、さらに多くのモダリティへと対象を広げました。RGB画像、点群(Point Cloud)、CADメッシュモデル、フロアプラン、テキスト── これら5種類ものデータを1つの共通空間にまとめ上げ、3Dシーン全体をモダリティ横断で対応づける(アライメントする)フレームワークです。 従来手法の限界:「完璧なデータ」がないと動かない これまでの3Dマルチモーダル学習には、大きな制約がありました。「椅子」「テーブル」といったオブジェクト単位で厳密にラベルを付けたアノテーションが必要で、しかも全てのモダリティ(画像・点群・CADなど)が揃っていなければ学習できなかったのです。 しかし現実のAR/VRやロボティクス、建設モニタリングといった現場では、そんな理想的なデータが揃うことはほとんどありません。ある部屋は3Dスキャンデータしかない、別の部屋はCAD図面と写真だけ ── こうした「歯抜け」のデータが当たり前です。 CrossOverの3つの工夫 CrossOverは、3つの工夫でこの「現実の壁」を突破しました。 1つ目は、 「シーンまるごと」で対応づける こと。従来はオブジェクト1つ1つに丁寧なラベルを貼る必要がありましたが、CrossOverは「部屋全体」「フロア全体」といったシーン単位で異なるモダリティを結びつけます。いわば、家具を1個ずつ辞書登録するのではなく、部屋全体の写真を1枚見せて「ここにあるもの全部まとめて覚えて」と教えるようなイメージです。 2つ目は、 データが歯抜けでも学習できる こと。5種類全てのデータが揃っている必要はありません。「この部屋は点群とテキストしかない」「あの部屋はRGB画像とCADだけ」── そんな不完全なデータでも、手持ちのモダリティだけで埋め込みを計算し、学習に組み込めます。 3つ目は、 段階的に賢くなる学習戦略 です。各モダリティに専用のエンコーダを用意し、最初は少ないデータの組み合わせから始めて、徐々に複雑な組み合わせへと学習を積み上げていきます。完璧なデータを待たずに、今あるデータから着実に知識を蓄積できるのです。 何ができるようになるのか CrossOverにより、データの形式を飛び越えた3D検索・解析が現実のものになります。 例えば、フロアプラン(間取り図)を検索クエリとして入力すれば、対応する3D点群データが検索結果として返ってくる。逆に、点群データから対応するフロアプランやCADモデルを見つけ出すことも可能です。さらに、明示的に学習していないモダリティの組み合わせ(例:フロアプラン ↔ テキスト)でも検索が機能する「創発的な汎化能力」が確認されています。 これはまさに、製造業や建築業が待ち望んでいた技術です。次の章では、この技術が製造現場でどのように活用されていくのかを具体的に見ていきます。 製造業における活用の未来 ここまで紹介してきたマルチモーダル埋め込み技術は、あくまで「要素技術」です。では、この技術が製造業の現場に入ると何が変わるのか? ── ここからは、最も実用化が近い「検索」の応用領域を見ていきます。 検索 ── 「探し方」が根本から変わる 製造業には、長年にわたって蓄積された膨大な図面・3Dデータ・仕様書が眠っています。しかし、その多くはファイル名や管理番号といった「メタデータ」でしか検索できません。番号を忘れたら最後、必要なデータにたどり着けない ── そんな経験のある方も多いのではないでしょうか。 マルチモーダル埋め込み技術は、この「探し方」を根本から変えます。データの本質的な「意味(特徴)」がベクトル化されることで、メタデータに頼らない、3つの新しい検索が可能になります。 クロスモーダル検索:形状で図面を探す 「データ形式の壁」を越える検索です。 設計担当者が、過去の類似製品の仕上がりを確認したい場面を想像してください。従来は管理番号を手がかりに過去資料を探し回る必要がありましたが、マルチモーダル埋め込みを使えば、開発中の3D CADデータをそのまま検索クエリとして入力するだけで済みます。AIが埋め込み空間上で「形状的・構造的に近い」過去の製品図面(画像)を瞬時に見つけ出してくれるのです。 管理番号が不明な古い部品でも、形状さえあれば「過去の類似品」を紐付けられる。これだけでも、設計の初期段階で過去の知見を活かしやすくなります。 マルチモーダル検索:「画像+テキスト」で絞り込む 1つのモダリティでは足りない場面に効く検索です。 例えば、「このフランジ(画像)と同じ形状で、かつ材質がステンレス(テキスト)のもの」を探したいとします。画像だけでは材質まで判別できませんし、テキストだけでは形状の微妙な違いを表現しきれません。 マルチモーダル検索では、画像の「視覚的な特徴ベクトル」とテキストの「仕様的な特徴ベクトル」を掛け合わせることで(ベクトル加算など)、両方の条件を満たすデータをピンポイントで特定できます。材質、硬度、耐熱性── 画像では見えない情報を言葉で補足することで、検索精度が格段に上がるのです。 AIチャット検索:現場の言葉でデータを引き出す 対話型インターフェースを介した検索です。 現場の作業員がタブレットに向かって「先週のライン停止時に、ベアリング付近を撮影した写真を見せて」と話しかける。あるいは「この3Dモデルの、取付穴が5つあるバリエーションをリストアップして」と指示を出す。── ユーザーの発話がテキストベクトルに変換され、データベース内の画像・3Dデータのベクトルと直接照合されることで、こうした自然な対話での検索が実現します。 複雑な検索コマンドを覚える必要はありません。専門知識がなくても「現場の言葉」で必要な技術資産にアクセスできるようになる点が、現場への浸透を考えると最大の強みです。 おわりに 本記事では、埋め込み技術の進化を「単語だけ」のWord2Vecから、「画像と言葉」を結んだCLIP、そして「3Dシーンを丸ごと対応づける」CrossOverへとたどりました。この10年余りで、AIが「意味」を捉える範囲は劇的に広がっています。 製造業にとって、この技術が意味するのは「データの探し方の根本的な変化」です。ファイル名や管理番号に頼っていた検索は、形状やテキストの「意味」で直接つながるようになります。ベテラン設計者の頭の中にしかなかった「あの時似たようなものを作った」という経験知は、埋め込み空間を通じて組織全体で共有できるデジタル資産へと変わります。 現時点では、製造業でのマルチモーダル埋め込みの社会実装はまだ始まったばかりです。しかし、研究の進展を見れば、この技術が現場に届くのは時間の問題でしょう。「形」と「言葉」の境界が消えたとき、製造業のデータ活用は新しいステージに入ります。 仲間を募集しています! キャディ株式会社では、本記事で紹介したマルチモーダル埋め込み技術をはじめ、AIモデル開発に全力で取り組み、ミッション「モノづくり産業のポテンシャルを解放する」の実現を目指しています。 この記事を読んで「自分ならこう解決する」「この技術、面白そう」と感じたエンジニアの方、ぜひご応募お待ちしております! 詳細は以下の採用ページからご覧いただけます! Data & Machine Learning / CADDi Tech Careers
こんにちは。キャディ株式会社でリサーチ組織の立ち上げを担っている福原です。 現在、私たちのResearchチームでは「 Staff Research Engineer, 2D/3D Vision & Multimodal Understanding 」のポジションを新たにオープンしています。 これまでのテックブログ では、VLMの空間推論における限界や、3D CADモデルの幾何情報処理の難しさといった「私たちが直面している技術課題(What)」について発信してきました。 今回は視点を変えて、 この挑戦的な技術領域で、どのようにして素早く価値を生み出し続ける研究開発が可能なのか 、そして なぜキャディでそれが実現できるのか についてお話しします。 製造業コンピュータビジョンの特異性 なぜキャディなのか:研究開発スピードの構造設計 技術ロードマップにおける3つの重点領域 3D形状の幾何学的・意味的理解の深化 2D図面と3D CADモデルの高度な対応付け マルチモーダル横断検索 求める人物像 製造業コンピュータビジョンの特異性 現在のコンピュータビジョン研究の主流は、RGB画像から3Dタスクを解くというアプローチです。自動運転、ロボティクス、AR/VRなど、多くの先端応用では、豊富なテクスチャ情報、照明の変化、背景のコンテキストといった視覚的手がかりを活用します。最新のVision-Language Model(VLM)も、このような「見た目の情報」を高度に理解することで、驚異的な性能を発揮しています。 しかし、製造業の現場では状況がまったく異なります。図面にも3D CADにも、色もテクスチャも背景も存在しません。そこにあるのは、ピュアな幾何学的情報のみです。 この違いは些細なものではありません。以前のテックブログでもお話ししましたが、言語処理タスクでは人間レベルに達するMLLMも、空間推論タスクでは60%の精度にすら届きません。建築図面のベンチマークでは、テキスト中心の質問応答では高精度を示す一方、空間認識では40〜55%程度という劇的な性能差が報告されています。既存の最先端モデルは、テクスチャなき幾何学の世界では「3歳児レベルの空間推論能力」に退化してしまうのです。 さらに、製造業では幾何学的特性に加えて物理的特性も同時に考慮する必要があります。ある形状が「見た目として正しい」だけでは不十分で、「製造可能か」「強度要件を満たすか」「加工コストは妥当か」といった物理的制約を満たさなければなりません。この点で、最近注目を集めているフィジカルAIの研究と技術的な親和性が高く、今後の研究トレンドとの接続も見えています。 つまり、製造業のコンピュータビジョンは、主流の研究結果をそのまま横に持ってきて使えるわけではない領域です。一見すると制約のように聞こえるかもしれませんが、逆に言えば独自性があるということでもあります。ビッグテックや巨大な外資企業が開発する汎用モデルは、確かにテキスト生成や一般的な画像認識では驚異的な性能を発揮しますが、テクスチャなき幾何学、製造ドメイン特有の制約、物理的特性といった領域では、汎用モデルをそのまま適用しても十分に機能しません。 これはリサーチエンジニアの視点では、 既に確立された正解がないからこそ挑戦的で面白い技術領域であること、そして一瞬で成果が水泡に帰すリスクが、他の領域と比べて相対的に低い ことを意味します。 製造業の実データは非公開のものが多く希少性が高いため、ビッグテックであっても容易に参入できる領域ではありません。加えて、扱う情報が様々なモダリティに跨る製造業の業務上の課題には学術的にもまだ確立された解決策が存在しないことも多く、データフォーマットや顧客が実現したい業務の個別性も高いという特徴があります。 汎用的なアプローチがそのまま正解にはならないからこそ、データや顧客の業務といったドメイン特化の深い知見と、製造業の実データを組み合わせた研究は、持続的な技術的Moat(競争優位性)を築きやすいテーマだと言えます。まだ解決すべき課題が山積しており、極めて挑戦的でエキサイティングな研究フロンティアなのです。これが、製造業AIの魅力です。 そして同時に、私たちを取り巻く環境は、かつてないスピードで変化しています。AI技術の進化は加速度的であり、新しい手法やモデルが日々登場しています。このような時代において、3年後、5年後に成果を出すというアプローチでは、市場や技術トレンドが大きく変わってしまう可能性があります。 私たちは、素早く価値を生み出し、検証し、次の価値創出につなげていく組織を目指しています。これは単なる目標ではなく、変化の激しい時代からの要請だと考えています。研究成果を小さく素早くリリースし、顧客のフィードバックを得て、次の研究テーマを磨き込んでいく。このサイクルを高速で回すことで、長期的な技術的優位性を築いていきます。 なぜキャディなのか:研究開発スピードの構造設計 では、なぜキャディにおいてこれほど高速な価値創出が可能なのでしょうか。 それは、単に「優秀な人材がいる」あるいは「リソースが潤沢である」という理由からではありません。研究開発のスピードを根本的に決定づけるのは、プロセスそのものをいかに速く回せる「構造」が整っているかです。 キャディの研究開発環境には、この構造を支える2つの大きな特徴があります。 1つ目は、自社でアプリケーションとデータプラットフォームを保有し、顧客の現場課題に深く入り込んでいることです。ビジネスメンバーが顧客と伴走し、見積、図面検証、製造プロセス管理といったコア業務の課題に日々向き合っています。この顧客との深い関係性が、机上の空論ではない「リアルな現場の知見」と「実データ」へのアクセスを可能にしています。 2つ目は、経営直下での研究開発の優先順位の高さです。キャディにおいて、独自のAI技術によるMoat構築は経営の最重要課題の一つです。そのため、CEO自らが研究開発の現場と連携し、技術検証や事業実装に向けたブロッカーを即断即決で取り除く体制が整っています。 これら2つの特徴により、私たちの環境では、一般的な研究組織が直面するボトルネックに対して、引けるレバー(選択肢)が圧倒的に多く、かつそれが機能する速度が速いのです。 例えば研究を進める上で壁にぶつかったとき、私たちには以下のような選択肢が即座に用意されます。 データが足りない → ビジネスチームと連携し、実際プロダクトを利用いただいている顧客に直接アプローチして新しいデータの整備をする ドメイン知識が足りない → 製造現場と直結したプロダクトを持つ強みを活かし、社内の専門家や実務者の知見へ即座にアクセスする 業界標準が曖昧で判断できない → 必要に応じて政府機関や業界団体と連携し、マクロな視点から課題を整理する リソースの壁に直面する → 経営層と連携し、課題と必要性を共有することで、人的リソースや計算資源の拡張の判断を仰ぐ ここで重要なのは、私たちが「誰とも関わらず研究だけに閉じこもれる温室」にいるわけではないということです。研究成果を事業価値に繋げるためには、PdMや他チームとの連携が不可欠であり、自ら主体的に動くことが求められます。しかし、そこには本質的ではない待ち時間や政治的な摩擦が極めて少なく、主体的に動くことで高速に研究開発のサイクルを回せる環境が存在します。 自分で動き、周囲を巻き込むことで、あらゆる問題が即座に解決していく。これは逆を言えば、 「言い訳ができない環境」 でもあります。研究成果を事業価値へとつなげ切る責任がある一方で、そのための障害は経営層や周囲のメンバーが全力で取り除いてくれます。 事業部と切り離された研究組織では、ともすれば大規模なリソース獲得の承認に数週間、社内部署の調整まで含めると数ヶ月といった遅延が発生しがちです。しかしキャディでは、そうした非本質的な遅延がほぼありません。リサーチエンジニアが本来向き合うべき技術検証と価値創出に、時間のほぼすべてを注ぎ込むことができる。この構造的な違いこそが、研究スピードを根本から変えているのです。 ここまで読んで、「それならアカデミアや大企業の研究所でも同じことができるのでは?」と思われるかもしれません。しかし、キャディの研究開発環境は独特のバランスの上に成り立っています。 アカデミアとの違い は、論文執筆は目的ではなく過程であり、実データでの検証と「顧客の課題解決」こそが最終的な価値基準である点です。 大企業研究所との違い は、意思決定のスピードです。事業実装までの判断が「週単位」で行われ、組織的なブロッカーは即座に解除されていきます。 スタートアップとの違い は、製造業という数兆円市場において、すでにグローバル4カ国に展開するSaaSプラットフォームと確固たる顧客基盤、実データを有している点です。 つまりキャディは、技術的な挑戦の深さ、スタートアップの意思決定スピード、事業基盤を持つ企業としての実装力という、通常はトレードオフになりがちな要素を併せ持つ、稀有な研究環境なのです。 技術ロードマップにおける3つの重点領域 このような環境で、私たちは具体的にどのような技術課題に挑んでいるのか。重点課題の中から、いくつかの領域をご紹介します。 3D形状の幾何学的・意味的理解の深化 まず一つ目の領域が、3D CADデータの純粋な幾何情報に対する深い理解です。 製造業のデータを扱うモデルには幾何形状や空間配置に関する高い認識力が求められますが、前述の通り既存の多くの基盤モデルはこの要件を満たせていません。私たちは、SaaSを通じて蓄積された大規模な3D CADデータを活用し、純粋な幾何情報から製造上意味を持つ特徴を認識できる表現を獲得する研究に取り組んでいます。 これは単なる形状の認識だけにとどまりません。3D CADの局所的な特徴(フィレット、ボス、リブ、穴など)を抽出し、「この穴はM6ボルトを通すためのもの」「このリブは強度を確保するためのもの」といった、設計者の意図や物理的な制約をモデルに推論させることが求められます。幾何学的な形状の認識だけでなく、強度や剛性、加工の実現可能性といった物理的特性も同時に扱う、フィジカルAI領域の先端的な挑戦でもあります。 このように3D形状に対する理解を深めることで、部品の3D CADモデルから加工難易度や適切な製造方法を自動推定することも可能になります。これにより、これまでベテランの暗黙知に依存していた見積業務の精度やスピードが飛躍的に向上し、SaaSプロダクトの強力なコア機能としての事業価値に直結します。 2D図面と3D CADモデルの高度な対応付け 二つ目の領域が、2D図面と3D CADモデルの高度な対応付けです。 多くの場合において、図面と3D CADは「部品単位」でしか紐付いておらず、それぞれが持つ詳細な情報の、システム上での密な関連付けは出来ていないのが現状です。 寸法などの詳細な情報は図面側に記載されていることが多い一方で、見積業務において、それらが3次元的にどう配置されているかの確認が必要になることが頻繁にあります。そのため、「図面上の情報を、3D CAD側の対応する適切なコンテキストに紐づけて処理をしたい」という実務的なニーズが存在します。 このようなプロセスを自動化するためには、2D図面と3D CADモデルの情報を相互に解釈し、高度に統合することが必須の要素技術となります。この技術の実現によって、図面と3D CADモデル間を横断したクロスチェックの効率化や、設計情報の整合性担保が可能になり、設計プロセスにおいて大きな事業価値を生み出します。 マルチモーダル横断検索 三つ目の領域が、あらゆる製造データをモダリティを超えて統合する検索・推論基盤の構築です。これには、3D CADや2D図面だけでなく、仕様書、過去の不具合報告、見積書といった自然言語のテキスト情報も含まれます。 最大の技術的挑戦は、純粋な幾何学的類似性(形状が似ている)と、セマンティックな類似性(用途や機能、関連する不具合の文脈が似ている)を両立させ、すべてのモダリティを統一された単一の埋め込み空間に整合させることです。 これが可能になれば、「この部品と類似した形状で、過去にどんな不具合があったか」「この図面形状を満たす最適な加工プロセスは何か」といった自然言語クエリでの高度な横断検索と推論が実現します。これまで個人の頭の中や各部署に散在していた暗黙知がシステム上で形式知化され、設計者や調達担当者の意思決定スピードを劇的に引き上げる事業価値をもたらします。 求める人物像 私たちは以下のような方と是非お話したいと考えています: VLM、3D Vision、マルチモーダル学習のいずれかで深い専門性を持つ方 :トップ会議への投稿経験や、博士課程での研究実績など 「面白いモデルができた」で満足せず、事業価値まで執着できる方 :研究と実装の間を往復しながら、顧客の課題解決に向き合える 研究スピードを上げるために、構造から変えることを厭わない方 :AI活用、プロセス改善、組織設計にも興味がある 提供できる環境 : 非本質的な摩擦を排し、研究成果を事業価値へと直結させる最速の社会実装環境 数兆円規模の巨大市場を動かし、独自の技術的Moat(競争優位性)を築く挑戦の場 変化の激しい時代において、最速のサイクルで価値を生み出し続けるアジャイルな組織 特許取得や学会発表をバックアップし、成果を社会へ還元するアウトプット支援 CADDiのリサーチエンジニアは、論文を書くだけでも、実装に専念するだけでもありません。仮説立案・モデル構築・SaaSへの組み込み・顧客検証までを一気通貫で担う「越境型」のポジションです。既存の基盤モデルが通用しない未踏の領域で、世界に先駆けて独自の技術的Moatを築き上げる。そして、その研究成果を圧倒的なスピードで数兆円規模の巨大産業へと実装していく。 あらゆる組織的摩擦が排除された「言い訳ができない」環境で、本質的な技術探求と事業価値創出のサイクルを最速で回し、自らの手でこのブレイクスルーを起こしたい。そう思われた方は、ぜひ一度カジュアルにお話ししましょう。エントリーを心よりお待ちしています。 募集要項: Staff Research Engineer, 2D/3D Vision & Multimodal Understanding カジュアル面談: 応募フォーム
RFC 8707 Resource Indicatorsの活用事例: マイクロサービス間のトークンの権限適正化 はじめに こんにちは、キャディ株式会社のControl Plane 部に所属している麻生です。 キャディでは、マルチテナントSaaSを提供しておりマルチプロダクト化を進めています。その上でアプリケーションを横断した機能をControl Plane として切り出して開発しています。 Control Plane 全体のアーキテクチャについては、以前のブログ記事「 CADDi の Control Plane を支えるシステムたちの紹介 」で紹介しました。是非こちらもご一読いただけると幸いです。 そのControl Plane において、ワークロード間の認証基盤として "M2M Token Issuer" を運用しています。これはOAuth 2.0 のClient Credentials Grant を用いた社内専用のトークン発行システムで、ユーザー操作を伴わないシステム間通信やバッチ処理で利用されています。 本記事では、M2M Token Issuer が発行するInternal Token を見直し、RFC 8707 Resource Indicators を適用して最小権限の原則を実装したエピソードを紹介します。 改修に至った課題 M2M Token Issuer は、まず動くものを素早く届けることを優先して構築した経緯があり、トークンの権限制御は最小限の設計にとどまっていました。 Client Credentials Grant では、ユーザーが認可の場にいないため同意プロセスがありません。その分、クライアントに許可された操作範囲をscopeで明示的に制約することが重要です。 社内システムでの活用が広がる中で、より安全な利用を促進するため、 「各クライアントに、どのサービスが、どのような操作を許可しているか」 これらを統制できるようにしたいと考えるようになりました。 設計と実装 RFC 8707 Resource Indicators の活用 RFC 8707 Resource Indicators for OAuth 2.0 に準拠する形にしました。これは、トークンリクエスト時に resource パラメータでアクセス先を指定する仕組みを定義しています。これにより、発行されるトークンの aud にリクエストで指定したリソース識別子が設定されます。 Control Plane の機能は他チームの開発者に認知してもらうことも重要になりますが、RFCに準拠することで、新しい仕様でも社内の開発者に対する説明がしやすくなるというメリットがあります。 改修後のトークンリクエストとレスポンスは以下のようになります。 POST /oauth2/token --user "{Client ID}:{Secret}" -d grant_type=client_credentials \ &resource=urn:caddi-service01 \ &scope=delete { " sub ": " {Client ID} ", " aud ": [ " urn:caddi-service01 " ] , " scope ": " delete ", " iat ": 1234567890 , ... } トークンを受け取るサービス側は、Istio の RequestAuthentication で aud を検証し、AuthorizationPolicy を用いて要求された scope が適切か検証します。自サービスのリソース識別子( urn:caddi-service01 )と一致しないトークンは拒否されるため、他のサービス宛てのトークンでアクセスすることはできません。 複数resource を指定した場合の設計 原則は「1トークン = 1リクエスト」の都度取得ですが、技術的には複数の resource を指定して1つのトークンに複数の aud を含めることもサポートしています。RFC上も、複数の aud を受け付ける仕様です。 POST /oauth2/token resource=urn:caddi-service01 resource=urn:caddi-service02 scope=delete { " aud ": [ " urn:caddi-service01 ", " urn:caddi-service02 " ] , " scope ": " delete " } ここでのポイントは、 scope がすべての指定resourceに対して共通(積集合)であることです。リクエストされたscopeは、指定されたすべてのresource に対して許可されている場合にのみ発行されます。 複数のresource に別々のscope を指定するケースについては、サポートしないこととしました。これらを提供可能にすると、トークンの権限範囲が広がります。これはトークンの権限適正化という元々の目的と合わないため、最小権限を実現することを優先しました。 この設計により、マイクロサービス間でリクエストがチェーンする場合にも、同一トークンを伝搬させて各サービスで aud と scope の両方を検証できます。「このトークンは自サービス宛てであり、かつ要求された操作が許可されている」ことは各サービスが独立して検証できます。 ネットワークトポロジーに依存しない識別子 RFC 8707の resource パラメータにはURIを指定します。Kubernetes 上でマイクロサービスを構築しているキャディにとって、最も直感的な選択肢はKubernetes クラスター内のサービスURL( http://caddi-service01.ns.svc.cluster.local )です。一方でキャディではControl Plane と、Data Plane / Application Plane を別のKubernetes クラスターに分割しており、クラスターを跨いでトークンをやり取りする場合、クラスター内URLはリソース識別子として一意性を担保できる保証はありません。 また、URLを使うと RFC 9728 OAuth 2.0 Protected Resource Metadata が取得できるエンドポイントと誤解されるリスクもあります。リソース識別子はあくまでトークンの aud に載せる論理的な名前であり、実際にアクセス可能なURLである必要はありません。 こうした背景からURLではなくURNを基本とする方針を採りました。ただし、厳格なフォーマットを強制するのではなく、ルールを提供し使う側に委ねる方式としています。 [MUST] URIフォーマット(URNを含む)であること [RECOMMEND] urn:{サービス名} のフォーマットを用いること [CAN] 一意性の担保のために必要に応じて、namespaceやパス情報を追加すること(例: urn:control-plane:caddi-service01 ) まとめ M2M Token IssuerのInternal Tokenに対して、RFC 8707 Resource Indicatorsを適用し、 aud と scope による最小権限の原則を実現しました。 設計ポイント 内容 resource による aud 指定 RFC 8707に準拠し、トークンのアクセス先をリクエスト時に明示。サービス側でIstioによる aud 検証が可能に scope の積集合設計 複数resource指定時もscopeは共通。resource別のscope指定は最小権限の観点からサポートしない URNベースのリソース識別子 ネットワークトポロジーに依存しない論理名を採用し、クラスター横断でも一意性を担保 今後の展望 今回の記事は aud に焦点を当てて解説しましたが、 scope についても運用で考えることが多くあります。トークン発行者としてどこまでルールを作るか、今後も検討をしていくことになると思います。 またこれはM2M Token Issuer に限った話ではありませんが、Control Plane の機能は、Data Plane / Application Plane で活用されて価値を発揮します。 新しい機能を導入する上で、開発者に負担なく、かつ開発のアジリティに影響しないように、それでいて全体の生産性にインパクトをもたらす工夫に努めていく所存です。 結び M2Mトークンの認可設計は、ユーザー認証と比べて議論されることが少ない領域ですが、マイクロサービスが増えるほどその重要性は高まります。同じ課題に取り組む方の参考になれば幸いです。 Control Plane の開発に興味がある方は、ぜひ以下のページもご覧ください。 recruit.caddi.tech
こんにちは、Control Plane部認証認可グループの平岩です。私たち認証認可グループではバックエンドAPIにGoを多く採用しています。共通基盤である特性上高RPSに耐えられる必要があり、また安定して低いレイテンシでリクエストを処理することが求められます。本記事ではGoの標準パッケージである sql.DB の内部実装を、 ソースコード (Go 1.26時点)を読みながら解説します。 はじめに sql.DB とは何か 内部実装の詳解 DB 構造体の主要フィールド クエリ実行の全体像 BadConn リトライの仕組み コネクションの取得: conn メソッド フェーズ1: idleコネクションがあれば再利用する フェーズ2: 最大接続数に達している場合の待機 フェーズ3: 新規接続の作成 コネクションの返却: putConn メソッド 非同期なコネクション作成: connectionOpener トランザクションとコネクション コネクションの定期削除: connectionCleaner DBStats: プールの状態を観測する コネクションプールの設定 4つの設定パラメータ デフォルト値の問題 各パラメータの解説 SetMaxOpenConns SetMaxIdleConns SetConnMaxLifetime SetConnMaxIdleTime まとめ はじめに GoでRDBを使うとき、何かしらのO/R mapperやライブラリを使うことが多いかと思います。ですが、内部的には標準パッケージの sql.DB が使われています。この sql.DB はコネクションプールの役割を持ち、4つの設定パラメータが存在します。これらの値をデフォルトのまま使うと本番環境でエラーやパフォーマンス問題を引き起こすことがあります。また、内部でリトライが行われるケースがあるなど隠蔽されている挙動も存在します。 本記事では前半で sql.DB の内部実装をソースコードを追いながら解説し、後半でそれを踏まえた設定の考え方を述べます。設定についてだけ知りたい方は コネクションプールの設定 まで読み飛ばしてください。 sql.DB とは何か sql.DB の実態はコネクションプールであり、複数のgoroutineから安全に使えます(goroutine safe)。 sql.Open は引数の検証のみを行い、この時点ではデータベースへのコネクションを作成しません。実際にコネクションが作れることを確認するには (*sql.DB).PingContext を使います(詳細は Opening a database handle - The Go Programming Language を参照してください)。 内部実装の詳解 DB 構造体の主要フィールド ここからは sql.DB の実装を見ていきます。以降のコードを読むにあたって、 DB 構造体 の以下の4つのフィールドを把握しておけば十分です。 freeConn []*driverConn : idleコネクションのスライス。プールに戻った時刻 returnedAt の古い順になる connRequests connRequestSet : コネクションを待っているgoroutineの集合 connRequestSet はSetと言っているが実体はindexでの参照を付けたスライス numOpen int : 使用中とidleの両方を含む、現在openなコネクション数 cleanerCh chan struct{} : プールにあるコネクションを閉じるgoroutineへの通知用のchannel すべてのフィールドへのアクセスは mu sync.Mutex で保護されています。 クエリ実行の全体像 まず、 QueryContext を例として全体像を示します。他の QueryRowContext ExecContext PingContext も基本的に同じ流れですが、 QueryContext のみが Rows.Close() を明示的に呼んでコネクションを返却する責任がuser側にあります。 flowchart TD A["(*sql.DB).QueryContext"] subgraph retryScope ["retryの範囲(ErrBadConnの場合、最大3回リトライされる)"] B["(*sql.DB).retry"] Q["(*sql.DB).query"] C["(*sql.DB).conn"] E["(*sql.DB).queryDC"] end A --> B B --> Q Q --> C C -- エラー --> ERR[error を返す] C -- 成功 --> E E -- ErrBadConn --> B E -- エラー --> ERR E -- 成功 --> F[Rows を返す<br>コネクション所有権が Rows に移動] subgraph userCode ["user"] K["(*sql.Rows).Next() で最後まで読むか、(*sql.Rows).Close()を呼ぶ"] end F --> K K --> G["(*sql.DB).putConn"] BadConn リトライの仕組み sql.DB には無効な接続に当たった場合に自動でリトライする仕組みがあります。 このリトライのトリガーとなるのが driver.ErrBadConn です。 godoc には次のように書かれています。 ErrBadConn should be returned by a driver to signal to the sql package that a driver.Conn is in a bad state (such as the server having earlier closed the connection) and the sql package should retry on a new connection. To prevent duplicate operations, ErrBadConn should NOT be returned if there's a possibility that the database server might have performed the operation. つまり、 driver.ErrBadConn はコネクションがDBサーバ側から切断された場合などに返り、 sql package側でリトライすべきエラーであるということです。 実際にリトライを制御する DB.retry を見ていきます。 database/sql/sql.go L1572-1584 : const maxBadConnRetries = 2 func (db *DB) retry(fn func (strategy connReuseStrategy) error ) error { for i := int64 ( 0 ); i < maxBadConnRetries; i++ { err := fn(cachedOrNewConn) // 👈 (1) // retry if err is driver.ErrBadConn if err == nil || !errors.Is(err, driver.ErrBadConn) { return err } } return fn(alwaysNewConn) // 👈 (2) } (1) 最大2回、 cachedOrNewConn のstrategyで試行します。このstrategyではまずidleコネクションを再利用しようとします。 (2) 2回とも driver.ErrBadConn だった場合、3回目は alwaysNewConn strategyで新規のコネクション作成を強制します。 コネクションの取得: conn メソッド retry から呼ばれる conn メソッドが、プール内のidleコネクションか新規の接続を返します。このメソッドはかなり長いため、全体を3つのフェーズに分けて読んでいきます。 flowchart TD A[connメソッド呼び出し] --> B{freeConnに<br>idleコネクションがある} B -- Yes --> C{コネクションが期限切れ} C -- No --> D[コネクションを再利用] C -- Yes --> E[driver.ErrBadConnを返す → リトライ] B -- No --> F{コネクション数が最大(maxOpen)に<br>達している} F -- Yes --> G[connRequestsで待機] F -- No --> H[新規接続を作成] フェーズ1: idleコネクションがあれば再利用する database/sql/sql.go L1331-1354 : // Prefer a free connection, if possible. last := len (db.freeConn) - 1 if strategy == cachedOrNewConn && last >= 0 { // Reuse the lowest idle time connection so we can close // connections which remain idle as soon as possible. conn := db.freeConn[last] // 👈 (1) db.freeConn = db.freeConn[:last] conn.inUse = true if conn.expired(lifetime) { // 👈 (2) db.maxLifetimeClosed++ db.mu.Unlock() conn.Close() return nil , driver.ErrBadConn } db.mu.Unlock() // Reset the session if required. if err := conn.resetSession(ctx); errors.Is(err, driver.ErrBadConn) { conn.Close() return nil , err } return conn, nil } (1) freeConn の末尾を取得しています(LIFO)。コメントにある通り、同時に必要とする数よりも多いコネクションがあった場合、余分なコネクションはidle状態のままcloseされていきます。 (2) 取得した接続が maxLifetime を超えていた場合は driver.ErrBadConn を返し、リトライの対象になります。 フェーズ2: 最大接続数に達している場合の待機 database/sql/sql.go L1358-1426 : if db.maxOpen > 0 && db.numOpen >= db.maxOpen { req := make ( chan connRequest, 1 ) delHandle := db.connRequests.Add(req) // 👈 (1) db.waitCount++ db.mu.Unlock() waitStart := nowFunc() // 👈 (2) select { case <-ctx.Done(): // 👈 (3) db.mu.Lock() deleted := db.connRequests.Delete(delHandle) db.mu.Unlock() db.waitDuration.Add( int64 (time.Since(waitStart))) if !deleted { select { default : case ret, ok := <-req: if ok && ret.conn != nil { db.putConn(ret.conn, ret.err, false ) } } } return nil , ctx.Err() case ret, ok := <-req: // 👈 (4) db.waitDuration.Add( int64 (time.Since(waitStart))) if !ok { return nil , errDBClosed } if strategy == cachedOrNewConn && ret.err == nil && ret.conn.expired(lifetime) { db.mu.Lock() db.maxLifetimeClosed++ db.mu.Unlock() ret.conn.Close() return nil , driver.ErrBadConn } if ret.conn == nil { return nil , ret.err } if err := ret.conn.resetSession(ctx); errors.Is(err, driver.ErrBadConn) { ret.conn.Close() return nil , err } return ret.conn, ret.err } } (1) maxOpen に達している場合、 connRequests に待機リクエストを追加します。 (2) コネクション作成の待ち時間を計測するために現在時刻を変数に入れています。この nowFunc は var nowFunc = time.Now と定義されており、テスト時に現在時刻を取得する関数を上書きするためのパターンが使われています。 (3) context がキャンセルされた場合はリークを避けるため connRequests から削除して ctx.Err() を返しています。 (4) 他のgoroutineがコネクションを返却した場合、ここでコネクションを受け取ります。 フェーズ3: 新規接続の作成 database/sql/sql.go L1429-1449 : db.numOpen++ // optimistically // 👈 (1) db.mu.Unlock() ci, err := db.connector.Connect(ctx) if err != nil { db.mu.Lock() db.numOpen-- // correct for earlier optimism // 👈 (2) db.maybeOpenNewConnections() db.mu.Unlock() return nil , err } db.mu.Lock() dc := &driverConn{ db: db, createdAt: nowFunc(), returnedAt: nowFunc(), ci: ci, inUse: true , } db.addDepLocked(dc, dc) db.mu.Unlock() return dc, nil (1) numOpen を楽観的に(optimistically)インクリメントしてからロックを解放し、接続を作成します。 (2) 接続作成に失敗した場合は numOpen を戻し、 maybeOpenNewConnections で待機中になっている数だけコネクションを作成しようとします。 コネクションの返却: putConn メソッド 接続の使用後は putConn を通じてプールに返却されます。このメソッドには戻り値がなく、引数で渡る dc *driverConn err error を直接変更しています。 database/sql/sql.go L1481-1531 : func (db *DB) putConn(dc *driverConn, err error , resetSession bool ) { if !errors.Is(err, driver.ErrBadConn) { if !dc.validateConnection(resetSession) { // 👈 (1) err = driver.ErrBadConn } } db.mu.Lock() if !dc.inUse { db.mu.Unlock() panic ( "sql: connection returned that was never out" ) } if !errors.Is(err, driver.ErrBadConn) && dc.expired(db.maxLifetime) { // 👈 (2) db.maxLifetimeClosed++ err = driver.ErrBadConn } dc.inUse = false dc.returnedAt = nowFunc() for _, fn := range dc.onPut { fn() } dc.onPut = nil if errors.Is(err, driver.ErrBadConn) { // 👈 (3) db.maybeOpenNewConnections() db.mu.Unlock() dc.Close() return } added := db.putConnDBLocked(dc, nil ) // 👈 (4) db.mu.Unlock() if !added { dc.Close() return } } (1) validateConnection は、ドライバが driver.Validator インタフェースを実装している場合に IsValid() を呼びます。 false が返ればコネクションは破棄されます。ドライバはクエリ結果のエラーをそのまま返しつつ、コネクション自体は破棄したい場合にこのインタフェースを使います。 (2) コネクションが maxLifetime を超過していたら err = driver.ErrBadConn とします。ここでreturnはしていませんが、 (3) の処理によってcloseされることになります。 (3) driver.ErrBadConn の場合、 maybeOpenNewConnections で待機リクエストに新規接続を用意してからcloseします。 (4) 正常なコネクションの場合、 putConnDBLocked で待機リクエストへの割り当てまたはidleコネクションのプールへの追加を試みます。 putConnDBLocked の実装も見てみます。 database/sql/sql.go L1542-1567 : func (db *DB) putConnDBLocked(dc *driverConn, err error ) bool { if db.closed { return false } if db.maxOpen > 0 && db.numOpen > db.maxOpen { return false } if req, ok := db.connRequests.TakeRandom(); ok { // 👈 (1) if err == nil { dc.inUse = true } req <- connRequest{ conn: dc, err: err, } return true } else if err == nil && !db.closed { if db.maxIdleConnsLocked() > len (db.freeConn) { db.freeConn = append (db.freeConn, dc) // 👈 (2) db.startCleanerLocked() return true } db.maxIdleClosed++ } return false // 👈 (3) } (1) 待機中の connRequest が1つでもあれば freeConn には追加せず、ランダムに1つ選んでコネクションを直接渡しています。 (2) 誰も待機中でなければidleコネクション( freeConn )の末尾に追加します。 (3) idleコネクション数が上限を超えている場合は false を返し、 putConn 側でコネクションがcloseされます。 非同期なコネクション作成: connectionOpener putConn や conn のコードに maybeOpenNewConnections の呼び出しがありました。この関数は待機中の connRequest の数だけ openerCh に通知します。 database/sql/sql.go L1230-1245 : func (db *DB) maybeOpenNewConnections() { numRequests := db.connRequests.Len() if db.maxOpen > 0 { numCanOpen := db.maxOpen - db.numOpen if numRequests > numCanOpen { numRequests = numCanOpen } } for numRequests > 0 { db.numOpen++ // optimistically // 👈 (1) numRequests-- if db.closed { return } db.openerCh <- struct {}{} } } (1) conn メソッドのフェーズ3と同じく、 numOpen を楽観的にインクリメントしてからシグナルを送ります。 openerCh を受け取るのは別goroutineで動いている connectionOpener です。このgoroutineは OpenDB の時点で起動されています。 database/sql/sql.go L1031-1046 : func OpenDB(c driver.Connector) *DB { ctx, cancel := context.WithCancel(context.Background()) db := &DB{ connector: c, openerCh: make ( chan struct {}, connectionRequestQueueSize), lastPut: make ( map [*driverConn] string ), stop: cancel, } go db.connectionOpener(ctx) // 👈 (1) return db } (1) connectionOpener は別のgoroutineで動いています。 openerCh からシグナルを受け取るたびに openNewConnection で接続を作成し、 putConnDBLocked で待機中の connRequest に渡します。 conn メソッドのフェーズ3では呼び出し元goroutineが直接 connector.Connect を呼んでいましたが、 maybeOpenNewConnections はこの専用goroutineが非同期に接続を作成します。 ErrBadConn で接続が壊れた場合や接続作成に失敗した場合に、待機中のgoroutineをブロックせずに新しい接続を用意するための仕組みです。 トランザクションとコネクション BeginTx はプールからコネクションを1つ取得し、 Tx に固定します。 トランザクション中の全操作はこの固定されたコネクションを使います。 flowchart TD A["(*sql.DB).BeginTx"] --> B["(*sql.DB).retry"] B --> C["(*sql.DB).conn: コネクション取得"] C --> D["BEGIN を発行し Tx を生成<br>コネクションを Tx.dc に固定"] D --> E["Tx 上の操作<br>tx.QueryContext / tx.ExecContext"] E --> F["grabConn: 固定されたコネクションを返す<br>(プールには行かない)"] F --> G["Commit() または Rollback()"] G --> H["tx.close → putConn"] 通常のクエリでは conn がプールからコネクションを取得しますが、 Tx 上の操作では grabConn が固定されたコネクション( Tx.dc )をそのまま返します。 プールへの問い合わせは発生しません。 Commit() または Rollback() が呼ばれると、内部の tx.close が releaseConn → putConn を呼び、コネクションをプールに返却します。 BeginTx で渡した context がキャンセルされた場合は、バックグラウンドの awaitDone goroutineが自動でロールバックしコネクションを返却します。 コネクションの定期削除: connectionCleaner 期限切れのidle接続を定期的にクリーンアップするためにバックグラウンドで動きます。 maxLifetime または maxIdleTime が設定されている場合のみ起動します。 database/sql/sql.go L1095-1136 : func (db *DB) connectionCleaner(d time.Duration) { const minInterval = time.Second // 👈 (1) if d < minInterval { d = minInterval } t := time.NewTimer(d) for { select { case <-t.C: case <-db.cleanerCh: // 👈 (2) } db.mu.Lock() d = db.shortestIdleTimeLocked() if db.closed || db.numOpen == 0 || d <= 0 { db.cleanerCh = nil db.mu.Unlock() return } d, closing := db.connectionCleanerRunLocked(d) // 👈 (3) db.mu.Unlock() for _, c := range closing { c.Close() } if d < minInterval { d = minInterval } if !t.Stop() { select { case <-t.C: default : } } t.Reset(d) } } (1) connectionCleaner は最短でも1秒間隔で動作します。 (2) 1秒経つか cleanerCh への通知でコネクション削除の処理が実行されます。 cleanerCh への送信は SetConnMaxLifetime SetConnMaxIdleTime が短くなるかDBがcloseされたときに発生します。 (3) この connectionCleanerRunLocked がcloseすべきコネクションのスライスを返します。実際の Close() はロック解放後に行っています。 database/sql/sql.go L1138-1189 : func (db *DB) connectionCleanerRunLocked(d time.Duration) (time.Duration, []*driverConn) { var idleClosing int64 var closing []*driverConn if db.maxIdleTime > 0 { // As freeConn is ordered by returnedAt process // in reverse order to minimise the work needed. idleSince := nowFunc().Add(-db.maxIdleTime) last := len (db.freeConn) - 1 for i := last; i >= 0 ; i-- { // 👈 (1) c := db.freeConn[i] if c.returnedAt.Before(idleSince) { i++ closing = db.freeConn[:i:i] db.freeConn = db.freeConn[i:] idleClosing = int64 ( len (closing)) db.maxIdleTimeClosed += idleClosing break } } // ... 次回チェック時刻の計算(省略) } if db.maxLifetime > 0 { expiredSince := nowFunc().Add(-db.maxLifetime) for i := 0 ; i < len (db.freeConn); i++ { // 👈 (2) c := db.freeConn[i] if c.createdAt.Before(expiredSince) { closing = append (closing, c) last := len (db.freeConn) - 1 // Use slow delete as order is required to ensure // connections are reused least idle time first. copy (db.freeConn[i:], db.freeConn[i+ 1 :]) // 👈 (3) db.freeConn[last] = nil db.freeConn = db.freeConn[:last] i-- } } db.maxLifetimeClosed += int64 ( len (closing)) - idleClosing } return d, closing } (1) 期限切れの(= maxIdleTime を超過した)コネクションを探します。 freeConn は returnedAt の古い順に並んでいるため、末尾(最新)から逆順に走査します。期限切れのコネクションを見つけたらそれより前のコネクションはすべて期限切れなので、すべてcloseの対象として closing に取り出しています。それ以降の残りが有効な freeConn となります。 (2) 次に maxLifetime を超過したコネクションを探します。 freeConn は createdAt の順では並んでいないため、全件走査する必要があります。 (3) 直前のコメントにある通り、 freeConn の順序を維持するために copy と nil の代入で要素を削除しています。LIFOでの再利用が正しく機能するには returnedAt 順で並んでいることが必要だからです。 この関数はcloseすべきコネクションを closing スライスに集めて返すだけで、 Close() 自体は呼びません。呼び出し元の connectionCleaner がロックを解放してから Close() を実行します。 ここまでで sql.DB によるコネクションプールの実装を確認しました。 DBStats: プールの状態を観測する db.Stats() はプールの現在の状態とカウンターを DBStats として返します。 各フィールドはそれぞれ sql.DB の対応するフィールドの値を返しています。ただし使用中のコネクション数を示す InUse のみ直接保持していないため (openコネクション数 - idleコネクション数) で算出されます。 フィールド 内部実装での算出方法 MaxOpenConnections maxOpen OpenConnections numOpen InUse numOpen - len(freeConn) Idle len(freeConn) WaitCount waitCount WaitDuration waitDuration MaxIdleClosed maxIdleClosed MaxIdleTimeClosed maxIdleTimeClosed MaxLifetimeClosed maxLifetimeClosed 上4つはスナップショットで、 Stats() を呼んだ瞬間の値です。 下5つは累積カウンターで、 DB の生存期間を通じて単調増加します。 コネクションプールの設定 4つの設定パラメータ sql.DB には4つの設定パラメータがあります。 各メソッドの詳細は Managing connections - The Go Programming Language を参照してください。 設定 デフォルト値 内部実装での役割 SetMaxOpenConns 0(無制限) conn で numOpen >= maxOpen なら connRequests で待機 SetMaxIdleConns 2 putConnDBLocked で freeConn に入れるかの上限 SetConnMaxLifetime 0(無制限) conn / putConn で createdAt からの経過をチェック SetConnMaxIdleTime 0(無制限) connectionCleaner で returnedAt からの経過をチェック デフォルト値の問題 Go公式ドキュメント( Managing connections - The Go Programming Language )にはこう書いてあります。 For the vast majority of programs, you needn't adjust the sql.DB connection pool defaults. ですが、本番環境で複数インスタンスからRDBに接続するWebアプリケーションでは事情が異なるのではと思います。 アプリケーションのインスタンスがスケールする一方でDB側には接続数の上限があり、デフォルトの設定では接続数が上限を超えてエラーになるリスクがあるからです。 各パラメータの解説 SetMaxOpenConns numOpen >= maxOpen になると、以降の conn は connRequests のチャネルで待たされます。 Go公式ドキュメント はデッドロックのリスクに言及しています。 Setting the limit makes database usage similar to acquiring a lock or semaphore, with your application queuing up to wait for an available database connection. デフォルト値は0であり、無制限を意味します。これではリクエストがスパイクした際に大量にコネクションが作られ、リソースを大量に消費してしまう可能性があります。そのためデフォルトのままは避けるべきで、デッドロックが起こらない程度に余裕を持ったある程度大きな値でも設定しておくべきと考えています。また、前述の通りDB側にも接続数の上限があるため、複数インスタンスから接続する構成ではDB側の上限をインスタンスが最大までスケールしても超えない値に設定しておく必要があります。 SetMaxIdleConns デフォルトは2( defaultMaxIdleConns )です。 公式ドキュメント にも "raise the limit to avoid frequent reconnects in programs with significant parallelism" とあります。 先ほど見た putConnDBLocked の実装から、デフォルトのままだと何が起きるかがわかります。 コネクションの返却時にすでに MaxIdleConns 以上idleコネクションがあった場合、即closeされるということです。 デフォルトの2のように少ない値だとコネクションの破棄→作成が繰り返されて非効率な可能性があります。 また、実際のところ SetMaxIdleConns は時間帯によって負荷が異なる中で常に適切な値を決めるのが難しいと感じます。そこで筆者らはシンプルに MaxOpenConns と同じ値に設定しています。長時間idleのコネクションが残ってしまうのを避けるためには SetConnMaxLifetime か SetConnMaxIdleTime を設定すれば十分と考えています。 SetConnMaxLifetime godoc の定義では「コネクションが再利用可能な最大時間」です。 内部的には、コネクションの createdAt からの経過で判定され、 conn の取得時と putConn の返却時にチェックされます。 設定しない場合、フェイルオーバーでプライマリが切り替わった際やDNSで接続先が変わった際に古いコネクションが残り続けるリスクがあります。ただ筆者らは ConnMaxLifetime は現時点では設定していません。今のところこれによる問題は発生していませんが、リスクはあるため今後設定を検討しています。 SetConnMaxIdleTime godoc の定義では「コネクションがidle状態でいられる最大時間」です。 内部的には returnedAt からの経過で判定されます。 公式ドキュメント では SetMaxIdleConns との併用が想定されており、バースト時に増やしたidle接続を負荷が減ったときに解放する用途です。 筆者らの環境ではKubernetes上でIstioを利用しており、Envoy sidecarのidle timeoutでDBサーバへのコネクションが切断されます。 そのためidle timeoutよりも短い ConnMaxIdleTime を設定し、Envoy側で切断される前にアプリケーション側からcloseするようにしています。 これにより切断済みのコネクションを使ってしまい ErrBadConn が発生するのを防いでいます。 まとめ 普段はO/R mapperの裏側に隠れていることも多い sql.DB ですが、内部実装を知っておくとDB関連の障害調査で「なぜこの接続が使われたのか」「なぜリトライが起きたのか」を説明できるようになります。また、goroutineやchannelを使った並行処理、mutexのロック範囲の最小化など、パフォーマンスを意識した実装パターンが随所に見られるので、Go自体の学びになる点も多いと感じました。この記事が sql.DB の設定を見直すきっかけや、その判断の参考になれば幸いです。 最後に、筆者ら認証認可グループでは以下の記事も公開しています。今後も発信していく予定ですので、あわせてご覧ください。 Go本格採用から1年──CADDi Control Planeの技術選定と振り返り CADDi の Control Plane を支えるシステムたちの紹介 Auth0を使って1年かけてSSOをサポートした話
こんにちは!Core SRE の 織田 英吾です。 キャディはクラウドネイティブ会議にブーススポンサーとして協賛させていただきました。 この記事では2日間にわたるイベント期間中のブースの様子や聴講したセッションの感想をレポートします。 ブース紹介 まずは、キャディのブースについて紹介します! ブースでは、 私たちが取り組む製造業にまつわる課題を知っていただくための「30秒図面捜索チャレンジ」を実施したり、おすすめの Claude Code Skills を共有するボードを用意しました。 以下が実際に寄っていただいた皆さんに記載いただいたボードの写真です。おすすめ Claude Code Skills や使っている機能などをたくさん教えていただきました。Skillsを自作されている方も多くいて、コードのレビューやドキュメント作成が特に多かった印象です。私が知らなかったものも多くあり、試してみようかと思いました。 私たちが提供している「製造業AIデータプラットフォームCADDi」のアプリケーションである「製造業データ活用クラウドCADDi Drawer」のデモも行いました。 デモを通して、製造業出身者や所属の方以外にも、製造業が抱える課題に共感していただくことができました。私自身、キャディが取り組んでいることの重要さを再認識することができた機会になりました。 名古屋での開催ということもあり、製造業出身や所属の方も多く、現場の声やペインポイントを聞くことができ、私が学ばせていただく機会にもなり、とてもいい経験をすることができました。 イベント期間中、キャディのブースに立ち寄ってくださった皆さん、本当にありがとうございました! 聴講したセッションの感想 次に、私や他のメンバーが聴講させていただいたセッションの感想を簡潔にまとめます。 「100マイクロサービスのTerraform/Kubernetes管理地獄から抜け出すためのAI活用術」 株式会社LegalOn Technologies の Ishigaki さんと Wada さんによる発表です。 本セッションでは、マイクロサービスの増加に伴って Kubernetes / Terraform 管理や設定変更、レビュー対応が大きな運用負荷になるという課題が紹介されました。その解決策として、定型的な変更作業は仕様化・ツール化し、AIエージェントを活用してサービス単位で並列に展開するアプローチが示されており、とても参考になりました。 特に印象的だったのは、AI にすべてを任せるのではなく、社内ルールやガイドラインを AI が参照しやすい形に整備し、人間が最終判断を担う設計にしていた点です。これにより、大量の PR 作成やレビュー支援、移行作業を効率化しながら、運用品質を保つ工夫がなされていました。 AI 活用の前提として、業務プロセスやナレッジを構造化しておくことの重要性を感じるセッションでした。(Core SRE 織田) 登壇資料: https://speakerdeck.com/markie1009/kubernetesguan-li-di-yu-karaba-kechu-sutamenoaihuo-yong-shu 「そのSLO99.9%、本当に必要ですか? 〜優先度付きSLOによる責任共有の設計思想〜」 株式会社TopotalのVTRyoさんによる発表を聴講しました。以前、SREKaigiでもVTRyoさんの発表を聞いたことがあり、とても参考になったので今回も楽しみにしていました。 本発表では、SREチームはDevチームのEnablingを突き詰めていくと、横断で複数のサービスを見ていくことが増えていくことで、運用しているサービスで何が起きているのか分からなくなる可能性が高くなりやすいと話されてました。実際に、VTRyoさんは自分たちはSREとしてサービスに価値を生み出していないのではないかと葛藤が生じたそうです。 そこで紹介されていたのが Product-Focused Reliability for SRE (PER)という考え方でした。自分は初めて聞いたのですが、SREチームが限られたリソースを有効活用するために、ビジネスやユーザーにとって最も重要なことに焦点を当て、優先順位をつけて活動することだそうです。 確かに、従来のSREはインフラやサービスそのものの安定稼働に責務を持つため、自然とDevはアプリケーション、SREはインフラという得意領域で境界が分かれてしまいやすいです。 PERの考え方は、SREの関心ごとをサービスからプロダクトの重要機能に移し、プロダクト中心にSLOを考えていくため、よりユーザーやビジネスの理解が重要になってきます。自分自身、SREが信頼性という指標を追っているのはユーザーの満足度とビジネスの成功度を上げるためだと思っているので、PERの考え方はとてもしっくりきました。 また、SREは日々のアラート対応に追われがちではありますが、それは本当にユーザーの離脱や解約につながっているのか?実は過剰な信頼性を追っているだけで、根本的な問題は機能不足なのではないか?、本セッションの問いとして投げかけられていた「そのSLO99.9%は本当に必要なのか」は定期的にSLOを見直す際に立ち返りたい問いだなと思いました。(Drawer SRE 松嶋) 登壇資料: https://speakerdeck.com/vtryo/is-that-99-dot-9-percent-slo-really-necessary-design-philosophy-of-shared-responsibility-through-prioritized-slos 登壇レポート キャディからCore SREチームリーダーの小林明斗が登壇しました。 セッション名:生成AI時代に信頼性をどう保ち続けるか - Policy as Codeの実践 概要:生成AIによりコードやIaCの変更量が増える中、人手のレビューやチェックリストだけでは信頼性リスクを見落としやすくなっています。本セッションで は、組織ポリシーのうち静的に評価できる項目をPolicy as CodeとしてCIや実環境に組み込み、人の認知に依存しすぎず信頼性を維持する設計を紹介し、ConftestやKyvernoの実装例を交え、誤検知対応、ポリシー強度、既存リソース是正など、形骸化しないガードレール運用の工夫を共有しました。 開発組織の変遷や SRE の歩みから入り Policy as Code の紹介、実装方法の共有がされていました。私はまだ入社3か月目なのもあり、キャディの私でも学べるセッションでした。 会場の席はほぼ埋まり、立ち見がでるほどの盛況ぶりで約100名の方が参加されていました! SNSでも「Policy as Code が気になっている」、「PRC のレビューが大変なのわかる」、「Policy 違反1,684件すべて修正完了はすごい」などたくさんの反響をいただきました。キャディから登壇者を出せてとても良かったです。 Policy 違反の Report に「Policy の詳細や OK/NG の例が記載された wiki」へのリンクを入れているらしい。これを見ることで Policy 違反が発生した時の開発者側での修正をやりやすくしてる感じっぽい。よさそう。 #cloudnativekaigi #cloudnativekaigi_b — こたつ&&みかん (@kota2and3kan) 2026年5月15日 x.com 1684件すべて修正完了 すごすぎる #cloudnativekaigi #cloudnativekaigi_b — yuki / ほにゃにゃ (@honyanyas) 2026年5月15日 x.com うおー、ポリシーに対するテストコード書けるのめっちゃいい。 #cloudnativekaigi #cloudnativekaigi_b — たか (@_nogtk_) 2026年5月15日 x.com 登壇資料: speakerdeck.com 終わりに ブースへの来場・セッション聴講・懇親会と充実した2日間でした。クラウドネイティブな技術の進化は非常に速いですが、今回学んだ知見を業務に活かしていきたいと思います。 スピーカーの皆さん、参加者の皆さん、クラウドネイティブ会議の運営の皆さん、素晴らしいイベントをありがとうございました。今後もキャディは、技術的な挑戦や課題解決の知見を発信し、エンジニアコミュニティへの貢献を続けていきます。今後のイベントでもお会いできるのを楽しみにしています!
プログラミング言語を選ぶとき、開発効率や学習コスト、エコシステムの充実度など、考慮すべき要素は多岐にわたります。OSS パッケージを標的にしたサプライチェーン攻撃の増加や脆弱性に対するゼロデイ攻撃の発生といった状況を踏まえると、「アプリケーションが依存するライブラリをどう管理するか」も技術選定の重要な軸となっています。 私の所属する Control Plane 部の認証認可グループでは、2025年から、 Go の利用を本格化しました。技術選定時に挙げられていたメリットの一つが、まさに依存管理にあります。 本記事では、私たちが Go を選んだ背景と、1年間実際に開発してみて分かったことを振り返ります。 なぜ Go を選んだか 対象システムの特性 キャディの主流言語: TypeScript Go の特徴 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ AIコーディングとの相性 苦労した点 ライブラリに頼らないことのトレードオフ Go の慣習に慣れるまでのコスト 期待と現実のギャップ 採用した技術スタック まとめ なぜ Go を選んだか 対象システムの特性 キャディでは、製造業AIデータプラットフォームCADDi の基盤として「Control Plane」と呼ばれるシステム群を開発しています。Control Plane は、テナント管理や認証認可など、アプリケーションの機能から独立した管理層を担うシステムです。詳しくは こちらの記事 で紹介しています。 開発言語を選ぶにあたって重要だったのは、Control Plane が持つ特性です。Control Plane は、1つの大きなサービスではなく、認証ゲートウェイ、トークン発行、テナント管理、認可といった、それぞれが明確な責務を持つ小さなサービスの集合体です。各サービスはシンプルな機能を提供する一方で、プラットフォーム基盤としての信頼性が求められます。 つまり、「小さなサービスを多数、手堅く作れること」が言語選定において重要な要件でした。 キャディの主流言語: TypeScript キャディのバックエンドでは TypeScript が主流です。TypeScript の開発エコシステムの充実度は群を抜いています。 また、フロントエンドとバックエンドを同じ言語で統一できるのも大きなメリットです。 一方で、依存管理の観点では、ライブラリの数が増えがちで、依存管理のコストが増大します。 Go の特徴 Go は Web API の構築に向いた言語です。標準ライブラリもサードパーティのライブラリもシンプルなものが多く、必要なパーツを組み合わせて開発するようなスタイルを取りやすいです。 こうした特性が、小さなサービスを数多く開発・運用する Control Plane とよくマッチすると判断しました。 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ Go を採用して最も良かったと感じているのは、メンテナンスコストの低さです。 Control Plane のアプリケーションは、一度作った後は数ヶ月単位で改修が入らないことも珍しくありません。そのような場合でも、セキュリティアップデートは定期的に適用する必要があります。このような作業は1つ1つは小さくても、積み重なると大きな負担になります。 Control Plane には、Go採用以前から開発・運用されている TypeScript(NestJS)アプリケーションがありますが、これと比較して Go アプリケーションはセキュリティアップデートが必要になる頻度が低いと感じています(およそ1/5程度)。 AIコーディングとの相性 Claude Code や Devin を用いたAIコーディングはキャディ社内でも主流となっていますが、構文がシンプルで書き手による差異が生じづらい Go は、AIが読み書きしやすいという点でもメリットがあります。また、コードフォーマッター( go fmt )やテストランナー( go test )が標準のツールチェインに組み込まれていて、かつ、高速に動作するという点も Go の強みです。 苦労した点 ライブラリに頼らないことのトレードオフ Go では依存ライブラリを少なく保って開発することができます。これは裏を返せば、基本的な処理を自前で実装する必要があるということです。例えば、以下のコードではサーバのグレースフルシャットダウンを行なっています。 ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() go func () { // e は Echo インスタンス if err := e.Start( ":" + c.Port); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error(fmt.Sprintf( "error occurred when starting the server: %v" , err)) } }() ... <-ctx.Done() ctx, cancel := context.WithTimeout(ctx, 10 *time.Second) defer cancel() if err := e.Shutdown(ctx); err != nil { slog.Error(fmt.Sprintf( "error occurred when shutting down the server: %v" , err)) } このような、他の言語であればフレームワークが担ってくれるような処理も自分たちで書く必要がありました *1 。 依存の少なさと自前実装の手間は、トレードオフの関係にあります。私たちはこのトレードオフを意図的に受け入れました。長期的な運用コストの低さのほうが、初期の実装コストより重要だと判断したからです。 Go の慣習に慣れるまでのコスト チームメンバーの多くは Go での開発経験がそれほど多くありませんでした。Go 特有の慣習やイディオム ── たとえばエラーハンドリングの作法や、パッケージ構成の考え方 ── に慣れるまでには時間がかかりました。言語仕様の学習コストは低くても、「Go らしいコード」がわかるようになるまでには別のハードルがあります。 期待と現実のギャップ Go の採用にあたっては、goroutine による並行処理のしやすさにも期待していました。しかし、現時点では並行処理を積極的に活用する場面はまだありません。現在の Control Plane のサービス群は、典型的なリクエスト/レスポンス型の API が中心であり、goroutine の恩恵を実感する局面がまだ訪れていないのが実情です。 今後、バッチ処理や非同期ワーカーのようなワークロードが増えてきた際に、この特性が活きてくると考えています。 採用した技術スタック Goコミュニティにおいて実績のある技術スタックの中で、薄めのものを選定しました。 通信: Connect フレームワーク: Echo ※アプリケーションによってはフレームワーク無しの場合も ORM: Bun JWT: lestrrat-go/jwx いずれも優れたライブラリですが、特に Connect は素の gRPC に比べてデバッグしやすく、重宝しています。 まとめ Go は万能な言語ではありません。例えば、Go の特徴の一つである明示的なエラー処理は、コードの冗長さと表裏一体です。しかし、小さなサービスを数多く開発・運用する Control Plane においては、Go のシンプルさが強みになりました。 技術選定は常に文脈次第であり、私たちの経験がそのまま他の組織に当てはまるとは限りません。それでも、「Control Planeを Go で作る」という選択は、私たちにとって正しかったと考えています。 今後も Go を活用しながら Control Plane を拡充し、CADDi のマルチテナントプラットフォームをより堅牢なものにしていきます。 Go を使った Control Plane の開発に興味がある方は、ぜひ以下のページもご覧ください。 caddi.tech tech.caddi.com *1 : このサンプルコードは Echo v4 です。Echo v5 ではフレームワークに グレースフルシャットダウン機能 が搭載されています。
はじめに こんにちは。キャディ株式会社でソフトウェアエンジニアとして製造業AI見積クラウドCADDi Quoteの開発を担当している松田です。 私たちのチームでは、新機能の開発時など、プロダクトに新しい概念を導入する際には、開発者のみならずPdMやQAといった他職種を交えて議論し、ユビキタス言語を定義しています。 ユビキタス言語とは、ドメイン駆動設計においてビジネスと設計の橋渡しをする共通言語です。ドメインエキスパートとエンジニアが、会話・コード・ドキュメントに渡って同じ言葉を使うことで、ビジネスと設計の間の翻訳コストの発生を防ぐことが目的です。 出典:エリック・エヴァンス著、今関剛監訳、和智右桂、牧野祐子訳『エリック・エヴァンスのドメイン駆動設計』翔泳社(2011) p.34より一部改変して筆者作成 私たちがこれまでにユビキタス言語として定義した用語の多くは、チームの業務において実際に用いられ、生きた言葉として価値を発揮しています。 その一方で、定義した用語が実際の議論やドキュメントの中でうまく定着せず、形骸化してしまうケースも経験しています。使われないユビキタス言語はなぜ生まれてしまうのでしょうか? この記事では、こうした経験をもとに、使われないユビキタス言語を生んでしまう7つのアンチパターンを紹介します。これらのアンチパターンは、「誰を巻き込むか」「どう考えるか」「定義後の運用」の3つの観点にまたがりますが、網羅的なものではなく、私が実際の運用の中で失敗や悩みを抱えたポイントに重点を置いています。ユビキタス言語という概念は知っているが、実践の中でつまずきを感じている方に読んでいただければと思います。 はじめに アンチパターン1: エンジニアだけで決めてしまう 問題 なぜ起きるか どうすればいいか アンチパターン2: ドメインエキスパートという概念に固執してしまう 問題 なぜ起きるか どうすればいいか アンチパターン3: ドメインエキスパートの言葉を無批判に受け入れてしまう 問題 なぜ起きるか どうすればいいか アンチパターン4: 英語名をAIに相談せずに決めてしまう 問題 どうすればいいか アンチパターン5: 理詰めで考えてしまう 問題 なぜ起きるか どうすればいいか アンチパターン6: 声に出した時の自然さを軽視してしまう 問題 なぜ起きるか どうすればいいか アンチパターン7: ユビキタス言語辞書の作成が目的化してしまう 問題 なぜ起きるか どうすればいいか おわりに アンチパターン1: エンジニアだけで決めてしまう 問題 ユビキタス言語の定義からして当たり前ですが、エンジニアだけで議論するのはアンチパターンです。私たちのチームではエンジニアもお客様を訪問したり、セールス、カスタマーサクセス、PdMといった他職種のお客様訪問レポートを読むなどして顧客理解に努めていますが、顧客理解という点においてPdMにはかないません。エンジニアだけで議論すると、PdMなどの他職種が持つ貴重な知見を設計に反映する機会を失うことになります。また、エンジニアはどうしても実装をイメージしながら議論してしまうので、用語が実装に引きずられがちにもなります。 この場合、PdMからするとユビキタス言語の存在自体を認識していないことになるので、エンジニアだけがユビキタス言語を使い、PdMは別の言葉でエンジニアと会話する状態になるでしょう。 なぜ起きるか 背景には、いくつかの思い込みがあります。 ひとつは、遠慮です。PdMなどの他職種は忙しいので、設計寄りの議論に巻き込むのは申し訳ないという気遣いが働くことがあります。 また、説明コスト回避の心理も働きます。ユビキタス言語という概念を他職種に説明しながら議論するのを面倒に感じ、エンジニアだけで決めてしまいたくなることがあります。 どうすればいいか タイムボックスを区切った上で、PdMなどの開発者以外のメンバーを集め、効率よく議論しましょう。 そのために有効な手法がイベントストーミングです。イベントストーミングは、複雑な業務ドメインを参加者のコラボレーションにより探索するワークショップ形式の手法です。参加者が付箋を出し合い、それらを構造化する作業を通じて、ドメインの全体像が見えてきます。 参加者がそれぞれの理解に基づいて付箋を貼る中で、同じ概念が人によって別の言葉で表現されている状況が可視化されます。メンバー間の言葉のずれを発見し、用語の統一を図る中で、自然とユビキタス言語が定義されていきます。 イベントストーミングの良いところは、ドメイン駆動設計やユビキタス言語といった概念の理解が薄いメンバーであっても、ファシリテーターの指示をステップバイステップでこなしながらワークに参加できるところです。ファシリテーターにとっても、ワークの進め方を一度身につけてしまえば、参加者に対する説明コストはそれほどかかりません。 また、イベントストーミングを終えると、ユビキタス言語だけでなく、要件やドメインモデルの全体像も見えてきます。要件に関する認識合わせの中で自然とユビキタス言語も整理される形となるため、時間効率も良いと言えます。 イベントストーミングについては、キャディのTech Blogでも紹介されていますので、ぜひご覧ください。 caddi.tech アンチパターン2: ドメインエキスパートという概念に固執してしまう アンチパターン1はエンジニアだけで閉じてしまうという問題でした。これから議論するのはその逆方向の問題です。理想のドメインエキスパートがいないなら、ユビキタス言語の議論をしても意味がないという心理に陥るケースです。 問題 ドメイン駆動設計では、ドメインエキスパートとの協働がユビキタス言語の前提とされています。 ドメインエキスパートとは誰なのでしょうか? 通常は、そのソフトウェアを利用するユーザーとなります。私たちキャディで言えば、製造業の現場で働く方々、とりわけ、業務全体を俯瞰し、業務プロセスの設計や変革をリードしている方々です。 そうすると、ユビキタス言語を定義する際には、実際のお客様を会議にお招きし、議論をする必要があるのでしょうか? 多くの場合、それは非現実的です。 このような場合に、ドメインエキスパートを呼べないのであれば、ユビキタス言語の定義をしても意味がないのでは? という議論が発生したり、悩みを抱えてしまうことがあります。これが2つめのアンチパターンです。 なぜ起きるか EvansのDDD本では、ドメインエキスパートと開発者が継続的に対話しながらユビキタス言語を洗練させていく様子が描かれています。しかし現実には、ドメインエキスパートは自身の担当業務で多忙であり、ソフトウェア開発のためだけに継続的な時間を確保してもらうのは困難です。 特にSaaSの場合、この難しさはより顕著です。SaaSにおいてサービス提供者と顧客は一対多の関係にあり、顧客の要望がすべてプロダクトに反映されるわけではありません。提示した意見がそのままプロダクトに反映されるならまだしも、ただ設計を洗練させるためだけに貴重な時間を割くのは、ほとんどのお客様にとって難しいでしょう。 どうすればいいか ドメインエキスパートという概念に固執するのはやめ、現実的な妥協をしましょう。 現時点で顧客の言葉遣いをよく把握している社内メンバーを探し、その人を知識の源泉として活用しましょう。私たちのチームの場合、幅広い顧客と日常的に接し、業界全体の課題を俯瞰的に捉えているPdMがドメインエキスパート役として最適なことが多いと考えています。教科書通りのドメインエキスパートを求めるのではなく、現実的にアクセスできる範囲内でドメイン知識を吸収しましょう。 アンチパターン3: ドメインエキスパートの言葉を無批判に受け入れてしまう 問題 ユビキタス言語を議論する際、PdMに「お客様はこれを何と呼んでるんですか?」と質問することがあります。ここで、議論に疲れていると、お客様が使っている言葉をそのまま採用したくなってしまう瞬間があります。 しかし、お客様の個別の業務の文脈においては意味が通じる言葉であっても、私たちが作ろうとするプロダクトの文脈にそのまま持ち込むと具体性が高すぎたり、逆に意味が曖昧であったりすることがあります。 私たちのチームでも、製造業の調達業務においてサプライヤー企業からバイヤー企業に対して提出されるさまざまな種類の見積資料をどう呼ぶかを議論した際に、この点が問題となりました。PdMに、リリース後最初の想定ユーザーとなるお客様がその資料を何と呼んでいるのかを質問したところ、お客様は単に「見積書」と呼んでいるとのことでした。そのお客様にとっては、さまざまな種類の見積資料のうち、「見積書」と言えばこれを指す、というのが暗黙の合意となっていたのでしょう。しかし、さまざまな企業のお客様を対象とするSaaSプロダクトの文脈においては、「見積書」という表現は曖昧であり、私たちが開発している機能のスコープとは全く性質の異なる資料までもがそこに含まれてしまう懸念がありました。 結局、私たちは、お客様が実際にそう呼んでいるかではなく、業務慣習の異なる他のお客様や、開発者も含めて誤解が生じないことを優先し、資料の内容が明確に分かる別の用語を定義することになりました。複数企業のお客様を対象とするプロダクトとして、曖昧性を避けることを優先した選択でした。 なぜ起きるか ユビキタス言語という概念を、ユーザーや非エンジニアが用いる言葉をそのまま使用して開発することだと思い込んでいることが原因です。 実際には、ユビキタス言語は、ドメインエキスパートと開発者の双方が批判的意見を提示しながら洗練させるものです。このことは、Evans本でも以下のように明記されています。 ドメインエキスパートは、ドメインについての理解を伝えるには使いにくかったり不適切だったりする用語や構造に意義を唱えるべきであり、開発者は、設計を妨害することになるあいまいさや不整合に目を光らせるべきである。 出典:エリック・エヴァンス著、今関剛監訳、和智右桂、牧野祐子訳『エリック・エヴァンスのドメイン駆動設計』翔泳社(2011) p.27 どうすればいいか ドメインエキスパートからのインプットを、ソフトウェア設計上の概念として利用可能な抽象度に洗練させる責任は開発者にあることを認識しましょう。ドメインエキスパートが提示した用語を使って設計やコーディングを行うイメージが湧くかを常に想像し、違和感があれば対案を提示しましょう。 開発者側から対案を提示する際には、その対案に対して開発者以外の参加者が納得感を持っているかどうかにも目を配りましょう。往々にして発生するのは、議論の場ではユビキタス言語が合意されたものの、結局、開発者以外のメンバーは独自の用語を使い続けているという状況です。言葉が揃っていないことで苦労するのは結局のところ開発者なので、開発者以外のメンバーも含めてちゃんと納得感を持って使ってもらえそうかには注意を払う必要があります。 アンチパターン4: 英語名をAIに相談せずに決めてしまう 問題 DDD本を執筆した当時のEric Evansも想像していなかったでしょうが、現代のソフトウェア開発では、ドメインエキスパートや開発者と並んで、AIもユビキタス言語の策定の参加者となり得ます。 とりわけAIが活躍するのは、英語名の提案です。日本語圏でドメイン駆動設計を実践する際、現実的な妥協として、ユビキタス言語は日英併記で定義し、会話やドキュメントといった自然言語では日本語名を、ソースコードでは英語名を用いるといった手法が取られることが多いと思います。 日本語話者が英語名を定義する際、日本語を直訳したような英語名となり、英語として存在しない、あるいはニュアンスが意図と異なる表現になってしまうことがあります。私がチームの一員として関わった例で言えば、生成AIの登場以前の話ではありますが、「受注」という概念の英語名を Received order としてしまったことがありました。軽くインターネット検索すると分かりますが、このような表現は英語として通常用いられないようです。 また、ボキャブラリーが貧弱であるがゆえに FooData や BarInformation といった曖昧で意味の薄い名称となってしまうこともあります。 英語名の定義の際に以上のような問題を回避するには、AIを活用することが有効です。 どうすればいいか 英語名に自信がない場合はAIに提案してもらいましょう。 英語名をAIに考えてもらう上で重要なのは、日本語名を単語ベースで渡して翻訳させるのではなく、日本語名とその意味合いの説明文(加えて、既に決まっている英語名があればそれも)を一覧化し、全体を見せた上で日本語名に対応する英語名を提案させることです。こうすることで、単なる直訳ではなく、用語のニュアンスや用語間の関連性も踏まえた上での回答が期待できます。 また、複数の候補を提案させ、それぞれについて、ニュアンスや英語としての自然さを解説させた上で、どれを採用するか人間が選ぶべきです。決定する際は、インターネット検索により、その単語が実社会で意図したとおりのニュアンスで使用されているかを確認するのも忘れないようにしましょう。これにより、AIが提案した英語名をそのまま採用したら、実は日本語名とニュアンスがずれていた、といった事故を防ぐことができます。 アンチパターン5: 理詰めで考えてしまう ここまで取り上げてきたアンチパターンは、誰を議論に巻き込むかという問題でした。続く2つのアンチパターンは視点を変え、どう考えるかという軸から、使われない用語が生まれる構造を見ていきます。 問題 ユビキタス言語を定義する際、論理的な整合性を追い求めるあまり、聞いた瞬間に意味が伝わりにくい用語が生まれることがあります。このような用語は、議論の参加者が最初に提示した用語がどれもぴたりとはまらず、ああでもないこうでもないと色々な別案を出していくうちに発生します。 用語の意味を論理的に説明して理解できるのであれば、一見問題のないように見えます。しかし、長々と説明しないと意味が伝わらない用語は、会話の中で使うたびに説明のコストが発生します。結果として、メンバーは無意識のうちにその用語を避けるようになり、日常の議論には登場しない「公式用語」として辞書に残るだけの形骸化したユビキタス言語になっていきます。 なぜ起きるか ドメインエキスパートの言葉遣いを観察することから出発するのではなく、演繹的に用語を定義しようとしているのが原因です。 母国語について考えれば分かるように、自然言語は、演繹的に定義されたものではありません。そうではなく、ある言葉が使われているという事実が先にあり、そこから帰納的に文法や言葉の意味が定義されているわけです。 自然言語のこういった性質を踏まえれば、ユビキタス言語を定義する際も、ある言葉が自然に使われているという事実を重視すべきです。しかしながら、自然に使われている用語を設計にふさわしい抽象度に洗練させていく中で、理論先行に陥ってしまうことがあります。 どうすればいいか 前提として、論理的に用語を定義することを全否定するわけではありません。ソフトウェアは人工的な創造物であり、現実世界に対応物のない抽象的な概念を扱わざるをえない場合があります。自然に使われている業務の言葉だけでは表現できず、抽象的な論理から出発して命名せざるを得ない場面は一定あります。 ですが、まず優先すべきは、実際の会話の中で自然に飛び交っている言葉から出発することです。ドメインエキスパートやPdMが議論の中で無意識にある言葉を使っているなら、その言葉が直感的に選ばれたことには何かしらの理由があります。理詰めで独自の用語を作り出す前に、ある言葉が直感的に選ばれた理由を考察すべきです。 その上で、自然に使われている言葉の延長線上では必要な概念を表現できない場合に限って、理詰めで考えましょう。ただし、新たな用語を理詰めで作り出した時は、使われない言葉を生み出す道に片足を突っ込んでいるという意識を持つべきです。議論に疲れて結論を出す前に、立ち止まって、未来の自分たちが本当にその言葉を使って議論しているイメージが湧くかを考えましょう。 アンチパターン6: 声に出した時の自然さを軽視してしまう アンチパターン5では、どこから出発するかという問題を取り上げました。アンチパターン6は視点を変え、候補となった用語をどう検証するかという問題を扱います。 問題 アンチパターン5とも関連しますが、用語を定義する際、意味的な正しさだけに着目し、会話の中で自然に使えるかどうか、という観点が抜け落ちてしまうことがあります。 特に、文字ベースでの議論により用語の追加や修正をする場合、実際に誰もその言葉を口に出していない場合があります。意味として正確に定義できていても、声に出して使ってみると長すぎて発音しづらかったり、あまりにも仰々しくて会話の中で使うイメージが湧かない場合があります。 そのような用語は、合意された後も日常の会話には登場しません。ドキュメントやソースコードでは使われるかもしれませんが、会話では別の独自の用語が使われるという状態になってしまいます。 私自身も、最近の新機能開発で、漢字6文字の長ったらしい用語を定義してしまったことがあります。意味としては正確に定義したつもりなのですが、残念ながら、会話の中で使うには発音しづらく、直感性にも欠けていました。結果的に、ソースコードやドキュメントではユビキタス言語が用いられているものの、口頭の会話では、より簡潔な別の用語が使われてしまう状態になってしまいました。 なぜ起きるか 実際に声に出して使えるかという観点を、命名の評価基準として持っていないのが原因です。意味的な正しさはロジカルに言語化できるため議論の焦点となりやすい一方、声に出した時の自然さは主観に依存するため、議論が白熱すればするほど観点から抜け落ちがちです。 どうすればいいか 「意味的に正しいか」と別の評価基準として、「声に出して言いやすいか」「聞いた瞬間に意味が伝わるか」を命名の評価軸として意識しましょう。舌がもつれて発音しにくい用語や、説明なしには理解されない用語は、使われません。 そして、命名を確定する前に、未来の自分たちがこの言葉を使って議論しているイメージが湧くかを自問自答しましょう。特に、エンジニア以外のメンバーがその言葉を日常の会話で使っているイメージが湧かないなら、それは危険信号です。 最も直接的な検証方法は、実際に声に出してみることです。その用語を声に出すたびにストレスを感じたり、無意識に別の言葉を使ってしまうようであれば、用語を見直すべきです。 アンチパターン7: ユビキタス言語辞書の作成が目的化してしまう アンチパターン1〜6は、どうユビキタス言語を「定義するか」という問題を扱ってきました。最後のアンチパターンは、定義した後の話です。 問題 ユビキタス言語は辞書を作って終わりではなく、開発の中で使い続けることに意味がある、というのは誰しも分かっていることです。ですが、辞書の作成がゴールとなり、現実にはあまり使われないということが往々にして起こります。 なぜ起きるか 言葉選びは個々人のメンタルモデルを強く反映します。全員の共通言語であるユビキタス言語は、自分のメンタルモデルにフィットする最適な言い回しとは少しだけ異なります。そのため、ユビキタス言語を使い続けることには一定のコストが発生します。 ユビキタス言語を全員が使い続けるには、そのための意識と仕組みが必要になります。ただユビキタス言語辞書を定義しただけでは、各自が自分にとって最適な言い回しで会話する引力に逆らえず、形骸化の道を辿ってしまいます。 どうすればいいか ユビキタス言語を使い続けることについて、開発者がオーナーシップを持ちましょう。ユビキタス言語は開発者だけのものではありませんが、会話・ドキュメント・ソースコードのすべてで頻度高くユビキタス言語を利用するという意味で、ユビキタス言語によるメリットを最も享受するのは開発者です。 辞書を作ったところで、PdMなど他職種は多忙であるため、議論やドキュメントの際に逐一、辞書を確認して正しい用語を使ったりはしてくれないでしょう。そのため、開発者が率先して、議論、ドキュメント、開発チケットなどのあらゆる場でユビキタス言語を使い、定着を促す必要があります。ユビキタス言語が目に触れる頻度が増えれば、他職種も自然とユビキタス言語を用いる機会が増えるでしょう。 開発者が自信を持ってユビキタス言語を使い続けるために重要なのは納得感です。実際の開発では時間制約の都合上、十分な納得感がないまま、ユビキタス言語を決めてしまうことがあります。このように納得感がないと、ユビキタス言語の運用がなあなあになり、各自が思い思いの言葉で仕事を進める状況を許容してしまいます。 ユビキタス言語に対して納得感を持つため、この記事で提案したような観点も意識しながら、徹底的に用語を検討しましょう。本来、ユビキタス言語は使いながら改善していくべきものですが、現実には、一度データベースやAPIのスキーマとして書き込まれてしまうと、後から変更するのは大変です。そのため、最初にどれだけ納得感を持てるかが、その後の運用のベースラインとなります。 その上で、辞書をチームが実際に触れる場所に置くことも重要です。GitHubなど、日常的に参照できる場所に辞書を置き、コードレビューや設計議論の場で自然に参照される状態を作りましょう。辞書をリポジトリに置いておくと、AIもコード生成や設計検討の際に参照し、ユビキタス言語が自然に反映されるというメリットもあります。 おわりに 誰を議論に巻き込むか、用語の正しさをどう考えるか、定義した後どう使い続けるか——。7つのアンチパターンはそれぞれ異なる場面を扱っていますが、問うべきことは常に1つです。 論理的に整合していても、辞書にきれいに記載されていても、実際の会話やドキュメントに登場しなければ、ユビキタス言語に意味はありません。逆に言えば、定義がやや荒くても、チームが自然に使い続けている言葉は、立派なユビキタス言語です。 「未来の自分たちが、この言葉を使って議論しているイメージが湧くか?」 ユビキタス言語の議論のたびにこの問いを思い出してもらえれば、この記事の目的は達成されます。
製造業データ活用クラウド CADDi Drawerで SREを担当している佐藤です。 組織編成でCADDi Drawer専任のSREになったことを機に、長らく後回しにしていたDatadog MonitorのTerraform構成の再設計に取り組みました。複数チームの監視設定が単一リソースに混在しOwnershipが不明確なため、各チームが自律的に改善に踏み出せない状態でした。 本記事ではこの課題をOwnership単位のモジュール設計で解消した経緯と学びについて紹介します。 監視設定が抱える課題 単一リソースに積み上がった監視設定 Ownershipの不在によるアラート改善の停滞 Ownershipを明確化したモジュール設計 監視ロジックとOwner情報の分離 Owner情報の一元管理 ふりかえり よかったこと 苦労したこと 移行の現状と今後 おわりに 監視設定が抱える課題 キャディでは監視基盤にDatadogを採用しており、Datadog Monitorはすべて監視用の単一リポジトリでTerraform管理しています。 歴史的経緯により、監視すべき内容については検討できていましたが、Terraformモジュールとしてどう実装するかは整理されていませんでした。 このためDatadog MonitorのTerraform実装には以下のような課題を抱えていました。 単一リソースに積み上がった監視設定 Terraformモジュールの設計が難しい問題です。適切な抽象化の粒度を見極め、再利用可能な形でモジュール化するには、対象サービスや組織の全体像が見えていることが前提になります。 組織・サービスが急成長していた時期には監視対象が次々と増え、その都度設計を見直す余裕はありませんでした。この結果、再利用可能な形でのモジュール化は採用せず、既存の設定を拡張する形で対応し続けていました。 具体的には、監視対象のサービスが増えるたびに新規のモジュールやモニターを構築することはせず、既存の単一のDatadog Monitorに対象サービスを追加し続けていました。 Terraformのdatadog_monitorリソースでは監視条件をquery属性の文字列として定義しますが、サービスが増えるたびにOR条件を追記し続けた結果、以下のような状態になっていました。 logs( "status:error (service:SERVICE_A OR (service:SERVICE_B -message:\"noisy message\") OR service:SERVICE_C OR service:SERVICE_D OR (service:SERVICE_E env:production))" ).index("*").rollup("count").last("5m") > 100 組織規模が小さく、サービス数が2-3個の場合やサービス固有の除外条件が含まれていない場合にはこれでも上手くいきました。 しかし、サービス数が増え、サービスごとの監視条件が混在するようになると、とたんにコードからどのサービスがどの条件を持つのか読み解くことが困難な状態になりました。 また、問題の兆候は認識していましたが、全サービスに影響する監視設定の一括整備に踏み切るきっかけがなく、他のタスクを優先していました。 Ownershipの不在によるアラート改善の停滞 この課題に向き合うきっかけになったのが2025年に実施した組織編成です。 それまで全社横断でインフラ運用を担っていたSREチームから、サービス・プロダクトごとにSREが配置される形になりました。 私はCADDi Drawer専任のSREとして新たな役割を担うことになりました。 CADDi Drawerの信頼性向上に絞った視点で改めて監視設定を見ると、Datadog Monitorの複雑化に関する問題が浮き彫りになりました。 複数サービスの設定が混在したクエリへの変更は、git diff や terraform planでも影響範囲が掴みにくい状態でした。 Ownerの異なるチームが同じリソースを共有しているため、「他チームに影響しないか」という懸念が変更の腰を重くし、細かな改善がどんどん後回しになっていました。 クエリ実装の複雑さ以上に懸念になっていたのがSlackのアラート通知先の混在でした。 Datadog MonitorのSlack通知チャンネルの動的切り替えはクエリ条件以上に複雑になるため、Datadog Monitorごとに単一のSlackチャンネルに通知していました。 その結果、複数チームのアラートが同じSlackチャンネルに通知され、関係のないアラートであっても内容を都度確認する割り込みコストが発生していました。 SREチームで議論した結果、これらの問題の根本は、どのチームがどのモニターに責任を持つかが不明確というOwnershipの欠如にあると判断しました。 複数チームのサービスが1つのdatadog_monitorリソースを共有しているため、変更が他チームに影響するかどうかを確認せずに修正できません。 自チームの判断だけで変更に踏み切れず、誰も主体的に手を入れられない状態になっていました。 Ownershipを明確化したモジュール設計 これらの課題を解決し、Datadog MonitorのOwnershipを明確化するため、監視項目のTerraformモジュールを設計して移行しました。 監視ロジックとOwner情報の分離 従来の構成では、通知先や担当チームといったOwner情報がモジュール内に直接書き込まれていました。Owner情報が埋め込まれた構造では、呼び出す側がチームごとの情報を渡す設計にはできません。 具体的には以下のような構成でした。 terraform/ ├── modules/ │ └── monitor/ │ └── shared_monitors/ # 全サービス・全チームの監視設定が混在 └── environments/ ├── <google_project>-development/ │ └── main.tf # shared_monitorsモジュールを呼び出す └── <google_project>-production/ └── main.tf # shared_monitorsモジュールを呼び出す terraform/environments/<environment>/main.tf が shared_monitors モジュールを呼び出す構成です。このモジュールに全サービス・全チームの監視設定が集中していたことが、前述の問題の原因でした。 そこでこのモジュールを解体し、監視ロジックの情報に限定したモジュールを作成した上で対象サービス・通知先・担当チームといったOwner情報は呼び出し元から変数として渡す設計にしました。 具体的には以下のような構成です。 terraform/ ├── main.tf # environments をモジュールとして呼び出す ├── teams.tf # チーム情報を一元定義 ├── modules/ │ └── common/ │ ├── cloudrun-service/ # Cloud Run監視モジュール │ ├── alloydb/ # AlloyDB監視モジュール │ └── ... └── environments/ └── <google_project>-production/ └── service-a/ ├── locals.tf # 通知先・Owner情報を定義 └── main.tf # 各監視モジュールをOwner情報と共に呼び出す 監視項目の種類( cloudrun-service など)ごとにモジュールを定義し、 terraform/environments/<environment>/<service>/ というサービス単位のディレクトリから呼び出す構成です。 モジュール自体は監視対象のプロダクトを知らず、呼び出すディレクトリごとにOwnershipが決まるため、「誰がどのアラートに責任を持つか」がディレクトリ構造として表れます。 各モジュールに対するOwner情報を変数として以下のように設定することで、サービスごと・モニターごとのOwnershipを明確化できました。 # environments/project-production/service-a/main.tf module "cloudrun_service_monitor" { source = "../../../modules/common/cloudrun-service" service_name = "service-a" env = "production" owner_teams = local.owner_teams mention_slack_alert_ch = local.mention_slack_alert_ch mention_oncall = local.mention_oncall } この設計により、変更の影響範囲が自チームのスコープに収まり、他チームを巻き込まずに監視設定を改善できるようになります。移行後はこの設計方針をドキュメントにまとめ、社内で共有しました。設計を標準として明文化することで、チーム間の設計認識を統一し、今後の監視設定追加・変更にも一貫したOwnership管理を維持できるようにしました。 Owner情報の一元管理 監視対象ごとにDatadog Monitorが分割されることで、移行後はモニターの総数が増えます。多数のモニターを管理するにはタグ情報が欠かせないため、各Datadog Monitorへのタグ付けを徹底しました。 タグ自体は従来から設計に組み込まれていましたが、今回の移行を機に全Datadog Monitorへの適用を強化しました。 タグ情報の適用を強化する上で、通知先Slackチャンネル、担当チーム名といったOwner情報を各サービスの設定に個別に書き込むと、組織変更のたびに全サービスのファイルを修正する必要があります。また、複数の場所に分散して書くとチーム名やSlackメンションの表記ゆれも起きやすくなります。 そこで、チーム情報を teams.tf に一元定義しました。Datadog Teamリソースもここで合わせて作成します。 # teams.tf locals { owner_teams = { team_alpha = { name = "Dept A > Group A > Team Alpha" mention = "<!subteam^XXXXX>" # @team-alpha tag = "team:team_alpha" } team_beta = { name = "Dept B > Group B > Team Beta" mention = "<!subteam^YYYYY>" # @team-beta tag = "team:team_beta" } } } resource "datadog_team" "teams" { for_each = local.owner_teams name = each.value.name handle = each.key } 組織変更が起きた場合も teams.tf の修正だけで全サービスに反映されます。 ふりかえり よかったこと サービスごとに通知先を変更できるようになったことがメリットとしては大きいです。 チームごとにSlackチャンネルを切り分け、CADDi Drawer 関連のアラートのみに集中できる環境を整えたことで、業務への割り込みを最小限に抑えることが可能になりました。 datadog_monitor がOwnerごとに分割されたことで、PRレビューの負荷も下がり、自チームの判断だけで変更をマージできるようになりました。 モジュール化に伴い監視条件を変数として宣言的に記述する構成になったことで、各サービスの監視条件を個別に把握できるようになった点も嬉しいポイントです。 苦労したこと 最も苦労したのはDatadog Monitor移行に伴うPRレビューです。Datadog Monitorリソースの分割には destroy / create が伴う上、クエリの整理も必要だったため、大量の terraform plan の差分が発生しました。これをすべて人手で確認しきることは難しく、想定以上にレビューに時間がかかってしまいました。 今回は利用できませんでしたが、AIを活用したリソース移行の半自動化や設定差分の検出など、効率化の仕組みを今後の大規模移行に活かしたいと考えています。 移行の現状と今後 作業リソースやスケジュールの都合上、通知頻度の高いコンポーネントの移行を優先して完了させました。旧モジュールへの新規追加を禁止することでこれ以上の複雑化を防ぎつつ、通知先のチャンネル分離も継続して進めています。一方、一部のDatadog Monitorはまだ解体する必要があるものが残っており、引き続き整備を進める予定です。 設計方針はドキュメントとして共有しているものの、まだ十分に浸透できているとは言えません。設計の定着にはドキュメントだけでなく、ポリシーチェックをCIに組み込むような仕組みも必要だと感じています。今後は組織横断チームと連携しながら検討を進めていきます。 おわりに 組織の自律性を高める上で、監視設定のOwnershipも合わせて整備することが重要だと実感しました。Ownershipが明確であれば、自チームの判断だけで監視設定を改善でき、変更のスピードも上がります。 今回は組織編成がきっかけとなり、長らく後回しにしていた構成の再設計に取り組むことができました。 普段から監視設定を気軽に修正できる状態を維持し、組織や役割が変化した際には監視のOwnershipが適切に機能しているかを継続的に見直していきます。
機械学習エンジニアの竹本です。普段は、製造業の膨大なドキュメントを対象にしたRetrieval-Augmented Generation(RAG)の検証に取り組んでおり、その一環として社内検証向けのベンチマークデータセットを作成しました。同じ課題を抱えるエンジニアの方の参考になればと思い、本記事を書きました。 TL;DR 作りたい。でも動き出せなかった 当時の思い込みと、今の自分からの答え合わせ 1. LLM-as-a-Judgeに頼るしかないと思っていた 当時の状況 データセットを作ってみると 2. データセットを作れるのか? そしてCSやユーザーに頼っていいのか? 当時の状況 CSに相談してみると 3. 時間も工数もかかるのに、そこまでしてやる価値があるのか 当時の状況 データセットを作ったら検証速度が5倍に向上 作る過程で得られた想定外の価値 まとめ TL;DR 最初は、コストや体制の壁があり作成に動き出せなかった 制約を取っ払って考え、「作りたい」と声に出したら、動き出せた ベンチマークデータセットを作って、検証スピードが約5倍になった 作る過程そのものも、ユーザー理解を深めるきっかけになった 作りたい。でも動き出せなかった RAGの検証を進める中で、製造業の深いドメイン知識や個社特有の知識が求められる領域では、LLMによる評価だけでは精度を正しく判断しきれないと感じるようになりました。評価用のベンチマークデータセットが必要だと分かっていたものの、 作成にかかる時間的コストの大きさと、自チームやアノテーションチームだけでは作りきれないという課題 から、なかなか動き出せずにいました。 転機となったのは、チーム全員で実施した「100本ノック」でした。3日間で一人100個ずつアイデアをスプレッドシートに書き出し、不確実性やコストといった制約をいったんすべて取り払って、「本当は何をすべきか」を純粋に考え抜く取り組みです。その中で、「ドメインエキスパートと一緒にデータセットを作りたい」と提案しました。議論してみると、LLM-as-a-Judgeだけでは正しく評価しきれないという課題感と、データセットがあれば検証の質とスピードが大きく変わるという期待は、チーム内のエンジニアやPdMにも共通のものでした。コストやタイムラインといった制約がある中で、優先度が上がりきっていなかっただけでした。制約を外して考えたことでその必要性が共通の課題として顕在化し、チームとして取り組む合意が得られ、データセット作成が動き出しました。 そして実際に作成してみると、想定以上の価値がありました。 ここからは、当時抱えていた以下3つの思い込みを1つずつ答え合わせをしていきます。 LLM-as-a-Judgeに頼るしかないと思っていた データセットを作れるのか? そしてカスタマーサクセス(CS)やユーザーに頼っていいのか? 時間も工数もかかるのに、そこまでしてやる価値があるのか 当時の思い込みと、今の自分からの答え合わせ 1. LLM-as-a-Judgeに頼るしかないと思っていた 当時の状況 当時は、プロンプトチューニングや評価基準の調整を行い、LLM-as-a-Judgeだけに頼った定量評価をしていました。一定の指標として数値は出せていたものの、そのスコアをどれだけ信頼してよいのかという違和感は常にありました。実際、評価軸は「謝罪せずに回答しているか」「クエリと関連しているか」「直接的に答えているか」といった回答品質に限定した比較的表層的なものに寄りがちで、プロダクトの精度が上がるにつれて、それらの指標では差がつきにくくなり、改善の効果をうまく捉えられなくなっていきました。具体的には、定量評価では有意な差が出ていても、定性評価をしてみると実質的には同等だったり、逆に定量評価では差がないのに定性評価では明らかな改善が見られるといった乖離がしばしば発生していました。 そもそも、製造業で扱うドキュメント(不具合報告書や製品仕様書など)は、専門用語や文書構造がテナント・部署ごとに大きく異なり、読み解くには業務固有の背景知識も求められるなど、非常に専門性の高い領域です。長年蓄積された情報同士の関係も複雑で、単純なテキスト理解だけでは評価しきれません。こうした背景もあり、ドメイン知識やテナント固有の文脈を持たないLLMはもちろん、エンジニアによる定性評価も、どうしても表面的な確認にとどまりがちでした。それでも他に現実的な手段がなく、 LLM-as-a-Judgeだけに頼らざるを得ない 、というのが当時の実態でした。 データセットを作ってみると 実際にデータセットを作成してみると、この前提は大きく変わりました。 製造業のドメイン知識やテナント固有の知識を織り込んだGround Truth(詳細は割愛しますが、検索されるべきドキュメントとあるべき回答の2種類)を用意できたことで、それを基準にした評価が可能になりました。これまでのような表層的な評価では見えなかった改善の差分も、Ground Truthとの比較によって定量的に捉えられるようになり、 評価の解像度が一段上がった感覚 があります。また、定性評価においても、「なぜこのケースはうまくいったのか/いかなかったのか」を考える際の拠り所ができ、分析の質も大きく変わりました。 2. データセットを作れるのか? そしてCSやユーザーに頼っていいのか? 当時の状況 自チームやアノテーションチームだけではデータセットを作れないと認識していました。製造業のドメイン知識に加え、テナントごとの固有知識まで含めて網羅する必要がありましたが、その両方を十分に備えた人材は身近にいませんでした。さらに、対象となるコーパスはテナントごとに数万から数百万規模のドキュメントファイルに及び、その量も大きな課題でした。 データセットを作りたいという思いはあったものの、具体的な進め方が見えていませんでした 。 CSに相談してみると そうした中でCSに相談し、どのように進めればデータセットを作成できるかを一緒に検討しました。その結果、CSがユーザーにヒアリングの場を打診し、ユーザーと一緒にミーティングを行いながらデータセットを作っていく進め方を提案してくださいました。 実際のミーティングでは、事前に用意したデータセット設計をもとに、欲しい情報を一つひとつヒアリングしながら作業を進めました。ただ、1つのテナントに格納されているドキュメントは膨大で、様々な部門の何年もの歴史の中で蓄積されたものです。そのテナントのユーザーでさえ、どのような回答があるべきかは簡単には分かりません。それを一緒に揃えていくには、やはり時間も手間もかかりました。 それでも、 CSはユーザー体験の向上のために、ユーザーは業務改善につながるならばと、それぞれ積極的に関わってくれました 。そうした関わりを通じて、みんなが同じ方向を向いていると分かったことは嬉しい驚きでした。 3. 時間も工数もかかるのに、そこまでしてやる価値があるのか 当時の状況 当時は、データセットの作成に多くの時間と工数がかかることが明らかであり、さらに不確実性も高く、そもそも実際に作りきれるのかどうかさえ分からない状況でした。そのため、かけたコストに見合う価値が得られるのかを判断できず、取り組むべきかどうかに迷いがありました。 データセットを作ったら検証速度が5倍に向上 実際にデータセットを作ってみると、その価値は想像以上に大きいものでした 。Ground Truthが存在することで成功と失敗の判断が明確になり、定性評価において悩んだり迷ったりする場面が大幅に減りました。 例えば、Ingestion改善の検証では、以前は数十〜数百件の検索結果と回答結果を一件ずつ目視で確認した上で評価・示唆出し・次の改善策の検討を行う必要があり、全体で約1週間かかっていました。データセット導入後は、定量評価で全体の傾向を把握した上で深掘りすべきケースを絞り込めるようになり、一件ずつ精査する作業負担が大きく減ったことで、この一連の検証サイクルが約1日で回せるようになりました。 また、RAGについては新しい手法が次々と登場しますが、信頼できるデータセットがあることで、それらの手法の効果を迅速に検証・比較できるようになり、試行錯誤のハードルが大きく下がりました。 作る過程で得られた想定外の価値 さらに、データセットを作る過程そのものからも、想定外の価値が得られました。過去の会話履歴を網羅的に確認し、データセットに用いるクエリを幅広く抽出・選定する中で、ユーザーがどのような情報を求め、どのようなクエリとして入力しているのかを観察したことで、ユーザー理解が深まりました。また、ユーザーと一緒にデータセットを作成する過程で、参照されるべきドキュメントを具体的に把握できるようになり、実際のユーザークエリから適切なドキュメントにたどり着くまでの検索の流れをより深く理解できました。加えて、ベンチマークの評価指標についてチーム内で議論を重ねる中で、自分たちのプロダクトが本来提供すべき価値が何かも明確になっていきました。 まとめ データセットの作成には多くの時間と工数がかかりますが、それでも得られる価値は当初の想定を大きく上回るものでした。このすべてのスタート地点は、制約を取っ払って考えたことと、「作りたい」と言葉にして伝えたことでした。 今回は最初の一歩として、優先度の高いクエリを選定してデータセットを作成しました。これだけでも検証の質とスピードは大きく変わりましたが、さらに質を維持しながら作成工数を減らし、クエリのバラエティや量を拡充していきたいと考えています。
最近はデータ基盤をこねこねしているData Fabric部の 伊藤 ( @amaya382 ) です。今回はテストケースをAIに量産させた話を題材に、AI時代の効率化で考えていたことをお届けします。 参画していたプロジェクトが順調に進み、いよいよ大詰めが見えた辺りで2人でシステムテストを整理していたんです。影響範囲が大きい機能だったので覚悟はしていたのですが、数えてみると観点は数十、テスト項目は100を超え、手順に至っては数百ステップ。ついでにリリース日も決まっていました。 結論から言うと、テストケース作成の大部分をAIに任せることで無事リリースに至りました。振り返ると「とりあえず全部AIに任せる」では到底無理で、効いたのはボトルネックとの見極めとAIが品質を保って自走できる環境を丁寧に整えるアプローチでした。 本稿ではそのAIを自走させるための "仕込み" にフォーカスします。特定のツールや手法のハウツーではなく、AI時代における効率化の考え方の一事例として読んでいただけると嬉しいです。 背景:大量のテスト観点を前にして 自動化までの流れ 1. コンテキストの準備 2. 初期ケースの手動作成 3. AIとの対話的なケース作成 4. テストケース作成のSkill化 — 再現性を手に入れる うまくいったこと AI-friendly な環境がもたらした相乗効果 テスト環境やパターンコンテキストの活用 Skill化による再現性 残ったボトルネックと次にやりたいこと A. コンテキストすら準備してもらう B. 人間はスケールしない C. テストファーストへの組み込み おわりに 先日、弊社小森による 『YAML×AIで脱Excel地獄!テストの本質に思考を集中させよう』 という記事の末尾で予告されていたものにあたります。 背景:大量のテスト観点を前にして 膨大なケースをどうしようかと考え始めたちょうどその頃、 『YAML×AIで脱Excel地獄!テストの本質に思考を集中させよう』 で紹介されたYAML形式 + testdocツールが使える見込み、つまりテキストベースでテストケースを管理できる見込みが立ち始めていたところでした。YAMLでテストケースを書ければ、構造化されたテキストとしてGitで管理でき、PRベースのレビューもできる。これ自体はとても良い環境です。 とはいえ数百の手順を手で書きたいわけもなく… リリース日は計画済みで、早く終わりの見込みを立てて落ち着きたいという状態でした 😓 そこでまず考えたのが「このテキストベースでテストケースを書ける環境をどうやって活かすか」です。まずはテキストベースなのでAIへの入出力がダイレクトかつ効率的に使えそう、しかも構造化されたフォーマットなのでAIが生成するテストケースの品質も安定しやすいはずというところから、筆者らは大規模なテストケース作成をAIに任せてみることにしました。 自動化までの流れ 件数が多いことは最初からわかっていたので、どうしたら再現性の高い形に落とし込んで大部分を効率的に圧縮できるか、端的に言えば いかに楽をできるか を考えていました。具体的には、自走させるためのコンテキスト準備とClaude Skill化 1 による再現性で一気に進められるのではという仮説を立てていました。 大まかな流れは以下の通りです: 自動化までのフロー 前半のコンテキストの準備は地道な作業ですが、ここを丁寧にやることが後半でAIが自走できるかどうかの分かれ道になりました。順に説明していきます。 1. コンテキストの準備 AIにテストケースを書かせるためには、人間が書くときと同じように十分なコンテキスト (前提知識) が必要です。具体的には以下のようなものを整理しました。 テスト環境の情報 : テスト環境の構成やテスト用アカウント、テストデータ情報 テストパターンのマトリックス (決定表) : 条件の組み合わせで挙動が変わることがあるため、そのパターンを表形式で整理したもの 要件定義書 : 画面や機能単位で階層化して書かれていたため、ほぼそのまま入力に利用 画面のスクリーンショット : 既存の仕様に明文化しきれていなかったUIに関する情報を補足するため 特に重要だったのがテストパターンのマトリックスです。今回のプロジェクトでは、データと条件の組み合わせに応じた結果が特に重要であり、その組み合わせパターンが膨大でした。このマトリックスをCSVとして整理しておくことで、AIがテストケースの期待結果を組み立てる際の判断根拠として参照できるようにしました。 表1: テストパターン情報のイメージ 図面-01 図面-02 図面-03 ... ユーザー01 閲覧可 閲覧可 閲覧可 ... ユーザー02 閲覧可 閲覧不可 閲覧可 ... ユーザー03 閲覧可 閲覧不可 閲覧不可 ... ... ... ... ... ... 2. 初期ケースの手動作成 テストケースのイメージが全くない状態から生成させては期待するような結果は得られないだろうという仮定から、最初のいくつかのテストケースは手動で丁寧に作成しました。 全体のケース数からすれば十分小さい数ですが、これがAIにとってのお手本として使えるだろうと考えました。 後ほど生成されたものを見ると、YAMLのフォーマット、各項目の記述粒度、強調表示のルール、期待結果の書き方... こうした暗黙的な品質基準を、お手本を通じてAIに伝えられたことが大きかったです。全体のケース数に比べれば数ケースの手動作成は非常に効果的な投資でした。 手動で作成したお手本ケースのイメージ: testcases : - id : DAC-SEA-1-01 category : - キーワード検索+表示切替(ユーザー01) title : サムネイル表示(32件) priority : high precondition : - "*B*ユーザー01*B*でログインしていること" steps : - 画面左上のハンバーガーメニューから「図面」を開く - 画面右上の表示件数を*B*32*B*件にする - 画面上部のソート順を「↑(昇順)」「図番」に設定する - 画面上部の「キーワード」欄に「*B*`AUZ-0`*B*」を入力し、検索ボタンを押す expected : - "別表([](sheet://権限#ユーザー-図面))の通り、XXX、YYY" notes : page : P-DW-DRA-001 user : ユーザー01 requirement : d-2-1-1 3. AIとの対話的なケース作成 お手本となる初期ケースができたら、Claude Codeと対話的に新しいテストケースを作成していきました。 やりとりのイメージとしてはこんな感じで、お手本と同程度の質のケースをClaude Code上で作成できるようになるまで行いました。 > 「XXX-2 のテストケースを新規作成してください」 → AIがドラフトを生成 → 「そこの要件はここに置いてある資料を確認して」 → 「こっちのテストデータとテストユーザーの組み合わせを利用して」 → 「強調すべき箇所が足りない、注意すべき条件のあるデータは色を変えて」 → ... 10 往復もしないうちにお手本ケースと同程度のケースが完成! 最初の数回はこうしたフィードバックを重ねる必要がありましたが、会話が進むにつれてAIの出力品質はどんどん安定していきました。テストパターンのマトリックスをほとんど破綻なく活用していたのには正直驚きました。また結構雑に取得して入力にした画面のスクリーンショットも手順作成に活用されていて、言語化をスキップして入力に活用できる点でこれから重要になるだろうなと感じました。 AIに生成させたケースのイメージ。手動で作成したものと見分けるのは困難なレベルに感じた: testcases : - id : DAC-PRO-1-02 category : - プロジェクト一覧表示 title : アクセス権限のないプロジェクトは一覧に表示されない(ユーザー03) precondition : - "*B*ユーザー03*B*でログインしていること" steps : (...省略...) - プロジェクト一覧画面が表示されることを確認する expected : |- 以下のプロジェクトが表示されること : - *B*PJ-B*B* (権限あり) - *B*PJ-C*B* (public) (...省略...) *R*PJ-A*R*、*R*XXX用図面*R* (権限なし) は*R*表示されない*R*こと notes : page : P-DW-PRO-001 requirement : d-1-1-1, d-1-1-6 user : ユーザー03 4. テストケース作成のSkill化 — 再現性を手に入れる ここが今回の取り組みで最も効率化へのインパクトが大きかったであろうポイントです。 数回のフィードバックでテストケースが対話的に作れるものの、これを100回も繰り返したくはありません。 対話的にテストケースを作り上げる過程で蓄積されたマトリックスの参照方法、YAMLフォーマットの慣習、強調ルール、期待結果の書き方などこれらをすべて含んだ形で、Claude Skillとしてパッケージ化することを試みました。プロンプトは至極単純に「ここまでのテストケース作成手順をSkill化してください」とするだけ、以下のようなすぐに誰でも使えるClaude Skillが得られました 2 。 --- name : create-testcase description : XXX 機能のテストケースを YAML フォーマットで作成する allowed-tools : Read, Glob, Grep, AskUserQuestion, Bash(ls:*) --- # Test Case Creation Skill このスキルは、XXX のテストケースを作成する際の標準的な手順とフォーマットを提供します。要件や仕様を漏れなく考慮し、必要十分なテストケースを作成することが目的です。 ## 前提条件 - テストケースは YAML フォーマットで記述する - `reference/` ディレクトリ配下に要件定義・テスト環境情報のCSVファイルが存在する (...省略...) ## 既存テストケースの参照 必要に応じて既存のテストケースを参照してフォーマットを確認 : ```bash # 既存のテストケースを確認 ls cases/*.yaml ``` 参考になるファイル : - `search.yaml`: 基本的なフォーマット (...省略...) Skill化してからは、テストケースの生成がワンショットで可能になりました。「XXX-3 のテストケースを作成してください」と伝えるだけで、Claude Code が必要なファイルを読み込み、マトリックスを参照し、YAMLフォーマットに従ったテストケースを出力してくれます。人間がやることは、出来上がったケースの微調整だけです。完璧とまでは言えないものの、ほとんどそのまま使えるレベルのドラフトが即座に作成できました。これは正直、期待以上の結果でした。 さらに、このSkillは自分以外のメンバーにも共有できます。テストケース作成のノウハウがファイルとして形式化されているので属人化の解消にもつながりました。実際、私がお休みの日に他のメンバーがテストケースを作成することもありました。 うまくいったこと AI-friendly な環境がもたらした相乗効果 テストケースのYAML化はもともとAI活用を見据えた意図もありましたが、実際にやってみると想像以上にうまく噛み合いました。常に構造化された状態でハンドルされるので安定したテストケースが得られました。スプレッドシートのままだったら、データの抽出と整形という余計な前処理が必要でトークン効率も速度も落ちてイテレーションが遅くなっていたでしょう。さらにテキストベースでGit管理しているおかげで、AIが生成したテストケースもそのままPRとして差分レビューできました。 結果として構造化は人間にもAIにもうれしい施策でした。 テスト環境やパターンコンテキストの活用 先述の通り、AIがテストパターンのマトリックス情報をほぼ破綻なく活用してケースの期待値を生成していたことには驚きました。もちろん100%完璧ではなく微調整は必要でしたが、ゼロから手で書くのと比べれば、工数は大幅に削減できました。 微調整のほとんどが要件や既存の機能に関するコンテキスト不足によるものでした。コンテキストが不足すると推測に推測を重ね、存在しない機能を持ち出したりと頓珍漢なケースを生成してしまうこともありました。勝手に推測させないようなプロンプトの追加なども試しましたが、やはり適切にコンテキストを補完するのが一番効果的でした。 Skill化による再現性 一度作成した Skill は何度でも再利用できます。新しい画面や機能のテストケースが必要になっても、要件を伝えるだけで同程度の品質のケースが生成されます。テストケース作成のナレッジが Skill として形式化されたことで、特定の人に依存しない仕組みが作れたのは大きな成果でした。 残ったボトルネックと次にやりたいこと 今回の取り組みでうまくいった部分は多いですが、まだまだ大きな改善の余地があると思っています。特に (A.) は段階的な準備で1週間はかかり、(B.) はエンジニアのアテンションをかなり持っていくものであるため、導入効果は大きいはずです。 A. コンテキストすら準備してもらう テストケースの量産はAIに任せられましたが、その前段にあるテストパターンマトリックス作成などのメタ的な作業は人手で行いました。ここはテストの核心であり、現時点では人間の判断が必要となっている領域です。 とはいえ、PRDや設計書が十分に構造化されていれば、テスト項目のドラフト作成もAIに任せられる余地は十分にあると信じています。今後試してみたい領域です。 B. 人間はスケールしない 現状AIが生成したテストケースにはレビューが必要です。ここで早くもコーディングでのAI利用と同じく、どれだけAIで作業を圧縮しようが人間のレビューがボトルネックとなってしまいました 3 。 ここでのテストケースのレビューとは「1. 要求・テスト観点に紐づいているか」と「2. 手順が検証として妥当かどうか」の2つに大別できると考えており、それぞれで解消を図っていきたいです: 「 1. 要求・テスト観点に紐づいているか 」: 生成されたテストケースを要求・テスト観点と突き合わせるレビュー専用 Agent の導入 「 2. 手順が検証として妥当かどうか 」: テストケースの字面を追う「机上のレビュー」から、Playwright などでテストの実行自体を自動化することで「テスト実行結果を見て判断するレビュー」へ転換 C. テストファーストへの組み込み 今回のシステムテストは開発の終盤に着手しました。これは一般的なプロジェクトでもよくある話ですが、不具合発覚時の手戻りリスクが大きいという問題があります。 もし自動テストが十分に高品質であれば、要件定義の段階でシステムテストケースを生成し開発と並行してテストを通していく、いわばシステムテストレベルでのテストファーストが実現できるかもしれません。 おわりに 今回の取り組みを一言でまとめるなら、「達成したいことに対してちゃんと効率化できるようにAIを自走させることの重要性」に尽きます。 テストケースの構造化、要件定義やマトリックスの整備、お手本となる初期ケースの丁寧な作成などの仕込みがあったからこそ、本来最も時間がかかるテストケース作成をAIで圧縮できました。 冒頭にも書いた通り、具体的なツールや手法は日々進化しています。その時代に取りうる選択肢とやりたいことに合わせて効率化を考えていきたいですね。みなさんの現場でも参考になれば幸いです。 Claude Codeにおいて、特定のタスクにおける手順・コンテキスト・参照先をまとめたプロンプトファイル。Claude Codeが会話に応じて自発的に呼び出すことができるほか、スラッシュコマンドとして呼び出せる。 ↩ 2026年4月現在であれば skill-creator の利用が安定した選択肢になります。 ↩ まるで アムダールの法則 ↩
こんにちは、SRE Team の大野です。製造業データ活用クラウド CADDi Drawer の Product SRE として、サービスの信頼性の向上や、効率的な運用を目指す取り組みを行っています。 今回は Cloud Run ワークロードの OOM 対応に Datadog Continuous Profiler を導入した際の、ハマりどころや学びについてのお話をします。 TL;DR はじめに Cloud Run での Profiler 選定 導入時のハードル 公式 Wrapper が使えない Profiler 単体で動かすための環境変数 プロファイリング結果の分析 プロダクト理解から見えた掛け算の構造 複数の観点で判断する 改善提案のまとめ方 Claude Code を活用した横断的調査 おわりに TL;DR Cloud Run ワークロードの OOM 調査にて Datadog Continuous Profiler を利用、Cloud Run 特有の導入ハードルや環境変数の罠があった Compare 機能でスパイク時のメモリ増分を特定し、複数要因の掛け算で消費が増大する構造を解明 問題指摘で終わらず、レイヤーごとの改善提案を開発チームに共有して意思決定を支援 はじめに CADDi Drawer は、図面や仕様・不具合情報といった複数種別のデータから、関連性や類似性をもとに検索・集約する機能を持っています。 先日、その処理を担う Cloud Run ワークロードが頻繁に OOM を起こすようになり、ユーザー体験に影響が出る程度のパフォーマンス劣化を引き起こしていました。ユーザー影響を抑えるためにひとまずメモリ増強によって延命したものの、根本原因は掴めておらず、再発リスクの見通しも立っていない状況でした。 原因を突き止めるためにメモリプロファイリングの手法を検討した結果、 Datadog Continuous Profiler を試験的に導入してメモリ消費構造を可視化することにしました。Profiler の結果と他の観点での分析を組み合わせて問題構造を紐解き、具体的な改善提案にまとめることができました。現在は改善が実施され、問題は解消しています。 Cloud Run での Profiler 選定 Cloud Run でメモリプロファイリングをやろうとすると、案外「これだ」と言える整った手法がないことに気づきます。取りうる手段をミニマムに整理しました。 🤔 Google Cloud Profiler: Google Cloud ネイティブだが、そもそも Cloud Run がサポート対象外 🤔 自前での heap dump: Node.js の場合はヒープスナップショット取得中にリクエスト処理が停止するため、本番環境での実施はリスクが高い 💡 Datadog Continuous Profiler: 全社的に Datadog を利用しており親和性が高い、2026 年 3 月時点ではプレビュー扱いで追加費用なく試せた、APM なしで Profiler 単体で動作する 消去法的ではありますが、スピード重視で Datadog Continuous Profiler の導入を決めました。 導入時のハードル いざ決まったものの、Cloud Run への導入は一筋縄ではいきませんでした。 公式 Wrapper が使えない Datadog は Cloud Run 向けに Terraform Wrapper モジュール を提供しており、これを使えば簡単に導入できるはずでした。しかし 2 つの壁にぶつかります。 1 つ目は Cloud Run API バージョンの問題です。 Wrapper モジュールは Cloud Run API v2 を前提としていますが、対象ワークロードは API v1 で管理されていました。 Google Cloud の移行手引き を参考に v1 → v2 への Terraform import で解消しましたが、API バージョン間でパラメータ記法の変更(annotation からの移行など)があり、単純な import だけでは済みません。意図しない差分からリソースの再作成を引き起こすとサービス断に直結するため、plan 結果を慎重に確認しながら進める必要がありました。 2 つ目は CI/CD フローとの衝突です。 このワークロードでは、Terraform の管理外で GitHub Actions がイメージタグを書き換えることでデプロイしていました。このため Terraform 側では lifecycle { ignore_changes } で image の変更を無視する必要があります。しかし利用時点の Wrapper モジュールではその設定ができず、apply のたびに意図しないイメージの巻き戻しが発生してしまいます。 この時点で Wrapper モジュールの利用を断念し、 serverless-init をサイドカーコンテナとして直接組み込む方式に切り替えました。この構成では、アプリケーションコンテナ側で dd-trace ライブラリがプロファイルデータを収集し、サイドカーの serverless-init が Datadog Agent として Datadog へ中継する形になります。 Profiler 単体で動かすための環境変数 弊社ではトレーシングに OpenTelemetry 経由で Google Cloud Trace を利用しているため、Datadog APM や Tracing は使わずに Profiler だけを有効化したいという状況でした。 しかし dd-trace にはトレーシング関連の環境変数が DD_TRACE_ENABLED , DD_TRACING_ENABLED , DD_APM_TRACING_ENABLED と似た名前で複数存在します。たとえば DD_TRACE_ENABLED=false は dd-trace ライブラリ全体を停止するため Profiler も止まってしまいます。 今回は組み合わせを検証する時間が限られていたため、なるべく Trace を送信せず APM も止める方向で安全側に倒すよう、下記で設定していました。 # アプリケーションコンテナ(dd-trace を init() する側) DD_PROFILING_ENABLED=true # Continuous Profiler を有効化 DD_APM_TRACING_ENABLED=false # Agent → Datadog APM への転送を無効化 DD_TRACE_SAMPLE_RATE=0 # トレースのサンプリングを 0% に # serverless-init サイドカー(Datadog Agent 側) DD_PROFILING_ENABLED=true # Profiler データの受け付けを有効化 DD_APM_TRACING_ENABLED=false # Agent → Datadog APM への転送を無効化 # DD_ENV, DD_SERVICE 等のユニバーサルサービスタグは両コンテナに必要(後述の公式ドキュメント参照) 詳細はライブラリやイメージのバージョンによって挙動が変わりうるため、 公式ドキュメント(共通設定) および Node.js 固有の設定 を参照のうえ、実環境での検証を組み合わせて確認することをおすすめします。 プロファイリング結果の分析 Profiler が動き始めると、 公式ドキュメント にもある通り、ある時点でのメモリ内訳確認や Insight によるボトルネック候補の自動提示など、いくつかの分析手法が使えるようになります。 今回特に効いたのが Compare 機能です。平常時とメモリスパイク発生時の Heap Live Size を並べることで、単一時点では区別がつきにくいスパイクの原因が明確になります。 冒頭で触れた通り、対象のワークロードは複数種別のデータから関連性や類似性をもとに検索・集約する処理を担っており、そのためのメモリ消費が中心になっています。 実際に比較すると、スパイク時にしか出現しない関数(Compare では「Only in B」と表示)が見つかりました。図ではマスクしていますが、特定条件下での検索クエリ構築やレスポンス保持の関数群にヒープ増分が集中していることが、画面から読み取れるようになっています。 Datadog Profiler 実画面イメージ この結果から、「常にメモリを多く使っている処理」ではなく「特定条件下でメモリを大量消費する処理」が原因であるという仮説が立ちました。 プロダクト理解から見えた掛け算の構造 Profiler の結果はあくまで「何がメモリを消費しているか」を示すだけで、「なぜそうなるのか」「どう対処すべきか」は別の分析が必要です。該当する機能がどのようなものでどう使われているのかを、仕様書や機能説明資料、直近の開発プロジェクトの動向から読み解きました。 CADDi Drawer は、顧客ごとに異なる課題を幅広く解決するデータプラットフォーム上でデータの探索・分析を担うプロダクトです。大半の利用シーンではメモリ消費も穏やかに収まりますが、特定の利用パターンでは画面表示のための検索エンジンへのクエリが、非常に複雑になることがあります。 まさに今回はこの事象を踏んでおり、リクエストあたりの処理量が複数の要因の掛け算で増大するケースに該当していました。個々の要因は単体では大きな問題にならないものの、これらが重なった際にメモリ消費が一気に跳ね上がります。 Profiler の Compare で特定の処理にヒープ増分が集中していたのも、この掛け算構造がスパイク時に顕在化した結果です。 複数の観点で判断する 掛け算構造が見えてくると、Profiler の結果だけで判断するのは危ういことに気づきます。チームメンバーからのフィードバックも踏まえ、以下の観点を加えました。 リクエストパターン: 同時かつ並列でリクエストが集中していないか 単発では問題にならないメモリ消費も、並列度が上がると一気にスパイクに繋がる リードタイム: 処理がどれくらい長引くか 長時間メモリを保持し続けることで、GC が追いつかずメモリ消費が積み上がるケースがある 発生頻度: 頻繁に起こるのか、稀なのか 稀であっても、データ量の増加やリクエストパターンの変化で「いつ爆発してもおかしくない」構造なのかを見極める必要がある 改善提案のまとめ方 問題構造が見えたら、開発チームが動きやすい形で提案をまとめる必要があります。今回は以下のように整理しました。 レイヤーごとに対策を分ける フロントエンド・バックエンド・API スキーマ・インフラなど 掛け算構造のすべての要因に単一の対策では対処できないため 今回の例では、API スキーマへの入力制約の追加や、検索エンジンへのクエリ構造の改善といった対策を提案 実現しやすさ順に並べる まず実態把握、次にすぐ効果が出る対策 構造的な改善は並行して検討 プロダクト判断が必要な境界を明示する 仕様の見直しを伴う対策は SRE だけでは判断できない どの対策が技術的に閉じるもので、どの対策が PdM や開発チームとの議論を要するのかを切り分けることで、関係者ごとに次のアクションが見えやすくなる 「ここが問題です」で終わるのと「こういう構造なので、この順で対処するのがよさそうです」まで持っていくのとでは、開発チームへの引き継ぎのスムーズさが変わります。 Claude Code を活用した横断的調査 今回の調査では Claude Code も活用しました。大規模かつ変化が激しいシステムでは、コードベース全体を頭に入れるのは現実的ではありません。複数リポジトリにまたがるコードフロー(ロジックの流れ、リクエストの経路、コンポーネント間の繋がり、インフラ設定)を横断的に追う場面で非常に重宝しました。 既に一般的になりつつありますが、工夫していた点をいくつか挙げます。 MCP 経由で調査対象を広げる GitHub のソースコードだけでなく、Datadog のログやメトリクスにも MCP Server 経由でアクセス コードの静的な構造とランタイムの挙動を突き合わせる調査を、コンテキストを切り替えずに一連の流れで実行できた subagent でコンテキストウィンドウを節約 フロントエンド・バックエンド・検索サービスのリポジトリ群を横断する必要があり、各コンポーネントの詳細調査を subagent に委譲 メインのコンテキストウィンドウには要約だけが残るので、Compacting Conversation の頻度を減らすことが出来た ドキュメントの下書きに使う 問題を構造的に説明しようとすると、ファクトを並べるだけでも文量が膨らむ 下書きを作らせたうえで、自分の理解と照らし合わせながら論点を絞り込んだ おわりに Cloud Run ワークロードに Continuous Profiler を導入し、OOM の根本原因を特定して改善提案に繋げた一連の流れを紹介しました。 振り返ると、重要だったのは以下の 3 点です。 ツールを入れて終わりにしない Profiler の出力はあくまで出発点であり、そこからプロダクトの仕様・使われ方・リクエストパターンを重ね合わせて、問題の構造を明らかにする 総合的に判断する プロファイリング結果だけでなく、頻度・リードタイム・並列度といった実利用での観点を加えて、対処の優先度を見極める 改善提案まで持っていく 問題を指摘するだけでなく、具体的な対策案と優先度の根拠を示して、開発チーム全体の意思決定を支援する 「ボトルネックを見つけて改善する」という行為は一見シンプルですが、そこに至るまでの選定・導入・分析・提案のそれぞれに SRE としての判断と工夫が求められます。同様の課題に取り組む方にとって、本記事が何かしらの参考になれば嬉しいです。 キャディの Product SRE は、インフラの運用に閉じることなく、プロダクトの仕様理解から改善提案まで踏み込める環境にあります。今回のようにプロファイリングで得た技術的なファクトを起点に、開発チームや PdM と協働しながらプロダクト全体の信頼性を高めていく働き方に興味がある方は、ぜひ キャディの Tech 採用ページ をご覧ください! また、キャディは 5 月 14 日(木)から 5 月 15 日(金)の 2 日間、名古屋で開催されるクラウドネイティブ会議にブーススポンサーとして協賛します! 当日は SRE や Platform Engineering に関わるメンバーを中心に 7 名が現地の会場を訪れる予定です。 参加される方は、ぜひ会場のブースにも遊びにお越しいただけると嬉しいです。 caddi.tech