TECH PLAY

CDN

イベント

該当するコンテンツが見つかりませんでした

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

はじめに こんにちは、NTT西日本の中川です。 本記事では、IndexedDB(保存)と Transformers.js(計算)を組み合わせ、ブラウザ内でメモの意味検索までのLocal-First AI入門を、ミニサンプルを用いて紹介します。 本記事は2026年6月時点の情報に基づきます。 生成AIが身近になった一方で、「APIにすべて任せればよいのでは?」と感じる方も多いと思います。 私も最初はそう思っていました。ただ、議事メモをクラウドに貼るたびに「この本文、送ってよいのかな」と迷う場面があり、そのたびに判断するのは負担に感じたので、ブラウザ内で完結する検索を試してみたところ、それなりに使い所があるように感じたため、サンプルを交えてご紹介します。 対象読者 LocalStorageやIndexedDBの基本的な使い方は理解している方 AIをアプリケーションに組み込みたいが、サーバー費用やプライバシー面に課題を感じている方 Local-First AI の有用性が抽象的で、自社プロダクトのどこで効くかイメージしづらい方 「AIにコードを書かせる」段階から、「AIをブラウザ環境へ最適化する」段階へステップアップしたい方 目次 はじめに 対象読者 目次 1. 背景・目的:なぜ今「Local-First AI」なのか? 1.1 Local-First AI とは何か 1.2 イメージしやすい Before / After(社内メモ検索の例) Before(クラウドAPI中心) After(Local-First 寄り) 1.3 本記事の知識が活きるユースケース 1.4 現実的な構成:ローカルとクラウドの役割分担 1.5 ベクトル検索を用いたアプリケーションが直面しやすい3つの課題 1.6 Local-First AIが向かないケース 2. 技術的アプローチ:IndexedDB(長期記憶)× Transformers.js(推論)× Worker(滑らかさ) 2.1 ブラウザを「AIの脳」にする:Transformers.jsと推論バックエンド(WASM/WebGPU) 2.2 IndexedDBを「AIの長期記憶」として使う 3. 設計上の前提 3.1 この記事で「守ること/守れないこと」 3.2 埋め込み化が遅い・動かないときの対処 4. 【実装】ブラウザだけで動く「ローカルAIメモ検索」サンプル 4.1 何を作るか(完成イメージ) 4.2 ファイル構成 4.3 試し方 4.3.1 デモデータと検索クエリ例 4.3.2 検索結果のスコアとコサイン類似度 判定の流れ(検索1回あたり) コサイン類似度(意味の近さ)とは スコアの見方 4.4 index.html(UI・Worker起動) 4.5 embedding.worker.js(CDN + IndexedDB + 検索) 4.6 動かない時のチェックポイント 4.7 つまずきポイント 5. まとめ:AIにコードを任せ、設計を握ることが大事 執筆者 参考資料・出典 商標 1. 背景・目的:なぜ今「Local-First AI」なのか? 1.1 Local-First AI とは何か Local-First AI は、「すべてのAIをブラウザだけで完結させる」ことと同義ではありません。クラウドのサーバーなどに頼るのではなく、自身のパソコンやスマートフォンなど手元の端末(ローカル環境)上でAIの処理や学習を完結させる考え方の事を指します。本記事で扱うのは、こんな設計です。 データ(メモ・ログ・下書き)はブラウザ内(IndexedDB)に置く 取得(埋め込み・類似検索・分類など)は端末側で回す 文章生成(長文の要約・高度な回答)は必要なときだけクラウドAPIへ寄せる つまりハイブリッドな構成です。「通常のLLMの圧倒的な賢さ」よりも、送りたくないデータを送らない/待ちを減らす/API代を抑える、といった要件にマッチする考え方になります。 1.2 イメージしやすい Before / After(社内メモ検索の例) 社内Webツールを想定した、体験の違いです。 Before(クラウドAPI中心) 議事メモ100件の中から検索したい 入力のたびに自前のバックエンドAPI経由で、Embeddingモデル(テキストや画像などの非構造化データを数値の配列(ベクトル)に変換する技術のこと)へ本文を送信し、埋め込み化 懸念: 情報持ち出しポリシー、ログ保存、API課金、回線が弱いと待ちが発生してしまう After(Local-First 寄り) メモ本文は IndexedDB に保存(ブラウザ内) 「先週の障害対応」などのクエリは端末内で埋め込み化 → 類似メモを上位表示 本文を外部へ送らない設計にしやすい(初回のモデル取得通信は別途発生) モデル取得後の検索は、埋め込み計算を端末側で実行し、サーバーへ本文を送らずに応答できる この記事のサンプルは After のうち「意味が近いメモを探す」部分だけです。要約やチャット生成までは扱わないので留意してください。 1.3 本記事の知識が活きるユースケース 次のようなユースケースでは本記事の考え方が活躍できるかなと思います。 ご自身のプロダクトと照らし合わせて検討してみてください。 シーン 困りごと Local-First でやりやすいこと 社内・個人のメモ・議事録の検索 クラウドLLMに貼ると情報管理が不安 本文を送らず意味検索(本記事のデモ) 回線が不安定な現場・店舗・イベント 回線が不安定でAPI待ちが致命的 モデル取得後はオフライン寄りで検索・分類 入力中の補助UI サーバー往復で入力の反応が鈍くなる Worker + ローカル推論で体感速度を改善 企画・検証フェーズ サーバー構築前に体験を試したい ローカルファイルで仮説検証 1.4 現実的な構成:ローカルとクラウドの役割分担 全部ローカルに寄せる必要はありません。役割を分けると設計しやすいです。 ブラウザ: 保存・検索・分類など「自分のデータにだけ効く処理」 クラウド: 品質最優先の生成、全社ナレッジ横断など「データを送ってもよい処理」 1.5 ベクトル検索を用いたアプリケーションが直面しやすい3つの課題 こうしたユースケースが効く背景として、次の3つがよく出てきます。 APIコスト: すべてのリクエストをサーバーへ送ると、ユーザー増・利用回数に伴いコストが膨らみやすい。 プライバシー: 議事メモ・顧客対応メモなど、外部サーバーへ送りたくないデータがある。 応答性(レイテンシー): ネットワーク経由の推論は、通常、待機時間が発生する。 IndexedDB(保存)と Transformers.js などを使った端末内推論(計算)を組み合わせると、これらを同時に緩和しやすい場面があります(端末性能・ブラウザ対応などの制約はあります)。 1.6 Local-First AIが向かないケース 万能ではないので、向かないケースも書いておきます。 高品質な長文生成が主目的 全端末で同じ品質を厳密に保証したい 社内文書を数百万件単位で横断検索したい(専用ベクトルDBが必要) モデル配布・更新を厳格に統制したい環境 こうした要件が主なら、サーバー側のRAG(検索拡張生成)やクラウドAPIを主役にし、ローカルは補助とする構成が現実的です。 今回のデモがやっているのは、これだけです。 自分のメモだけを、キーワード一致ではなく「意味が近い順」で探せる。メモ本文を自前APIへ送らずに済む。 RAG(検索拡張生成)でいうと、本デモは R(Retrieval=検索)だけです。LLM への渡し方や回答生成(G)は扱いません。 2. 技術的アプローチ:IndexedDB(長期記憶)× Transformers.js(推論)× Worker(滑らかさ) 先ほどの図で示した「ブラウザ内」を、具体的な技術要素に分解します。 2.1 ブラウザを「AIの脳」にする:Transformers.jsと推論バックエンド(WASM/WebGPU) 現代のブラウザはWebAssemblyやWebGPU APIを通じて、高速な演算を活用できる環境が増えてきました。ここにJavaScript向け推論ライブラリ(例:Transformers.js)を組み合わせると、Python環境や自前サーバーに依存せず、ブラウザ上で推論を実行できる構成を取りやすくなります。 サンプルは、Transformers.js を CDN(jsDelivr)から読み込むだけで動かします。ビルドツールは使いません。 Transformers.js は環境に応じて WebAssembly(WASM)や WebGPU などへフォールバックします。本記事のサンプルコードは、デフォルトの WASM バックエンドで推論します。 2.2 IndexedDBを「AIの長期記憶」として使う RAG(検索拡張生成)は「必要な知識を検索してから生成に渡す」考え方です。ここでは、ブラウザ内に保存したメモ群を埋め込み(ベクトル)化してIndexedDBに保存し、クエリに近いものを取り出す、というできるだけ軽めな構成にしています。 また、ブラウザでAI処理を動かす際のボトルネックになりやすいのが、メインスレッドの占有です。UI描画・入力・スクロールが詰まると、ユーザー体験に悪影響が出ます。メインスレッドと Worker で役割を分けます。 メインスレッド: UIの描画、ユーザー入力、結果表示 Web Worker: モデルロード、埋め込み計算、類似度計算、IndexedDBへの書き込み 3. 設計上の前提 3.1 この記事で「守ること/守れないこと」 「ローカルに保存する=安全」ではありません。ここは誤解が起きやすいので、前提を設けます。 守りやすいこと(設計でコントロールしやすい) ユーザーが入力したメモ本文を、自前APIへ送らない設計にできる APIコストを抑えやすい(端末内推論に寄せるほど) 守れないこと(別対策が必要) 端末のマルウェア感染、悪性ブラウザ拡張、端末盗難などはローカル保存だけでは防げない XSS(クロスサイトスクリプティング)で同一オリジン上のデータを読まれるリスクは致命的(CSP=Content Security Policy・サニタイズなどが前提) 初回だけ外部通信が発生する Transformers.js はモデル重みを CDN / Hugging Face から取得します(メモ本文そのものを送るわけではないが、通信自体は発生します) 社内プロキシやファイアウォールで外部 CDN が制限されている環境では、初回のモデル取得が失敗することがあります(別途ネットワーク設定の確認が必要) 3.2 埋め込み化が遅い・動かないときの対処 端末やブラウザによって、埋め込みの速さはかなり変わります。本記事のサンプルは、Transformers.js の既定どおり WASM(CPU上で動く方式)で動く前提です。WebGPU が使える環境では速くなることもありますが、必須ではありません。 困ったときは、この順で試すとよいです。 まず動かす — WASM で登録・検索まで通す(今回のデモはここまで) 余力があれば速くする — 端末が対応していれば WebGPU を使う(Transformers.js が自動で選ぶ) それでも厳しい — 意味検索をやめるのではなく、負荷を下げる(キーワード検索に切り替える、登録件数や topK を減らす など) 4. 【実装】ブラウザだけで動く「ローカルAIメモ検索」サンプル 4.1 何を作るか(完成イメージ) 1章の After を、HTML とJavaScriptの2ファイルで動かす段階です。メモ登録 → 埋め込み保存 → クエリで意味検索、という流れだけに絞っています。チャット回答や社外API連携は範囲外です。 4.2 ファイル構成 次の2ファイルを同じフォルダに置きます。 index.html … UI(メインスレッド) embedding.worker.js … 埋め込み化・IndexedDB・検索(Worker) 4.3 試し方 この章の index.html と embedding.worker.js のコードを、それぞれ同名ファイルとして同じフォルダに保存する Visual Studio Code の Live Server などで、そのフォルダを http://localhost:... 経由で開く( file:// で直接開くと Worker が動かないことが多いです) Google Chrome または Microsoft Edge で index.html を表示する 「デモデータを一括登録」でサンプルを入れる(初回はモデル取得で数十秒かかることがあります)。「登録」から1件ずつ足して試してもかまいません 下の検証表と結果を比べたいときだけ、件数が6件でなければ「すべて削除」→「デモデータを一括登録」で揃える 検証表のクエリで検索してみる(メモが6件未満でも検索は動きます。表示件数は、登録件数と5件の少ない方まで) 動作確認の目安: 検証表を使う場合、各クエリで「期待される上位デモデータ」が1位、環境によっては2位以内なら意図どおりです。利用モデルは Xenova/all-MiniLM-L6-v2 で、英語向けが主です。日本語のデモでも動きますが、順位は端末やクエリで前後することがあります。結果の数値(例: 0.165 )の読み方は、続く「検索結果のスコアとコサイン類似度」を見てください。Transformers.js は @xenova/transformers@2.17.2 (CDN)で固定しています。 4.3.1 デモデータと検索クエリ例 「デモデータを一括登録」すると6件入ります。6件しか扱えないわけではなく、下の検証表がこの6件を想定しているだけです。 一括登録されるデモデータ [障害対応] 2026-03-15 認証基盤タイムアウトの件。原因はDBのコネクションプール枯渇。再発防止策としてリトライ上限を3回に変更し、アラート閾値を調整した。 [手順] 社外からのVPN接続について。社外ネットワークからはポータル経由でのみアクセス可能。パスワードはITサポートにチケットを発行して取得すること。 [顧客A社] 契約上の特記事項:ログに個人情報(氏名・メアド)を一切残さないこと。問い合わせ対応は専任のサポート窓口を経由すること。 [議事録] 4/1 開発定例。次期リリースの目玉はRAG機能の強化。インフラ費用を抑えるため、一部の検索処理をフロントエンド(ブラウザ側)にオフロードする方針で合意。 [技術メモ] 埋め込み推論は Transformers.js(WASM)が既定。端末が対応していれば WebGPU バックエンドで GPU 推論に切り替え可能。 [オンボーディング] 新入社員向けPCセットアップ手順。初期パスワードは入社初日に配布される資料を参照。まずはセキュリティ研修動画の視聴を完了させること。 検索クエリと期待される上位デモデータ 検索クエリ 期待される上位デモデータ 先週の認証エラーの再発防止 メモ1 社外から社内システムに入る メモ2 個人情報の取り扱いルール メモ3 コスト削減の設計 メモ4 GPU 推論 メモ5 新人の初期設定 メモ6 登壇 該当メモなし(語句一致なし・相対順位のみ) 4.3.2 検索結果のスコアとコサイン類似度 検索結果は、だいたい次の形で出ます。 0.165 (意味 0.165) [語句一致 +0.12] - [id=5] [技術メモ] 埋め込み推論は Transformers.js(WASM)が既定。… 0.411 (意味 0.411) [語句一致なし] - [id=14] [手順] 社外からのVPN接続について。… 先頭の 0.165 は最終スコアで、括弧内の 意味 0.165 はコサイン類似度(意味の近さ)だけを抜き出した値です。 [語句一致 +0.12] はクエリ文字列が本文に含まれるときの加点、 [語句一致なし] は加点が付いていないことを示します。サンプルでは searchScore でスコアを作り、降順に並べています。 判定の流れ(検索1回あたり) 「検索」ボタンを押してから結果が並ぶまでの流れです(Web Worker 内で完結し、メモ本文は外部 API へ送りません)。 topK(トップケイ)は、スコア順の上位K件だけ返す、という意味です。K は返す件数で、変数名として topK と書くことが多いです。本サンプルでは worker.postMessage({ type: 'search', payload: { query, topK: 5 } }) のとおり 5件です。 メモ1件あたりのスコア計算( searchScore )は、意味の近さ(ベクトル)と語句一致(文字列)を組み合わせています。 登録時は逆方向です。メモ本文を埋め込み化して text と vector を IndexedDB に保存します(検索のたびに再計算しません)。 コサイン類似度(意味の近さ)とは Transformers.js のような埋め込みモデルは、メモ本文も検索語もベクトル(数値の並び)に変換します。似た意味の文は、似たベクトルになります。意味で探すときは、ベクトル同士の近さで並べるのが一般的です。 その近さを測る指標がコサイン類似度です。画面の (意味 0.xxx) がこれで、おおむね 0〜1 の範囲です(1に近いほど意味が近い)。検索の方法自体は他にもあり、語が本文に含まれるかだけを見るキーワード検索(Google検索に近い)もよく使われます。本デモは意味検索を主にし、語句一致は [語句一致] の小幅な加点として足しています。そのため「障害」「タイムアウト」といった語が本文に無くても、内容が近ければ上位に来ます。 最終スコアは、コサイン類似度に、検索語が本文にそのまま含まれるときの小幅な加点( [語句一致] )を足したものです。 スコアの見方 スコアを読むときは、次の3つを見れば十分です。 順位:0.05 でも 0.4 でも、数値より並びが自然かを見る [語句一致] :付いていれば、検索語が本文に含まれている。無ければコサイン類似度だけで並んでいる 件数:最大5件まで出るが、登録が3件なら3件まで。近い順に並んでいるだけのことがある 検証表どおり6件入れた状態で 登壇 を検索すると、 0.411 などの数値が出て「見つかったのかな?」と迷いました。どのメモにもその語はなく、すべて [語句一致なし] でした。数字が付いていても、登録されているメモのうち相対的に近かっただけ、という意味です。画面下の注意文と (意味 0.xxx) もあわせて見てください。 4.4 index.html (UI・Worker起動) メインスレッドは UI と Worker へのメッセージ送受信だけを担当します。 <!doctype html> < html lang = "ja" > < head > < meta charset = "UTF-8" /> < meta name = "viewport" content = "width=device-width, initial-scale=1.0" /> < title > Local-First AI Memo Search </ title > < style > body { font-family : system-ui , sans-serif ; max-width : 720px ; margin : 2rem auto ; padding : 0 1rem ; line-height : 1.6 ; } textarea , input { width : 100% ; box-sizing : border-box ; } button { margin-top : 0.5rem ; } #status { min-height : 1.5rem ; color : #333 ; margin-top : 1rem ; } ol , ul { padding-left : 1.25rem ; } #memos li { margin-bottom : 0.5rem ; font-size : 0.95rem ; } #memos .meta { color : #666 ; font-size : 0.85rem ; } .note { font-size : 0.9rem ; color : #555 ; background : #f6f6f6 ; padding : 0.75rem 1rem ; border-radius : 6px ; } </ style > </ head > < body > < h1 > Local-First AI Memo Search </ h1 > < section > < h2 > メモ登録 </ h2 > < textarea id = "memo" rows = "4" placeholder = "メモを入力" ></ textarea > < button id = "add" type = "button" > 登録 </ button > < button id = "loadDemo" type = "button" style = "margin-left: 1rem;" > デモデータを一括登録 </ button > < button id = "clearAll" type = "button" style = "margin-left: 1rem;" > すべて削除 </ button > </ section > < section > < h2 id = "memosHeading" > 登録済みメモ(読み込み中…) </ h2 > < ul id = "memos" ></ ul > </ section > < section > < h2 > 検索 </ h2 > < input id = "q" type = "search" placeholder = "例: WebGPU 推論 / 社外から社内システム / 登壇(無関係クエリの例)" /> < button id = "go" type = "button" > 検索 </ button > < p class = "note" style = "margin-top: 0.5rem;" > 結果は < code > 最終スコア (意味スコア) [語句一致] </ code > の順です。語句一致が無いときは、 登録メモの中でベクトルが相対的に近い順に並んでいるだけで、必ずしも関連メモがあるわけではありません。 </ p > < ol id = "results" ></ ol > </ section > < div id = "status" aria-live = "polite" ></ div > < script type = "module" > const memoEl = document . getElementById ( 'memo' ) ; const addBtn = document . getElementById ( 'add' ) ; const loadDemoBtn = document . getElementById ( 'loadDemo' ) ; const clearAllBtn = document . getElementById ( 'clearAll' ) ; const memosHeading = document . getElementById ( 'memosHeading' ) ; const memosEl = document . getElementById ( 'memos' ) ; const qEl = document . getElementById ( 'q' ) ; const goBtn = document . getElementById ( 'go' ) ; const resultsEl = document . getElementById ( 'results' ) ; const statusEl = document . getElementById ( 'status' ) ; const worker = new Worker ( './embedding.worker.js' , { type : 'module' }) ; function refreshMemos () { worker . postMessage ({ type : 'listMemos' }) ; } function renderMemos ( memos ) { memosHeading . textContent = `登録済みメモ( ${ memos . length } 件)` ; memosEl . innerHTML = '' ; if ( memos . length === 0 ) { const li = document . createElement ( 'li' ) ; li . textContent = 'まだメモは登録されていません。「デモデータを一括登録」で試せます。' ; memosEl . appendChild ( li ) ; return; } for ( const m of memos ) { const li = document . createElement ( 'li' ) ; const meta = document . createElement ( 'div' ) ; meta . className = 'meta' ; meta . textContent = `id= ${ m . id } ` ; li . appendChild ( meta ) ; li . appendChild ( document . createTextNode ( m . text )) ; memosEl . appendChild ( li ) ; } } worker . onmessage = ( ev ) => { const { type , payload } = ev . data ; if ( type === 'listMemosResult' ) { renderMemos ( payload . memos ) ; return; } if ( type === 'addMemoResult' ) { statusEl . textContent = `登録しました(id= ${ payload . id } )` ; refreshMemos () ; return; } if ( type === 'loadDemoResult' ) { statusEl . textContent = `デモデータ( ${ payload . count } 件)を一括登録しました(既存データは置き換え済み)。` ; refreshMemos () ; return; } if ( type === 'clearAllResult' ) { statusEl . textContent = 'すべてのメモを削除しました。' ; refreshMemos () ; return; } if ( type === 'searchResult' ) { resultsEl . innerHTML = '' ; if ( payload . items . length === 0 ) { const li = document . createElement ( 'li' ) ; li . textContent = '該当するメモがありません' ; resultsEl . appendChild ( li ) ; statusEl . textContent = '' ; } else { const anyKeyword = payload . items . some (( it ) => it . keywordMatch ) ; for ( const it of payload . items ) { const li = document . createElement ( 'li' ) ; const kwTag = it . keywordMatch ? ` [語句一致 + ${( it . keywordBoost ?? 0 . 12 ) . toFixed ( 2 )} ]` : ' [語句一致なし]' ; li . textContent = ` ${ it . score . toFixed ( 3 )} (意味 ${ it . semantic . toFixed ( 3 )} ) ${ kwTag } - [id= ${ it . id } ] ${ it . text } ` ; resultsEl . appendChild ( li ) ; } if ( ! anyKeyword ) { statusEl . textContent = 'いずれも語句一致なしです。表示は topK=5 件の「相対順位」であり、クエリに近いメモが無い場合でもスコア付きで並びます。本番ではスコア閾値やメタデータ絞り込みを検討してください。' ; } else { statusEl . textContent = '' ; } } return; } if ( type === 'error' ) { statusEl . textContent = `エラー: ${ payload . message } ` ; } } ; addBtn . addEventListener ( 'click' , () => { const text = memoEl . value . trim () ; if ( ! text ) return; statusEl . textContent = '登録中...(初回はモデル取得のため数十秒かかる場合があります)' ; worker . postMessage ({ type : 'addMemo' , payload : { text } }) ; memoEl . value = '' ; }) ; loadDemoBtn . addEventListener ( 'click' , () => { statusEl . textContent = 'デモデータを登録中...(初回はモデル取得のため数十秒かかる場合があります)' ; worker . postMessage ({ type : 'loadDemoData' }) ; }) ; clearAllBtn . addEventListener ( 'click' , () => { if ( ! confirm ( '登録済みメモをすべて削除します。よろしいですか?' )) return; worker . postMessage ({ type : 'clearAllMemos' }) ; }) ; goBtn . addEventListener ( 'click' , () => { const query = qEl . value . trim () ; if ( ! query ) return; resultsEl . innerHTML = '' ; statusEl . textContent = '検索中...' ; worker . postMessage ({ type : 'search' , payload : { query , topK : 5 } }) ; }) ; refreshMemos () ; </ script > </ body > </ html > 4.5 embedding.worker.js (CDN + IndexedDB + 検索) Worker 側で Transformers.js(CDN)を読み込み、IndexedDB(生API)へ保存・検索します。 import { pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2' ; const DB_NAME = 'AI_Memory' ; const DB_VERSION = 1 ; const STORE_NAME = 'memos' ; const MODEL_ID = 'Xenova/all-MiniLM-L6-v2' ; let embedder ; // --- IndexedDB(生API) --- function openDb () { return new Promise (( resolve , reject ) => { const req = indexedDB . open ( DB_NAME , DB_VERSION ) ; req . onupgradeneeded = ( event ) => { const db = event . target . result ; if ( ! db . objectStoreNames . contains ( STORE_NAME )) { const store = db . createObjectStore ( STORE_NAME , { keyPath : 'id' , autoIncrement : true , }) ; store . createIndex ( 'timestamp' , 'timestamp' ) ; } } ; req . onsuccess = () => resolve ( req . result ) ; req . onerror = () => reject ( req . error ) ; }) ; } function dbAdd ( record ) { return openDb () . then ( ( db ) => new Promise (( resolve , reject ) => { const tx = db . transaction ( STORE_NAME , 'readwrite' ) ; const req = tx . objectStore ( STORE_NAME ) . add ( record ) ; req . onsuccess = () => resolve ( req . result ) ; req . onerror = () => reject ( req . error ) ; tx . oncomplete = () => db . close () ; }) , ) ; } function dbGetAll () { return openDb () . then ( ( db ) => new Promise (( resolve , reject ) => { const tx = db . transaction ( STORE_NAME , 'readonly' ) ; const req = tx . objectStore ( STORE_NAME ) . getAll () ; req . onsuccess = () => resolve ( req . result ) ; req . onerror = () => reject ( req . error ) ; tx . oncomplete = () => db . close () ; }) , ) ; } function dbClearAll () { return openDb () . then ( ( db ) => new Promise (( resolve , reject ) => { const tx = db . transaction ( STORE_NAME , 'readwrite' ) ; const req = tx . objectStore ( STORE_NAME ) . clear () ; req . onsuccess = () => resolve () ; req . onerror = () => reject ( req . error ) ; tx . oncomplete = () => db . close () ; }) , ) ; } async function listMemosForUi () { const all = await dbGetAll () ; return all . map (( m ) => ({ id : m . id , text : m . text , timestamp : m . timestamp })) . sort (( a , b ) => b . timestamp - a . timestamp ) ; } // --- ベクトルユーティリティ --- function float32ToBuffer ( f32 ) { return f32 . buffer . slice ( f32 . byteOffset , f32 . byteOffset + f32 . byteLength ) ; } function bufferToFloat32 ( buf ) { return new Float32Array ( buf ) ; } function cosineSimilarity ( a , b ) { if ( a . length ! == b . length ) throw new Error ( 'ベクトル長が一致しません' ) ; let dot = 0 , na = 0 , nb = 0 ; for ( let i = 0 ; i < a . length ; i ++ ) { dot += a [ i ] * b [ i ] ; na += a [ i ] * a [ i ] ; nb += b [ i ] * b [ i ] ; } const denom = Math . sqrt ( na ) * Math . sqrt ( nb ) ; return denom === 0 ? 0 : dot / denom ; } /** 短いクエリ向け: 本文にクエリ語が含まれるときだけ小幅ブースト(ハイブリッド検索の簡易版) */ function searchScore ( query , qvec , memo ) { const semantic = cosineSimilarity ( qvec , bufferToFloat32 ( memo . vector )) ; const q = query . trim () . toLowerCase () ; if ( q . length < 2 ) { return { score : semantic , semantic , keywordMatch : false , keywordBoost : 0 } ; } const text = memo . text . toLowerCase () ; const keywordMatch = text . includes ( q ) ; const keywordBoost = keywordMatch ? 0 . 12 : 0 ; return { score : semantic + keywordBoost , semantic , keywordMatch , keywordBoost , } ; } // --- 埋め込み(Transformers.js / デフォルトは WASM バックエンド) --- async function getEmbedder () { if ( embedder ) return embedder ; embedder = await pipeline ( 'feature-extraction' , MODEL_ID ) ; return embedder ; } async function embedText ( text ) { const e = await getEmbedder () ; const out = await e ( text , { pooling : 'mean' , normalize : true }) ; const data = out ?. data ?? out ; if ( data instanceof Float32Array ) return data ; if ( Array . isArray ( data )) return new Float32Array ( data . flat ()) ; throw new Error ( '埋め込みベクトルの形式が想定外です' ) ; } // --- メッセージ処理 --- self .onmessage = async ( ev ) => { try { const { type , payload } = ev . data ; if ( type === 'listMemos' ) { const memos = await listMemosForUi () ; self . postMessage ({ type : 'listMemosResult' , payload : { memos } }) ; return; } if ( type === 'clearAllMemos' ) { await dbClearAll () ; self . postMessage ({ type : 'clearAllResult' , payload : {} }) ; return; } if ( type === 'loadDemoData' ) { await dbClearAll () ; const demoMemos = [ "[障害対応] 2026-03-15 認証基盤タイムアウトの件。原因はDBのコネクションプール枯渇。再発防止策としてリトライ上限を3回に変更し、アラート閾値を調整した。" , "[手順] 社外からのVPN接続について。社外ネットワークからはポータル経由でのみアクセス可能。パスワードはITサポートにチケットを発行して取得すること。" , "[顧客A社] 契約上の特記事項:ログに個人情報(氏名・メアド)を一切残さないこと。問い合わせ対応は専任のサポート窓口を経由すること。" , "[議事録] 4/1 開発定例。次期リリースの目玉はRAG機能の強化。インフラ費用を抑えるため、一部の検索処理をフロントエンド(ブラウザ側)にオフロードする方針で合意。" , "[技術メモ] 埋め込み推論は Transformers.js(WASM)が既定。端末が対応していれば WebGPU バックエンドで GPU 推論に切り替え可能。" , "[オンボーディング] 新入社員向けPCセットアップ手順。初期パスワードは入社初日に配布される資料を参照。まずはセキュリティ研修動画の視聴を完了させること。" ] ; for ( const text of demoMemos ) { const vec = await embedText ( text ) ; await dbAdd ({ text , vector : float32ToBuffer ( vec ) , timestamp : Date . now () , model : MODEL_ID , }) ; } self . postMessage ({ type : 'loadDemoResult' , payload : { count : demoMemos . length } }) ; return; } if ( type === 'addMemo' ) { const { text } = payload ; const vec = await embedText ( text ) ; const id = await dbAdd ({ text , vector : float32ToBuffer ( vec ) , timestamp : Date . now () , model : MODEL_ID , }) ; self . postMessage ({ type : 'addMemoResult' , payload : { id } }) ; return; } if ( type === 'search' ) { const { query , topK = 5 } = payload ; const qvec = await embedText ( query ) ; const all = await dbGetAll () ; const items = all . map (( m ) => { const { score , semantic , keywordMatch , keywordBoost } = searchScore ( query , qvec , m , ) ; return { id : m . id , text : m . text , score , semantic , keywordMatch , keywordBoost , } ; }) . sort (( a , b ) => b . score - a . score ) . slice ( 0 , topK ) ; self . postMessage ({ type : 'searchResult' , payload : { items } }) ; return; } self . postMessage ({ type : 'error' , payload : { message : `不明な操作です: ${ type } ` } }) ; } catch ( e ) { self . postMessage ({ type : 'error' , payload : { message : String ( e ?. message ?? e ) } }) ; } } ; 4.6 動かない時のチェックポイント 「検索結果がおかしい」「いつも同じ」ように見えるときは、よくあるのは次のどれかです。 Workerが動いていない( type: 'module' を忘れている、 file:// で開いている) out.data の形が想定と違う( embedText の分岐で吸収) ベクトルの正規化が効いていない( normalize: true を確認) 保存した vector の復元が壊れている( ArrayBuffer の扱いミス) 4.7 つまずきポイント IndexedDBの永続性: ブラウザ設定やストレージ圧迫状況によっては、データが削除される可能性があります。 初回のモデルロード: 初回はモデル取得で時間がかかります。ローディング表示を出しておくと安心です。 端末差: 推論速度(WASM/WebGPUの処理能力)は端末依存です。フォールバックや件数上限(例: 最大100件)を検討してください。 件数が増えると重くなる: 本サンプルは IndexedDB から全件取得し、1件ずつスコア計算します(6件のデモでは問題になりにくい)。数百件以上では索引付きベクトルDBが必要になります。 5. まとめ:AIにコードを任せ、設計を握ることが大事 「AIにIndexedDBのコードを書いて」と言えば、一瞬で動くものが手に入る時代です。ただ、今回のデモでも、語句一致なしでも特定の件数表示されるといった挙動や、初回だけモデル取得が走る点は、コードを書くだけでは見落としやすいと感じました。設計側(制約、フォールバック、UX)を握る価値は、むしろ大きくなっていると思います。 LocalStorage は一時的な設定、IndexedDB はメモとベクトルの置き場、Transformers.js と Web Worker は推論の実行場所、と分けて考えるとよいです。 HTML 2ファイルとブラウザだけで、保存・埋め込み・類似検索の流れは試せます。動かしたあと、「うちのプロダクトで、外部に送りたくないデータは何か?」をチームで1つ決めてみてください。ローカルに寄せる範囲の判断が、だいぶはっきりしてきます。 ぜひいろいろ試してみてくださいね。 執筆者 中川 拓哉(NTT西日本 デジタル革新本部 デジタル改革推進部所属) NTT西日本のWebアプリケーションの開発・運営に従事。 好きな技術スタック:TypeScript, Vue.js, GraphQL, Laravel 参考資料・出典 本記事を執筆するにあたり、以下のサイトを参考にしました。 MDN Web Docs: IndexedDB( https://developer.mozilla.org/docs/Web/API/IndexedDB_API ) MDN Web Docs: WebGPU( https://developer.mozilla.org/docs/Web/API/WebGPU_API ) MDN Web Docs: Web Workers( https://developer.mozilla.org/docs/Web/API/Web_Workers_API ) Transformers.js( https://github.com/huggingface/transformers.js ) jsDelivr CDN( https://www.jsdelivr.com/ ) 商標 「Google Chrome」は、Google LLCの商標または登録商標です。 「Microsoft Edge」は、Microsoft Corporationの商標または登録商標です。 「Visual Studio Code」は、Microsoft Corporationの商標または登録商標です。 「Firefox」は、Mozilla Foundationの商標または登録商標です。 「Node.js」は、OpenJS Foundationの商標または登録商標です。 「Hugging Face」は、Hugging Face, Inc.の商標または登録商標です。 Transformers.js は、Hugging Face, Inc.が提供するライブラリです。 記載の会社名・製品名は、それぞれ各社の商標または登録商標です。
p:has(> img) { width: 100% !important; } はじめに こんにちは、ZOZOMO部FBZブロックの杉田です。普段は Fulfillment by ZOZO が提供するAPIシステムを開発・運用しています。昨年からは、社内における開発者向けAI支援ツールの推進を担う専門チームでも兼務で活動しています。 ZOZOMO部SREブロックの𠮷富です。普段はFulfillment by ZOZOのSREをしています。 2026年6月25日・26日の2日間、幕張メッセにて「 AWS Summit Japan 2026 」が開催されました。本記事では、会場や各ブースの様子に加え、特に印象に残ったセッションについてご紹介します。 目次 はじめに 目次 AWS Summit Japanとは 会場の様子 セッション紹介 AI 駆動開発ライフサイクル (AI-DLC) のご紹介(AIM221) AI生産性のパラドックス 開発手法「AI-DLC」の紹介 実践事例と驚異的な成果 サイバーエージェントにおける AI 推進戦略と変革への取り組み(AIM229) エンジニアの評価制度 全社AI活用を可視化・底上げする「AI番付」の取り組み AIを「楽しむ」文化の醸成 人間がボトルネックにならないための「Human in the Loop」設計 生成 AI ブームのその先へ -AI を使いこなす人材戦略とプラットフォームエンジニアリング-(PRT237-S) 生成AIプロジェクトの成果創出までの「4つの壁」 本番化までの時間と「Governance as code」 生成AI時代の「FinOps」 Amazon S3セキュリティベストプラクティス(STG357) セッションのテーマと背景 S3セキュリティ8つのベストプラクティス 統制の分離とスケール Advanced VPC Networking-知っておきたいAWSネットワークの最新動向:VPC関連サービスのアップデートを総整理-(CDN320) Amazon VPC Latticeのさらなる進化とカスタムDNS対応 AWS Direct Connectとハイブリッド/マルチクラウド接続の強化 ワークフローオーケストレーターにおける複雑性と非決定性のコントロール(CNS454) ワークフローの役割とAWSサービスの紹介 指揮者は歌わない AIエージェントの非決定性を制御するアプローチ まとめ AWS Summit Japanとは AWS Summit Japanは、AWSの最新技術や活用事例を学べる日本最大級のイベントです。2026年も幕張メッセで2日間にわたり開催され、260以上のセッションや展示、ハンズオン、コミュニティ企画など、幅広いコンテンツが用意されていました。AIエージェントやサーバーレス、クラウド活用の最新動向など、今後のシステム設計や運用に関わるテーマが多く取り上げられていました。現地参加だけでなくオンデマンド視聴もできるため、気になるセッションを後から見返すこともできます。 会場の様子 紹介:𠮷富 早い時間から多くの参加者で賑わっていました。基調講演の開始時刻の前後は列が伸び、入場に1時間以上かかることもあったようです。 同僚提供写真 先着5,000名に毎年恒例のクッションとお弁当引換券が配布されました。さらに基調講演の開始前に入場したところ朝食も配布されていました。 いくつかのセッションは入場時に配布されるイヤホンを利用して視聴するサイレントセッションでした。座席に設置されたレシーバーを利用してセッションの音声を視聴できます。 AWS Builders’ FairではAWSサービスを利用したプロジェクトが多数紹介されていました。ゲーム形式のものが多く、楽しく学べました。 「Physical Hands'on Blocks 組んで守れ! AWSアーキテクチャ」を実際に体験しました。ブロックを動かしアーキテクチャを完成させ、攻撃から守るゲームです。 ELBからEC2を切り離す痛恨のミス Physical AI特設エリアではロボットの実機を体験することができました。 セッション紹介 AI 駆動開発ライフサイクル (AI-DLC) のご紹介(AIM221) 紹介:杉田 AI生産性のパラドックス AIを開発に導入しても速度向上は10〜15%にとどまり、場合によっては「AIを使うと19%遅くなる」というデータが紹介されました。たとえコーディングで時間を節約しても、開発ライフサイクルの他の部分で時間が失われているためです。 AIは決して銀の弾丸ではないので、適材適所で効果的な使い方をしないとかえって状況の悪化を招く場合があると、実感としても強く感じています。この研究結果から、AIに丸投げしたり、逆にAIを単なる補助ツールとして狭く使ったりするだけではダメなのだとあらためて感じました。 開発手法「AI-DLC」の紹介 AIのポテンシャルを最大限に発揮したうえで、ビジネスの意図をソフトウェアシステムとして価値提供できるようにするには、従来の方法の延長線上では限界があります。具体的には、以下のような課題が挙げられます。 人間の関与を最小限またはゼロにした開発は信用が難しく、説明責任も果たすことができない AIによるアジリティは開発ライフサイクル全体では限定的 コーディングで節約した時間は他の部分で失われている これらの課題を解決するためにAWSが再構想したのは「AI-DLC」です。AI-DLCは以下の3つのステップに分かれ、人間の意図伝達、AIによる計画・実行、人間によるレビューや承認という形で進むオペレーティングモデルです。 Inception Construction Operation AI利用を前提とした開発が行われる昨今では、開発プロセス自体をAIネイティブに作り直すというアプローチはとても重要だと思いました。 コンテキストの扱いがポイントとなるAI利用を前提とした開発において、各ステップが次のステップのコンテキストを構築していく流れは、とても理にかなっていると感じました。 実践事例と驚異的な成果 実際の実践事例として、AI-DLCの導入により属人性の高い領域でも開発生産性が200%以上も向上したほか、従来の方法では2か月想定の開発をわずか48時間で達成させた事例が紹介されました。 ここまでの成果を出すには、AI単体の力ではなく、ビジネス、開発、QAなどのチームが垣根を越えて協働する「モブエラボレーション」の体制づくりが不可欠になります。 本セッションを通じて、AIを単なる開発支援のツールとして使う段階は終わったとあらためて痛感しました。適用できそうな領域からAI-DLCの思想を取り入れ、人間とAIが強みを最大化し合う新しい開発プロセスへの移行を検討していきたいと思います。 サイバーエージェントにおける AI 推進戦略と変革への取り組み(AIM229) 紹介:杉田 エンジニアの評価制度 サイバーエージェントでは、2028年の開発プロセス完全自動化を見据え、AIを武器に開発領域からビジネス領域へ越境する「ビジネスリードエンジニア」などを新設していました。技術のみならず課題発見から実装・運用までを一気通貫で担える人材を正当に評価する枠組みです。 詳細については、以下のブログに記載があったので、気になる方はご覧ください。 developers.cyberagent.co.jp AIの台頭により、コーディングそのものの価値が相対的に変化している中で、エンジニアのキャリアパスを組織主導で再定義している点が印象的でした。AI時代においてエンジニアが目指すべき姿を考えるうえで、参考になる評価軸だと感じました。 全社AI活用を可視化・底上げする「AI番付」の取り組み 各事業部におけるAI活用の「成果インパクト」や「カルチャー醸成」などを、相撲の番付表(横綱〜幕下)に見立てて評価・可視化している取り組みが紹介されました。 単にツールを導入して終わるのではなく、組織ごとの成熟度を定量・定性的に測る仕組みを作ったうえで、全社的なナレッジ共有と底上げを図る非常に効果的なアプローチだと感じました。特に、結果を分析レポートとしてまとめ、事業ごとの改善案を提案する仕組みは参考になりました。 AIを「楽しむ」文化の醸成 AIをフル活用して実装速度を競う「AI開発RTA」や、2日間でゼロからプロダクトを作る「AI Agent Arena」を開催した様子が紹介されました。これらのイベントを通じて、エンジニアがAIに楽しく触れる場を提供し、ベストプラクティスの創出・共有を促していました。 利用を強制するのではなく、イベントを通じて「AIフレンドリー化」を推進する姿勢は、組織の熱量を高める取り組みとして参考になりました。 人間がボトルネックにならないための「Human in the Loop」設計 自動化が進むと人間の監視が形骸化する「プロセスの萎縮」というリスクがあります。このリスクに対して、タスクのリスクに応じて「全件レビュー」「スコアベース」「エスカレーション」の3パターンで人間の関与を設計している取り組みが紹介されました。 タスクに関係なく一律でHuman in the Loopを採用するのではなく、リスクや複雑さを加味したうえで段階的にHuman in the Loopを設計している点が参考になりました。 サイバーエージェントの取り組みは、ツールの導入にとどまらず、評価制度、企業文化、そして開発プロセスそのものをAI時代に適合させていくといったものでした。AIと協働する新しい開発組織のあり方については、私の所属組織でも重要なテーマなので、今後の活動の参考になるセッションでした。 生成 AI ブームのその先へ -AI を使いこなす人材戦略とプラットフォームエンジニアリング-(PRT237-S) 紹介:杉田 生成AIプロジェクトの成果創出までの「4つの壁」 同社のアンケートから、2024年から2025年にかけて生成AIの活用率は増加している一方で、DXの成果について「期待通り以上」と回答した割合は減少していることが確認できました。 また、PoCの成功はゴールではなく、その先には「本番化までの時間」「LLMの精度不足」「利用が広がらない」「想定外のコスト増」という4つの壁が存在すると指摘されていました。 本番化までの時間と「Governance as code」 セキュリティやコンプライアンスの都度審査により、本番の開発開始までに数か月を要するケースがあると紹介され、これは組織の規模感が大きくなることでより顕著になる傾向があります。対策として、ガバナンス要件を組み込んだ開発テンプレート(Governance as code)を用意し、事前承認済みの型を開発者に利用させるアプローチが紹介されました。 守りのプロセスを自動化・テンプレート化して、開発者の認知負荷を下げる取り組みはまさにプラットフォームエンジニアリングの真骨頂だと思いました。Governance as codeは、社内での車輪の再発明を回避する手段として、OSSで有名な Backstage をはじめとした開発者向けポータルとの相性が良いと感じました。 生成AI時代の「FinOps」 生成AIの利用拡大に伴い請求額が想定の数倍に膨らむリスクに対し、コストの可視化とプロンプトキャッシング等の対策が紹介されました。 プロンプトキャッシングの仕組みとして、キャッシュは先頭から一致した場合のみヒットするため、プロンプトの上部に変数(可変部)が存在すると効果が薄くなる点について解説されました。 そこで、この性質を活かすために可変部をプロンプトの下部に移動させる構造変更をしたことで、1ワークフローあたり400円かかっていたコストが80円(80%削減)へと劇的に改善した事例が紹介されました。 機能としてキャッシングを有効にするだけでなく、内部の仕組みを理解してキャッシュヒット率を最大化している点は学びでした。生成AIを活用するうえで、それらのコストは切っても切り離せない課題だと思うので、機能を使いこなすことによるコストインパクトに驚きました。 Amazon S3セキュリティベストプラクティス(STG357) 紹介:𠮷富 Amazon S3はシステム構成において、データ保存先、ログ出力先、バックアップ、データ連携基盤など、さまざまな場面で利用されています。利用用途が増えるにつれて、アクセス制御や利用者・アプリケーションごとの権限管理、意図しない公開の防止など、セキュリティ面で考慮すべきポイントも増えていきます。 このセッションでは、Amazon S3のセキュリティを高めるためのベストプラクティスを改めて学べました。 セッションのテーマと背景 パブリックアクセスのブロック(BPA)や全新規オブジェクトの暗号化などがデフォルトで有効になっており、データを安全に活用しやすいデフォルト設定が用意されています。 S3セキュリティ8つのベストプラクティス データ保護を実現するためのプラクティスとして以下が提示されました。 パブリックアクセスをブロックする 。 バケットキーを有効にする :SSE-KMS利用時のAWS KMSコストを最大99%削減。 統制を分離してスケールする 。 セキュリティ変更をモデル上でテストする 。 AWS Organizationsを活用する :RCP/SCPによる組織レベルの強力なガードレールを適用。 データ保護をアプリケーションにも広げる :チェックサムの活用など、アプリケーション側でもデータ保護を考慮する。 ログを有効にする :異常検知や自動修復などの対応を実装する。 耐久性とリカバリを事前に計画する 。 統制の分離とスケール ビジネスや組織の成長に合わせて、権限管理における統制を分離する必要があります。単一のバケットポリシーだけですべてを管理しようとすると保守が難しくなりやすいため、以下の機能を活用して権限を適切に委譲・分割する考え方が紹介されていました。 S3 Access Points :ユースケースごとに独自のポリシーを持たせる。 S3 Access Grants :特定のプリンシパルに対して、バケットやプレフィックスへのアクセス権をプログラムで付与する。 ABAC(属性ベースアクセス制御) :リソースやプリンシパル名ではなく、タグに基づいてセキュリティポリシーを定義する。 S3のセキュリティはバケットの設定だけで完結するものではなく、組織全体のガバナンスやアプリケーション側も含めて設計する必要があることを再認識しました。紹介されていた内容の中には、パブリックアクセスブロックや暗号化、ログ設定など、既存のバケットでもすぐに確認できる項目が多くあります。S3を利用している環境がある場合は、ぜひこの機会に設定状況を見直してみてください。 Advanced VPC Networking-知っておきたいAWSネットワークの最新動向:VPC関連サービスのアップデートを総整理-(CDN320) 紹介:𠮷富 このセッションでは、VPCやネットワーク関連サービスにおける2025年のアップデートを網羅的にキャッチアップできました。普段VPCを触っている人ほど、こんなにアップデートがあったのかと驚く内容だったと思います。紹介量が多く見応えのある内容だったので、ネットワーク構成に関わる方はぜひチェックしてみてください。特に気になったアップデートを2つ紹介します。 Amazon VPC Latticeのさらなる進化とカスタムDNS対応 VPC Lattice は、複数のVPCやアカウントをまたぐサービス間通信を論理的にグループ化(サービスネットワーク)してつなぐ機能です。これまではサービスやリソースに自動生成された一意のFQDNが割り当てられていましたが、2025年のアップデートとして、カスタムDNS名や設定可能なIPに関する機能強化が紹介されていました。自分で設定したDNS名によるアクセスが可能になり、既存のアプリケーションからのアクセスルーティングがやりやすくなります。 AWS Direct Connectとハイブリッド/マルチクラウド接続の強化 マルチクラウド接続を容易にする「AWS Interconnect Multicloud/Last Mile」が一般提供(GA)を開始しました。Direct Connectを経由してオンプレミス環境や他クラウド、AWS上のアプリケーションを接続しやすくなり、ハイブリッド/マルチクラウド環境におけるネットワーク設計の選択肢が広がりました。 VPC関連サービスでは、2025年だけでも150以上の新機能が追加されたそうです。このセッションはVPC周辺の最新情報をまとめてキャッチアップする良い機会になりました。 2024年のVPC関連サービス 2025年のVPC関連サービス ワークフローオーケストレーターにおける複雑性と非決定性のコントロール(CNS454) 紹介:𠮷富 ワークフローは事業の成長、システムの改修に伴って複雑になってしまうことがあります。さらに障害の監視・管理やリトライ処理といった例外的な処理の整備によりシステムが複雑化したり、それらの解決のためにあらゆるシステムでAI・MLとの結合が求められたりするようになっています。 このセッションでは、複雑化しがちなワークフローの管理手法と、AIエージェント特有の「非決定的」な挙動をどう扱い、どのようにアーキテクチャに組み込むべきかを学べました。 ワークフローの役割とAWSサービスの紹介 ワークフローは、複雑なシステム連携における例外処理・リトライ・並列処理・Map実行などの課題に対応し、可観測性(オブザーバビリティ)を高めるために用います。セッションでは、要件に応じたAWSの主要なワークフローオーケストレーションサービスの使い分けが紹介されました。 AWS Step Functions :GUI上でワークフローを視覚的に設計できるサービス。JSONataに対応しており、ステートマシン内でデータの変換・抽出・加工を行える。 AWS Lambda durable functions :JavaScript/TypeScriptやPythonなどで、長時間実行されるワークフローをコードとして記述できる仕組み。最大1年の実行に対応し、リトライや状態管理を扱いやすい。 Amazon MWAA :Apache AirflowをAWS上で利用できるマネージドサービス。DAG(有向非巡回グラフ)によってタスクの依存関係や実行順序を管理でき、AWS外のシステム連携にも向いている。 指揮者は歌わない 複雑性保存の法則 が示す通り、システムが持つ複雑さを完全に消し去ることはできません。重要なのは「複雑性をどこに置き、どうコントロールするか」であり、その基本方針がワークフローの分割です。そこであげられていたのは「指揮者は歌わない(関心の分離)」という設計原則です。 例えばStep FunctionsのChoice stateに複雑なメール検証のような検証ロジックを書くと、保守が困難になります。オーケストレーターは進行管理に徹し、ビジネスロジックはAWS Lambdaなどのコード側に切り出すべきです。 AIエージェントの非決定性を制御するアプローチ ワークフローにとってAIは、予期せぬ判断や無限ループを引き起こす可能性のある「非決定的」なシステムです。この揺らぎをシステムに組み込む方法として、決定論的ワークフローで包み込むアプローチが紹介されました。 Outer/Inner Loopパターン:AIの非決定論的な推論を、タイムアウトやリトライを備えたワークフローで制御。 Human in the Loop(HITL)パターン:重要な判断はAIに任せきらず人間の承認フローを挟む。 Workflow as Toolsパターン:既存の決定的なワークフローを、AIから安全なツールとして呼び出す。 また、Amazon BedrockなどのAIサービス利用時は、スロットリング対策としてExponential BackoffやJitterの活用が重要です。 Exponential Backoff リトライ間隔を指数関数的に増加させることで、サーバーの負荷を軽減する。 Jitter リトライ間隔にランダムな揺らぎを加えることで、同時リトライを防ぐ。 AIエージェントを利用する場合でも「処理を適切な単位で分割・委譲する」という設計の基本が大切なことを再認識しました。 まとめ 今回のAWS Summit Japan 2026を通じて、AI活用を前提にした開発・運用のあり方を改めて考える機会になりました。 生成AIは単なる開発支援ツールではなく、開発プロセス、組織文化、評価制度、ガバナンス、コスト管理まで含めて向き合うべきテーマになっていると感じました。一方で、AIエージェントのような非決定的な仕組みを安全に扱うためには、Human in the Loopや決定論的なワークフローによる制御など、従来から大切にされてきた設計原則も引き続き重要です。また、S3やVPCといった基盤領域についても、サービスの進化に合わせて継続的に設計や設定を見直す必要性を再認識しました。 今回得た学びを、日々の開発・運用だけでなく、社内でのAI活用推進にも活かしていきたいと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。Yahoo! ID連携のiOS SDKの開発・運用を担当している矢倉です。現在、多くのヤフーのiOSアプリでは、ローカル認証を活用した独自のデバイス認証による再認証機能を搭載しています。こ...

動画

書籍