株式会社エブリーのブログ - TECH PLAY

TECH PLAY

株式会社エブリー

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

全474件

はじめに こんにちは、リテールハブ開発部の杉森です。 社内向けのリモート MCP サーバーを Amazon Bedrock AgentCore Gateway 上に構築する検証を進めており、認証には Amazon Cognito を使っています。Claude のカスタムコネクタや Claude Code から OAuth で接続する構成を調べる中で、MCP のクライアント登録の方式と、それに対する Cognito の対応状況が気になったので調べてみました。 本記事では、調べた内容の整理と、検証環境で実際に接続を試した結果を紹介します。内容は 2026 年 9 月時点の情報に基づいています。 MCP の認可の基本 登場する3つの役割 MCP の認可仕様は OAuth 2.1 をもとにしており、以下の3つの役割が登場します。 役割 仕様での説明 本記事の構成での例 MCP クライアント ユーザーに代わって MCP サーバーにリクエストを送る。OAuth 2.1 のクライアントにあたる Claude のカスタムコネクタ、Claude Code MCP サーバー アクセストークンを受け取ってリクエストを処理する。OAuth 2.1 のリソースサーバーにあたる AgentCore Gateway 認可サーバー 必要に応じてユーザーとやり取りし、MCP サーバーで使うアクセストークンを発行する Amazon Cognito ※ MCP のアーキテクチャの用語では、Claude Code のようなアプリ自体は MCP ホストと呼ばれ、MCP クライアントはホストの中で MCP サーバー1つとの接続を担当する部品を指します(ホストは接続する MCP サーバーごとに MCP クライアントを作ります)。本記事では OAuth のやり取りに注目するため、両者を区別せずに MCP クライアントと呼びます。 参考: Authorization(MCP 仕様 2026-07-28) Architecture overview(MCP 公式ドキュメント) 接続の流れ MCP の認可仕様によると、MCP クライアントから認可が必要な MCP サーバーへの接続は、おおまかに以下の流れで進みます。 MCP サーバーにトークンなしでリクエストし、401 と Protected Resource Metadata の場所を受け取る Protected Resource Metadata を取得し、どの認可サーバーを使うかを知る 認可サーバーメタデータ(認可サーバーが公開している、エンドポイントの URL や対応している機能を記載した JSON)を取得する クライアント登録を行い、client_id を得る ブラウザでユーザーが認可し、アクセストークンを取得する アクセストークンを付けて MCP サーバーにリクエストする 本記事で扱うのは、「4.」のクライアント登録と、その判断材料になる「3.」の認可サーバーメタデータです。 MCP のクライアント登録の仕組み client_id を得る3つの方式 MCP の認可仕様では、MCP クライアントは認可フローを始める前に client_id を取得しなければならないとされており、その方法として以下の表の3つが定められています。 MCP クライアントは、接続の流れの「3.」で取得した認可サーバーメタデータを見て、どの方式が使えるかを判断します。CIMD と DCR は、認可サーバーがメタデータに表の3列目のフィールドを含めることで、使えることを示します。事前登録はクライアント側に client_id を設定するだけで使えるため、対応を示すフィールドはありません。 方式 仕組み 対応を示すメタデータのフィールド Client ID Metadata Documents(CIMD) クライアントがメタデータを記載した JSON を HTTPS の URL で公開し、その URL を client_id として使う。認可サーバーは URL から JSON を取得して検証する client_id_metadata_document_supported: true 事前登録(Pre-registration) 事前に登録して得た client_id(と必要ならクレデンシャル)を、クライアントに組み込むか、ユーザーが入力する なし Dynamic Client Registration(DCR) クライアントが実行時に登録エンドポイントへ自分の情報を送り、client_id を発行してもらう registration_endpoint クライアントが方式を選ぶ順番 MCP 認可仕様の Client Registration によると、すべての方式に対応するクライアントは、以下の優先順位で登録方式を選ぶことが推奨されています。 事前登録: そのサーバー向けに事前登録されたクライアント情報があれば、それを使う CIMD: メタデータに client_id_metadata_document_supported があれば、CIMD を使う DCR: メタデータに registration_endpoint があれば、フォールバックとして DCR を使う いずれも使えない場合: ユーザーにクライアント情報の入力を求める 参考: Client Registration(MCP 仕様 2026-07-28) バージョンごとの仕様の変化 3つの方式の扱いは、仕様のバージョンごとに変わってきています。 バージョン CIMD 事前登録 DCR 2025-06-18 記載なし 規定なし(DCR に対応しない認可サーバー向けの代替手段として説明のみ) SHOULD 2025-11-25 SHOULD SHOULD(クライアントのみ) MAY(後方互換のため) 2026-07-28 SHOULD SHOULD(クライアントのみ) MAY(Deprecated) ※ CIMD と DCR は認可サーバーとクライアントの両方に対する要件ですが、事前登録はクライアントが静的なクライアント情報を扱えるようにすることを求める要件で、認可サーバー側への要件はありません。 ※ 2026-07-28 版で非推奨になった DCR は後方互換のために残されていますが、非推奨機能の一覧では、2027-07-28 以降にリリースされる版で削除される可能性があるとされています。 参考: Deprecated Features(MCP 仕様 2026-07-28) Cognito の対応状況 クライアント登録の方式への対応 2026 年 9 月時点で、Cognito は DCR にも CIMD にも対応していません。AgentCore 開発者ガイドの Runtime の OAuth のページには、Cognito は DCR(RFC 7591)に対応していないため、事前に Cognito でクライアントを登録して client_id を取得する必要があると明記されています。CIMD についても、AWS のサンプルリポジトリで Cognito のユーザープールは URL 形式の client_id を受け付けないと説明されています。 参考: Runtime の OAuth 認証(Amazon Bedrock AgentCore 開発者ガイド) sample-serverless-architecture-cimd-with-cognito(GitHub) 認可サーバーメタデータの置き場所 認可サーバーメタデータの置き場所は、以下の2つの仕様でそれぞれ定められています。 仕様 置き場所 RFC 8414(OAuth 2.0 Authorization Server Metadata) /.well-known/oauth-authorization-server OpenID Connect Discovery 1.0 /.well-known/openid-configuration ただし、Cognito の issuer(https ://cognito-idp.<リージョン>.amazonaws.com/<ユーザープールID>)のようにパスを含む場合は、URL の組み立て方が仕様によって異なります。RFC 8414 は .well-known をホストとパスの間に挿入し、OpenID Connect Discovery はパスの末尾に追加します。さらに RFC 8414 の Section 5 では、openid-configuration を RFC 8414 の組み立て方で置く形も示されています。そのため、置き場所の候補は以下の3つになります。 候補 URL 組み立て方 ① /.well-known/oauth-authorization-server/<ユーザープールID> RFC 8414(ホストとパスの間に挿入) ② /.well-known/openid-configuration/<ユーザープールID> RFC 8414 の組み立て方で openid-configuration を置く形(RFC 8414 Section 5) ③ /<ユーザープールID>/.well-known/openid-configuration OpenID Connect Discovery(パスの末尾に追加) Cognito が公開しているのは ③ の OpenID Connect Discovery のメタデータのみで、RFC 8414 のメタデータは公開されていません。Cognito のユーザープールは OpenID Connect のプロバイダーとして動作するため、アプリ側で接続先のエンドポイントをあらかじめ設定しておく通常の連携では、この点は問題になりません。AgentCore Gateway 自身も、インバウンド認可の設定では OpenID Connect Discovery の URL を指定します。 MCP クライアントは接続先の MCP サーバーがどの認可サーバーを使うかを事前に知らないため、Protected Resource Metadata から認可サーバーメタデータまでを自動でたどります。現行の MCP 認可仕様では、認可サーバーは RFC 8414 と OpenID Connect Discovery のどちらか一方を提供すればよく、クライアントは両方の置き場所を探すことが必須とされているため、Cognito のメタデータも見つけられます。 参考: Gateway のインバウンド認可(Amazon Bedrock AgentCore 開発者ガイド) Authorization Server Discovery(MCP 仕様 2026-07-28) MCP クライアントから AgentCore Gateway に接続する方法 前述のとおり、Cognito は DCR にも CIMD にも対応していないため、そのままでは MCP クライアントが client_id を得られません。これに対応する方法として、Cognito のまま client_id を事前に登録する、Cognito の前段で DCR / CIMD を受ける、認可サーバー自体を変える、の3つが考えられます。 1. 事前登録したアプリクライアントを使う Cognito にアプリクライアントを作成し、その client_id(と必要なら client_secret)を MCP クライアント側に設定する方法です。Claude のカスタムコネクタや Claude Code では、接続の設定で client_id と client_secret を指定できます。 あわせて、Cognito のアプリクライアントに MCP クライアントのコールバック URL を許可し、AgentCore Gateway の Allowed clients にも同じ client_id を登録しておく必要があります。 構成は最もシンプルですが、接続する MCP クライアントごとに、こうした登録や設定が必要です。 参考: Get started with custom connectors using remote MCP(Claude ヘルプセンター) MCP(Claude Code ドキュメント) Amazon Cognito(Amazon Bedrock AgentCore 開発者ガイド) 2. Cognito の前段に DCR / CIMD を受けるプロキシを置く Cognito はそのまま ID 基盤として使い、MCP クライアントとの間に登録を肩代わりするプロキシを置く方法です。プロキシは registration_endpoint や client_id_metadata_document_supported を含む独自のメタデータを公開して MCP クライアントからの登録を受け付け、認可やトークンの発行は裏側の Cognito に転送します。トークンの発行元は Cognito のままです。 MCP クライアント側の設定は不要になりますが、プロキシを自前で実装・運用する必要があります。MCP クライアントに Cognito ではなくプロキシを認可サーバーとして見つけさせる仕組みや、登録エンドポイントの保護など、考慮することが多くなります。 3. DCR / CIMD に対応した認可サーバーに変える DCR や CIMD に対応した認可基盤を使う方法です。たとえば Auth0 は DCR に対応しており、CIMD についても、管理者が CIMD の JSON を取り込んでクライアントを登録する形で対応しています。 ただし、既存の Cognito ユーザープールのユーザーや、Cognito に接続している既存のアプリの移行が必要になります。 参考: Register your MCP Client Application(Auth0 ドキュメント) 実際に試してみた 検証用の AgentCore Gateway に、MCP 公式の TypeScript SDK(@modelcontextprotocol/sdk)で接続しました。リクエストとレスポンスを HTTP のレベルで記録するため、Claude のアプリではなく SDK を使っています。以下の2つのパターンを試しています。 client_id を設定せずに接続する client_id を事前に設定して接続する client_id を設定せずに接続する SDK のリクエストとレスポンスは以下のとおりです(ID とドメインは伏せています)。 POST https://<Gateway>/mcp -> 401 www-authenticate: Bearer error="invalid_token", resource_metadata="https://<Gateway>/.well-known/oauth-protected-resource" GET https://<Gateway>/.well-known/oauth-protected-resource -> 200 GET https://cognito-idp.ap-northeast-1.amazonaws.com/.well-known/oauth-authorization-server/<ユーザープールID> -> 400 GET https://cognito-idp.ap-northeast-1.amazonaws.com/.well-known/openid-configuration/<ユーザープールID> -> 400 GET https://cognito-idp.ap-northeast-1.amazonaws.com/<ユーザープールID>/.well-known/openid-configuration -> 200 Error: Incompatible auth server: does not support dynamic client registration まず、トークンなしのリクエストに対して Gateway が 401 を返し、WWW-Authenticate ヘッダーで Protected Resource Metadata の場所を伝えています。Protected Resource Metadata の authorization_servers には、Cognito の issuer が入っていました。 { " authorization_servers ": [ " https://cognito-idp.ap-northeast-1.amazonaws.com/<ユーザープールID> " ] , " resource ": " https://<Gateway>/mcp ", " scopes_supported ": [] } 次に、SDK は Cognito の認可サーバーメタデータを前述の ① → ② → ③ の順で探し、③ で取得しています。これは MCP 認可仕様の Authorization Server Metadata Discovery が定める順序と同じです。③ で取得できたメタデータは以下のとおりです。 { " authorization_endpoint ": " https://<ドメイン>.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize ", " end_session_endpoint ": " https://<ドメイン>.auth.ap-northeast-1.amazoncognito.com/logout ", " id_token_signing_alg_values_supported ": [ " RS256 " ] , " issuer ": " https://cognito-idp.ap-northeast-1.amazonaws.com/<ユーザープールID> ", " jwks_uri ": " https://cognito-idp.ap-northeast-1.amazonaws.com/<ユーザープールID>/.well-known/jwks.json ", " response_types_supported ": [ " code ", " token " ] , " revocation_endpoint ": " https://<ドメイン>.auth.ap-northeast-1.amazoncognito.com/oauth2/revoke ", " scopes_supported ": [ " openid ", " email ", " phone ", " profile " ] , " subject_types_supported ": [ " public " ] , " token_endpoint ": " https://<ドメイン>.auth.ap-northeast-1.amazoncognito.com/oauth2/token ", " token_endpoint_auth_methods_supported ": [ " client_secret_basic ", " client_secret_post " ] , " userinfo_endpoint ": " https://<ドメイン>.auth.ap-northeast-1.amazoncognito.com/oauth2/userInfo " } メタデータに client_id_metadata_document_supported がないため、SDK は CIMD を使わずに DCR に進みました。しかし registration_endpoint もないため、「Incompatible auth server: does not support dynamic client registration」というエラーで停止しました。 client_id を事前に設定して接続する 事前登録の方法で、Gateway の Allowed clients に登録している Cognito のアプリクライアントの client_id を SDK に設定し、同じように接続しました。 (③ の openid-configuration の取得までは同じ) リダイレクト先: https://<ドメイン>.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize パラメーター: response_type, client_id, code_challenge, code_challenge_method, redirect_uri, resource メタデータを取得した後、登録の処理を飛ばして Cognito の認可エンドポイントへのリダイレクトまで進みました。この後はブラウザでユーザーがログインする手順になります。 まとめ 今回の検証では、事前登録したアプリクライアントの client_id を MCP クライアントに設定すれば、Cognito のままでも認可の手順に進めることを確認できました。 一方で、現行の MCP 仕様では CIMD が推奨され、DCR は非推奨になっています。接続する MCP クライアントが増えていくなら、Cognito の前段にプロキシを置くことや、CIMD に対応した認可サーバーを使うことも検討が必要になりそうです。 MCP の認可仕様はバージョンごとの変化が大きいため、構成を検討する際は仕様の最新版と、利用する認可サーバー・MCP クライアントの対応状況をあわせて確認するのがおすすめです。
AI AgentのタイムアウトからGoのイテレータを俯瞰する こんにちは、岩﨑 ( @uho-wq ) です。 デリッシュキッチン開発部でプレミアム機能を開発しています。 先日、アプリ版デリッシュキッチンでGenerative UIを搭載したデリッシュAIをリリースしました。 この記事ではAI Agentが引き起こしたタイムアウトのバグから、Go 1.23で入ったイテレータについて俯瞰してみたいと思います。 デリッシュAI デリッシュキッチンでは、AIアシスタントと対話しながら作りたいレシピを探すことができます。 Generative UIによる選択肢生成によって選ぶだけで会話が続き、作りたいレシピにたどり着けるようになっています。 デリッシュAI ツールコールや応答をストリーミングで表示し、無料ユーザーも上限付きで使えます。 AI Agent はすべて Go で実装しました。使用したエージェント開発フレームワークは Agent Development Kit (ADK) for Go (v2.1.0) です。 LLM の応答は SSE で配信しており、イベントの形式は AG-UI (The Agent-User Interaction Protocol) の イベントタイプ に合わせて実装しています。 異常時のイベント RUN_ERROR は今後も出てくるため、ここで紹介しておきます。 1 回の実行には context.WithTimeout で 60 秒のタイムアウトを設けています。超えた場合も RUN_ERROR を返します。 以降のコード引用と行番号は、断りがない限り adk-go v2.1.0 のものです。 ツール呼び出しがタイムアウトまで止まらなかった ある日、Sentry に次のようなエラーが届きました。 run <run_id>: context deadline exceeded これは、1 回の実行(run)が 60 秒のタイムアウトに達したことを示すエラーです。普段は数秒で終わる処理なので、明らかに異常です。 ログを追ってみると、Agent が Generative UI( A2UI )の UI 生成ツールを、1 回の実行の中で約 50 回も呼び続けていました。 そしてツール呼び出しが終わらないまま 60 秒が経過し、ユーザーには RUN_ERROR が返ってしまっていた、というわけです。 ログを深掘りすると、そもそも Agent がツールに渡す引数が間違っていたことが分かりました。さらにツールは毎回エラーを返していたにもかかわらず、Agent はそれを受けて引数を直すことなく、同じ呼び出しを繰り返していました。 つまり「間違った引数で呼ぶ → エラーが返る → また同じ引数で呼ぶ」というループに陥っていたのです。 ツール呼び出しのリトライ上限は実装済みだった 実は、実装が漏れていたわけではなく、Agent の暴走を懸念してリリース前からリトライ上限は実装済みでした。 それを説明するために、まず ADK for Go でのツールの作り方を見てみます。 ADK for Go では、 functiontool.New の第 2 引数(handler)にツールの処理を書くことで、Go の関数を Agent から呼べるようになります。 以下は2つのintを加算するツールの実装例です。 type SumArgs struct { A int `json:"a"` B int `json:"b"` } type SumResult struct { Sum int `json:"sum"` } func sumFunc(ctx agent.Context, in SumArgs) (SumResult, error ) { return SumResult{Sum: in.A + in.B}, nil } t, err := functiontool.New(functiontool.Config{ Name: "sum" , Description: "sums two integers" , }, sumFunc) 私は呼び出し回数のカウンタをこの handler の中に実装していたのですが、これが原因でした。 実際にツールが呼ばれるまでの流れは次のようになっています。 [起動時] [リクエスト時] functiontool.New Runner.Run └ resolvedSchema └ Flow.Run (for {}) └ jsonschema.For ── 推論 ──▶ f.inputSchema ──▶ functionTool.Run struct → additionalProperties:false └ ConvertToWithJSONSchema └ Validate 起動時に、handler の引数の struct から JSON Schema が推論されます。 そしてリクエスト時には、 functionTool.Run が handler を呼ぶ前に、 ConvertToWithJSONSchema (google/jsonschema による検証)で map[string]any の引数を型変換します。 input, err := typeutil.ConvertToWithJSONSchema[ map [ string ] any , TArgs](m, f.inputSchema) if err != nil { return nil , err } https://github.com/google/adk-go/blob/v2.1.0/tool/functiontool/function.go#L197-L200 引数がスキーマに合わないとここで error を返して終了してしまうため、handler まで処理が届かず、カウントされなかったのです。 この問題の対処法 この問題は、カウンタを handler ではなく BeforeToolCallback に実装すれば解決できます。 BeforeToolCallback は、ツールを実行する直前に呼ばれる関数です。 functionTool.Run がスキーマを検証するより前に呼ばれるので、引数が間違っていても回数を数えられます。 上限に達したら、 SkipSummarization を true にして結果を返します。 func limitToolCalls(limit int32 ) llmagent.BeforeToolCallback { var n atomic.Int32 return func (ctx agent.Context, t tool.Tool, args map [ string ] any ) ( map [ string ] any , error ) { if n.Add( 1 ) <= limit { return nil , nil // nil ならツールをそのまま実行する } ctx.Actions().SkipSummarization = true return map [ string ] any { "error" : "tool call limit reached" }, nil } } SkipSummarization が true のイベントは、 IsFinalResponse() の先頭の条件で true になります。これで Agent ループを終了させることができます。 func (e *Event) IsFinalResponse() bool { if (e.Actions.SkipSummarization) || len (e.LongRunningToolIDs) > 0 { return true } return !hasFunctionCalls(&e.LLMResponse) && !hasFunctionResponses(&e.LLMResponse) && !e.LLMResponse.Partial && !hasTrailingCodeExecutionResult(&e.LLMResponse) } https://github.com/google/adk-go/blob/v2.1.0/session/session.go#L212-L218 ここまでで、カウンタが動かなかった理由は分かりました。 ではなぜ、ツールは毎回エラーを返していたのに、Agent のループは終わらなかったのでしょう。 実装では、ADK が返す iter.Seq2[*session.Event, error] を range で回し、error を受け取ったら SSE で RUN_ERROR を返して終了する設計になっていました。ツールのエラーがここまで届いていれば、ループはその場で止まっていたはずです。 Go のイテレータ(range-over-func) 本題に入る前に、Go のイテレータについておさらいします。 Go 1.23 で for range の対象に関数が加わりました。対象になる関数は次の 3 形式です。 func(func() bool) func(func(V) bool) func(func(K, V) bool) iter パッケージは後ろ 2 つに名前を付けています。 package iter type Seq[V any ] func (yield func (V) bool ) type Seq2[K, V any ] func (yield func (K, V) bool ) イテレータ関数は、引数で受け取った関数(慣習的に yield と呼びます)に値を渡して呼ぶことで、 for range のループ本体に値を 1 つずつ渡します。 func Count(n int ) iter.Seq[ int ] { return func (yield func ( int ) bool ) { for i := range n { if !yield(i) { // ループ側が break したら false が返る return } } } } for i := range Count( 3 ) { fmt.Println(i) // 0, 1, 2 } yield は、ループを break や return などで途中で抜けない限り true を返し、抜けた場合は false を返します。 イテレータはデータが尽きれば自分から return して終われますが、途中で止めたいときはループ側が break や return で抜けて false を伝えてあげる必要があります。 イテレータはそれに従って yield を呼ぶのをやめます。 つまり途中で止めるかどうかを決めるのはループ側で、イテレータはその合図に従うことで終了できます。 ADK for Go の iter.Seq2[*session.Event, error] ADK for Go は iter.Seq2[K, V] の V に error を入れています。ここからは、ツールの error が yield されていたのかを確認するために ADK for Go の実装を見てみます。 ADK for Go の実行は 3 層になっています。呼び出し側が使う Runner.Run が、セッションを解決して LLMAgent を動かします。 LLMAgent は agent 本体で、モデルとツールを持ちます。そして LLMAgent は「モデル呼び出し → ツール呼び出し」のループを、内部の Flow に任せています。 それぞれのメソッドの戻り値は iter.Seq2[*session.Event, error] で、内側の yield を外側がそのまま通すような実装になっています。 LLMAgent.run を見ると、 Flow.Run を range して受け取ったイベントを逐次 yield しているだけです。 func (a *llmAgent) run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error ] { // ... return func (yield func (*session.Event, error ) bool ) { for ev, err := range f.Run(ctx) { a.maybeSaveOutputToState(ev) if !yield(ev, err) { return } } https://github.com/google/adk-go/blob/v2.1.0/agent/llmagent/llmagent.go#L454-L460 Flow.Run の中身が、最終応答が出るまでモデル呼び出しを繰り返す for ループです。 func (f *Flow) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error ] { return func (yield func (*session.Event, error ) bool ) { for { var lastEvent *session.Event for ev, err := range f.runOneStep(ctx) { if err != nil { yield( nil , err) return } // forward the event first. if !yield(ev, nil ) { return } lastEvent = ev } // A thought-only ("thinking") turn reports as final but has no // answer; don't stop on it — call the model again. if lastEvent == nil || (lastEvent.IsFinalResponse() && !isThoughtOnlyTurn(lastEvent)) { return } if lastEvent.LLMResponse.Partial { // We may have reached max token limit during streaming mode. // TODO : handle Partial response in model level. CL 781377328 yield( nil , fmt.Errorf( "TODO: last event is not final" )) return } } } } https://github.com/google/adk-go/blob/v2.1.0/internal/llminternal/base_flow.go#L103-L130 runOneStep は、モデルを 1 回呼び、その応答に含まれるツールを実行します。つまりツール呼び出しも runOneStep の中で起こります。 外側の for {} は、最終応答のイベント (IsFinalResponse) が出るまで runOneStep を繰り返します。 Flow.Run の終了条件 この Flow.Run が終わるのは、次の 5 つの場合です。 正常終了 モデル呼び出し・コールバックの error イベントが 1 つも出なかった場合 ストリームが途中で切れた場合 ループ側が range を抜けた場合 ツールのエラーはこの 5 つの終了条件に含まれません。 実際のツール呼び出しは以下のような処理になっています。 func (f *Flow) callTool(toolCtx agent.Context, tool toolinternal.FunctionTool, fArgs map [ string ] any ) map [ string ] any { var response map [ string ] any // ... if err != nil { return map [ string ] any { "error" : err.Error()} } return response https://github.com/google/adk-go/blob/v2.1.0/internal/llminternal/base_flow.go#L1233-L1278 ツールが返した Go の error をそのまま返すのではなく、 {"error": ...} という map[string]any 型に変換して返していることが分かります。 ADK for Go はこの map を Event として yield(ev, nil) します 。つまり Seq2 の error 側には乗らず、正常なイベントとして流れてくるのです。 そして、ツールの応答(FunctionResponse)を含むイベントは、先ほどの IsFinalResponse() で false になります。そのためループは終わらず、次のモデル呼び出しへ進みます。 私は「error が返ってきたらループは終わる」と思っていました。しかし実際には、 error には Seq2 の error と、イベントの一部に含まれる error の 2 通りがあった のです。 ここまでの話をまとめると以下のようになります。 モデルがスキーマに合わない引数で function call を出す functiontool が引数変換で error を返す callTool が {"error": ...} に変えてイベントとして流す イベントは function response を含むので IsFinalResponse() は false でループ継続 モデルへ戻ると、モデルは同じ引数でもう一度呼ぶ 1 に戻る。60 秒のタイムアウトまで繰り返す サーバー側の err != nil は一度も true にならないので、 RUN_ERROR を返す分岐にも入りません。これが、タイムアウトまで止まらなかった理由でした。 イベントにエラーを詰めることに対する考察 ツールのエラーをイベントとして流す設計には、それなりの理由があると思っています。 イベントで返せば range ループを抜けることがないため、エラーがそのままモデルに渡ります。これによって Agent 自身がリトライ(自己修正)できるようになります。今回のように直せないこともありますが、多くの場合はこの方が都合がよいはずです。 一方で、ツールが失敗したことを知る手段は、FunctionResponse の中の "error" キーの有無だけになります。 iter.Seq2[*session.Event, error] の設計について ADK for Go はイベントを逐次 yield してストリーム処理を実現しています。 モデル呼び出しやツール呼び出しのエラーは、イベントを複数流したあとに起こります。 (iter.Seq[Event], error) の戻り値では途中のエラーを表現できないので、現状の形式になったのではないかと理解しています。 そもそも iter.Seq2[T, error] の形式は適切なのか Go はエラーを値として返すため、 Seq2[K, V] の V に error を入れることは言語仕様上可能です。ただし、規約としては定まっていません。 実際に標準ライブラリを調べてみましたが、 公開 API で Seq2[T, error] を返すものは 1 つもなく、 Seq2 を返す API はすべて key-value か index-value の形式を取っていました。 では、 iter パッケージが提案された時点ではどうだったのでしょう。 提案 golang/go#61897 の本文には、当初 value-error の形式が書かれていました。 Seq2 represents a sequence of paired values, conventionally key-value, index-value, or value-error pairs. If iteration can fail, it is conventional to iterate value, error pairs: // Lines iterates through the lines of the named file. // Each line in the sequence is paired with a nil error. // If an error is encountered, the final element of the // sequence is an empty string paired with the error. func Lines(file string ) iter.Seq2[ string , error ] ところが、 Russ Cox 氏は提案文を godoc に取り込むとき、value-error の記述を意図的に省いています。 2024-06-07 の commit fe36ce669c ("iter: add doc comment from proposal")のメッセージにこうあります。 The edits are to drop mention of value-error Seq2 usage and to adjust for the bool result changes. 現行の iter パッケージの Seq2 の説明にも、error への言及はありません。 省いた理由をまとめて書いた箇所は、私が追った範囲では見つかりませんでした。 つまり Seq2[T, error] の振る舞いは言語も標準ライブラリも規定していないため、ループ側で責務を持ち、制御するしかありません。 ADK for Go における制御 実は ADK for Go もエラーに対する振る舞いについて決めかねています。 TODO: check if we should stop iterator on the first error from stream or continue yielding next results. https://github.com/google/adk-go/blob/v2.1.0/internal/llminternal/base_flow.go#L805 https://github.com/google/adk-go/blob/v2.1.0/internal/llminternal/base_flow.go#L822 実際に、adk-go の中だけで 3 パターンが混在しています。 パターン 場所 挙動 yield(nil, err); return model/gemini/gemini.go L146-L148 error で打ち切る yield(nil, err) して continue runner/run_node.go L148-L153 error を流して継続する yield(event, err) して continue runner/runner.go L316-L320 event と error を両方流して継続する error の後にイテレーションが続くかどうかは、イテレータ側で自由に決定できるため、ループ側では error を受け取っても次の要素が来る前提で備える必要があります。 止めたい場合は、ループ側で break や return でループから抜ける処理を実装する必要があります。 従来の関数の戻り値は 1 回きりなのでこの問題は起きず、何度でも yield できる Seq2[T, error] で初めて生じる問題といえます。 まとめ 今回のタイムアウトは、ツールのエラーが iter.Seq2 の error 側ではなく、イベントの一部として流れていたことが主な原因でした。イベント側へ流れたことにより、ループ側の err != nil は一度も true にならず、Agent がタイムアウトするまでツールを呼び続けていました。 Seq2[T, error] の error は「失敗したら必ず返される」とは限りません。そもそも error を yield するか、yield した後も要素を流し続けるかは、イテレータの実装が決定できます。言語も標準ライブラリもこの形に規約を設けておらず、ADK for Go の中でも 3 つのパターンが混在していました。 一方でループを続けるかどうかはイテレータ側がどう実装されていてもループ側によって決定できます。 ただし、ループ側が正しく判断するには、イテレータがエラーをどう扱うかを知る必要があります。ADK for Go がツールのエラーをイベントとして流してループを続けるのは、Agent に自己修正させるためでした。 もし iter.Seq2[T, error] の実装に出会ったら、error がどこに流れ、その後もイテレーションを続けることを実装が意図しているかを確認すると安全だと思います。逆に実装する側になったら、error の後も続けるのか、それはなぜなのかをドキュメントに書き残しておくと、ループ側が止める条件を決めやすくなりそうです。
はじめに 株式会社エブリーは、2026年9月11日(金)〜13日(日)に有明セントラルタワーホール&カンファレンスで開催された iOSDC Japan 2026 に、ゴールドスポンサーとして参加しました! 開発本部から6名のエンジニアが現地参加したので、ブースの様子や印象に残ったセッションをご紹介します。 会場の様子 3日間で70本以上のトークや LT が行われ、現地には約1,000名規模の参加者が集まる大盛況でした。 セッションやブース以外の企画も盛りだくさんで、朝9:30からドーナツサービスに始まり(おいしかった!!!)、声優さんによる館内アナウンス、スポンサーブースを回るスタンプカードビンゴ、ペンライトが揺れる LT 大会など、会場全体がお祭りムードでした。 エブリーブース エブリーは今回、ゴールドスポンサーとしてブースを出展させていただきました。ブースでは、参加型のアンケートボードとデリッシュAI の実機デモの2つを展示しました。 当日は多くの方々にブースへお立ち寄りいただき、iOS や AI 開発についてさまざまなお話を伺うことができました。足を運んでいただいた皆様、本当にありがとうございました! アンケートボード「iOSアプリ開発、AIにどこまで任せていますか?」 「iOSアプリ開発、AIにどこまで任せていますか?」をテーマに、「開発工程のどの段階まで AI に任せているか」と「iOS エンジニアの経験年数」の2軸をシールで答えてもらう参加型のアンケートを実施しました。 3日間で皆様に、ボードいっぱいシールを貼っていただきました。 ざっくりまとめると 完全自律させている人はまだ少数 実装は AI に任せている方が大多数 最終的な確認・判断は人間が行っている エンジニア歴との相関関係は特になし という結果でした。 シールを貼っていただいた流れでリアルな開発事情を共有し合うこともできました。 特に盛り上がったのが「iOS の UI 確認や E2E テストをどう行っているか?」というテーマです。「実装は AI に任せられるけれど、動作確認はスピードやトークン消費の兼ね合いでまだ難しい…」という声が多く、現場のリアルな工夫を聞くことができました。 ドメインの複雑さや求められる品質レベル、個人開発か業務かによって AI 活用のアプローチは異なり、各社・各人が置かれた環境の中で、ベストな開発方法を模索している印象でした。ちなみに数少ない「完全自律させている」方にお話を聞くと、最後は「勇気と度胸」という声もありました。 AI 以外の話題では、iOS エンジニア歴15年以上の歴戦の猛者から、Objective-C / iPhone 3G / Xcode 3 時代の古き良きお話を聞けたのもリアルイベントならではの醍醐味でした。 デリッシュAI の実機デモ もう一つの展示は、iOS 版で新しくなった「デリッシュAI」の実機デモです。デリッシュAI は、チャットで相談するとレシピを提案してくれる『デリッシュキッチン』の AI アシスタントで、提示された選択肢をタップしていくだけで会話が進み、作りたいレシピにたどり着けます。無料プランでも1日5回までお試しいただけるので、ぜひ触ってみてください! 今回デリッシュAI のアップデートに伴い、Generative UI というアプローチを取り入れる技術的挑戦を行いました。 仕組みとしては、サーバー側の AI エージェントがおすすめレシピといったデータだけでなく「画面構成」までを返し、アプリ側がそれを SwiftUI で動的に描画しています。AI にはあらかじめ定義した数種類のコンポーネントのみを渡し、「どれをどの順で配置するか」の判断に絞らせることで、デザインの品質を保ちながらも、対話や選択肢のバリエーションを柔軟に拡張できるようにしました。 ブースでは、Generative UI という考え方そのものはもちろん、それを実際のプロダクトへ落とし込んでいる点に対して「面白い!」という声を多数いただき、非常に嬉しかったです。 他社のスポンサーブース 3日間で他社のブースも回らせていただきました。どこも工夫を凝らした企画で、ノベルティ含め個性豊かな展示でした。全部は回りきれなかったので、立ち寄れた中から一部を紹介します。 ピクシブ さん :デジタルシールを作成・交換できるアプリ「ぴくしーる」をデモ展示されていました。今年の iOSDC のために開発されたそうで、イベント専用にアプリを作り込まれる熱量に驚きました。 ZOZO さん :ZOZO検定というクイズ企画でした。社内で実施されたクイズの出張版とのことで、全10問に挑戦し、正解数に応じてノベルティがもらえました。 LINEヤフー さん :Yahoo!知恵袋を題材にした「#リアル知恵袋」。来場者が付箋で質問や回答を寄せ合う企画で、技術ネタから人生相談まで多様な交流が付箋上で生まれていました。 転職ドラフト さん :AI 時代に評価されるエンジニアとは何か、をテーマに、来場者が付箋で意見を貼っていくアンケートボードを実施されていました。AI 時代に iOS エンジニアとして、エンジニアとして生き残っていくために何が必要か、考えさせられる内容でした。 セッション紹介 ブース運営の合間に聴講したセッションから、印象に残ったものを紹介します。 柔軟性のあるUIで、多くの環境でアプリを活躍させる 発表者: noppe さん iOSDC 開幕前日の9月10日(日本時間)に折りたたみ iPhone「iPhone Duo」が発表されたこともあり、レスポンシブな UI 設計というタイムリーなテーマに会場は超満員でした。多様な画面サイズにどう適応していくかは、これから多くの iOS エンジニアが向き合うことになる課題であり、そこにどう向き合うかのヒントとなるようなセッションでした。 noppe さんは「UI の柔軟性」を、状況を問わずに価値を提供できること、と定義していました。UI を「目的(ユーザーが実現したいこと)」「コンテキスト(ユーザーが置かれた状況)」「インターフェース(目的を実現する手段)」という3つの要素の組み合わせとして捉え直すと、コンテキストが変わったときに目的を守るにはインターフェースの側が変わるしかない。マルチプラットフォーム対応や新しい画面サイズへの対応を「利用者が少ない割にコストが高いもの」として後回しにするのではなく、同じ目的に対して複数のインターフェースを持てる状態をつくろう、というのがセッションの主張です。 では、インターフェースはコンテキストの変化にどう応じるのか。セッションではそれを「連続的な適応」と「段階的な適応」の2つに整理していました。前者は枠のサイズに合わせて伸縮させる方法、後者は境界を越えたら情報量やレイアウトの構造そのものを切り替える方法で、その境界は「同じインターフェースのまま目的を十分に達成できるか」で決める、という基準もあわせて示されていました。 単に「デザインの崩れを修正する」のではなく、「目的とコンテキストに基づいて UI を設計する」という本質ベースなアプローチの重要性を再認識させるセッションでした。連続性と段階性という捉え方も、これまで感覚的に対応していた部分を言語化していただいた印象です。iPhone Duo へのリサイズ対応に限らず、UI の設計指針として参考になる内容でした。 世界の中心でAI(App Intents)を叫ぶ - App Intents中心設計の実践ガイド 発表者: touyou(藤井 陽介)さん(株式会社グッドパッチ) App Intents はアプリの機能をアプリの外部から呼び出すための仕組みであり、2022年の登場以来、Siri、ウィジェット、Spotlight、Apple Intelligence、そして今年の Siri AI と、年々呼び出し口を広げてきました。Apple はあらゆるアプリが Siri や Apple Intelligence と統合されたエコシステムを目指しており、その手段となる App Intents の重要性は年々高まっています。これほど重要になっているなら、後から対応するのではなく最初から App Intents を設計の中心に据えてしまおう、というのが本セッションの主題「App Intents 中心設計」でした。 原則として次の3つが挙げられていました。 App Intents から設計する なるべく DRY でいること 色んな動線で使ってもらうこと 実装としては、UI も Siri も Spotlight もウィジェットも同じ Intent を通して Service を呼ぶ構成にする、アプリ内のボタンも Button(intent:) で Intent 経由にする、などというものでした。処理が一箇所にまとまり、どの内部 / 外部システムからでも同じ機能が使えるようになります。 さらに AppIntent をアプリの「動詞」、AppEntity をアプリの「名詞」と捉えれば、App Intents から設計するとは、そのアプリで「誰が何をできるのか」を動詞と名詞で棚卸しすることに他ならない、つまり App Intents 中心設計はユースケース中心設計と同じ発想に立っている、という話でした。 これまで私のなかで App Intents は「ショートカットやウィジェットの実装に必要なもの」という位置づけで、そこに iOS 27 で Siri AI が登場したためAppIntent や AppEntity を用いて音声入力によるアプリ操作をできるようにしたいな、くらいに考えていました。が、Apple が想像以上に App Intents に注力していることを知り、であれば App Intents を起点に設計するという考え方(App Intents Driven Development)も面白そうだと感じました。とはいえ、既存アプリを全面的に App Intents 中心へ移行するのは相応の工数がかかりそうなので、やるとすれば主要なユースケースをいくつか Intent として切り出すところから始めるのが、現実的なアプローチかなと思っています。 アフターイベントのお知らせ ゆめみ、フェンリル、ヤプリ、ウェルスナビ、セーフィー、エブリーの6社合同で「DroidKaigi & iOSDC After Talks Night 2026」を開催します。DroidKaigi 2026 と iOSDC Japan 2026 の合同アフターパーティーで、iOS と Android のエンジニアが同じ場に集まり、各社の LT や懇親会でプラットフォームを越えた交流ができる会です。 項目 詳細情報 開催日時 2026年10月2日(金)19:00〜21:00 開催場所 住友不動産麻布十番ビル 9F アクセンチュア・イノベーション・ハブ 東京(東京都港区三田一丁目4番1号) 開催形態 オフライン / オンライン コンテンツ 各社の Android & iOS に関するセッション / 懇親会 現地参加の申込はすでに締め切られていますが、オンライン(配信)での参加は引き続きお申し込みいただけます。当日はエブリーのメンバーも LT で登壇予定ですので、ぜひご覧ください! 詳細やオンライン参加のお申し込みは以下からどうぞ。 yumemi.connpass.com 最後に 3日間、多くの方にブースに足を運んでいただきました。エブリーのことを知っていただくだけでなく、iOS 開発や AI 活用の話を情報交換できて、とても良い機会になりました。 これからもデリッシュキッチン、そしてエブリーをよろしくお願いします! 最後までお読みいただき、ありがとうございました!
目次 はじめに 前提 プロジェクト概観 AI ネイティブな開発環境を実現するために 具体的な作業は AI に任せる skills agents hooks PreToolUse hook Stop hook ドキュメントも AI-Friendly な構成にする 実際の開発フロー 要件定義 タスク分割 各タスクの実装サイクル レビューの構造 所感 skill / agent 化の効果 コンテキストが大きくなりがち 完了までの時間がかかる トークン消費が激しい 良くも悪くもスクラップ&ビルドで進めやすい まとめ おわりに はじめに こんにちは。 開発本部開発3部トモニテ開発部所属の庄司( @ktanonymous )です。 直近で新規開発に携わる機会があり、「具体的な中身を作っていくのは AI」という前提のもとで開発環境全体を設計してみました。 本記事では、どのような環境を構築したのか、実際の開発フローはどうなっているのか、実際の開発を通じて感じている課題について紹介したいと思います。 なお、本記事の内容は 2026 年 9 月 14 日時点の情報に基づいています。 前提 プロジェクト概観 今回のプロジェクトは、既存のコードやインフラを持たない状態から立ち上げた Web アプリケーションの開発でした。 完全な新規開発ということで、技術スタックもゼロから選定していて、主な構成は以下のようになっています。 なお、エブリーでは全社員に Claude が配布されているため Claude を前提として設計していますが、設計の考え方自体は Claude であることに依存しないと思います。 モノレポ: pnpm + Turborepo ランタイム: Cloudflare Workers(API と SPA を単一の Worker に統合) バックエンド: Hono + Hono RPC フロントエンド: React + TanStack Router + Vite データベース: PlanetScale for Postgres(ORM は Drizzle) 認証: Better Auth UI: Tailwind CSS v4 + shadcn/ui ツールチェーン: oxlint / oxfmt、TypeScript 7、Vitest また、過去にチーム内で実装サイクルにおける AI 活用を設計していたこともあり、今回の設計のベースとして活用しています。 下記の記事でチームでの実装サイクルの AI 化の取り組みについて取り上げていますのでぜひご覧ください。 tech.every.tv AI ネイティブな開発環境を実現するために 具体的な作業は AI に任せる 意思決定の記録や要件の実現などの具体的な作業を AI に安心して任せられるように、まず初めに AI が判断に使うためのドキュメント群を整備することに時間をかけました。 結果的に、skills 13 個、agents 8 個、hooks 2 個という構成に着地しました。 ドキュメントや AI 設計の詳細については後述します。 skills 開発フロー全体を役割ベースで分解し、大きく 3 つの系統に分離した skills を定義しています。 要件定義: write-requirements (統括)、 draft-requirements (執筆)、 evaluate-requirements (採点)、 draft-design (外部設計) 実装・レビュー: write-tasks (タスク分割)、 implement-task (統括)、 review-implementation (統括)、 audit-architecture-approach (規約の監査)、 review-protocol (レビュー系エージェント共有の規約) ドキュメント: write-adr 、 write-ubiquitous-language 、 write-okf-document 、 write-architecture-approach 必要に応じてワークフロー全体をコントロールするオーケストレータのスキルと、実際に作業を行うスキルやエージェントを分離しています。 たとえば write-requirements では要件定義書の実体を自分で書かせることはせず、 執筆は draft-requirements に、採点は evaluate-requirements に委譲し、返ってきた結果を集約してループを判定します。 implement-task も同様に、自分ではコードを書かず、実装は feature-implementer エージェントに、レビューは review-implementation スキルに委譲します。 オーケストレータのスキルはいずれも「統括 → 執筆・評価への委譲 → 集約」という同様のフローで動かします。 もう 1 つ意識したこととして、採点基準や合否条件を執筆側・実装側に渡さないことがあります。 draft-requirements と evaluate-requirements は context: fork で独立したコンテキストとして実行され、執筆側にはスコアの基準となる情報を渡しません。 implement-task も、ループの終了基準(合否条件・上限回数)を feature-implementer への委譲メッセージに含めない設計にしています。 これは、作業者側が採点基準そのものに最適化してしまうことを防ぐためであり、指標そのものを目標にして指標が機能しなくならないようにしています。 出力の形式と品質基準がブレないようにするため、出力の期待結果を書いた evals/ や参照資料 ( references/ ) 、出力例 ( assets/examples/ ) も作成しています。 また、frontmatter で model と effort を明示するようにしています。 agents agents は 8 個あり、役割に応じて model と effort を frontmatter で指定しています。 エージェント model effort 役割 feature-implementer sonnet high 実装(1 起動 1 タスク) backend-reviewer opus high バックエンド層のレビュー frontend-reviewer opus high フロントエンド層のレビュー test-reviewer opus high テスト戦略のレビュー contract-and-types-reviewer opus xhigh 層をまたぐ型安全性のレビュー convention-auditor opus xhigh 規約文書自体の監査 finding-verifier sonnet medium 指摘の独立再検証 okf-document-manager haiku 指定なし ドキュメント一式の作成・更新(形式は後述) 作業と評価を同じモデルにすることで出力に対するバイアスがかかってしまうことを考慮し、作業と評価の間では必ずモデルを変えるようにしています。 hooks hooks では、以下のように機械的に判定できる制約に関する役割を持たせています。 PreToolUse hook ツールの実行直前に呼ばれます。 レビュー系のエージェントについては、ファイルを書き換える操作を拒否し、コマンドの実行も lint・型チェック・テストに限って許可します。 これにより、レビュアーが「read-only である」ことを保証します。 Stop hook セッションを完了しようとしたときに呼ばれます。 レビューを経ていない実装の変更が残っている場合には完了をブロックし、レビューを飛ばして作業を終えられないようにしています。 ただし、ブロックが一定回数続くと Claude Code 側が完了を許可する仕様のため 1 、絶対の保証ではありません。 ドキュメントも AI-Friendly な構成にする ドキュメントの基本フォーマットとして、Google が提唱している Open Knowledge Format(OKF) 2 を採用しました。 OKF は、YAML frontmatter 付きの Markdown ファイルのディレクトリとして知識を表現する仕様で、 index.md (一覧)と log.md (変更履歴)を併設した「バンドル」という単位で管理します。 ただし、すべてのドキュメントを OKF にしているわけではありません。 OKF を適用しているのは 1 件ずつ独立した知識単位であり、分類とクロスリンクが必要なドキュメントである ADR(Architecture Decision Record)とユビキタス言語としています。 例えば、ユビキタス言語は、業務領域ごとに複数のコンテキストへ分けて管理していて、実装規約は「原則層 + 具体ルール層(バックエンド・フロントエンド・型契約・テスト戦略)」の 2 層で構成しています。 一方で、フォーマットを揃えること自体が目的ではなく、ドキュメント管理の効率化が目的のため、散文主体の文書や時系列の文書は通常の Markdown のままにしています。 これらのドキュメントの作成・更新も skills と agents で行います。 ADR とユビキタス言語では、それぞれ専用のスキルが人間へのヒアリングとドキュメント固有のテンプレート・検証を担当し、OKF 作成の基本作業は共通のエージェント( okf-document-manager )に委譲します。 実装規約は執筆専用のスキルが作成・更新し、規約自体の監査は後述するレビューの構成の中で行っています。 ドキュメント作成に関わる skills / agents 実際の開発フロー 開発フローを構成する skills と agents の呼び出しの関係は以下のようになっています。 上にオーケストレータのスキルがいて、その下に委譲先となる skills / agents が示されています。 矢印には呼び出しに使うツールと受け渡すファイルを書いています。 開発フローを構成する skills と agents メインのセッションはオーケストレーションをメインタスクとし、そこからタスクに応じてサブエージェントを起動させます。 要件定義 要件定義は write-requirements スキルが統括します。 流れは「ヒアリング → 執筆 → 採点」のループです。 write-requirements が人間にヒアリングを行い、回答を inputs.md に記録する(ヒアリングをスキップして、あらかじめ用意した情報から自動で進めることも可能) draft-requirements が EARS(Easy Approach to Requirements Syntax)記法で要件定義書を執筆する。要件ごとに信頼度マーク(確認済み / 推測 / 業務上の未確認)を付与する evaluate-requirements が 8 観点 100 点満点で採点する 完成条件を満たすまで、採点結果のフィードバックを付けて執筆に戻す(採点は最大 3 回、執筆は最大 5 回) EARS 記法は、「〜のとき、システムは〜しなければならない」のように条件と振る舞いを定型文で書く記法です。 完成条件は「総合 85 点以上、かつすべての観点で B 評価(配点の 70%)、かつ業務上の未確認が 0 件」としています。 ここでは以下のような観点と配点でスコアリングしています。 業務要件の網羅性: 20 点 要件の具体性・非曖昧性: 20 点 受入基準の検証可能性: 20 点 異常シナリオの業務列挙: 10 点 業務由来の非機能目標: 5 点 トレーサビリティと名称体系: 10 点 根拠と信頼度の識別: 10 点 スコープの明確さ: 5 点 タスク分割 要件定義が完成したら、 write-tasks スキルで 1 日以内に終わる粒度を目安とした小さいタスクに分割していきます。 分割の原則として以下の 2 観点を取り入れています。 レイヤーごとの横割り(Repository 層・Service 層・Handler 層で別タスク)ではなく、ユースケース単位で入力からロジック、永続化、出力、テストまでを 1 タスクとする カバレッジゲート: すべての要件・受入基準・横断要件について、それを主に担当するタスクが 1 件以上あることを生成時に検証する。欠けている場合はタスクファイルを出力しない 各タスクの実装サイクル 各タスクは implement-task スキルが統括します。 このスキル自身はコードを書かず、以下のサイクルを回します。 要件・タスクを確認し、対象タスク 1 件だけを feature-implementer に渡す。実装規約は全文を渡さず、そのタスクに関係するルールだけを要約して渡す feature-implementer が実装し、フォーマット・lint・型チェックまで通す review-implementation スキルでレビューする(実装とレビューは別モデル) 実装を止めるべき指摘(後述する規約準拠の指摘)のうち、根拠まで確認できたものが残っていれば、修正して 3 に戻る(最大 3 回) エージェントで判断できない部分だけ人間が確認する 実装を止めるべき指摘が残っておらず、オーケストレータが実行する lint・型チェック・テストがすべて通れば、そのタスクは完了です。 前述の Stop hook がレビューを経ていない変更での完了をブロックするため、レビューのサイクルを飛ばして終わることがないようになっています。 レビューの構造 レビューに関わる skills / agents / hooks の構成は以下のようになっています。 review-implementation が実装のレビューを、 audit-architecture-approach が規約ドキュメント自体の監査を統括し、どちらの指摘も finding-verifier が再検証します。 レビューに関わる skills / agents / hooks の構成 review-implementation の処理は以下の順で進みます。 変更されたファイルのパスで対象レビュアーを決める 該当するレビュアーを並列に起動する。全員 read-only で、lint・型チェック・テストの結果で指摘を裏付ける すべての指摘を finding-verifier に一括で渡し、独立に再検証する。指摘ごとに信頼度(根拠まで確認できたか、可能性が高いにとどまるか)を付け直し、偽陽性を棄却し、意味的に重複する指摘を統合する。(ここでは新しい指摘は出さない) 検証済みの指摘を 2 系統に分けて出力する 1. のレビュアーの選び方は以下の通りです。 バックエンド層の変更があれば backend-reviewer 、フロントエンド層の変更があれば frontend-reviewer を起動する テストの変更または欠落があれば test-reviewer を起動する 共有パッケージの変更があれば contract-and-types-reviewer を必ず起動する。バックエンドの変更で API の公開面(ルートやレスポンスの型)が変わる場合や、フロントエンドの変更でクライアント側の型の使い方が変わる場合にも起動する 無関係なレビュアーは起動しない方針だが、判断が曖昧な場合は広めに起動する 4. の 2 系統というのは「規約準拠の指摘」と、本プロジェクトで「規約疑義」と呼んでいる指摘です。 規約準拠の指摘は「実装が規約に従っていない」という指摘で、実装のブロッキング要因になります。 規約疑義は、レビュアーが「実装ではなく規約側に誤りや陳腐化がある可能性」を検出した指摘です。 規約疑義は実装をブロックせず、専用の置き場所に 1 件 1 ファイルで記録します。 蓄積された規約疑義は、 audit-architecture-approach スキルが convention-auditor を起動して規約文書自体を監査する際の入力になります。 規約の対象方針の最終判断は人間が行います。 「実装が規約に従っているか」と「規約自体が正しいか」は、この 2 系統に分けて扱っています。 この 2 つを別の指摘として扱うことで、規約が間違っているときに実装側を直してしまったり、規約を疑って実装が進まなくなったりしないようにしています。 所感 ここからは、実際に運用してみて感じていることについて書いていきます。 skill / agent 化の効果 ドキュメント作成から実装・レビューまでのすべてを skill / agent 化したことで、自分で手を動かして作業する時間はほぼなくなりました。 実際に自分で手を動かすのは、要件を固める部分などドキュメントをより精密なものにしていく部分がほとんどです。 開発開始時点でまとまった時間をドキュメント整備に費やし、いわゆるハーネスを整備したことでより根幹の部分に集中することができていると思います。 コンテキストが大きくなりがち 一方で、AI に渡すコンテキストは大きくなっているような気がしています。 整備したドキュメントもありますが、実装から修正までのループを繰り返す中でコンテキストが徐々に大きくなっていく影響もあると考えています。 まだ着手できていませんが、ドキュメントやコードの関係性をグラフ化して効率的にコンテキストをコントロールできるようにしていきたいと思っています。 完了までの時間がかかる 1 タスクあたりの所要時間は、体感でだいたい 1 時間前後になっています。 依存関係を持つタスクを進める場合にはかなりの時間を要してしまいます(それでも AI がなかった頃に比べれば圧倒的に早いですが)。 トークン消費が激しい トークンが足りないと感じることも増えてきました。 トークンリミットで作業が止まってしまうと中途半端な状態で止まってしまい、セッションの文脈も考慮した時に手戻りもしづらい状態になってしまいます。 そのため、サイクルの途中で止まると、最初からやり直すか、リセットまで待つかしかなくなります。 良くも悪くもスクラップ&ビルドで進めやすい 作り直しの判断が軽くなるのは、利点でもあり欠点でもあります。 プロジェクト進行という観点では、実際に動くものを早く見られるので、改善ループが回しやすくなりました。 一方で、既存のものを大きく壊す変更は、AI にとっても推論の負荷が高くなりやすいと思います。 各セッション・各プロンプト(場合によってはプロジェクトレベル)で「何を変えてよくて、何を変えてはいけないか」を丁寧に設計して意図しない変更が行われないように意識する必要があります。 まとめ トークン効率化の手法は色々あると思いますが、どの程度効率化できるのか、手持ちの資源に対する充足度はどうなのか、今後どこまで AI に任せるのか、考えないといけないことは多いと感じています。 また、AI プラットフォーム側の一時的な障害で作業が止まることも実際にあり、AI 前提の仕組みにすればするほど、AI サービス側の可用性がそのまま開発にも影響するようになることを改めて実感しました。 昨今コンテキストの効率化がホットトピックではありますが、そこを目指す以前の指標として効率化を意識するくらい使い切れているのかという視点もあるかと思います。 そういう意味では、AI を前提とした開発環境を整備したことで「使い倒す」という観点において一定の成果を出せたのではないかと感じていますが、ここからさらに踏み込んで効率的な AI 開発環境を目指すことが重要になっていくと考えています。 おわりに 今回は、新規開発のプロジェクトで構築した AI ネイティブな開発環境と、実際の運用の中で感じている課題について整理してみました。 まだまだ試行錯誤の部分も多いですが、キャッチアップや検証を繰り返してより良いものにしていきたいと思っています。 この記事が、AI を活用した開発環境を整備している方の参考になれば幸いです。 最後まで読んでいただき、ありがとうございました。 Best practices for Claude Code (2026年9月14日閲覧) 。「Claude Code overrides the hook and ends the turn after 8 consecutive blocks.」と記載されています。 ↩ GoogleCloudPlatform/open-knowledge-format (2026年9月14日閲覧) 。採用時点の仕様は v0.1 で、現行の v0.2 は v0.1 を置き換えるマイナーバージョンアップとして公開されています。 ↩
Go Conference 2026 目次 はじめに エブリーのスポンサーブース アンケートボード 食クイズ・くじ引き ノベルティ セッション紹介 Go における FFI のこれまでとこれから cgo とその課題 WebAssembly を使った FFI wasmify と wasm2go 感想 ワークショップ紹介 go.devの歩き方、その先へ 〜Go公式リソースの旅。明日からの調べ方を手に入れるワークショップ〜 参加した理由 ワークショップの流れ 設問2 の調査 ワークショップで得たこと Go × SIMDで高速化するベクトル検索 ~ ルーフラインモデルでSIMDが効く境界を探れ! SIMDとは GoからSIMDを使う ベンチマークで効果を確認する int8量子化でデータ量を減らす ワークショップを体験して まとめ 非公式アフターイベント Go BASH Vol.3 のお知らせ 最後に はじめに 2026年9月11日(金)に中野セントラルパークで開催された「Go Conference 2026」に今年も参加させていただきました! 今年も参加レポートとして、会場の様子やセッションの感想についてお届けします! gocon.jp 今年のテーマは「 Go Far, Go Together 」でした。 Go Far, Go Together このテーマの通り、人との繋がりを重要視していることが強く感じられるカンファレンスだったと思います。 sanposhiho さんのKeynoteセッション「Open Source, Open World」に始まり、 speakerdeck.com コミュニケーションスペースのGo Context Wall、伝Go板、スポンサーブース、難易度別セッション・ワークショップ、そして最後は懇親会と、Goを始めたての人でも、熟練の方でも最初から最後まで楽しめたカンファレンスだったのではないかなと思います。 エブリーのスポンサーブース エブリーは今年は Silver スポンサーとしてブースを出展させていただきました! 弊社サービスであるデリッシュキッチンをイメージした黄色基調のブースとなっています。 エブリーブース ブースに足を運んでいただいた皆様、本当にありがとうございました! アンケートボード 昨年に引き続きアンケートボードを用意しました! 「あなたのGo興味教えてください」というテーマで、Go歴 (Goの利用歴)と気になるトピックが交わる場所にシールを貼っていただきました。 最終結果はこちらです! アンケート結果 Go歴0〜15年以上の方まで幅広いGopherに回答いただきましたが、Go歴0〜1年の方は設計思想、ある程度経験のある方は最新のGo1.27のトピックに関心が若干寄っているところが面白い部分でした。 またどのGo歴の方もGoとAIに対して関心があり、どんなAI Agentが社内で使われているのか、またどのようにHarnessを作っているのかといった話題についてブースではたくさんお話しすることができました! 食クイズ・くじ引き 今年から導入した新しいブース企画である、クイズも非常に盛り上がりました! クイズは食に関する問題2問、Goに関する問題2問の計4問で構成されています。 最終問題のGoの実行結果を答えるクイズで、言語仕様を理解していないと解けなかったり、考えたことがなかったユースケースに対して回答する必要があったりと、難しめに設定しただけあって苦戦している参加者の方も多く見られました。 クイズ ノベルティ 景品は軽量スプーン、まな板、菜箸、しゃもじなど普段の料理で使えるキッチングッズです。 デリッシュキッチングッズ ハズレを引いた方にも、弊社CTOが自らテイスティングして選んだ「CTOブレンド」のコーヒーとステッカーをお渡ししました。 CTOブレンド CTOブレンドの制作秘話については下記を参照ください。 人々へ明るい変化を提供する、オリジナルブレンドコーヒー「every CTO Blend」を制作 キッチングッズが当たった方からは、「最近自炊を始めたのでこれを使って頑張ります」といった感想や、「まさに今欲しかったものなので嬉しいです」といった感想をいただけて運営としても嬉しかったです! また、このくじ引きで当選したまな板を今でも使っているという方もいたりと、何度もイベントに出展しているとこういうこともあるのかと嬉しくなりました。 セッション紹介 Go における FFI のこれまでとこれから 発表者: goccy さん(株式会社LayerX) レポート: あかがわまさとも スライド: speakerdeck.com 食事管理アプリ ヘルシカ を担当している あかがわまさとも です。私からは、goccy さんの発表「Go における FFI のこれまでとこれから」について紹介させていただきます。 FFI(Foreign Function Interface)は、同一プロセス内で、ある言語で書かれたプログラムから他の言語で実装された機能を呼び出すための仕組みです。本セッションでは、従来の cgo を使う方法とそれに対する課題から、goccy さんによる WebAssembly を活用した新しい方法までを紹介していただきました。 cgo とその課題 スライド 3〜15 ページ FFI での呼び出しは、ABI(Application Binary Interface)という「バイナリ間で関数を呼び出すための約束」に合わせて行う必要があります。Go では、この ABI に合わせた呼び出しを cgo が引き受けています。 発表の序盤では、cgo が抱える課題が3つの観点から整理されていました。 メモリ管理 : Go と C ではメモリの管理が分かれているためメモリリークが起きやすく、C 側でセグメンテーション違反(SIGSEGV)が起きると Go のプロセスごと落ちます。さらに両者はアドレス空間を共有しているので、C 側の不具合で Go 側のメモリを読み書きできてしまいます。 型変換 : 構造体へのポインタや、コールバックのための関数ポインタの受け渡しが複雑になります。相手が C++ だとさらに難しくなります。 開発・デプロイ : C コンパイラが必要になるため Go のクロスコンパイルの手軽さが失われ、Go のプロファイラやデバッガも C 側のコードには使えません。 そのうえで、ポインタのライフサイクル管理、コールバックの実装、静的リンクによるシングルバイナリ化という3つのテクニックが紹介されました。ただしこれらは対症療法で、メモリの管理が分かれていることや C コンパイラが必要になることは、cgo を使う限り変わりません。 普段は Go だけで完結する開発をしているので cgo を書く機会は全くないのですが、課題を詳しく紹介していただき、あまり活用されていない現状や難しさにかなり納得しました。 WebAssembly を使った FFI スライド 16〜23 ページ ここまでの課題を踏まえて、「メモリを安全に扱いたい」「Go のクロスコンパイルの恩恵を受けたい」という動機から WebAssembly(WASM)を使う方法が紹介されました。 WASM モジュールは、自分専用に割り当てられた連続したメモリ領域(リニアメモリ)しか読み書きできないため、ホストとアドレス空間が完全に分離されます。範囲外アクセスはプロセスのクラッシュではなく Go のエラーになります。システムコールも、ホストが許可したものだけが WASI 経由で実行されます。WASI は WebAssembly System Interface の略で、WASM からファイルやネットワークといったホスト側の機能を使うための標準インターフェースです。Go 側は WASM を embed で埋め込み、Pure Go のランタイムである wazero で実行するので、先ほどの課題の多くはここで解消されます。 ただし、C/C++ プロジェクトから WASM を作ること自体が難しく、アドレス空間の異なるホストとのブリッジを書くのも簡単ではありません。cgo なら文字列のアドレスと長さを渡すだけで済むところが、WASM ではリニアメモリ上に領域を確保し、そこにホスト側から書き込む必要があります。安全のために境界を引いた分、その境界をまたぐコードは自分で書くことになります。 wasmify と wasm2go スライド 24〜50 ページ 発表の後半は、この境界を越える手間を減らすために goccy さんが開発したツールの話でした。 wasmify は、C/C++ ライブラリから FFI 用途の WASM とブリッジコードを生成する手順を抽象化した、AI エージェントのハーネスとして利用できるツールです。プロジェクトごとに大きく異なるビルド手順の部分は AI に吸収してもらい、正規化した設定をファイルに残すことで、CI などでは AI なしで同じ結果を再現できるようにしています。非決定的な部分だけを AI に任せ、その結果を固定して冪等性を担保するという設計は、FFI に限らず AI を開発に組み込むときの考え方として参考になりました。 ところが、wasmify で作り直した Pure Go 版の bigquery-emulator(BigQuery のローカルエミュレータ)については、リリースの翌日からパフォーマンス低下の報告が相次いだそうです。あるケースでは 0.6 秒だった処理が 17.3 秒と、約30分の1の速度になっていたとのことでした。この問題への対処として開発されたのが wasm2go で、WASM を Go と Plan9 asm に変換してしまうというアプローチです。メモリが分離されているという WASM の性質は保ったまま、変換後は WASM 由来の実行時の制限を受けないため、最適化の余地が生まれます。 しかし、wasm2go にも課題はありました。たとえば、変換後の Go コードが巨大になるため、そのままではメモリ不足でコンパイルが通らなくなってしまいます。これに対しては、コンパイルの単位を分けて依存関係を直列にし、そこで生じる循環参照を go:linkname で回避するという解決策が取られていました。 go:linkname は、呼びたい関数が定義されている package を import せずにその関数を参照できるコンパイラディレクティブです。ほかの課題への対処も含め、どれも Go の処理系やツールチェーンの挙動を踏まえた解き方で、ここは聞いていて一番面白かったです。 最後に、wasm2go の成果物として go-googlesql や go-python、go-llama といった Pure Go 実装が公開されており、これらを Go から組み合わせて呼び出す Cross Language Binding や、Go のための Agent Sandbox といった応用先も紹介されました。 感想 goccy さんはほぼ毎回 go:linkname の話をされているそうです。発表の本筋ではないですが、今回も後半で循環参照の解消の文脈で登場しました。初めて goccy さんの発表を聞く機会を頂いたのは、今年2月の Go Conference mini in Sendai 2026 でしたが、当時は何のことか分からず困惑した状態で聞いたのを覚えています。 speakerdeck.com 今回はこの go:linkname が何かを知った状態で臨むことができたので、前回よりも楽しく拝聴させていただきました。人の話を聞いて知るきっかけをもらい、前よりも技術を楽しめるようになって、またそこで知らないことを知る。この繰り返しが起きるのが、カンファレンスという場所の素敵なところだなと思います。 goccy さん、興味深い発表をありがとうございました!そして、エブリーブースにも来ていただきありがとうございました!直接感想を伝えられて嬉しかったです! ワークショップ紹介 go.devの歩き方、その先へ 〜Go公式リソースの旅。明日からの調べ方を手に入れるワークショップ〜 講師: Koki Narumi さん(ANDPAD) レポート: 野村 こんにちは。開発1部でデリッシュキッチンのプレミアム機能を開発している新卒エンジニアの 野村 です。 私はワークショップ「go.devの歩き方、その先へ」に参加しました。 参加した理由 Go 歴は 4 ヶ月ほどです。 Go でわからないことがあったとき、AI に聞いて済ませることもできますが、自分の手で調べた方が理解は深まると感じていました。 AI の回答が間違っている可能性を頭に置きながら進めたり、その回答が正しいかを確かめたりするためにも、公式ドキュメントを調べる力があった方がよいと考えて参加しました。 ベテランの方々が公式ドキュメントをどのように読んでいるのかにも興味がありました。 ワークショップの流れ 当日は andpad-dev/gotan リポジトリの教材に沿って、次の流れで進みました。 リポジトリの説明 初級、中級、上級のレベルごとに分かれて 4 人の班を組む 班ごとに GitHub の Issue を 1 つ持ち、そこに自己紹介を書き込む 班でカテゴリとテーマを選ぶ 設問ごとに 2 人に分かれて調査し、調査結果を Issue に書き込んでいく 答え合わせ 私は Go 歴が浅いので初級を選びました。 カテゴリは次の 4 つから選びます。 Go の標準パッケージ Go の機能や文法構造 Go のコマンド Go の言語仕様、標準ライブラリの実装、設計の背景 私たちの班は「Go のコマンド」を選びました。 初級には 3 つのテーマがあり、その中から go run のテーマ を選びました。 テーマには設問が 3 つあり、私は設問2「go run でビルドされた実行ファイルや $WORK ディレクトリは、実行後どうなるか」を担当しました。 各設問にはヒントと答えが用意されていて、必要になったときに読める形になっています。 設問2 の調査 ここでは、設問2 をどう調べていったかを紹介します。 設問1と設問2はつながっているので、まず設問1にも目を通しました。 最初は手がかりがなかったため、答えに近づきすぎないようにヒントをざっと眺め、そこに書かれていたコマンドをまず実行してみることにしました。 go run -a -x main.go 2 > first.log この -a と -x が何をするフラグなのかを調べました。 調べ方は 03-cmd-tools の README にガイドがあり、それを見ながら進めました。 go help build でも読めますが、今回は pkg.go.dev/cmd/go をページ内検索して確かめました。 -a : すでに最新状態にあるパッケージも強制的に再ビルドする -x : 実行するコマンドを表示する first.log を開くと、1 行目に $WORK ディレクトリのパスが書かれていました。 WORK=/var/folders/bj/1bpczlgx0nxd7gyh6k1gxnhm0000gp/T/go-build2456729570 ここで $WORK に移動しようとしましたが、このディレクトリはすでに存在しませんでした。 cd: no such file or directory: /var/folders/bj/1bpczlgx0nxd7gyh6k1gxnhm0000gp/T/go-build2456729570 first.log の末尾を見ると、実行ファイルを cp している行があったので、そのコピー先に移動してみました( 班の調査ログ )。 cp $WORK/b001/exe/main /Users/yuto.nomura/Library/Caches/go-build/96/9677dc4b2f735a2ab8ac9be91e1f1c1ce061dcd1e81e39106ac0b016771e252e-d/main # internal $WORK/b001/exe/main コピー先には main の実行ファイルが残っていました。 つまり $WORK ディレクトリは削除されているが、実行ファイルはキャッシュとして残っている、ということになります。 次に、手詰まりだったのでもう一度ヒントを読みました。 次のようなヒントに注目しました。 go help build のフラグ一覧を眺め、一時ディレクトリに言及しているフラグを探してみましょう。見つけたフラグの説明文を読み、それが「デフォルトでは行われないことを追加で行う」フラグなのか、「デフォルトの動作を止める」フラグなのかを見分けてみましょう。 そこで、再び pkg.go.dev/cmd/go でフラグ一覧を調べました。 すると -work というフラグがあり、「一時作業ディレクトリの名前を表示し、終了時にそれを削除しない」と説明されていました。 この一時作業ディレクトリが、先ほどから調べていた $WORK ディレクトリです。 つまり、 -work を付けないと $WORK は実行後に削除されます。 そこで -work を付けて実行しました。 go run -a -work main.go 今度は $WORK ディレクトリに移動でき、中に実行ファイルがありました。 ここで時間がなくなり、答え合わせの時間になりました。 答え合わせの中で、 -a とキャッシュの関係を理解しました。 次の 2 つのコマンドを実行して比べます。 go run -a -work main.go go run -work main.go -a ありでは $WORK の中に中間ファイルと実行ファイルがありましたが、 -a なしでは $WORK の中は空でした。 答えの中で参照されていた Go 1.24 のリリースノート を読みました。 次のように書かれています。 Executables created by go run and the new behavior of go tool are now cached in the Go build cache. This makes repeated executions faster at the expense of making the cache larger. See #69290. つまり、Go 1.24 から go run でビルドした実行ファイルもビルドキャッシュに保存されるようになった、ということです。 -a を付けると $WORK の中でビルドが実行され、できた実行ファイルがキャッシュに残ります。 -a を付けない場合、ソースに変更がなければビルドを省略してキャッシュ済みの実行ファイルをそのまま実行するため、 $WORK は作られるものの中身は空になります。 以上から、設問2の答えは次の 3 点です。 何もフラグを付けない場合、 $WORK ディレクトリは一時的に作成され、実行が終わると削除される $WORK の中にあった実行ファイルは、ビルドキャッシュのディレクトリに保存される -work を付けると、 $WORK ディレクトリが削除されずに残る ワークショップで得たこと 今回のワークショップでは、 go help と pkg.go.dev を頼りに公式ドキュメントを読み進めることができました。 公式ドキュメントの読み進め方という点では、次のことを意識しました。 go help コマンドを初めて触り、その中身を理解するというステップを踏んだことで、公式ドキュメントを読むことへのハードルが少し下がりました。 公式ドキュメントは翻訳して読んでよいと知ったことでもハードルが下がり、調べやすくなりました。 そのうえで、ページ内検索で公式ドキュメントを検索しながら地道に読んでいくことを意識して取り組みました。 今回のテーマがコマンドツールだったので、コマンドツールの調べ方として意識したこともあります。 go run を実際に動かして観察し、フォルダやファイルが存在するかを確かめたり、フラグを付けて挙動がどう変わるかを見たりしました。 加えて、普段使っているコマンドツールはどのように動いているのだろうと自分なりの仮定を持ってみると、疑問が浮かび、より良い調査ができると感じました。 一次情報を押さえながら進めたので、理解が積み上がっていく感覚がありました。 一次情報にあたる経験に加えて、 go run の仕組みへの理解も深まり、他のコマンドも同じやり方で調べてみたくなりました。 今後 Go を学ぶときにも、この調べ方を使っていきたいです! Go × SIMDで高速化するベクトル検索 ~ ルーフラインモデルでSIMDが効く境界を探れ! 講師: Hiromu Nakamura さん レポート: 黒髙 こんにちは、開発本部の 黒髙 です。普段は デリッシュキッチン の開発に携わっています。 私は、 Hiromu Nakamura さんによるワークショップ「 Go × SIMDで高速化するベクトル検索 ~ ルーフラインモデルでSIMDが効く境界を探れ! ~ 」に参加しました。 このワークショップでは、GoでSIMDを使う方法に加えて、計測結果からボトルネックを見極め、次の高速化手法を選ぶ進め方を学びます。教材本編は、次の流れで構成されています。 ベクトル検索とSIMDの仕組み、Goでの使い方を知る 「ルーフラインモデル」で計算能力とメモリ帯域から性能の上限を考え、マシンの上限を測る スカラ版の全探索を基準として測る(Stage 0) 内積の計算をSIMDで高速化する(Stage 1) int8量子化でデータ量を減らす(Stage 2) 1bit量子化でさらにデータ量を減らし、速度と検索精度の変化を見る(Stage 3) 絞り込んだ候補を元のfloat32で再採点し、検索精度を回復する(Stage 4) 私はStage 2まで取り組みました。ここでは、SIMDの機能と、実際に確認できた高速化の効果を中心に紹介します。 資料と教材は以下で公開されています。 speakerdeck.com github.com 題材は、384次元のベクトル10万件から、クエリとの内積が大きい上位10件を探す処理です。ベクトル検索では、文章などを数値の並びで表し、その近さを使って検索します。 SIMDとは SIMD (Single Instruction, Multiple Data)は、1つの命令で複数のデータに同じ演算を適用するCPUの機能です。その働きを、今回のワークショップで扱う内積計算を例に見てみます。 内積は、2つのベクトルの同じ位置にある要素を掛け合わせ、その結果をすべて足す計算です。8要素のベクトルなら、次のようになります。 a = [1, 2, 3, 4, 5, 6, 7, 8] b = [2, 3, 4, 5, 6, 7, 8, 9] 内積 = 1×2 + 2×3 + 3×4 + 4×5 + 5×6 + 6×7 + 7×8 + 8×9 = 2 + 6 + 12 + 20 + 30 + 42 + 56 + 72 = 240 Goで素直に書くと、次のようになります。 var sum float32 for i := range a { sum += a[i] * b[i] } このループでは、 a[i] と b[i] を1組ずつ掛け、その結果を1つの変数 sum に足していきます。8要素なら、この処理を8回繰り返します。このように、演算で値を1要素ずつ扱うのが スカラ処理 です。 一方、8要素を扱えるSIMD命令なら、 1×2 から 8×9 までの掛け算をまとめて実行できます。掛け算の部分に注目すると、スカラ命令では8回かかるところを、SIMD命令なら1回で処理できます。掛け算命令の実行回数は1/8です。 SIMDで8組の掛け算を1命令で実行し、結果を合計して内積を求める模式図 図1:SIMDで8組の掛け算をまとめて実行し、その結果を合計して内積を求めます。 どちらも8組の掛け算を行いますが、SIMDではそれを1つの命令にまとめられます。SIMDでまとめて掛けた結果は [2, 6, 12, 20, 30, 42, 56, 72] という8個の値なので、内積を得るには最後にこれらを合計する処理が必要です。 このように、同じ演算を大量の要素に繰り返す処理で、1命令あたりに処理できる要素を増やせることがSIMDの便利な点です。今回の検索では、384要素の内積を10万件のベクトルに対して繰り返すため、SIMDによる高速化が期待できます。SIMDは1つのCPUコアの中でも利用できる仕組みです。 GoからSIMDを使う Go 1.27では、実験的なパッケージ simd/archsimd を使い、Goの型やメソッドでCPU固有のSIMD演算を扱えます。 archsimd 自体はGo 1.26で導入され、1.27ではAPIの改訂やarm64・WebAssemblyへの対応が追加されています。まだAPIは安定しておらず、ビルド時に GOEXPERIMENT=simd を指定して有効化します( Go 1.27リリースノート )。 CPUには、計算中の値を置く レジスタ という小さな記憶領域があります。今回のamd64向け実装では、256bit幅のSIMDレジスタを利用します。 float32 は1個32bitなので、1つのレジスタに8個の値が入ります。この1要素ぶんの区画を レーン と呼びます。 Goでは、この「32bitの値を8レーン」という形を archsimd.Float32x8 という型で表します。先ほどの図と同じく、8要素をまとめて扱えます。次のコードは、384要素ある a と b のうち、先頭8要素を処理する部分を示したものです。 var acc archsimd.Float32x8 // 8レーンとも初期値は0 va := archsimd.LoadFloat32x8(a) // aの先頭8要素を読む vb := archsimd.LoadFloat32x8(b) // bの先頭8要素を読む acc = va.MulAdd(vb, acc) // 各レーンでva×vbをaccへ足す LoadFloat32x8 は、スライスから8要素を読み込む関数です。 va と vb にはそれぞれ8個の値が入り、 acc にも途中結果をためる場所が8個あります。 va.MulAdd(vb, acc) では、各レーンで「 va の値 × vb の値 + acc の値」を計算します。 MulAdd は、掛け算と足し算をまとめて行うFMA(Fused Multiply-Add)に対応します。 Float32x8 では、この積和演算を8レーンまとめて1つの命令で行います( MulAddのドキュメント )。 a と b の読み込む位置を進めながら同じ処理を繰り返すと、各レーンに積和の途中結果をためていけます。最後にレーンごとの値を合計すると、内積が求まります。 このようなSIMD命令を、アセンブリやcgoを自分で書かずに、Goの型やメソッドを通して利用できるのが archsimd の特徴です。 ベンチマークで効果を確認する ここからは、冒頭で紹介したStage 0(スカラ版の計測)→Stage 1(SIMD化)の流れに沿って、実際の効果を見ていきます。まず make bench0 でスカラ版の全探索の速度を測り、続く make bench1 でスカラ版とSIMD版を比較しました。 計測には、ワークショップで用意された教材のGitHub Codespaces環境を使いました。実行環境はLinux / amd64で、CPUはAMD EPYC 7763でした。 教材には、内積単体を測るベンチマークと、10万件すべてとの内積計算から上位10件の選択までを含む、検索全体のベンチマークがあります。SIMD版も全件を走査する流れは同じで、内積の計算をSIMDに置き換えています。実装では途中結果をためる変数を acc0 と acc1 の2本に分け、互いの結果を待たずに計算できるようにしています( 教材の内積の実装 )。 8要素をまとめて計算できても、処理全体がそのまま8倍速くなるとは限りません。以下は、 make bench1 で内積単体と検索全体を計測した結果です。 計測対象 スカラ版 SIMD版 高速化の倍率 float32の内積 393.8 ns 58.71 ns 約6.7倍 10万件の検索 36.10 ms 9.76 ms 約3.7倍 内積単体では約6.7倍、検索全体でも約3.7倍の高速化を確認できました。 内積単体の改善が、そのまま検索全体の倍率になるわけではないことが分かります。検索ではベクトルの読み込みも必要で、計算だけを速めても読み込みが追いつかなければ、CPUはデータを待つことになります。こうした性能の上限を考えるために、スライドではルーフラインモデルが紹介されていました。 Go × SIMDで高速化するベクトル検索 ~ルーフラインモデルでSIMDが効く境界を探れ! ~ - Speaker Deck この図は、横軸が算術強度、縦軸が1秒あたりの演算回数で、屋根の形をした線が性能の上限を表しています。 算術強度は「演算回数/バイト」、メモリ帯域は「バイト/秒」なので、両者を掛けると「演算回数/秒」になります。これが、メモリからのデータ供給によって決まる性能の上限です。 線が折れ曲がる点(リッジ)より左側では、算術強度 × メモリ帯域 < 演算ピークとなります。CPUの計算能力より、データを供給する速さが低い上限を作るため、この領域の上限はメモリ帯域によって決まります。算術強度を上げると、同じ転送量でできる計算が増えて上限も上がるため、グラフは右上がりの斜線になります。 リッジより右側では、算術強度 × メモリ帯域 > 演算ピークとなるため、CPUの計算能力が性能の上限を決めます。算術強度をさらに上げてもこの上限は変わらないので、グラフは水平になります。実際の計測点がこの線より下にある場合は、SIMDなどで計算を効率化し、上限に近づけられる可能性があります。 int8量子化でデータ量を減らす メモリから読み込めるデータ量には1秒あたりの上限があるため、同じ件数のベクトルでもデータ量が少なければ読み込みにかかる時間を短くできます。この読み込み時間を減らして検索を速めるのが、Stage 2(int8量子化)です。 この段階では、 make bench-int8 でint8量子化を使った実装を計測しました。量子化は、値を少ないビット数で近似して表す方法です。float32をint8に変換すると、走査するベクトル本体は153.6 MBから38.4 MBになります。 検索全体の時間は、Stage 1のfloat32 SIMD版の約9.76 msから、int8 SIMD版の約4.00 msへ短縮され、さらに約2.4倍速くなりました。量子化によるデータ量の削減と、int8向けのSIMD計算を組み合わせた効果です。 int8の内積単体でも、スカラ版の412.8 nsからSIMD版の32.17 nsへ、約12.8倍の高速化を確認できました。 ただし、量子化は値そのものを変えるので、全探索をしても、検索結果が正解とずれる可能性があります。そのため、教材ではRecall@10(正解の上位10件と何件一致したか)という指標を用いて、検索結果の精度も検証していました。この評価では、元のfloat32版で得られた上位10件を正解としています。 ワークショップを体験して 今回のワークショップでは、Goの実験的なSIMDパッケージを使い、ベクトル検索を段階的に高速化する方法を学びました。ルーフラインモデルで計算能力とメモリ帯域の制約を考えながら、SIMDによる内積の高速化から、量子化によるデータ量の削減へと進む流れでした。資料では、さらに1bit量子化と再採点を組み合わせて、速度と検索精度の両立を目指すところまで紹介されていました。 Goのコードを動かして内積や検索が速くなる様子を確かめ、SIMDによる並列化の恩恵を感じることができました。さらに、量子化でデータ量を減らすといった高速化の手法にも、手を動かしながら触れられてよかったです。 まとめ Go Conference運営の皆さん、今年もカンファレンスを開催していただきありがとうございました! 昨年との違いとして気づいたのは、今回はGo Conferenceだけでなく「DroidKaigiで弊社を見かけた」、「iOSDCにも参加予定」といったエンジニアの方が多数みられたことです。 DroidKaigi のアンケートでもある通り、どの企業様のエンジニアも越境する意識があるのだなと感じさせられました。 今年もたくさんのGopherとお話したり、セッションを拝聴したりと充実した1日を送ることができました! Go Conference 2027もぜひ参加したいです! 非公式アフターイベント Go BASH Vol.3 のお知らせ ANDPAD、OPTiM、Resilire、エブリーの4社合同で、非公式アフターイベント Go BASH Vol.3 を開催します! 2026年9月30日(水) 19:30〜、会場はエブリー本社です。Go Conference 2026 の感想戦や各社のセッションを用意していますので、ぜひご参加ください! connpass.com 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
はじめに この度、株式会社エブリーは、2026年9月11日(金)〜13日(日)に開催される「iOSDC Japan 2026」に、ゴールドスポンサーとして協賛することになりました! 弊社はこれまで Go Conference や TSKaigi などに協賛してきましたが、今年は9月頭の DroidKaigi 2026 に続いて、iOSDC Japan にも初めて協賛します。初めての参加ということで、ブースでどんな方々とお話しできるのか、今から楽しみにしています。 本記事では、開催概要と弊社のブース企画、iOSDCチャレンジのトークン紹介、そしてアフターパーティーのご案内をお届けします。 iOSDC Japan 2026 とは? iosdc.jp iOSDC Japan は、iOS 関連技術をコアテーマとしたソフトウェア技術者のためのカンファレンスです。会場でのトークセッションやスポンサーブースに加えてオンライン配信もあり、iOS エンジニアが年に一度集まる大規模なイベントです。 今年の開催概要は以下のとおりです。 開催日時 Day 0: 2026年9月11日(金) Day 1: 2026年9月12日(土) Day 2: 2026年9月13日(日) 開催場所 有明セントラルタワーホール&カンファレンス 開催形態 オフライン / オンライン(ニコニコ生放送) ブース出展日程 2026年9月11日(金)〜9月13日(日)の3日間 ブース企画 弊社のブースでは、次の2つの企画を用意しています。開発本部のメンバーが現地に参加しますので、ぜひお気軽にお立ち寄りください! アンケートボード「iOSアプリ開発、AIにどこまで任せていますか?」 「iOSアプリ開発、AIにどこまで任せていますか?」をテーマにした参加型のアンケートボードを設置します。 実装からレビュー、テストまで、日々の iOS 開発のどこまでを AI に任せているのか、エンジニア歴とあわせて皆さんのリアルな声をお聞かせください。 集計結果は、イベント後の事後レポート記事で公開する予定です。シールを1枚貼るだけで参加できますので、セッションの合間にぜひ立ち寄ってみてください! 新しくなったデリッシュAI をブースで触れます! また、iOS版でリリースしたばかりのデリッシュキッチンの新しい「デリッシュAI」を、実機でデモ展示します! 新しいデリッシュAI は、チャットで相談するとレシピを提案してくれる機能です。たとえば「子供に人気のレシピ教えて」と聞くと、まず「ハンバーグ / カレー / から揚げ …」と選択肢で聞き返してくれて、「ハンバーグ」を選ぶと、子供向けのハンバーグレシピをすぐに提案してくれます。その下には「もっと簡単」「野菜入り」のような次の絞り込みや、「こんな質問もできます」の候補が並ぶので、文字を打たなくてもタップだけで会話が進みます! 技術的には、サーバー側の AI エージェントが「どの UI をどう並べるか」まで生成し、iOS アプリがそれを SwiftUI で描画する、いわゆる Generative UI の作りになっています。 「LLM に UI を任せてデザインは崩れないの?」「サーバーが返した UI を SwiftUI でどう描画してるの?」と気になった方は、ぜひブースへ! 実機を触りながら、iOS エンジニアが設計の中身までお話しします。 デリッシュAI は最新版のアプリでご利用いただけます。iOSDC までにぜひ一度、「今日の夕飯、何がいいかな」と聞いてみてください! iOSDCチャレンジのトークン紹介 iOSDC Japan では、会場をはじめ様々なところに散りばめられた「iOSDCトークン」を探し、見つけた数に応じて抽選券を獲得できる「iOSDCチャレンジ」が開催されます。集めた抽選券は、会場の抽選カウンターでノベルティが当たる抽選に使えます。 弊社もこの企画に参加しており、トークンは全部で3つ用意しました。トークンは「#」から始まるスペースを含まない文字列です。この記事ではそのうち2つを見出しとして掲載し、残りの1つはブースで公開します。 #AIファースト・カンパニー 弊社は「AI ファースト・カンパニー」を掲げ、AI を前提に組織や仕事の進め方を組み替えている最中です。 Claude の全社導入で個人の AI 活用は一気に進みました。ここからは、そこで生まれたツールやプロンプト、スキルを個人に閉じたままにせず共有資産へ育てる「個人から組織へ」を進めきることと、AI に任せられる範囲が広がるなかで人間の役割を意思決定と設計へ寄せていくことが、次のテーマです。 #誰でも簡単においしく作れる デリッシュキッチンが大切にしているのは、「誰でも簡単においしく作れる」レシピ動画を届けることです。 料理が得意な人だけでなく、料理に苦手意識のある人、忙しい日の夕飯に悩んでいる人、料理を始めたばかりの人にも「今日はこれ作ってみよう」と思ってもらえるように、レシピの選び方から作り方の見せ方まで、サービスの体験全体をこの言葉を軸に作り込んでいます。 残りの1つはブースで 3つ目のトークンは、弊社のブースに掲示しています。 アンケートボードやデリッシュAI の展示を見に来ていただいたついでに、ぜひ探してみてください。 アフターパーティーのご案内 DroidKaigi 2026 の協賛記事でもご案内しましたが、ゆめみ、フェンリル、Yappli、WealthNavi、セーフィー、エブリーの6社合同で「DroidKaigi & iOSDC After Talks Night 2026」を開催します! DroidKaigi 2026 と iOSDC Japan 2026 の合同アフターパーティーですので、iOS エンジニアと Android エンジニアが同じ場に集まり、モバイルエンジニア全体で技術交流や LT などを楽しめる会になる予定です。 開催日時 2026年10月2日(金)19:00〜21:00 開催場所 住友不動産麻布十番ビル アクセンチュア・イノベーション・ハブ 東京 開催形態 オフライン / オンライン コンテンツ ・各社の Android & iOS に関するセッション ・懇親会 詳細・お申し込みは以下からご確認ください。皆さんのご参加をお待ちしています! yumemi.connpass.com おわりに 初めての iOSDC Japan への協賛ということで、チーム一同、当日をとても楽しみにしています。 ブースでは、デリッシュキッチンの iOS 開発の裏側や技術スタック、AI 時代のアプリ開発についてのご質問や雑談も大歓迎です。「トークンを探しに来た」「新しいデリッシュAI の技術的な中身を聞いてみたい」「ちょっとエンジニアと話してみたい」くらいの軽い気持ちで構いませんので、ぜひ弊社のブースに足をお運びください。 有明の会場、そして10月のアフターパーティーで、皆さんとお会いできることを楽しみにしています! 最後までお読みいただき、ありがとうございました!
Go Conference 2026 に 今年はSilver スポンサーとして協賛いたします! はじめに この度、株式会社エブリーは、2026 年 9 月 11 日(金)に開催される「Go Conference 2026」に、Silver スポンサーとして今年も協賛することになりました! Go Conferenceとは? gocon.jp プログラミング言語 ”Go”ユーザーのためのカンファレンスです。今年はハイブリッド開催で、会場でのセッションやワークショップに加え、セッションのオンライン配信も予定されています! 今年の開催概要は以下のとおりです。 開催日時 2026年9月11日(金) 開催場所 東京都中野区中野4丁目10番2号 中野セントラルパーク サウス 1F / B1F 開催形態 オフライン / オンライン(セッションのみ配信) コンテンツ ・基調講演 ・セッション ・ワークショップ ・Official Party(懇親会) 昨年は Go 1.24 / 1.25 の新機能や言語内部を深掘りする発表が中心でしたが、今年はすでにGoコミュニティに参加している方、はじめてGoコミュニティに参加する方、みんなの学びや好きがより遠く深く広がるようなカンファレンスになるよう「 Go Far, Go Together 」というテーマが掲げられています。 また、プロポーザルの審査基準にも「自身の経験、知見に基づく、独自性のある内容であるか」とあるように今年の タイムテーブル を見ると独自性に富んだテーマが多く見られます。 例えば、海上で動くGoサーバー、Go におけるコンソールゲーム開発、9年のOSS保守で見た標準ライブラリとtestingの進化などです。 筆者個人としては、convto さんの「標準パッケージに uuid が追加された背景から見る Go らしい意思決定」が気になっています! イベント当日について エブリーのブースでは、料理に関するクイズや、ノベルティ・キッチングッズなどが当たるくじ引き、アンケートボードを設置しています! デリッシュキッチン 食クイズ ノベルティは弊社CTO慣習のオリジナルドリップコーヒー「CTO Blend」とステッカーをご用意しています! ノベルティ 当日は弊社の Go エンジニアも現地に参加しますので、会場でお見かけの際はぜひお気軽にお声がけください! 非公式アフターイベントのご案内 Go BASH Vol.3 ANDPAD、OPTiM、Resilire、エブリーの4社合同で、非公式アフターイベント Go BASH Vol.3 を開催します! 今回の会場はエブリー本社です! Go Conference 2026 の感想戦や各社のセッションなどのコンテンツを用意していますので、みんなで盛り上がりましょう! 開催日時 2026年9月30日(水) 19:30〜21:00(19:15 開場) 開催場所 東京都港区六本木3-2-1 住友不動産六本木グランドタワー38F 株式会社エブリー本社 開催形態 オフライン コンテンツ ・各社のGoに関するセッション ・Go Conference 2026 感想戦 ・交流会 お申し込みは以下で行っております。みなさんのご参加をお待ちしています! connpass.com 最後までお読みいただき、ありがとうございました!
Codespaces で開発環境を配布する はじめに エブリーでデリッシュキッチンの開発をしている本丸です。 毎年、エブリーでは夏季にインターンシップを行っているのですが、今年は今まで行っていた長期のほかに短期でのインターンシップも行っています。 そこで問題になったのが、参加者の開発環境をどうするかというものでした。 本記事では、GitHub が提供している Codespaces を利用してどのように開発環境を用意するのか、組織で Codespaces を利用する上で設定した項目についてお伝えできればと思います。 GitHub Codespaces とは GitHub Codespaces は、GitHub のリポジトリに対してクラウド上の開発環境を立ち上げ、ブラウザや手元の VS Code から接続して開発できるサービスです。Codespaces は Dev Container の仕組みの上で動いており、リポジトリに devcontainer.json を置いておくと、Codespaces はそれを読んで環境を組み立てます。 devcontainer.json とは何を決めるファイルか devcontainer.json は、「この開発環境はこう作る」という手順をリポジトリに置いておくための設定ファイルです。Codespaces 専用の形式ではなく、同じ定義をローカルの VS Code(Dev Containers 拡張)などで利用することができます。 { "name": "...", "build": { "dockerfile": "Dockerfile" }, "remoteUser": "node", "features": { "ghcr.io/devcontainers/features/github-cli:1": {} }, "postCreateCommand": "pnpm install", "forwardPorts": [3000] } build でベースになるイメージを決め、 features でそこに入っていないツール(ここでは GitHub CLI)を後乗せし、 postCreateCommand で作成後に流すコマンドを書き、 forwardPorts で見せるポートを指定する、という構成です。ほかにも docker compose を丸ごと指定したり、VS Code の拡張機能やエディタ設定を一緒に配ったりと、開発環境まわりのことは一通り設定できます。 起動方法や使い方のポイントなど 使い方 Codespace の作成は、 https://github.com/codespaces を開き、 New codespace から対象のリポジトリを選んで起動するのが基本の流れです。 codespace からの起動 Create codespace のボタンを押した後、数秒から数分待つと Codespace がブラウザで立ち上がり、そのまま開発を始められます。 devcontainer.json さえ置いてあれば、ほとんど設定不要で開発環境を作成できます。 Codespace の停止は、 https://github.com/codespaces から対象の「…」メニュー → Stop で行えます。 ポイント ポート ポートは forwardPorts と portAttributes を使って、ラベル付きで転送しています。 転送されたポート一覧 転送されたポートには、Codespace ごとの URL( https://<codespace 名>-3000.app.github.dev のような形式)が発行されます。たとえば Next.js の開発サーバーを 3000 番で動かしておけば、この URL をブラウザで開くだけで動作確認ができます。 転送されたポートには GitHub の認証がかかっているため、URL を知られただけでは開けません。 また、必要であればポートをローカルに転送することもできます。たとえばローカルの GUI クライアントから DB を見たい場合は、次のコマンドで 3306 番をローカルに持ってこられます。 gh codespace ports forward 3306:3306 Prebuild Codespace は何も設定しないと作成のたびにイメージのビルドから始まります。ビルドに時間のかかるリポジトリでは立ち上がりが遅くなるため、そうした場合は Prebuild を設定しておくと初回起動が速くなります。ビルド済みのイメージをあらかじめ GitHub 側に用意しておき、Codespace の作成時にはそれを取ってくるだけで済ませる仕組みです。 組織アカウントでの運用 最初の設定 組織アカウントに対して、次の設定が必要になります。 まず budget の設定です。 budget の設定 ここで設定した budget を上限として Codespaces を利用していくことになります。 次にアクセス権の設定です。 アクセス権の設定 どのメンバーが Codespaces を利用できるかの設定です。 加えて owner の設定と、必要に応じて policy の設定を行います。 所有権の設定 所有者の設定 ここでは組織所有かユーザー所有かを切り替え、課金対象のユーザーを指定します。 注意しなければならないのは、支出上限に達すると新規作成も起動もできなくなり、稼働中のものも停止されることです。イベント中に全員が同時に止まる事故になり得るため、上限の設定は慎重に行う必要があります。 コストの構造 課金される軸は3つだけですが、止まっていても課金され続けるものがあるのがポイントです。 項目 課金単位 課金されるタイミング compute コア時間(コア数 × 稼働時間) 稼働中のみ (Stop すれば止まる) storage GB-month 停止中も継続 (削除するまで止まらない) prebuild 生成は Actions 分数 / 保管は Codespaces storage 常時 (無効化するまで) 組織ポリシー 組織所有の Codespace に対しては、マシンタイプの制限、アイドルタイムアウトの最大値、保持期間、1ユーザーあたりの上限といった制約を設定できます。 リポジトリ単位のポリシー compute に関しては稼働中のみコストがかかる仕組みになっているので、アイドルタイムアウトを設定することで無駄なコストがかかることを防げます。 なお、ポリシーの項目にユーザー単位の上限を含めると、適用するリポジトリの選択ができなくなります。 ユーザー単位のポリシー 一定以上のスペックが必要な場合やコストを抑える目的などで設定をする場合が多いかと思います。 他メンバーの Codespace 利用の確認 ブラウザ上で見えるのは自分の Codespace だけです。組織オーナーであっても、他メンバーの Codespace を一覧する導線がありません。棚卸しや停止・削除は gh コマンドなどから行うことになります(admin 権限が必要で、対象は組織所有のものだけです)。 # 組織の Codespace を一覧(owner / 状態 / ブランチ / 作成日時) gh codespace list --org ORGANIZATION # 停止(compute 課金を止める。作業内容は保持される) gh codespace stop --org ORGANIZATION --user USER --codespace CODESPACE_NAME state が Available であれば稼働中(compute 課金中)、 Shutdown であれば停止中(storage のみ)です。 CLI から確認した Codespace の状態 その他 Prebuild の設定は組織自体の設定ではなく、個別のリポジトリごとの設定になっているので注意が必要です。 まとめ 組織で利用する場合は、課金の仕組みや所有権まわりで注意しなければならない点があります。とはいえ、 devcontainer.json を置いておくだけで、GitHub アカウントさえあれば誰でも同じ開発環境を作れるのは非常に便利で、環境構築に時間を取られることがありません。 実際、インターンシップ当日も概ね問題なく動きました。こうしたイベントに限らず、普段の開発でも使い道はありそうだと感じました。 参考文献 GitHub Codespaces とは GitHub Codespaces の課金について 組織内の Codespace を一覧する Codespace のマシンタイプ タイムアウト期間を設定する
はじめに 2026年に開催された DroidKaigi 2026 に、弊社の開発本部から 3 名のエンジニアが参加してきましたので、イベントの様子や印象に残ったセッションをご紹介します。 イベントの様子 スポンサーブース エブリーは今回、ゴールドスポンサーとしてブースを出展させていただきました! 足を運んでいただいた皆様、本当にありがとうございました! ブース企画 アンケートボード ブースでは、「AI時代!どこまで越境したいですか?」をテーマにした参加型のアンケートボードを実施しました! Android開発をベースにしつつ、バックエンドやPdM、データサイエンスといった他の領域へどのようにスキルを広げていきたいか、皆様のリアルな声を聞かせていただきました! 回答いただいた多くの皆様、ありがとうございました!最終結果はこちらです……! 2日間でいただいたシールは、合計およそ 400 枚。領域ごとの内訳は次のようになりました。 最も票が集まったのは「Android」でしたが、Backend と iOS がほぼ同数で並び、この3つが上位を分け合う形になりました。一方で全体を見ると、Android 以外の領域に貼られたシールは全体の 8 割弱。「Android を軸に据えつつ、その外側にも手を伸ばしていきたい」という方が多数派でした。 また、シールを貼っていただきながら、こんな声も聞かせていただきました。 モバイル領域が好きなので、クロスプラットフォームでやっていきたい コードは AI が書いてくれるので、プロダクトをどうグロースさせるかを考えられるようになりたい 「AI 時代にどう越境するか」という問いに対して、技術の横方向に広げていく方向と、プロダクトづくりそのものへ踏み込んでいく方向、その両方のリアルな声を伺うことができました。 ※シール数は写真からの集計のため、概算値です。 Xフォロー&くじ引き エブリー開発部の X アカウントをフォローいただくと、くじを1回引けるという企画も実施しました。 景品は、レンジ調理鍋・まな板・計量スプーン・しゃもじ・お箸など、普段の料理で使えるキッチングッズです。 ハズレの方にも、CTO 自らがテイスティングして選んだ「CTO ブレンド」のコーヒーをお渡ししていたので、くじを引いてくださった方には全員何かしらお持ち帰りいただけるようにしていました。 キッチングッズが当たった方に喜んでいただけて、こちらも嬉しかったです! またXをフォローいただいた皆様、本当にありがとうございました!X ではテックブログの更新情報も発信しているので、ぜひチェックしていただけると幸いです! ネイル体験会 会場ではプロのネイリストによるネイル体験会が開催されており体験してきました! 流れとしては、ネイルをする指を2本選び、それぞれのデザインを決めていくというもの。ベースカラーはネイリストの方と相談しながら決められるので、ネイルに詳しくなくても安心して選ぶことができました。 デザインは、DroidKaigi のキャラクター3種類とロゴの中から好きなものをチョイスできました。指先に DroidKaigi のキャラクターがいてくれるので、ふとした拍子に目に入るたび嬉しくなります。 他社のスポンサーブース REALITY さん REALITY さんは、AEP 対応に関するアンケートを行っていました! AEP (Apps Experience Program) は、Google が指定した要件を満たすと認定を受けられ、Google Play の新しい料金表の適用などの特典が得られるプログラムです。 Material3 は対応済み (80% 以上) の回答が多く、予想以上でした。一方でフルコンポーズ化は、まだ対応中・検討中という回答の方が多いようでした。 アーキテクチャも公開されていました。3D アバター以外の箇所はネイティブで作成されているとのことで、Unity を使っていると思っていたので驚きました。 BIZREACH さん BIZREACH さんは、AI が進化して楽になったことについてアンケートを行っていました! テストコードの生成やエラーの原因調査のような、コーディング業務の補佐的な立ち位置に留まらず、相談相手として活用している方が多く面白かったです。 晩ご飯の献立については、他と比べると少ないようでした。 デリッシュキッチン の出番のようです! エムスリー さん エムスリーさんは、毎年恒例の、プログラムのコードが印刷されたクリアファイルを配布されていました。 なんと去年よりコードが短くなっているとのことでした! クリアファイルの詳細については、昨年版のものになりますが エムスリーさんのテックブログ で公開されていますので、ぜひご覧ください! セッション紹介 なんとかする力 〜Androidエンジニアからマネージャー、さらにその先へ?〜 発表者: m.coder さん(フラー株式会社) レポート: 岡田 m.coder さんに、「目の前の課題を『なんとかする』の積み重ねが今の自分を作ってきた」という考えをもとに、キャリアとの向き合い方を語っていただいたセッションでした。 仕事のやりやすさは「何を・いつまでに・どこまでやるか」が決まっているかで大きく変わり、曖昧な箇所を明確にして不確実性を下げること自体が価値ある仕事だという話から始まりました。 印象的だったのは、役職が上がっていくにつれ、皆等しく抽象度の高い課題の解決を求められるというお話です。「なんとなくチームの雰囲気が悪い」「なんかプロジェクトの品質が悪い気がする」といった、課題かどうかすら曖昧なものを扱う必要があるという具体例に痛く納得しました。こういった漠然とした課題については、どうしても目を瞑りがちなので、自身のマインドセットを見直す必要があるなと痛感しました。 またテックリードとマネージャーは向き合い方が違うだけで、どちらも「自分以外の領域(チームや組織)をなんとかする」役割だという整理も面白かったです。 自分のキャリアを考えるうえで、抽象度の高い問題に立ち向かうべきという方針や、それを実現させる方法について非常に学びになりました。ご自身の経験から語られている箇所も多く、熱いメッセージをいただいた気持ちになりました。 また冒頭で『エンジニアリング組織論への招待』を紹介していただきました。1 章だけでも読む価値があるとのことなので、ネクストアクションとしてはこちらを読もうと思います。 あなたのANRはどこから? — 発生する仕組みを診断し、症状別に処方する 発表者: chomi さん(NRIネットコム株式会社) レポート: 岡田 会場が皆うなずいていたセッションだったように思えます。 メインスレッドについての解説を経て、まずは誰しもが経験したことがあるであろう、メインスレッドでの I/O についてのお話から始まりました。その後起動時の重い初期化、ロック競合と進みました。 起動時の重い処理については、特にレガシーコードを触ったことがある人なら対応したことがあるのではないかと思います。本当に Application で初期化すべきかを考えるというのは ANR 以外にも、パフォーマンスの観点から非常に重要です。例として FirebaseSDK の初期化について出ましたが、こちら誰しもがなんとかならないかなと調べたことがあると勝手に思っているので面白かったです。また固有端末依存や Binder 経由の呼び出し先での ANR などについても話があり、やはり皆さん困っているのだなと共感しました。 何より構成と見せ方が完璧だったと思います。スライドは要点だけが目に入る作りで、定義や例などもとても丁寧でしたので、スッと内容が入ってきました。終章の ANR 診断フローチャートについても綺麗にまとめられており、参考になりました。 発表での再現には、公開されているサンプルアプリ DorodoroTimer を用いたそうです。デモモードをONにすると上記の ANR が実際に発生し、コード内の [ANR-xx] マーカーから問題箇所と修正版を見比べられます。 AndroidにおけるServer-Sent Events: 工場の現場を生き抜くリアルタイムストリーム 発表者: Mr. Jasveen Sandral (Industrial Android, Toyota Group Japan) レポート: 鈴木 ( @0muji4_eng ) 本講演は、AndroidにおけるServer-Sent Events(SSE)を用いたリアルタイムストリーミング実装の課題と、その具体的な解決策について論じています。 Webブラウザとは異なり、Androidの標準的なライブラリ(OkHttpなど)にはSSEの自動再接続や状態管理の機能が不足しており、通信障害時にエラーを検知できず画面のデータがフリーズしてしまうエラーケースが存在します。講演者はこの事象を "The Trap of Silence" (沈黙の罠) と呼んでいました。この問題を克服するためには、サーバーに依存するのではなく、クライアント側(Android側)で堅牢な自己回復機能を持つ独自の仕組みを設計する必要性が生じます。 具体的には、サーバーからの定期的な通信(ハートビート)を監視してタイムアウトなどの切断を検知する仕組みや、厳密な状態管理(ステートマシン)の実装が不可欠です。あわせて、再接続時には最後に受信したID(Last-Event-ID)をサーバーへ送信することで、通信断絶中のデータ欠落を補完し、安全にストリーミングを再開する必要があります。 また、頻繁な双方向通信に適したWebSocketとの技術的な比較や、端末がオフラインになった際の適切なUI制御にも触れられています。最終的に、一方向のデータ監視システムにおいてSSEを有効に活用するには、サーバー側でのバッファリングといった設計だけでなく、クライアント側がいかにして通信の切断と復帰に耐えうるアーキテクチャを構築できるかが重要であると結論付けています。 WebAssembly in Android Apps 〜 WASMはJNIの夢を見るか 発表者: keiji_ariyama さん (C-LIS CO., LTD.) レポート: 鈴木 ( @0muji4_eng ) 本講演は、Androidアプリ開発において、C++などで書かれた既存のネイティブライブラリ(OpenJPEG など)を、WebAssembly(Wasm)を用いて安全に再利用するためのアーキテクチャ設計について論じています。 背景として、運転免許証やパスポート、マイナンバーカードなどに格納されている顔写真データ(JPEG 2000形式など)を読み込む際、従来のJNI(Java Native Interface)経由の直接実行では、悪意のある細工された画像データによって深刻な脆弱性を突かれ、アプリ全体が危険にさらされるリスクがありました。 この課題に対する実践的な解決策として、講演者は Wasm と Jetpack JavaScript Engine を組み合わせた多層防御(Defense-in-Depth)を提案しています。Wasmによってシステムコールを持たないメモリ隔離環境(第一層)を構築し、さらにJS Engineによってネットワークやローカルファイルへのアクセス権限を持たない別プロセス(第二層)として実行します。これにより、万が一デコーダーの脆弱性を突かれても、被害をサンドボックス内に完全に封じ込め、アプリ本体への影響を防ぐことが可能になります。 また、実装上の大きな障壁となる プロセス間のデータ転送コスト についても詳細な検証が行われています。文字列変換によるデータ受け渡しでは、プラットフォーム側にネイティブ実装が存在する Base64 を使用するのが最もパフォーマンスが高いことが実証されました。しかし現在では、JS Engine バージョン1.1.0で導入された MessagePort API を活用することで、バイナリデータの双方向通信が可能となり、エンコードのオーバーヘッドが劇的に解消されることが解説されています。あわせて、プロセス間通信の1MB容量制限も、RAM 上のファイルディスクリプターを介することで安全に回避できる点が示されています。 結論として、Wasm はメモリコピーが発生する点(ゼロコピー不可)やコードの隠蔽化に向かない点においてJNIとトレードオフの関係にあります。しかし、外部からの信頼できないデータを処理する要件においては、過去の優れたネイティブ資産を極めて安全にモバイル環境へ持ち込むための、非常に有効なベストプラクティスであると位置づけています。 まとめ 今年は例年と違い、 AI 関連のトピックが増加した印象です! ブースでは AI を用いた開発に関してのアンケートが多数見受けられました! セッションでは デバイス操作はAIエージェントの時代へ。mobile-mcpを活用したAndroid UI/E2Eテストの挑戦 や AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す のような AI を開発効率化に用いる内容から、 Google のオープンモデル Gemma を活用した最新の AI 開発のトレンド のような AI 開発そのものについてまで幅広く講演されており、時代の変化を感じました! またブースには本当に多くの方に足を運んでいただき、たくさんの人にエブリーを知っていただけて、とても良い機会でした! これからもデリッシュキッチン、エブリーのことをよろしくお願いいたします! 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv さらに、 DroidKaigi & iOSDC After Talks Night 2026 を、ゆめみ、フェンリル、Yappli、WealthNavi、セーフィー、エブリーの6社合同で開催いたします!なお、今回はiOSDC Japan 2026のアフターパーティーも兼ねているので、AndroidエンジニアだけでなくiOSエンジニアの方も交えて、プラットフォームの垣根を越えた活発な技術交流や情報交換をお楽しみいただけます。両カンファレンスの熱気をそのままに、各社によるLTセッションや懇親会をご用意しております。 項目 詳細情報 開催日時 2026年10月2日(金) 19:00 ~ 21:00 開催場所 東京都港区三田一丁目4番1号 住友不動産麻布十番ビル 開催形態 オフライン / オンライン コンテンツ 各社のAndroid & iOSに関するセッション / 懇親会 詳細や参加登録につきましては、以下のリンクよりご確認ください。 yumemi.connpass.com 最後までお読みいただき、ありがとうございました!
はじめに 株式会社エブリーは、2026年9月に開催される DroidKaigi 2026 にゴールドスポンサーとして協賛いたします。「エンジニアが主役のAndroidカンファレンス」である本イベントは、2026年9月1日(火)から3日(木)にかけて ベルサール渋谷ガーデン で開催されます。 項目 詳細情報 イベント名称 DroidKaigi 2026 開催日程 2026年9月1日(火)〜 9月3日(木) 会場 ベルサール渋谷ガーデン(東京都渋谷区南平台町) スポンサーランク ゴールドスポンサー ブース出展日程 2026年9月2日(水)〜 9月3日(木)の2日間 エブリーはこれまでも、 Go Conference 2025におけるPlatinum "Go"ld スポンサー や、 TSKaigi 2026におけるゴールドスポンサー としての参加など、技術コミュニティを積極的に応援してきました。今回のDroidKaigiへの協賛も、自社の開発現場で得られた知見をコミュニティに還元し、エンジニアの皆様と共に成長していくための大切な取り組みの一環です。 tech.every.tv tech.every.tv ブース出展:「AI時代!どこまで越境したいですか?」 9月2日および3日に出展するエブリーのブースでは、「AI時代!どこまで越境したいですか?」をテーマにした参加型のアンケートボードを設置します。Android開発をベースにしつつ、バックエンドやPdM、データサイエンスといった他の領域へどのようにスキルを広げていきたいか、皆様のリアルな声をお聞かせください。 また、 エブリーの公式X(旧Twitter) をフォローしていただいた方には、ハズレなしのくじ引きをご用意しています。お鍋や計量スプーン、まな板など、デリッシュキッチンならではの実用的なキッチングッズをプレゼントしますので、ぜひお立ち寄りください。 TSkaigiでもお配りした景品例 事後レポートを公開予定です! イベント終了後の9月3日(木)には、 every Tech Blog にて最速事後レポートを公開する予定です。開発部メンバーによるセッションの感想や、アンケートボード「AI時代の越境」の集計結果など、現場のリアルな熱量をお届けします。過去のイベント協賛時と同様に、オフラインで得られた知見をいち早くコミュニティに共有していきます。 アフターパーティーも開催します! さらに、 DroidKaigi & iOSDC After Talks Night 2026 を、ゆめみ、フェンリル、Yappli、WealthNavi、セーフィー、エブリーの6社合同で開催いたします!なお、今回はiOSDC Japan 2026のアフターパーティーも兼ねているので、AndroidエンジニアだけでなくiOSエンジニアの方も交えて、プラットフォームの垣根を越えた活発な技術交流や情報交換をお楽しみいただけます。両カンファレンスの熱気をそのままに、各社によるLTセッションや懇親会をご用意しております。 項目 詳細情報 開催日時 2026年10月2日(金) 19:00 ~ 21:00 開催場所 東京都港区三田一丁目4番1号 住友不動産麻布十番ビル 開催形態 オフライン / オンライン コンテンツ 各社のAndroid & iOSに関するセッション / 懇親会 詳細や参加登録につきましては、以下のリンクよりご確認ください。 yumemi.connpass.com おわりに 株式会社エブリーでは、技術コミュニティの発展を応援するとともに、開発現場で得た知見や知恵を共有し合う文化を大切にしています。今回の DroidKaigi 2026 への協賛を通じて、多くのエンジニアの皆様と技術やキャリアに関するお話ができることを楽しみにしています。 当日のブースでは、デリッシュキッチンをはじめとするプロダクト開発のリアルな話や技術スタック、AI時代におけるエンジニアの挑戦に関する雑談・ご質問も大歓迎です。「ちょっとノベルティのくじ引きをしてみたい」「エンジニアと軽く話してみたい」といった軽い気持ちで構いませんので、ぜひ気軽にエブリーのブースへ足をお運びください。 DroidKaigi 2026 の会場、そして10月のアフターパーティーで、皆様とお会いできることをチーム一同、心より楽しみにお待ちしております!
はじめに 前提:エージェントの構成 課題:画面を離れると回答が消える 設計方針:実行の作成と購読を分離する DB スキーマ:残す履歴と実行中の状態を分ける 保存単位は「AG-UI の Message 1 件 = 1 行」 DynamoDB ではなく Aurora MySQL を選んだ理由 会話履歴はサーバーが組み立てる 書き込み設計:イベントは保存せず「生成途中の回答」を上書きする 再合流:会話全体のスナップショットで追いつく 同時実行制御:実行中ロックを NULL 可のユニーク列で作る ロックの解放漏れに備える 停止:切断とキャンセルを区別する Redis は要るか まとめ はじめに 開発本部 開発1部の いくまる です。 私たちのチームでは、Web アプリの新機能として、チャット形式でデータを分析できる AI エージェントを開発中です。開発を進める中で、「回答の生成中に画面を離れると、その回答を受け取れなくなり、会話も残らない」という課題に向き合うことになりました。 本記事では、この課題を解決するために行った「会話履歴の永続化」と「バックグラウンド実行」の設計と実装を紹介します。DB スキーマ・実装コード・検討して捨てた案まで含めて書きます。 前提:エージェントの構成 このエージェントは次の構成で動いています。 ブラウザ(チャット UI) │ AG-UI イベント(SSE) ▼ Next.js(API Route ── ブラウザと AgentCore の中継役) │ InvokeAgentRuntime(SSE) ▼ Amazon Bedrock AgentCore Runtime(Strands Agents 製エージェント) │ MCP ▼ MCP サーバー(自社データの検索・集計ツール群) Amazon Bedrock AgentCore : AI エージェントの実行基盤となる AWS のサービスです。セッションごとに microVM 単位で実行環境が分離されます。 Strands Agents : AWS が公開しているオープンソースの AI エージェント SDK です。 AG-UI : エージェントとフロントエンド間のイベントストリーミングのプロトコルです。 RUN_STARTED ・ TEXT_MESSAGE_CONTENT ・ TOOL_CALL_* ・ RUN_FINISHED などのイベント型を定めています。転送方法は SSE に限定されませんが、このアプリでは SSE で流しています。 ユーザーが質問を送ると、エージェントが MCP ツールでデータを取得・分析し、回答をストリーミングで返します。ツールを繰り返し呼ぶため、1 回の回答に数十秒かかることがあります。 課題:画面を離れると回答が消える AI チャットで広く使われるのは「POST + ストリーミング応答」の構成です。ブラウザが質問を POST し、サーバーが生成イベントを流し、画面に逐次表示します。画面離脱を想定しなければ、これで十分に機能します。私たちの初期実装もこの構成でした。 私たちの場合は回答に数十秒かかるため、「生成中に画面を離れても実行は完走してほしい」という要件が加わりました。この要件を満たそうとすると、3 箇所が問題になります。 改修前の構造。画面遷移した瞬間に、以降のイベントを受け取る手段がなくなる 1 つ目はフロントエンドです。 この構成では、チャット画面の hook が unmount 時に実行を中断( abortRun() )する作りになりがちです。画面遷移がそのまま実行中断になります。 // チャット画面の hook(unmount 時に実行を中断する作り) useEffect( () => () => { stopRequestedRef. current = true ; agentRef. current ?.abortRun(); } , [] , ); 2 つ目は中継役の API Route です。 ブラウザと AgentCore の間で SSE を中継する Next.js の API Route は、クライアントの切断を上流の AgentCore への読み取りキャンセルとして伝播します。 ReadableStream の cancel() は「クライアントがもう読まない」ときに呼ばれるコールバックで、そこで上流の読み取りも止めると、切断とキャンセルの区別がなくなります。 // 中継処理(クライアントが切れると上流の読み取りも止まる作り) const readable = new ReadableStream( { async start ( controller ) { // 上流(AgentCore)の SSE を読み、そのままクライアントへ中継する } , cancel () { reader.cancel(); } , } ); 3 つ目は保存先です。 イベントは中継されるだけで、どこにも保存されません。仮に 1 つ目と 2 つ目を直して実行が完走するようにしても、戻ってきた画面に表示するデータがありません。 「実行状態を React のグローバルストアに持てば、画面遷移に耐えられるのでは」という案も検討しました。しかしこのアプリでは、チャット画面から他の画面への遷移が window.location.href によるページ全体の再読み込みで実装されています。再読み込み後のページは JavaScript の実行環境ごと新しく作られるため、React の state やグローバルストアに入れた値は引き継がれません。 設計方針:実行の作成と購読を分離する 大きく変えたのは次の 3 つです。 作成と購読の分離 : POST /runs は実行を開始して 202 { runId } を即座に返します。表示は GET /runs/{runId}/events の SSE で受け取ります。この「SSE を受信し続けること」を、本記事では「購読」と呼びます。購読はいつ切れてもよく、何度でも再開できます。 実行ワーカーの独立 : AgentCore の SSE を最後まで読み切って記録する処理(実行ワーカー)を、HTTP レスポンスから独立した非同期タスクにしました。ブラウザが切断しても実行は完走します。 二層の保存 : 実行中は「生成途中の回答」を DB に上書き保存し続け、完了したら完成したメッセージを DB の履歴テーブルに保存します。テーブル構成は次の節で説明します。 なお、実行ワーカーは Next.js と同じプロセス内で動かしているため、リクエストごとに実行環境が終了するサーバーレス環境ではこの形は取れません。現在は検証段階のためこの構成にしていますが、Next.js のデプロイに走行中の実行が巻き込まれないようにするため、本来は実行ワーカーを独立したプロセスに切り出す方が望ましいと考えています。現状、プロセスがデプロイなどで止まる場合の後始末は、同時実行制御の節で説明する回収の仕組みが担います。 改修後の構造。実行は接続と無関係に完走し、購読は何度でも再入場できる ブラウザとサーバーの間の API は次の 5 本です。 API 役割 POST /runs 実行を作成して 202 { runId, conversationId } を即返す GET /runs/{id}/events SSE 購読。切断・再入場が自由 POST /runs/{id}/cancel 明示的なキャンセル GET /conversations 会話一覧(履歴サイドバー用) GET /conversations/{id} 会話の全メッセージ + 実行中の run(あれば) 最後の API がポイントです。リロードや別タブで会話を開いた直後、クライアントは会話 ID しか知らず、実行中の run があるかどうかも分かりません。そこで GET /conversations/{id} は、会話のメッセージに加えて「実行中の run の ID」を返します。クライアントはその ID で GET /runs/{id}/events を購読し、生成途中から表示を再開します。 DB スキーマ:残す履歴と実行中の状態を分ける このアプリでは以前から、本体機能のデータを Aurora MySQL 8.0 + Prisma で管理しています。エージェントの履歴も同じ DB に、3 つのテーブルで持つことにしました。ずっと残す「履歴」と、実行中だけ使う「実行状態」でテーブルを分けています。 区分 テーブル 役割 行の扱い 履歴 conversations 会話スレッド 1 件のメタ情報 ずっと残す 履歴 conversation_messages メッセージ 1 件 = 1 行。完成した発話を保存 ずっと残す 実行状態 runs 実行 1 回の状態 + 生成途中の回答 + ロック 行は実行 1 回ごとに増え、終了後も記録として残る。生成途中の回答やロックは実行中だけ使う ER 図 Prisma スキーマは次の通りです。実際に採用したものから、タイムスタンプ列・リレーション定義・enum 定義(RunStatus / MessageStatus)を省いています。 model Conversation { id String @id @default(cuid()) companyId Int @map("company_id") userId String @map("user_id") threadId String @map("thread_id") @db.Char(36) title String @db.VarChar(255) deletedAt DateTime? @map("deleted_at") @@unique([companyId, userId, threadId]) @@map("conversations") } model ConversationMessage { id String @id @default(cuid()) conversationId String @map("conversation_id") runId String? @map("run_id") sequence Int role String @db.VarChar(16) parts Json status MessageStatus @default(complete) @@unique([conversationId, sequence]) @@map("conversation_messages") } model Run { id String @id @default(cuid()) conversationId String @map("conversation_id") clientTurnId String @map("client_turn_id") @db.VarChar(64) status RunStatus @default(queued) lockKey String? @unique @map("lock_key") ownerInstanceId String? @map("owner_instance_id") @db.VarChar(64) errorCode String? @map("error_code") @db.VarChar(64) partialState Json? @map("partial_state") heartbeatAt DateTime @default(now()) @map("heartbeat_at") @@unique([conversationId, clientTurnId]) @@index([status, heartbeatAt]) @@map("runs") } lockKey の UNIQUE、 clientTurnId の複合ユニーク、 [status, heartbeatAt] のインデックスがそれぞれ何のためにあるかは、後の節で順に説明します。 また、本記事には 4 種類の ID が登場します。ここで整理しておきます。 ID 何を指すか conversationId DB 上の会話。API で会話を指すときに使う threadId AG-UI 上の会話 ID。DB 上の会話( conversationId )と 1 対 1 で対応 runId 質問 1 回ぶんの実行 runtimeSessionId AgentCore の実行環境を束ねる ID。 t{companyId}-u{userId}-{threadId} の形式で、会話ごとに固定 保存単位は「AG-UI の Message 1 件 = 1 行」 conversation_messages は追記専用で、AG-UI の Message をそのまま parts (JSON)に格納します。テキストだけのターンは user / assistant の 2 行、ツールを使うターンは assistant(ツール呼び出し)と tool(結果)の行が挟まって 4 行以上になります。 1 会話のメッセージ行の例。ツールを使うターンは user・assistant(ツール呼び出し)・tool・assistant の 4 行、使わないターンは 2 行になる この保存単位を選んだ理由は、フロントの表示ロジックの作りにあります。ライブ表示は「AG-UI の Message 配列を受け取り、ターンの区切りやツールの実行ステップ表示を組み立てる純粋関数」として自前で実装しています。保存した Message 列をそのままこの関数に渡せば、画面を離れなかった場合と同一の表示が再現されます。保存時に表示用の形へ加工してしまうと、同じ表示を再現できなくなります。 DynamoDB ではなく Aurora MySQL を選んだ理由 会話履歴のアクセスパターン(会話 ID + 連番で順に全件取得、追記専用、JSON 主体)は DynamoDB の得意領域で、実際に移行案も検討しました。それでも Aurora MySQL 一本にしています。 まず、トランザクション要件が構成の選択肢を絞ります。質問の受付時には「会話 + user メッセージ + 実行(ロック)の INSERT」を、完了時には「assistant メッセージの INSERT + 実行の完了 + ロック解放」を、それぞれ単一トランザクションで行う必要があります。「履歴は DynamoDB、実行状態は Aurora」のように 2 つのストアに分けると、この原子性を保証できません。原子性が無いと、たとえば次のような壊れ方をします。 回答は残ったのに次の質問ができない : 完了処理の「回答を保存」と「ロック解放」の間でプロセスが落ちると、画面には回答が出ているのに DB はロックを握ったままになり、次の質問が「実行中です」と拒否され続けます。 答えのない質問が履歴に残る : 送信の二度押しで 2 本目が「質問を保存 → ロックで弾かれる」の順に進むと、誰も回答しない質問だけが履歴に残ります。1 トランザクションならロック取得の失敗と同時に質問の保存も取り消され、エラー応答だけを返せます。 したがって選択肢は「全部 Aurora」か「全部 DynamoDB」に絞られます。後者も技術的には成立します(DynamoDB でも TransactWriteItems で複数の項目をまとめて原子的に書けます)。 それでも Aurora にしたのは、既存の運用との一貫性のためです。このアプリの他のデータはすべて Aurora + Prisma で管理していて、マイグレーションの手順やレビューの観点といったチームの運用もそこで揃っています。データストアを 2 つにすると、この運用も 2 系統になります。規模の面でも、DB への書き込みはピークでも毎秒数十回の見積もりで、Aurora で十分に受けられます。DynamoDB のスケール性能が必要になる水準ではありません。 会話履歴はサーバーが組み立てる エージェントは毎回の呼び出しで会話の全履歴を受け取り、状態をゼロから組み立て直す作りにしています。この全履歴を誰が用意するかには 2 つの形があります。クライアントが手元の Message 配列を毎回送るか、 サーバーが DB から組み立てる かです。私たちは後者にしました。クライアントが送るのは新しいメッセージ 1 件だけです。 前者を避けた理由は、バックグラウンド実行と相性が悪いからです。実行を放置して別のタブで完走させると、元のタブが持っている履歴は古いままになります。その古いタブから次の質問を履歴ごと送ると、完走したはずの回答がモデルへの入力から抜け落ち、会話のつじつまが合わなくなります。後述するロックは実行中しか効かないため、この事故は防げません。最新の会話を常に持っているのは DB だけです。 なお、AgentCore 側に会話の状態を持たせる案も 2 つ検討し、見送りました。 実行環境(microVM)のメモリに持つ : セッション ID は会話ごとに固定なので、同じ会話の呼び出しは同じ実行環境に届き、メモリに状態を残すこと自体はできます。ただしこの環境は無操作 15 分(デフォルト)などで終了し、メモリごと消えます。時間を空けて続く会話の置き場にはできません Memory サービスに持つ : AgentCore には会話を保存する Memory というサービスもあります。ただし履歴はどのみち表示のために自前の DB へ保存するので、足すと同じ役割の保存先が 2 つになります POST /runs のボディは { conversationId, message, clientTurnId } だけです。モデルへ渡す履歴は、実行ワーカーが Aurora から組み立てます。 // 実行ワーカーの一部:DB から会話履歴を読み出し、モデル入力用に整える export async function buildModelMessages ( conversationId : string ): Promise < ModelMessagesResult > { const rows = await prisma.conversationMessage.findMany( { where : { conversationId , status : "complete" } , orderBy : { sequence : "asc" } , select : { parts : true } , } ); return normalizeHistory(rows); } // 実行ワーカーの一部:組み立てた履歴をエージェントに渡して生成を開始する const history = await buildModelMessages(claimed.conversationId); const { companyId , userId , threadId } = claimed.conversation; const agent = new AgentCoreAgent( { threadId , initialMessages : history .messages, agentArn , runtimeUserId : `c ${ companyId } :u ${ userId } ` , runtimeSessionId : `t ${ companyId } -u ${ userId } - ${ threadId } ` , } ); こうすると、会話の内容はサーバー(DB)だけが持つ構造になります。AgentCore の microVM がタイムアウトで終了しても、クライアントがリロードで状態を失っても、会話は Aurora から再構成できます。 書き込み設計:イベントは保存せず「生成途中の回答」を上書きする 1 回の回答で AG-UI イベントは数十〜数百個流れます。本文の断片(デルタ)1 つ 1 つやツール呼び出しがそれぞれイベントになるため、回答が長いほど増えます。これを 1 行ずつ INSERT すると、1 回答ごとに大量の行が永久に積み上がります。採用したのは次の形です。 時点 DB 操作 内容 質問送信 INSERT conversations に会話(新規会話のときのみ)、 conversation_messages に質問、 runs に実行レコード(ロック取得を兼ねる)の最大 3 行 生成中 runs.partial_state を上書き UPDATE AG-UI のイベントでは本文が細切れ(デルタ)で届く。実行ワーカーはそれをつなぎ合わせた「その時点のメッセージ配列」を保持しており、これを数秒ごとに同じ 1 行へ上書き保存。行は増えない 完了 conversation_messages に INSERT そのターンで生まれたメッセージ(回答、ツールを使った場合はその呼び出しと結果も)を保存。 runs.partial_state を空にし、ロックを解放 質問送信時のトランザクションは以下のようになっています。ロック( lockKey )の取得と質問の保存が、同時に成立するか同時に失敗するかのどちらかになります。 // API Route の一部:質問受付時の書き込み return prisma.$transaction( async ( tx ) => { const conversationId = existingId ?? ( await tx.conversation.create( { /* 省略 */ } )).id; // ロック取得に失敗したら、下で保存する質問ごと取り消される(同一トランザクションのため) const run = await tx.run.create( { data : { conversationId , clientTurnId , lockKey : conversationId } , select : { id : true } , } ); // aggregate(集計クエリ)で会話内の最大 sequence を取り、次の連番を振る const highest = await tx.conversationMessage.aggregate( { where : { conversationId } , _max : { sequence : true } , } ); await tx.conversationMessage.create( { data : { conversationId , runId : run. id , sequence : (highest._max.sequence ?? 0 ) + 1 , role : "user" , parts : { id : randomUUID(), role : "user" , content : message } , } , select : { id : true } , } ); return { runId : run. id , conversationId } ; } ); 生成中の partial_state は 3 秒間隔で間引いて書きます。間引きに加えて「前の UPDATE が完了するまで、次の UPDATE を発行しない」という制御も入れています。UPDATE を発行した順と DB に反映される順は一致するとは限らないため、古い内容の UPDATE が新しい内容の後に適用されると、保存済みの「生成途中の回答」が巻き戻ってしまうからです。 // 実行ワーカーの一部:生成途中の回答を数秒ごとに DB へ上書き保存する const PARTIAL_STATE_INTERVAL_MS = 3_000 ; return { schedule() { // 前の書き込みが完了するまで次をスケジュールしない(古い内容への巻き戻りを防ぐ) if (timer || writing || truncated) return ; timer = setTimeout (() => { timer = null ; writing = true ; void writePartialState(runId, ownerInstanceId, produced()) . then (( state ) => { truncated = state.truncated; } ) . catch (( error : unknown ) => console .error( "partial_state の更新に失敗しました" , { runId , error } ), ) . finally (() => { writing = false ; } ); } , PARTIAL_STATE_INTERVAL_MS); } , } ; 数秒おきに UPDATE を発行し続けて DB の負荷は大丈夫なのか、という点は検討しました。結論としては、同時に走る生成が多くても数十本という規模では問題になりません。更新は各実行が自分の 1 行だけを主キー指定で行い、実行間のロック競合はありません。さらに、接続中のユーザーの画面へは実行ワーカーがメモリ上のイベントを直接流すため、 partial_state の用途は後述する再合流だけです。毎秒書く必要も、イベントを 1 個ずつ書く必要もありません。 完了時は「回答の確定保存」「実行ステータスの完了への更新」「ロック解放」「生成途中の回答( partial_state )の削除」を 1 トランザクションで行います。 // 実行ワーカーの一部:完了時の書き込み。lock_key を外し損ねるとその会話が永久に 409 になる return prisma.$transaction( async ( tx ) => { const claimed = await tx.run.updateMany( { where : terminableWhere(runId, ownerInstanceId), data : { status , errorCode , finishedAt : new Date (), lockKey : null , partialState : Prisma.DbNull, } , } ); // 別の経路(キャンセルや、後述する異常終了時の回収処理)が先にこの実行を // 終わらせていたら、生成物は保存しない if (claimed. count === 0 ) return false ; await tx.conversationMessage.createMany( { data : messages. map (( message , index ) => ( { conversationId : targetId, runId , sequence : base + index + 1 , role : message.role, parts : message as Prisma.InputJsonValue , status : messageStatusAt( status , message. id , openMessageIds), } )), } ); return true ; } ); メッセージの status は通常 complete で保存します。キャンセルやエラーで実行が正常に終わらなかった場合は、そこまでに生成できていた分を partial(部分的、の意味)として保存し、画面に残せるようにしています。 再合流:会話全体のスナップショットで追いつく 実行中の会話に購読者が入ってくると、サーバーはまず RUN_STARTED (「実行が進行中です」の合図)と MESSAGES_SNAPSHOT を送ります。どちらも上流から届いたイベントの中継ではなく、この購読のためにサーバーが新しく作って送るものです。 MESSAGES_SNAPSHOT を受け取ったクライアントは、手元のメッセージ一覧を捨てて、スナップショットの内容で丸ごと置き換えます。そのため、スナップショットに生成途中の 1 件だけを入れると、過去のメッセージが画面からすべて消えてしまいます。必ず会話の全メッセージを入れて送ります。 その後の配信は 2 つのモードに分かれます。分かれ目は「購読がいつ始まったか」です。 モード いつ使われるか 配信内容 live 質問の送信直後から購読している場合(生成イベントがまだ 1 件も流れていないうちに購読が始まったとき) 実行ワーカーが受け取る生成イベント( TEXT_MESSAGE_CONTENT など)を、メモリからそのまま逐次中継 poll それ以外すべて(リロード・別タブ・離脱して戻ってきた場合) 1 秒間隔で DB を読み、確定済み履歴と partial_state の生成途中回答をマージした会話全体の MESSAGES_SNAPSHOT を、内容が変わったときだけ送り直す。実行が終わったら RUN_FINISHED (または RUN_ERROR )で締める つまり、途中から戻ってきた購読者が受け取るのは live 配信のイベント列ではなく、「会話全体のスナップショットの送り直し」です。 // 配信モードの選択。イベントが 1 件でも流れた後に始まった購読は poll に回す export function attach ( runId : string , signal : AbortSignal ): LiveSubscription | null { const fanout = fanouts. get (runId); if (!fanout) return pollInstead(runId, "fanout_absent" ); if (fanout. closed ) return pollInstead(runId, "fanout_closed" ); // 1 件でも中継済みなら列の途中からになるので、履歴を出せる poll に任せる if (fanout.relayed > 0 ) return pollInstead(runId, "already_relayed" ); // 省略(購読者を登録し、生成イベントを流す AsyncGenerator を返す) } // poll 配信のループ。会話全体のスナップショットを、内容が変わったときだけ送り直す while ( true ) { const event = snapshot(stored, readPartialMessages(progress.partialState)); const serialized = JSON . stringify (event); if (serialized !== previous) { previous = serialized; yield event; } if (isTerminal(progress. status )) break ; await sleep(POLL_INTERVAL_MS, signal); progress = await readProgress(run. id ); if (isTerminal(progress. status )) stored = await loadConversationMessages(run.conversationId); } yield terminalEvent(run, progress); 途中合流の購読者を live のイベント列に合流させず poll に回すのは、正しさを優先したためです。デルタの続きから流すには、「スナップショットに含めた分」と「これから流すデルタ」の境界を厳密に合わせる必要があります。境界がずれると、 content += delta の積み上げで本文が二重に連結されます。会話全体のスナップショットを送り直す形なら、毎回が丸ごとの置き換えなので、この事故が原理的に起きません。その代わり、poll 配信の画面は live 配信のようなストリーミング表示にはならず、数秒おきに文章がまとまって進む表示になります。途中合流でもストリーミング表示にすることは、後続の課題にしています。 同時実行制御:実行中ロックを NULL 可のユニーク列で作る 「同一会話に実行中の run は 1 本だけ」を DB で強制します。PostgreSQL なら 部分インデックス (partial index。 CREATE UNIQUE INDEX ... WHERE status IN ('queued','running') のように、条件を満たす行だけへ一意制約をかけられます)で書けますが、MySQL 8.0 には相当する構文が用意されていません。 代わりに runs.lock_key (NULL 可・UNIQUE)を使いました。実行中は lock_key = conversationId 、終了時に NULL へ戻します。MySQL のユニークインデックスは NULL を重複として扱わない ため、終了済みの run は何本でも共存でき、実行中は会話ごとに 1 本に絞られます。 同一会話への 2 本目の POST /runs は、INSERT 時にユニーク制約違反のエラーとして原子的に弾かれます。重複には 2 種類あります。1 つは「同じ送信の二度押し」です。クライアントは送信 1 回ごとに ID( client_turn_id )を発行し、リトライでも同じ ID を送るため、この列の重複で検出できます。送信ボタンの連打はフロントでも抑止できますが、ネットワーク不調時の自動再送などフロントの制御では防げない経路が残るため、DB でも守ります。もう 1 つは「別の質問の並行送信」( lock_key の重複)で、同じ会話を複数のタブで開いているときに起きます。どちらだったかを引き直して、応答を分岐します。 // API Route の一部:重複キーエラーの解釈 } catch (error) { if (!isUniqueViolation(error)) throw error; // 二度押しなら、先行の run をそのまま返す(同じ送信は 1 回として扱う) const raced = await findRunByClientTurnId(params); if (raced) return { ok : true , runId : raced. id , conversationId : raced.conversationId } ; // 並行送信なら、実行中の run を添えて拒否へ const activeRunId = await findActiveRunIn(conversationId); if (activeRunId) return { ok : false , reason : "active_run" , activeRunId } ; } // API Route の一部:並行送信への応答 if (result.reason === "active_run" ) { return Response .json( { errors : "この会話はいま実行中です" , activeRunId : result.activeRunId } , { status : 409 } , ); } 409 のレスポンスに activeRunId を含めているのは、UI がそれを使って「拒否」ではなく「実行中の run への購読切り替え」に変換できるようにするためです。 ロックの解放漏れに備える ロックには解放漏れへの備えも必要です。サーバーのプロセスが突然落ちると、running のままロックを握った run が残ります。そうなると、誰も実行していないのにその会話への質問が「実行中です」と拒否され続けます。備えは 2 つの仕組みの組み合わせです。 実行ワーカーは、実行中の run の heartbeat_at を定期的に現在時刻へ更新します(処理が続いていることの記録です) それとは別の掃除処理が、 heartbeat_at の更新が一定時間止まっている queued / running の run を「担当プロセスが異常終了した」とみなして failed にし、ロックを解放します。 heartbeat_at は INSERT 時に現在時刻が入るため、202 を返した直後・実行が始まる前にプロセスが落ちて queued のまま残った run も、この経路で回収されます(スキーマの @@index([status, heartbeatAt]) はこの検索用です) 「サーバー起動時に、残っている running を全部 failed にする」というより単純な方法は採れませんでした。デプロイ中は新旧のサーバーがしばらく同時に動いており、旧サーバーがまだ実行している最中の run を、新サーバーの起動処理が誤って failed にしてしまうためです。run に owner_instance_id (どのサーバーがその実行を担当しているか)を持たせているのも同じ理由です。掃除処理は、自分のサーバーがいま実行している run を誤って回収しないよう、この ID とメモリ上の実行一覧を突き合わせて判定します。 この回収の仕組みは、デプロイやスケールインでプロセスごと止められた場合の後始末も兼ねています。止まったプロセスが抱えていた実行は途中から再開できませんが、heartbeat が途絶えるため数分以内に failed になり、そこまでの生成分は partial として履歴に残り、会話のロックも解放されます。ユーザーは失敗を確認して、すぐ次の質問に進めます。 停止:切断とキャンセルを区別する この設計では、タブを閉じる・画面を遷移するのは「切断」であり、実行は継続します。明示的に止めたいときは POST /runs/{id}/cancel を呼びます。実行ワーカーにキャンセル要求の印を立てて上流への購読を切り離し、実行を「キャンセル」として記録します。記録後に遅れて届いた生成物は、前述の完了時トランザクションの「別の経路が先に終わらせていたら保存しない」分岐で破棄されます。 // API Route の一部:キャンセル処理 if (getActiveRun(runId) || run.ownerInstanceId === OWNER_INSTANCE_ID) { // 終了の記録は実行ワーカーに任せる。ここで書くと、ワーカーが「先に終了済み」と判定して生成物を捨てる const active = registerRun(runId); active.cancelRequested = true ; await active.agent?.detachActiveRun(); return { ok : true } ; } // 別のサーバーが担当している run。会話のロックを解放するために記録だけ書く await finalizeRun( { runId , status : "cancelled" , finalizedBy : `cancel: ${ OWNER_INSTANCE_ID } ` } ); 実行中の会話への追加送信は、前述のロックにより 409 で拒否されます。ただし 409 を返すだけだと、「戻ってきたら画面が止まって見える → もう一度送る → エラー」という流れになりやすいため、途中経過の可視化(再合流)を初回リリースの範囲に含めています。実行中であることが見えていれば追加送信は起きにくく、方向を変えたい場合も「停止してから送る」導線に誘導できます。 Redis は要るか 同種の設計では、実行中イベントの共有に Redis(Redis Streams)を使う構成がよく知られています。ただし Redis が必要になるのは「タスクを 2 つ以上に増やし、かつストリーミング表示を保ちたい」場合です。今回はどちらにも当てはまらないため、入れていません。 現在は ECS 1 タスクで動かしており、通常時は再接続のリクエストが実行ワーカーと同じプロセスに届きます。イベントはプロセス内のメモリで手渡せるため、Redis なしで live 配信が成立します。 タスクを 2 つ以上に増やすと、購読のリクエストが実行ワーカーのいない方のタスクへ届くことがあります。live 配信はワーカーと同じプロセスのメモリを介して成り立っているため、別のタスクに届いた購読では使えません。ただし DB はどのタスクからも読めるので、poll 配信はそのまま動きます。つまり live 配信できたはずの購読が poll 配信になり、ストリーミング表示が数秒おきの更新になるだけで、履歴も途中経過も見られます。設計方針の節で触れた「実行ワーカーを独立したプロセスに切り出す」場合も、ワーカーと購読者が必ず別プロセスになるため、同じく中継が必要になります。当面は 1 タスクで足りる規模のため、現時点ではこの構成にしています。 まとめ 実行の作成と購読を分離し、実行ワーカーを HTTP 接続から独立させることで、画面を離れても実行が完走する構造にしました。 履歴は「メッセージ 1 件 = 1 行」でずっと残し、生成途中の回答は上書き更新の 1 行に分けました。イベントの逐次保存はせず、モデルへ渡す履歴もサーバーが DB から組み立てます。 同時実行制御は MySQL の「NULL 可ユニーク列」によるロックで実現しました。キャンセルは購読の切り離しと、実行を「キャンセル」として記録することで実現し、切断とは明確に区別しています。 AI チャットの「履歴」と「バックグラウンド実行」は別々の機能に見えますが、作ってみると、どちらも「会話の状態はサーバー側で持つ」という同じ設計に行き着きました。AI チャットの実行基盤を作る際に共通して現れる論点だと思うので、同じものを作る方の参考になれば幸いです。
目次 はじめに Partial Prefetchingは何を解決するのか すべてを先読みする方法と、何も先読みしない方法の中間を作る OISHYでPartial Prefetchingを試す 検証条件 計測方法 実験1:クリック前の取得を減らすと、通信と遷移はどう変わるか 実験2:URL固有のデータを待つ間に何が見えるか 補足:適用範囲によって先読みの形が異なった まとめ 参考文献 はじめに こんにちは、開発本部の 黒髙 です。最近はアメリカ向けレシピサービスである OISHY の開発に関わっています。 Next.js 16.3 では、ページ遷移の応答性を改善する仕組みとして Instant Navigations が追加されました。クリック前に再利用できるUIやデータを準備し、クリック後に必要な部分を取得することで、遷移直後から次のページを描画しやすくする仕組みです。 Instant Navigationsを構成する機能のうち、今回取り上げるのは Partial Prefetching です。本記事では、Partial Prefetchingを紹介したうえで、OISHYの既存ページでは通信と画面遷移にどのように現れるのかを検証します。 Partial Prefetchingは何を解決するのか すべてを先読みする方法と、何も先読みしない方法の中間を作る Next.jsの <Link> によるprefetch(先読み) では、リンクが画面内に入ると、ユーザーがクリックする前に遷移先のデータを取得します。クリック時にデータが揃っていれば、遷移先をすぐに表示できます。 一方、リンクが多い画面では、実際にはクリックされないリンクのデータも取得します。先読みを無効にすれば事前通信はなくなりますが、その場合はクリックしてから遷移先のデータを取得することになります。 Next.js 16.3のPartial Prefetchingは、この二つの中間を作る機能です。Cache Componentsを使うページへの既定の <Link> では、URLごとの完成形をすべて先読みする代わりに、同じ種類のページで再利用できるApp Shellを先読みします。クリックされたURLに固有のデータは、必要になった時点で取得します。 App Shell とは、URL固有のデータを待たずに表示できるページの共通部分です。サイト共通のレイアウトや、データの読み込み中に表示するUIなどが含まれます。 レシピ詳細ページを例にすると、レシピ名や材料はURLごとに変わりますが、それらを待っている間の枠組みは複数のレシピで共有できます。 同じルートを指す複数のリンクで一つのApp Shellを再利用できる ため、リンクごとに同じ共通部分を取得し直す必要もありません。 この仕組みは、次のようなページで効果が現れやすいと考えられます。 商品、記事、レシピなど、同じ種類の詳細ページへのリンクが多数並ぶ 表示されたリンクのうち、実際にクリックされるのは一部だけである 詳細ページに、共通レイアウトやデータ取得中の表示など、URLをまたいで再利用できる部分がある OISHYのトップページにも、同じ /recipes/[id] へ遷移するレシピリンクが多数並びます。そこで今回は、レシピ詳細ページをPartial Prefetchingへ切り替えたとき、クリック前の通信とクリック後の表示がどう変わるかを確かめました。 OISHYでPartial Prefetchingを試す OISHYでは、すでに Cache Components を有効にしています。まず、レシピ詳細ページへの遷移にPartial Prefetchingを適用するため、次の設定を追加しました。 // app/recipes/[id]/page.tsx export const prefetch = 'partial' ; 公式リファレンスでは、 prefetch = 'partial' はリンクではなく遷移先に設定する ものと説明されています。この設定により、レシピ詳細ページをPartial Prefetchingへ段階的に切り替えます。 検証条件 主な検証では、次の3条件を比較しました。 条件 Next.js Partial Prefetchingを適用する場所 データ取得中の表示 条件A 16.2.11 適用しない なし 条件B 16.3.1 レシピ詳細ページ なし 条件C 16.3.1 レシピ詳細ページ スケルトン表示 実験1では条件Aと条件Bを比較し、クリック前の取得を減らしたときに通信と画面遷移がどう変わるかを確認しました。実験2では条件Bと条件Cを比較し、URL固有のデータを待つ間にApp Shellが画面にどう現れるかを確認しました。 以下で扱う条件B・Cと補足検証の先読み通信は、Next.js 16.3.1で観測したものです。内部的な通信の形は、今後のバージョンで変わる可能性があります。 OISHYの既存構成では、Next.jsの standalone server をECSで動かし、その前段にCloudFrontとALBを置いています。今回の計測もこの配信経路で行いました。 計測方法 対象 :同じ10件のレシピを、それぞれ3回ずつ計測 操作 :トップページを新しいタブで開き、対象リンクが画面内に入る位置までスクロール。5秒待ってからクリックし、クリック前の先読み通信とレシピタイトルが表示されるまでの通信・時間を記録 ブラウザ条件 :画面サイズは1280×900、回線速度の制限はなし。ブラウザ側のHTTPキャッシュは計測のたびに無効化し、スクロール位置とクリックまでの待ち時間を統一 集計 :各レシピの3回の中央値を求めたあと、10件の中央値を代表値として使用。通信量にはChrome DevTools Protocolの Network.loadingFinished.encodedDataLength を使い、ブラウザが受信したデータ量に近い値を比較 実験1:クリック前の取得を減らすと、通信と遷移はどう変わるか 最初に、Next.js 16.2.11の条件Aと、16.3.1へ更新してレシピ詳細ページをPartial Prefetchingへ切り替えた条件Bを比較しました。この比較にはNext.js自体の更新も含まれるため、Partial Prefetchingだけの効果ではなく、OISHYで16.3.1への更新と機能導入を行った前後差として扱います。 ここでいう RSC(React Server Components)データ は、Next.jsがクライアント側の画面を更新するために送るデータです。 指標(中央値) 条件A(変更前) 条件B(レシピ詳細ページに適用) クリック前のRSCリクエスト 75 6 クリック前の転送量 約319KB 約20KB クリック後のRSCリクエスト 0 1 クリック後の転送量 0KB 約20KB クリック前後の合計リクエスト 75 7 クリック前後の合計転送量 約319KB 約38KB クリック前のリクエスト数は約92%、転送量は約94%減りました。クリック後には、選択したレシピのRSCリクエストが1件発生しています。それを含めた合計転送量も約88%減っており、クリック前の通信がそのまますべてクリック後へ移ったわけではありませんでした。 変更前は、画面内に並ぶ多数のレシピについてURL固有のRSCデータを取得していました。変更後は、共通部分を先読みし、選択したレシピのデータをクリック後に取得しています。今回の画面では、クリックされなかったレシピへの先読みが減ったことが、通信量の差として大きく現れました。 次の画像は1回の計測例で、表の数値は反復計測から求めた代表値です。 条件Bでのクリック前のNetwork記録の例 一方、クリックからレシピタイトルが表示されるまでの代表値は、変更前が約44ms、変更後が約82msで、どちらも100ms未満でした。URL固有のデータをクリック後に取得するようになったことと整合しますが、今回の条件では目視できるほど長い待ち時間にはなりませんでした。大きく変わったのは、完成画面が出るまでの見た目よりも、クリック前に取得するデータの量でした。 なお、この結果は回線速度を制限しない環境でのものです。Partial Prefetchingは、クリック前の転送量を減らす代わりに、URL固有データの取得をクリック後へ移す仕組みです。低速な回線では、クリック後の待ち時間が今回より長くなる可能性がある一方、クリックされないリンクへの先読みを抑える効果は大きくなるため、有効に働くかどうかは通信環境によってトレードオフになり得ます。 実験2:URL固有のデータを待つ間に何が見えるか Partial Prefetchingでは、URL固有のデータがクリック時点で揃っていなければ、クリック後にデータの取得を待つ時間が生じます。その間にApp Shellがどのように現れるかを見るため、条件Cではレシピデータの取得中に表示するスケルトンを loading.tsx として定義しました。 loading.tsx 自体はNext.js 16.3の新機能ではありません。同じルート階層のページを Suspense 境界で囲み、定義したUIをデータの準備中に表示する仕組みです。Partial Prefetchingでは、 この代替表示を含むApp Shellが先読みの対象になります 。 // app/recipes/[id]/loading.tsx export default function RecipeDetailLoading () { return < RecipeDetailSkeleton /> ; } スケルトンとは、完成後のレイアウトに近い枠を先に表示し、データを待っている場所を示すUIです。今回のレシピ詳細ページでは、次のスケルトンがApp Shellとして先に表示される部分になります。 レシピ詳細ページのスケルトン表示例 30回中25回はスケルトンを経由せず、完成したレシピ詳細が直接表示されました。スケルトンが先に現れたのは5回で、クリックから14〜22msで表示され、レシピタイトルより約68〜856ms先行しました。 この結果から、スケルトンは毎回挟まる中間画面ではなく、レシピ固有のデータがクリックまでに揃わなかった場合だけ、遷移先の枠組みとして先に表示されることが分かりました。 また、条件Cの通信を追加で確認すると、条件Bとは異なる挙動が見つかりました。条件Bではレシピ固有のデータをクリック後に取得していた一方、条件Cの補完計測では、10回中4回で複数のレシピ固有データをクリック前に取得するRSC通信が並びました。 条件Cでクリック前に観測したレシピ固有データの先読み この違いがPartial Prefetchingの適用範囲と関係するのかを確認するため、追加計測しました。 補足:適用範囲によって先読みの形が異なった 条件Cでは、レシピ詳細ページだけに prefetch = 'partial' を設定していました。 prefetch = 'partial' は、設定したページより上位のルート階層まで同じ設定にするものではありません。 Next.js 16.3.1の実装 では、それぞれのルート階層が、その階層で指定された設定かアプリ全体の既定値を参照します。 この挙動を調べる中で、 段階導入時の先読みを扱うNext.js 16.3.1の公式テスト を見つけました。このテストにも、動的なページだけをPartialにし、未設定の上位階層を従来の方法で扱う構成で、リンク先固有のデータまで先読みする例があります。 そこで、条件Cの loading.tsx とページ側の設定を残したまま、 アプリ全体の既定値をPartialにする partialPrefetching: true だけを追加しました。 // next.config.ts const nextConfig = { cacheComponents : true , partialPrefetching : true , } ; 同じ導線を10回計測した結果は次のとおりです。 Partial Prefetchingの適用範囲 URL固有データの先読み 共有部分の先読み レシピ詳細ページのみ 10回中4回で観測 あり アプリ全体 10回中0回 あり 同じ loading.tsx を残したまま適用範囲を変えると、今回の導線ではURL固有データの先読みが10回中4回から0回になりました。一方、App Shellを構成する共有部分の先読みは続いていました。Next.js 16.3.1の実装と公式テストを合わせると、今回観測した通信にはPartial Prefetchingの適用範囲が関係していたと考えられます。 まとめ Partial PrefetchingをOISHYのレシピ一覧で試したところ、16.3.1への更新と機能導入後、クリック前のRSC転送量が約94%減り、クリック後を含む合計でも約88%減りました。 一方、変更前からレシピタイトルまでの表示は約44msと十分に速く、回線速度を制限しない今回の環境では、通信量ほど大きな見た目の差はありませんでした。スケルトンを含むApp Shellは、URL固有のデータがクリックまでに揃わなかった場合だけ、完成したページより先に表示されました。 また、レシピ詳細ページだけに適用した場合と、アプリ全体に適用した場合では、クリック前の先読みも異なりました。ページ単位で試せる機能ではありますが、今回の検証では適用範囲も実際の通信に関係していました。 Partial Prefetchingは、商品・記事・レシピの一覧のように、同じ種類の詳細ページへのリンクが多く、その一部だけがクリックされる画面を想定しています。クリックされないURL固有データの先読みを抑えながら、データが間に合わない場合にはApp Shellを先に表示する仕組みです。今回のOISHYでは、先読み通信量には大きな差が出た一方、見た目の差は小さいという結果でした。 参考文献 Next.js 16.3 Next.js 16.3: Instant Navigations Instant Navigationガイド Adopting Partial Prefetching <Link> のprefetch(先読み) Cache Components Server and Client Components output: 'standalone' prefetch のルート設定(Next.js 16.3.1) 同じルートでApp Shellを再利用する説明(Next.js 16.3.1) partialPrefetching の全体設定(Next.js 16.3.1) loading.tsx とSuspense境界(Next.js 16.3.1) loading.tsx の代替表示を先読みする説明(Next.js 16.3.1) ルート階層ごとの先読み設定を扱う実装(Next.js 16.3.1) 段階導入時の先読みを確認する公式テスト(Next.js 16.3.1) Chrome DevTools Protocol: Network.loadingFinished
title はじめに こんにちは、株式会社エブリーでデリッシュキッチンのiOSアプリの開発をしている成田です。 現在は、プレミアムユーザーの登録数の向上やプレミアムユーザーの体験をより良くすることを目的としたチームで開発をしています。 サブスクリプションの解約は、これまで開発者にとってブラックボックスでした。ユーザーが App Store の管理画面で「サブスクリプションをキャンセルする」を押すとき、アプリ側にできることは何もありません。引き止めのメッセージも、オファーの提示も、そもそも解約されようとしていることを知ることさえ、その瞬間にはできませんでした。 WWDC26 で発表された Retention Messaging は、ここに初めて介入手段を与える機能です( セッション309 )。解約確認画面に、アプリからのメッセージやオファーを差し込めるようになります。 Appleによれば、この機能を導入したサブスクリプションでは、解約を取りやめて利用を継続するユーザーの割合が改善されているとのことです。解約抑止にも取り組む立場としては、無視できない機能です。 まだ一般提供はされていませんが、具体的に何ができるのか、どう始めればよいのか、現時点で分かっていることを整理します。 何ができるのか ユーザーが App Store のサブスクリプション管理から解約しようとすると、確認画面が出ます。Retention Messaging を設定しておくと、この画面にアプリからのコンテンツが表示されます。 表示できる形式は3つです。 形式 内容 メッセージのみ ローカライズ済みの引き止め文言 メッセージ + 画像 テキストメッセージに Asset Library 等から設定した画像を添えて訴求 メッセージ + オファー 割引や無料期間付きなどのオファーを提示する ユーザーがオファーの対象である場合、オファーの表示が画像を置き換えます。「解約する前に、3ヶ月無料で続けられるオファーがあります」のような画面を、Apple の解約フローの中に出せるわけです。この画面は App Store 側が描画するもので、公式ドキュメントによれば iOS 15.1 以上で利用できます。アプリの最低対応バージョンと関係なく届くのは、地味に嬉しいところです。 オファーが引き換えられたかどうかはサーバー側で確認できます。署名付きトランザクションに新しいオファー種別が入ります。 { " offerType ": 5 , " offerIdentifier ": " Yoga_2026_cancel_free_3m ", " offerDiscountType ": " FREE_TRIAL ", " offerPeriod ": " P3M " } offerType はトランザクションに「どの種類のオファーが絡んだか」を刻むフィールドで、これまで4種類あったものに追加で Retention Offer が加わった形です。 offerType 種類 1 お試し 2 プロモーション 3 オファーコード 4 再獲得 5 Retention Offer メッセージを用意する方法は2つある ここまでが主に「解約確認画面に何が出るか」の話です。次は、その表示をアプリ側がどう用意するかです。 方法は2つあって、手軽さが大きく違います。App Store Connect で設定するだけの方法と、自前のサーバーを立てて顧客ごとにリアルタイムで出し分ける方法です。順に見ていきます。 方法1: App Store Connect 側での設定 App Store Connect 上でメッセージ・画像・オファーを設定し、対象のサブスクリプションにマッピングするだけです。自前のサーバー実装は不要で、Apple 側が表示を担います。 流れはこうです。 ローカライズ済みのメッセージ文言を作る 任意で Asset Library の画像、Retention Offer を添える 1つ以上のサブスクリプションにマップする Sandbox 環境でテストして公開 方法2: リアルタイム API 型(ユーザーごとの出し分け) 方法1の弱点は、全員に同じものしか出せないことです。 新しく発表された Retention Messaging API を使うと、「誰に・何を出すか」を自社のデータで決められるようになります。 誤解しやすい点を先に書いておくと、 解約の理由そのものが Apple から届くわけではありません 。リクエストに入っているのは「誰の契約か( originalTransactionId )」までです。ただ、この ID で自社のユーザーデータを引けば、手持ちの情報が使えます。例えば、購読してからどれぐらいか、最後にアプリを開いたのはいつか、月額プランか年額プランか、過去にオファーで引き止めたことがあるかなどがあるでしょう。 理由そのものは分からなくても、こうしたデータから仮説は立てられます。 出し分けのロジックが自前のサーバーにあることで A/B テストでの検証も可能になるはずなので、その仮説を検証することもできそうです。 次に、仕組みを見ていきます。 Retention Messaging API を使うと、解約操作が起きたまさにその瞬間に、App Store から自前のサーバーへ問い合わせが来ます。 // App Store からのリクエスト { " originalTransactionId ": " 123456789 ", " appAppleId ": 6745974591 , " productId ": " Yoga_summer_2026 ", " userLocale ": " en-US ", " requestIdentifier ": " c03248af-dd76-4e9b-9c1e-4489cd19a768 ", " environment ": " Production ", " signedDate ": 1780920000000 } これに対して、 このユーザーに何を出すか を返します。返せる応答は3種類です。 ① メッセージ { " message ": { " messageIdentifier ": " 551ee7c0-... " } } ② プラン切替の提案(alternateProduct) { " alternateProduct ": { " messageIdentifier ": " ed7f25fc-... ", " productId ": " Yoga_summer_2026_annual " } } 同じサブスクリプショングループ内の別プランへの乗り換えを提案できます。「月額×12ヶ月コミット」の新プランタイプとも連動していて、たとえば年額プランを解約しかけた人に「月額の12ヶ月コミットなら続けやすいですよ」という導線が作れたりしそうです。 ③ プロモーショナルオファー { " promotionalOffer ": { " messageIdentifier ": " 80135e2b-... ", " promotionalOfferSignatureV2 ": " eyJhbGciOiJFUzI… " } } プロモーショナルオファーは、開発者が特定のユーザーだけに提供できる限定オファーです。 対象ユーザー以外には利用されないように、開発者のサーバーが秘密鍵を使って「このユーザーに、このオファーを適用してよい」という署名を発行します。アプリはこの署名を使って、オファーが正しく発行されたものかを確認します。 promotionalOfferSignatureV2 は、このデジタルな許可証を標準的な形式である JWS(JSON Web Signature) で表現する新しい仕様です。 ところで、ここまでの応答例が messageIdentifier という ID しか返していないことに気づいたでしょうか。メッセージの文言や画像の実体は、あらかじめ Apple に登録しておく設計になっています。解約フローの真っ最中に文言ごと送るのではなく、実体は事前登録しておいて、その場では「どれを出すか」を ID で選ぶだけです。 この事前登録を担うのが、管理用のエンドポイント群です。メッセージや画像の登録のほか、リアルタイム問い合わせの受け口 URL の設定、性能テストの実行もここで行います。 POST /messages // メッセージ登録 GET /messages // 登録済み一覧 DELETE /messages/{messageId} // 削除 POST /images // 画像登録 GET /images DELETE /images/{imageId} POST /defaultMessages // デフォルトメッセージ設定 GET /defaultMessages/{productId}/{locale} DELETE /defaultMessages/{productId}/{locale} POST /realtimeUrl // 受け口URLの設定 GET /realtimeUrl DELETE /realtimeUrl POST /performanceTests // 性能テスト GET /performanceTests/{testId} フォールバックは段階的 リアルタイム応答が使えない・不正な場合は App Store Connect で設定されたものに、それも無ければ API で設定したデフォルトメッセージにフォールバックされます。 また、Sandbox には自社サーバーの応答性能を測るためのテスト用エンドポイントが用意されています。解約フローの中で同期的に呼ばれる API なので、応答が遅ければ体験を壊します。本番前にここで確認しておく、という建て付けだと理解しています。 まとめ Retention Messaging は、解約確認画面という最後の接点に初めて介入できる機能 用意する方法は2つ。サーバー不要の App Store Connect 設定型と、ユーザーごとに出し分けるリアルタイム API。フォールバックも整理されている 返せるのはメッセージ、プラン切替提案、プロモオファーの3種 メッセージや画像の実体は事前登録しておき、リアルタイム応答では ID で選ぶだけ。Sandbox には応答性能のテスト用エンドポイントも用意されている 解約はこれまで、起きてから初めて知るものでしたが、今回からは解約を思いとどまってもらうための施策を、解約のタイミングに合わせて出せるようになります。サブスクリプションを運営しているチームは、サービス提供が始まる前に、どんなメッセージを出すか、どんなオファーを用意するかを検討しておくとよさそうです。
Cloudflare Walletsが使う決済プロトコル x402 を理解する 目次 はじめに x402が対象とする課題 プロトコルフロー ② 402 Payment Required と PaymentRequirements scheme が取る値 — exact と upto ③ authorization への署名 ④ PaymentPayload の送信 ⑤ verify / settle と facilitator 決済の確定と不可逆性 HTTPを利用する構成のメリット 支払ってよいかの判断は仕様の範囲外 まとめ 参考 はじめに こんにちは、開発本部開発1部の 赤川 です。食事管理アプリ ヘルシカ の開発をしています。 ダイエット・食事管理・体重管理・カロリー計算 - ヘルシカ every, Inc. ヘルスケア/フィットネス 無料 2026年8月4日、Cloudflareが Cloudflare Wallets を発表しました。これは、Web上でサービスの購入や入金を行うためのウォレットで、Cloudflareのアカウントに紐づきます。現時点ではウォレットの識別子の予約のみが提供されており、僕もとりあえず予約してみました。 Cloudflare Wallet Tagを予約した Cloudflare Walletsでは、アカウントが持つWalletから、エージェントが使うVirtual Walletに支出権限を委任できます。エージェントはVirtual Walletを使って、API、MCPツール、コンテンツなどを購入できます。 この購入のやり取りには、 x402 という決済プロトコルが使われています。x402は、HTTPのリクエストとレスポンスの中だけで完結し、支払いが必要であることを示すのに、HTTPステータスコードの 402 Payment Required 1 を使っています。 本記事ではこの x402 について、ドキュメントをあたりながらまとめてみました。 x402が対象とする課題 例えば有料APIを利用しようと思ったら、我々は一般に次の手順を踏みます。 サービスにサインアップする クレジットカードなどの決済手段を登録する APIキーを発行する そのキーをリクエストに付けて呼ぶ 月末にまとめて請求される 認証と課金がフローとして完全に分離しており、いずれも事前のサインアップを前提としています。 人間が利用する分には何の問題もないですが、クライアントがAIエージェントである場合、この前提が制約になります。手順1〜3のうち、サインアップページは人間向けのUIとして作られており、決済手段の登録も人間の承認が必要です。エージェントは事前登録された識別子も支払い手段も持たないため、この3手順の実行には人間の介在が必要になります。 x402は、事前の登録手続きなしに支払いを成立させる方式を定義しています。 プロトコルフロー プロトコルのフローは次のとおりです。この章の図や記述は、 x402 Specification v2 と Cloudflare の x402 ドキュメント に基づいており、執筆時点で最新のv2について書いています。 x402のプロトコルフロー このフローには、アカウントやセッション、事前に共有したAPIキーなどは含まれません。ClientとServerはこのリクエストで初めて通信し、支払いが成立します。 ※ 実際の支払いは、ブロックチェーン上でトークンを送ることで成立します。USDCなどのステーブルコイン(米ドルなどの法定通貨に価値を連動させたトークン)が使われます。 ② 402 Payment Required と PaymentRequirements ①のリクエストに対して、Serverは②で 402 Payment Required を返します。支払いの条件は PAYMENT-REQUIRED ヘッダに、Base64エンコードされたJSONとして載ります。 このJSONの accepts フィールドに、支払いの条件が配列で入ります。要素1つが「この条件で払ってくれれば、このリソースを渡すよ」という1件分の提示にあたり、これを PaymentRequirements と呼びます。配列なのでServerは条件の異なる複数の選択肢を並べられ、Clientはその中から1つを選びます。 PaymentRequirements は次のフィールドから構成されます。 フィールド 内容 scheme 支払いスキーム( exact / upto ) network 対象ブロックチェーン(Base、Ethereum、Solana など) amount 金額 asset トークン種別(USDC など) payTo 支払いの宛先アドレス maxTimeoutSeconds タイムアウト scheme が取る値 — exact と upto scheme フィールドは支払い方式を指定します。 Cloudflare のドキュメント に記載されているのは次の2つです。 exact : 固定額の送金です。EVM(Ethereum Virtual Machine) 2 上では USDC を payTo のアドレスに送金します。②の時点で金額が確定しているケースに対応します。 upto : 上限額を先に承認し、実際の課金額を決済時に確定します。LLMの推論APIのように、実行するまでトークン数が確定せず、金額が事後に決まるケースに対応します。 ③ authorization への署名 Clientは、 accepts の中から採用する PaymentRequirements を1つ選び、その内容を authorization オブジェクトに反映して署名します。authorization の形は、 scheme とチェーンの種類の組み合わせで決まり、EVM上で exact を使う場合は次のフィールドを持ちます。 フィールド 内容 from 送金元アドレス to 宛先アドレス value 金額 validAfter / validBefore 有効期間 nonce 一度きりの値 署名は自身の秘密鍵で行います。署名が保証するのは、その鍵の保有者にしか生成できないこと、署名対象が1バイトでも変われば検証に失敗すること、の2点です。 nonce は一度きりの値であり、 validBefore で有効期間が区切られるため、同じ署名を他の支払いに再利用することはできません。 ④ PaymentPayload の送信 署名と authorization は PaymentPayload にまとめられ、 PAYMENT-SIGNATURE ヘッダに載って、同じURLに再送されます。 PaymentPayload は次の構造です。 { " x402Version ": 2 , " resource ": { ... } , " accepted ": { /* ②の accepts から選んだ PaymentRequirements */ } , " payload ": { " signature ": " ... ", " authorization ": { " from ": " ... ", " to ": " ... ", " value ": " ... ", " nonce ": " ... " } } } ⑤ verify / settle と facilitator ⑤でServerが行う検証と決済は、facilitator に委譲できます。facilitator は、支払いの検証とブロックチェーンへの送信を担うサービスで、次の2つのエンドポイントを提供します。 POST /verify : 支払いペイロードが有効かを、ブロックチェーン上で送金を実行せずに検証する POST /settle : 検証済みの支払いをブロックチェーンに送信し、送金を確定させる facilitator の利用は必須ではありませんが、ブロックチェーンとのやり取りを抽象化できるため、Cloudflareのドキュメントでは推奨されています。 Clientは facilitator と直接通信せず、ServerとHTTPで通信します。⑤の内側を展開すると、次のようになります。 facilitator を含む⑤の内側 決済が確定すると、Serverは⑥でリソースを返します。決済の確認は PAYMENT-RESPONSE ヘッダに載ります。 決済の確定と不可逆性 x402の決済は、1回の送金で確定します。カード決済のように、支払いを承認する段階と実際に資金が動く段階が分かれることはありません。これにより、リソース1件ごとのような少額の支払いでも短時間で決済できますが、同時に次の制約が生じます。 決済を取り消せない プロトコルとして返金手段を定義していない 誤送金や詐取による送金があった場合、プロトコルの範囲では資金は戻りません。返金や救済を実装する場合は、アプリケーション層で別途定義することになります。 HTTPを利用する構成のメリット x402は決済専用のプロトコルを新設せず、支払いのやり取りをHTTPのリクエスト/レスポンスサイクルの中で表現します。そのためクライアントを選びません。エージェント、curl、サーバ間通信のいずれも対象になり、SDKも必須ではありません。 402応答は特定のURLに対する支払い要求なので、値付けの単位もURLになります。エンドポイントごと、MCPツールごとに価格を返せます。 支払ってよいかの判断は仕様の範囲外 x402の仕様が定義しているのは、3つのHTTPヘッダ、 PaymentRequirements と PaymentPayload の構造、facilitator の verify / settle の手順、および scheme です。「どのような条件で支払いを承認するか」は定義されていません。仕様上、正しく署名されたペイロードは正当な支払いとして処理されます。 Cloudflare Walletsは、この判断をウォレット側で縛る仕組みを持っています。エージェントに渡すVirtual Walletには、使ってよい金額の枠(allowance)、支払い先の許可リスト(allow list)、1回あたりの上限額(maximum transaction size)を設定できます。 これらはウォレット側の設定なので、エージェントが何をしようとしても、枠を超える送金や、許可リストにない相手への送金は成立しません。x402が定義していない「支払ってよいか」の判断を、エージェント任せにせず、ウォレットの設定として持たせている構成です。 まとめ x402の仕様を整理すると、次のようになります。 決済専用のプロトコルを新設せず、HTTPのリクエスト/レスポンスサイクルで支払いを表現する 事前のアカウントもAPIキーも使わず、402応答とその再送だけで支払いが成立する scheme として exact (固定額)と upto (上限額の事前承認)を定義する 検証と決済は facilitator に委譲できる。利用は必須ではない 決済はブロックチェーン上で確定し、プロトコルとして取り消し手段を持たない 支払ってよいかの判断は仕様の範囲外で、クライアント側に委ねられる 最後までお読みいただきありがとうございました。 参考 Announcing Cloudflare Wallets: The programmable wallet for the agentic Internet - The Cloudflare Blog (2026年8月4日) x402 - Cloudflare Agents docs x402 Specification v2 - coinbase/x402 RFC 9110: HTTP Semantics - 15.5.3. 402 Payment Required 現行のHTTP仕様である RFC 9110 では "The 402 (Payment Required) status code is reserved for future use." と記述されています。402が標準として用途を定義されてこなかったことを、僕は今回の記事を書くまで知りませんでした。 ↩ Ethereum が採用しているブロックチェーンの実行環境です。これを採用するチェーンは多く、Cloudflareのドキュメントも EVM に対応するチェーンとして Base、Ethereum、Polygon などを挙げています。 ↩
はじめに こんにちは、株式会社エブリーのデリッシュキッチンにて7月から約1か月間エンジニアとしてインターンシップに参加していました、亀井と申します。 この記事では、インターン中に取り組んだことの中心である「Databricks LakebaseへのOAuth M2M認証の導入」と、その先でデータを読むための「GoのPostgreSQL向けライブラリ選定」の2つについて、どう実装・検証・調査したのかを書きたいと思います。 インターンで取り組んだこと インターンでは、Go言語で書かれたデリッシュキッチンのAPIサーバーを主な対象として、次の4つに取り組みました。 デリッシュAIの表示順ソートのロジックの実装 Databricks LakebaseへのOAuth M2M認証の実装 GoのPostgreSQL向けデータアクセスライブラリ(ORM)の比較調査 M2M認証を動かすための環境変数・ECSタスク定義の追加 この記事では主に2と3について書きます。 開発の背景 デリッシュキッチンのサーバーから、Databricks上の新しいリソースに接続したいという需要が生まれていました。接続先はLakebase Autoscaling PostgresというDatabricksが提供するフルマネージドなPostgreSQLで、projects → branches → endpointsの3階層でリソースを管理する新世代のサービスです。 Databricks本体は分析寄りで、アプリが求める個別レコードを低レイテンシで引く用途には向きません。そこで、Databricksでテーブルを一元管理するカタログ機能であるUnity Catalog側のテーブルをsourceとしてLakebaseのPostgresに自動同期し(synced table)、アプリからは読み取り専用のPostgresテーブルとして高速に読む、という構成を取ります。 問題は認証です。既存のDatabricks接続はPAT(Personal Access Token)という長命トークンで認証していました。しかしPATは失効管理が手動で、そもそもLakebaseのPostgres認証(OAuth role)は「発行から60分で失効するトークン」を前提としており、PATでは接続できません。静的なパスワードで認証するroleも作れますが、DatabricksのID管理に紐づかない長命の静的クレデンシャルになるため、漏洩時のリスクを考えると避けたいところです。 Databricksはサービスプリンシパルの認可にはOAuth 2.0を推奨しており、ここではOAuth M2M認証、すなわちサービスプリンシパルとclient credentialsフローの組み合わせを実装しました。用語を整理すると: サービスプリンシパル(SP): 人間ではなくアプリ自身に与えるIDアカウント。人に紐づけると退職や異動等で処理が止まり、権限も広くなりがちなので、機械用のアカウントを別に立てます client credentialsフロー: ユーザーの同意画面を介さず、アプリがClient IDとClient Secretを認可サーバーに提示してアクセストークンを得るOAuth 2.0のフロー つまり、サーバーが自分自身のIDとシークレットでトークンをもらい、そのトークンでAPIを呼ぶ仕組みです。 M2M認証の実装と検証 設計方針 設計方針の要点は次の3つです。 環境分離はbranchで行う:Lakebaseのbranchはコピーオンライト(CoW)でデータを分岐でき、 production (親)と development (子)で本番と開発を分離します。SPは環境別に2本立て、dev用SPのOAuth roleを production branchに作らないことが、dev環境から本番データへの到達を防ぐ唯一の境界になります。roleの状態はbranch間で独立している、というのが公式の仕様です。 認証専用のパッケージは作らない:トークンの発行・キャッシュ・更新はすべてSDK内部の責務で、自前コードに共通化すべきロジックが発生しないためです。 環境変数はSDKの標準名( DATABRICKS_HOST / DATABRICKS_CLIENT_ID / DATABRICKS_CLIENT_SECRET )を使う:databricks-sdk-goの統一認証チェーンは、これらの環境変数があればSPとしてM2M認証し、なければ開発者個人のCLIプロファイルに自動でフォールバックします。本番はSP・ローカルは個人認証という切り替えが分岐コードゼロで手に入り、SPのシークレットを開発者の端末に配らずに済みます。 この設計方針の下で実装を進めてきました。 実装 実装は公式ドキュメントのGoサンプルをほぼそのまま踏襲する形式になりました。核になるのは、GoのPostgreSQL接続ライブラリpgxが提供する接続プールの BeforeConnect フックです。 BeforeConnect は、プールが新しい接続を張る直前に毎回呼ばれる関数で、ここで接続に使う設定を書き換えられます。 w, err := databricks.NewWorkspaceClient(&databricks.Config{}) // 空のConfigでSDKの統一認証チェーンに委ねる: // DATABRICKS_*環境変数(本番ECS)→ CLIプロファイル(ローカル) // Lakebaseは全接続にSSL/TLSを必須とするため、sslmodeはコード側で固定する poolCfg, err := pgxpool.ParseConfig( "sslmode=require" ) // credentialは発行から60分で失効する。BeforeConnectは新規接続にしか // 効かないため、失効前に接続自体を作り直させる(公式サンプル準拠)。 // Jitterで寿命をばらつかせ、一斉失効による再接続の集中を防ぐ poolCfg.MaxConnLifetime = 45 * time.Minute poolCfg.MaxConnLifetimeJitter = 5 * time.Minute poolCfg.BeforeConnect = func (ctx context.Context, connCfg *pgx.ConnConfig) error { cred, err := w.Postgres.GenerateDatabaseCredential(ctx, postgres.GenerateDatabaseCredentialRequest{ Endpoint: endpointName, // projects/{project}/branches/{branch}/endpoints/{endpoint} }) if err != nil { return err } connCfg.Password = cred.Token // 接続確立ごとに新鮮なトークン return nil } ポイントは3つあります。 Database Credentialはキャッシュしない:60分で失効するトークンをキャッシュすると、失効間際のトークンを掴む事故が起きます。毎回発行するとAPIコールが増えそうに見えますが、発行が走るのは接続プールが新規接続を作るときだけなので、実際の頻度は低く抑えられます。 接続寿命をトークンのTTL(有効期間)未満に制限する: BeforeConnect が効くのは新規接続だけで、確立済みの接続にあとから新しいトークンを渡す手段はありません。接続を作りっぱなしにすると、認証に使ったトークンが失効した接続がプールに残り続けます。そこで公式サンプルと同じく MaxConnLifetime = 45 * time.Minute で、失効前に接続自体を作り直させます。さらに、プールの接続はデプロイ直後などにまとまって作られるため、寿命が同じだと作り直しのタイミングも一点に集中します。これをずらすのが、寿命に上乗せするランダムな幅であるジッターです。ジッターを足しても接続寿命は最長50分で、TTLの60分を確実に下回ります。 接続先のdatabaseはコードで切り替えられるようにする:これはレビュー指摘で直した点です。当初は接続先のdatabase名を環境変数 PGDATABASE 任せにしており、接続先が1つに固定されていました。しかし今後は複数のdatabaseを読む可能性があります。そこでdatabase名を型として定義し、database単位に接続プールを分けて、接続文字列の dbname で明示指定する形に改めました。pgxでは接続文字列の設定が環境変数より優先されるためです。endpointと認証credentialはdatabaseを問わず共通なので、 BeforeConnect の仕組みは全プールでそのまま共有できます。 環境変数とタスク定義 このコードを本番で動かすには、AWSのコンテナ実行サービスであるECSのタスク定義への環境変数の追加が必要です。 DATABRICKS_* の3変数に加えてPostgres接続用の PG* 変数を、dev / prdそれぞれのタスク定義に追加しました。ひとつ注意が要るのは PGUSER で、本番ではSPのclient ID、ローカルでは開発者個人のIDと、環境で値の種類そのものが違うため、ハードコードせず環境ごとに注入します。 検証 作った接続コードが「本当に意図通り動くのか」を示すために、疎通確認用のCLIをリポジトリ内に用意しました。確認したいことが3層に分かれているので、CLIも段階を選べるようにしてあります。 --auth-only : Postgresには触らず、ワークスペースAPIへの認証だけを確認します フラグなし: credentialを発行してPostgresにログインし、 SELECT 1 を実行します --count 2 --interval 65m : 接続寿命を跨いで再接続できるかを確認します なぜ分けるかというと、1と2は通信経路も認可の仕組みも別物だからです。段階1はワークスペースのAPIに届くかの確認で、認可は、ワークスペースの機能を使う権利であるエンタイトルメントが担います。段階2は発行されたcredentialでPostgresにログインする確認で、認可はbranch単位のOAuth roleが担います。一気に確認すると、失敗したときにどの層が原因か切り分けられません。 データを読むライブラリをどう選ぶか 認証が通ったら、次はその接続でデータを読む実装です。ここで「GoからPostgreSQLを扱うのに何を使うか」というライブラリの比較を行いました。 結論から書くと、第一候補はBob、第二候補はsqlcです。理由は、クエリの書きやすさ、型の安全性、そして上で作った既存の *pgxpool.Pool をそのまま使えることです。 選定の前提は次の3つで、それぞれが結論の理由に対応しています。 対象はsynced tableです。スキーマはUnity Catalog側が所有し、アプリからは ALTER を発行しません:主キーのないテーブルを扱えること、スキーマ変更に気付けることが効いてきます 接続は、 BeforeConnect による認証をバイパスしないよう既存の *pgxpool.Pool を使います:プールを直接扱えるライブラリが有利です 読み取るテーブルは今後増えていきます:1テーブルなら手書きでも書けますが、増えるほど書きやすさと型安全性が効いてきます 選定の理由 選定の過程では生成AIを用いてORMを列挙させ、上記の観点から7つのライブラリを選出しました。利点と欠点で比較をし、参考程度にDocker上のPostgreSQLで実測を行いました。その後議論を行い、以下のORMを選定しました。 Bob: 既存の *pgxpool.Pool を公式に直接扱えます。ビルダでクエリを書きやすく、コード生成で型安全性を足せます。主キーのないテーブルでも生成が通り、読み取り専用のモデルになるためsynced tableの性質と噛み合います。 sqlc: SQLを書いてコードを生成する型で、列名や型の誤りを生成時に検出できます。スキーマをUnity Catalog側が所有する今回の状況では、上流のスキーマ変更を再生成時のコンパイルエラーとして捕まえられる固有の強みがあります。 他の候補を見送った理由も簡単に説明します。GORMはフルORMの利点が今回の要件では効きません。entはジェネレータが主キー id を必須とするため、主キーのないsynced tableへの全件取得が失敗します。scanyは短く書けますが開発が止まっています。手書きのpgxは型が go build 時点で固定される書き方ですが、テーブルが増えるほど手書きの Scan が積み上がります。 まとめ インターンを通して、実際にユーザーに使われているデリッシュキッチンのサーバーに触れられたのは、とても貴重な経験でした。チームの設計方針や、既存のコードを読み解きながら実装を行う経験は開発現場だからこそ得られるものだと思います。インターンシップで学んだことも活かしつつ、今後もさまざまな技術に触れながら、エンジニアとして成長していきます。 最後に、1か月間サポートしてくださったデリッシュキッチンの皆様、本当にありがとうございました。 参考 Connect external app to Lakebase using SDK OAuth machine-to-machine (M2M) authentication Branching Manage Postgres roles Connection security databricks-sdk-go Bob: pgx driver — NewPool ent: Fields — ID
はじめに こんにちは。リテールハブ小売アプリ開発チームの池です。 小売アプリチームでは、昨年から CodeRabbit を利用して PR レビューを行っています。日々活用してはいるものの、アップデートの内容を追いきれていませんでした。そこで本記事では、2026 年にアップデートされた内容を整理し、気になった機能をピックアップして紹介します。 なお、本記事の内容は 2026 年 8 月 12 日時点の情報をもとにしています。CodeRabbit はアップデートの頻度が高いため、機能の挙動やプラン要件は変わっている可能性があります。最新の情報は公式ドキュメントをご確認ください。 また、私たちのチームでは Pro プランを利用しているため、Pro プランで利用できる範囲を中心に、Pro+ プラン限定の機能はその旨を明記しながら紹介します。 2026 年アップデートの全体像 CodeRabbit の Changelog および 公式ドキュメント をもとにアップデートの全体像を整理します。2026 年 1〜8 月の changelog は約 100 件あり、主なアップデート内容を独自に抜粋して整理すると次のようになります。 カテゴライズ 主なアップデート(月) 概要 レビューUX ・Multi-Repo Analysis(2 月、6 月拡充) ・Review auto-pause(2 月) ・Slop PR 検出(3 月) ・Quiet プロファイル(7 月) マルチリポ分析の登場とレビューノイズの削減 Finishing Touches ・Autofix(2 月) ・Simplify(3 月) ・Resolve Merge Conflicts(3 月) ・Custom Recipes(3 月) ・Fix CI(7 月) レビュー後の修正作業をエージェントに委譲 CLI・コーディングエージェント連携 ・Claude Code Plugin(2 月) ・CodeRabbit Skills(2 月) ・CLI --agent モード(3 月) ・Codex plugin(4 月) Claude Code などのコーディングエージェントにレビューを統合 静的解析ツールの追加 ・Trivy / TFLint(2 月) ・zizmor(5 月) ・oasdiff(6 月) ・ast-grep の Dart 対応(6 月) ・e18e(7 月) 対応言語・領域の拡大(今年だけで 10 種超) Analytics・ダッシュボード ・Learnings ダッシュボード(2 月) ・ダッシュボード再編(3 月) ・利用メトリクス(6 月) ・レートリミット可視化(7 月) 運用状況の可視化が大幅に強化 外部ツール連携・CodeRabbit Agent ・Slack 向け Agent(4 月) ・メッセージトリガー自動化(4 月) ・Post-Merge Actions(7 月) ・Agent Skills(7 月) Slack を入口にした汎用エージェントと SaaS 接続の拡大 Plan ・Issue Planner(2 月) ・VS Code での Plan(6 月) Issue からの実装計画生成 セキュリティ ・Betterleaks(3 月) ・Security Agent(7 月) ・Secrets 検証ステータス(7 月) ・Git 履歴スキャン(7 月) PR レビューからコードベース全体のスキャンへ ナレッジ・Learnings ・承認フロー(6 月) ・Learnings API(6 月) ・Code Guidelines 管理 UI(6 月) 学習内容の可視化と統制 組織管理・ガバナンス ・Custom Roles(2 月) ・Audit Logs(3 月) ・Pro+ プラン導入(4 月) ・Global Overrides(4 月) エンタープライズ向け統制と料金体系の再編 Change Stack ・semantic diff / Code Peek(5 月) ・Change Stack 各プラットフォーム展開(5〜7 月) ・アプリ内レビュー会話(7 月) 意味的に構造化された PR レビュー専用UI と各プラットフォーム展開 ここからは、この中から気になった 5 つのテーマをピックアップして紹介します。 ① レビューUX 2 月: Multi-Repo Analysis が登場 3 月: Pro プランでリンク可能リポジトリ数が 1→2 に増加 6 月: レビュー対象 ref の固定(Multi-Repo Ref Selection)、自動リポジトリリンクとその管理 UI(Pro+ プラン) Multi-Repo Analysis は、PR のレビュー時に関連リポジトリのコードも合わせて参照して分析する機能です。単一リポジトリの diff だけでは見つけられない、リポジトリをまたぐ問題(API の破壊的変更、型の不一致、依存のドリフトなど)を検出できます。たとえばモバイルアプリと API サーバーが別リポジトリに分かれている場合、サーバー側の API 変更に対して「アプリ側の呼び出しが壊れないか」という観点をレビューに加えられます。 Pro プランでは自動リンク機能が使えないので、管理画面から手動で関連するリポジトリを設定しないと本機能は適用されないようです。設定方法を見ていきます。 設定方法 リンク設定は、管理画面の Review > Repositories > Settings > Knowledge base にあります。手動でのリンクと自動リンクの 2 通りがあります。なお、リンクできるリポジトリ数について、changelog には 3 月に Pro プランで 1→2 に増加したと記載がありますが、執筆時点の管理画面では 1 リポジトリまででした。 手動でリンクする リポジトリ設定はデフォルトで Organization 設定を継承しており、この状態ではリポジトリ単位のカスタマイズができません。Review > Repositories > Settings > General にある「Use Organization Settings」のトグルを一度クリックすると、設定の継承は on のまま、リポジトリ単位で編集できるようになります。 Linked repositories でリンクするリポジトリを選択し、Instruction にリポジトリ間の関係を記述できます。 保存すると Linked repositories の一覧に表示され、リンク設定は完了です。 自動リンク(Automatic repository linking) Automatic repository linking を有効にすると、Organization 内の関連するリポジトリが自動的に検出されリンクされます。なお、自動リンクは Pro+ プラン以上の機能です。 Multi-Repo Analysis のレビューでの見え方 Multi-Repo Analysis の結果は、PR Walkthrough の Review details > Additional context used に、リンクされたリポジトリ名ごとにグループ化されて表示されます。自動リンクされたリポジトリが含まれる場合は、Review info セクションに CodeRabbit が参照したリポジトリの一覧が表示され、それぞれに手動リンクか自動検出かのラベルが付きます。また、関連がある場合はインラインのレビューコメントやコメント返信にも指摘として現れます。 なお、Review details セクションを表示するには、 .coderabbit.yaml で review_details を有効にしておく必要があります。 # .coderabbit.yaml reviews : review_details : true ② Finishing Touches Finishing Touches は、レビュー後の仕上げ作業(指摘の修正、docstring や単体テストの作成など)を、PR コメントや PR Walkthrough のチェックボックスから CodeRabbit のエージェントに任せられる機能群です。 2 月: Autofix(未解決の指摘への修正を自動適用)、Chat code editing(Early Access) 3 月: Simplify、Resolve Merge Conflicts、Custom Recipes 7 月: Fix CI(CI 失敗の調査から修正・PR 作成までを行う) Finishing Touches の一覧 執筆時点で利用できる Finishing Touches は以下の 7 種類です。一部は Pro+ プラン限定となっています。 Autofix(指摘の自動修正) Docstrings(docstring の生成) Fix CI(CI 失敗の修正、Pro+ プラン) Resolve Merge Conflicts(コンフリクトの解決、Pro+ プラン) Unit Tests(単体テストの生成、Pro+ プラン) Simplify(コードの簡素化、Pro+ プラン) Custom Recipes(カスタム定義による修正、Pro+ プラン) トリガー方法は 2 種類あります。 PR コメント: @coderabbitai generate docstrings のようなコメントを投稿する チェックボックス: PR Walkthrough のコメント上のチェックボックスにチェックを入れる Autofix Autofix は、PR に残っている未解決のレビュー指摘を対象に、サンドボックス内で修正を作成する機能です。修正の反映先は、現在のブランチへの直接コミットか、stacked PR かを選べます。 以下のようにコメントすることで利用できます。 @coderabbitai autofix # 修正を現在のブランチにコミット @coderabbitai autofix stacked pr # 修正を stacked PR として作成 Autofix 機能はデフォルトで有効になっています。リポジトリ単位で無効にしたい場合は、 .coderabbit.yaml で設定します。 # .coderabbit.yaml reviews : finishing_touches : autofix : enabled : false # Autofix を無効化 実際に試したところ、人間のレビュアーのコメントに対して @coderabbitai autofix stacked pr を実行しても、以下のようにスキップされました。 Autofix の対象は、CodeRabbit 自身が投稿した修正手順つきの未解決レビューコメントのみで、人によるレビューコメントの修正は任せられないようです。 次に、CodeRabbit の修正手順つき未解決コメントが残っている PR で実行したところ、今回は受け付けられました。特定の 1 件の指摘スレッドへの返信として実行しましたが、PR 上の未解決指摘 17 件すべてが修正対象になりました。コメントを投稿した場所に関係なく、PR 単位で一括適用される挙動のようです。 実行から約 6 分半で、12 ファイルの修正を含む stacked PR が作成されました。 ③ CLI・コーディングエージェント連携 CodeRabbit CLI は、ターミナルから手元のコード変更に対して CodeRabbit のレビューを実行できるツールです。PR を作成する前に、ローカルで指摘の検出と修正を済ませられます。2026 年は、この CLI を土台にした Claude Code などのコーディングエージェントとの統合が進みました。 2 月: Claude Code Plugin 対応、CodeRabbit Skills(open agent skills 標準) 3 月: CLI --agent モード(構造化 JSON 出力)、CLI 用 Usage-based アドオン(従量課金オプション) 4 月: CLI ブラウザ完結のサインイン、Codex plugin 対応 5〜8 月: coderabbit doctor 、 --light モード、レビュー信頼性の改善 CLI は無料プランでも利用できます(基本的な静的解析、1 時間あたり 3 回まで)。Pro プランにすることで、組織の Learnings を活用した強化レビューになり、レートリミットは 1 時間あたり 5 回に引き上げられます。 コーディングエージェントに CLI を統合するメリット CodeRabbit は、Claude Code などのコーディングエージェントと CLI を組み合わせる意義を、次のような役割分担として説明しています。 専門家による問題検出 : 一般的なリンターでは見逃しやすい競合状態・メモリリーク・論理エラーを CodeRabbit が検出する AI を活用した修正 : Claude Code が CodeRabbit の分析結果に基づいて修正を実装する コンテキストの保持 : 問題の発生場所・深刻度・推奨される対処法が簡潔に Claude Code へ渡される 継続的なワークフロー : ツールを切り替えることなく、レビュー → 修正 → 反復のループを開発の流れの中で回せる Claude Code への導入方法 導入は「CLI のインストール・認証」と「Claude Code へのプラグイン追加」の 2 段階です。 まず CodeRabbit CLI をインストールします。 curl -fsSL https://cli.coderabbit.ai/install.sh | sh # または brew install coderabbit 次に認証を行います。4 月の CLI v0.4.0 からサインインはブラウザで完結するようになりました。 coderabbit auth login Claude Code 側では、公式プラグインマーケットプレイスに CodeRabbit が用意されているので、選択するだけで使えるようになります。 プラグインの内容 導入すると、Claude Code 内で coderabbit-review コマンドと、code-review・autofix の 2 つのスキルが利用できるようになります。 /coderabbit:coderabbit-review : 手元の変更に対して CodeRabbit のレビューを 1 回実行し、結果を表示するコマンド /coderabbit:code-review : CodeRabbit のレビューをワークフローとして扱うスキル。エージェントがレビューを必要と判断した場面でも自動起動する /coderabbit:autofix : GitHub PR のレビュースレッドにある CodeRabbit の指摘を、変更ごとに承認を挟みながら安全に適用する また、プラグインとは別に、open agent skills 標準に対応した CodeRabbit Skills も提供されています。Claude Code の場合はプラグインの導入でスキルも一緒に入るため追加の作業は不要ですが、Cursor / Codex / Gemini CLI など他のエージェントでも使いたい場合は、以下のコマンドで各エージェントのスキルディレクトリに導入できます。 coderabbit skills code-review と autofix の 2 つのスキルの実体は、ワークフローを記述した Markdown ファイル(SKILL.md)です。それぞれ次の内容がワークフローとして明記されています。 code-review : 「実装 → レビュー → Critical/Warning の修正 → 再レビュー → クリーンになるまで繰り返す」という自律ループ autofix : エージェント自身が指摘の妥当性をローカルコードで検証し、1 件ずつユーザーの承認を取りながら修正を適用する手順 ループが前提として設計されている点、必要な箇所で人の承認を挟んでいる点から、AI を使った開発ワークフローに CodeRabbit を自然に組み込める仕組みだと感じました。 ④ 静的解析ツールの追加 静的解析ツールの追加は活発に行われており、1〜8 月だけで 10 種を超えるツールが追加・更新されています。対応しているツールの一覧は 公式ドキュメントの Tools 一覧 を参照してください。 2 月: Trivy(IaC のセキュリティスキャン)、TFLint(Terraform)、Stylelint(スタイルシート)、TruffleHog(シークレットスキャン)、OpenGrep(Semgrep 互換の静的解析エンジン)など 9 種 3 月: Betterleaks(シークレットスキャナを Gitleaks から Betterleaks へ置き換え。既存の gitleaks 設定キーがそのまま使える) 5 月: zizmor(GitHub Actions ワークフローのセキュリティ解析) 6 月: Infer(C / C++ / Java の null 参照・リソースリーク・並行処理バグ検出)、oasdiff(OpenAPI の破壊的変更検出)、React Doctor(React のセキュリティ・パフォーマンス・アクセシビリティの問題を検出)、ast-grep の対応ファイルタイプ拡大(Dart、TOML、Markdown など) 7 月: e18e ESLint plugin(モダンな代替がある依存や、メンテナンスされていないパッケージを指摘) これらの静的解析ツールは CodeRabbit ではほぼすべてデフォルトで有効になっており、該当するファイルタイプの変更を検出すると自動で実行されます。 ルールの詳細を制御したい場合は、各ツールの設定ファイルを使います。リポジトリに .eslintrc.js や pyproject.toml があれば CodeRabbit はそれを尊重するため、既存の設定をそのまま生かせます。 また、対象領域は、シークレット・IaC・CI 設定といったアプリケーションコードの周辺へと拡大しています。PR レビューの対象が、コードそのものからリポジトリ全体の安全性へ広がっていると感じました。 複数のリポジトリを運用していると、リポジトリごとに静的解析ツールを選定して管理していくのはそれなりのコストになります。ツールの追加・実行・アップデートを CodeRabbit 側が担ってくれることで、この導入・運用コストを下げられるのはメリットの一つだと思いました。 ⑤ Analytics Analytics は、CodeRabbit の管理画面で組織のレビュー活動を可視化するダッシュボード機能です。レビューされた PR の数や指摘への対応状況、レビューにかかった時間、Learnings などナレッジの活用状況といったメトリクスを、リポジトリ・ユーザー・期間で絞り込みながら確認できます。 2 月: Learnings ダッシュボード(KPI カード、利用回数・最終利用日などのメタデータ) 3 月: ダッシュボード再編(Git プラットフォームレビューと IDE/CLI レビューの分離、Knowledge Base / Pre-merge Checks / Reporting などの新ページ) 6 月: Path 指示・Finishing Touches の利用メトリクス 7 月: レートリミット影響の可視化(Usage Rate-Limit Insights)、Explore ページのレコメンデーション 3 月の再編により、Analytics は大きく「PR reviews」と「IDE/CLI reviews」の 2 つに分かれています。それぞれ見ていきます。 PR レビューのメトリクス PR レビュー側は Summary / Quality Metrics / Time Metrics / Knowledge Base / Organization Trends / Pre-merge Checks / Reporting / Data Metrics のページで構成されており、すべては書ききれないほど多くのメトリクスがあります。以下は Summary ページの画面です。 ここでは代表的な項目を挙げます。 レビュー成果 : レビュー済みでマージされた PR 数、投稿コメント数、Acceptance Rate(開発者が指摘に対応した割合)、Reviewer Time Saved(AI 推定のレビュー工数削減時間) 時間系 : レビュー可能になってから「マージ」「最初の人間レビュー」などまでの時間(平均・中央値・P75・P90)と週次トレンド ナレッジ系 : Learnings の作成数・適用率、Path-based Instructions の利用率、MCP サーバー別の PR カバレッジ 運用系 : Pre-merge Checks や Finishing Touches の実行数と結果、レートリミットの影響 これらのメトリクスは、開発プロセスの改善、効果の定量的な説明、CodeRabbit 自体のチューニングなど、幅広く活用できそうです。 IDE/CLI レビューのメトリクス IDE / CLI がチームでどれだけ使われているかを示すメトリクスです。 アクティブユーザー数・レビュー数・レビューコメント数を、IDE 拡張 / CLI の種別ごとに集計 Learnings や Path-based Instructions がローカルレビューに適用された割合 SAST・リンターによるツール検出の内訳(ツール別・重要度別) ユーザー別の明細(拡張の種別、導入日、最終利用日時、レビュー数) 従来見えづらかった PR に到達する前の品質活動を追えるのは、開発フローの改善を測るうえで示唆がありそうです。 おわりに 本記事では、CodeRabbit の 2026 年 1〜8 月のアップデートを整理し、気になった 5 つのテーマをピックアップして紹介しました。 アップデートの内容を追うと、CodeRabbit が PR レビューのツールにとどまらず、開発ライフサイクル全体を支えるエージェントプラットフォームへと広がっていると感じました。 AI によりコードの生成量が増大する中で、いかに効率良く品質を担保するかは、AI 活用における課題の一つだと思います。CodeRabbit を適切に活用することは、この課題を解決する方法の一つになると感じています。 私たちのチームでも、今回整理したアップデート内容を実際の開発フローに組み込みながら、活用を広げていければと思います。 本記事が少しでも参考になれば幸いです。最後まで読んでいただきありがとうございました。
はじめに こんにちは、開発1部でデリッシュキッチンの開発をしている蜜澤です。 現在データ基盤はDatabricksを使用しており、データはUnity Catalog(以下UC)のテーブルです。 UCのテーブルをプロダクトのAPIから配信したいという状況が直近の1年で増えてきました。 例えば、「レコメンド結果をアプリの画面に出したい」「集計したアプリのログデータを分析できる社外向けのダッシュボードツールを作成したい」といった要望があります。 これをアプリ側から読めるようにするには、UCのテーブルのデータをAPIが参照できるDBに複製する必要があります。DatabricksのLakebase(マネージドPostgreSQL)を使えば、これを実現できそうです(Lakebaseは 2026年6月30日に東京リージョンで利用可能になりました )。 ただ、複製の方法はLakebase以外にもあります。本記事では、従来の方法と比較しながらLakebaseで実現するのが良いのかどうかを考えてみます。複製先のDBは、既存のアプリのDB(MySQL互換のRDS)かLakebaseのどちらかになります。 比較する際に考慮したのは以下の4点です。 ダウンタイム(DBのデータが空や一部欠けた状態で読まれる時間)が発生するかどうか アトミック性(一連の更新がすべて反映されるか、まったく反映されないかのどちらかになる) 実装コスト(初期整備や、配信テーブルを増やすたびに必要な実装の量) 金銭的コスト(追加で発生するインフラ費用) 紹介するのは以下の3種類の方法です。 DatabricksからRDSに直接書き込む バックエンドのバッチで取り込む Lakebaseのsynced tableを使用する DatabricksからRDSに直接書き込む 概要 DatabricksからJDBC接続で、アプリのRDSに直接書き込みます(Sparkの df.write.format("jdbc") )。 UC (Delta) --[Spark JDBC writer]--> アプリの RDS --> API アプリ側から見れば、普通のDBのテーブルが1つ増えるだけになります。 メリット 実装コストが小さいです。 バックエンド側の実装はテーブル定義のマイグレーションのみで、Databricks側の実装は書き込み処理を書くだけであり、最小限の実装で済みます。 ただし、これはあくまでアプリケーションコードの話で、DatabricksからRDSにネットワーク的に到達できるようにする設定(VPCピアリングなど)は別途必要です。 デメリット mode("overwrite") で書き込みを行うと、ダウンタイムが発生してしまいます。 mode("overwrite") はテーブルを空にしてから書き直しますが、Sparkはパーティションごとに独立したトランザクションでコミットするため、この一連の処理を1つのトランザクションで囲む方法がありません。結果として、テーブルが空の状態や一部しか入っていない状態が、そのままAPIから読めてしまいます。つまり書き込みが終わるまでの間、データが欠損した状態で配信されるダウンタイムが発生します。 この問題は mode("append") を使えば解決できます。 append は既存行を消さないため、テーブルが空の状態や一部しか入っていない状態が読まれることはありません。 ただし append は追記しかしないため、全件を更新したい場合は使えません。ダウンタイムを発生させずに全件更新したい場合は、本番とは別のテーブルに全件を書き込んでおき、 RENAME TABLE で本番のテーブルと入れ替えることで実現できます。 RENAME TABLE は1つの文に渡した複数テーブルの付け替えをアトミックに実行するため、読み手からは入れ替えの前か後のどちらかの状態しか見えず、空や一部欠けた状態が読まれることはありません。 なお、厳密には入れ替え時にメタデータロックの取得待ちが発生し得ます。長時間のクエリや未コミットのトランザクションが同じテーブルに触れていると、 RENAME TABLE が待たされるだけでなく後続の読み取りクエリもその後ろで待たされるため、入れ替えは長時間のクエリと重ならないタイミングで実行するのが安全です。 この構成では「アプリのDBに対するテーブルの入れ替え(DDL)をDatabricks側から実行する」ことになります。アプリ側がマイグレーションで管理しているスキーマをETLが作り替える形になるため、動きはするものの設計としてはあまり綺麗ではない印象です。 また、外部キー制約がある子テーブルを書き込もうとすると、キーの管理が煩雑になります。参照先の親テーブルがアプリのDBにしか存在しない場合、そのidを確認するためにアプリのDBを見る必要があります。子テーブルのデータを作るには、アプリのDBからidを取得してきて、それと整合するようにDatabricks側で外部キーを振る必要があり、idの管理をDatabricks側で抱え込むことになります。 用途 向いている 検証段階で、まず動くものを最短で作りたい場合 データの全件更新が不要で増分更新のみでよい場合 全件更新時の短時間のダウンタイムを許容できる場合 ダウンタイムを許容できない場合でも、 RENAME TABLE による入れ替えを許容できる場合 向いていない ダウンタイムなしの全件更新が必要で、 RENAME TABLE による入れ替えを許容できない場合 外部キー制約で既存のテーブルと繋がる子テーブルを作成したい場合 バックエンドのバッチで取り込む 概要 DatabricksからUCのテーブルのデータをCSVファイルとしてS3に出力し、それをアプリのバックエンドに実装した取り込みバッチで読み込んでRDSに書き込みます。 Databricks側はS3にファイルを置くところまでを責務とし、そこから先の取り込みはアプリ側が担います。 UC (Delta) --> S3(CSV) --> [アプリ側の取り込みバッチ] --> アプリのRDS --> API メリット 全件更新したい場合でもアトミックな切り替えが可能になります。アプリの通常のコードなので、単一プロセスから単一トランザクションで書けます。実際にはアプリのコードでS3のCSVを読み込んでバルクINSERTを繰り返す形になりますが、構造としては以下の通りです。 BEGIN ; DELETE FROM xxx; INSERT INTO xxx ... ; COMMIT ; DELETE はトランザクショナルなので、確定は COMMIT の1回だけです。読み手は切り替わりの前か後しか見ません。 取り込み時に加工・検証ができます。参照先マスタの実在チェック、正規化、既存データとの整合確認をアプリのドメインロジックで書けます。外部キー制約も張れるので、参照先が消えたときの挙動をDBに任せられます。 デメリット 実装コストが大きくなりがちです。Databricksから直接書き込む方法ではDatabricks側に書き込み処理を数行書くだけで済みますが、この方法では数百行程度とはいえ取り込み処理を追加で実装する必要があります。さらに、以下のような定期実行の仕組みがバックエンド側に整っていない場合は、それらも合わせて実装する必要があります。 スケジューリング : バッチを動かす実行環境の定義と、デプロイパイプラインへの組み込み 資格情報 : S3の読み取り権限とDBの書き込み権限、それぞれのIAMとシークレットの管理 監視とアラート : 取り込みの失敗に気づくための通知の仕組み 冪等性と再実行 : 途中で失敗しても安全にやり直せる作り データ受け渡しのルール : ファイル形式・置き場所・命名の取り決めや、書き込み途中のファイルを読まないための完了通知 ここを整えないと、取り込み処理自体はできているのに手動実行の運用が残ってしまう、ということになりがちです。 また、S3上のファイルとDBの両方にデータのコピーが存在することになるため、ETLからS3へファイルを置く処理が失敗した場合、ETL側の再実行だけでは復旧できず、バックエンドの取り込みバッチも合わせて再実行する必要が出てきます。 用途 向いている 取り込み時に加工・検証・既存マスタとの整合が必要なとき 外部キーでDBの他テーブルと繋がるような場合 向いていない アプリ側に実行基盤が整っておらず、整備する工数を取れない場合 Lakebaseのsynced tableを使用する 概要 DatabricksのLakebase(マネージドPostgreSQL)を使い、UCのテーブルをPostgresのテーブルとして同期します。 UC (Delta) --[synced table]--> Lakebase (Postgres) --> API 同期モードは3つあり、同期のされ方が異なります。 Snapshot : 実行のたびに同期元の全量を取得し、テーブルを丸ごと置き換える。手動・API・スケジュールで起動する Triggered : 初回に全量を取得し、以降は前回実行からの差分だけを適用する。起動方法はSnapshotと同じ Continuous : 初回に全量を取得し、以降はパイプラインが常時実行され、変更をほぼリアルタイムで適用し続ける 参考: Databricksの公式ガイド メリット 全件更新をアトミックに行えます。Snapshotモードの同期は、全量をコピーしたうえでテーブルをアトミックに置き換えるため、空の状態や一部だけ入れ替わった状態が読まれることはありません。Databricksから直接書き込む方法では RENAME TABLE を使って自前で実装する必要があった入れ替えを、プラットフォームの機能として提供してくれています。 また、アプリ側に取り込みバッチを実装する必要がありません。バックエンドのバッチで取り込む方法で課題になった定期実行の仕組みの整備が不要で、スケジューリングも監視もETL側の仕組みに閉じます。 読み取りは通常のPostgresと同じなので、配信のレイテンシもミリ秒オーダーです。配信するテーブルを増やしたい場合もsynced tableを1つ追加するだけでよく、アプリ側は接続の仕組みを共有できます。 デメリット 他の方法と比べて金銭的なコストが大きくなります。ここまでの2つの方法は既存のRDSにデータを入れるため追加のインフラコストがほとんどかかりませんが、この方法ではRDSとは別にLakebaseを新しく持つことになるため、その分の料金が追加で発生します。Autoscalingプランは従量課金で、Computeの料金は1 CU-hourあたり$0.111程度(2026年8月12日時点の 公開価格 )、これに加えてストレージなどの料金がかかります。使っていない時間にコンピュートを落とすscale-to-zeroという機能もありますが、常時リクエストが来るAPI配信ではそもそも落ちる時間がなく、むしろコールドスタートを避けるために最小キャパシティを確保しておく必要があります。 また、接続まわりの作り込みが必要です。認証はOAuthトークンかネイティブPostgresロールパスワード認証(Autoscalingプロジェクトではデフォルト無効)の2方式で、OAuthの場合はトークンの有効期限が1時間のため、新規接続のたびにトークンを取得し直す仕組みをアプリ側に組み込む必要があります。認証方式の詳細は 公式ドキュメント を参照してください。 synced tableは読み取り専用として扱う必要があります。データのソースはUC側であり、Postgres側で直接更新すると同期元との整合が壊れてしまうため、読み取りのみにすることが推奨されています。なお、Lakebase自体は通常のPostgresなので、synced tableとは別に書き込み可能なテーブルを作ること自体は可能です。ただし、アプリのDB(RDS)とは別のデータストアであることに変わりはないため、アプリのマスタテーブルとの外部キー制約は張れません。参照先のマスタが消えた場合の整合性は、ETL側で担保するか配信時のフィルタで守ることになります。 外部依存が増えるため、Lakebaseに障害があったときにアプリの機能全体を落とさないような設計も必要になります。 用途 向いている 分析基盤が作ったデータをそのままAPIから配信したいとき アプリ側に取り込みの仕組みを作らず、ETL側で完結させたいとき 向いていない 接続まわりの初期実装に工数をかけられない場合 配信データを既存のアプリのDBと同じDBで管理したいとき(別のデータストアになるため不可) Lakebaseの予算を捻出できない場合 まとめ 今回紹介した3つの方法をまとめると以下になります。 Databricksから直接書き込む バックエンドのバッチで取り込む Lakebaseのsynced table ダウンタイムなしの全件更新の実現方法 別テーブルに書き込み、 RENAME TABLE で入れ替え(自前で実装) 単一トランザクションでの書き込み Snapshotモードの同期 初回のみ必要な整備 VPCピアリングなどの経路設定 定期実行の仕組みの整備 Lakebaseの作成 + トークン管理など接続まわりの作り込み テーブルごとに必要な作業 書き込み処理の実装 取り込み処理の実装 synced tableの追加 追加のインフラコスト ほぼなし ほぼなし あり(Lakebaseの従量課金) データの加工・検証を行う場所 Databricks側(書き込み前) アプリ側(取り込みバッチ内) Databricks側(同期元テーブルの生成時) 既存テーブルとの外部キー制約 張れる(id管理が煩雑) 張れる 張れない ETLが失敗したときの再実行範囲 ETLのみ ETL + 取り込みバッチ ETL + 同期 従来の方法とLakebaseを使用する方法を比較しましたが、LakebaseはUCのデータをアプリに届ける選択肢になり得ると感じました。ダウンタイムなしの全件更新がプラットフォームの機能として手に入り、アプリ側に取り込みの仕組みを作らずETL側で完結できるのは、従来の2つの方法にはない強みです。 ただし、Lakebaseを新しく持つことによる金銭的コストと接続まわりの初期実装が必要で、既存のテーブルと外部キーで繋ぐこともできないため、どんな場合でもLakebaseが最適というわけではありません。 どの方法も一長一短ではあるので、使用する状況に合わせて選定するのが良いと思います。個人的には、次のフローチャートに沿って判断するのが良いかなと思っています。 最後まで読んでいただきありがとうございました! この記事がいつか誰かの参考になれば嬉しいです!
はじめに こんにちは!デリッシュキッチンで主にバックエンドの開発を担当している秋山です。 AIツールの導入などで、ツール費用が増えている方も多いのではないでしょうか。 有料ツールを導入すると当然支出は増えますが、予算が無限にあるわけでもありません。 私も先日、有料ツールを導入したいと考えたときにコストの壁にぶつかりました。 そこで「先に既存のムダを削って、浮いた分の範囲内でまずは導入/検証する」という進め方を取ることにしました。 その中でコストとどう向き合うか考えるきっかけになったので、その時のことを紹介していきます。 どうやってコスト削減に取り組んだか コスト削減の対象として真っ先に思いついたのは、AWSでした。 インフラの費用は金額が大きいので、削れた時の効果もそのぶん大きくなります。 とはいえ、どこにムダがあるのかを自力で探し回るのは大変です。 そこで今回はAWS Cost Optimization Hubを使いました。 Cost Optimization Hub(コスト最適化ハブ)とは Cost Optimization Hubとは、コスト最適化に関する推奨事項を統合的に閲覧できるAWSのコストツールです。 どのリソースでどのくらいコスト削減できそうかがわかります。 また、表示される削減額は購入済みのReserved InstancesやSavings Plans等を考慮した上で、請求額がどれだけ変わるかを見積もった金額になっているようです。 そのため、定価どうしの単純な比較ではなく、実際の請求に近い形で優先順位を付けられます。 Cost Optimization Hub による機会の特定 - AWS コスト管理 削減額機会のページには、下記の項目があります。 削減額機会の一覧のヘッダ 削減額機会の一覧のヘッダ2 「最も推奨されるアクション」の欄には、「アップグレード」「アイドル状態または未使用のリソースを削除」「適切なサイズ設定」など、どのような方法でコスト削減が期待できるかが表示されます。 そのため、明確なアクションを計画することができます。 また、「実装作業」の欄には、実際にアクションする際の作業コストが「非常に低い」「高」などランクづけされて表示されます。 ただし、実装作業コストが低いからといって、安易に実行してよいとは限りません。 例えばReserved InstancesやSavings Plansの購入は、作業としては購入するだけなので手間はありませんが、年単位のコミットメントを負うことになり、購入後に条件を変更することはできません。 作業の手間と判断の重さは、別のものとして考えたほうがよさそうです。 結果 今回は比較的実装作業コストが低い、不要なリソースの削除や開発時に使用しているリソースのスケールダウンなどを行いました。 その結果、導入したかったツールの検証費用分は削減できました。 コスト削減に向き合う中で得た気づき 実際にコスト削減をしてみて、改めて大事だと思ったことがいくつかありました。 無駄を減らす 当たり前のことではあるのですが、コスト削減において無駄を減らすのは最も確実な手段だと思います。 使われていないリソースは、止めても何かが動かなくなるわけではありません。 そのため誰かが困って報告することもなく、気づかれないまま動き続ける可能性があります。 一方で、一旦使われていないことがわかれば、削除作業自体はとても簡単にできます。 技術的負債の解消でコストも減る 技術的負債の解消により、実装コストだけでなく、ツールやインフラのコストも減る可能性はあると思いました。 監視ツールを例に挙げると、多くは扱うデータ量に応じた従量課金です。 無駄な処理が多ければ、そのぶん送られるデータも増えて料金が上がります。 逆に処理を整理すればデータ量が減り、その分安くなります。 負債を抱えたままでも動いてはいるので、普段は困りません。 ですが動かし続けるための費用は、その裏で払い続けていることになります。 もっとも、これは削減額として測りにくい部分でもあります。 「この負債を解消したから月いくら下がった」と切り出すのは難しいので、あくまで副次的な効果として捉えています。 また、上述の「無駄を減らす」にも繋がるのですが、使用していないツールやインフラのリソースは、それ自体がメンバーの認知負荷を高める負債にもなり得ます。 そのため、負債を減らすと言う意味でもやはり「無駄を減らす」のは大切だと思いました。 キャッチアップが大事 コスト削減においても技術的な情報のキャッチアップは大事だと思いました。 AWSなどの外部サービスを使っていると、新サービス・新機能を使ったり、バージョンをアップデートするだけで料金が下がることがあります。 たとえばAWSで言うと、 EBSをgp2からgp3にアップデートする インスタンスをx86からGravitonに移行する などでコスト削減を行うこともできます。 Amazon EBS 汎用 SSD ボリューム - Amazon EBS ARM Processor - パフォーマンスプロセッサ - AWS EC2 Graviton - AWS こういったことは、日々キャッチアップをしていないとなかなか気づきづらいかなと思います。 というのも、一度組んだ構成は動いている限り見直す機会は多くありません。 そのため、安くなる選択肢が出ていても、こちらから探しにいかないと気づけないままになります。 その点では、Cost Optimization Hubのようなツールに頼るのも一つの手だと思いました。 アップグレードの推奨を出してくれるので、自分が追いきれていない部分を拾ってもらえます。 まとめ 本記事では、有料ツールの導入に向けてAWSのコストを削減した話を紹介しました。 Cost Optimization Hubを使うと、どこにムダがあるかと、それを直す作業のコストまで一覧で見られます。 まずはここを見て、作業コストの低いものから手を付けるのが取りかかりやすいと思います。 そして今回やってみて感じたのは、コスト削減は一度やって終わりではないということです。 無駄なリソースは気づかないうちに増えますし、新しい機能を追えていないだけで払い続けている分もあります。 最後まで読んでいただきありがとうございました! 参考文献 Cost Optimization Hub による機会の特定 - AWS コスト管理 削減の機会の表示 - AWS コスト管理 月間節約額の見積り - AWS コスト管理
こんにちは。開発1部でデリッシュキッチンのプレミアム機能を開発している新卒エンジニアの野村です。 初めて渡されたタスクの話です。実装がほぼ終わったタイミングで、大きな手戻りに気づきました。新しく追加するはずだった機能の一部が、すでに存在していました。 なぜ最後まで気づけなかったのかを振り返り、AIエージェントを使った開発で何を見落としやすいか、既存のシステムにどう向き合うべきかを考えました。 Claude Codeと進めたタスクで、既存機能を見落とした 2026年4月にエブリーへ新卒として入社し、研修を終えて最初に渡されたタスクは、アプリのLPに出すボタンの種類を切り替える機能でした。 デリッシュキッチンには個人プランとファミリープランの2種類の課金プランがあり、それぞれに無料期間があります。どちらのプランを出すかと、その無料期間を何日にするかを、それぞれダッシュボードから変更できるようにする、というのが依頼の内容でした。 タスクの進め方は、DesignDocument(DD)を作成してレビューを受け、サーバーを実装し、最後にダッシュボードを実装する流れでした。DDには、何を作るかに軽く触れたうえで、どう作るかを中心に書きます。エブリーではClaude Codeが全社で使えるようになっていたので、DDの作成からサーバーの実装まで、Claude Codeに相談しながら進めました。 フロントエンドの実装に入ったとき、修正箇所のコードに「無料期間」と書かれたフィールドがすでにあることに気づきました。調べてみると、個人プランの無料期間は、すでにダッシュボードから変更できるようになっていました。DDを見返すと、テーブル設計にもAPI設計にも、既存の無料期間フィールドはきちんと書かれていました。 DDはClaude Codeと相談しながら自分で出したものでしたが、そこに書かれていた既存の無料期間を、実装が終わるまで読み取れていませんでした。 ゼロから作っていた学生時代は、AIの出力を判断できた 学生時代は、インターン先でiOSアプリやWebサービスの開発をしていました。2023年から2025年にかけての時期で、何を作るかは案件ごとに違いましたが、共通していたのは、設計から実装まで自分でゼロから考えて作っていたことです。 当時はまだAgentのように自律的に動くAIはなく、ChatGPTへのコピペや、コード補完としてAIを使う程度でした。システムがどう動いているかは自分自身で作っているので、人に詳しく説明できる状態で開発を進めていました。 2025年の半ばにClaude Codeを使い始めてからは、それまでの実装時間よりもずっと速く開発できるようになりました。この速さを知った状態で、2026年4月にエブリーへ入社しました。 ゼロから作っていた開発では、AIが何を出力しても、それが正しいかどうかを自分の頭の中にある設計と照らし合わせて判断できました。そう判断できたのは、システムがどう動くべきかという理解が、判断軸として自分の中に自然にあったからです。 判断軸がないまま、AIに進めてもらった 私が渡されたタスクは、自分でゼロから作るものではありませんでした。何年も運用されてきた既存のシステムに、新しい機能を追加するものでした。 学生時代の開発速度をイメージしていましたが、実際に進めてみると、想像していたよりも、私のタスクを進めるスピードは遅いと感じていました。AIの出力の意味が自分にはわからないことが、進みを遅くしていました。 説明されてもそれが事実かどうか腑に落ちないまま、新卒として早く成果を出したい気持ちと、以前の開発速度への意識に押されて、そのまま進めていました。DDに対してはPRでレビューをもらえる仕組みがあり、そこで安心感を得ていた面もあります。 既存のシステムで実際に製品として触れる部分があっても、自分の手で触るのではなく、AIに調べてもらうという進め方をしていました。 AIとのやり取り越しでは、既存の仕様に気づけない 無料期間フィールドの存在に気づかなかったのは、コードを読む以前の話です。プロダクトのUIを自分で触っていれば、あるいはAPIの入出力を自分で確認していれば、個人プランの無料期間がすでにダッシュボードから変更できると気づけたはずでした。AIとのやり取り越しに見ているだけでは、そこに気づく機会がありませんでした。 自分が頼んだのは「個人プランの表示・非表示を切り替え、表示する場合は無料期間を設定できるようにする」という指示でした。この指示自体に、既存の無料期間フィールドとの整合性を確認する視点が入っていませんでした。既存にすでにある機能を、新しくAPIに追加しようとしている、という違和感に、指示を出す前に気づく余地はありました。 そこに気づかないまま、AIが返す説明に納得してしまうという状態が続いていました。たとえば「本当にそうなっていますか」とAIに確認しても、返ってくるのはこちらが納得できる説明です。その説明が実際に妥当かどうかを見極める判断軸がないまま、説明を読んで納得してしまう、という繰り返しでした。 DDを自分の手で書き、コードを自分の手で追った この見落としのあと、進め方を変えました。DDは自分の手で書き、AIは補助として使うようにしました。 DDのテンプレートに、目的、ゴール、非ゴール、システムの概要図、DBとAPIの変更、といった項目を自分で作りました。このテンプレートに沿って、まず自分の言葉で埋めていくようにすると、タスクを進めるうえで意識しなければならないことが、それまで抜け落ちていたと気づきました。 なかでも効いたのは、ゴールの項目に「どうなれば成功と言えるか」を自分の言葉で書く作業でした。ここが埋められていなければ、AIが出した実装がそれを満たしているかどうかも判断できません。 コードについても、AIに解説してもらうことはありましたが、最終的には自分の手で追うようにしました。この2つを変えただけで、考慮漏れは明らかに減りました。 AIと対話を重ねて理解しようとする進め方には、限界がありました。目の前のタスクは解決しますが、次の課題に当たったときにはまた同じように対話を重ねる必要があり、理解が場当たり的になっている感覚がありました。コードという一次情報を自分で追うようにしてからは、理解が積み上がっていく実感がありました。 まとめ: 判断軸は、一次情報からしか作れない 検証する判断軸は、AIの解説を聞くだけでは作れないと感じています。AIの解説は、こちらが理解できているかどうかにかかわらず、同じようにわかりやすく返ってきます。そのため、判断軸がないままでも、わかったつもりになってしまいます。 判断軸を作るには、DDを自分の手で書く、コードを自分の手で追うといった、一次情報に自分であたる作業が必要だと思います。ゼロから作る開発では、実装そのものが一次情報になるので、判断軸は作業の中で自然にできあがります。既存のシステムに新しく加わる開発では、自分からあたりにいかない限り判断軸はできあがらないというのが、今回の経験でした。その作業を積み重ねることが、次の課題にも使える判断軸を作る方法だと考えています。 新卒として早く成果を出したい、AIを使って早く実装したい、という気持ちは今もあります。同じように感じている方は多いのではないかと思います。ただ、急いで進めた結果が今回の手戻りでした。遠回りに見えても、一次情報にあたって理解を積み上げていくほうが、最終的には早いのだと感じています。 今回は新卒として入社した自分の経験ですが、新卒に限った話ではないとも思っています。中途入社や異動で既存のシステムに新しく参画するときも、そのシステムについての判断軸はまだ持っていない状態から始まります。自分が理解できていないシステムに携わるときは、同じことが当てはまるのではないかと感じています。 ここに書いたのは、新卒としてエブリーで働き始めてまだ数ヶ月の自分が、今の時点でたどり着いた解決策です。自分が経験を重ねることでも、AIの精度が上がっていくことでも、この考え方自体は更新されていくのだと思います。
こんにちは、トモニテ開発部 iOS エンジニアの村田です。 iOS エンジニアもしくは Android・クロスプラットフォームを含めてモバイルエンジニアの皆さんに対して気になっていることがあります。 みなさん AI 開発どんな感じでやってますか?どんなハーネス組んでますか? エンジニアリング業界では、単に指示を出して書いてもらう「バイブコーディング」の次の段階として、AI エージェントの自律化や「ハーネスエンジニアリング」「ループエンジニアリング」といった話題が急速に広がっています。 Web やサーバーサイド領域では、こうした最新の AI 活用の情報が活発に共有されている一方、iOS(モバイル)開発における実践的なナレッジはまだまだ各所に散らばっており個々で孤軍奮闘している印象を受けています。 私自身、久しぶりに iOS 開発に取り組んでいて、Claude Code と git worktree を用いた並行開発をする中で iOS 特有のハマりどころをいくつか感じました。 今回は「並行開発」という観点にフォーカスして、iOS で並行開発したときに感じた課題と対処方法を記載します。 開発環境 前提として、以下のような環境・仕組みで開発しています。 使用技術 AIエージェント : Claude Code エディタ : VSCode 並行開発 : Git Worktree 対象 : iOS アプリ(Swift / UIKit / SPM / .xcodeproj) ビルド方法 : XcodeBuildMCP(Claude 経由の MCP)・XcodeBuildMCP(CLI 版)・Xcode(GUI) ディレクトリ構成 <repo>-trees/ ← worktree develop/ ← メイン worktree(base ブランチ) feature-A/ ← タスクごとの worktree(並列) feature-B/ ← タスクごとの worktree(並列) refactor-C/ ← タスクごとの worktree(並列) ... 開発手順 タスク開始時に git worktree add で develop ブランチから新規 worktree を切る worktree 配下で Claude Code のセッションを開始し、要件定義 → 実装 → PR 作成など開発フローを進行 タスクが終わったら、その worktree ごと片付ける ---bin なぜ並行開発が欠かせないのか あちこちで語られている話なので、要点だけ。 AI に開発させると、人間の役割は「実装する人」から「複数のエージェントを監督する人」に変わります。「どこまで自律的に AI に開発させられているか」にも依りますが、設計・実装・ビルド・テストと多くのフェーズで人間の手が空くようになるため、1 タスクを直列で進めるよりも複数タスクを並行で回す方が効率よく進められます。 そこで注目を浴びたのが git worktree です。ブランチごとに独立した作業ディレクトリ(worktree)を持てるため、作業ファイルの競合を気にせず、安全に複数タスクを同時進行できます。 そうして git worktree を用いて iOS 開発を始めたのですが、いくつか課題が発生しました。 問題その1: DerivedData がストレージを食い尽くす 何が起きたか iOS のビルドでは DerivedData を生成します。DerivedData には Xcode がビルドのたびに吐き出す中間生成物(ビルドキャッシュ・インデックス・成果物など)が含まれており、デフォルトでは ~/Library/Developer/Xcode/DerivedData/ に生成されます。XcodeBuildMCP でビルドする場合は、 ~/Library/Developer/XcodeBuildMCP/workspaces/<worktree>-<hash>/DerivedData/<プロジェクト名>-<hash> に生成されます。 DerivedData は worktree の内部ではなく、ユーザー共通の場所にまとめて溜まるため、worktree の数に比例して独立したビルドキャッシュが蓄積されていきます。 私の環境では 1 worktree あたり数 GB〜20 GB 台、合計およそ 86 GB になっていました。 結果としてディスクの空き容量が枯渇し、スワップ領域も確保できなくなって、ビルドどころではなくなりました。 # Xcode(GUI)の既定 — プロジェクト名 + ハッシュ名 ~/Library/Developer/Xcode/DerivedData/ ├── <プロジェクト名>-a1b2c3d4efgh…/ ├── <プロジェクト名>-e5f6g7h8ijkl…/ ├── <プロジェクト名>-i9j0k1l2mnop…/ ├── <プロジェクト名>-q7r8s9t0uvwx…/ └── … # XcodeBuildMCP の既定 — worktree 名 + ハッシュ名 ~/Library/Developer/XcodeBuildMCP/workspaces/ ├── develop-a1b2c3…/DerivedData/ 19 GB ├── feature-A-d4e5f6…/DerivedData/ 21 GB ├── feature-B-g7h8i9…/DerivedData/ 11 GB ├── feature-C-j0k1l2…/DerivedData/ 8.8 GB └── …(他 4 worktree) 26 GB 計 86 GB キャッシュを削除しようと試みたところで Xcode の場合、DerivedData のフォルダ名がハッシュ化されていて、フォルダ名だけではどの worktree に対応するかわからない ビルドしなくてもインデックス更新などで更新日時が変わるため、「更新日が古い=不要」という判断も効かない といった理由により、使い終わった worktree のゴミだけを削除することが難しかったです。 かといって全部消すと、全 worktree がコールドビルドに逆戻りしてしまいます。 XcodeBuildMCP はデフォルトでフォルダ名に worktree 名が入るため対応関係は分かりやすいのですが、 git worktree remove (worktree の削除)をしても、この孤児フォルダが残り続ける点は Xcode の既定と同じです。 対処方針: DerivedData を worktree 配下に保存する 対応策として DerivedData の置き場所を worktree フォルダの直下( <worktree>/DerivedData )に固定すると、次のメリットが得られました。 どのキャッシュがどの worktree のものか、置き場所を見れば分かる git worktree remove すれば DerivedData も一緒に消える 開発時のライフサイクルは以下のようなイメージです。 DerivedData の置き場所を変更する方法 ① Xcode の GUI でビルドする場合 全プロジェクト一律でよい場合 : Xcode → Settings → Locations → Derived Data を Default から Relative に変える 特定のプロジェクトだけ有効にしたい場合 :対象プロジェクトを Xcode で開いた状態でメニューの File → Workspace Settings… ( .xcworkspace を開いていない場合は Project Settings… )を選び、 Derived Data を Workspace-relative Location に切り替える 後者の場合、実態としてはプロジェクト内の WorkspaceSettings.xcsettings ( xcuserdata 配下・通常は Git 管理外のユーザーローカル)の設定と同等のため、GUI を使わず次の内容を直接書いても実現できます。 <? xml version = "1.0" encoding = "UTF-8" ?> <!-- <project>.xcodeproj/project.xcworkspace/xcuserdata/<user>.xcuserdatad/WorkspaceSettings.xcsettings --> <! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" > <plist version = "1.0" > <dict> <key> DerivedDataLocationStyle </key> <string> WorkspaceRelativePath </string> <key> DerivedDataCustomLocation </key> <string> DerivedData </string> </dict> </plist> ② XcodeBuildMCP でビルドする場合 こちらは .xcodebuildmcp/config.yaml の derivedDataPath で決まります(worktree のルートからの相対パスで解決されます)。 # <worktree>/.xcodebuildmcp/config.yaml schemaVersion : 1 sessionDefaults : projectPath : "<プロジェクトパス>" scheme : "<スキーム名>" derivedDataPath : "./DerivedData" platform : "iOS" useLatestOS : false bundleId : "<バンドル ID>" 💡 どちらの方法でも DerivedData がリポジトリ配下に生成されるため、 .gitignore に DerivedData/ を追加する必要があります。 ただし「Xcode 版で特定のプロジェクトだけ Relative にしている場合」や「XcodeBuildMCP 版で .xcodebuildmcp/config.yaml を Git 管理していない場合」は、新しい worktree を作るたびにこれらの設定を行う必要があります。毎回手動で設定するのは面倒なので、Git の post-checkout hook で worktree の作成時に自動設定されるようにするのがおすすめです。 post-checkout hook で DerivedData の設定を自動化する git worktree add や git checkout を実行すると、 post-checkout という hook が発火します。hook は共通の Git ディレクトリ( git rev-parse --git-common-dir )に置かれ全 worktree で共有されるので、そこに設定することで新規 worktree 作成時の処理を仕込むことができます。 下記のような処理を .git/hooks/post-checkout に記述することで、新規 worktree 作成時に「DerivedData を各 worktree 配下( <worktree>/DerivedData )に保存する」ように設定できます。 なお以下の hook では ② の config.yaml を直接出力していますが、正の config.yaml をデフォルトブランチなどで管理し、post-checkout で cp する方式でも構いません。 #!/bin/bash # .git/hooks/post-checkout if [ " $3 " = "1" ]; then PROJECT = $( find . -maxdepth 1 -name " *.xcodeproj " | head -1 ) if [ -n " $PROJECT " ]; then # ① Xcode GUI 用: WorkspaceSettings に worktree 相対の DerivedData を書く DIR = " ${PROJECT} /project.xcworkspace/xcuserdata/ $( whoami ) .xcuserdatad " PLIST = " ${DIR} /WorkspaceSettings.xcsettings " mkdir -p " $DIR " [ ! -f " $PLIST " ] && plutil -create xml1 " $PLIST " plutil -replace DerivedDataLocationStyle -string WorkspaceRelativePath " $PLIST " plutil -replace DerivedDataCustomLocation -string DerivedData " $PLIST " # ② XcodeBuildMCP 用: config.yaml を worktree 直下に生成 if [ ! -f .xcodebuildmcp/config.yaml ]; then mkdir -p .xcodebuildmcp cat > .xcodebuildmcp/config.yaml <<'YAML' schemaVersion: 1 sessionDefaults: projectPath: "<プロジェクトパス>" scheme: "<スキーム名>" derivedDataPath: "./DerivedData" platform: "iOS" useLatestOS: false bundleId: "<バンドル ID>" YAML fi fi fi 問題その2: シミュレータを複数セッションが奪い合う 何が起きたか iOS 開発では、シミュレータ上での実操作が必要な場面が多々あります。 画面情報・遷移情報の取得 コード変更後の動作確認 E2E テストの実行 AI に並行開発させている中でこれらの動作をさせようとすると、1 台のシミュレータを複数のセッション(エージェント)が奪い合い、開発が滞る問題が発生しました。 操作の割り込み・競合: あるエージェントの検証中に、別のエージェントが起動・操作を行って画面を奪い合う 状態の破壊: 実行中のアプリ領域やデータが上書きされ、E2E テストや動作確認が誤判定される 処理の順番待ち: シミュレータの空き待ちが発生し、並行開発のスピード感が失われる 対処方針: worktree ごとに専用シミュレータを持たせる 対策として、 worktree ごとに専用シミュレータを持たせる運用にしました。 具体的にはシミュレータ名を <worktree 名> - <元の機種名> (例: feature-A - iPhone 16 Pro )のように紐付けます。これにより「このセッションが触っていいのはこの 1 台のみ」と明確にし、他のセッションで稼働中のシミュレータへの誤干渉を防いでいます。タスクが完了して役目を終えたシミュレータはシャットダウンし、別の worktree での開発で名前を変更して再利用する流れです。 💡 基本的には AI による並行開発を前提としていますが、人間が最終的な動作確認を行う場合にもメリットがあります。各 worktree 専用のシミュレータ上にビルド済みアプリがそのまま残っているため、スムーズに動作確認を進められます。 シミュレータの選定ルール ある worktree の開発で使うシミュレータの選定ルールは、以下のようにしました。 worktree 名で始まるシミュレータが起動済みなら、それをそのまま使う worktree 名で始まるシミュレータが未起動なら、起動して使う 無ければ、起動していないシミュレータを <worktree 名> - <元の機種名> にリネームして起動する これを実装に落とし込んだものが以下のコードです。 resolve_udid は ①→③ の順に「その条件に合うシミュレータがあるか」を確認していき、最初に見つかった 1 台を、その worktree に対応するシミュレータの UDID(デバイスの一意 ID)として採用します(見つかった時点で残りは確認しません)。あとはこの UDID を指定してビルドすれば、その worktree 専用のシミュレータに向けて実行できます。 wt = " $( basename " $PWD " ) " # worktree 名(例: feature-A)をシミュレータ名の接頭辞に使う # シミュレータ一覧を JSON で取得して devices_json にキャッシュする refresh_devices() { devices_json = $( xcrun simctl list devices available --json ) ; } # 未起動のシミュレータを起動する boot_sim() { xcrun simctl boot " $1 " 2 > /dev/null ; } # UDID から元の機種名を引く(既に worktree 名が付いていれば落とす。付け足すと再利用のたびに接頭辞が積み重なるため) sim_name() { printf ' %s ' " $devices_json " | jq -r --arg u " $1 " \ ' [.devices[][] | select(.udid == $u) | .name][0] // empty | sub("^.+ - "; "") ' } # 指定した起動状態(booted / unbooted)で、worktree 名で始まるシミュレータの UDID を返す(無ければ空文字) pick_named() { printf ' %s ' " $devices_json " | jq -r --arg wt " $wt " --arg want " $1 " \ ' [.devices[][] | select((.name | startswith($wt + " - ")) and (if $want == "booted" then .state == "Booted" else .state != "Booted" end)) | .udid][0] // empty ' } # 未起動(Shutdown)の空きシミュレータを1台返す(無ければ空文字) pick_spare() { printf ' %s ' " $devices_json " | jq -r \ ' [.devices[][] | select(.state == "Shutdown") | .udid][0] // empty ' } resolve_udid() { refresh_devices # ① worktree 名で始まるシミュレータが起動済みなら、そのまま使う udid = $( pick_named booted ) ; [ -n " $udid " ] && { echo " $udid "; return; } # ② worktree 名で始まるシミュレータが未起動なら、起動して使う udid = $( pick_named unbooted ) ; [ -n " $udid " ] && { boot_sim " $udid "; echo " $udid "; return; } # ③ 無ければ、空きシミュレータの接頭辞を「<worktree 名> - 」に付け替えて起動する spare = $( pick_spare ) [ -z " $spare " ] && { echo " 空きシミュレータがありません " >&2; return 1 ; } xcrun simctl rename " $spare " " $wt - $( sim_name " $spare " ) " boot_sim " $spare " echo " $spare " } 開発フローとクロージング処理 ここまでの処理を開発フローに沿って並べると次のようになります。 開始 : タスクごとに worktree を切り、専用の作業場を用意する 実装 : その worktree で Claude セッションを開始し、1 つの機能を実装する 動作確認 : その worktree 専用のシミュレータを起動し、ビルド & 実行・テストを行う レビュー : 実装が固まったら PR を作成する クロージング処理 : PR マージと同時に、worktree 削除・Issue クローズ・シミュレータのシャットダウンを行う 開発フローの最後には、クロージング処理を置いています。 close-feature のようなスキルにまとめ、PR マージ → Issue クローズ → worktree 削除 → 専用シミュレータのシャットダウンを一括で実行しています。 このクロージング処理をフローに組み込むことで、問題その1で挙げた DerivedData によるストレージ圧迫を防ぎつつ(worktree を削除すれば配下の DerivedData も一緒に消える)、起動したままのシミュレータが積み上がってメモリを圧迫するのも同時に抑えられます(シャットダウンした端末は、次のタスクで再利用される)。 問題その3: .xcodeproj のコンフリクトが多発する .xcodeproj を Git 管理しているとブランチの切り替えやマージのたびにコンフリクトが起きやすい、という問題があります。これ自体は昔からある iOS 開発の悩みですが、AI に複数タスクを並行開発させるようになってコンフリクトの頻度も増え、無視できないコストになったと感じています。 トモニテの PJ では実現できていませんが、下記の理由により XcodeGen を用いた YAML 管理への移行を検討しています。 1. 並行開発でのコンフリクト軽減 git worktree などを活用して複数ブランチでの並行開発を進める場合、 project.pbxproj のコンフリクトが多発する可能性があります。 特に AI の活用によって開発の速度や並行性が上がるほど、その発生頻度も多くなりがちです。 XcodeGen を導入して .xcodeproj を自動生成の成果物として扱い .gitignore に追加することで、 .xcodeproj でのコンフリクト問題を解消できると考えています。 2. AI と YAML の相性 Xcode のプロジェクト管理は GUI 操作を前提とした仕組みであり、AI エージェントとは相性が良くありません。これを project.yml による宣言的なテキスト管理に切り替えることで、「YAML での管理・変更 → コマンド実行( xcodegen generate )」という AI に適したフローで構成を変更できるようになると考えています。 3. コードレビューのしやすさ コード差分が複雑な pbxproj からシンプルな project.yml に変わることで、変更内容を容易に把握できます。 AI がコードを書き、人間がレビューするフェーズでは可読性の観点でもメリットがあると考えています。 まとめと残課題 ここまで git worktree を用いた iOS の並行開発で出会った問題点と、その対処方法を紹介してきました。 とはいえ、こうして並行タスクを回す仕組みを整えても、human-in-the-loop 的な体制では並行開発の効果も限界があると感じています。監督する人間のコンテキストスイッチがボトルネックになるため私の場合、同時に見られるのは 3〜4 タスクほどでした。これでは開発生産性も頭打ちになり、「プロダクトや事業をどう伸ばすか」という本質的な問いに集中できないと感じています。 この課題を突破するためには、やはり「ループエンジニアリング」など AI がより自律的に開発を回せるような仕組みを追求していくことが不可欠だと感じています。 関連リンク XcodeBuildMCP XcodeGen