TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

976

.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、グローバルシステム部フロントエンドブロックの林です。 私が所属するチームでは ZOZOMETRY というBtoBサービスを開発しています。スマートフォンで身体を計測し、計測結果を3Dモデルやデータとして可視化・Web上で管理できるサービスです。 私たちのチームではAIにユニットテストを書かせ、マージまでの過程を改善する施策を実施しました。結果としては、2か月でテスト数が57%増え、カバレッジは約2倍になりました。 この取り組みはテストを増やすという面ではうまくいきましたが、AIが書いたコードを人間がどうレビューするかという点で、いくつかの壁にぶつかりました。 この記事では、以下の点を紹介します。 AIが書いたテストコードを素早くレビューするために、どのような仕組みを設計したのか 運用する中でどのような課題が見えてきて、どう対処したのか AIと協業する開発フローにおいて、人間が関与すべきポイントはどこだったのか 目次 はじめに 目次 背景と課題 テスト生成の仕組み Claude Codeコマンドの設計 統一フォーマット describeのネスト構造 テスト名と日本語コメント テスト対象ごとの実装パターン テストサマリの付与 成果 運用で見えた課題 AIの生成速度と人間のレビュー速度のミスマッチ 「ノールックでマージするのは怖い」 「インプットとアウトプットだけ見ればいい」仮説の崩壊 課題への対策 サマリの自動生成でレビューの入口のハードルを下げる 粒度の制御でレビュー1回あたりの負荷を下げる 目視確認のプロセス化 振り返り:AI協業における人間の関与ポイント AI生成コードのレビューで人間が見るべき範囲 生成速度とレビュー速度のバランス設計 導入コストを下げるアプローチ まとめ 背景と課題 私たちのチームでは、機能開発を優先するあまりテストが慢性的に不足しており、以下のような課題が続いていました。 品質管理はQAチームに大きく依存している状態 テスト作成の品質や粒度にばらつきがある テストの目的や内容を理解するためのドキュメントが十分に整備されておらず、「このテストは何を守っているのか」を説明しにくい 施策の開始時点でのテスト数は324件、カバレッジは4.72%でした。 この状況を改善するにあたって、いくつかの選択肢がありました。人手でテストを書くのが最も確実ですが、機能開発と並行して進めるリソースがありませんでした。AIにテストを生成させれば速度は出ますが、品質の保証は未知数です。 結果として、AIにテストコードを生成させ、人間がレビューする体制を選びました。とはいえ、最初からAIに品質を丸投げできるとは考えていませんでした。この実験にはもう1つの目的がありました。AIと協業するうえで、人間が関与すべきポイントはどこなのか。それを見出すための取り組みでもあったのです。 テスト生成の仕組み テスト生成の仕組みを以下の3点で構成しました。 Claude Codeコマンドによるテスト生成の定型化 統一フォーマットによるテスト構造の標準化 テストサマリの自動付与 Claude Codeコマンドの設計 Claude Codeのカスタムコマンド /create-unit-test を作成しました。このコマンドは対象ファイルのパスを受け取り、以下のワークフローを順に実行します。 対象ファイルの分析 :ファイルタイプ(フック / ユーティリティ / ストア / コンポーネント)を特定し、エクスポートされる関数の一覧や依存関係を把握する テスト設計書の作成 : docs/test-design/ にテスト設計書を生成し、テストケースを正常系・異常系・エッジケースに分類する テストファイルの作成 :設計書に基づいてテストコードを test/unit/ に配置する テスト実行と検証 : pnpm test でテストを実行し、カバレッジを確認する テストサマリの記録 : docs/test-summaries/test-summary.md にテスト内容を追記する # 実行例 /create-unit-test hooks/useClientData.ts /create-unit-test utils/detectGender.ts 各ステップでユーザーの承認を挟む設計にしています。AIに一気に生成させるのではなく、分析→設計→実装→検証の各段階で人間が判断する余地を残しました。 コマンドの設計で重視したのは再現性です。誰が実行しても同じ粒度・同じ構造のテストが生成されることで、レビューする側の認知負荷を一定に保つことを狙いました。 統一フォーマット 生成されるテストの構造を揃えるために、以下のルールを定めました。 describeのネスト構造 テスト対象の関数ごとに describe をグループ化し、その中を Success case / Error case / Edge cases に分類します。 describe ( 'useCreateClient' , () => { describe ( 'Success case' , () => { ... } ); describe ( 'Error case: Argument problems' , () => { ... } ); describe ( 'Error case: Response errors' , () => { ... } ); describe ( 'Edge cases' , () => { ... } ); } ); この構造が揃っていることで、レビュアは「このテストはどの分類のケースを見ているのか」をコードの構造から即座に判断できます。 テスト名と日本語コメント テスト名は should [期待される動作] の形式で統一しました。加えて、各 describe や it の前に日本語コメントを付けることで、テストの意図をコードを読み込まずとも把握できるようにしています。 // 性別判定機能のテスト describe ( 'detectGender' , () => { // 男性の場合、正しいメッセージを返すことを確認 it ( 'should return the correct message for MALE' , () => { expect (detectGender( 'MALE' )).toEqual( 'Male' ); } ); } ); テスト対象ごとの実装パターン 対象のファイルタイプに応じて、テストの書き方を使い分けています。テストケースが少ないフックには renderHook を使い、セットアップを簡潔に保ちます。テストケースが多いフックには直接呼び出しと describe のネストを組み合わせ、テストケースごとの独立性を確保します。ユーティリティ関数は入力と出力の対応を直接検証し、Zustandストアは act で状態更新をラップすることでReactの非同期性に対応しています。 この使い分けもコマンド側で自動的に判断するため、生成されたテストのパターンがばらつくことを防いでいます。 テストサマリの付与 テスト実行後、 docs/test-summaries/test-summary.md にサマリを追記する仕組みを導入しました。サマリには以下の情報を含めています。 テスト対象ファイルとタイプ テスト内容:関数シグネチャと、どの分類(正常系・異常系・エッジケース)をテストしたか テスト結果:成功数 / 全体数 以下は実際のサマリの例です。 ## ` utils/fileName.ts ` - 2025-12-04 14:28:00 **タイプ**: ユーティリティ **テストファイル**: ` test/unit/fileName.test.ts ` ### テスト内容 - ` getDisplayFileName(name, maxLength?, headLength?): string ` - 正常系(短い/長いファイル名、デフォルトパラメータ、境界値)、エッジケース(空文字列、日本語) - ` isValidFileName(name, maxLength?, includeExtension?): boolean ` - 正常系(英数字・日本語・記号)、異常系(不正な拡張子、長さ超過)、エッジケース(複数ドット、最小長) **結果**: ✅ 全テスト成功 (32/32) このサマリはPRのレビュー時にも参照します。レビュアはまずサマリを読んでテストの全体像を把握した後で、実際のコードに問題点がないかを確認するフローにしました。 成果 2か月の実施期間で、ユニットテスト数は324件から509件へ57%増加しました。カバレッジは4.72%から9.25%へ、約2倍に改善しています。 定量的な成果に加えて、以下の定性的な改善もありました。 テスト設計書とサマリが蓄積されたことで、テストの目的やカバー範囲をチーム全体で把握できるようになった テストの構造が統一されたことで、レビュー時に「何を見ればいいか」が明確になった 既存テストの品質を見直すきっかけにもなった 運用で見えた課題 成果は出ましたが、運用する中でレビュー面の課題が顕著になりました。課題の本質は「AIの出力品質」ではなく、正しいと判断するための「検証コスト」にありました。 AIの生成速度と人間のレビュー速度のミスマッチ AIによりPull Request(以下PR)の生成時間が大幅に短縮されたため、未レビューのPRが溜まるようになりました。PRを作った側にはレビュー依頼やリマインドへの心理的障壁が生まれました。レビューする側も次々と届くPRにプレッシャーを感じる状態でした。この状態でチームの生産性を最大化するのは難しいものでした。 「ノールックでマージするのは怖い」 AIが書いた、品質に直結する部分のコードをノールックでマージするのは怖いと感じました。チームで話し合った結果、人間が差分を目視で確認することにしました。 しかし目視確認にも課題が隠れていました。PRの粒度が大きくなりがちで、人間の認知負荷が増加したのです。 「インプットとアウトプットだけ見ればいい」仮説の崩壊 CI/CDで実行を管理しているので、変更されたコードを見なくてもインプット(プロンプト)とアウトプット(テスト実行結果)だけ確認すればいいのではないか。そういった仮説を立てました。 しかし現実には、インプットが本当に期待しているインプットなのかを判断するためのコンテキストが属人化していました。設計や詳細なコードを把握していないメンバーは自力で調査する時間が増え、かえって非効率になりました。この状態を改善しなければ、サービスの品質向上や本質的な改善は難しい状況でした。 課題への対策 これらの課題に対して、3つの施策で対処しました。 サマリの自動生成 AIにプランニングさせ粒度を制御する仕組み 人間が差分を目視で確認するプロセスを明示的に残す サマリの自動生成でレビューの入口のハードルを下げる テストされている箇所の設計や実装を把握していないメンバーでもレビューに入りやすくすることを目的としています。前述のサマリを活用したレビューフローを通じて、不慣れな領域でもテストの全体像をあらかじめ把握した状態でコードレビューへ臨めるようにしました。 これにより、不慣れな領域のレビューに対する心理的障壁を軽減し、迅速にレビューへ入れるようになりました。 粒度の制御でレビュー1回あたりの負荷を下げる コマンド実行時、どの範囲のテストを作成するかAIへプランニングさせる仕組みにしました。PRサイズは100行程度を目安に設定しています。 テストカバレッジを一度に大きく上げたくなりますが、レビューする側の認知負荷を超えないことでレビューに臨むハードルを下げることができました。 目視確認のプロセス化 「ノールックでマージしない」というチームの方針に基づき、人間が差分を目視で確認するプロセスを明示的に残しました。AIの出力を無条件に信頼するのではなく、品質の最終判断は人間が担う体制です。 これらの改善施策により、レビューまでのリードタイムが減りメンバーの心理的な負担も少なくなりました。 振り返り:AI協業における人間の関与ポイント この実験を通じて、AIと協業する開発フローにおけるいくつかの知見が得られました。 AI生成コードのレビューで人間が見るべき範囲 「インプットとアウトプットだけ見ればいい」という仮説は成立しませんでした。コンテキストの共有が前提条件として必要であり、それが属人化している状態では、コードの差分を目視で確認する以外に品質を担保する手段が見つかりませんでした。 チームが出した結論は「差分のコードを目視で確認するのは、やはり人間が担当すべき」というものです。レビューのコストが上がる課題は引き続き残りますが、品質の担保を優先しました。 生成速度とレビュー速度のバランス設計 AIの生成速度に人間が追いつけない構造的な問題に対しては、生成側で粒度を制御することが有効でした。レビュー側の運用を変えるのではなく、生成側の出力を調整するアプローチです。 導入コストを下げるアプローチ 完全に新しいプラクティスを一から導入するのはコストが高いため、現行の開発フローをコンポーネント化し、AIに任せられる部分だけを切り出すアプローチを取りました。大きく変えるのではなく、今あるものの一部を置き換えていく形です。 まとめ AIにテストコードを生成させる施策を通じて、テスト数を57%増やし、カバレッジを約2倍に改善しました。一方で、運用面の課題も見えてきました。AIの生成速度に人間のレビューが追いつかないこと、コンテキストの属人化によりインプット/アウトプットだけでは品質を担保できないことです。 これらの課題に対しては、サマリの自動生成と粒度の制御という仕組み側の改善で対処しました。しかし「人間が差分を目視で確認する」という部分は残しています。ここを自動化できる条件は、まだ見出せていません。 AIと協業する開発フローにおいて、人間が関与すべきポイントはどこなのか。この問いに対する私たちの暫定的な答えは、「コードの差分を確認し、品質を判断すること」です。この判断を下せるのは、コードを書いてきた経験の上に成り立つ審美眼があるからだと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、WEAR開発部バックエンドブロックのブロック長を務めている伊藤です。普段は弊社サービスである WEAR のバックエンド開発・組織運営を担当しています。 WEARのバックエンドブロックは約10名のエンジニアで構成されています。組織としてはマトリックス型を採用しており、各メンバーはバックエンドブロックに所属しながら、複数の職種で構成されるスクラムチームにも1〜3名ずつ配置されています。スクラムチームにはPdM(プロダクトマネージャー)やデザイナー、フロントエンドエンジニア、QAなど他職種のメンバーが集まります。加えてリモートワークが基本の環境です。 この体制ではコードレビューのリードタイムが長期化しやすいという課題がありました。本記事では、PRオープンからマージまでの平均時間を約26時間から約11時間へと短縮した取り組みを紹介します。 目次 はじめに 目次 抱えていた課題 コンテキストの分断 レビューのボトルネック化 構造的に後回しになるレビュー 課題を解決したアプローチ 5名ずつの2グループ制 全体朝会+グループ朝会の二段構成 段階的にたどり着いた「もくもくレビュータイム」 Gatherを活用する理由 Findy Team+による指標の可視化と週次改善 コードレビューガイドラインとAIレビューの活用 効果と得られた知見 段階的な施策でリードタイムが半減する コンテキストの把握範囲を狭めることでレビュー速度が上がる 「仕組みだけ」では不十分、同期の時間が文化を変える メトリクスの可視化が「感覚」を「共通言語」に変える AIレビューは「人間のレビューの質」を高める おわりに 抱えていた課題 コンテキストの分断 マトリックス組織では、バックエンドエンジニアが複数のスクラムチームに分散して配置されます。WEARのバックエンドブロックでは約10名が1〜3名ずつ別々のチームに所属しており、隣のメンバーが何を開発しているかが見えにくい状態でした。 PRが作成されても、レビュアーにとってはまず「このPRの背景にある仕様は何か」を理解するところから始まります。コンテキストが共有されていないため、レビューの入口でつまずくことが頻繁に起きていました。 レビューのボトルネック化 WEARのバックエンドブロックでは品質担保のため、2名以上のApproveを必須としています。しかしコンテキストがない状態でのレビューは仕様理解から始まるため、1件あたりの負荷が大きくなります。 改善に取り組む前はレビュアーをランダムに2名アサインしていましたが、得意領域や所属チームがバラバラで、忙しさも人によって異なります。結果として、レビューが後回しになりやすく、PRオープンからマージまでに24時間を超えるケースが多々ありました。 構造的に後回しになるレビュー チーム全員がレビューの重要性は理解していました。しかし、自身のスクラムチームの開発タスクとレビュー依頼が常に競合する状態では、レビューは「割り込みタスク」として後回しにされがちです。 リモートワーク環境では、オフィスで自然に発生する「ちょっと見てほしい」という一声が生まれません。PRを出しても反応のないまま放置される状況が常態化していました。 これは個人の意識の問題ではなく、仕組みで解決すべき構造的な課題でした。 課題を解決したアプローチ 5名ずつの2グループ制 まず、10名を5名ずつの2グループに分けました。グループの編成にあたっては、以下の点を考慮しています。 同じマトリックスチームのメンバーを同一グループにまとめる 関連度の高いチーム(似た領域を触るチーム)やドメインが近い人を同じグループにする ベテラン社員が偏らないようにし、レビューや設計レビューの質にむらが出ないようにする 5名という規模は、全員の作業状況を把握できるギリギリのサイズです。この単位にすることで、「何の仕様に取り組んでいるか」が自然と共有される状態をつくりました。 各グループにはグループリーダーを立て、グループ単位でPDCAを自走できる体制にしています。リーダーがグループ内の課題を拾い、改善施策を回しています。そこで得られた知見はもう一方のグループにも共有し、チーム全体の底上げにつなげています。 全体朝会+グループ朝会の二段構成 毎日の朝会は、全体朝会(30分)とグループ朝会(30分)の二段構成で運用しています。 全体朝会(30分) では、バックエンドブロック全員が集まり、以下の内容を共有します。 小話やLT(チームの雑談・学びの共有) タスク共有(各メンバーの作業状況) 案件共有(お問い合わせ対応のアサインなど) 共有・相談(曜日ごとに担当者が議題を持ち寄る) グループ朝会(30分) では、各グループに分かれて以下を行います。 各スクラムチームから現在の作業状況を報告する チームメンバーのOpen PRを確認し、レビュー依頼をリマインドする 新規PRはPR作成者が画面共有しながらメンバーに内容を説明する 朝会後はそのまま「もくもくレビュータイム」としてレビューに取り組む(詳細は後述) 週1回、Findy Team+のチーム比較を確認し、1週間の振り返りと改善点を話し合う グループ朝会の司会は1週間交代で担当します。特定の誰かに運営が偏らないようにすることで、全員が主体的に関わる仕組みにしています。 ポイントは、グループ朝会で「未レビューのPR」を毎日確認する仕組みにしていることです。これにより、PRが誰の目にも触れずに放置されるという事態を構造的に防いでいます。 段階的にたどり着いた「もくもくレビュータイム」 実は、最初からレビュー専用時間を設けていたわけではありません。取り組みの初期はグループを分けて朝会でPRを確認するところから始めました。 それだけでもリードタイムは改善しましたが、新たな課題が見えてきました。朝会でPRの内容を共有しても、レビューに取り組む時間が仕組みとして確保されていなかったため、結局は各自の開発タスクが優先されがちでした。 そこで、グループ朝会の後にそのまま「もくもくレビュータイム」を設けることにしました。朝会が終わったらそのまま Gather (仮想オフィスツール)に残り、レビューに取り組みます。 もくもくレビュータイムの運用ルールは以下の通りです。 Gatherに集まり、各自が黙々とレビューする 必須でレビューしてほしい人がいる場合は、PR内でその人をメンションしておく メンションされたPRを優先的に確認し、メンションされた人のレビューは必須とする メンションは任意とし、各自の判断で行う(例:その機能に詳しい人へ仕様チェックを依頼したい場合など) この「朝会→もくもくレビュータイム」の流れを毎日のリズムとして定着させたことで、レビューが「空いた時間にやるタスク」から「毎日の習慣」に変わりました。 さらに、朝会後のレビュータイムとは別に、午後にも30分のもくもくレビュータイムを設けています。午前と午後の2回、同期的にレビューする接点をつくることで、1日を通してPRを素早くキャッチできるようになっています。 以下は、1日の流れを図にしたものです。 Gatherを活用する理由 もくもくレビュータイムにGatherの仮想オフィスを使っているのには明確な理由があります。 まず、レビュー中に聞きたいことがあればその場ですぐに声をかけられます。MTGをセットしたりSlackで非同期にやりとりしたりする必要がありません。さらに、他のメンバーが質問している内容も一緒に聞こえるので、自然と共通認識が形成されます。 リモートワークでは「ちょっと聞く」のハードルが高くなりがちですが、Gatherで同じ空間にいることで、オフィスの隣の席で気軽に質問するような感覚を再現できています。 Findy Team+による指標の可視化と週次改善 チームの開発パフォーマンスを Findy Team+ で継続的に計測しています。設定している目標値は以下の通りです。 PRオープン〜マージ:16時間以内 PRオープン〜1人目のレビュー:3時間以内 レビュー〜マージ:13時間以内 以下は実際にチームで確認しているレビューサマリの画面です。 週1回のグループ朝会で、2つのグループ間でリードタイムを比較し、「どこにボトルネックがあったか」を具体的に議論しています。以下はグループ間の比較画面です。 数値があることで「なんとなく遅い」ではなく「今週は1人目のレビューまでが遅かったのはなぜか」という建設的な振り返りができるようになりました。 Findy Team+の計測対象からの除外漏れがないかも毎週確認しています。具体的には、Dependabotによる自動PR、他部署の作業待ちが発生するPR、検証が必要でやむを得ずマージを保留するPRなど、チームのレビュープロセスの実態を反映しないものを除外しています。正確な数値を維持することで、指標の信頼性を保ち、チーム全体が同じデータを見て議論できる状態を担保しています。 グループ間の比較は健全な競争意識にもつながっています。「今週は相手グループの方がリードタイムを短縮できていた」という事実は、翌週の改善アクションを自発的に生み出す原動力になっています。この仕組みによって、改善が一時的な取り組みではなく、継続的に回り続けるサイクルとして定着しました。 コードレビューガイドラインとAIレビューの活用 レビュー観点を明文化したガイドラインを整備しました。以下の観点を体系的に定義し、レビュアーごとの品質のばらつきを低減しています。 Railsのベストプラクティス(RESTfulなAPI設計、Strong Parametersの適切な使用など) セキュリティ(SQLインジェクション対策、JWT認証、環境変数による機密情報の管理など) パフォーマンス(N+1クエリの検出、 nolock スコープによるロック回避、バッチ処理など) API設計(バージョニングの整合性、エラーレスポンスの統一フォーマットなど) テスト(RSpecのベストプラクティス、FactoryBotによる適切なテストデータ生成など) プロジェクト固有の規約(設計思想ドキュメントへの準拠、既存パターンとの一貫性など) 加えて、GitHub ActionsとClaude(Anthropic)を組み合わせたAIレビューの仕組みを導入しました。PRのコメントで @claude-review と呼びかけるだけで、上記ガイドラインに沿った自動レビューが実行されます。PRの差分を読み取り、インラインコメントと全体のまとめを日本語で返すため、人間のレビュアーが着手する前の一次スクリーニングとして機能しています。 実際のレビューでは、以下のフィードバックが返ってきます。 まとめコメント(抜粋) 🟡 Important N+1クエリ対策 : preload ではなく includes の使用を推奨 nolock スコープの使用 : 読み取り専用クエリでのパフォーマンス最適化 🟢 良い点 適切なバッチ処理: find_in_batches を使用してメモリ効率を考慮 充実したテストカバレッジ: 網羅的なテストケースで品質を担保 インラインコメント(抜粋) パラメータの型定義が既存のAPIと一貫していません。他のエンドポイントでは integer で定義されているため、一貫性を保つために型を変更することを推奨します。 注目すべきは、単に一般的なベストプラクティスを指摘するだけでなく、プロジェクト固有の設計思想ドキュメントや既存の実装パターンを踏まえた指摘をする点です。これは、AIレビューのプロンプトに「まずCLAUDE.mdと設計思想ドキュメントを読んでからレビューせよ」と指示しているためです。 また、PR作成前の段階でもClaude CodeやCursor、Codexなど、各メンバーがそれぞれのAIツールを使ってセルフレビューしています。AIのセルフレビュー → @claude-review を使った機械レビュー → 人間によるレビューという多段構成を取っています。これにより、人間のレビュアーが設計判断やビジネスロジックの妥当性に注力できる環境を整えています。 効果と得られた知見 段階的な施策でリードタイムが半減する 以下は、約2年間のリードタイム推移です。グループ制の導入(2024年4月)、生成AIによるPR数増加(2025年8月頃)、もくもくレビュータイム導入(2025年10月)の前後で変化が見て取れます。 各フェーズの平均時間は以下の通りです。 改善前(〜2024年3月) :約26時間 グループ制導入後(2024年4月〜) :約16時間まで短縮 生成AIによるコーディング普及後(2025年8月頃〜) :PR数が週4〜6件から週8〜12件へ約2倍に増加し、約18時間へ上昇 AIレビュー・もくもくレビュータイム導入後(2025年10月〜) :約11時間まで短縮 グループ制だけでも約10時間の改善がありましたが、生成AIの活用でPR数が約2倍に増えた際、一時的にリードタイムが上昇しました。そこにもくもくレビュータイムとAIレビューを組み合わせることで、PR数が増えた状態でもさらに短縮できています。 コンテキストの把握範囲を狭めることでレビュー速度が上がる チームを分けてレビューすることで、各メンバーが把握すべきコンテキストの範囲が大幅に狭まりました。10名全員の状況を追うのではなく、5名の動きだけ把握すればレビューに入れる状態をつくったことが、最も効果の大きかった施策です。 グループ朝会で毎日Open PRを確認する運用と組み合わせることで、「誰がどんなPRを出しているか」が常に頭に入っている状態になります。レビューに着手する際のコンテキストスイッチのコストが大幅に下がりました。 「仕組みだけ」では不十分、同期の時間が文化を変える グループ分けと朝会での情報共有だけでは、レビューのリードタイムは十分には改善しませんでした。転機となったのは「もくもくレビュータイム」の導入です。 情報を共有しても、レビューする「時間」が確保されていなければ結局後回しになります。午前と午後に同期的な接点を設け、「みんなが同じタイミングでレビューする」という習慣を作ったことで、レビューが日常のリズムの一部に変わりました。 重要なのは長い会議を増やすことではなく、短い同期時間を毎日の習慣として組み込むことです。 メトリクスの可視化が「感覚」を「共通言語」に変える Findy Team+の数値とグループ間比較により、改善が「個人の頑張り」ではなく「チームの仕組み」として回るようになりました。 特に週1回のFindy Team+チェックを定例化したことで、数値が悪化したときに早く気づき、翌週の改善アクションにつなげるサイクルが定着しています。ボトルネックを感覚ではなくファクトで議論できることが、継続的な改善を支えています。 AIレビューは「人間のレビューの質」を高める AIレビューの効果は、リードタイム短縮だけではありません。コーディング規約への準拠やN+1クエリの検出といった機械的に判断できる指摘をAIが担うことで、人間のレビュアーがそれらを一つひとつ確認する必要がなくなりました。その分、設計判断やビジネスロジックの妥当性といった、より本質的な観点へ集中できるようになっています。 また、PR作成者自身がAIツールでセルフレビューしてからPRを出すことで、レビュー時の指摘事項が減り、レビュー1件あたりの負荷が下がっています。結果として、レビューの「速度」と「質」を両立できる状態に近づいています。 おわりに レビューのリードタイム改善は、個人の意識改革ではなく仕組みの設計で実現できます。本記事で紹介した施策をまとめると、以下の4点に集約されます。 認知範囲の縮小 :グループを分けることで、把握すべきコンテキストを絞る 同期の接点の設計 :朝会でPRを共有し、もくもくレビュータイムで実行する。午前と午後に接点を分散させることで情報のキャッチアップを早める 指標の可視化 :Findy Team+で数値を計測し、週1回振り返る。数値で語れる文化をつくり、改善を仕組み化する AIによるレビュー品質の底上げ :AIレビューとセルフレビューで定型的な指摘を自動化し、人間は設計判断に集中する 私たちのチームも最初からうまくいったわけではありません。グループ分けだけでは足りず、レビュー専用時間の追加やFindy Team+での振り返りの定例化、AIレビューの導入など、段階的に改善を重ねてきました。結果として、PRオープンからマージまでの平均時間は約26時間から約11時間へと短縮しています。 マトリックス組織×リモートという環境は、コードレビューにとって不利な条件が揃いやすい構造です。しかし適切な単位でチームを分割し、同期と非同期のバランスを設計し、指標で振り返る仕組みを整えれば、質を落とさずに速度を上げることは十分に可能です。 約11時間まで短縮できましたが、改善の余地はまだあると考えています。AIレビューのプロンプトを磨いてレビュー精度を高めることや、AIレビューの品質向上を前提に2名Approveのルール自体を見直すことなど、取り組みたいテーマは尽きません。今後もチームの変化に合わせて仕組みをアップデートしていきます。 同様の課題を抱えるチームにとって、本記事が何かの参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、新規事業部フロントエンドブロックの 池田 です。普段は ZOZOマッチ のアプリ開発を担当しています。ZOZOマッチは、ファッションの好みからZOZO独自のAIが「好みの雰囲気」の相手を紹介するマッチングアプリです。開発にはFlutterを採用しています。 フロントエンドブロックは2024年に発足したチームです。発足間もないチームゆえに、開発を進める中でさまざまな課題に直面しました。本記事では、私たちが「課題をチーム全体で認識し、解決していける文化」を築くために取り組んできたことを紹介します。発足間もないチームでチームビルディングに悩んでいる方や、メンバー間の連携・知見共有に課題を感じている方、新規事業部の取り組みに興味のある方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 取り組み KPTによる改善サイクル KPTから生まれた改善施策 進捗・困りごとの可視化 AIエージェントツール知見共有の仕組みづくり PRレビュー速度の改善 まとめ 背景・課題 フロントエンドブロックは3名と外部パートナーで構成されるチームです。新規事業部では、市場の変化に素早く対応しながらプロダクトを成長させていく必要があります。そのため、少人数でもスピード感を持って開発を進められる体制と、走りながら改善していける柔軟性が求められます。しかし、発足から日が浅いこともあり、チームとして改善サイクルを回す文化がまだ根付いていませんでした。開発プロセスや技術的な課題に直面しても、その解決が個人の力量に左右される状況があり、課題が個人の中に閉じてしまっていました。 具体的には以下のような問題がありました。 メンバーの困りごとが見えにくい :各メンバーの抱える課題や悩みがチーム内で可視化されておらず、助け合いやサポートが難しい状態だった 改善を議論する場がない :課題を感じても、改善案を提案・議論する場が整備されていなかったため、ナレッジがチームに蓄積されなかった こうした状況を打開するには、まずチーム全体で課題を共有し、改善を積み重ねていける仕組みづくりが必要でした。 取り組み ここからは、これらの課題に対してチームで取り組んできた改善施策を紹介します。まず改善サイクルを回すための仕組みとしてKPTを導入し、その中で見えてきた具体的な課題に対して個別の施策を実施してきました。 KPTによる改善サイクル チームとして改善を回していく文化を根付かせるため、KPT形式の振り返り会を実施しています。KPTでは、Keep(良かったこと)・Problem(課題)・Try(次に試すこと)の3つの観点で振り返ります。案件でやって良かった取り組みや大変だったことを洗い出し、次の案件へ活かせるようにしています。 振り返りの流れ 振り返りにはMiroを活用しています。具体的な流れは以下のとおりです。 付箋の記入 :各メンバーがKeep・Problem・Tryの各エリアに付箋を貼る 投票 :特に議論したい項目に投票し、優先度を付ける 議論 :投票数の多い項目を中心に議論し、Problemに対しては具体的なTryを設定する 議論の際は、単に事象を共有するだけでなく、一歩踏み込んだ振り返りを意識しています。Keepについては「なぜうまくいったのか」を深掘りし、成功要因を言語化することで再現性を高めています。Problemについては「今後どうすればうまくいくか」をチームで話し合い、具体的な改善策を導き出すようにしています。次回の振り返りでは前回のTryの効果を検証し、継続するか改善するかを判断します。このサイクルを回すことで、個人の中で閉じていた課題がチーム全体で共有され、改善へつなげられるようになりました。 ツールと頻度の選定 振り返りツールにはいくつかの選択肢を試しました。当初は Findy Team+ のKPT振り返り機能や Parabol を使っていました。しかし、Miroに慣れているメンバーが多かったことと、付箋からJiraチケットへの変換が容易だったことから、最終的にMiroを採用しました。 頻度は隔週1時間程度で実施しています。 KPTを継続する中で、メンバー自らが課題を発見し改善策を提案するボトムアップの文化が根付いてきました。以降で紹介する施策も、その多くはKPTでメンバーから挙がった声がきっかけになっています。今後は付箋の数が減ってきたタイミングで、イベントタイムラインなどを取り入れることも検討していきたいと考えています。 KPTから生まれた改善施策 進捗・困りごとの可視化 KPTで「メンバーの困りごとが見えにくい」という課題が挙がりました。開発初期は特に実装するチケットが多く、誰がどのタスクを進めているのか、どこまで進んでいるのかが見えづらい状況でした。メンバーの進捗を日常的に把握するため、以下の取り組みを行っています。 朝のSlackスレッドでの共有 毎朝Slackのリマインダーが自動投稿されるので、出勤したらそのスレッドに今日やることを書くようにしています。テキストで残すことで、非同期でも状況を把握でき、困りごとがあればすぐにフォローできる体制が整いました。 スレッド内でのやり取りなので、気軽に質問を投げられるのもポイントです。業務に関する質問だけでなく、雑談や改善提案の話もそこから自然と生まれるようになりました。 夕会でのアクティブなスプリントの確認 フロントエンドブロックでは毎日夕会を実施し、今日やったタスクと困っていることを共有しています。 以前はメンバーがそれぞれやったことをConfluenceに書いて報告していました。しかし、この方法にはいくつかの課題がありました。 アサインされているチケットがどれくらいあるのか見えづらい レビュー待ちのチケットが溜まっているのか把握しにくい 書く人によってタスクの粒度が異なり、状況を正確に把握しづらい これらの課題を解決するため、Jiraのアクティブなスプリントを画面共有しながら進捗を確認する運用に変更しました。スクラムボードのアクティブなスプリントでは、現在進行中のタスクをステータス別(未着手・進行中・レビュー待ち・完了など)に並べカンバン形式で確認できます。 この変更により、各メンバーがアサインされているチケットやステータスが一目でわかるようになりました。ステータスが長く変わっていないチケットも把握できるため、困っていることがないか声をかけやすくなりました。また、報告のために文章を書く手間が減り、ボードを見ながら自然と議論が生まれるようにもなりました。 また、夕会では明日やることも共有しています。アサインされているチケットが前倒しで完了した人にはチケットが多い人から分配するといった、チーム内での負荷調整もこの時間で実施しています。 AIエージェントツール知見共有の仕組みづくり KPTでは「AI活用をチーム全体に広げたい」という声が挙がりました。ZOZOではClaude CodeやGitHub Copilotなど、さまざまな開発AIエージェントツールを利用できる環境が整っています 1 。新規事業部では、こうした新しい技術やツールを積極的に取り入れ、開発プロセスの改善にチャレンジできる文化があります。私たちのチームでは、執筆時点ではClaude Codeをメインに、実装からPR作成・レビュー・CI修正まで開発プロセス全体で活用しています。特定のツールに限定するルールは設けておらず、Codexなど他のツールで検証するメンバーもいますが、チーム全体としてはClaude Codeの利用が中心です。しかし、AIツールを使いこなしているメンバーとそうでないメンバーとの間に差があり、チーム全体で活用していくにはまだ改善の余地がある状態でした。 この状況を改善するため、チーム内で知見を蓄積・共有するための仕組みを整備しました。 AI関連の知見を集約するSlackチャンネル AI活用に関する知見を集約する専用のSlackチャンネルを開設しました。このチャンネルにはエンジニアだけでなく、PMやビジネスなどのメンバーも参加しており、日々の業務改善にAIを活用できないかざっくばらんに話し合っています。チャンネルでは以下のような情報を共有しています。 「こういう場面で使えた」という活用事例 ツールの設定方法やTips 勉強になった記事の共有 記事を共有する際には、チームとして取り組めそうな部分についても議論しています。チャンネルを開設したことで、メンバー全体のAI活用度が向上しました。また、AIに関する質問や相談が気軽にできるようになり、知見がチームへストックされるようになりました。 Claude Codeプラグインの共有リポジトリ Claude Codeにはプラグインとマーケットプレイスという機能があります。プラグインはClaude Codeの機能を拡張するための仕組みです。 公式ドキュメント では以下のように説明されています。 Plugins let you extend Claude Code with custom functionality that can be shared across projects and teams. プラグインには、再利用可能な命令セットである「スキル」、外部ツールと連携するための「MCPサーバー」、イベント駆動型の自動化を行う「フック」などのコンポーネントを含めることができます。マーケットプレイスは、これらのプラグインを配布・共有するためのカタログです。マーケットプレイスを追加すると、そこに登録されているプラグインを簡単にインストールできます。 私たちはこの機能を活用し、プロジェクト用の共有リポジトリを作成しました。このリポジトリは以下の目的で整備しています。 プロジェクト全体でAI活用できる環境を整備する チーム間でのAIの知見を共有できるようにする 車輪の再発明を防ぐ リポジトリには、各チームが必要なプラグインを追加していく運用にしています。現在はAtlassian MCP関連、Git関連、FlutterやWeb関連など、さまざまな用途のプラグインが集約されています。 リポジトリの構成は以下のようになっています。 plugins/ ├── atlassian-mcp/ ├── browser/ ├── flutter/ │ ├── .claude-plugin/ │ ├── skills/ │ └── .mcp.json ├── git/ │ └── .claude-plugin/ └── commands/ ├── branch/ ├── commit/ ├── issue/ └── pr/ └── help.md 各プラグイン( flutter/ 、 git/ など)の中には、 .claude-plugin/ ディレクトリやスキル、MCPサーバーの設定ファイルが含まれています。 commands/ ディレクトリには、ブランチ作成やコミット、PR作成などの汎用的なカスタムコマンドを集約しています。 運用としては、新しいコマンドやプラグインを追加したい場合はPRを出してもらい、レビュー後にマージする流れです。また、新規プラグインをメンバーがキャッチアップできるよう、リポジトリの更新内容を先述のAI知見共有チャンネルへ自動投稿するGitHub ActionsのWorkflowも導入しています。 共有リポジトリを整備した1つ目のメリットは、Claude Codeのマーケットプレイス機能を活用することで、導入の手間を大幅に削減できる点です。プロジェクトの .claude/settings.json に extraKnownMarketplaces を設定すると、メンバーがプロジェクトを開いた際にプラグインがインストール候補として提示されます 2 。この設定ファイルはGitで管理されるため、チーム全体で共有でき、新しいメンバーも特別な手順なしで利用を開始できます。また、マーケットプレイスの自動アップデート機能を有効にすると、プラグインに更新があった際に自動で最新バージョンに更新されます 3 。そのため、チーム全体で常に最新のプラグインを利用できます。 { " enabledPlugins ": { " flutter@xxxx-marketplace ": true , " git@xxxx-marketplace ": true } , " extraKnownMarketplaces ": { " team-tools ": { " source ": { " source ": " github ", " repo ": " org/claude-plugins " } } , " project-specific ": { " source ": { " source ": " git ", " url ": " https://github.com/org/claude-plugins.git " } } } } 2つ目のメリットは、アプリ・バックエンド・Webの各チームで個別に管理していたスキルを一元管理できるようになり、知見の共有が促進された点です。同じようなプラグインを各チームで作成する重複作業がなくなり、他のチームが活用している便利なスキルをキャッチアップしやすくなりました。 PRレビュー速度の改善 KPTでは「PRレビューが遅い」という課題も繰り返し挙がっていました。レビュー依頼からマージまでのリードタイムが長く、各自の実装タスクが優先されることでレビューが後回しになりがちでした。この状況を改善するため、以下の取り組みを行いました。 Findy Team+によるレビュー状況の可視化 Findy Team+を利用し、PRのレビュー時間やサイクルタイムを可視化しています。KPT振り返り会では、Findy Team+のサイクルタイム分析やレビュー分析を定期的に確認しています。全体の指標を俯瞰しつつ、数値が悪化している項目を重点的にチェックすることで、開発プロセスのどこにボトルネックがあるのかをチームで共通認識として持てるようになりました。 実際にこの分析を通じて、KPTで挙がっていた「PRレビューが遅い」という課題がデータでも裏付けられました。感覚的な指摘が数値として可視化されたことで、具体的な改善アクションへつなげられるようになりました。 Google Engineering Practices Documentationの輪読会 レビュー待ち時間が長いという課題を受けて、 Google Engineering Practices Documentation の輪読会を実施しました。このドキュメントでは、コードレビューが遅れることによる弊害として以下の点が挙げられています。 チーム全体の開発速度が低下する :新機能やバグ修正のリリースが遅延し、チーム全体のベロシティに影響を与える 開発者の不満が増加する :レビューの滞りは開発者のモチベーション低下を招き、チームの雰囲気にも悪影響を及ぼす コード品質が悪化する :レビューの遅れはPRの肥大化を招き、結果的にレビューの質も低下する悪循環に陥る 輪読会を通じて、これらの弊害をチームで共有しました。また、コードレビューはタスクの合間に行うものではなく、優先度の高い作業として扱うべきという認識を揃えることができました。さらに、輪読会はメンバー同士がコードレビューに対する考え方や心理的なハードルを知る機会にもなりました。「どこまで指摘すべきか迷う」「実装チケットを優先的に終わらせたい」といった悩みを共有することで、お互いの視点を理解し、レビューの進め方について建設的な議論ができました。 AI活用による効率化 レビュー時に「不具合の原因や実装背景が分かりづらい」「動作確認の手順が不明確」といった意見も挙がっていました。これらの課題についても、Claude Codeを活用して改善に取り組んでいます。具体的には、以下のようなカスタムスキルを整備しました。 スキル 用途 /pr-create 変更内容の要約に加え、不具合の原因や実装背景、動作確認の手順を含めたPRを作成する /pr-review コード規約やベストプラクティスに基づいたレビューコメントを生成する /pr-ci-fix CIの失敗原因を分析し、修正してコミットする /flutter-code-review ZOZOマッチアプリのコード規約をスキルとして登録し、実装やレビューの際に規約に沿った指摘ができるようにする これらのスキルにより、定型的な作業の時間を削減し、本質的なレビューへ集中できるようになりました。 さらに、Codexを活用したPRの自動レビューも導入しています。PRをオープンすると自動でCodexがコードレビューを実施するため、レビュアーはCodexの指摘を踏まえつつ、人間ならではの観点でレビューできるようになりました。 PRレビュー改善の成果 これらの取り組みの結果、Findy Team+の各分析で改善が確認できました。 サイクルタイム分析では、以下の改善が見られました。 PR作成数が約3倍に増加 :AI活用の促進やレビュープロセスの改善により、PRを細かく分割して作成する文化が浸透しました オープンからマージまでの平均時間を約70%改善 :レビュー待ち時間の可視化やCodexによる自動レビューの導入により、レビューのリードタイムが大幅に改善しました 一方で、グラフからはPR作成数が多い週ではマージまでの時間が増加する傾向も見て取れます。PRの量が増えるとレビュー負荷が高まり、結果としてリードタイムが延びてしまうという課題が残っています。今後は、レビュー体制の強化やさらなる自動化を通じて、PR数が増加してもマージまでの時間を維持・短縮できる仕組みづくりに取り組んでいきたいと考えています。 レビュー分析でも、Codexでの自動レビューとClaude Codeのスキル整備の前後で、各指標に改善が見られました。 指標 改善前 改善後 オープンからレビューまでの平均時間 10.3h 7.5h レビュー依頼からレビューまでの平均時間 10.1h 8.1h レビューからアプルーブまでの平均時間 17.1h 9.3h 特にレビューからアプルーブまでの平均時間は17.1hから9.3hへと約46%改善しました。Codexによる自動レビューでレビュアーの負担が軽減されたことに加え、輪読会を通じてレビューの優先度に対する意識が変わったことも、この改善に寄与していると考えています。 まとめ 本記事では、発足間もないチームが「課題をチーム全体で認識し、解決していける文化」を築くために取り組んできたことを紹介しました。KPTによる振り返りを起点に、進捗・困りごとの可視化、AIエージェントツールの知見共有、PRレビュー速度の改善といった施策を実施してきました。これらの取り組みを通じて、個人の中に閉じていた課題がチーム全体で共有され、継続的に改善を回せる体制を整えることができました。今後はDevinやFigma MakeといったAIツールも活用しながら、チーム内の改善にとどまらず、プロジェクト全体に対しても改善を働きかけていきたいと考えています。発足間もないチームでチームビルディングに悩んでいる方や、改善サイクルを回す仕組みづくりに課題を感じている方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co 2026年1月時点におけるZOZOで利用可能な代表的なAIツールは「 ZOZOにおけるAI活用の現在 ~開発組織全体での取り組みと試行錯誤~ 」をご参照ください ↩ Configure team marketplaces - Claude Code ↩ Configure auto updates - Claude Code ↩
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、カート決済部カート決済基盤ブロックの林です。普段はZOZOTOWN内のカート機能や決済機能の開発、保守運用、リプレイスを担当しています。 ZOZOTOWNの購入フローは、セッションに強く依存したロジックが長年の改修により肥大化し、機能改善や保守の際の調査・改修コストが増大していました。この課題を解決するため、私たちのチームは2024年5月から約2年にわたる段階的なリプレイスプロジェクトを進めています。 ミッションクリティカルな購入フローを無停止で移行するため、私たちは3つのフェーズに分けた段階的なアプローチを採用しました。本記事では、その実践的な進め方と、実際に直面した課題について紹介します。 なお、同じチームの多田と三浦が、このリプレイスにおけるアーキテクチャ選択(モジュラモノリス)の背景と設計について別の記事で紹介していますので、併せてご覧ください。 techblog.zozo.com 目次 はじめに 目次 背景・課題 セッションに依存したロジックの肥大化 サービス無停止でのリプレイス要件 新システムの構成とセッション問題の解決 既存システムの問題点 新システムの構成 Shopping BFF + Redisによる解決 リプレイス戦略の全体像 なぜ段階的なアプローチを選んだか 3つのフェーズに分けたアプローチ フェーズの関係性 現在の進捗状況 フェーズ1:一部機能の比較フェーズ 比較対象の選定 並行稼働の仕組み 比較・検証方法 フェーズ1で得られた知見 フェーズ2:段階的な入れ替えフェーズ 入れ替え順序の工夫 切り替えの仕組み 二重開発の最小化 フェーズ2の成果 フェーズ3:全体リプレイス n%リリースによる段階的な移行 n%リリースの設計方針 各段階での検証項目 全体リプレイスにおける課題 課題への対応策 今回の課題と今後の改善点 今後の展望 まとめ 背景・課題 セッションに依存したロジックの肥大化 ZOZOTOWNの購入フローは、Classic ASP(以下、ASP)で実装されており、長年の改修によりセッション情報がいたるところで更新される構造になっていました。この結果、ロジック間の依存関係が複雑化し、機能改善や保守の際の調査コストが増大するとともに、部分的な入れ替えも困難な状態でした。 サービス無停止でのリプレイス要件 ZOZOTOWNの購入フローは、ECサイトの中核をなすミッションクリティカルな領域のため、サービスを停止できません。さらに、リプレイス期間中であっても、新規機能の追加や運用改善は継続的に発生し、それらは既存ロジック側とリプレイス側のどちらにも反映する必要がありました。 その結果、移行にあたっては以下の制約を考慮する必要がありました。 ビッグバン切替の回避 :一括での全面切り替えはリスクが高すぎる 二重開発の不可避 :新旧両システムを並走する以上、修正は両方に入れる必要がある 段階的検証の必須化 :各段階で十分に検証しながら、慎重に移行を進める必要がある これらの課題に対し、私たちは新システムの構築と段階的な移行戦略により解決を図りました。 新システムの構成とセッション問題の解決 既存システムの問題点 既存のASPシステムでは、購入フロー内のいたるところでセッション情報が更新される構造になっており、以下の問題がありました。 1. セッション更新の分散 どのタイミングで何が更新されるか追跡困難 機能改善時の影響範囲を特定しづらい デバッグやトラブルシューティングに時間がかかる 2. 部分的な入れ替えの困難さ セッション情報と密結合しているため、機能単位での切り出しが難しい 段階的なリプレイスを阻害する要因 3. 保守性の低下 新規メンバーがシステムを理解するのに時間がかかる セッション構造の変更が困難 新システムの構成 これらの問題を解決するため、責務を明確に分離した構成を採用しました。 既存システム(リプレイス前) ASP すべてのロジックとセッション管理が密結合 新システム(リプレイス後) 画面層(以下、Shopping BFF) ユーザーの入力値や購入フロー内の一時的な状態を管理 セッションで管理していた情報を専用のRedisで管理(購入フローに閉じた情報のみ) Java/Spring Boot ビジネスロジック層(以下、Shopping API) ビジネスロジックの中核を担う カート、注文、決済などのドメインロジックを実装 Java/Spring Boot、モジュラモノリス構成 Shopping BFF + Redisによる解決 新システムでは、Shopping BFFで状態管理を集約する構成にしました。 設計方針 購入フローに閉じた情報のみをBFFで管理 他のサービスと共有する必要のないデータ 注文プロセス中のみ必要な一時的な状態 Redisでの永続化 ZOZOTOWN共通のセッションではなく専用のRedisで管理 TTL(Time To Live)を設定し、不要なデータは自動削除 メリット 管理箇所の集約 :BFFに状態管理を集中させた結果、見通しが向上 独立性の確保 :各ドメイン(カート、注文、決済)の状態が明確に分離 状態管理をShopping BFFに集約し、ビジネスロジックをShopping APIに分離したことで、状態とロジックの責務を明確に切り分けました。これにより、セッション依存による密結合は解消され、機能単位での段階的な入れ替えが可能になりました。あわせて、システム全体の見通しと保守性が向上しました。 リプレイス戦略の全体像 なぜ段階的なアプローチを選んだか 段階的なアプローチを採用した理由は以下の通りです。 リスクの分散 :機能を小さく区切って検証し、問題発生時の影響範囲を限定する 二重開発の最小化 :新実装へ移行した範囲は旧実装への改修を減らせるため、早期リリースで並行開発の期間を短縮する 継続的なフィードバック :各フェーズで得られた知見を次のフェーズへ反映する チームのモチベーション維持 :小さな成功体験を積み重ね、長期プロジェクトでも前進感を保つ 3つのフェーズに分けたアプローチ これらの理由から、私たちは以下の3つのフェーズに分けてリプレイスを進めてきました。 フェーズ1:一部機能の比較フェーズ 既存ロジック(ASP)と新ロジックを並行稼働 結果を比較・検証し、差分を解消 フェーズ2:段階的な入れ替えフェーズ 比較で問題ないことを確認した機能から順次切り替え 二重開発の負担を最小化 フェーズ3:全体リプレイス 画面を含めた購入フロー全体を新システムに移行 n%リリースで段階的にトラフィックを切り替え フェーズ1とフェーズ2を機能単位で繰り返し、重要機能の切り替えが完了した段階でフェーズ3を実施します。 フェーズの関係性 フェーズ1・2ではビジネスロジック層のリプレイスを先行して進め、フェーズ3では画面層のリプレイスを実施する計画です。ビジネスロジック層を先に安定化させることで、画面リプレイス時には既に検証済みのAPIを使用できる体制を整えています。 現在の進捗状況 2026年2月時点で、フェーズ1・2は既にリリースが完了しており、本番環境で稼働しています。フェーズ3については開発が完了し、現在はリリースに向けた最終準備を進めている段階です。 次の章から、各フェーズの詳細について説明します。 フェーズ1:一部機能の比較フェーズ 比較対象の選定 リスクを最小限に抑えるため、 読み取り系の機能 を比較対象としました。具体的には以下の機能です。 お届け先一覧の取得 支払い方法一覧の取得 配送日時の指定一覧の取得 これらの機能を選んだ理由は以下の通りです。 読み取り専用 :データの更新を伴わないため、万が一の不具合でもユーザーへの影響が限定的 検証が容易 :結果の比較が容易で、差分の原因を特定しやすい 段階的な複雑化 :条件が少ないお届け先一覧から始め、徐々にロジックが複雑な機能へと比較対象を拡大 即時停止が可能 :比較モードを停止するだけで対応可能 並行稼働の仕組み 既存のASPから新しいShopping APIを呼び出す形で並行稼働を実現しました。以下がその仕組みです。 この仕組みの重要なポイントは以下の通りです。 フラグによる制御 :対象機能を柔軟に切り替えられ、トラフィックの切り替えも即座に実施できる ASP側での比較 :両方の結果をASP側で受け取り、差分をチェックする ユーザーへの影響なし :ユーザーには常にASPの結果を返すため、体験に影響を与えない 比較情報の保存 :各種情報をJSONで保存し、集計・分析できる 負荷の考慮 :比較は一部サーバーに限定して実施する 比較・検証方法 保存した比較情報を定期的にチェックし、以下の観点で検証しました。 一致率の確認 :どの程度の割合で結果が一致しているか 差分の原因分析 :不一致の場合、どのようなケースで発生しているか 優先度の判断 :ビジネスへの影響度から修正の優先順位を決定 特に重要だったのは、差分が発生した際の 根本原因の特定 です。多くの場合、以下のような原因がありました。 ASP側のロジックの暗黙的な仕様(ドキュメント化されていない挙動) データベースの参照タイミングによる差異 購入フローはミッションクリティカルな領域のため、これらの差分を1つずつ解消し、 100%一致するまで比較を継続 しました。 フェーズ1で得られた知見 比較フェーズを経て、以下の知見が得られました。 暗黙知の可視化 :長年のシステムに埋もれていた仕様が明らかになった テストケースの充実 :差分から得られた知見をテストケースに反映できた 段階的移行の有効性 :小さく始めるアプローチが、リスク管理に有効であることが明らかになった 次のフェーズでは、この比較で問題ないことを確認した機能から、実際に切り替えを進めていきます。 フェーズ2:段階的な入れ替えフェーズ 入れ替え順序の工夫 比較フェーズで十分に検証した機能から、段階的に切り替えを進めました。具体的な順序の例と、その選定理由をいくつか紹介します。 順序 機能 特徴 1 お届け先一覧の取得 条件が少なく、ロジックがシンプル 2 支払い方法一覧の取得 決済方法の選択可否の判定ロジックが複雑 3 配送日時の指定一覧の取得 在庫や配送条件との連携が必要 この順序で進めた理由は以下の通りです。 リスクの段階的な増加 :シンプルな機能から複雑な機能へ段階的に広げる 経験の蓄積 :各段階で得られた知見を次に活かす 影響範囲の管理 :問題発生時にトラフィックの切り替えをしやすい順序にする 切り替えの仕組み 比較フェーズで差分がないことを確認した機能については、以下のように切り替えました。 重要な変更点は以下の通りです。 比較処理を廃止 :検証済みのため、比較情報の保存は不要 API結果を直接返す :Shopping APIの結果をそのままユーザーに返す 旧ロジックの保守を停止 :切り替え済み機能はASP側を保守対象から除外 二重開発の最小化 このフェーズでは、 二重開発を最小化 できたことが大きな効果でした。 段階的リプレイスのメリット: 切り替え済みの機能に対する新規追加や修正は、新ロジック(Shopping API)のみに実施する 旧ロジック(ASP)への反映が不要になり、開発工数を削減できる ビッグバンリリースとの比較: ビッグバンの場合、切り替え完了まで旧ロジックにも改修が必要になる 開発期間中は変更を二重に実装する必要がある 一方、段階的リプレイスでは、切り替え済み範囲が増えるたびにこの期間を短縮できる 例えば、新しい決済方法の追加が必要になった場合の修正範囲は以下の通りになります。 段階的リプレイス(API層移行済み) :Shopping APIのみ修正 ビッグバンでのリプレイス :ASPとShopping APIの両方を修正 このように、切り替え済みの機能については旧ロジックへの反映が不要になり、長期プロジェクトにおける開発負担を大きく軽減できました。 フェーズ2の成果 フェーズ2を通じて以下の成果を得ました。 対象機能の完全移行 :読み取り系の主要機能をすべて新システムに移行 開発工数の削減 :二重開発の期間を最小化し、チームの生産性が向上 品質の向上 :段階的な検証により、問題を早期に発見・修正 次のフェーズでは、画面を含めた購入フロー全体を新システムに移行します。 フェーズ3:全体リプレイス フェーズ3では、画面を含めた購入フロー全体を新システムに移行する計画で進めています。 n%リリースによる段階的な移行 全体リプレイスでは、トラフィックを段階的に切り替える n%リリース を採用する計画です。同一ユーザーが途中で旧フロー・新フローを行き来しないよう、ユーザー単位で一度割り当てた割合を保持する形でルーティングする設計としています。 n%リリースの設計方針 段階的な切り替えにおいて、以下の方針で進める計画です。 段階 目的 検証期間 備考 1% 本番環境での動作確認と想定外の問題の早期発見 長期 注文から発送完了まで問題なく処理されることを検証 20% 1%で得られた知見を反映し、より大きなトラフィックでの検証 短期 問題がなければ次の段階へ 50% 本番に近い負荷での最終検証 中期 大規模なトラフィックでの安定性を確認 100% すべてのトラフィックを新システムに移行 - 万が一の問題に備え、即座にロールバックできる体制を維持 各段階での検証項目 各n%段階で、以下の項目を重点的に監視する計画です。 機能的な正常性 注文完了率 決済成功率 エラーログの監視 パフォーマンス レスポンスタイム スループット データベースの負荷 問題が発見された場合は、即座に前の割合に戻す体制を整えています。 全体リプレイスにおける課題 フェーズ3では、フェーズ1・2とは異なる課題に直面しています。 1. 機能改修とのコンフリクト 画面を含む全面リプレイスのため、リリースまで約1年半の開発期間が必要になる その間、既存システム(ASP)にも新規機能や改善が継続的に追加される 新旧両方のシステムに同じ変更を反映する 二重開発 が発生する 具体例としては以下が挙げられます。 新しい決済方法の追加 ユーザーインタフェースの改善 バグ修正や運用改善 これらをASPとShopping BFF/Shopping APIのすべてに実装する必要があり、工数が増加しています。 2. モチベーションの維持 長期にわたる開発により、チームのモチベーション維持が課題 「いつ終わるのか」という不安感 同じ機能を二重に実装する徒労感 課題への対応策 これらの課題に対し、以下のように対応しました。 1. フェーズ2の成果を活用 API層はフェーズ2で既に移行済みのため、Shopping APIへの修正のみで対応できる範囲が拡大 BFF層のみの修正で済むケースも多く、完全な二重開発を避けられた 2. 優先度の明確化 新規機能の開発は、極力リプレイスのリリース後に行う 緊急性の高い修正のみ二重開発で対応 今回の課題と今後の改善点 長期にわたるリプレイスプロジェクトにおいて、チームのモチベーション維持が課題として浮上しました。約1年半の開発期間中、同じ機能を二重に実装する必要があり、「いつ終わるのか」という不安感や徒労感がチームに蓄積しやすい状況でした。 今回の経験を踏まえると、次に同様のプロジェクトに取り組む際には、以下の点をあらかじめ検討しておくべきだと感じました。 プロジェクト初期段階でのマイルストーン設計 :長期プロジェクトでも前進感を保てるよう、細かいマイルストーンを設定 定期的な成果の可視化 :各段階での成果を可視化し、チーム全体で進捗を共有する仕組みの構築 チームメンバーのケア体制 :長期プロジェクトにおけるメンバーの心理的負担に配慮した体制づくり 今後の展望 リプレイスプロジェクトの完了後も、以下のような継続的な改善と発展を計画しています。 新システムでの運用安定化と監視体制の強化 リリース完了後は、新システムの安定稼働を最優先とし、監視体制の強化に取り組みます。具体的には、パフォーマンスメトリクスの継続的な収集・分析や、エラー検知の精度向上、アラート体制の整備などを進めていきます。また、運用ノウハウの蓄積と共有により、障害発生時の迅速な対応体制を構築します。 モジュラモノリスからマイクロサービスへの段階的な移行検討 現在のモジュラモノリス構成は、開発効率とシステムの見通しの良さを両立できていますが、将来的にはマイクロサービスへの移行も視野に入れています。ただし、マイクロサービス化はトレードオフを伴うため、ビジネス要件やチーム体制、技術的な成熟度を考慮しながら、慎重に検討を進める方針です。 さらなるパフォーマンス改善と開発体験の向上 新システムへの移行により開発効率は向上しましたが、さらなる改善の余地があります。レスポンスタイムの最適化やデータベースクエリのチューニング、開発ツールの整備など、継続的な改善を通じて、より快適な開発体験とユーザー体験を実現していきます。 得られた知見を他のシステムのリプレイスにも適用 本プロジェクトで得られた段階的リプレイスの手法や、並行稼働による検証のノウハウ、長期プロジェクトにおけるチーム運営の知見は、社内の他システムのリプレイスにも適用可能です。これらの知見を共有し、組織全体の技術力向上に貢献していきます。 まとめ 本記事では、ZOZOTOWNの購入フローにおける段階的リプレイスの実践について紹介しました。 セッションに強く依存した既存システムを、3つのフェーズに分けて段階的にリプレイスしています。 一部機能の比較フェーズ :並行稼働により、リスクを抑えながら新旧ロジックの差分を解消 段階的な入れ替えフェーズ :検証済みの機能から順次切り替え、二重開発の期間を最小化 全体リプレイス (進行中):n%リリースにより、画面を含む購入フロー全体を安全に移行する計画 また、Shopping BFF + Redisの構成により、セッション依存の問題を解消し、システムの見通しと保守性の向上を実現しています。 大規模ECにおける段階的リプレイスを検討している方や、ミッションクリティカルなシステムの無停止移行に取り組んでいる方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
.entry .entry-content .table-of-contents > li > ul, .table-of-contents li:nth-child(2) { display: none; } はじめに こんにちは、ECプラットフォーム部の権守です。普段はZOZOTOWNの会員基盤やID基盤の開発に携わっています。 本記事では、会員基盤で導入したデータベースへの書き込みを伴う処理のテスト手法について紹介します。この手法では実行前後のデータベースの差分に注目することで特定のレコードだけでなく、データベース全体への副作用を網羅的に検知することを目的とします。 目次 はじめに 目次 従来手法の課題 差分検証によるアプローチ Goによる差分検出ツールの実装 利用イメージ 差分抽出の実装 複数データベースへの対応 導入時の工夫点 非固定値の取り扱い 期待値の正規化 差分の除外 まとめ 従来手法の課題 データベースへの書き込みを伴う処理のテストでは、一般的に以下のように関数の返り値と処理対象である特定のレコードを検証することが多いと思います。 // 1. テスト対象の関数を実行 refundedPoints, err := usecase.CancelOrder(ctx, orderID) AssertEqual(t, nil , err) // 2. 返還ポイント(返り値)を検証 AssertEqual(t, 500 , refundedPoints) // 3. 特定のレコードの状態を検証 order, _ := orderRepo.FindByID(ctx, orderID) AssertEqual(t, "CANCELLED" , order.Status) しかし、これらのテストだけではデータベースへの「期待しない副作用」を防げないことに課題を感じていました。例えば、更新や削除の条件指定に誤りがあると想定外のレコードに影響を及ぼすことが考えられます。この場合には、処理対象である特定のレコードのみを検証したとしても、その他のレコードが破壊されていることに気づくことはできません。 差分検証によるアプローチ この課題を解決するには、データベースへの副作用を網羅的に検証する必要があります。そこで、データベースの実行前後の全レコードをキャプチャして比較し、その差分を検証するアプローチを採用しました。 このアプローチでは、特定のテーブル・レコード・カラムを見るのではなく、データベース全体への副作用をテスト対象の出力の1つとして捉えます。出力の期待値として差分を指定し、期待した副作用のみが存在することを検証することで、期待しない副作用が生じた際にそれを検知できます。 差分は以下の3種類としてそれぞれ抽出します。 作成 (Create):新規レコードのカラム全体の値を保持 更新 (Update):キー情報と、変更があったカラムの「変更後の値」を保持 削除 (Delete):レコードを特定するキー情報を保持 更新の差分の表現としては、更新前後の値を含める方が一般的ですが、本手法ではあえて更新後の値のみを保持しています。テストという観点では、更新前の値は事前条件の一部であり、テストデータのセットアップ内容と重複するためです。 Goによる差分検出ツールの実装 利用イメージ 会員基盤はGoで実装されているため、テストへの組み込みやすさを考慮して今回はGoのコード上で実装しました。 以下に、どのように差分をGoの構造体で表現し、利用するかのイメージを示します。 // 差分データの構造イメージ type Diff struct { C []Record // CREATE: 追加されたレコード群を指定。各レコードは全フィールド値を指定 U map [KeyHash]Record // UPDATE: 更新のあるレコード群を主キーのハッシュ値で指定。各レコードは更新後のフィールド値を指定 D []Record // DELETE: 削除されたレコード群を指定。各レコードは主キー値を指定(主キーが存在しない場合は全フィールド値) } type Diffs map [ string ]Diff // mapのキーはテーブル名 // 挿入時の差分検出の利用イメージ var result int diffs := DiffDB(ctx, db, func () { result = insertMember(member) }) AssertEqual(t, 1 , result) AssertRecords(t, Diffs{ "members" : { C: []Record{ { "id" : 1 , "age" : 20 , "nickname" : "taro" , }, }, }, }, diffs) // 更新時の差分検出の利用イメージ var result int diffs = DiffDB(ctx, db, func () { result = updateMemberName( 1 , "jiro" ) }) AssertEqual(t, 1 , result) AssertRecords(t, Diffs{ "members" : { U: map [KeyHash]Record{ HashKey({ "id" : 1 }): { "nickname" : "jiro" , }, }, }, }, diffs) // 削除時の差分検出の利用イメージ var result int diffs = DiffDB(ctx, db, func () { result = deleteMember( 1 ) }) AssertEqual(t, 1 , result) AssertRecords(t, Diffs{ "members" : { D: []Record{ { "id" : 1 , }, }, }, }, diffs) このようにデータベースに対する副作用を出力値として検証できるため、「レコードを取得して特定のカラムを検証する」という命令的な記述を繰り返す必要がなくなり、テストコードの可読性と保守性が向上します。 具体的には、Goで広く採用されているテーブル駆動テストのスタイルと親和性が高く、複数のテストケースを簡潔に記述できます。例えば、条件分岐で書き込むテーブルが変わる関数をテストする場合、従来の手法では、テストケースによって検証処理も分岐するか、テストケースの構造体に検証処理を持つ必要がありました。検証処理の分岐はテストコードの複雑化を招き、テストケースの構造体に検証処理を持たせることはテーブル駆動テストのメリットである宣言的な記述を損ないます。 しかし、今回導入した手法であれば宣言的な記述を維持できます。以下にそれぞれの手法の例を示します。 // 従来手法その1(検証処理の条件分岐) tests := [] struct { name string orderID string expectRefund bool // 返金テーブルを確認するかどうかのフラグ expectPointReset bool // ポイント更新を確認するかどうかのフラグ }{ { name: "クレジットカード決済のキャンセル(返金あり)" , orderID: "order_card" , expectRefund: true , expectPointReset: false , }, { name: "全額ポイント払いのキャンセル(ポイント還元あり)" , orderID: "order_point" , expectRefund: false , expectPointReset: true , }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { // 1. テスト対象の実行 err := usecase.CancelOrder(ctx, tt.orderID) AssertEqual(t, nil , err) // 2. 注文ステータスの検証(共通) order, err := orderRepo.FindByID(ctx, tt.orderID) AssertEqual(t, nil , err) AssertEqual(t, "CANCELLED" , order.Status) // 3. 条件分岐による個別テーブルのアサーション // テストケースが増えるたびに、この分岐ロジックのメンテナンスが必要になる if tt.expectRefund { refund, err := refundRepo.FindByOrderID(ctx, tt.orderID) AssertEqual(t, nil , err) AssertEqual(t, Refund{OrderID: tt.orderID, amount: 1000 }, refund) } if tt.expectPointReset { user, err := userRepo.FindByID(ctx, order.UserID) AssertEqual(t, nil , err) AssertEqual(t, 1500 , user.Points) } }) } // 従来手法その2(テストケースごとに検証処理を持つ) tests := [] struct { name string orderID string assertFunc func (t *testing.T, orderID string ) }{ { name: "クレジットカード決済のキャンセル(返金あり)" , orderID: "order_card" , assertFunc: func (t *testing.T, orderID string ) { // 注文ステータスの検証 order, err := orderRepo.FindByID(ctx, orderID) AssertEqual(t, nil , err) AssertEqual(t, "CANCELLED" , order.Status) // 返金テーブルの検証 refund, err := refundRepo.FindByOrderID(ctx, orderID) AssertEqual(t, nil , err) AssertEqual(t, Refund{OrderID: orderID, amount: 1000 }, refund) }, }, { name: "全額ポイント払いのキャンセル(ポイント還元あり)" , orderID: "order_point" , assertFunc: func (t *testing.T, orderID string ) { // 注文ステータスの検証 order, err := orderRepo.FindByID(ctx, orderID) AssertEqual(t, nil , err) AssertEqual(t, "CANCELLED" , order.Status) // ユーザーポイントの検証 user, err := userRepo.FindByID(ctx, order.UserID) AssertEqual(t, nil , err) AssertEqual(t, 1500 , user.Points) }, }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { // テスト対象の実行 err := usecase.CancelOrder(ctx, tt.orderID) AssertEqual(t, nil , err) // テストケース固有の検証処理を実行 tt.assertFunc(t, tt.orderID) }) } // 差分検出を用いた手法 tests := [] struct { name string orderID string expectedDiff Diffs }{ { name: "クレジットカード決済のキャンセル(返金あり)" , orderID: "order_card" , expectedDiff: Diffs{ "orders" : { U: map [KeyHash]Record{ HashKey({ "id" : "order_card" }): { "status" : "CANCELLED" , }, }, }, "refunds" : { C: []Record{ { "order_id" : "order_card" , "amount" : 1000 , }, }, }, }, }, { name: "全額ポイント払いのキャンセル(ポイント還元あり)" , orderID: "order_point" , expectedDiff: Diffs{ "orders" : { U: map [KeyHash]Record{ HashKey({ "id" : "order_point" }): { "status" : "CANCELLED" , }, }, }, "users" : { U: map [KeyHash]Record{ HashKey({ "id" : "user_1" }): { "points" : 1500 , }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { // 全テーブルの差分をキャプチャしつつ実行 var err error diffs := DiffDB(t.Context(), db, func () { err = usecase.CancelOrder(ctx, tt.orderID) }) // 返り値の検証 AssertEqual(t, nil , err) // 差分の検証 AssertRecords(t, tt.expectedDiff, diffs) }) } 差分抽出の実装 差分抽出は、以下の手順で行います。 全テーブルの主キー情報を含むスキーマ情報を取得 テスト対象の関数実行前に、対象データベースの全テーブルの全レコードを取得し、メモリ上に保存 テスト対象の関数を実行 関数実行後に、再度全テーブルの全レコードを取得 実行前後のレコードを比較し、差分を抽出 スキーマ情報の取得や全レコードの取得は、データベースの種類に依存するため、DBSourceインタフェースを定義し、各データベースに応じた実装を用意しました。 ここでは、スキーマ情報と全レコードの取得の実装については割愛し、差分抽出のコアロジックを示します。 func createDiff(source DBSource, before, after map [ string ] map [KeyHash]Record) Diffs { diffs := Diffs{} // 各テーブルごとに差分を抽出 for tableName := range before { diff := Diff{U: map [KeyHash]Record{}} // 各テーブルのスキーマ情報を取得 schema := source.schemata()[tableName] // レコードごとに差分を比較 // keyは各レコードの主キーのハッシュ値 for key, record := range before[tableName] { // 実行前にあったレコードが実行後にも存在する場合 if afterRecord, ok := after[tableName][key]; ok { updates := Record{} for k, v := range record { // 値が異なるカラムのみを抽出 if !reflect.DeepEqual(v, afterRecord[k]) { updates[k] = afterRecord[k] } } // 更新があった場合のみdiffに追加 if len (updates) != 0 { diff.U[key] = updates } // Createを抽出するために、afterから既存レコードを削除 delete (after[tableName], key) continue } keyValues := map [ string ]any{} for _, key := range schema.keys { keyValues[key] = record[key] } diff.D = append (diff.D, keyValues) } // 残ったafterのレコードは新規作成されたレコード for _, record := range after[tableName] { diff.C = append (diff.C, record) } // 期待値を書く際に差分がない場合は省略可能にするため、空スライスはnilに変換 if len (diff.U) == 0 { diff.U = nil } // テーブルに何らかの差分があった場合のみdiffsに追加 if len (diff.C) != 0 || len (diff.U) != 0 || len (diff.D) != 0 { diffs[tableName] = diff } } // 期待値を書く際に差分がない場合は省略可能にするため、空のDiffsはnilに変換 if len (diffs) == 0 { return nil } return diffs } 複数データベースへの対応 ZOZOTOWNではリプレイスを進めるにあたり、一時的に既存環境と新環境それぞれのデータベースに書き込むケースが存在します。それぞれで期待した差分があるかを検証できるように複数データベースにも対応しました。DiffExtractorにラベルを付けて複数設定することで、差分出力時にそれぞれどのデータベースで生じた差分かを判定できます。 // 複数データベースに対応した実装 type DiffExtractor struct { // 複数のデータベースを抽出対象とする // mapのキーはデータベースを特定するためのラベル sources map [ string ]DBSource } func NewDiffExtractor(sources map [ string ]DBSource) DiffExtractor { return DiffExtractor{sources: sources} } func (de DiffExtractor) Diff(ctx context.Context, f func ()) map [ string ]Diffs { diffs := map [ string ]Diffs{} before := map [ string ] map [ string ] map [KeyHash]Record{} for name, source := range de.sources { // テスト対象実行前の各データベースをキャプチャ before[name] = source.dump(ctx) } f() after := map [ string ] map [ string ] map [KeyHash]Record{} for name, source := range de.sources { // テスト対象実行後の各データベースをキャプチャ after[name] = source.dump(ctx) // 各データベースの差分抽出 diffs[name] = createDiff(source, before[name], after[name]) } return diffs } // 複数データベースで利用する場合のテストヘルパー例 func DiffDBForDoubleWrite(ctx context.Context, mysqlDB *sql.DB, mssqlDB *sql.DB, f func ()) map [ string ]Diffs { mysqlSource := dd.NewMySQLSource(ctx, mysqlDB) mssqlSource := dd.NewMSSQLSource(ctx, mssqlDB) extractor := dd.NewDiffExtractor( map [ string ]dd.DBSource{ "mysql" : mysqlSource, "mssql" : mssqlSource, }) return extractor.Diff(ctx, func () { f() }) } 導入時の工夫点 差分検出を導入するにあたり、テストの安定性を保つためにいくつか工夫しました。 非固定値の取り扱い 本手法では、特定のカラムだけでなくレコード全体を対象とするため、自動採番されたIDや現在時刻、乱数など実行のたびに値が変わるカラムについても常に考慮する必要があります。 IDの自動採番については、各テストケースの実行前にオートインクリメントなどのシーケンスをリセットするために、TRUNCATE文を実行することで対応しました。これにより、発行されるIDを固定し、期待値を固定できます。 // テスト実行前のセットアップ例 func SetupTestDB(t *testing.T, db *sql.DB) { t.Helper() // ユーザー定義された全テーブル名の取得 tables := GetTableNames(db) for _, table := range tables { _, err := db.Exec(fmt.Sprintf( "TRUNCATE TABLE %s" , table)) if err != nil { t.Fatal(err) } } } 実際にTRUNCATE文を実行するには外部キーの制約チェックを一時的に解除する、もしくはテーブルの処理順序を制御するといったことも必要になります。 現在時刻については、関数内で time.Now() を使わず、時刻を引数として渡すか、インタフェースを介して注入することでテスト内の時刻を固定しています。これにより、時刻に関する期待値も固定できます。 乱数については、乱数生成の箇所をインタフェース化して期待値を固定する方法などが考えられますが、それが難しい場合も考慮して、アサーション関数において値一致以外も可能にしました。具体的には文字列に対する期待値に *regexp.Regexp を指定した場合には正規表現マッチを行うようにしました。 // 乱数を含むフィールドの検証例 expectedDiffs := Diffs{ "orders" : { C: []Record{ { "order_id" : regexp.MustCompile( `\A[0-9a-f]{32}\z` ), // 乱数を正規表現で表現 "amount" : 10 , "order_at" : "2026-01-01T00:00:00Z" , }, }, }, } AssertRecords(t, expectedDiffs, actualDiffs) 現状は使うケースがなかったため用意していませんが、数値型の乱数を利用する場合にはそれぞれ専用の型を用意して、検証処理を切り替えることも検討しています。 期待値の正規化 会員基盤ではテストデータのセットアップにレコードデータではなくモデルデータを利用しているため、データベースから抽出した差分の値とテストケースの期待値とでは形式が異なることもあります。例えば、モデルデータではbool型のフィールドが、データベースからの出力時はint型の0もしくは1になるケースがあります。他にもモデルデータでは値オブジェクトとして定義されているフィールドが、データベースからの出力時はその値オブジェクトの内部の値になるケースもあります。 このような場合に、テストケースの期待値をデータベースからの出力に合わせた形式で記述するのは、テストケースの可読性を損なうため、アサーション関数内で比較時に正規化する方針としました。実装の詳細は割愛しますが、リフレクションを用いて reflect.ValueOf 関数で reflect.Value に変換した後、 Kind() メソッドで元となる型を判定して正規化を行っています。 差分の除外 データベースのトリガー処理による時刻の挿入などアプリケーション側から制御できない値や、もうアプリケーション上から利用していないカラムのような例外的に差分から除外したいケースが存在します。そこで、抽出した差分からカラムを指定して除外するための Ignore() メソッドを用意しました。また、用意されていない方法で特定のカラムを検証するために一旦、差分から取り除いた上で別途検証するという場合にも利用できます。 diffs := DiffDB(ctx, db, func () { someFunc() }) // hogeは廃止済みのカラムで期待値の管理対象外とする AssertRecords(t, expectedDiffs, diffs.Ignore( "hoge" )) また、特定のテストケースによらず、アプリケーション全体で除外したい条件があるような場合に対応するため、DiffExtractorに除外用の関数をオプションで設定できるようにしました。 var someIgnoreColumnFunc = func (tableName, columnName string ) bool { // 例えば、全テーブルのhogeカラムを常に除外する場合 return columnName == "hoge" } // 除外関数をオプションに設定したテストヘルパー例 func DiffDB(ctx context.Context, db *sql.DB, f func ()) dd.Diffs { source := dd.NewMySQLSource(ctx, db) source.WithIgnoreColumnFunc(someIgnoreColumnFunc) extractor := dd.NewDiffExtractor( map [ string ]dd.DBSource{ "mysql" : source, }) diffs := extractor.Diff(ctx, func () { f() }) return diffs[ "mysql" ] } まとめ 本記事では、Goを用いたデータベースのレコード差分検出によるテスト手法について紹介しました。 複雑なテストケースになるほど、データベースへの副作用を網羅的に検証することの重要性が増します。本手法を導入することで、期待しない副作用を検知しやすくなり、テストコードの可読性と保守性も向上しました。今後も、より良いテスト手法の模索と改善を続けていきたいと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは。商品基盤部の藤本です。 私たちのチームでは、Spring Bootで実装したJavaアプリケーションの起動時間の短縮に取り組んでいます。今回の記事では、Class Data Sharing(以下、CDS)を本番で稼働しているアプリケーションに実際に適用した内容を紹介します。 導入時には、Datadog Java Agentとの両立という課題にも直面しました。そのため、トレースとメトリクスの送信をOpenTelemetryとMicrometerに置き換える対応もあわせて実施しました。 本記事では、CDSの概要、導入効果、導入手順、Datadogの問題とOpenTelemetryへの移行までを順に説明します。 環境 今回の取り組みは次の環境で実施しました。 Java 21 (Eclipse Temurin) Spring Boot 3.5 Class Data Sharing(CDS)とは CDSは、クラスメタデータを jsa (Java Shared Archive)ファイルとして保存し、起動時に再利用する仕組みです。クラスロード時の処理を省略できるため、起動時間とメモリ使用量の改善が期待できます。 CDSには次の2種類があります。 Default CDS JDK標準ライブラリのクラスを対象とする方式 Application CDS アプリケーションクラスも対象に含める方式 各ベンダーから配布されているJDKを使用する場合は、通常はDefault CDSが有効になっています。一方、 jlink で作成したカスタムJREでは jsa ファイルが含まれておらず、そのままではCDSが機能しません。今回の取り組みではこの点も考慮して、カスタムJRE向けのCDS生成も実施しました。 CDS導入の効果 実際にApplication CDSを導入し、本番環境で導入前後を比較しました。比較対象は起動時間と、クラスロードの影響でレイテンシが高くなりがちな初回のエンドポイント実行時間です。計測結果は次のようになりました。 指標 導入前 導入後 改善率 起動時間 6.440 s 3.354 s 47.92%短縮 初回のエンドポイント実行時間 371 ms 165 ms 55.53%短縮 この数値は本番環境で10回計測した値を平均したものです。この結果から、起動時間だけでなく初回アクセス時のレイテンシも改善できることを確認できました。 導入手順 ここからは、実際に適用した手順を説明します。 カスタムJRE向けのDefault CDSの生成 カスタムJREでCDSを使用するには、 jlink でJREを作成した後に -Xshare:dump を実行してCDSアーカイブを生成します。 jlink --add-modules java.base,java.compiler --output /javaruntime /javaruntime/bin/java -Xshare :dump -XX :+UseCompressedOops /javaruntime/bin/java -Xshare :dump -XX :-UseCompressedOops このコマンドを実行すると、 classes.jsa と classes_nocoops.jsa という名前でアーカイブが生成されます。実行環境によって参照先アーカイブが変わるため、 UseCompressedOops の有無で両方を生成しておくことをおすすめします。 Application CDS(Dynamic CDS archive)の生成 次に、アプリケーション終了時のロード済みクラス情報から jsa ファイルを生成します。以前のApplication CDSでは、手動でクラスリストを作成してからアーカイブを作る手順が必要でした。現在はDynamic CDS archiveという方式により、アプリケーションを終了した時点のロード済みクラスから jsa ファイルを自動生成できます。この方式はJDK 13で導入されています。 openjdk.org Spring Framework 6.1以降では、 -Dspring.context.exit=onRefresh を使ってアプリケーションを起動直後に終了できます。 java \ -XX :ArchiveClassesAtExit = /path/to/application.jsa \ -Dspring .context. exit= onRefresh \ -jar my-app.jar CDSが正しく有効になっているか確認するには、 -Xlog:cds オプションを使ってCDSの読み込み状況をログへ出力します。別の方法として、 -Xshare:on を使ってCDSの使用を強制できます。 -Xshare:on を指定すると、 jsa ファイルを正しく読み込めない場合はアプリケーションの起動が失敗する点に注意してください。 java \ -Xshare :on \ -XX :SharedArchiveFile = /path/to/application.jsa \ -jar my-app.jar Datadogの問題とOpenTelemetryへの移行 Application CDS導入時に最も大きかった課題は、Datadog Java Agentとの両立でした。Datadog Java Agentは実行時にクラスパスへクラスを追加するため、CDSが前提とする「生成時と実行時の整合性」が崩れてしまいます。 この挙動はGitHub上でもIssueとして報告されています。 github.com なお、このIssueは2026年1月にクローズされ、Datadog側の方針として「CDSをサポートする」方向ではなく「JEP 483(Ahead-of-Time Class Loading & Linking)の利用を推奨する」ことが示されています。コメントでは、Java 25以降で dd-java-agent と JEP 483 を組み合わせる利用方法が案内されています。 Datadog Java AgentがCDSに対応していないため、トレースとメトリクスの送信方式を次の構成に移行しました。 トレース:OpenTelemetry(OTLP)→ Datadog Agent メトリクス:Micrometer(DogStatsD)→ Datadog Agent 送信先をDatadog AgentではなくOpenTelemetry Collectorに変更することも検討しましたが、構成や運用の変更にかかるコストの観点から今回は見送りました。 OpenTelemetryによるトレース送信 OpenTelemetryでトレースを送信するために、アプリケーションへ opentelemetry-spring-boot-starter を導入しました。Spanの開始と終了は HandlerInterceptor で制御しながら、送信内容が重複しないよう spring-webmvc の自動計装は無効化しました。 otel : instrumentation : spring-webmvc : enabled : false exporter : otlp : endpoint : "http://${DD_AGENT_HOST:localhost}:4318" traces : exporter : otlp sampler : always_on Datadog Agent側では、OTLP受信を有効化します。 DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT = 0 . 0 . 0 .0:4318 DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT = 0 . 0 . 0 .0:4317 Micrometerによるメトリクスの送信 メトリクスは micrometer-registry-statsd を追加し、DogStatsD形式でDatadog Agentへ送信できるようにします。 management : statsd : metrics : export : enabled : true flavor : datadog host : ${DD_AGENT_HOST:localhost} port : 8125 Spanの属性のマッピング調整 OpenTelemetryで送ったトレースがDatadogで期待通りに絞り込めない場合は、属性名の差分が原因になることがあります。目的のトレースを検索できるようにするため、たとえば次のようなDatadog上の属性を付与する必要があります。Datadogのマッピング表を参照し、必要な属性をSpan属性とResource属性へ明示的に設定しました。 Span属性: http.method 、 http.url などのリクエストごとに変わる値 Resource属性: service.name 、 env 、 container.name などの共通値 docs.datadoghq.com メトリクスについても、ダッシュボードで計測している指標に合わせて、メモリやGCに関する収集項目を追加し、メトリクス名やタグを調整しました。 おわりに 本記事では、Application CDSを活用して起動時間を短縮する取り組みと、Datadog Java Agentとの両立課題への対応を紹介しました。実測では、起動時間と初回エンドポイント実行時間の両方で改善を確認できました。 CDSはオプション追加だけで始められますが、カスタムJRE、暖機処理、監視ライブラリの挙動まで含めて設計することで効果を最大化できます。Javaアプリケーションの起動時間に課題がある方は、まずは小さな範囲で計測しながら段階的に導入してみてください。 現在はJava 25とSpring Boot 4への移行を進めており、その後でAOT Cacheの導入を予定しています。引き続き、運用環境に即した形でパフォーマンス改善の取り組みを進めていきます。 ZOZOでは、一緒にサービスを盛り上げてくれる仲間を募集中です。興味のある方は以下の採用情報をご確認ください。 corp.zozo.com
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、新規事業部バックエンドブロックの三浦です。2025年6月にリリースされたマッチングアプリ「 ZOZOマッチ 」のバックエンド開発を担当しています。 ZOZOマッチでは、App StoreやGoogle Playの決済システムを利用したアプリ内課金を提供しており、定期購読(サブスクリプション)することで一部機能の制限解除や機能拡張が可能になります。アプリ内課金の実装には、アプリからの購入処理と購読のキャンセル・返金・自動更新といったライフサイクルイベントの同期処理が必要です。ZOZOマッチではこれらの処理をスクラッチで開発しました。 本記事では、特に開発が難航した、ライフサイクルイベントによって変更される課金ステータスをバックエンドに同期する仕組みについて紹介します。AppleとGoogleそれぞれが提供する通知の仕組みの違いや、同期処理の実装における課題と工夫についても解説します。 目次 はじめに 目次 ZOZOマッチにおけるアプリ課金 購読ステータス同期の必要性 購読開始までの流れ 取引IDの管理 レシート検証 購読情報の同期 (Apple) App Store Server Notifications ZOZOマッチでの同期方法 通知内容 通知の署名検証とデコード 課金ステータスの更新 同期処理における注意点 ASSNへのレスポンス返却 購読情報の同期 (Google) Real-time Developer Notifications ZOZOマッチでの同期方法 通知内容 通知の検証とデコード 課金ステータスの更新 同期処理における注意点 RTDNへのレスポンス返却 ASSNとの違い比較 開発・運用を通じて大変だったこと 開発時のテストの難しさ 運用面での課題 まとめ さいごに ZOZOマッチにおけるアプリ課金 はじめに、ZOZOマッチにおける課金の概要について説明します。前述の通り、ZOZOマッチではアプリ内課金を利用して 複数の定期購読プラン を提供しています。定期購読することで一部機能の制限解除や機能拡張が可能になり、いずれもサービスの利用体験に関わる内容となっています。 設計当初はストア以外での決済手段も検討しましたが、以下の理由からアプリ内課金のみを採用しました。 AppleおよびGoogleの規約上、ZOZOマッチで販売しているコンテンツはアプリ内課金を利用する必要がある Web決済をする場合はWebでも同様の機能を提供する必要がある(ZOZOマッチはアプリ専用サービスのため該当しない) 実装面では、開発工数の削減を目的にアプリ内課金の管理をサポートするSaaSの導入を検討しました。しかし、費用対効果の観点から最終的にはスクラッチ開発を選択しました。 購読ステータス同期の必要性 定期購読には、購読開始から更新・解約までのライフサイクルがあります。購読開始時はアプリ内で購入処理が行われ、ストア側で決済が完了すると購入情報がアプリに返されるため、バックエンドへの反映は比較的シンプルです。 一方で、以下のようなイベントはストア側で自動的に処理されるため、アプリを経由せず発生します。 自動更新:購読期間の終了時の自動課金 更新失敗:支払い方法の問題による課金失敗 解約:ユーザーによる自動更新の停止 これらのイベントをバックエンドへ正確に同期しなければ、「解約したのに有料機能が使える」「自動更新したはずなのに有料機能が使えなくなった」といった不整合が発生してしまいます。社内ではこのストア側の購読ステータス同期に関するノウハウが少なかったため、Apple/Googleそれぞれの公式ドキュメントを読み込み、チーム内で調査するところから始めました。 以降、購読開始の流れを簡単に説明した後、今回調査や実装が特に難航したストア側の購読ステータスの同期方法をAppleおよびGoogleに分けて解説していきます。 購読開始までの流れ 購読開始および購読再開の場合、ユーザーがアプリを通じてApp StoreまたはGoogle Play Storeで購入手続きを行います。 購入手続きが完了すると、ストア側からアプリにレシート(購読情報)が返されます。アプリはこのレシート内の取引IDをバックエンドに送信し、バックエンド側でレシートを検証します。 取引IDの管理 レシート情報には、ストア側で管理している取引IDが含まれています。AppleとGoogleでは取引IDの体系が異なるため、それぞれの仕様を理解する必要があります。 プラットフォーム プロパティ 内容 Apple originalTransactionId 購読の一生を通じた親識別子。同一の購読契約(同一 Apple ID/同じ購読系列)であれば解約、再購読しても変わらない Apple transactionId 各トランザクションの個別識別子。新規購読、自動更新、返金など課金イベントごとに発行される Google purchaseToken 購読の取引識別子。購読〜更新〜解約まで変わらない。有効期限切れ後の再購読やプラン変更時に再発行される Google linkedPurchaseToken プラン変更や再購読時に設定される、1つ前の購読の取引識別子。新旧の契約を紐付けるために使用される レシート検証 ZOZOマッチでは、これらの取引IDを履歴としてDBに保持し、以下の観点でレシートを検証します。 リプレイ攻撃対策:同じレシートが複数回使用されていないか 不正利用チェック:他ユーザーのレシートを流用していないか 有効性検証:取引IDを基にストアAPIから取得したレシート情報が有効であるか すべての検証が完了したら、ユーザーの購読ステータスを更新し、購読対象のサービスが利用可能になります。 購入時も後述するストア側からの通知(Apple Server Notifications / Google RTDN)が送信されます。ただし、アプリ経由でレシートを取得しているため、通知による同期処理はスキップされます。 購読情報の同期 (Apple) App Storeにおけるアプリ内課金では、自動更新や解約などのステータス変更はApple側で管理されています。これらの情報をバックエンドへ同期するために、Appleでは App Store Server Notifications というサービスが提供されています。 App Store Server Notifications App Store Server Notifications(以下ASSN)は、Appleが提供するサーバー間通知サービスです。App Storeで発生した課金イベントをApple側から直接指定のエンドポイントに通知する仕組みです。この仕組みによって、定期購読の自動更新などユーザーがアプリを開かずに発生する課金イベントも同期できます。 通知先のエンドポイントはApp Store Connect上で設定します。購読の開始、更新、解約などの課金イベントが発生すると、このエンドポイントに対してHTTP POSTリクエストで購読情報が送信されます。 ASSNにはV1とV2の2つのバージョンが存在しますが、V1は非推奨となっており、V2の利用が推奨されています。ZOZOマッチでもV2を利用しています。 ZOZOマッチでの同期方法 ZOZOマッチでは、ASSNからの通知を受け取るためのWebhookエンドポイント(以下ASSN通知受信Webhook)を用意しています。課金イベントが発生すると、このエンドポイントに対してHTTP POSTリクエストが送信されます。 下記は全体のアーキテクチャ図です。 ASSN通知受信Webhook自体はFargate上で稼働しており、API Gateway経由で通知を受け付けます。Appleからのみリクエストを受け付けるよう、WAFでIPアドレス制限をかけています。加えてWebhook側で署名検証を実施し、不正なリクエストを防止しています。 下記はASSN通知受信Webhookの全体の処理フローです。 ASSNからはoriginalTransactionIdを含む通知がHTTPリクエストで送信されます。originalTransactionIdを基にDBから既存の購読情報を特定し、通知内容に応じてユーザーの課金ステータスを更新します。更新後はレスポンスとして処理の成否をASSNへ返却します。 通知内容 ASSNから受け取る通知のペイロードは、JWS(JSON Web Signature)形式で署名されたJSONデータです。 ZOZOマッチのバックエンドはJava + Spring Bootを採用しています。そのため、JWS署名付きペイロードの検証とデコードにはAppleが公式で提供している app-store-server-library-java というオープンソースのライブラリを使用します。 以下はAppleで公開されているペイロードのサンプルです。実際に送られてくるペイロードは通知全体の情報、その中の取引情報(signedTransactionInfo/signedRenewalInfo)で二重にJWS署名されているため、各々検証が必要です。 { " notificationType ": " SUBSCRIBED ", " subtype ": " INITIAL_BUY ", " version ": 2 , " data ": { " environment ": " Production ", " bundleId ": " co.oceanjournal ", " appAppleId ": 1231451896 , " bundleVersion ": 1 , " signedTransactionInfo ": " ewogICAgInRyZ...eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.AReJJaUWG8fc-Y8n8YHj… ", " signedRenewalInfo ": " ewogICAgInRyYW5z...eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ARcVInaJJG8fG-8t5TY8n8YHj… " } , " signedDate ": " 1767229200 " } エンコードされている取引情報の詳細は、下記公式ドキュメントを参照してください。 signedTransactionInfo signedRenewalInfo 通知の署名検証とデコード 以下ASSN通知受信Webhook内の署名検証とデコードの実装例です。apple-app-store-server-sdk-javaの SignedDataVerifierクラス を利用して、ASSNから送られてきたJWS署名付きペイロードの検証・デコードをします。 SignedDataVerifierは初期化時にEnvironment(SANDBOX/PRODUCTION)を指定する必要があります。これは環境によって検証時に利用する公開鍵が切り替わるためです。ZOZOマッチでも本番環境とテストやアプリ公開へ向けた審査時に利用されるSandbox環境の両方が存在しているため、環境ごとにSignedDataVerifierのBeanを分けて定義しています。 // SignedDataVerifierのBean定義 @Bean ( "signedDataVerifierForPrd" ) public SignedDataVerifier signedDataVerifierForPrd( final AppStoreProperty property) { return createSignedDataVerifierClient(property, Environment.PRODUCTION); } @Bean ( "signedDataVerifierForSandbox" ) public SignedDataVerifier createSignedDataVerifierClientForSandbox( final AppStoreProperty property) { return createSignedDataVerifierClient(property, Environment.SANDBOX); } private SignedDataVerifier createSignedDataVerifierClient( final AppStoreProperty property, final Environment environment) { return new SignedDataVerifier( getRootCertificates(), property.getBundleId(), property.getAppAppleId(), environment, property.getEnableOnlineCheck() ); } 署名の検証処理では、まず本番環境用のSignedDataVerifierで検証を試みます。環境の不一致によるエラー(Sandbox環境からの通知)が発生した場合は、Sandbox環境用のSignedDataVerifierで再検証します。 @Qualifier ( "signedDataVerifierForSandbox" ) private final SignedDataVerifier signedDataVerifierForSandbox; @Qualifier ( "signedDataVerifierForPrd" ) private final SignedDataVerifier signedDataVerifierForPrd; public AppStoreNotification decodedNotificationInfo( @NonNull AppStoreSignedPayload signedPayload) throws PurchasePlatformServerException { try { // ペイロード全体の署名検証とデコードを行う final var decodedNotification = signedDataVerifierForPrd.verifyAndDecodeNotification(signedPayload.value()); // デコードしたペイロードからトランザクション情報を取得し、トランザクション情報の署名検証とデコードを行う final var decodedTransactionInfo = TrustedAppStoreSubscription.of( signedDataVerifierForPrd.verifyAndDecodeTransaction(decodedNotification.getData().getSignedTransactionInfo()) ); return AppStoreNotification.of(decodedNotification, decodedTransactionInfo); } catch (VerificationException e) { if (e.getStatus() == VerificationStatus.INVALID_ENVIRONMENT) { return fallbackToSandbox(signedPayload); } throw new PurchasePlatformServerException( "AppStoreNotificationV2から取得した署名付きペイロードの検証に失敗しました。" , e); } } 課金ステータスの更新 デコードした通知情報には発生した課金イベントの種類を示す notificationType が含まれています。この値に応じて、ユーザーの課金ステータスを更新します。主な通知タイプは以下の通りです。 通知タイプ 内容 SUBSCRIBED ユーザが新しくサブスクリプションを購入した・あるいはユーザがサブスクリプションを再購入した DID_RENEW サブスクリプションが正常に更新された DID_FAIL_TO_RENEW 課金の問題によりサブスクリプションが更新に失敗 REFUND サブスクリプション課金の払い戻しがされた REFUND_REVERSED 顧客の異議申し立てにより、以前に払い戻されたサブスクリプション課金を取り消した(REFUND処理の取り消し) DID_CHANGE_RENEWAL_STATUS 顧客が自身でサブスクリプションの自動更新を有効/無効化した TEST テスト用の通知 詳細な通知タイプについては Apple公式のドキュメント を参照してください。 同期処理における注意点 ASSNでは通知される順番の保証がされていません。例えば「定期購読の更新に失敗」→「成功」の順でイベントが発生しても、「更新成功」の通知が先に届く可能性があります。この場合、後から届いた「更新失敗」の通知で課金ステータスを上書きしてしまうと、実際には有効な購読が無効として扱われてしまいます。 ZOZOマッチでは、このような先祖返りが発生しないよう以下の対策を実施しています。 通知内のoriginalTransactionIdを基に、DBから既存の購読情報を取得 DB上の最新のレコードのトランザクション発生日時と受け取った通知内のトランザクション発生日時を比較 DBのトランザクション発生日時 >= 通知内のトランザクション発生日時 の場合は、古い情報で上書きする可能性があると判断。更新処理は行わずエラーを投げる 下記はサブスクリプションが自動更新された場合(DID_RENEW)の通知サンプルです。取引情報の一部を抜粋しています。 { " transactionId ": " 2000001120654880 ", " originalTransactionId ": " 2000001120642345 ", " appStoreBundleId ": " jp.test ", " appStoreProductId ": " monthly_subscription_02 ", " purchaseDate ": 1767229200 , " expiresDate ": 1769907600 , " signedDate ": 1767229500 , " price ": 980 , " transactionReason ": " RENEWAL " } 比較するトランザクション発生日時は通知内の purchaseDate または signedDate を利用します。signedDateは通知全体の署名の発行日時を表しています。購入/再購入時はpurchaseDateが明示的に含まれますが、自動更新や解約など他のイベント単位の日付のパラメータは存在しないため、代替としてsignedDateを使用しています。 また、ネットワークの問題などにより、同一の通知が複数回届くこともあります。ZOZOマッチでは通知に含まれる一意の識別子 notificationUUID を記録し、同じ通知が再度処理されないようにしています。 前述の通りASSN通知受信Webhookでは前段のWAFでASSNの通知元IPアドレスからの通信のみを許可しています。IP制限を入れる場合、App Store Serverが使うIPブロック 17.0.0.0/8 からの通信を許可する必要があります。このIPブロックでSandbox環境とProduction環境の両方をカバーしています。 ASSNへのレスポンス返却 ASSN通知受信Webhookはリクエストを受け取った後、ASSNに対してHTTPレスポンスを返却する必要があります。ASSNに対してHTTPステータス200を返却した場合は、ASSN側でも通知が正常に受信されたとみなされ処理は完了となります。200系以外のステータスコードを返却した場合は、ASSN側は配信失敗とみなし再送を試みます。しかし再送回数には最大5回までと制限があり、5回全て失敗した場合は以降の再送は行わないため注意が必要です。 対策として、Appleが提供するApp Store Server APIで通知履歴を取得できます。 POST https://api.storekit.itunes.apple.com/inApps/v1/notifications/history 公式ドキュメント: Get Notification History このAPIでは本番環境で過去180日分の通知履歴を取得できます。リクエストボディで特定の期間、失敗した通知のみ、特定のtransactionIdに紐づく通知のみなど、様々な条件で絞り込みが可能です。これにより、失敗した通知を定期的にチェックし、バックエンド側で再処理する仕組みの構築も可能です。 購読情報の同期 (Google) Google Playにおけるアプリ内課金でも、自動更新や解約などのステータス変更はGoogle側で管理されています。これらの情報をバックエンドへ同期するために、Googleでは Real-time Developer Notifications という機能が提供されています。 Real-time Developer Notifications Real-time Developer Notifications(以下RTDN)はGoogleが提供する通知サービスで、アプリ内課金の状態変化をリアルタイムで通知します。AppleのASSNがHTTP POSTで直接通知を送信するのに対し、RTDNは Google Cloud Pub/Sub をベースにした非同期メッセージングで通知します。 Google Play Console上でRTDN用のPub/Subトピックを設定し、メッセージの受信側としてSubscriberを用意します。これにより、RTDNの通知をPub/Sub経由で受信できるようになります。 ZOZOマッチでの同期方法 ZOZOマッチでは、RTDNの通知を取得するためのSubscriberアプリケーションを用意しています。Pub/SubにはPush型とPull型の2つの方式があります。Push型ではSubscriptionがSubscriberに対してメッセージを送信し、Pull型ではSubscriberがSubscriptionからメッセージを取得しにいきます。ZOZOマッチではPull型を採用し、Subscriptionからメッセージを取得して購読情報の同期処理を行います。 下記は全体のアーキテクチャ図です。 SubscriberはFargate上で稼働しているアプリケーションで、NAT Gateway経由でPub/Subに接続します。 下記はSubscriberの処理フローです。 RTDNの通知内容はPub/SubのTopic経由でSubscriptionに送信されます。SubscriberアプリケーションはこのSubscriptionからメッセージを取得します。 DB内の購読情報は purchaseToken または linkedPurchaseToken を基に特定します。purchaseTokenは固定値ではなく、購読プランの変更などで新しい値が発行される場合もあります。その際、前回のpurchaseTokenが linkedPurchaseToken として紐付けられるため、既存の購読情報を特定する際はこちらを優先して使用します。 ただし、RTDNの通知には最小限の情報しか含まれておらず、purchaseTokenのみが通知されます。linkedPurchaseTokenを取得するには、purchaseTokenを使って下記のGoogle Play Developer APIを呼び出し、購読の詳細情報を参照する必要があります。 GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token} 詳細なAPI仕様は Googleの公式ドキュメント を参照してください。 DBから取得した内容と通知内容に応じてユーザーの課金ステータスを更新します。最後に更新の成否を該当のSubscriptionに送信して処理が完了します。 通知内容 RTDNの通知はJSONデータで、base64エンコードされたdataフィールドに購読イベントの詳細情報が含まれています。以下はGoogleが公開しているペイロードのサンプルです。 { " message ": { " attributes ": { " key ": " value " } , " data ": " eyAidmVyc2lvbiI6IHN0cmluZywgInBhY2thZ2VOYW1lIjogc3RyaW5nLCAiZXZlbnRUaW1lTWlsbGlzIjogbG9uZywgIm9uZVRpbWVQcm9kdWN0Tm90aWZpY2F0aW9uIjogT25lVGltZVByb2R1Y3ROb3RpZmljYXRpb24sICJzdWJzY3JpcHRpb25Ob3RpZmljYXRpb24iOiBTdWJzY3JpcHRpb25Ob3RpZmljYXRpb24sICJ0ZXN0Tm90aWZpY2F0aW9uIjogVGVzdE5vdGlmaWNhdGlvbiB9 ", " messageId ": " 136969346945 " } , " subscription ": " projects/myproject/subscriptions/mysubscription " } dataフィールドのbase64デコード後の内容は以下の通りです。 { " version ": " 1.0 ", " packageName ": " com.some.thing ", " eventTimeMillis ": 1767229220 , " oneTimeProductNotification ": {} , " subscriptionNotification ": {} , " voidedPurchaseNotification ": {} , " testNotification ": {} } Notification で終わるフィールドは購読イベントの種類によって該当するフィールドのみがレスポンスに含まれます。そしてこのフィールドの中に詳細のイベント内容を表す notificationType というプロパティが含まれています。取引IDである purchaseToken もこの中に入っています。 プロパティ名 内容 oneTimeProductNotification 1回だけの単発購入に関する通知 subscriptionNotification サブスクリプション(更新、解約など)に関する通知 voidedPurchaseNotification システム側でサブスクリプションを無効化した場合や、返金を行った場合などの通知 testNotification Google Play Consoleから手動で送信するテスト用の通知 通知毎の内容の詳細は Google公式のドキュメント を参照してください。 通知の検証とデコード SubscriberからPub/Subへの接続には Spring Cloud GCP を利用しています。具体的には、Spring Integrationベースのアダプター PubSubInboundChannelAdapter を使用しています。 以下の設定をしたPubSubInboundChannelAdapterをBeanとして登録することで、Pub/Subからのメッセージを指定したMessageChannelで受け取れるようになります。 設定値 内容 pubSubTemplate Pub/Sub操作用のヘルパー(接続情報等を持つ) subscriptionName メッセージ取得先のSubscription名 ackMode メッセージの処理結果を自動で返すか、アプリ側で明示的に返すかの設定 outputChannel 取得したメッセージの出力先チャンネル @Configuration @RequiredArgsConstructor public class PubSubConfig { @Value ( "${rtdn-subscriber.subscription-name}" ) private String subscriptionName; /** * Pub/Subサブスクリプションのメッセージを受け取るアダプターを生成する. */ @Bean public PubSubInboundChannelAdapter messageChannelAdapter( @Qualifier ( "pubsubInputChannel" ) MessageChannel inputChannel, PubSubTemplate pubSubTemplate) { final var adapter = new PubSubInboundChannelAdapter(pubSubTemplate, subscriptionName); adapter.setAckMode(AckMode.MANUAL); adapter.setOutputChannel(inputChannel); return adapter; } @Bean public MessageChannel pubsubInputChannel() { return new DirectChannel(); // 同期処理。メッセージを受け取ったスレッドでそのまま処理 } } 下記はSubscriber側の実装例です。 @ServiceActivator アノテーションを付与することで、前述のPubSubInboundChannelAdapter経由で受け取ったメッセージを処理できます。ペイロードをデシリアライズして通知情報を取得し、購読イベントの詳細情報を取得します。購読ステータスの更新処理が完了した後にSubscriptionに対してレスポンスを送信します。レスポンス値については後続で説明します。 private final RtdnMessageConverter converter; private final SubscriptionNotificationUseCase subscriptionNotificationUseCase; @ServiceActivator (inputChannel = "pubsubInputChannel" ) public void messageReceiver( @Header (GcpPubSubHeaders.ORIGINAL_MESSAGE) BasicAcknowledgeablePubsubMessage message, @Payload String payload) { var shouldAck = false ; try { final var notificationMessage = converter.convertFromPayloadToNotificationMessage(payload); shouldAck = subscriptionNotificationUseCase.handleNotification( converter.convertFromRtdnMessageToNotificationInfo(notificationMessage, message.getPubsubMessage().getMessageId()) ); } finally { try { if (shouldAck) { message.ack(); } else { message.nack(); } } catch (Exception e) { log.error( "ACK/NACKの送信に失敗しました。" , e); } } } 課金ステータスの更新 通知情報にはどのような課金イベントが発生したかを示す notificationType プロパティが含まれています。主な通知タイプは以下の通りです。 通知内容を含むフィールド名 通知タイプ 内容 subscriptionNotification SUBSCRIPTION_PURCHASED ユーザが新しく購読した subscriptionNotification SUBSCRIPTION_RENEWED サブスクリプションの自動更新が正常に更新された subscriptionNotification SUBSCRIPTION_IN_GRACE_PERIOD サブスクリプションの自動更新の猶予期間に入った subscriptionNotification SUBSCRIPTION_EXPIRED サブスクリプションが期限切れになった subscriptionNotification SUBSCRIPTION_CANCELED サブスクリプションがキャンセルされた oneTimeProductNotification ONE_TIME_PRODUCT_PURCHASED 1回だけの単発購入が行われた oneTimeProductNotification ONE_TIME_PRODUCT_CANCELED 1回だけの単発購入がキャンセルされた 詳細な通知タイプに関しては Google公式のドキュメント を参照してください。 同期処理における注意点 RTDNでも通知の到着順序は保証されていません。そのため、ASSNと同様にDB上の最新トランザクション発生日時と通知のトランザクション発生日時を比較します。 下記はサブスクリプションが自動更新された場合(SUBSCRIPTION_RENEWED)の通知サンプルです。 { " version ": " 1.0 ", " packageName ": " jp.test ", " eventTimeMillis ": " 1770889888958 ", " subscriptionNotification ": { " version ": " 1.0 ", " notificationType ": 2 , " purchaseToken ": " a1b2c3d4e5f6g7h8i9j0k ", " subscriptionId ": " monthly_subscription_01 " } } RTDNの場合は通知内の eventTimeMillis をトランザクション発生日時として比較します。eventTimeMillisは対象の課金イベントが発生した日時をミリ秒単位で表しています。 RTDNでも同様に DB上のトランザクション発生日時 >= 通知内のトランザクション発生日時 の場合は、古い情報で上書きする可能性があると判断し、更新処理は行わずエラーを投げます。 RTDNへのレスポンス返却 Google Cloud Pub/SubにはACK(Acknowledgement)とNACK(NegativeAcknowledgement)という仕組みがあります。これはSubscriberがメッセージを正しく受信・処理したことをSubscriptionに伝えるためのものです。Subscriberは処理完了時にACKをPub/Subに送信することで、Subscription側では対象メッセージの処理が完了したとみなし削除します。NACKを送信した場合や一定時間内にACKが送信されなかった場合、Pub/Sub側ではメッセージは「未確認」扱いとなり一定時間が経過した後に再送が行われます。 ASSNとの違い比較 最後に、AppleのASSNとGoogleのRTDNの違いを簡単にまとめます。 Apple(ASSN) Google(RTDN) 通信方式 Push型(HTTP POST) Push型 / Pull型(Pub/Sub) ペイロードの署名 JWS署名あり なし リトライ制御 Apple側で最大5回 Pub/Subの設定で制御可能 開発・運用を通じて大変だったこと 開発時のテストの難しさ 開発当時はアプリ内課金のスクラッチ開発に関する参考情報が少なく、特に動作確認に苦労しました。 Appleの課金テストをする際はApple側で提供しているアプリ内課金やApple Payトランザクションを無料でテストできるSandbox環境を利用します。ドキュメントを確認しながらの手探りでのテストだったため、以下のような点で苦労しました。 事前にApple Store Connect上でテスト用のユーザーアカウントの作成が必要だった Sandbox環境だとサブスクリプションの更新頻度が短いなど本番とは異なる挙動があった また、AppleやGoogleの公式ドキュメントに記載されている購読ステータスの各条件を、実際にテスト端末で1つずつ再現して確認しました。通知が正しく処理されているかはログやDBの状態から確認する必要があり、地道な検証作業が続きました。中でも返金に関するテストは特に情報が少なく、再現手順の調査から始める必要がありました。 運用面での課題 運用開始後は、経理部門へ渡す売上データの作成も必要になりました。こちらもスクラッチで開発しましたが、売上データとしてどのような情報が必要かを経理側の要件と公式ドキュメントを照らし合わせながら調査し、レシート内のどのフィールドが該当するかを特定していきました。 また、運用を続ける中でストア側の仕様変更を即座にキャッチできないことがありました。SaaSを利用していれば、こうした仕様変更への追従やサポートを受けられるかもしれません。 課金周りの実装や知見が属人化してしまっているのも課題の1つです。今後新しい課金プランの追加や仕様変更が発生した際にも対応できるよう、ドキュメント整備やナレッジ共有を進めていきたいと考えています。 まとめ 本記事ではZOZOマッチにおけるアプリ内課金の同期方法について紹介しました。アプリ内課金の導入を検討している方がいれば、ぜひ参考にしてみてください。 さいごに ZOZOでは、ZOZOTOWNの一本足打法からの脱却を狙い、新規事業にも果敢に取り組んでいます。このような挑戦を一緒に楽しめる仲間を募集しています。ご興味のある方は、採用ページをご覧ください。 hrmos.co
アバター
.entry-content ul > li > ul { display: none; } tr td:first-child { white-space: nowrap; } .nowrap2+table tr td:nth-child(2) { white-space: nowrap; } td { text-align: left !important; } 目次 目次 はじめに この記事の対象読者 背景・課題 背景 課題 AI駆動開発ワークフローの概要 AIサービスごとの役割 Devin Playbook ユーザー起動のPlaybook(Slack → Devin) !ai_task(単一タスク実装) !ai_tasks(タスク分割&並列実装) !human_review(人間承認フロー) 人間レビューが必要なケース ワークフロー自動呼び出しのPlaybook !fix_ci_failure(CI失敗時の自動修正) !fix_review_comments(レビュー指摘の自動修正) !context_curation(AIコンテキストの週次更新) 使用技術 機能一覧 アーキテクチャ SlackからPR承認までの完全フロー 2つのワークフローの役割 フロー別の使い分け 実装 設定ファイル AI Task Implementation:Issueから実装までの自動化 AI Task Dispatcher(ゲートウェイ) 実装ワークフローの動作 ラベルによる動作の分岐 重複実行の防止の仕組み キャンセル時のクリーンアップ AI Review Orchestrator:レビューから承認までの自動化 レビュー・ワークフローの動作 統合レビュー・修正ループの詳細フロー ジョブ構成と責務 Phase 1: Gate(スキップ判定) Phase 2: Perspective Router(ハイブリッド分類方式) 分類フロー 事前分類ルール Phase 3: CI完了待機 CI完了を待つ理由 Phase 4: Claude Review Perspectiveラベルに基づくレビュー レビュープロンプトの構造 分類判定ルール 自動承認の対象外となる変更タイプ 指摘の重大度とblockingフラグ(Conventional Comments準拠) PR説明文の自動生成・更新 関連 Issue の自動取得 PRタイトル・説明文の自動生成 タイトル生成ルール Phase 5: Devin自動修正 Devin修正の種類 修正ループの詳細 ループカウントの管理 Perspectiveラベルの更新ルール ループ終了条件 自動承認の条件 データ連携方式 Claude → Devinのデータ渡し 構造化出力スキーマ 技術的なポイント 1. concurrencyによる並行実行の制御 2. Devinセッションの自動停止 trapハンドラー(即時対応) if: cancelled()バックアップステップ GitHub Actionsのキャンセルシーケンス 3. AWS BedrockによるClaude呼び出し 4. マクロ形式によるプロンプト管理 5. HEAD SHAの検証 使い方 AI Task Implementationの使い方(Slack → Issue → 実装) 起動方法 注意点 AI Review Orchestratorの使い方(PR → レビュー → 承認) 自動レビューの流れ Claudeへの質問・修正依頼 操作手順まとめ 導入効果 定量的な効果 定性的な効果 Renovate PRの自動レビュー・承認 自動承認の条件 AI開発の効果測定(週次レポート) 計測される KPI レポート例 AIコンテキストの自動育成 動作フロー 更新対象ファイル ドキュメントの階層構造 各ディレクトリの役割 残っている課題 運用上の注意点 スキップラベルの活用 人間のレビューが必要なケース ワークフロー別ラベル操作 トラブルシューティング まとめ 実現した効果 重要なポイント 今後の展望 最後に 参考リンク はじめに こんにちは、新規事業部 マネジメントポータルブロックの岡本です。 「PRを作ったけど、レビューまで時間がかかる」「忙しいときレビューが後回しになる」──この悩み、開発者なら誰もが経験したことがあるのではないでしょうか。 私たちのチームでは、ClaudeとDevinを組み合わせたAI駆動開発ワークフローを導入し、レビュー待ち時間ゼロ・CIエラーの自動修正・Issueからの即時の実装開始を実現しました。この記事では、その設計思想から実装詳細、導入効果までを解説します。 この記事の対象読者 GitHub Actionsを使ったCI/CDの経験がある方 AIコーディングアシスタント(Claude、Devinなど)に興味がある方 チーム開発の効率化を検討している開発リーダー・マネージャー 背景・課題 背景 私たちのチームはZOZOマッチ管理画面を少人数で運用しており、各メンバーがフロントエンド・バックエンド・LP作成と複数の領域を担当しています。そのため、コンテキストスイッチが頻繁に発生し、「PRを作成したが、レビュアーが別タスクに集中している」「CIエラーを修正しようとしたら、他の緊急対応が入った」といった状況が日常的に起きていました。 結果として、レビューやCI修正の待ち時間が発生しやすく、開発のリズムが途切れがちでした。そこで、「人間は創造的な作業に集中し、定型的な作業はAIに任せる」という方針のもと、Claude(レビュー・判断)とDevin(実装・修正)を組み合わせたワークフローを整備しました。 課題 従来の開発フローでは、次のような課題がありました。 課題 詳細 レビュー待ち時間 PR作成から人間のレビューまでに数時間〜1日かかることがあった レビュー品質のばらつき レビュアーの経験や専門分野によって指摘内容に差があった Issue着手の遅延 Issueが起票されてから実装開始まで時間がかかっていた ライブラリ更新の負担 Renovateが作成する大量の依存関係の更新PRのレビュー・マージ作業が開発者の負担になっていた これらの課題を解決するため、2つのGitHub Actionsワークフローを中心に自動化を構築しました。 AI駆動開発ワークフローの概要 AIサービスごとの役割 このワークフローでは、2つのAIサービスを組み合わせて活用しています。GitHub Actionsとの連携における実行方式の違いから、次のように役割を分けています。 Devin(実装・修正):Issueの実装、CIエラー修正、レビュー指摘の修正を担当。非同期でセッションを開始し、修正完了まで一貫した作業が可能。ブランチ操作からコミット・プッシュまでを単一セッションで実行でき、修正完了後にPRが更新されてワークフローが再トリガーされる設計とも相性が良い Claude Code(レビュー・判断):PRのレビュー、分類判定、自動承認の判断を担当。同期的に結果を返すため、「自動承認 / 人間レビュー依頼 / Devin修正起動」の分岐処理を同一ワークフロー内で完結できる。構造化出力(JSONスキーマ)により、後続の処理時に扱いやすい形式でレビュー結果を取得できる Devin Playbook Playbookは、Devinに対する再利用可能な指示セットです。タスクの種類に応じた判断ロジック(セキュリティチェック、タスクサイズ判定など)、GitHub連携処理、エラーハンドリングが定義されており、一貫した品質でタスクを実行できます。 Playbookには、Slackからユーザーが起動するものと、GitHub Actionsワークフローが自動呼び出しするものの2種類があります。今回作成したワークフローでは、Devin APIを使用し、macro形式(macro IDとpayloadをJSONで渡す)でPlaybookを呼び出しています。 ユーザー起動のPlaybook(Slack → Devin) Slackで @devin をメンションし、 ! で始まるコマンドを送信すると、対応するPlaybookが実行されます。 コマンド 用途 対象タスク !ai_task 単一タスク実装 Small〜Medium(半日以内) !ai_tasks タスク分割&並列実装 Large(1日以上、複数コンポーネント) !human_review 人間承認フロー セキュリティ関連、要確認タスク !ai_task (単一タスク実装) 指定されたタスクを直接実装し、PRを作成します。 !ai_tasks (タスク分割&並列実装) 複雑なタスクを分析し、最大5つのsub-issueに分割して並列実装します。 !human_review (人間承認フロー) セキュリティ関連や重要な変更など、人間の確認が必要なタスク向けです。 人間レビューが必要なケース 認証・認可の変更 個人情報(PII)の扱い シークレット・APIキーの変更 ビジネスロジックの大幅な変更 本番環境への影響が大きい変更 ワークフロー自動呼び出しのPlaybook 次のPlaybookはGitHub Actionsワークフローから自動的に呼び出されます。ユーザーが直接実行することはありません。 Playbook 用途 呼び出し元 !fix_ci_failure CI失敗時の自動修正 ai-review-orchestrator.yml (CI失敗時) !fix_review_comments レビュー指摘の自動修正 ai-review-orchestrator.yml (blockingの指摘があるとき) !context_curation AIコンテキストの週次更新 devin-context-refresh.yml (毎週月曜09:00 JST) !fix_ci_failure (CI失敗時の自動修正) CI(Lint、Typecheck、Test)が失敗したPRを自動修正します。 GitHub ActionsのCIログを解析してエラーを特定 エラー種別に応じて修正(Lint → pnpm run lint:fix 、Type → 型エラー修正、Test → テスト更新) 修正をコミット・プッシュしてセッション終了 ビジネスロジックの変更が必要な場合は修正せず人間にエスカレーション !fix_review_comments (レビュー指摘の自動修正) Claude Reviewからの指摘( blocking: true )を自動修正します。 Blocker / Major の指摘を必須の修正対象として処理 各指摘の suggestion に従って修正 修正をコミット・プッシュしてセッション終了 認証・認可に関わる変更は修正せず人間にエスカレーション !context_curation (AIコンテキストの週次更新) PRコメントから学習可能な知見を抽出し、AI関連ドキュメントを週次で自動更新します。 直近7日間のPRコメント(Bot除外)を収集 コメントから有用な変更点、落とし穴、ルール例外、命名・設計ガイドラインを抽出 AGENTS.md 、 docs/ai-context/ 、 docs/guidelines/ 、 docs/review-perspectives/ を更新 更新内容をPRとして作成(人間がレビュー・マージ) 使用技術 ワークフローは、次の技術・サービスを組み合わせて構成しています(ワークフロー上の使用順)。 技術・サービス 用途 Slack タスク起点のオペレーション(Devinへの指示) GitHub Issues/PR/Labels 進行管理とトリガー Devin API Issueの実装・CIエラー修正・レビュー指摘の修正 GitHub Actions ワークフロー実行(CI待機、ジョブ分岐、通知) AWS Bedrock Claude実行基盤(セキュリティ要件対応) Claude Code Action PRレビュー実行・分類判定・自動承認 機能一覧 機能 内容 主な効果 AI Task Implementation Slack/ラベル起点で Devinが実装しPRを作成 Issue着手の即時化 AI Review Orchestrator Claudeがレビューし、Devinが修正 レビュー待ちの解消と品質の均一化 CI自動修正 CI失敗時にDevinが修正を実行(最大3回) 手戻り削減 自動承認 AI_ONLYかつ指摘なしで自動承認 PR処理の高速化 週次メトリクス KPIを週次で自動計測しIssue化 効果測定の継続 ナレッジ更新 PRコメントからドキュメントを自動更新 レビュー品質の継続改善 アーキテクチャ SlackからPR承認までの完全フロー 2つのワークフローの役割 ワークフロー トリガー 役割 AI Task Implementation Issueに ai-task / ai-tasks ラベル追加 DevinがIssueの実装を担当し、PRを作成 AI Review Orchestrator PRの作成・更新 Claudeがレビュー、Devinが修正、条件を満たせば自動承認 フロー別の使い分け 各Playbookの詳細なフローは「 Devin Playbook 」セクションを参照してください。 シナリオ Slack コマンド 処理フロー 単一タスク(直接の実装が可能) @devin !ai_task タスク説明 Devin → Issue + PR → 自動レビュー → 承認 複雑なタスク(分割が必要) @devin !ai_tasks タスク説明 Devin → 親 Issue + sub-issue → 並列実装 → 自動レビュー 要承認タスク @devin !human_review タスク説明 Devin → Issue 作成 → 人間承認 → 自動実装 Claudeに質問・修正依頼 @znm-claude-code 依頼内容 Claudeが対応(PR/Issueコメント) 実装 設定ファイル ワークフローはGitHub Actionsの設定ファイルで定義しています。主要ファイルと役割は次のとおりです。 ファイル 役割 .github/workflows/ai-task-dispatcher.yml Issueラベル検知・条件判定のゲートウェイ(条件を満たす場合のみimplementationを起動) .github/workflows/ai-task-implementation.yml DevinによるIssue実装(dispatcher から呼び出し) .github/workflows/ai-review-orchestrator.yml PRレビュー・修正ループ・Renovate統合 .github/workflows/pr-perspective-router.yml Perspectiveラベルの再分類 .github/workflows/ai-renovate-review.yml Renovate PRの依存関係レビュー(orchestrator から呼び出し) .github/workflows/ai-metrics-report.yml 週次メトリクス生成 .github/workflows/devin-context-refresh.yml コンテキスト更新 AI Task Implementation:Issueから実装までの自動化 AI Task Dispatcher(ゲートウェイ) ai-task-dispatcher.yml は、Issueのラベル追加を検知し、条件を満たす場合のみ実装ワークフローを起動するゲートウェイです。 Dispatcherを分離している理由は3つあります。 条件判定の一元化:ラベル検知と条件チェックを単一ファイルに集約し、保守性を向上 不要なワークフロー発火の防止:条件を満たさない場合は早期終了し、リソースを節約 責務の分離:「いつ起動するか」(dispatcher)と「何をするか」(implementation)を明確に分離 実装ワークフローの動作 ai-task または ai-tasks ラベルがIssueに追加されると、dispatcher経由でDevinが自動的に実装を開始します。トリガー条件は次のとおりです。 Issueの labeled イベントで起動(dispatcherが検知) ai-task / ai-tasks ラベル付与時のみ対象 in-progress が付いていない場合だけ開始 起点は2パターンあります。 Playbook起動(Slackなど):PlaybookがIssueを作成 → ai-task / ai-tasks を付与 → このワークフローが起動 既存Issue起点:このワークフローが skip_issue_creation: true を渡してPlaybookを実行(Issue作成はスキップ) ラベルによる動作の分岐 ラベルに応じて対応するPlaybookが実行されます。各Playbookの詳細なフロー(セキュリティチェック、タスクサイズ判定、実装手順など)は「 Devin Playbook 」セクションを参照してください。 ラベル Playbook 動作 ai-task !ai_task 単一タスクとして実装 ai-tasks !ai_tasks タスク分割 → 並列実装 sub-issue作成には gh sub-issue 拡張が必要です(Playbook側 or Devinのリポジトリ初期設定で事前インストール)。 重複実行の防止の仕組み 同じIssueに対する重複実行を防ぐため、concurrency設定でIssue番号単位の同時実行を1つに制御し、進行中の実行があれば新しい実行でキャンセルします。 例えば、同じIssueに短時間でラベル追加が続いた場合、 labeled イベントが複数回発火します。この二重起動を防止します。 キャンセル時のクリーンアップ ワークフローがキャンセルされた場合、次のクリーンアップ処理を実行します。 キャンセル時にDevinセッションを停止 キャンセル時に in-progress ラベルを削除して再実行可能にする 失敗時も in-progress ラベルを削除して再実行可能にする AI Review Orchestrator:レビューから承認までの自動化 レビュー・ワークフローの動作 PRが作成・更新されると、 ai-review-orchestrator.yml が自動実行されます。このワークフローはPRの作成・更新をトリガーに発火し、PRの作成元は問いません。 AI Task Implementation経由のPR: DevinがPR作成 → このワークフローが発火 手動作成されたPR: 開発者の直接PR作成 → このワークフローが発火 なお、 ai-task-dispatcher.yml とこのワークフローは独立しており、直接の呼び出し関係はありません。 統合レビュー・修正ループの詳細フロー ジョブ構成と責務 ai-review-orchestrator.ymlは約1,500行の大規模ワークフローです。6つのフェーズに分かれています。 フェーズ ジョブ名 責務 依存関係 1 gate スキップ判定、PR情報取得、イテレーション管理 なし 2 perspective-router PR分類、perspective:* ラベル付与(初回のみ、Renovate以外) gate 3 wait-for-ci CI完了待機(最大20分、30秒間隔ポーリング) gate, perspective-router 4a devin-ci-fix CI失敗時のDevin自動修正 gate, wait-for-ci (CI失敗時) 4b-1 renovate-review Renovate PR専用のClaude依存関係レビュー( ai-renovate-review.yml 呼び出し) gate, wait-for-ci (CI成功かつRenovate PR時) 4b-2 claude-review CI成功/未検出時のClaudeレビュー実行(Renovate以外) gate, wait-for-ci (CI成功/未検出時) 4c devin-review-fix レビュー指摘ありの場合のDevin自動修正 gate, claude-review (指摘あり時) 4d auto-approve AI_ONLY + 指摘なしの場合の自動承認 gate, claude-review or renovate-review 4e needs-human-review NEEDS_HUMAN + 指摘なしの場合の人間レビュー依頼 gate, claude-review or renovate-review 4f vrt Visual Regression Testingの実行 auto-approve or needs-human-review 5 max-iterations-reached イテレーション上限到達時の通知 gate 6 ci-timeout CI待機タイムアウト時の通知 gate, wait-for-ci Phase 1: Gate(スキップ判定) 次の条件でワークフローをスキップします。 条件 判定方法 理由 Draft PR pull_request.draft == true 作業中のためレビュー不要 Bot作成PR(Devin・Renovate 以外) actor がbotかつDevin・Renovateでない 無限ループ防止 Fork PR pull_request.head.repo.fork == true シークレット/権限制約のためスキップ skip-ai-review ラベル ラベル存在チェック 明示的スキップ Renovate PR actor が renovate[bot] Renovate専用レビュー( renovate-review )へルーティング ai-auto-approved ラベル ラベル存在チェック 既に承認済み また、新しいpushがあった場合はAI関連ラベルを削除し、承認を取り消して再レビューします。ラベルの削除ルールはpush元によって異なります。 人間のpush: ai-auto-approved / needs-human-review / perspective:* を削除して再分類 Botのpush: ai-auto-approved / needs-human-review のみ削除して分類は維持 Phase 2: Perspective Router(ハイブリッド分類方式) PRの変更内容を分析し、適切なレビュー観点を決定します。キーワードベースの事前分類 + Claudeフォールバックのハイブリッド方式を採用しています。 分類フロー 事前分類ルール ラベル パスパターン キーワード perspective:workflow .github/workflows/ , .github/actions/ , action.yml workflow, ci, cd, pipeline, jobs, steps, runs-on perspective:security auth/ , login/ , session/ auth, token, secret, credential, password, encrypt, sanitize, xss, csrf, pii, vulnerability perspective:quality *.test.* , *.spec.* , __tests__/ , eslint , prettier , tsconfig test, spec, lint, format, accessibility, a11y, aria-, vitest, jest perspective:dependency package.json , pnpm-lock.yaml , yarn.lock , package-lock.json dependency, package, version, upgrade, license perspective:api /api/ , openapi , swagger , schema , .dto. api, endpoint, schema, openapi, graphql, rest perspective:perf (パスパターンなし) performance, optimize, cache, lazy, memo, bundle, split, prefetch perspective:business domain/ , business/ permission, role, billing, payment, validation, status, transition, rule perspective:general 上記に該当しない場合のみ - 複数パターンにマッチした場合、該当ラベルがすべて付与されます。 perspective:general は他のラベルと併用されません。 Phase 3: CI完了待機 他のCIジョブ(型チェック、lint、テストなど)の完了を待機します。待機の仕組みは次のとおりです。 自分自身のワークフローやVRTは待機対象から除外 部分一致の除外パターンも併用して精度を上げる 最大20分、30秒間隔でポーリング CI Runが一定時間見つからない場合(約5分)は ci_status=skipped としてClaudeレビューへ進みます。 success / neutral / skipped は成功扱いです。 CI完了を待つ理由 CIが失敗している状態でレビューしても意味がない CI失敗 → Devin修正 → またCI失敗 → またレビュー... という無駄なループを防ぐ CIが通った状態のコードに対してレビューすることで、品質の高いフィードバックが可能 Phase 4: Claude Review Claude Code Actionを使用して、PRを包括的にレビューします。 Perspectiveラベルに基づくレビュー Claude Reviewでは、Phase 2で付与された perspective:* ラベルに応じて、対応するレビュー観点ドキュメントを動的に読み込みます。 ラベル 対応ファイル 主なレビュー観点 perspective:workflow docs/review-perspectives/workflow.md GitHub Actions/CI/CD の設計・セキュリティ perspective:security docs/review-perspectives/security.md 認証・認可・入力検証・機密情報の管理 perspective:quality docs/review-perspectives/quality.md テスト・Lint・アクセシビリティ perspective:dependency docs/review-perspectives/dependency.md 依存関係の追加・更新・削除 perspective:api docs/review-perspectives/api.md API設計・スキーマ・エラーハンドリング perspective:perf docs/review-perspectives/perf.md パフォーマンス・キャッシュ・最適化 perspective:business docs/review-perspectives/business.md ビジネスロジック・権限・バリデーション perspective:general docs/review-perspectives/general.md 汎用的なコード品質(他に該当しない場合) レビュー実行時の読み込みフローは次のとおりです。 PRから perspective:* ラベル一覧を取得 対応する docs/review-perspectives/*.md を読み込み 取得内容をプロンプトに差し込みレビュー実行 たとえば、セキュリティ関連のPRには security.md の観点が、API関連のPRには api.md の観点が適用されるといった具合です。 レビュープロンプトの構造 レビュープロンプトは次のルールで構成されます。 Bedrock経由でClaudeを実行 Perspective指定と重大度定義をプロンプトに含める Blocker/Majorを blocking: true として扱う 分類判定ルール 判定 条件 自動承認 AI_ONLY テストコード、型定義、明確なバグ修正、リファクタリング 可能 NEEDS_HUMAN ドキュメント、スタイル、設定ファイル、セキュリティ関連 不可 自動承認の対象外となる変更タイプ 次の変更は必ず人間のレビューが必要です( NEEDS_HUMAN と判定)。 ドキュメント更新:README.md, *.mdファイル、docs/配下の変更 スタイル調整:CSS, Tailwindクラス、デザイン・レイアウト変更 設定ファイル変更:tsconfig, eslint, prettier, vite.config等 指摘の重大度とblockingフラグ(Conventional Comments準拠) 重大度 blocking 意味 修正要否 [Blocker] true 必ず修正が必要 必須 [Major] true 強く推奨される修正 必須 [Minor] false 推奨される改善 任意 [Nit] false 微細な指摘(スタイル等) 任意 [Question] false 仕様・意図の確認質問 回答必須(修正不要) [Praise] false 良いコードへの称賛 対応不要 blocking: true の指摘が1件以上あると、Devin自動修正が実行されます。 PR説明文の自動生成・更新 Claude Reviewでは、レビュー結果と同時にPR説明文を自動生成・更新する機能があります。 関連 Issue の自動取得 PR本文からclosing keywords( Closes #123 , Fixes #456 など)を自動的に抽出し、関連Issueを特定します。抽出されたIssue番号はレビュープロンプトに含まれ、Claudeが変更の意図を理解する際のコンテキストとして活用されます。 PRタイトル・説明文の自動生成 ClaudeはPR内の全てのコミットと変更ファイルを分析し、次の手順でタイトルと説明文を自動生成します。 PR内の全コミット一覧を取得(最大50件) 全ての変更ファイルを分析 PR全体を代表するタイトルを日本語で生成(Conventional Commits形式) .github/PULL_REQUEST_TEMPLATE.md に沿って説明文を生成 タイトル生成ルール 形式: {prefix}: {変更内容の要約} (例: feat: ユーザー認証機能の追加 ) prefix:feat / fix / docs / refactor / chore / testから変更種別に応じて選択 50文字以内で簡潔に日本語で記載 複数の変更がある場合は、主要な変更を代表するタイトルに 生成されたタイトルと説明文は、レビュー完了後に自動的にPRへ反映されます(それぞれ生成された場合のみ更新)。PR作成者がタイトルや説明文を適切に書けなかった場合でも、Claudeが内容を自動生成してくれます。最新のコミットだけでなくPRに含まれる全てのコミットを考慮するため、複数回のpushがあっても正確な内容が生成されます。 Phase 5: Devin自動修正 Claudeのレビューで指摘があった場合、またはCIが失敗した場合、Devinが自動修正します。 Devin修正の種類 ジョブ名 用途 参照 Playbook devin-ci-fix CI失敗の修正(CIログを解析して自動修正) fix-ci-failure.devin.md devin-review-fix レビュー指摘の修正(Claudeの指摘に基づいて自動修正) fix-review-comments.devin.md Devinへの修正指示は次の手順で行います。 blocking: true の指摘だけを抽出し、抽出結果をmacro payloadに組み込んでDevinに送信します。 修正ループの詳細 ループカウントの管理 イテレーションはCI修正 + レビュー修正の合計回数でカウントします。 Push種別 カウント動作 理由 人間がPR作成 0からスタート 新規PR Devinがpush カウント継続 自動修正ループ中 人間が修正push 0にリセット 人間の修正後は新規扱い 上限(3回)に達した後でも、人間が修正してpushすればカウントがリセットされ、レビューが再開されます。 Perspectiveラベルの更新ルール Push種別 Perspective Router ラベル動作 人間がPR作成 ✅ 実行 新規ラベル付与 Devinがpush ❌ スキップ 既存ラベル維持 人間が修正push ✅ 実行 既存ラベル削除 → 再分類 ループ終了条件 正常終了:CI成功 + レビュー指摘なし → 自動承認 上限到達: iteration_count >= MAX_ITERATIONS → 人間レビュー依頼 タイムアウト:CI待機が20分超過 → 人間への確認依頼 自動承認の条件 次のすべてを満たした場合のみ自動承認されます。 CI成功 Claudeレビュー完了 分類が AI_ONLY blockingな指摘がない HEAD SHAが変更されていない(レース条件対策) データ連携方式 Claude → Devinのデータ渡し Claudeのレビュー結果をDevinに渡す方式は、ジョブ出力による直接連携を採用しています。 構造化出力スキーマ Claudeに --json-schema オプションでレビュー結果の構造を指定し、次の項目を必須にしています。 classification (AI_ONLY / NEEDS_HUMAN) Blocker/Major/Minorの件数フラグとカウント fix_instructions (修正指示文) issues (指摘一覧:severity / blocking / file / messageなど) 技術的なポイント 1. concurrencyによる並行実行の制御 同一PRに対して1つのワークフローのみ実行( group で制御) cancel-in-progress: true のため、Devinが修正をpushした場合: 新しいワークフローが開始される 古いワークフローは自動的にキャンセルされる Devinセッションはtrapハンドラーにより自動停止される 新しいワークフローはCI完了を待機してからレビュー 2. Devinセッションの自動停止 ワークフローがキャンセルされた際、実行中のDevinセッションを確実に停止するため、二重のセーフティネットを実装しています。 trapハンドラー(即時対応) Devin API呼び出し中にシグナル(SIGINT/SIGTERM)を受信した場合、trapハンドラーが即座にセッションを停止します。SIGINT/SIGTERMを受けると即座にcleanupを実行し、 /tmp/devin_resp.json から session_id を取得して10秒以内にセッションを終了します。 if: cancelled() バックアップステップ API呼び出しが完了してからキャンセルされた場合に備えて、専用のクリーンアップステップを用意しています。 outputs または /tmp/devin_resp.json から session_id を取得してセッションを停止します。 GitHub Actionsのキャンセルシーケンス GitHub Actionsはキャンセル時に次の順序でシグナルを送信します。 SIGINT 送信 → 7.5秒待機 SIGTERM 送信 → 2.5秒待機 強制終了 trapハンドラーは最初の SIGINT でcleanupを実行するため、最大10秒の猶予内でDevinセッションを停止できます。 3. AWS BedrockによるClaude呼び出し セキュリティ要件を満たすため、Claude APIではなくAWS Bedrock経由でClaudeを呼び出しています。AWS認証情報を設定し、Claude Code Actionの use_bedrock を有効化しています。 設定項目 値 リージョン us-east-1 IAMロール <your-bedrock-access-role> モデル(レビュー) Claude(Bedrock Application Inference Profile) モデル(分類) Claude Haiku( anthropic.claude-3-haiku-20240307-v1:0 ) 4. マクロ形式によるプロンプト管理 Devinへの指示は、macro IDとpayloadをJSONとして渡すマクロ形式で標準化しています。Playbookのmacro IDと一対一で対応しており、一貫性のある指示が可能です。 5. HEAD SHAの検証 レース条件を防ぐため、承認前にHEAD SHAが変更されていないことを確認しています。実行開始時のHEAD SHAと現在のSHAを比較し、一致しない場合は自動承認をスキップします。 使い方 AI Task Implementationの使い方(Slack → Issue → 実装) SlackからDevinにタスクを依頼すると、Issue作成から実装、PR作成までを自動で行います。各Playbookの詳細なフローは「 Devin Playbook 」セクションを参照してください。 起動方法 方法 手順 動作 Slackから(推奨) @devin !ai_taskタスク説明 DevinがIssue作成 → ai-task ラベル付与 → ai-task-dispatcher.yml 発火 → 実装開始 Slackから(複雑なタスク) @devin !ai_tasksタスク説明 Devinが親Issue作成 → sub-issueに分割 → 並列実装 GitHub Issueから Issue に ai-task / ai-tasks ラベルを付与 ai-task-dispatcher.yml 発火 → 実装開始 注意点 in-progress ラベルがあるIssueはスキップされます(実装中のため) セキュリティ関連を検出した場合、 !human_review への切り替えを案内してセッション終了 タスクサイズが合わない場合、適切なPlaybookへの切り替えを案内してセッション終了 AI Review Orchestratorの使い方(PR → レビュー → 承認) PRが作成・更新されると、 ai-review-orchestrator.yml が自動起動し、Claudeがレビューします。 自動レビューの流れ CI完了待機(最大20分) Perspective分類(初回のみ、変更内容に応じてラベル付与) Claudeレビュー(Perspectiveに基づく観点でレビュー) 結果に応じた分岐: 指摘あり → Devin自動修正(最大3回) AI_ONLY + 指摘なし → 自動承認 NEEDS_HUMAN → 人間レビュー依頼 Claudeへの質問・修正依頼 PRやIssueに対してClaudeに質問や修正依頼をしたい場合は、 @znm-claude-code をメンションしてコメントします。 操作手順まとめ 入口を選ぶ。 @devin !ai_task (単一タスク)/ @devin !ai_tasks (分割タスク)/ @devin !human_review (人間の承認が前提)。Slackを使わない場合はIssueにラベルを付与する DevinがIssue/PRを作成し、 ai-review-orchestrator.yml が自動起動する CI完了後にClaudeがレビューし、指摘があればDevinが修正する(最大3回) AI_ONLY かつ指摘なしなら自動承認、 NEEDS_HUMAN は人間レビューへ。Claudeへの質問や修正依頼は @znm-claude-code を利用する 運用上の例外( skip-ai-review / renovate など)は「運用上の注意点」を参照してください。 導入効果 定量的な効果 指標 導入前 導入後 PR作成からレビュー開始まで 平均4時間 即時 CIエラー修正時間 平均30分/回 自動 単純なIssueの実装開始 平均1日 即時 RenovatePRの処理 手動確認・マージ 依存関係レビュー後に自動承認 定性的な効果 開発者が集中する時間の確保 定型的なエラー修正から解放され、創造的な作業に集中できるようになった レビュー品質の均一化 Perspectiveに基づくレビューで、一貫した品質のレビューが実現した レビュアーの経験や専門分野に依存しない指摘が可能になった 心理的安全性の向上 AIが最初にレビューすることで、人間のレビューでの「些細な指摘」が減った レビュアーは本質的な議論へ集中できるようになった ナレッジの蓄積 レビュー観点ドキュメント( docs/review-perspectives/ )が知識ベースとして機能 新しいメンバーのオンボーディングにも活用できる ライブラリ更新の自動化 Renovateが作成する依存関係の更新PRをClaudeがレビュー AI_ONLYかつ指摘なしなら自動承認 セキュリティパッチの迅速な適用に貢献 Renovate PRの自動レビュー・承認 Renovateが作成した依存関係の更新PRは、専用ワークフロー( ai-renovate-review.yml )で自動処理されます。 自動承認の条件 patch / minorの安全な更新のみ 破壊的変更なし Blocker / Major指摘が0件 AI開発の効果測定(週次レポート) ai-metrics-report.yml により、AI駆動開発の効果を週次で自動計測し、GitHub Issueとして報告します。 計測される KPI 指標 目標 説明 AI_ONLYレビュー通過率 60%以上 AIのみで承認可能と判定されたPRの割合 自動承認率 - 実際に自動承認されたPRの割合 Devinの修正成功率 70%以上 Devinが修正を試みたPRのうち、マージに至った割合 イテレーション上限到達率 10%以下 修正ループが上限(3回)に達したPRの割合 平均イテレーション回数 1.5回以下 PRあたりの平均の修正回数 CI失敗率 20%以下 CI失敗が発生したPRの割合 レポート例 ## AI 開発効果測定 週次レポート **集計期間**: 2025-12-15 ~ 2025-12-22 **対象 PR 数**: 25 件 ### 📊 主要指標(KPI) | 指標 | 実績 | 目標 | 状態 | |------|------|------|------| | AI _ ONLY レビュー通過率 | 72.0% | 60% 以上 | ✅ | | 自動承認率 | 68.0% | - | - | | Devin 自動修正成功率 | 85.7% | 70% 以上 | ✅ | | イテレーション上限到達率 | 4.0% | 10% 以下 | ✅ | | 平均イテレーション回数 | 1.2 回 | 1.5 回以下 | ✅ | | CI 失敗率 | 16.0% | 20% 以下 | ✅ | 🎉 すべての主要指標が目標を達成しています! このレポートは毎週月曜10:00 JSTに自動生成され、 ai-metrics ラベル付きのIssueとして作成されます。 AIコンテキストの自動育成 devin-context-refresh.yml により、PRコメントからのフィードバックを元にAIコンテキストドキュメントを週次で自動更新します。 動作フロー 更新対象ファイル カテゴリ ファイル ルート AGENTS.md docs/ai-context/ coding-standards.md , context.md , data-model.md , design-template.md , glossary.md , implementation-patterns.md , metrics.md , pr_review_classification.md , pr_review_comment_rules.md , role.md , task-assignment.md docs/guidelines/ architecture.md , environment-variables.md , error-handling.md , naming-conventions.md , validation.md docs/review-perspectives/ api.md , business.md , dependency.md , general.md , perf.md , quality.md , security.md , workflow.md ドキュメントの階層構造 AIエージェントがコンテキストを読み込む際、次の階層構造で参照が行われます。 CLAUDE.md:Claude Codeが最初に読み込むファイル。 @AGENTS.md でメインガイドを参照 AGENTS.md:AIエージェント向けメインガイド。最重要ルール(3-5個)と各ドキュメントへのパス参照を記載(300行未満を推奨) docs/:詳細情報は各サブディレクトリに委譲し、AGENTS.mdからパス参照のみ記載 この構造によってコンテキストの段階的読み込みが可能になり、トークン消費を抑えつつ必要な情報にアクセスできます。 各ディレクトリの役割 ディレクトリ 役割 使用タイミング CLAUDE.md Claude Codeのエントリーポイント。 @AGENTS.md でAGENTS.mdを参照 Claude Code起動時に自動読み込み AGENTS.md AIエージェント向けメインガイド。最重要ルール(3-5個)と各ドキュメントへの参照を記載 Devinがセッション開始時に読み込む docs/ai-context/ コーディング規約、実装パターン、用語集、データモデルなどのコンテキスト情報 Playbook内で参照先として指定。週次更新でPRコメントから学んだ知見を蓄積 docs/guidelines/ アーキテクチャ設計、エラーハンドリング、バリデーションなどの詳細な技術ガイドライン Playbook内で参照先として指定。週次更新で継続的に改善 docs/review-perspectives/ perspective:* ラベルに対応したレビュー観点ドキュメント Claude ReviewがPRのPerspectiveに応じて動的に読み込み( 詳細 ) PRレビューで蓄積されたナレッジが自動的にドキュメントに反映されるため、Claudeのレビュー品質も継続的に向上していきます。 残っている課題 AIでも対応が難しいドメイン固有の判断があり、最終的に人間の合意が必要になるケースが残る ワークフローが複雑になり、運用ルールの理解・周知コストが発生している AIによる正しい指摘でも、チームの合意形成に時間を要する場合がある 運用上の注意点 スキップラベルの活用 特定のPRでAIレビューをスキップしたい場合は、次のラベルを使用します。 skip-ai-review : AIレビューを完全にスキップ renovate : Renovate PR(専用ワークフローで処理) 人間のレビューが必要なケース 次の変更は NEEDS_HUMAN と判定され、必ず人間のレビューが必要です。 セキュリティ関連の変更 ドキュメント・READMEの更新 設定ファイルの変更 スタイル・デザインの変更 ワークフロー別ラベル操作 ワークフロー 付与するラベル 削除するラベル ai-task-dispatcher.yml -(条件判定のみ、ラベル操作なし) - ai-task-implementation.yml in-progress in-progress (失敗時/キャンセル時) ai-review-orchestrator.yml ci-failed , ai-iteration-{N} , ai-auto-approved , needs-human-review ci-failed , ai-auto-approved , needs-human-review , perspective:* , ai-iteration-{N} pr-perspective-router.yml perspective:* perspective:* (再分類時) ai-renovate-review.yml perspective:dependency , renovate - ai-metrics-report.yml ai-metrics , automated (Issueに付与) - トラブルシューティング 問題 対処法 Devinセッションが開始しない DEVIN_API_KEY の設定を確認 Claudeレビューが実行されない AWS IAMロールの権限を確認 イテレーション上限に達した 手動で修正し、 ai-iteration-* ラベルを削除 CIタイムアウト CIの実行時間を確認、 CI_WAIT_MINUTES の調整を検討 まとめ この記事では、Claude(レビュー・判断)× Devin(実装・修正)の役割分担によるAI駆動開発ワークフローについて解説しました。 実現した効果 指標 Before After PRレビュー待ち時間 平均4時間 即時(Claudeが自動レビュー) レビュー指摘の修正 手動対応 Devinが自動修正 CIエラー修正 手動対応 Devinが自動修正(最大3回リトライ) Issue実装開始 平均1日 Slackから即時着手 Renovate PR 手動確認・マージ Claudeが依存関係レビューし、AI_ONLYかつ指摘なしなら自動承認 効果測定 なし 週次レポートでKPIを自動計測 ナレッジ育成 なし PRコメントからドキュメントを週次で自動更新 重要なポイント 役割分担:Claudeはレビュー(判断)、Devinは実装(作業)という明確な役割分担 人間との協調:AIはあくまでサポートであり、最終判断は人間が行う設計 段階的な自動化:すべてを自動化するのではなく、信頼性の高い部分から自動化 透明性:すべての判断理由がPRコメントとして記録される セーフティネット:イテレーション上限、キャンセル時のクリーンアップ、HEAD SHA検証 今後の展望 レビュー観点の自動学習 MCP連携強化 Confluenceドキュメント読み込み Figma MCPからデザイン取得 → 実装まで一気通貫 Jiraチケット起点の連携追加(Slackと同様のフローで起動) 人間レビューコメント時の対応方針の定義(優先度・担当・再レビュー) CodeRabbitとの併用検討 Slack通知の拡充(開始/完了/要人間対応などを分かりやすく通知) 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 参考リンク anthropics/claude-code-action - Claude CodeのGitHub Action Devin API Documentation - Devin APIリファレンス
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、検索基盤部の 朝原 です。ZOZOTOWNの検索改善を担当しています。 日々の分析業務では、ユーザーの行動ログを集計するSQLクエリを頻繁に作成します。クエリ作成には定型的なパターンも多く、作業時間を短縮する手段として生成AIの活用を検討しました。しかし、社内固有のログ構造や前提条件が多く、生成AIを利用しても期待どおりにクエリを作成できないという課題がありました。 本記事では、Claude CodeのSkills機能やサブエージェント機能を活用してこの課題を解決した方法を紹介します。 目次 はじめに 目次 なぜ生成AIでログ分析のSQLを書くのが難しいのか 1. 社内固有のログ構造を参照できない 2. チームのSQL規約に準拠した出力が担保できない 3. 暗黙的に共有されているナレッジを生成AIが持っていない 解決策の全体像 技術的アプローチ 1. Claude Code向けログ定義書の整備 方針 Skillsの具体的な内容 2. 3段階エージェントによるコンテキスト最適化 エージェント定義ファイル 3. カスタムスラッシュコマンドによるワークフロー自動化 実行例と結果 プラグイン化 課題と今後の展望 より多くのテーブルへの対応 テーブルインデックスの自動更新 まとめ なぜ生成AIでログ分析のSQLを書くのが難しいのか 私たちは、検索体験におけるユーザー行動ログを、日々SQLで集計して指標化し、施策検証や課題発見に活用しています。具体的には、検索結果画面における商品およびサジェスト(キーワード検索の補完機能)のインプレッション、クリック、ならびに購買に至る一連のイベントを集計対象としています。生成AIの登場により多くのコーディング業務が自動化されました。しかし、社内のログ分析においては以下の課題により活用が進んでいませんでした。 1. 社内固有のログ構造を参照できない 生成AIは一般的なSQLの生成が可能です。しかし、社内固有のログのテーブル構造については前提知識がありません。ログのテーブル名やスキーマとクエリが同じリポジトリで管理されていないため、コーディングエージェントがそれらを参照できません。 2. チームのSQL規約に準拠した出力が担保できない インデントの数や予約語の取り扱いなど、チーム内で定められたコーディング規約があります。チーム内でのSQLクエリの共有を考えると、生成AIが作成するクエリも規約に準拠している必要があります。一方で、生成AIの標準的な出力ではこれらを完全に再現できません。 3. 暗黙的に共有されているナレッジを生成AIが持っていない どのIDで集計すべきか、どのタイムスタンプを基準にするべきかといったドキュメント化されていない知識が存在します。こうした暗黙知をコーディングエージェントは持っていません。 こうした要因が重なり、「生成AIにSQLを書かせても、結局手直しが必要で効率化にならない」という状況が続いていました。 解決策の全体像 これらの課題を解決するため、Claude Codeを活用してクエリを自動生成するフローを構築しました。 以下がSQLクエリを生成するフローの全体像です。 以下のようなコマンドを入力するだけで、ログの構造や規約を踏まえた実用的なSQLクエリが自動生成されます。 /make_sql 商品カテゴリ「スニーカー」の表示回数、クリック数、クリック率をプラットフォーム毎に調査するクエリを作成してください 技術的アプローチ LangGraph、Agent Development Kit、Strands Agents等のエージェント開発フレームワークを使うことでよりカスタマイズ性の高いものを構築できます。しかし、今回は「低コストかつ短期間に、社内展開しやすい形で実現する」ためClaude Codeを採用しました。 現在ZOZOではエンジニア社員に月額200米ドルの基準のもと、コーディングエージェントを導入しております。ご興味のある方は以下の記事もご参照ください。 corp.zozo.com 今回紹介する手法で用いるClaude Codeの設定ディレクトリ構造は以下の通りです。 .claude/ ├── agents/ # エージェント定義 │ ├── log-data-advisor.md # 要件定義&ログ情報取得 │ ├── query-builder.md # クエリ作成 │ └── query-validator.md # 品質管理 └── skills/ ├── make_sql/ # ワークフロー定義(スラッシュコマンド) │ └── SKILL.md ├── table-index/ # テーブル一覧・関係性 │ └── SKILL.md ├── table-details/ # テーブル詳細仕様 │ ├── SKILL.md │ ├── product_impressions.md # サポートファイル │ ├── product_clicks.md # サポートファイル │ └── ... └── quality-check/ # 品質チェック └── SKILL.md 以降で各要素について詳しく説明します。 1. Claude Code向けログ定義書の整備 方針 まずはClaude Code向けに社内のログ情報をドキュメント化するところから始めました。 ログは非常に多くのテーブルが存在するため、全ての情報を1つのファイルにまとめるとLLMに渡すコンテキストが肥大化し、必要な情報が埋もれて出力精度が低下しやすくなります。そこで、Claude CodeのSkills機能 1 をドキュメントの定義先として採用し、役割ごとに分割して管理することにしました。Skillsとは SKILL.md ファイルに指示を記述することでClaudeの機能を拡張する仕組みです。 .claude/skills/<skill-name>/SKILL.md というディレクトリ構造で作成します。 Skillsを採用した理由は、エージェントが必要なときに必要な情報だけを参照できる点にあります。すべてのログ情報を一度に読み込むとコンテキストが肥大化しますが、Skillsとして分離しておけば、エージェントは必要なスキルだけを選択的に参照できます。 用途ごとに、以下の3つのSkillsに分離しました。 Skills 用途 table-index ログテーブルの特徴、関係性を把握 table-details ログテーブルの詳細仕様を把握 quality-check チーム内SQL規約に基づいた品質チェック table-index スキルで必要なテーブルを特定し、 table-details スキルで詳細仕様を参照、 quality-check スキルでチーム内のSQL規約に従っているかチェックを行う流れです。ログテーブルの情報を2回に分けることで、エージェントがまずテーブル一覧で必要なテーブルを特定し、次に詳細仕様で実装に必要な情報を取得する、というコンテキスト効率の良い参照フローが実現できます。 Skillsの具体的な内容 以下は各スキルの具体例です。実際のログ情報とは異なります。 .claude/ ├── ... └── skills/ ├── table-index/ # テーブル一覧・関係性 │ └── SKILL.md ├── table-details/ # テーブル詳細仕様 │ ├── SKILL.md │ ├── product_impressions.md # サポートファイル │ ├── product_clicks.md # サポートファイル │ └── ... └── quality-check/ # 品質チェック └── SKILL.md .claude/skills/table-index/SKILL.md このスキルでは、ログテーブルの一覧とテーブル間の関係性を提供します。エージェントはまずこのスキルを参照して、要件に合うテーブルを特定します。テーブル名やカラム名だけでなく、各テーブルの説明やテーブル間の関係性なども自然言語で記載しています。このアプローチは、Tiger Data社の研究 2 で報告されている「セマンティックカタログ」と同様の効果を狙ったものです。同研究ではデータベースのメタデータに加え、ビジネスロジックなどを自然言語で記述したセマンティックカタログを用意することで、LLMによるSQL生成の精度が最大27%向上したと報告されています。 --- name: table-index description: 分析に使用可能なログテーブルの一覧と関係性を取得 --- # ログテーブル一覧   `` `yaml  tables:   product_impressions:   description: "商品が表示された時のログ"   product_clicks:   description: "商品がクリックされた時のログ"  relationships:   - from: product_impressions   to: product_clicks   join_key: [impression_id, platform] description: "プロダクトの表示時にimpression_idが発行され、クリック時に同じimpression_idが記録される"   ``` .claude/skills/table-details/SKILL.md このスキルでは、ログのテーブル定義の詳細を提供します。エージェントは table-index スキルを元に選定したログの詳細情報を、このスキルを参照して取得します。スキルディレクトリには SKILL.md に加えて、マークダウンドキュメントやスクリプトなどのサポートファイルを配置できます。 SKILL.md からサポートファイルを参照することで、エージェントは必要なログテーブルの詳細情報のみを読み込みます。これによりコンテキストを効率的に管理できます。 --- name: table-details description: 指定されたログテーブルの詳細仕様を取得 --- # テーブル詳細仕様 詳細な仕様は以下のサポートファイルを参照してください: - [ product _ impressions.md ]( product_impressions.md ) - [ product _ clicks.md ]( product_clicks.md ) .claude/skills/table-details/product_impressions.md(サポートファイル) このファイルでは、ログテーブルの実際の詳細仕様を記載しています。テーブルの詳細仕様は社内ドキュメントツールで各チームが管理しています。各ログのドキュメントURLを記載することで、エージェントがMCPを通じて最新のテーブル情報を取得します。これにより、ドキュメント側のテーブル構造が更新されても、Claude Code側の設定を変更することなく、最新の情報を参照できます。また、推奨されるJOINの条件やタイムスタンプの扱いなど、ドキュメント化されていない知識もここに記載しています。 # 商品表示ログ(product _ impressions) MCPを使用して以下のドキュメントを取得してください。 - テーブル仕様: https://docs.example.com/logs/product _ impressions ## 暗黙知・注意事項 - JOINする際は必ず ` impression_id ` と ` platform ` の両方で結合 - 期間絞り込みは ` event_timestamp ` を ` Asia/Tokyo ` でDATE変換して行う .claude/skills/quality-check/SKILL.md このスキルでは、SQLのコーディング規約を提供します。エージェントがこのスキルを参照し、生成されたクエリが社内のコード規約を遵守しているかのチェックと自動修正をします。 --- name: quality-check description: SQLコーディング規約のチェックと自動修正 --- # SQLコーディング規約 ## フォーマットルール - インデントは半角スペース2つ - 予約語は小文字(select, from, where等) - カンマは行頭に配置 - 1行の文字数は100文字以内 ## 命名規則 - エイリアスは意味のある短い名前(imp, click等) - CTEは処理内容がわかる名前(impressions, clicks等) 2. 3段階エージェントによるコンテキスト最適化 ドキュメントを整備しただけでもクエリの自動生成は可能でした。しかし、大量のログドキュメントを一度に処理すると、コンテキストの肥大化という問題が発生するため、3段階のサブエージェント 3 に分割し、各エージェントが必要最小限の情報だけを扱う設計にしました。サブエージェントは、特定の種類のタスクを処理する特化した エージェントです。各サブエージェントは、カスタムシステムプロンプト、特定のツールアクセス、および独立したコンテキストウィンドウで実行されます。 ステップ エージェント 役割 入力(コンテキスト) 出力 1 要件定義&ログ情報取得 自然言語からの要件定義、必要なテーブルを特定 table-index スキル 必要なテーブル情報、実装方針 2 クエリ作成 SQLクエリを生成 table-details スキル、ステップ1の出力 SQLクエリ 3 品質管理 規約チェックと自動修正 quality-check スキル、ステップ2の出力 SQL規約準拠済みのSQLクエリ この分割により、各エージェントが扱う情報量を最小限に抑え、コンテキストの肥大化を防いでいます。 エージェント定義ファイル 各エージェントは .claude/agents/ ディレクトリにマークダウンファイルとして定義します。 .claude/ ├── ... ├── agents/ # エージェント定義 │ ├── log-data-advisor.md # 要件定義&ログ情報取得 │ ├── query-builder.md # クエリ作成 │ └── query-validator.md # 品質管理 └── ... 各エージェントが誤って自身に関係のないSkillsを読み込むと、コンテキストが肥大化してしまうおそれがあります。そこで、各エージェントの定義ファイルでは、そのエージェントが参照できるSkillsを明確に指定しています。 .claude/agents/log-data-advisor.md --- name: log-data-advisor description: ユーザーの分析要件から必要なログテーブルを特定する skills: - table-index --- あなたはログデータアドバイザーです。ユーザーの分析要件を理解し、必要なテーブルを特定します。 詳細化が必要な場合は、追加の質問を行ってください。 ## 責務 1. ユーザーの分析目的を理解 2. 必要なログテーブルを特定 3. データの関連性と結合条件を明確化 ## 出力形式 - 使用するテーブル名のリスト - テーブル間の結合条件 - 実装方針の概要 .claude/agents/query-builder.md --- name: query-builder description: テーブル情報に基づいてSQLクエリを生成する skills: - table-details --- あなたはSQLクエリビルダーです。テーブル詳細仕様に基づいてBigQuery向けの最適なクエリを生成します。 ## 責務 1. /table-detailsのスキルを参照してテーブル詳細を取得 2. 暗黙知・注意事項を踏まえたクエリ作成 3. パフォーマンスを考慮した最適化 .claude/agents/query-validator.md --- name: query-validator description: 生成されたSQLがコーディング規約に準拠しているかチェックし、自動修正する skills: - quality-check --- あなたはSQL品質管理者です。生成されたクエリのコーディング規約チェックと自動修正を行います。 ## 責務 1. インデント、予約語の大文字小文字などの規約チェック 2. 規約違反の自動修正 3. 修正内容のレポート 3. カスタムスラッシュコマンドによるワークフロー自動化 ここまででもSQL自動生成は可能ですが、3つのエージェントを個別に呼び出すのは手間がかかるため、カスタムスラッシュコマンドで一連のワークフローとして自動化しました。 Skillsはスラッシュコマンドとしても機能します。 .claude/skills/<skill-name>/SKILL.md として作成すると、 /skill-name で呼び出せます。フロントマターで disable-model-invocation: true を指定すると、Claudeが自動的にスキルを適用するのを防げます。これにより、ユーザーがコマンドで明示的に呼び出した場合のみ実行されるよう制御できます。 .claude/ ├── ... └── skills/ ├── make_sql/ # ワークフロー定義(スラッシュコマンド) │ └── SKILL.md ├── ... ... .claude/skills/make_sql/SKILL.md に以下のような設定ファイルを作成します。 --- name: make _ sql description: "ユーザーの要件に基づいて、ログデータを分析し、最適なSQLクエリを生成します" disable-model-invocation: true allowed-tools: Task(log-data-advisor), Task(query-builder), Task(query-validator) --- # SQLクエリ生成ワークフロー このコマンドは、ユーザーの分析要件に基づいて、内部ログデータから最適なSQLクエリを生成する3段階のワークフローです。 ## ワークフローの流れ ### ステップ1: ログデータ要件の分析 @agent-log-data-advisor を使用して: - ユーザーの分析目的を理解 - 必要なログテーブルを特定 - データの関連性と結合条件を明確化 ### ステップ2: SQLクエリの生成 @agent-query-builder を使用して: - ステップ1で特定した要件に基づくクエリ作成 - パフォーマンスを考慮した最適化 ### ステップ3: 品質管理と規約検証 @agent-query-validator を使用して: - 生成されたクエリのコーディング規約チェック - 自動修正による品質保証 実行例と結果 商品の表示やクリックに関する分析クエリを生成する例を紹介します。なお、以下は説明用のサンプルで、実際のテーブル名・スキーマ構造とは異なります。 入力コマンド /make_sql 商品カテゴリ「スニーカー」の表示回数、クリック数、クリック率をプラットフォーム毎に調査するクエリを作成してください 生成されたSQLクエリ(一部省略) -- 商品の表示・クリック分析 with impressions as ( select platform , impression_id , product_category , event_timestamp from `example_project.search_logs.product_impressions` where true and date (event_timestamp, ' Asia/Tokyo ' ) >= ' 2025-09-01 ' and date (event_timestamp, ' Asia/Tokyo ' ) <= ' 2025-09-30 ' and product_category = ' スニーカー ' ) , clicks as ( select platform , impression_id from `example_project.search_logs.product_clicks` where true and date (event_timestamp, ' Asia/Tokyo ' ) >= ' 2025-09-01 ' and date (event_timestamp, ' Asia/Tokyo ' ) <= ' 2025-09-30 ' ) -- メインクエリ select imp.platform , count ( distinct imp.impression_id) as total_impressions , count ( distinct click.impression_id) as total_clicks , round ( safe_divide( count ( distinct click.impression_id) * 100.0 , count ( distinct imp.impression_id) ), 2 ) as click_through_rate from impressions as imp left join clicks as click on imp.impression_id = click.impression_id and imp.platform = click.platform group by imp.platform order by total_impressions desc このように自然言語で要件を伝えるだけで、社内のログを踏まえ、チームのコード規約に従った分析用のクエリが自動生成されました。 プラグイン化 本記事で紹介した一連のフローは、Claude Codeのプラグイン機能 4 を活用して全社展開しています。 プラグインは、Claude Codeの設定やSkills、サブエージェントなどを配布できる機能です。社内にマーケットプレイスリポジトリ 5 を構築してプラグインを公開しているため、社員であれば以下のコマンドで誰でもインストールして利用できます。 マーケットプレイスリポジトリをClaude Codeに登録する。 /plugin marketplace add { リポジトリ名 } マーケットプレイスリポジトリからプラグインをインストールする。 /plugin install { 登録したマーケットプレイス上のプラグイン名 } プラグインを利用する。 / { 登録したマーケットプレイス上のプラグイン名 } :make_sql { 要件 } プラグインの詳細については 公式ドキュメント を参照してください。 課題と今後の展望 より多くのテーブルへの対応 現在テーブルインデックスに記載しているログは、弊チームでよく使うテーブルに絞っています。しかし、社内にはより多くのログが存在しているため、単純に SKILL.md に定義を追加するだけでは非常に多くのコンテキストを消費してしまいます。そこで、要件に合うテーブルを自動で検索する仕組みを検討しています。 テーブルインデックスの自動更新 今回はMCPでテーブルの仕様を社内ドキュメントから取得し、エージェントにテーブルインデックスを生成させていました。しかし、ログの仕様が変わる毎にテーブルインデックスを更新する必要があるため、運用コストが高い状態です。そこで、長期的には自動更新する仕組みを検討する必要があります。 まとめ 本記事では、Claude CodeのSkills機能を活用したSQLを自動生成するフローについて紹介しました。近年AIエージェントの文脈では、コンテキストエンジニアリングが重要であると言われています。コーディングエージェントにおいても、今回のように社内固有の情報やエンジニアが持っている暗黙知を提供することで、より実用的なアウトプットが得られると実感しました。「生成AIは社内のログの知識を持っていない」という課題に対し、ドキュメント整備とエージェント設計で対処するアプローチは、SQL生成に限らず他の業務自動化にも応用できると考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co Claude Code Skills https://code.claude.com/docs/ja/skills ↩ TigerData. "The Database's New User: LLMs Need a Different Database" https://www.tigerdata.com/blog/the-database-new-user-llms-need-a-different-database ↩ Claude Code Sub-Agents https://code.claude.com/docs/ja/sub-agents ↩ Claude Code Plugins https://code.claude.com/docs/ja/plugins ↩ Claude Code Marketplace https://code.claude.com/docs/ja/plugin-marketplaces ↩
アバター
はじめに こんにちは、Developer Engagementブロックの @wiroha です。2月10日に「 ZOZO.swift #2 」をオンラインで開催しました。ZOZOのiOSエンジニアによるiOS特化のイベントです。昨年12月に第1回を開催しており、今回第2回目を開催できました。 イベントはオンライン開催でしたが、可能なメンバーはオフィスに集まって配信しました。その当日の雰囲気も含めてレポートします! 登壇内容まとめ ZOZOのエンジニア5名と、技術顧問の岸川さんが発表しました。 発表タイトル 登壇者 旅先で iPad + Neovim で iOS 開発・執筆した話 lap/らぷ ( @laprasDrum ) デザインもAIに任せる!iPhoneで行うiOS開発 イッセー / 上田 壱成 ( @15531b ) ZOZOTOWN、SceneDelegateへのお引越し つっきー / 續橋 涼 ( @tsuzuki817 ) LiDARが変えたARの"距離感" かっつん / 渡邊 魁優 Claude Code で画面の仕様書を作ろう だーはま / 濵田 悠樹 ( @ios_hamada ) 浮動小数の比較について(XCTestとswift-numerics、微妙な実装の違い) 岸川克己 ( @k_katsumi ) 当日の発表はYouTubeのアーカイブ動画をご覧ください。 www.youtube.com 旅先で iPad + Neovim で iOS 開発・執筆した話 speakerdeck.com らぷからは、旅行中にiPadとNeovimを使ってiOS開発を行った話について発表しました。iPadでもさまざまなツールを駆使して開発できており、楽しんでいる様子が伝わってきました。 デザインもAIに任せる!iPhoneで行うiOS開発 speakerdeck.com 上田からは、AIを活用し、デザインまで含めてiPhoneでiOS開発をした話について発表しました。PCなしで開発するという趣旨の発表が偶然2件続き、需要の高さを感じます。 ZOZOTOWN、SceneDelegateへのお引越し speakerdeck.com 續橋の発表では、AppDelegateからSceneDelegateへの移行についてお話ししました。影響範囲が広い中、イベントやリスクなど丁寧に分類してQAを進めていたそうです。 LiDARが変えたARの"距離感" speakerdeck.com 渡邊からは、LiDARセンサーを活用した計測について発表しました。鏡を写した場合は信頼度がLowとなるなど、さまざまなケースが想定されているのは興味深かったです。 Claude Code で画面の仕様書を作ろう speakerdeck.com 濵田からは、Claude Codeを使って画面の仕様書を自動生成する取り組みについて発表しました。実際のプロンプト例も紹介され、実用的な内容でした。 浮動小数の比較について(XCTestとswift-numerics、微妙な実装の違い) speakerdeck.com 岸川さんからは、Swiftにおける浮動小数点数の比較について発表いただきました。許容誤差に応じて適切なツールや実装を選択する必要があると分かりました。 最後に 今回のイベントでは、iOS開発を行う環境の多様化やAI活用など、さまざまなトピックについて共有しました。ご参加くださったみなさま、ありがとうございました。今後もZOZO.swiftを開催していきますので、ぜひご参加ください! ZOZOでは一緒に働くエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは。Developer Engagementブロック(略称DevEngブロック)の @wiroha です。ZOZO TECH BLOGの運営や、開発者向けイベントの企画・運営などを担当しています。 TECH BLOGの運営において、レビューには一定の工数がかかるため、効率化を進めています。その一環として、Claude CodeのAgent Skills(以下、スキル)を用いたレビュー支援の仕組みを整備しました。Claude Code上で記事のレビューを依頼すると、定義したルールに基づくレビュー結果を得られます。 以下は、スキルによるレビュー結果の抜粋です。 本記事では、このスキルを用いたTECH BLOGレビューの取り組みについて紹介します。 目次 はじめに 目次 背景・課題 解決の方針 スキルの設計 SKILL.md rules.md スキルの使用方法 実行例 導入効果 運用上の注意点 今後の展望 まとめ 付録:rules.md全文 背景・課題 ZOZO TECH BLOGは執筆したチーム内で技術的な正確性をレビューした後、DevEngブロックで文章表現・社内ルール準拠などをレビューして公開しています。DevEngブロックでは、現在2名体制で年間約100本の記事をレビューしており、次の課題があります。 担当者が少なく属人化しやすい 時間がかかってしまう 指摘が多いと漏れが発生しやすく、修正と再レビューの往復で進行が詰まりやすい 記事はGitHub上で管理しており、文章を校正する textlint のGitHub Actionsを動かしているため、ある程度のチェックは自動化されています。 1 ただ、それだけでは網羅できない観点が多くあるため、AIを活用してレビューの自動化をさらに推進することにしました。 解決の方針 AIを活用したレビューには複数の選択肢がある中で、Claude Codeのスキルを用いることにしました。Claude Codeはブラウザやエディタを介さず、ターミナル(CLI)で動作し、手元のファイルや外部サービスの情報を扱いながら作業を支援できるAIツールです。スキルは特定のタスクを実行するためのカスタムモジュールで、ドメイン知識やルールに基づいた処理を一貫した手順でAIに実行させられます。スキルの詳細はClaude Codeの公式ドキュメントをご覧ください。 code.claude.com 今回の場合、 claude コマンドでClaude Codeを起動し、「記事をレビューしてください」のように指示するだけで、スキルが呼び出されてレビューを実行できます。 AIレビューの手段として、ChatGPTのGPTsやGeminiのGemなど対話型AIのカスタマイズ、GitHub Actionsによる自動チェックも検討しました。最終的にスキルを採用した理由は次のとおりです。 ブラウザで使用するAIツールと異なり、記事本文をコピーしてAIにペーストする手間が省ける 同じ仕組みをDevEngブロック以外も使用でき、執筆者によるセルフレビューとしても使える 社内にはClaude Codeの利用者が多く、操作に慣れている レビュールールをGitHub上で管理し、誰でもPRを出して改善提案する運用にできる Claude Code GitHub Actions や Devin でPR上に自動コメントするよりは、任意利用から始めて少しずつAIの精度を高めたい 何度も実行しやすく、レビュールールを継続的に改善しやすい仕組みが重要だと考えました。 スキルの設計 Claude Codeの公式ドキュメント「 Claude をスキルで拡張する 」を参考に、TECH BLOGレビュー用のスキルを設計しました。 執筆時点でのスキルの内容を掲載します。スキル定義ファイルであるSKILL.mdと、TECH BLOGレビューのルールをまとめたrules.mdの2つのファイルで構成されています。 SKILL.md --- name: techblog-review description: "ZOZO TECH BLOGの記事をレビューする。「記事をレビュー」「テックブログをチェック」「entry.mdを確認」などのリクエストで起動" allowed-tools: Read, WebFetch --- ## テックブログ記事レビュー `entry.md` をZOZO TECH BLOGのルールに基づいてレビューします。 ### 手順 1. `entry.md` を読む 2. `rules.md` のルールに基づいてレビューする 3. 記事内のリンクをWebFetchで確認し、リンク切れがないかチェックする 4. 問題がある場合のみ、以下のルールで出力する(「問題なし」「確認のみ」といった項目は記載しない) ### 出力ルール **L{行番号}** - {観点}:{修正内容の要約} ```diff - {修正前の文} + {修正後の文} ``` ### 出力例 **L73** - 文法:「〜なこと」→「〜こと」 ```diff - ドキュメントが古いなことが原因で + ドキュメントが古いことが原因で ``` **L89** - 冗長表現:「〜というのは」→「〜のは」 ```diff - 自動化できるというのは大きな利点です。 + 自動化できるのは大きな利点です。 ``` descriptionには、スキルを起動する際のキーワードを記載しています。allowed-toolsには、スキルが使用できるツールを指定しています。Readは、rules.mdや記事原稿であるentry.mdを読むために必要です。記事内のリンク切れをチェックするために、WebFetchも許可しています。 rules.md 肝となるレビュールールは、DevEngブロックがこれまでに行ってきたレビューやSuggestコメントをもとにしています。Claude Codeに過去3年間のレビューコメントをGitHub CLI 2 で収集させ、ルール案を作成させました。PR上のレビュー履歴を資産として活用し、案は人手で精査してブラッシュアップしました。 以下では、rules.mdの一部を紹介します。全文は長くなるため、付録として記事の末尾に掲載します。 # ZOZO TECH BLOG レビュールール ## 1. 文体・表現 ### 1.1 文体の統一 - **である調とですます調を混在させない** - 基本的にですます調を使用する ### 1.2 文は短くする - **一文が長すぎると読みにくい** - 複数の情報を含む文は分割する - 目安:一文100文字以内 ### 1.3 口語表現の回避 - 口語表現は文語表現に置き換える | 口語 | 文語 | |------|------| | 〜なんですが | 〜ですが | | 無かったです | ありませんでした | | ですので | そのため / したがって | | ないですが | ありませんが | | 食わせる | 与える / 読み込ませる | ### 1.4 曖昧表現の回避 - 「〜と思います」より「〜です」を使う - 「〜ような」で曖昧にせず断定する - 断定できる場合は断定する ``` ❌ 複数の機能を一度にデプロイするようなリリースサイクルを採用していました。 ✅ 複数の機能を一度にデプロイするリリースサイクルを採用していました。 ``` これらの観点の一部は以前Zennの記事「 技術をわかりやすく伝えるテクニカルライティングのtips 」などにまとめて共有していましたが、そこに含まれない多くの観点を整理できました。なお、rules.mdに含まれる例文は、生成されたルール案をもとに手動で改変し、特定の記事が推測されないよう配慮しています。 スキルの使用方法 スキルはブログ執筆用リポジトリのルートに配置しているため、プロジェクトスキルとして自動的に有効になります。 Claude Code上で「今書いている記事をレビューしてください」などと指示すれば、スキルが起動してレビューを実行します。ローカルにある自分の記事をセルフレビューする場合の例は次のとおりです。 Use skill "techblog-review"? と聞かれるので「Yes」と答えると結果が返ってきます。 DevEngブロックのメンバーは、自分が書いていない記事もレビューします。ローカルにない記事もClaude Code上で「 <URL> の記事をレビューしてください」のように指示すれば、GitHub CLIを使って自動的に記事を取得し、レビューしてくれます。 手動でチェックアウトしてきたり、コピー&ペーストしたりする必要がなく、非常に便利です。 実行例 本記事を執筆しながら、実際にスキルを使ってレビューした際の結果を例として紹介します。 1. SKILL.mdでの指定どおり、リンク切れをチェックして有効である旨を表示しています。 2. ルールに書いてある、冗長表現を見つけて指摘しています。 3. ルールに書いていない観点でもチェックしてくれます。行頭に不要なスペースが入っている例です。 4. リンクを埋め込み形式で表示するための embed:cite が誤って入っているのも見つけています。 5. ルールにある「正式名称・表記」の観点で、サービス名の誤りを指摘しています。 6. 広く知られているサービス名だけではなく、ファイル名の誤りも発見できるのは驚きでした。 7. TODOと書いていなくても、消し忘れか対応漏れかもしれないコメントを指摘しており、細やかです。 8. ルールにある「文の流れ」の観点はやや厳密にも感じますが、たしかに読点を入れた方が読みやすいため修正しました。 9. 次の例ではURLの誤りを指摘しており、それ自体は適切であるものの、修正案が英語版ドキュメントのURLになってしまっていました。手動で日本語の方に修正しましたが、こういった誤りもあるため、自動適用はせず人が確認する運用にしています。 以上のように、表記・記法・リンクといった観点を中心に、幅広く指摘できました。 導入効果 実行例で示したとおり、さまざまな観点でチェックできており、修正案を考える時間を短縮できるようになりました。rules.mdで定義した観点に基づき、過去30件のPRのレビューコメントをAIで分類したところ、約75%をカバーできると推定されました。残り25%は「文章の圧縮・再構成」「表現の適切さ」「説明の追加・明確化」といった文脈依存の判断が必要なもので、ルールベースでの検出ができない点でした。 これまで暗黙知としてDevEngブロックメンバーの中で蓄積されていたチェック観点を、rules.mdとして明文化し、AIが担えるようになりました。その結果、レビュー観点が個人の経験やスキルに依存しにくくなり、再現性のあるチェックが行えるようになっています。 また、特定分野の技術を知っていないと見つけづらいタイポなど、自分では見落としていた点も検出でき、記事の品質向上に寄与しています。細部のチェックをAIに任せることで、全体の構成やわかりやすさといった観点に意識を向けやすくなり、読みやすい記事づくりにつながっています。 執筆者にはまだ展開したばかりで、セルフレビューで用いるのは必須とはしていません。ルールの精度を向上させ、執筆段階でセルフレビューとして使うケースが増えていけば、修正と再レビューの往復が減り、執筆者・レビュアー双方の負担軽減につながると考えています。 運用上の注意点 実行例の部分にも記述したとおり、現在の運用ではAIの指摘をそのまま採用せず、提案として扱っています。指摘に誤りが含まれる場合や、厳密すぎる場合があるためです。たとえばルールでは口語表現を回避するよう定めていますが、イベントレポート系の記事では口語の表現を残した方が感情を伝えやすい場合もあります。そのあたりのバランスは人が判断しています。 また、社外秘や推測可能な情報が混入していないかなども人が確認しています。 今後の展望 今後は、スキルの精度向上と機能拡張に継続して取り組みます。記事をレビューした結果をもとにフィードバックループを回し、改善を図ります。 執筆者からの意見次第では、Claude Code以外のAIツールへの対応も検討しています。rules.mdを整備したことで、同じ観点を他のツールにも転用しやすくなりました。Claude Codeを使用していない人でも利用できる導線を用意してもよいかもしれません。 また、本記事に反響があれば、スキルをオープンソース化し最新のルールを社外の方も利用できるようにすることも検討したいと考えています。各社で技術ブログの運用方法は異なるため、本取り組みを通じて、より良いレビューや運用方法について意見交換ができればと思います。 まとめ 本記事では、Claude CodeのAgent SkillsとしてTECH BLOGのレビュー観点を集約し、効率化した取り組みを紹介しました。AIの発展は目覚ましく、優れたツールが次々と登場しています。DevEngブロックは新しい技術を積極的に取り入れ、より良い執筆体験を提供できるよう努めていきます。今回の記事が生成AIをレビュー支援に取り入れる際の設計・運用のヒントになれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。技術記事の執筆が好きな方も大歓迎です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 付録:rules.md全文 # ZOZO TECH BLOG レビュールール ## 1. 文体・表現 ### 1.1 文体の統一 - **である調とですます調を混在させない** - 基本的にですます調を使用する ### 1.2 文は短くする - **一文が長すぎると読みにくい** - 複数の情報を含む文は分割する - 目安:一文100文字以内 ### 1.3 口語表現の回避 - 口語表現は文語表現に置き換える | 口語 | 文語 | |------|------| | 〜なんですが | 〜ですが | | 無かったです | ありませんでした | | ですので | そのため / したがって | | ないですが | ありませんが | | 食わせる | 与える / 読み込ませる | ### 1.4 曖昧表現の回避 - 「〜と思います」より「〜です」を使う - 「〜ような」で曖昧にせず断定する - 断定できる場合は断定する ``` ❌ 複数の機能を一度にデプロイするようなリリースサイクルを採用していました。 ✅ 複数の機能を一度にデプロイするリリースサイクルを採用していました。 ``` ### 1.5 体言止めの回避 - **体言止めは曖昧になりやすいため、技術記事では避ける** - 文として言い切る形にする - **ただし、箇条書きの場合は体言止めでも許容** ``` ❌ 先月、ついに新機能をリリース。 ✅ 先月、ついに新機能をリリースしました。 ``` ### 1.6 「〜たり」の用法 - 並列の「〜たり」は**2回以上繰り返して使う** ``` ❌ コードを書いたりレビューができます ✅ コードを書いたりレビューをしたりできます ``` ### 1.7 「〜になります」「〜となります」の回避 - 変化しない場合は「です」を使う ``` ❌ 本ツールはログ収集の定番ライブラリになります。 ✅ 本ツールはログ収集の定番ライブラリです。 ``` ### 1.8 丁寧すぎる表現の回避 - 物に対する過剰な丁寧表現は避ける ``` ❌ 環境変数を設定ファイルに追記してあげる必要があります。 ✅ 環境変数を設定ファイルに追記する必要があります。 ``` ### 1.9 敬称 - 社外の方(技術顧問、登壇者等)には「さん付け」を使う - 自社社員には敬称は不要とする ### 1.10 簡潔にできる表現(任意) 簡潔にしたい場合は、以下のように表現を変える - 「なぜ〜したか」→「〜した理由」 - 「どのように〜するか」→「〜する方法」 - 「どれくらい閲覧されたか」→「閲覧数」 --- ## 2. 受動態と能動態 ### 2.1 能動態を優先する - **受動態が多いと読みづらくなる** - 主語を明確にして能動態で書く ``` ❌ 複数の設定ファイルはバッチ処理から順次実行されることで、対象システムの状態を更新していきます。 ✅ バッチ処理が複数の設定ファイルを順次実行し、対象システムの状態を更新します。 ``` ### 2.2 受動態と能動態の混在を避ける ``` ❌ そのAPIに対してタイムアウトを設定されており ✅ そのAPIにはタイムアウトが設定されており ``` --- ## 3. 主語と述語 ### 3.1 主語を省略しない - 主語がないと何の話かわからない ``` ❌ この処理の流れの特徴として、決済完了のタイミングで管理画面からメールをユーザーへ同期的に送信します。 ✅ この処理の流れの特徴として、システムは決済完了のタイミングで管理画面の機能を介してメールをユーザーへ同期的に送信します。 ``` ### 3.2 主語と述語のねじれを避ける - 主語と述語が対応しているか確認する ``` ❌ 特に気になった変更点は、新しいログ出力機能が追加されました。 ✅ 特に気になった変更点は、新しいログ出力機能が追加されたことです。 ❌ 今回の会場は、東京国際フォーラムで開催されました。 ✅ 今回の会場は、東京国際フォーラムです。 ✅ 今回のイベントは、東京国際フォーラムで開催されました。 ❌ 結論はコストと移行の容易さからマネージドサービスを選定しました。 ✅ 結論として、コストと移行の容易さからマネージドサービスを選定しました。 ``` ### 3.3 「〜とは」の後に意味を書く - 「〇〇とは」と書いたら、その後に定義・意味を書く ``` ❌ マイクロサービスアーキテクチャとは、システムを独立した小さなサービス単位に分割し、開発スピードと拡張性を大幅に向上させることができます。 ✅ マイクロサービスアーキテクチャとは、システムを独立した小さなサービス単位に分割し、開発スピードと拡張性を大幅に向上させる設計手法のことです。 ``` --- ## 4. 助詞の使い方 ### 4.1 助詞の誤用 | 助詞 | 悪い例 | 良い例 | |------|--------|--------| | を | サーバーを起動を開始 | サーバーの起動を開始 | | を | パラメータを設定をしました | パラメータを設定しました | | が | テスト実行にかかる時間が、全体時間を占める割合が増えた | テスト実行にかかる時間の、全体時間に占める割合が増えた | ### 4.2 助詞を省略しない ``` ❌ 空レスポンス ✅ 空のレスポンス ❌ パフォーマンス影響が出た ✅ パフォーマンスに影響が出た ❌ この項目は必須でありません ✅ この項目は必須ではありません ``` ### 4.3 助詞と動詞の対応 ``` ❌ 設定値を環境間に複製 ✅ 設定値を環境間で複製 ❌ 2018年より会社を設立しました ✅ 2018年に会社を設立しました (「より」は継続の開始を示すため、一時点の出来事には「に」を使う) ❌ 会場内にノベルティが配布されました ✅ 会場内でノベルティが配布されました ❌ このツールには標準で自動リトライ機能を提供しています ✅ このツールは標準で自動リトライ機能を提供しています ``` ### 4.4 冗長な表現を避ける | 冗長 | 簡潔 | |------|------| | 〜について | 〜を | | 〜に対して | 〜に | | 〜に関して | 〜の / 〜を | | 記載をします | 記載します | | 〜というのは | 〜のは | | 〜ということで | 〜ため / 〜ので | ``` ❌ 運用コストを削減できるというのは大きなメリットでした。 ✅ 運用コストを削減できるのは大きなメリットでした。 ``` ### 4.5 逆接でない「〜が」を避ける - 逆接でない「〜が」を接続に使うことを避ける - **主格の「が」は問題ない** - 単なる文の接続に「〜が」を使うのは避ける ``` ❌ 本機能は外部APIを利用してデータを取得しますが、取得したデータはデータベースへ保存され、ユーザー画面に表示されます。 ✅ 本機能は外部APIを利用してデータを取得します。取得したデータはデータベースへ保存され、ユーザー画面に表示されます。 ``` --- ## 5. 漢字とひらがなの使い分け ### 5.1 ひらがなにする語 - 公用文作成の要領や記者ハンドブックでは、実質的な意味を持たない言葉をひらがなで書くよう定めており、概ねそれに則る。 1. 常用外漢字 2. 形式名詞 3. 接続詞 4. 補助動詞 5. 一部の動詞 6. 副助詞 7. 副詞 など 具体例 | 漢字 | ひらがな | 分類 | 備考 | |------|----------|------|------| | 殆ど | ほとんど | 常用外漢字・副詞 | | | 為 | ため | 形式名詞 | 「〜のため」 | | 事 | こと | 形式名詞 | 「〜すること」 | | 所 | ところ | 形式名詞 | 「改善したいところ」 | | 尚 | なお | 接続詞 | | | 但し | ただし | 接続詞 | | | 又 | また | 接続詞 | | | 下さい | ください | 補助動詞 | 「ご確認ください」 | | 頂く | いただく | 補助動詞 | 「教えていただく」 | | 居る | いる | 補助動詞 | 「動いている」※補助動詞の場合 | | 無い | ない | 補助動詞 | 「問題ない」※補助形容詞の場合 | | 有る | ある | 補助動詞 | 「設定してある」※補助動詞の場合 | | 出来る | できる | 補助動詞・動詞 | | | 迄 | まで | 副助詞 | | | 位 | くらい/ほど | 副助詞 | | | 是非 | ぜひ | 副詞 | | | 丁度 | ちょうど | 副詞 | | | 更に | さらに | 副詞 | | | 予め | あらかじめ | 副詞 | | | 様々 | さまざま | 慣習 | 公用文では漢字表記 | ### 5.2 送り仮名の確認 ``` ❌ 楽しく話ながら作業した ✅ 楽しく話しながら作業した ``` --- ## 6. 語順・修飾 ### 6.1 修飾語は被修飾語の近くに置く ``` ❌ 細かいアーキテクチャの説明は省略します ✅ アーキテクチャの細かい説明は省略します ``` ### 6.2 語順を整理する ``` ❌ 過去1年間のコミット数を開発状況を可視化するダッシュボードで確認してみたところ ✅ 開発状況を可視化するダッシュボードで過去1年間のコミット数を確認してみたところ ``` ### 6.3 同じ単語・名詞の繰り返しを避ける - ただし、並列構造の場合は同じ単語が続いても問題ない ``` ❌ フローが複雑化してリードタイム低下とならないよう、フローの再設計を行いました。 ✅ 複雑化によるリードタイム低下を避けるため、フローの再設計を行いました。 # 並列構造の例(問題なし) ✅ 2024年には初期段階としてフェーズ1をリリースし、翌年の2025年には計画どおりフェーズ2をリリースしました。 ``` --- ## 7. 見出し・構成 ### 7.1 タイトル - 「〜の話」で終わるタイトルは避ける - 主題と副題をダッシュ(──)で繋ぐ形式を推奨 ### 7.2 見出しの統一 - 見出しの文体・形式を統一する(体言止め or 文) - 見出しがバラバラだと読みにくい ### 7.3 前提を書く - 読者が知らない前提(社内ルールなど)は明記する ### 7.4 結論から書く - 前置きが長いと読者が離脱する - 結論→理由→詳細の順で書く ### 7.5 「後述します」の多用を避ける - 「後述」を多用すると読者が混乱する - 可能な限りその場で説明する ### 7.6 文の流れを意識する - 唐突に話が変わらないようにする - 前後の文脈をつなげる ``` ❌ 現在、チームのチャットツールは有料プランで運用しています。必要な情報を後から見返せる仕組みは、業務上欠かせないと考えています。しかし無料プランへ移行すると、過去ログの検索期間に制限がかかります。 ✅ 現在、チームのチャットツールは有料プランで運用しています。無料プランへ移行すると、過去ログの検索に制限がかかります。業務上、情報を後から見返せる仕組みは不可欠と考えています。 ``` ### 7.7 複雑なことは図示する - 文章だけで説明が難しい場合は図を追加する --- ## 8. 記号・フォーマット ### 8.1 全角スラッシュは使わない - 「/」(全角スラッシュ)は使わない - 「/」(半角スラッシュ)または「・」を使う ### 8.2 英語の頭文字をとった略語は大文字 - API(Application Programming Interface)など、英語の頭文字をとった略語は大文字で表記する - class名やファイル名などで小文字表記の場合は例外とする ### 8.3 引用記法 - 引用記法(`>`)は**実際の引用のときのみ**使用する ### 8.4 CustomPathはハイフンつなぎ - アンダースコアは使わない ``` ❌ CustomPath: my_article_path ✅ CustomPath: my-article-path ``` ### 8.5 ダッシュの表記 - タイトルの主題と副題を繋ぐ際は**─を2つ + 前後に半角スペース**を使用 ``` ❌ システム移行の課題にどう立ち向かうか - 私たちが実践した解決策 ✅ システム移行の課題にどう立ち向かうか ── 私たちが実践した解決策 ``` --- ## 9. 正式名称・表記 ### 9.1 サービス・製品名は正式名称を使用 | 誤 | 正 | 参考URL | |----|----|----| | Golang | Go | https://go.dev/ | | Go言語 | Go | https://go.dev/ | | Spring boot | Spring Boot | https://spring.io/projects/spring-boot | | Alloy DB | AlloyDB | https://cloud.google.com/alloydb | | Firebase app check | Firebase App Check | https://firebase.google.com/docs/app-check | | Secrets manager | Secrets Manager | https://aws.amazon.com/secrets-manager/ | | slack bot | Slackbot | https://slack.com/help/articles/202026038 | | Kintone | kintone | https://kintone.cybozu.co.jp/ (小文字始まり) | | docker | Docker | https://www.docker.com/ (大文字始まり) | | JS Nation | JSNation | https://jsnation.com/ (スペースなし) | | iosDC | iOSDC Japan | https://iosdc.jp/ | | ArgoCD | Argo CD | https://argo-cd.readthedocs.io/ (スペースあり) | ### 9.2 社名・サービス名の正式表記 - 社名やサービス名は公式サイトを確認し、正式名称を使用する - スペースの有無、大文字小文字を正確に記載する - ルールに記載されていない名称も都度検索して確認すること **ZOZOに関する表記** - 「ZOZO」はZOZOTOWNの略称ではない - 「ZOZO Yahoo!店」→「ZOZOTOWN Yahoo!店」 --- ## 10. はてなブログ固有のルール ### 10.1 太字記法 - はてなブログでは `**太字**` の前後に半角スペースは入れない ### 10.2 埋め込み記法 - URLは埋め込み形式にすると見栄えが良い - `[https://example.com/:embed:cite]` ### 10.3 キャプション記法 - 画像にはfigureタグでキャプションを付けられる ```html <figure class="figure-image figure-image-fotolife" title="タイトル"> [f:id:vasilyjp:20230101120000] <figcaption>キャプション</figcaption> </figure> ``` ### 10.4 Markdownの改行 - パラグラフをわけるときは空行を入れる - 同じパラグラフ内で改行する必要がある場合は `<br>` か ` `(半角スペース2つ)による強制改行を使う。ただし、デバイス依存の表示崩れやSEO・アクセシビリティの低下などを招くため、必要な場合のみに留める。 ``` 1行目 2行目(別のパラグラフになる) ``` ### 10.5 箇条書きの前後には空行が必要 - 箇条書き(`-` や `1.`)の前後には空行を入れる - 空行がないとレイアウトが崩れる場合がある ``` ❌ 下記の項目を記載しています。 - 依頼の概要 - 依頼部門の課題 ✅ 下記の項目を記載しています。 - 依頼の概要 - 依頼部門の課題 ``` ### 10.6 続きを読む記法 - `<!-- more -->` ははてなブログで「続きを読む」の区切りとして使用される - このコメントは削除しない --- ## 11. リンク・参照 ### 11.1 リンク切れの確認 - リンクが有効か確認する(404になっていないか) ### 11.2 リンク先の確認 - リンク先が正しいか確認する - リンク先が不適切・不確かな情報でないか確認する ### 11.3 リンクテキストの書き方 - 「こちら」だけでなく、リンク先が分かるようにテキストを明示する ``` ❌ 詳しくは[こちら](https://example.com)をご覧ください。 ✅ 詳しくは[公式ドキュメント](https://example.com)をご覧ください。 ✅ 詳しくは「[独自のアプリケーションの作成](https://example.com)」を参考にしてください。 ``` ### 11.4 採用リンク - 記事末尾の採用リンクは有効か確認する --- ## 12. その他 ### 12.1 ファイル末尾 - ファイル末尾の改行は必須ではない ### 12.2 機密情報 - 機密情報が含まれていないか確認する - 例示で使う数字はダミーにし、その旨を明記する ### 12.3 将来の機能・予定 - 未確定の将来計画は公開可否を確認する ### 12.4 引用・著作権 - 他サイトの画像・スクリーンショットを使用する場合は引用の要件を満たす - 引用元を明記する - 参考: [文化庁「著作権を学ぶ(教材・講習会)」](https://www.bunka.go.jp/seisaku/chosakuken/seidokaisetsu/index.html) ### 12.5 画像の掲載許諾 - 人物写真は掲載許諾を確認する - 他社ロゴ・アイコンは使用許可を確認する ### 12.6 追記の書き方 - 記事公開後に追記する場合は日付を明記する ``` ❌ 追記:〜 ✅ (2023/5/16追記)〜(追記ここまで) ``` 既存の仕組みの詳細は TECH BLOG執筆支援のためのCI/CD導入事例 をご覧ください。 ↩ GitHub公式のコマンドラインツール https://docs.github.com/ja/github-cli/github-cli/about-github-cli ↩
アバター
はじめに こんにちは、ZOZOTOWN開発2部Androidブロックの大江です。普段はZOZOTOWN Androidの開発を担当しています。 UIのリグレッション防止を目的として、2024年からVisual Regression Testing(VRT)の導入に取り組んできました。その第1弾として共通UIコンポーネントのモジュールに対してJetpack ComposeのPreviewを使ったVRTを運用しています。 本記事では、VRT導入について、VRTをCI/CDツール上で実行するための工夫を中心に紹介します。 目次 はじめに 目次 ZOZOTOWN Androidの課題 VRTとは VRTの冗長な処理を防ぐための工夫 画像生成 画像比較 結果出力 まとめ ZOZOTOWN Androidの課題 ZOZOTOWN Androidは画面の数が多く、長年の開発によってモジュールの依存関係やUIコンポーネントの共通化の仕組みが複雑になっています。そのため、レイアウト変更を伴う開発では、変更した箇所以外の画面にも影響を及ぼす可能性があります。 こうした影響を漏れなく把握することは難しく、レビュー時の目検だけでは、意図しないレイアウトの変化を見落としてしまうケースが多くありました。これらの課題を解決するため、私たちはVisual Regression Testing(VRT)の導入を決定しました。 VRTとは Visual Regression Testing(VRT、ビジュアルリグレッションテスト)は、レイアウトの差分を自動的に検出するテスト手法です。アプリケーションの実装を変更する前後でスクリーンショットの画像を生成して、両者を比較することで意図しないレイアウトの変更を検出します。VRTを導入することで、目検では見落としやすいレイアウトの変更を自動で検出できるようになります。 VRTは強力な仕組みですが、画像を大量に生成するため、マシンのリソースを消費しやすいなどの課題があります。そのためGitHub Actionsなどで実行する際には冗長な処理を防ぐことが重要です。 VRTの冗長な処理を防ぐための工夫 VRTの冗長な処理を防ぐための工夫を説明します。実際に運用しているGitHub ActionsでPull Requestに対してVRTを適用するフローを紹介します。 例として以下の状況のPull Requestに対してVRTを適用した場合を考えてみます。 develop branchの最新commitのhashがf9c2dcc develop branchからfeature branchを作成 feature branchに対してhashがce56c9dとbfe0a66のcommitを追加 feature branchをdevelop branchにmergeするPull Requestを作成 わかりやすくするために、VRTを以下の3つのステップに分けて説明します。 画像生成 画像比較 結果出力 画像生成 まず実装を変更する前後のアプリケーションに対して画像を生成します。ZOZOTOWN Androidでは、Roborazziというライブラリを使用してComposableのPreviewの画像を生成しています。 実装を変更する前後としてPull Requestのbase branchとhead branchの画像をそれぞれ生成します。画像の生成はマシンのリソースを消費しやすいため、冗長な処理を防ぐことが重要です。そこで生成した画像を可能な限り使い回すように工夫しました。 head branchはcommit&pushされて実装が変更される度に画像を生成する必要があります。一方、base branchはPull Requestがmergeされるまで変更されず、複数のPull Requestで同じbase branchが参照されることもあります。そこで生成したbase branchの画像をbase branchの最新のcommitのhashに紐づけて保存しておき、このhashが変わらない限り使い回すようにしています。 このように生成した画像をcommitのhashに紐づけて保存するためにアプリケーションのプロジェクトとは別にGitHubのRepositoryを用意しました。画像保存用のRepositoryに対して、commitのhashをbranch名として画像を保存することでこの仕組みを簡単に実現できました。前述の例ではf9c2dccというbranch名でbase branchのアプリケーションの画像が保存されます。 この仕組みを次の図に表しました。 画像比較 次にPull Requestのbase branchとhead branchの画像を比較します。 Pull Requestにcommitがpushされる度にhead branchの画像を生成し、base branchは最新のcommitのhashに紐づいた画像がない場合のみ画像を生成します。両者の画像が揃ったところで画像を比較します。 ZOZOTOWN Androidでは、画像の比較にも前述のRoborazziというライブラリを使用しています。Roborazziはコマンドを実行するだけで画像を比較し、その結果を出力してくれます。 比較結果は差分を抽出した画像として出力されます。例として、ボタンの横幅がわずかに変わってしまっていることを検出した際の出力結果を載せておきます。 このように目検では見落としがちな意図しないレイアウトの変更をVRTによって検出できます。 結果出力 最後に比較結果の画像を出力してPull Requestに添付します。 Pull Requestにはリンクで比較結果の画像を添付するため、比較結果の画像をクラウドストレージに保存する必要があります。ZOZOTOWN Androidでは、前述したアプリケーションの画像生成の工程と同様に、比較結果の画像もGitHubのRepositoryに保存しています。ただし管理を容易にするため、アプリケーションの画像を保存するRepositoryと比較結果の画像を保存するRepositoryは分けています。 比較結果の画像はPull Requestと紐づけて管理します。Pull Requestのhead branchとbase branchのbranch名を_to_でつないだ値をbranch名として保存しています。前述の例ではfeature_to_developというbranch名で比較結果の画像が保存されます。 まとめ 本記事では、ZOZOTOWN AndroidにおけるVisual Regression Testing(VRT)の導入について紹介しました。 現状では共通パーツのみにVRTを適用しているため、全体の約14%のComposableがVRTの対象となっています。今後、VRTを適用するモジュールを増やすなど、VRTをさらに活用していきます。Android開発でVRTの導入を検討されている方がいれば、ぜひ本記事を参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、情報セキュリティ部の 兵藤 です。日々ZOZOの安全を守るためSOC業務に取り組んでいます。 本記事ではサイバー脅威インテリジェンスプラットフォーム「OpenCTI」からSplunkへの脅威インテリジェンスの取り込みについて紹介します。また、この内容については以下の「OpenCTIをSentinelに食わせてみた」に関連した内容となっています。 techblog.zozo.com 目次 はじめに 目次 背景と概要 構築 Splunkへのログ移行 Splunk Add-on EventhubとMDEの設定 SplunkでのOpenCTI連携設定 OpenCTI Add-on for Splunkの利用 OpenCTI側のStream設定 運用 Splunk ESでのThreat Intelligence framework OpenCTIのインジケータをThreat Intelligence frameworkに組み込む Threat Activity Detected ルール MDEのファイルハッシュのバージョン差分 除外の設定 まとめ おわりに 背景と概要 ZOZOのSOC業務において以前はMicrosoft Sentinelを活用していました。現在では業務範囲の拡大や運用効率化など、様々な理由からSplunk Enterprise Security(以降 Splunk ES)へ移行しました。移行に伴い、Sentinelで構築していた脅威インテリジェンスを用いた検知をSplunkでも行うため、OpenCTIからの脅威インテリジェンスの取り込み方法を再検討する必要がありました。 以前の記事では、Logic Appsを使用してOpenCTIからSentinelへ脅威インテリジェンスを取り込む方法を紹介しましたが、今回はSplunkに対応するため異なるアプローチを採用しました。 Splunkにはデータを取り込むための各種Add-onが提供されており、その中には「 OpenCTI Add-on for Splunk 」も含まれています。このAdd-onを利用することで、OpenCTIからSplunkへのデータ取り込みが容易になります。 上記を用いて、脅威インテリジェンスを用いた検知を自動で行うようにOpenCTIのインテリジェンスをSplunk ESに食わせました。 構築 以下の環境で構築しました。 OpenCTI 6.9.13 Splunk Cloud 9.3 Splunk Enterprise Security 8.3 ※ 最近バージョン7系から8系にアップグレードしました Splunkへのログ移行 SentinelからSplunkへの移行に伴い、各種ログソースのデータをSplunkに取り込む必要がありました。Splunkはオンプレからクラウド環境まで各種ログをindexerというコンポーネントに取り込むことが可能です。取り込み方も多種多様で、各ログソースに応じた方法があります。ここでは、脅威検知の対象となるログの1つとしてMicrosoft Defender for Endpoint(MDE)を例に取り込み方法を紹介します。 Splunk Add-on Splunkで脅威情報を検知するためには、MDEのインシデントやアラートのログだけではなくテレメトリのログを投入する必要があります。MDEのテレメトリログをSplunkに取り込むためには、「 Splunk Add-on for Microsoft Cloud Services 」を使用します。 また、ログを正規化(CIM化)するために「 Microsoft Defender Advanced Hunting Add-on for Splunk 」のAdd-onも追加します。 Microsoft Cloud ServicesのAdd-onを用いてMDEのログを取得するためにはAzureのアプリケーション登録が必要です。Splunkからこのアプリケーション経由でログをとりに行くのですが、そのためにはクライアントシークレットが必要なので以下の画像のようにそちらも登録します。 クライアントシークレット このシークレットを使用してAdd-onの設定上にある「Azure App Account」に登録します。 Azure App Account その後、Add-onの「入力」設定からどのようなログを取り込むかの設定をします。今回はAzureの Event Hubs 経由でMDEのログを取得するように設定しました。ここからの設定は事前にEvent Hubsを構築しておく必要があります。 Event Hubsでの入力 ここでIndexやソースタイプなどを選択可能です。ソースタイプには「Microsoft Defender Advanced Hunting Add-on for Splunk」を入れたら適用できるようになる mscs:azure:eventhub:defender:advancedhunting を設定するとCIM化ができます。 詳しくはこちらの 公式ドキュメント を参照してください。 EventhubとMDEの設定 SplunkにMDEのログを取り込む為にEvent Hubsを作成する必要があります。詳しい作成方法は 公式ドキュメント を参照してください。 先ほどSplunk Add-onで登録したAzureアプリケーションに、このEvent Hubsへの読み取り権限を付与することで、ログを読み取れるようになります。具体的にはAzureのARM APIを用いたロールです。Entraなどの設定ではなくAzure Resourceごとの設定となります。Event Hubsなどの「アクセス制御 (IAM)」の項目から設定できます。 Event HubsのIAM設定 ロールとしてはデフォルトで存在するものでは「Azure Event Hubs のデータ受信者」のロールを付与すればSplunkからログを読み取れるようになります。以前は SASを用いてアクセスする方法 もありましたが、ロールを定義してアクセスする方法が推奨されています。 続いて、MDEからEvent Hubsへログを送信する設定します。MDEのポータルから「システム」→「設定」→「Microsoft Defender XDR」→「ストリーミング API」へ進み、「追加」をクリックします。以下のようにEvent Hubsの情報を入力できます。 ストリーミングAPIの設定 これでMDEのログがEvent Hubsへ送信され、Splunk Add-onで取り込めるようになります。 SplunkでのOpenCTI連携設定 OpenCTI Add-on for Splunkの利用 OpenCTIからSplunkへ脅威インテリジェンスを取り込むには、Notionの 「OpenCTI Ecosystem」 に紹介されている以下の「Splunk App」を利用します。 NotionのOpenCTI Ecosystem Splunkbaseには OpenCTI Add-on for Splunk で紹介されています。このAdd-onをSplunkにインストールし、OpenCTIとの連携設定を行います。基本的に、OpenCTIへのURLとAPIトークンを設定し、入力を作成するだけでOpenCTIからデータを取得できます。 OpenCTI Add-on for Splunkの設定 OpenCTI Add-on for Splunkの入力設定 入力欄の「Stream ID」はOpenCTI側で設定が必要です。Streamを作成しておきましょう。 OpenCTI側のStream設定 Stream の設定はOpenCTIの「データ共有」の項目から設定します。 データ共有 「ライブストリーム作成」をクリックすると以下のようにStreamの作成画面が表示されます。 ライブストリーム作成 基本的に、APIトークンでアクセスするため、公開ストリームにする必要はありません。自作したGroupを「アクセス可能」の欄に設定するだけでそのGroup内に所属するUserであればこのStreamにアクセス可能となります。例えば、SOC運用でOpenCTIを利用しているGroupを作成し、そのGroupにアクセス権限を付与すると以下のようになります。 アクセス可能グループの設定 これでSOC運用を行なっているメンバーがこのStreamへアクセスできるようになります。うまくStreamできていることが確認できれば、Stream読み取り用のロールを持つGroupを作成しておくと良いでしょう。 また、Splunkに連携するインジケータもフィルタリングして選別が可能です。以下のようにスコアなどでフィルタリングできます。 フィルタリング設定 条件をつけ作成したら以下のようにStream IDの発行されたライブストリームが作成されます。例としてAbuse.chのフィードを流すStreamを作成しました。 Stream IDの発行 最後に開始ボタンを押してStreamを開始します。 Stream開始 その後、このStream IDをSplunkのAdd-on側で設定すればOpenCTIからSplunkへデータが流れ込みます。以下のようにAdd-onの「Indicators Dashboard」で確認できれば接続成功です。 Indicators Dashboard 運用 ここからの運用のお話はSplunk ESのバージョン7系のものを引き継いだ内容を記載します。8系の最新機能などはあまり含まれていないため、ご了承ください。 Splunk ESでのThreat Intelligence framework Splunk ESで脅威インテリジェンスを活用するにはどのようなフレームワークを用いているか理解するとスムーズです。どのようなイメージでインテリジェンスを処理しているかはSplunk DEVのドキュメント 1 の図を見るとわかりやすいです。 簡単に整理すると、インテリジェンスのデータソースから読み込んだ脅威インテリジェンスを正規化し、Splunk内にkvstore(脅威インテリジェンスのマスタテーブルに相当するもの)として保存されます。その後、 Threat_Intelligence のデータモデルがある程度マッチさせてくれます。 Threat Intelligence framework Splunk ESでの具体的な名称は以下のようになります。IPアドレスの場合を例に挙げます。 データソース: local_ip_intel emerging_threats_compromised_ip_blocklist kvstore: ip_intel OpenCTIのインジケータをThreat Intelligence frameworkに組み込む ではここでOpenCTIのインジケータがAdd-onを通じてSplunkにどのように取り込まれているか確認します。Add-onの 解説 を見ると、以下のように opencti_lookup のkvstoreに保存されます。 opencti_lookup Add-onにはこのkvstoreを用いたカスタムアラートアクション作成手順が記載されています。ですが、ここではSplunk ESのThreat Intelligenceのframeworkに組み込む方法を紹介します。 Splunk ESのインテリジェンスのデータソースの設定は「設定」→「Threat intelligence」→「Threat intelligence sources」から確認できます。ローカルからのソース設定もできますが、kvstoreの設定はないので直接 opencti_lookup から取り込むことはできそうにありません。そのため opencti_lookup の内容を既存の local_ip_intel や local_file_intel などのデータソースに流し込む必要があります。 解決策としてスケジュールサーチを用いて opencti_lookup からデータを正規化し、各データソースに流し込む方法を採用しました。データソースに流し込むためのフィールドは 公式ドキュメント を参考に設定できます。 以下はIPアドレスのSPL例です。 | inputlookup opencti_lookup | where type IN ("ipv4-addr", "ipv6-addr") AND strptime(valid_from, "%Y-%m-%dT%H:%M:%S.%QZ") <= (now() - 9*3600) AND relative_time(now(), "+12h") <= strptime(valid_until, "%Y-%m-%dT%H:%M:%S.%QZ") | eval description=created_by, ip=value, weight=score | fields description ip weight | outputlookup local_ip_intel インジケータの有効期限のフィールド valid_from や valid_until もOpenCTI側で設定できるため、これらを用いて有効なインジケータのみを取り込むようにしています。また各種フィールド名もSplunk ESのデータソースに合わせて変換しています。最後に outputlookup でデータソースに流し込みます。 description のフィールドは脅威検知のSPLで見ることができる項目のため、こちらに好きなフィールドを設定すると良いでしょう。例えば created_by に設定しておくとどこからのインテリジェンスなのかが一目でわかります。またこの設定だと同じインテリジェンスが複数回 local_ip_intel に流し込まれます。ですが threat_key をKeyとしたkvstore内では上書きが走るだけなのでkvstore内のデータが膨れ上がることはありません。既存のインテリジェンスがあるものは join を利用し where で除外する方法もありますが、最新の情報に更新したい場合はkvstoreの性質を活用し上書きする形が適していました。 このスケジュールスキャンをどのタイミングで実行するかは local_ip_intel が ip_intel に取り込まれるタイミングによって設定すれば良いです。デフォルトの値は12時間(43200s)なので、12時間毎にとりあえず試してみるのも良いでしょう。この値は変更できるので好きなタイミングで実行間隔を制御できます。 Threat Activity Detected ルール kvstoreに格納されたOpenCTIのインテリジェンスを用いて検知するルールは「Threat Activity Detected」のEvent-based detectionのみで完結します。Splunkのデータモデルが活きてくるいい例です。 このdetectionのSPL冒頭は以下となっており、単純にマッチした Threat_Activity を拾ってくるだけで検知することがわかります。 | from datamodel:"Threat_Intelligence"."Threat_Activity" どのようなログのデータをマッチさせているかは「Threat matching」の項目から確認できます。例えば src フィールドに入ってくるIPとマッチさせる設定は以下のようになっています。 srcのマッチング設定 各データモデル( All_Traffic や Web など)の src に脅威インテリジェンスのIPアドレスがマッチした場合に Threat_Activity として検知されます。OpenCTIから取り込んだインジケータもこの ip_intel に格納されているため、OpenCTI由来のインジケータであっても問題なく検知されます。 逆に言えば、各種データモデルのtagが付いてない生ログだとこのデータモデルにログとして取り込まれないので検知がされません。MDEのCIM化されたログは基本的に network などはついているものの、 web などのtagがついていません。MDEのログと脅威インテリジェンスの以下 url などの項目をマッチさせるためには別途「計算済みフィールド」と「タグ」の設定を追加する必要があります。基本的にMDEはエンドポイントのログなので url の設定はお好みで良いでしょう。今回はIPのマッチングやファイルハッシュのマッチングに絞って記載します。 urlのマッチング設定 「Threat Activity Detected」はデフォルトの状態からONにするだけで利用できます。ですが、公開サーバのログを取り込んでいる場合は src のマッチングで大量検知の可能性があります。 dest で抜けていく通信は見たいが、 src で来るものは除外する要件がある場合には工夫が必要です。そこで、ZOZOでは必要に応じてルールの条件を以下のように絞っています。 | where NOT (orig_sourcetype="<公開Productのトラフィックログソースタイプ>" AND source="threatmatch://src") このようにすることで、公開サーバのトラフィックログでの src マッチングを除外しています。必要に応じて条件を追加してみてください。 実際に検知が起こると以下のように表示されます。 Threat Detected ! MDEのファイルハッシュのバージョン差分 MDEのログをCIM化する「Microsoft Defender Advanced Hunting Add-on for Splunk」のAdd-onアプリによって以下の file_hash で検知挙動に大きく差が出ます。 file_hashのマッチング設定 このAdd-onのバージョン 1.4.2 より前では MD5 のハッシュ値が file_hash フィールドに格納されていました。 1.4.2 以降のバージョンでは SHA256 のハッシュ値が格納されるようになりました。この変更後OpenCTIから取り込んだインジケータで SHA256 のハッシュ値を用いている場合、 file_hash でのマッチングが行われます。元々 MD5 でインジケータを取り込んでいる場合はマッチングされなくなるため注意が必要です。 この変更はCommit 0684d4d 2 で行われた内容のようで、最新のバージョン 1.4.2 でSplunkbaseに組み込まれたようです。GitHub上では、まだ 1.3.9 のリリースしか見えないため、バージョン管理が少しわかりづらいですが、Splunkbase上のバージョンを確認して適切なバージョンを利用してください。 除外の設定 脅威検知まではこれでできるようになりますが、このインテリジェンスは調査の結果「検知までする必要はないかも」といったケースも出てきます。そういった場合はサーチマクロを作成し、除外設定を行います。以下が一例です。 where NOT threat_match_value IN ("<IPアドレス1>", "<IPアドレス2>") このマクロを「Threat Activity Detected」内のSPLに追記するだけで除外が可能です。 8系では、 SafeList という除外リスト機能が追加されています。今後はこちらを活用していくことも検討しています。 まとめ 本記事ではOpenCTIからSplunkへ脅威インテリジェンスを取り込み、Splunk ESで脅威検知する手法について紹介しました。OpenCTI Add-on for Splunkを用いることで比較的容易に連携が可能となり、SOC業務での脅威インテリジェンス活用が促進されます。 OpenCTIを用いた自動でのインテリジェンス検知運用の記事は事例が少ないので、参考になれば幸いです。 ZOZOではこれからも脅威インテリジェンスを逐次収集し、SOCの検知能力の向上に努めていき、ZOZOの安全性を高めていきたいと考えています。 おわりに ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com Threat Intelligence framework in Splunk ES ↩ Commit 0684d4d ↩
アバター
ZOZO開発組織の2026年1月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2026年1月は、前月のMonthly Tech Reportを含む計14本の記事を公開しました。特に次の2記事は反響も大きく、とても多くの方に読まれています。ぜひご一読ください。 techblog.zozo.com techblog.zozo.com 登壇 LODGE XR Talk Vol.35 1月22日に開催された「 LODGE XR Talk Vol.35 」に、技術戦略部の諸星( @ikkou )が登壇しました。 techblog.zozo.com ZOZO開発部門の事例に学ぶ!組織横断で成果を生むAI活用 1月30日に開催された「 ZOZO開発部門の事例に学ぶ!組織横断で成果を生むAI活用 〜潜在課題を定量データで捉える課題解決アプローチ〜 」に、技術戦略部 テックリードの堀江( @Horie1024 )が登壇しました。 掲載 ITmedia ビジネスオンライン ITmedia ビジネスオンラインに、2025年に実施したChatGPTの自作GPTs利用率を競わせる社内の生成AIコンテスト「ZOZO GPTs LEAGUE」に関する記事が掲載されました。 www.itmedia.co.jp マイナビニュース マイナビニュースに、1月29日から31日の3日間にかけて開催された「TOKYO PROTOTYPE」にZOZO NEXTが出展した「Alternative Crafts produced by ZOZO NEXT」の様子が掲載されました。 news.mynavi.jp その他 2026年3月期 第3四半期決算発表 1月30日に2026年3月期 第3四半期決算を開示しました。詳細は以下のリンクにある開示資料をご確認ください。 corp.zozo.com 以上、2026年1月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
.table-of-contents > li > ul > li > ul > li { display: none; } はじめに 検索基盤部 検索基盤ブロックの佐藤( @satto_sann )です。普段はZOZOTOWNの検索システムの開発や、社内エンジニア向けAI活用推進に取り組んでいます。 AI活用推進の一環として、エンジニアを対象とした プロンプトエンジニアリングのハンズオン を実施しました。本記事では、そのハンズオンの内容や、実施を通じて得られた学びについて紹介します。 具体的には、以下の内容をまとめています。 代表的な8つのプロンプトエンジニアリング手法と、実務で使える具体的なプロンプト例(良い例・悪い例) ハンズオン実施後に実施した参加者アンケートの結果 企画・運営を通じて見えてきた学びや改善ポイント 社内で生成AIの活用を推進している方や、「プロンプトエンジニアリングという言葉は知っているものの、実務ではうまく使いこなせていない」と感じている方にとって、少しでも参考になれば幸いです。 目次 はじめに 目次 プロンプトエンジニアリングとは ハンズオン実施の背景と目的 背景 なぜハンズオン形式を選んだのか ハンズオンの目的 ハンズオンの内容 基本原則:AIとの対話の基本 紹介した8つの手法 1. ロールプロンプト(役割の付与) 2. Zero-Shot・One-Shot・Few-Shot 3. CoT(Chain-of-Thought) 4. Zero-Shot CoT 5. プロンプトチェーン プロンプトの構造化 6. 区切り記号を使った構造化 7. XML形式を使った高度な構造化 8. 出力形式の指定 ハルシネーションへの注意 ハンズオンの成果 ハンズオンの内容満足度 業務活用イメージ 感想 実施してみての学び 体系的な学習機会の重要性 実務に即した例の効果 継続的な学習のサポート まとめ おわりに プロンプトエンジニアリングとは プロンプトエンジニアリングとは、LLM(大規模言語モデル)から期待通りの出力を得るために、LLMへの指示文(プロンプト)を設計する技術のことです。 どんなに強力なAIツールを使っていても、適切なプロンプトでなければ本来の能力を十分に発揮できません。適切なプロンプトエンジニアリングにより、求めている情報や形式に沿った的確な出力を引き出すことができるようになります。つまり、プロンプトエンジニアリングは、AIという強力なツールを「使いこなす」ための必須スキルです。 ハンズオン実施の背景と目的 背景 生成AIを業務に取り入れる企業が増える中、「プロンプトエンジニアリング」という言葉を耳にする機会も多くなっているのではないでしょうか。しかし、体系的な学習機会がないまま、なんとなくの理解で終わっている方も少なくないと感じています。 社内でもChatGPTをはじめとする生成AIツールの活用が進んでいましたが、プロンプトエンジニアリングを体系的に学べる資料や機会は提供できていませんでした。そのため、各手法をどこまで知っているかは個人によってばらつきがある状態だったと考えています。 また、プロンプトエンジニアリングの手法についてまとめた記事は世の中にたくさんありますが、実務で応用しやすい形式になっているものは意外と少ないと感じていました。そこで、社内のエンジニア向けに体系的な学習ができるハンズオンを企画・実施しました。 なぜハンズオン形式を選んだのか 社内でプロンプトエンジニアリングに関する知識を共有する方法として、ドキュメント配布や動画共有も検討しました。しかし、プロンプトエンジニアリングは実際に手を動かして試行錯誤することで身につく部分が大きいと考え、ハンズオン形式を採用しました。 参加者が自分の手でプロンプトを入力し、出力の違いを体感することで、より実践的な理解が得られると期待していました。 ハンズオンの目的 参加者が以下を持ち帰れることを目指しました。 「なんとなく使っていた」状態から「意図を持って使い分けられる」状態への変化 明日から業務で試せる具体的なプロンプトパターン ハンズオンの内容 ハンズオンでは、まずプロンプトを書く上での 基本原則 を確認した後、実務で活用しやすい 8つの手法 を実際に試してもらいました。最後に、生成AIを使う上で避けて通れない ハルシネーション についても注意喚起を行いました。 以降では、それぞれの内容を具体的なプロンプト例とともに紹介します。 基本原則:AIとの対話の基本 プロンプトを書く上での基本原則は以下の4つです。 簡潔 :過不足なく簡潔に伝える 明確 :タスクは具体的かつ明確に定義する 一問一答 :質問は1つずつ絞る 察して禁止 :AIはあなたの意図を理解しません。例や背景、課題を添えて理解を助ける 特に「察して禁止」については、人間同士のコミュニケーションとの違いとして強調しました。AIは文脈を察する能力が限定的なため、必要な情報は明示的に伝える必要があります。 紹介した8つの手法 1. ロールプロンプト(役割の付与) 項目 説明 使いどころ ・ 特定の専門知識や視点を持つ人物として回答してほしいとき ・ 複数の立場からの意見を得たいとき メリット ・ 回答の深さ・具体性が増す ・ トーンや表現が一貫する 注意点 ・ 役割設定が曖昧だと効果が薄れる ・ ターゲット(誰向けか)も併せて指定すると効果的 AIに特定の役割やペルソナを与える手法です。「あなたは熟練したPythonエンジニアです」のように役割を指定することで、その専門性を持った視点からの回答を得られます。 ハンズオンでは、コードレビューのシーンを例に、役割を指定した場合としない場合の出力の違いを確認しました。 ❌ 悪い例 以下コードをレビューしてください ### コード ### import csv # CSVファイルを読み込む with open('data.csv', mode='r', encoding='utf-8') as file: reader = csv.reader(file) for row in reader: この例では、相手(ペルソナ)の属性が不明で、誰の立場でフィードバックするかが不明確です。 ✅ 良い例 あなたは熟練したPythonの知識があり、チーム開発を行う優しい先輩エンジニアです。 Pythonに不慣れな後輩エンジニアから以下コードのレビュー依頼があったので フィードバックしてください。 ### コード ### import csv # CSVファイルを読み込む with open('data.csv', mode='r', encoding='utf-8') as file: reader = csv.reader(file) for row in reader: ロールが明確になることで、指摘の深度や伝え方が調整され、より実務的で具体的なフィードバックが得られることを体感してもらえたと思います。 2. Zero-Shot・One-Shot・Few-Shot 項目 説明 使いどころ ・ Zero-Shot:シンプルなタスク ・ One-Shot:フォーマット指定 ・ Few-Shot:複雑なルールの理解 メリット ・ 例を増やすほど精度と一貫性が向上 ・ 微妙なニュアンスにも対応可能 注意点 ・ 例が多すぎるとコストが増加 ・ タスクの複雑さに応じて適切な例数を選択する AIに示す例の数を調整する手法です。 Zero-Shot :例を示さずにタスクだけを渡す One-Shot :1つの例を示して参考にさせる Few-Shot :複数の例を示して複雑なパターンを理解させる 実務では、例なしで十分な場合はZero-Shot、精度を上げたい場合はFew-Shotを使うケースが多いです。ハンズオンでは、商品属性の抽出を例に、Zero-ShotとFew-Shotの違いがより顕著に現れることを体感してもらいました。 ❌ 悪い例(Zero-Shot) 商品説明から属性を抽出して 「柔らかなカシミヤ100%のタートルネックニット。上品なベージュカラー。」 ✅ 良い例(One-Shot) 商品説明文から、以下の属性を抽出してください。 入力: 「軽量で撥水加工を施したナイロンジャケット。ブラックとネイビーの2色展開。」 出力: - 素材: ナイロン - 機能: 撥水加工, 軽量 - カラー: ブラック, ネイビー - アイテム: ジャケット 【抽出対象】 入力:「柔らかなカシミヤ100%のタートルネックニット。上品なベージュカラー」 出力: 1つの例を示すことで、例と同じ形式で出力されます。形式が決まっているようなタスクには One-Shot が有効です。 加えて、例を増やす( Few-Shot )ことでより多様なパターンへ対応できます。 ✅ 良い例(Few-Shot) 商品説明文から、以下の属性を抽出してください。 【例1】 入力: 「軽量で撥水加工を施したナイロンジャケット。ブラックとネイビーの2色展開。」 出力: - 素材: ナイロン - 機能: 撥水加工, 軽量 - カラー: ブラック, ネイビー - アイテム: ジャケット 【例2】 入力: 「ストレッチ性に優れたコットンデニム。スリムフィット。ダメージ加工。」 出力: - 素材: コットンデニム - 機能: ストレッチ - フィット: スリム - デザイン: ダメージ加工 - アイテム: デニム 【抽出対象】 入力: 「柔らかなカシミヤ100%のタートルネックニット。上品なベージュカラー。」 出力: 複数の例で抽出パターンを示すことで精度が上がり、一貫した出力が得られます。 3. CoT(Chain-of-Thought) 項目 説明 使いどころ ・ 複雑な判断や計算が必要なとき ・ AIの判断根拠を知りたいとき メリット ・ 思考プロセスが可視化される ・ 各ステップのチューニングが容易になる 注意点 ・ シンプルなタスクには過剰 ・ ステップ設計に手間がかかる場合はZero-Shot CoTを検討 複雑な問題を小さなステップに分解し、AIに段階的に考えさせる手法です。 ❌ 悪い例 この商品の適正価格を教えて 商品: レザージャケット 原価: ¥15,000 この例では判断基準が不明で、思考プロセスが見えません。 ✅ 良い例 以下の商品の販売価格を提案してください。 段階的に考えて、最終的な推奨価格を提示してください。 【商品情報】 - 商品: レザージャケット - 原価: ¥15,000 - ブランド: URBAN LEATHER (中価格帯、若年層向け) - ターゲット: 20-30代男性 - 季節: 秋冬 - カテゴリ平均価格: ¥25,000〜¥35,000 【価格設定の考え方ステップ】 ステップ1: 原価率から基本価格を算出 - 原価の2.5〜3.5倍が一般的な小売価格 - 計算してください ステップ2: 競合分析を考慮 - 同カテゴリの平均価格帯を考慮 - ブランドポジショニング(中価格帯)を反映 ステップ3: ターゲット顧客の支払い意欲 - 20-30代の価格感度を考慮 - 購入しやすい価格設定 ステップ4: 最終的な推奨価格 - 上記を総合して、推奨価格を提示 - 理由も簡潔に説明 【出力形式】 各ステップの考察と、最終推奨価格を記載してください。 ステップごとに判断根拠が可視化され、最終的な出力結果が意図しない場合もステップごとの出力を見て修正・変更ができるようになります。 4. Zero-Shot CoT 項目 説明 使いどころ ・ 日常的なタスクで手軽に精度を上げたいとき ・ CoTのステップ設計が面倒なとき メリット ・ 一文追加するだけで精度向上 ・ 導入コストが極めて低い 注意点 高度に専門的なタスクでは、明示的なステップ設計(CoT)の方が効果的な場合もある プロンプトに「ステップバイステップで考えて」の一文を追加するだけで、AIが自動で段階的な思考をするようになる手法です。 カフェテリアには23個のリンゴがありました。 彼らは20個を昼食に使い、さらに6個のリンゴを購入した場合、現在何個のリンゴがありますか? ステップバイステップで考えてください。 日常的なタスクで手軽に使える点が好評でした。思考プロセスをAIに指示するのはカロリーが高いですが、この一文を追加するだけで精度が高くなる傾向にあります。 5. プロンプトチェーン 項目 説明 使いどころ ・ 大規模タスクを段階的に進めたいとき ・ 各ステップで異なる視点や出力形式を求めたいとき メリット ・ 途中段階で確認・修正が可能 ・ ロールを段階的に変化させて精度を高められる 注意点 ・ 複数回のAI呼び出しが必要 ・ シンプルなタスクには過剰な場合がある 複雑なタスクを複数の小さなタスクに分割し、各タスクの出力を次のタスクの入力として使用する手法です。資料作成を例に、4つのステップに分けて実演しました。 ステップ1:要件整理 あなたは社内向けのプレゼン資料作成の専門家です。 ChatGPTをチーム内で正式導入するため、上司(AIツールに詳しくないが、 業務効率化に関心がある)へ提案する資料を作成してください。 ### ステップ1の指示 ### 次の提案資料を作る前に、以下の項目を整理してください。 - 提案の目的(何を実現したいか) - 想定読者(上司の立場や関心) - 伝えたいメッセージ(この資料で最も訴えたい点) テーマ:「ChatGPTをチーム内に正式導入する提案」 出力は箇条書きで、簡潔にまとめてください。 ステップ2:構成設計 あなたはビジネスプレゼンの構成設計者です。 ### ステップ2の指示 ### ステップ1で整理した内容をもとに、上司向けの提案資料構成を設計してください。 各スライドのタイトルと要点(2~3行)を提示してください。 ステップ3:本文作成 あなたはプレゼン資料作成の編集者です。 ### ステップ3の指示 ### ステップ1で整理した内容とステップ2構成案をもとに、 各スライド内容をMarkdown形式で作成してください。 出力形式: - 各スライドを「# スライドタイトル」「- 要点」で表記 - ビジネスプレゼンに適した簡潔な文章 - 上司が3分以内に理解できる情報量 ステップ4:添削・リライト あなたはIT企業で働くチームマネージャー (AIツールに詳しくないが、業務効率化に関心がある)です。 ### ステップ4の指示 ### メンバーが作成したプレゼン資料について、伝わりやすいトーンにリライトしてください。 修正ポイント: - 専門用語を避け、平易な表現に言い換える - 「なぜ導入すべきか」を強調する - 文のリズムを整え、読みやすくする 「一度で完璧を目指す」よりも「段階的に精度を積み上げる」という考え方がプロンプトチェーンの本質です。途中段階で確認と修正を挟めるため、最終出力が意図と大きくずれるリスクを軽減できます。 プロンプトの構造化 ここまで、役割付与や思考プロセスの設計など、プロンプトの「内容」に関する手法を紹介してきました。ここからは、プロンプトの「構造」を整理することで、AIの解釈精度を高める3つの手法を紹介します。 これらの手法は、指示・データ・制約条件を明確に区別することで、AIの誤解釈を防ぎ、より正確な出力を得ることができます。 6. 区切り記号を使った構造化 項目 説明 使いどころ ・ 常に利用をオススメ ・ 特に長文や複雑な構造を持つプロンプトで効果的 メリット ・ 指示・データ・制約の境界が明確になる ・ AIの解釈ミスを防止できる 注意点 ・ 区切り記号の種類は統一する ・ 過度に複雑な構造はXML形式を検討 ### 、 """ 、 --- などの区切り記号を使って、プロンプトを指示・入力データ・制約条件などに明確に分離する手法です。情報の境界を視覚的に示すことで、AIの解釈ミスを防ぎます。 商品説明の翻訳を例に確認しました。 ❌ 悪い例 以下の日本語商品説明を英語に翻訳してください。 - 対象読者: 海外の30代女性顧客 - トーン: 商品説明として自然で魅力的な英語 - 専門用語は一般的な表現に - ECサイトの商品ページに掲載される文章 このニットは柔らかいカシミヤ100%で作られています。 肌触りが良く、保温性に優れているため、秋冬シーズンに最適です。 上品なデザインで、カジュアルからオフィススタイルまで幅広く対応します。 お手入れは手洗いを推奨しております。 翻訳された英語テキストのみを出力してください。 この例では、どこまでが翻訳対象か不明確で、指示と内容が混在しています。 ✅ 良い例 以下の日本語商品説明を英語に翻訳してください。 ### 指示 ### - 対象読者: 海外の30代女性顧客 - トーン: 商品説明として自然で魅力的な英語 - 専門用語は一般的な表現に - ECサイトの商品ページに掲載される文章 ### 翻訳対象 ### """ このニットは柔らかいカシミヤ100%で作られています。 肌触りが良く、保温性に優れているため、秋冬シーズンに最適です。 上品なデザインで、カジュアルからオフィススタイルまで幅広く対応します。 お手入れは手洗いを推奨しております。 """ ### 出力形式 ### 翻訳された英語テキストのみを出力してください。 この手法は常に利用することをオススメしており、参加者からも「すぐに使える」という声が多く聞かれました。 7. XML形式を使った高度な構造化 項目 説明 使いどころ ・ プロンプトが複雑かつ長文のとき ・ データを変数として埋め込みたいとき メリット ・ 開始・終了が明確でAIの誤認を防止 ・ ネスト構造で複雑なデータも表現可能 注意点 ・ シンプルなタスクには過剰 ・ 区切り記号で十分な場合はそちらを優先 <data>xxx</data> のようなXMLタグを使い、指示や情報を明確に区別する手法です。プロンプトの構造が複雑な場合や、データを変数として埋め込む際に特に有効です。 顧客レビュー分析を例に確認しました。 ❌ 悪い例 あなたはデータアナリストです。 A社とB社の顧客レビューを分析し、経営チーム向けにレポートを作成してください。 A社の配送が早くて助かりました。品質も良いです。なお、今後もこのスピードを維持してほしいです。 B社の商品はデザインが良いが、到着が遅かったです。なお、配送体制の見直しが必要だと思います。 A社のカスタマーサポートが親切でした。なお、対応スピードも安定しています。 なお、成功した施策と改善が必要な領域を明確にしてください。 この例では、レビュー内容と指示が混在しており、AIがどこまでがデータでどこからが指示かを判断しづらくなっています。 ✅ 良い例 あなたはデータアナリストです。 A社やB社のレビューを分析し、<report_target name='A社'>経営チーム</report_target> 向けのレポートを作成してください。 ### レビュー <review> <good_reviews> - A社の配送が早くて助かりました。品質も良いです。なお、今後もこのスピードを維持してほしいです。 - A社のカスタマーサポートが親切でした。なお、対応スピードも安定しています。 </good_reviews> <bad_reviews> - A社のサイトは少し使いにくいと感じました。商品検索がもう少し改善されると嬉しいです。 - B社の商品はデザインが良いが、到着が遅かったです。なお、配送体制の見直しが必要だと思います。 </bad_reviews> </review> ### 命令 成功した施策と改善が必要な領域を明確にしてください。 <good_reviews> や <bad_reviews> タグ内にレビューをまとめることで、以下メリットが受けられます。 データと命令の境界が明確になる タグのネストも可能で、自然言語を複雑な構造で表現できる タグを利用することでデータの変更・追加がしやすくなり、再利用性も向上 その他タグを使った例やベストプラクティスについては以下ドキュメントが参考になります。 OpenAI Prompt Engineering ガイド(Markdown/XMLのメッセージ整形) Claude 公式ドキュメント(XMLタグ) Google Vertex AI 公式ドキュメント(構造化プロンプト) 8. 出力形式の指定 項目 説明 使いどころ ・ システム連携やデータ処理に使うとき ・ 複数項目を一貫した形式で取得したいとき メリット ・ 出力の一貫性が保証される ・ 後続処理やデータベース登録が容易になる 注意点 形式だけでなく、各項目の定義や欠損時の扱いも明示すると精度が向上 JSON、Markdown、CSV、表など、出力形式を明示的に指定する手法です。システム連携やデータ処理を見据えた構造化データを取得したい場合に活用します。 商品データの構造化抽出を例に確認しました。 ❌ 悪い例 商品情報を抽出して 「このシルクブラウスは高級感のある光沢が特徴。 サイズはS、M、Lの3展開。ホワイト、ブラック、ネイビーの3色。 価格は¥9,800です。」 この例ではフォーマットが不明で、出力結果の一貫性が保証されません。 ✅ 良い例 以下の商品説明から情報を抽出し、JSON形式のみ出力してください。 ### 商品説明 ### 「このシルクブラウスは高級感のある光沢が特徴。 サイズはS、M、Lの3展開。ホワイト、ブラック、ネイビーの3色。 価格は¥9,800です。」 ### 出力形式 ### 以下のJSON形式で出力してください: { "item_name": "商品名", "material": "素材", "sizes": ["サイズ1", "サイズ2", ...], "colors": ["カラー1", "カラー2", ...], "price": 数値(円), "features": ["特徴1", "特徴2", ...] } ### 注意事項 ### - 記載のない項目はnullとする - 価格は数値のみ(円記号不要) - サイズとカラーは配列で返す 明確なデータ構造を指定することで、システム連携が容易になり、一貫した出力が得られます。 より厳格な出力を求める場合は、各社モデルで提供している 構造化出力(Structured Output) をAPI経由で利用するのがオススメですが、ここでは詳細は省きます。気になる方は以下ドキュメントを参考にしてください。 OpenAI 公式ドキュメント(Structured Outputs) Gemini API 公式ドキュメント(Structured Output) Claude 公式ドキュメント(Structured Outputs) ハルシネーションへの注意 ハンズオンでは、手法の紹介だけでなく、ハルシネーション(幻覚)への注意喚起も行いました。LLMが実際には存在しない情報をあたかも正しいように生成してしまう現象であり、これを理解しておくことは非常に重要だと考えています。 回避方法として、以下を紹介しました。 情報源を明示的に尋ねる 事実確認が必要な場合は公式サイトや一次情報で裏付けを取る 曖昧な質問を避け、具体的かつ検証可能な指示を出す わからない場合は「わかりません」と答えるように促す ハンズオンの成果 ハンズオン実施後、参加者にアンケートを実施しました。76名の方にご回答いただき、以下のような結果が得られました。 ハンズオンの内容満足度 「ハンズオンの内容には満足いただけましたか?」という質問(5段階評価)に対して、 上位2段階(「満足」及び「大変満足」)と回答した方が100% でした。 大変満足:68.4% 満足:31.6% 業務活用イメージ 業務活用イメージについても同様に5段階評価で質問したところ、 上位2段階(「ある程度湧いた」及び「とても湧いた」)と回答した方が100% でした。 とても湧いた:68.4% ある程度湧いた:31.6% 感想 参加者からいただいた感想の中から、いくつかの声をご紹介します。 学習効果について :「良い例・悪い例を比較できたことが、プロンプトの理解に効果的だった」 構成について :「内容が整理されており、初心者でも理解しやすかった。業務への応用イメージも湧いた」 今後の活用について :「様々な手法やテクニックを知れた。目的や状況に応じた使い分けを実践していきたい」 実施してみての学び 今回のハンズオンを実施して、アンケート結果や参加者の声からいくつかの学びを得ることができました。 体系的な学習機会の重要性 プロンプトエンジニアリングは、日々の業務の中で自然と身につく部分もあります。しかし、参加者の感想にあった「様々な手法やテクニックがあることを知れた」という声からも分かるように、体系的な学習機会の提供が効果的です。基本原則や代表的な手法を学ぶことで、より効率的にスキルを習得できることを実感しました。 業務活用イメージの設問で100%の方が「湧いた」と回答してくださったことは、「なんとなく使っていた」状態から「意図を持って使い分けられる」状態への変化を示唆していると考えています。 実務に即した例の効果 ハンズオンでは、コードレビューや資料作成、商品データの抽出など、参加者の実務に近い例を多く取り入れました。「業務への応用イメージも湧いた」「初心者でも理解しやすい構成だった」といった感想をいただけたことから、抽象的な説明よりも具体的なユースケースを通じて学ぶアプローチが効果的だったと感じています。 また、「良い例・悪い例の形式が理解に効果的だった」という声もあり、比較を通じて違いを体感できる構成が学習効果を高めたと考えています。 継続的な学習のサポート ハンズオンは一度きりの機会ですが、参加者が日常業務の中で思い出せるよう、資料を「チートシート」として活用できる形式で提供しました。学んだ内容を定着させるためには、継続的に振り返れる仕組みが重要だと考えています。 こうしたハンズオンの実施も、ZOZOが開発組織の全体で進めているAI活用推進の一環です。現在、開発者の約77%がAI活用に満足しているという調査結果も出ています。詳細は以下のスライドをご確認ください。 speakerdeck.com AIワーキンググループの設置や知見の組織的な展開など、継続的な取り組みを進めています。今後は新しい手法やツールの登場に合わせた追加セッションの開催や、実践事例の共有会なども検討しており、組織全体でAI活用スキルを高めていく取り組みを継続していく予定です。 まとめ 本記事では、社内で実施したプロンプトエンジニアリングのハンズオンの取り組みについて紹介しました。 プロンプトエンジニアリングは、生成AIの業務活用で欠かせないスキルになりつつあります。しかし、体系的に学ぶ機会がないまま、なんとなくの理解で終わっている方も多いのではないでしょうか。 同様の課題を抱えている方や、社内でのAI活用推進を検討されている方にとって、本記事が何かしらの参考になれば幸いです。 おわりに ZOZOでは、AIを活用した業務効率化やプロダクト開発に積極的に取り組んでいます。一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご覧ください。 corp.zozo.com
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、ブランドソリューション開発本部WEAR開発部Androidブロックの武永です。普段は ファッションコーディネートアプリ「WEAR」 のAndroidアプリを開発しています。 背景 本記事では、Epoxy(Airbnb製のRecyclerView向けUI構築ライブラリ)を撤去し、Jetpack Compose(Compose)へ移行した背景を整理します。 移行の直接のきっかけは、当時の構成でKotlin Annotation Processing Tool(kapt)依存が残っていたことです。kapt依存が残っていたため、Kotlin Symbol Processing(KSP)への移行が進みませんでした。その結果、KotlinおよびDagger Hilt(Hilt)の更新判断も先送りされていました。 一方で、Epoxy自体はKSP対応が進んでいました。しかし、Data Binding用プロセッサはKSP未対応で、Data Bindingを使う構成ではkaptが必要でした( Epoxy 5.0.0beta02のリリースノート )。ビルド時間でもボトルネックになっていました。その際、EpoxyのData Bindingを外して使い続ける案も検討しましたが、UIをComposeに統一するメリットが大きいと判断し、修正量は増えてもCompose化に踏み切りました。 本記事の範囲 あわせて、進め方と得られた効果もまとめます。Data Bindingを使っている場合はkaptを外すための置き換えも必要です。この記事ではその前提を踏まえ、EpoxyのCompose化にフォーカスします。 記載方針 本記事では当時の実体験と現在の判断を分けて整理します。当時はAIエージェント(GitHub Copilot)をVisual Studio Codeで使っていました。現在は選択肢が増えていますが、内容は当時の作業基準に基づいています。以降の「リファクタリングする画面の問題点」から「Epoxy削除とCompose移行を進める」までは当時の判断をもとに記載します。一方、「改善の余地があった点」は現在の視点で振り返った内容です。 ※コードは社外向けに匿名化・簡略化しています。 目次 はじめに 背景 本記事の範囲 記載方針 目次 リファクタリングする画面の問題点 KotlinおよびHiltのバージョンアップが止まっていた kapt依存がボトルネックになっていた Epoxy削除とCompose移行を進める 同等性の検証方法 画面全体のCompose化 プロンプト 差分(抜粋) Epoxyの1モデルを1コンポーネントに置き換える プロンプト 差分(抜粋) AI支援による効率化 改善の余地があった点 まとめ リファクタリングする画面の問題点 KotlinおよびHiltのバージョンアップが止まっていた kapt依存のある構成ではKotlinおよびHiltの更新が進まず、関連するライブラリやビルド設定の更新にも踏み切れませんでした。結果としてアップデート作業が停滞していました。 kapt依存がボトルネックになっていた kapt依存はビルド時間の増加にもつながり、見直しのきっかけになりました。事前に簡易計測したところ、kaptの削減だけでも少なくとも30〜60秒は短縮できそうだと見込めました。 Kotlin Annotation Processing Tool(kapt)はJavaベースのアノテーション処理で、ビルド時にJavaのstubファイルを生成してから処理するため時間がかかる KSPはKotlin専用のアノテーション処理で、Kotlinコードを直接処理できるため、kaptより高速 EpoxyのData Binding用プロセッサがkapt依存だったため、Data Bindingを使う構成ではKSP移行が進まず、ビルド時間にも影響していた Epoxy削除とCompose移行を進める Epoxyを撤去し、UIをComposeへ移行 XMLレイアウト(ConstraintLayout中心)とEpoxyRecyclerViewを廃止し、ComposeのScaffold + LazyVerticalGridで再構成 AIエージェントの自動置換を活用し、確認と微調整をしながら置換作業を進めた 特に重視したのは見た目の完全一致と依存の排除です。EpoxyやXMLのConstraintLayoutを避ける構成にし、Composeの標準的なレイアウト(Column/Row)で組み直しました。 同等性の検証方法 移行前後で同一条件のスクリーンショット(同一端末・同一画像サイズ)を撮り、目視で比較しました。差分が出た場合は余白・文字サイズ・色の順で確認しました。今回は目視確認でしたが、Compose化したことで今後はスクリーンショットテストの導入も検討したいと考えています。 画面全体のCompose化 画面全体をComposeに置き換える場合は、EpoxyRecyclerView中心の構造をComposeの画面構成へ移す方針で進めました。 プロンプト TestGridLayoutFragment の EpoxyRecyclerViewの構成を Compose に置き換えてください。 - デザインは既存UIと完全に一致させること - Epoxyの使用は禁止 - ComposeのConstraintLayoutは使わず、シンプルなレイアウトで実装する - @Preview を必ず作成する 差分(抜粋) 1) FragmentのView生成(XML→ComposeView) Before(XML + Data Binding) _binding = DataBindingUtil.inflate(inflater, R.layout.fragment_test_grid_layout, container, false ) return binding.root After(ComposeView) return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { AppTheme { DetailScreen() } } } 2) グリッド構築(Epoxy→LazyVerticalGrid) Before(Epoxy + RecyclerView) binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), SPAN_COUNT) binding.recyclerView.withModels { // Epoxy models... } After(Scaffold + LazyVerticalGrid) Scaffold(topBar = { AppTopBar.Default(...) }) { paddingValues -> LazyVerticalGrid( columns = GridCells.Fixed(SPAN_COUNT), state = gridState, modifier = Modifier.fillMaxSize().padding(paddingValues), ) { item { /* Section */ } } } Epoxyの1モデルを1コンポーネントに置き換える 画面全体の置き換えだけでなく、Epoxyの「モデル単体」をComposeに置き換える方法も進めました。Epoxyのモデルクラス( @EpoxyModelClass )とXMLレイアウトを削除し、同じ役割のComposableを用意して既存の表示フローに組み込みます。 プロンプト ActionCompletedModel をComposeのUIコンポーネントに置き換えてください。 - デザインは既存UIと完全に一致させること - EpoxyモデルとXMLレイアウトは削除する - Epoxyの使用は禁止 - ConstraintLayoutは使わず、シンプルなレイアウトで実装する - @Preview を必ず作成する 差分(抜粋) Before(Epoxyモデル + XML) @EpoxyModelClass abstract class ActionCompletedModel : DataBindingModel<ActionCompletedBinding>() { @EpoxyAttribute lateinit var actionInfo: ActionInfo @EpoxyAttribute (DoNotHash) var onPrimaryActionClick: View.OnClickListener? = null override fun getDefaultLayout(): Int = R.layout.action_completed override fun bind(binding: ActionCompletedBinding, context: Context) { binding.actionButton.setOnClickListener(onPrimaryActionClick) binding.actionTitle.text = actionTitle(context, actionInfo) binding.actionSubtitle.text = actionSubtitle(context, actionInfo) } override fun unbind(binding: ActionCompletedBinding) { binding.actionButton.setOnClickListener( null ) } } After(Composable + 組み込み) LazyColumn { item { ActionCompletedContent( actionInfo = actionInfo, onPrimaryActionClick = { onPrimaryAction(actionInfo) }, ) } } Before(XML) <androidx . constraintlayout . widget . ConstraintLayout android : layout_width = "match_parent" android : layout_height = "wrap_content" > <com . example . RoundRectLayout android : id = "@+id/actionButton" android : layout_width = "match_parent" android : layout_height = "56dp" > <androidx . constraintlayout . widget . ConstraintLayout android : layout_width = "match_parent" android : layout_height = "match_parent" > <TextView android : id = "@+id/actionTitle" /> <ImageView android : id = "@+id/actionIcon" /> <TextView android : id = "@+id/actionSubtitle" /> </androidx . constraintlayout . widget . ConstraintLayout> </com . example . RoundRectLayout> </androidx . constraintlayout . widget . ConstraintLayout> After(Compose / ActionCompletedContent 内の抜粋) @Composable fun ActionCompletedContent( actionInfo: ActionInfo, onPrimaryActionClick: () -> Unit , ) { val context = LocalContext.current Column(modifier = Modifier.fillMaxWidth()) { Surface( modifier = Modifier .fillMaxWidth() .height( 56 .dp), shape = RoundedCornerShape( 12 .dp), ) { Box( modifier = Modifier .fillMaxSize() .clickable(onClick = onPrimaryActionClick), ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Row(verticalAlignment = Alignment.CenterVertically) { Text(text = actionTitle(context, actionInfo)) Icon( painter = painterResource(id = R.drawable.ic_action), contentDescription = null , ) } Text(text = actionSubtitle(context, actionInfo)) } } } } } AI支援による効率化 ざっくりした記録ですが、AIに置換を任せたPRは合計28件でした。1件あたりの作業時間は、人力だと3〜5時間、AI支援後は1〜2時間くらいで終わるケースが多かったです。単純計算で1件あたり2〜4時間短縮できたので、合計では約56〜112時間(7〜14人日)ほどの短縮になりました。 副次効果として、反復系の単純作業をAIに任せることで、人間が差分を照合するときのケアレスミスを減らせた実感がありました。 改善の余地があった点 プロンプトで「完全一致」と指示しても既存UIを完全には再現できず、細かい余白・フォント・色差で手戻りが発生しました。色定義をテーマから使うべきところで個別指定になり、見た目の統一にも手戻りが出ました。XMLのConstraintLayoutで組まれていたレイアウトが多く、Composeでも旧来の発想に引っ張られ、Column/Row前提で組み直す場面もありました。 今振り返ると、AIエージェント向け作業ガイドを整備しておけば、こうした手戻りを減らせたと思います。例えば、画面ごとのUI要素の対応表、余白/フォント/色の参照先、Composeでの置き換えルール、スクリーンショット比較の手順を1枚にまとめる、といった形です。 まとめ Epoxy削除によりkapt依存の主要因を外し、KSP移行に向けて前進できました。KotlinおよびHiltのアップデート準備も進みました。依存解消を目的にUI移行を進めると判断がぶれにくく、AIエージェントは置換作業の加速に向いていると感じました。Compose移行は「更新可能性」と「開発体験」の改善にもつながりました。 最後までご覧いただきありがとうございました。ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
.table-of-contents > li > ul > li > ul { display: none } はじめに こんにちは、データシステム部MLOpsブロックの 木村 です。MLOpsブロックではZOZOTOWN、WEAR by ZOZOをはじめとして、弊社で提供するさまざまなサービスに関わるML機能を開発・運用しています。 本記事で紹介する ZOZOマッチ (以下、本アプリ)は「ファッションで恋する」をコンセプトとしたマッチングアプリです。本アプリもML機能を持ち、MLOpsブロックが機能を開発・運用しています。 本アプリの特徴的なML機能として ファッションジャンル診断 (以下、ジャンル診断)があります。ジャンル診断とは全身画像をストリートやモードなどZOZOが定義した12種類のファッションジャンルに分類し、該当するジャンルの割合を「ジャンル傾向」として円グラフで表示する機能です。 ジャンル診断には2つの利用方法があります。1つ目はプロフィールに登録した自分の全身画像から自分の「ジャンル傾向」を判定する方法です。2つ目は好みのコーディネート画像を複数枚選んで自分の「好みの雰囲気のジャンル傾向」を判定する方法です。これら2つの利用方法によって「ファッションで恋する」レコメンドを実現しています。 このジャンル診断を実現するために、全身画像からファッションジャンルを判定する「ジャンル診断API」を開発しました。ジャンル診断APIはMLモデルによる推論処理を行い、全身画像を入力として12種類のジャンルそれぞれに属する確率を返します。 今回、このジャンル診断APIの推論サーバーとして Triton Inference Server (以下、Triton)を導入しました。TritonはNVIDIAが提供するオープンソースの推論サーバーです。 本記事ではジャンル診断APIの開発で推論サーバーにTritonを採用した背景から、導入時に遭遇した課題とその解決策、導入効果までを説明します。Tritonの導入にあたり 公式ドキュメント の説明だけでは理解しにくかった点もあり、いくつかの課題に直面しました。これらの知見がTritonを使った推論サーバーの構築・運用を検討する際の参考になれば幸いです。 本記事の内容は2026年2月時点の情報であることにご留意ください。 目次 はじめに 目次 Tritonを導入した背景 従来のML推論API構成とその課題 推論サーバーの選定 Tritonを採用するメリット 推論処理の高速化 処理の分離による保守性向上 設定変更の容易さ 推論単体の性能検証 ジャンル診断APIへの導入経緯 システム構成 全体構成 リクエストの流れ 通信プロトコルの選定 Tritonがサポートするプロトコル HTTPとgRPCの比較 Kubernetesリソースの構成 Tritonコンテナの起動設定 Deploymentマニフェスト InitコンテナとVolume Mountによるモデル共有 OpenTelemetryによるトレース設定 ヘルスチェック設定 セキュリティ設定 Triton Podへの接続設定 GCSの認証設定 オートスケーリングの設定 モデルのディレクトリ構成 モデルの設定 推論ゲートウェイAPIの実装 Triton接続の設定 StyleFeatureExtractorクラスの実装 推論リクエスト処理の実装 導入時の課題と解決策 課題1:gRPCで負荷分散されない 原因 解決策 1. Headless Serviceの採用 2. クライアント側でround_robinロードバランシングを設定 スケールアウト時の負荷分散 課題2:Datadog APMでトレースが連携されない 原因 解決策 1. W3C Trace Contextによるトレース伝播 2. Triton専用NodeにDatadog AgentをDaemonSetで配置 3. Tritonの起動オプションでトレース設定 課題3:PyTorchモデルがロードされない 原因 解決策 導入効果 導入時の注意点 コスト面の考慮 まとめ Tritonを導入した背景 従来のML推論API構成とその課題 MLOpsブロックでは従来、前処理から推論、後処理までの全処理を単一のAPI内で実行していました。Pod起動時に Google Cloud Storage (以下、GCS)からMLモデルをダウンロードし、入力データの取得から後処理までを同一プロセス内で実行する構成です。 この構成には2つの課題がありました。 1つ目はリソース効率が悪いことです。画像ダウンロードのようなI/Oバウンドな処理とMLモデルでの推論のようなCPU/GPUバウンドな処理ではボトルネックが異なります。しかし単一のAPIではこれらを分離できません。このリソース効率の悪さはパフォーマンス面とコスト面の両方に影響がありました。パフォーマンス面では、どちらか一方がボトルネックでもAPI全体を水平・垂直スケールする必要があり、ボトルネックではない処理のリソースまで増やすことになっていました。コスト面では、推論にGPUを使用したい場合、すべてのAPI PodにGPUを割り当てる必要があります。ただし実際にGPUを使うのは推論処理の間だけです。前処理・後処理の間もGPUを占有し続けるためGPU使用率が低くなり、無駄なコストが発生していました。 2つ目は推論速度の向上が難しいことです。ML推論APIではモデルの推論処理に要する時間が全体のレイテンシの大部分を占めます。しかし推論処理は単一のAPI内に組み込まれていることで、汎用的なWebフレームワーク上で実行されるため推論に特化した最適化手段が限られていました。そのため推論のレイテンシ改善が難しく、リアルタイム推論を活用したUXの良い機能の提供が困難でした。 推論サーバーの選定 これらの課題を解決するため推論サーバーの導入を検討しました。主要な推論サーバーを比較した結果は次の通りです。 名称 対応フレームワーク 実装言語 TorchServe PyTorch専用 Java/Python TensorFlow Serving TensorFlow専用 C++ BentoML 複数対応 Python Triton Inference Server 複数対応 C++ MLOpsブロックではPyTorchやTensorFlowなど複数のMLフレームワークを利用しています。また今回のジャンル診断だけでなく今後の別プロジェクトでも活用できる共通基盤として、複数フレームワークに対応しつつ高速な推論基盤が必要でした。 TorchServe と TensorFlow Serving はどちらも特定フレームワークのモデルで手軽にAPIを作成したい場合に有効です。しかし、複数フレームワークに対応していません。 BentoML は様々なMLフレームワークに対応していますが、Python実装のためパフォーマンス面で不利な場合があります。 一方でTritonは様々なMLフレームワークに対応しておりC++実装のため高速です。構成が複雑でキャッチアップコストはありますが、汎用性・機能性が最も高く共通基盤として最適と判断しTritonを選定しました。 Tritonを採用するメリット 複数の推論バックエンドに対応していることに加えてTritonを導入するメリットは次の4つがあります。 推論処理の高速化 処理の分離による保守性向上 設定変更の容易さ 推論単体の性能検証 以降で各メリットについて詳しく説明します。 推論処理の高速化 FastAPIなどPythonベースの推論サーバーは汎用的なWebフレームワークであり推論処理に最適化されていません。一方Tritonは推論処理に特化したサーバーです。リクエストの受付と推論実行を別々のスレッドプールで処理するため、推論中も新しいリクエストを受け付けられます。またC++で実装されているためPythonのGlobal Interpreter Lockの制約を受けず、推論処理をマルチスレッドで並列実行できます。 また Dynamic Batching により複数の推論リクエストをサーバー側でバッチ処理できます。最初のリクエストを受け取ってから指定した待機時間内に届いたリクエストを動的にまとめることで、個別処理よりもスループットが向上します。 さらに instance_group により同一モデルの複数インスタンスを並列実行できます。デフォルトでは同時に1つのリクエストのみ実行されますが、インスタンス数を増やすことで複数のリクエストを並列処理できます。 処理の分離による保守性向上 推論処理をTritonに委譲することでAPIサーバーは前処理・後処理に、Tritonは推論に専念できます。この分離により各コンポーネントの責務が明確になりコードの保守性が向上します。 スケーリングの観点でも大きなメリットがあります。具体的には水平スケール(Pod数の増減)や垂直スケール(CPUやメモリの割り当て変更)をAPIサーバーとTritonそれぞれに対して個別に適用できるようになります。推論がボトルネックの場合はTriton Podのみを、前処理・後処理がボトルネックの場合はAPIサーバー Podのみをそれぞれ独立してスケールできます。従来の一体型構成では推論のボトルネック解消のためにPod全体をスケールする必要がありました。分離構成ではボトルネックとなっているコンポーネントのみをスケールすればよくリソース効率が向上します。またAPIサーバーとTritonのメトリクスを個別に監視することでどちらがボトルネックになっているかを把握しやすくなります。 設定変更の容易さ Tritonではモデルの設定を config.pbtxt で宣言的に管理します。バッチサイズの上限、モデルインスタンス数、Dynamic Batchingの待機時間などをコード修正なしで変更できます。 従来のPythonベースの実装でもバッチサイズ等を環境変数で外部化できます。一方 config.pbtxt では max_batch_size や dynamic_batching 、 instance_group など推論サーバーに必要な設定項目が標準化されています。これらの設定項目を自前で設計する必要がありません。また config.pbtxt とモデルファイルを含むディレクトリをそのまま別環境にコピーすれば動作するため、環境間での設定の共有も容易になります。 推論単体の性能検証 従来の構成では推論性能の測定にはAPI全体への検証が必要であり、前処理・後処理の影響を含んだ結果しか得られませんでした。 Triton公式の Perf Analyzer を使用することで推論部分のみの性能を切り出して測定できます。Perf AnalyzerはTritonで実行される機械学習モデルの推論パフォーマンスを測定・最適化するためのCLIツールです。汎用的な負荷試験ツールでTritonをテストする場合はテンソル形式でのリクエスト組み立てを自前で実装する必要があります。一方Perf Analyzerではモデルのメタデータを自動取得してリクエストを生成するためその実装が不要です。さらにサーバー内部のレイテンシをqueueとcomputeに分解して出力します。queueは推論スケジューラのキュー内でリクエストが待機していた時間を示し、computeは入力データの準備、実際の推論実行、出力データの取得にかかった時間を示します。この分解によりボトルネックの切り分けが容易になり、モデル更新時のパフォーマンス確認や推論パラメータの調整に役立ちます。 ジャンル診断APIへの導入経緯 ここではなぜジャンル診断APIにTritonを導入したのかを説明します。 ジャンル診断APIは「ファッションで恋する」を実現するZOZOマッチの根幹となる機能です。このAPIには低レイテンシとスケーラビリティの2つの要件がありました。 オンボーディング時に使用されるAPIのため遅延があるとユーザー離脱につながります。またプロフィール画像を変更するたびに診断が実行されるためユーザー増加に伴いスケーラビリティも必要です。前述の通りTritonはC++実装やDynamic Batchingにより低レイテンシと高スループットを実現でき、これらの要件に適していると判断しました。 さらにTritonは社内での導入実績がありませんでした。既存の本番サービス(ZOZOTOWN、WEAR等)は利用者数が多く、問題発生時の影響範囲が大きくなります。一方ZOZOマッチはサービス立ち上げ段階であり、ZOZOTOWNやWEARなどの既存サービスと比較するとユーザー数が少ないです。また新規構築のため既存システムとの依存関係も少なく、新技術の検証に適していると判断しました。 以降ではジャンル診断APIのシステム構成とTritonを利用した推論API開発時に遭遇した課題について説明します。 システム構成 全体構成 本システムではAPIサーバーとTritonを分離する構成を採用しました。前述の「 処理の分離による保守性向上 」で説明した通り、この構成により責務の明確化とボトルネックに応じた柔軟なスケーリングが可能になります。 リクエストの流れ 次の図にリクエストの流れを示します。 ユーザーが自分の全身画像をプロフィール画像として登録するとBackend Serverがその画像データをAPIサーバーに送信します。APIサーバーはリサイズと正規化の前処理を行いgRPCプロトコルでTritonに推論リクエストを送信します。Tritonは12種類のファッションジャンルそれぞれに対して0〜1のスコアを返します。スコアが高いほどそのジャンルに該当する可能性が高いことを示します。以下はレスポンス例です。 { " genres ": [ { " genre_id ": 1 , " score ": 0.045 } , { " genre_id ": 2 , " score ": 0.696 } , ... { " genre_id ": 12 , " score ": 0.333 } ] } APIサーバーがこの診断結果をBackend Serverに返し、最終的にユーザーへレスポンスが返却されます。なおオンボーディング時に表示される選択肢画像についてはバッチ処理で事前にスコアを算出済みのためリアルタイムでの推論は不要です。リアルタイム推論が必要なのはユーザーがプロフィール画像を登録・変更した場合のみです。 通信プロトコルの選定 上記の通りAPIサーバーとTriton間の通信にはgRPCを採用しています。ここではTritonがサポートするプロトコルとgRPCを選定した理由を説明します。 Tritonがサポートするプロトコル TritonはHTTPとgRPCの2種類のプロトコルで推論リクエストを受け付けます。HTTPはポート8000、gRPCはポート8001で公開されます。どちらのプロトコルも同じ推論機能を提供しますがパフォーマンス特性が異なります。 HTTPとgRPCの比較 TritonのHTTPエンドポイントはHTTP/1.1ベースで、curlや各種HTTPライブラリで簡単にリクエストでき、ブラウザからも直接アクセスできるため広く利用されています。一方gRPCはHTTP/2ベースでサーバー間の内部通信に適していますが、ブラウザAPIでサポートされていないためブラウザから直接呼び出すことができません。しかし本システムではAPIサーバーからTritonへのサーバー間通信のみを対象としておりブラウザからのアクセスは不要なため、この制約は問題になりません。 gRPCがサーバー間通信に適している理由はHTTP/2による多重化です。HTTP/1.1では1つのTCP接続で同時に1つのリクエストしか処理できないため、高頻度のリクエストでは接続のオーバーヘッドが課題となります。HTTP/2では単一の接続上で複数のリクエストを同時に処理できます。本システムではAPIサーバーからTritonへ頻繁にリクエストを送信するためこの多重化の特性が有効です。 一方でgRPCを採用する際の注意点があります。HTTP/2の長時間接続がKubernetesの負荷分散と相性が悪いという問題です。この課題と解決策については後述の「 課題1:gRPCで負荷分散されない 」で説明します。 Kubernetesリソースの構成 MLOpsブロックではパブリッククラウドにGoogle Cloudを使用し Google Kubernetes Engine (以下、GKE)上にサービスを構築しています。ここではTritonをGKE上でデプロイするために作成したKubernetesリソースを解説します。TritonをGKE上で動かすために以下のリソースを作成しました。 Deployment :Triton Podの稼働 Service :Triton Podへのネットワークアクセスの提供 ServiceAccount :GCSアクセスのための認証設定 ScaledObject :KEDAによるオートスケーリングの設定 Tritonコンテナの起動設定 TritonをデプロイするためのDeployment構成を解説します。まず全体のマニフェストを示し、その後に各設定の詳細を説明します。 Deploymentマニフェスト apiVersion : apps/v1 kind : Deployment metadata : name : triton-inference-server labels : app : triton-inference-server spec : selector : matchLabels : app : triton-inference-server template : metadata : labels : app : triton-inference-server spec : serviceAccountName : triton-inference-server affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - "<triton-nodepool-name>" tolerations : - key : "dedicated" operator : "Equal" value : "<triton-nodepool-name>" effect : "NoSchedule" initContainers : - name : gcloud image : gcr.io/google.com/cloudsdktool/cloud-sdk:418.0.0 env : - name : MODEL_REPOSITORY_PATH value : gs://your-bucket/model_repository - name : MODEL_DIR value : "/tmp/models/" volumeMounts : - name : models mountPath : "/tmp/models/" command : - "/bin/sh" - "-c" - "gsutil cp -r $(MODEL_REPOSITORY_PATH)/* $(MODEL_DIR)" containers : - name : triton-inference-server image : "nvcr.io/nvidia/tritonserver:25.03-py3" imagePullPolicy : IfNotPresent resources : limits : cpu : 3000m memory : 3Gi command : [ "tritonserver" ] args : - "--model-store=/tmp/models/" - "--trace-config" - "mode=opentelemetry" - "--trace-config" - "opentelemetry,url=http://$(DD_AGENT_HOST):4318/v1/traces" - "--trace-config" - "rate=1" - "--trace-config" - "level=TIMESTAMPS" - "--grpc-max-connection-age=30000" - "--grpc-max-connection-age-grace=10000" env : - name : DD_AGENT_HOST valueFrom : fieldRef : fieldPath : status.hostIP volumeMounts : - name : models mountPath : "/tmp/models/" ports : - containerPort : 8000 name : http - containerPort : 8001 name : grpc livenessProbe : initialDelaySeconds : 10 periodSeconds : 5 failureThreshold : 6 httpGet : path : /v2/health/live port : http readinessProbe : initialDelaySeconds : 10 periodSeconds : 5 failureThreshold : 6 httpGet : path : /v2/health/ready port : http volumes : - name : models emptyDir : {} securityContext : runAsUser : 1000 fsGroup : 1000 以下、マニフェストの各設定について詳しく解説します。 InitコンテナとVolume Mountによるモデル共有 本システムでは Initコンテナ でGCSからMLモデルをダウンロードしています。InitコンテナはPod内でアプリケーションコンテナの前に実行されるコンテナで、マニフェストの initContainers フィールドで定義します。ダウンロード先のディレクトリをTritonコンテナでもVolume MountすることでダウンロードしたMLモデルをTritonから参照できる仕組みです。 initContainers : - name : gcloud image : gcr.io/google.com/cloudsdktool/cloud-sdk:418.0.0 env : - name : MODEL_REPOSITORY_PATH value : gs://your-bucket/model_repository - name : MODEL_DIR value : "/tmp/models/" volumeMounts : - name : models mountPath : "/tmp/models/" command : - "/bin/sh" - "-c" - "gsutil cp -r $(MODEL_REPOSITORY_PATH)/* $(MODEL_DIR)" containers : - name : triton-inference-server volumeMounts : - name : models mountPath : "/tmp/models/" volumes : - name : models emptyDir : {} emptyDir はPod起動時に作成される一時ボリュームで同一Pod内のコンテナ間でデータを共有できます。 Triton Pod起動時の処理の流れは次の通りです。 Initコンテナが起動し、GCSからモデルリポジトリ全体を /tmp/models/ にダウンロード Initコンテナが正常終了 アプリケーションコンテナ(Triton)が起動し、同じ /tmp/models/ からモデルをロード この構成によりモデルファイルをコンテナイメージに含める必要がなく、モデル更新時もイメージの再ビルドが不要になります。 OpenTelemetryによるトレース設定 本システムではDatadog APMでモニタリングを行っています。Tritonは OpenTelemetry (以下、OTel)形式でのトレース出力に対応しています。そのためTritonコンテナ起動時に以下の引数を指定することでDatadog APMと連携できます。 args : - "--trace-config" - "mode=opentelemetry" - "--trace-config" - "opentelemetry,url=http://$(DD_AGENT_HOST):4318/v1/traces" - "--trace-config" - "rate=1" - "--trace-config" - "level=TIMESTAMPS" env : - name : DD_AGENT_HOST valueFrom : fieldRef : fieldPath : status.hostIP 各パラメータの意味は次の通りです。 パラメータ 設定値 説明 mode opentelemetry OTelのAPIを使用してトレースを出力 opentelemetry,url http://$(DD_AGENT_HOST):4318/v1/traces トレースデータの送信先 rate 1 N番目のリクエストごとにトレース。 1 は全リクエスト、 1000 なら1000件に1件(デフォルト: 1000) level TIMESTAMPS 各リクエストの実行タイムスタンプを記録 DD_AGENT_HOST には status.hostIP (NodeのIP)を指定しています。Datadog Agentの推奨構成ではUNIXドメインソケットを使用します。しかしTriton(25.03時点)はOTLP Exporter経由のトレース送信においてUNIXドメインソケットに対応しておらず、HTTPプロトコルのみに対応しています。そのためホストIPとポートを指定する構成としました。 ヘルスチェック設定 livenessProbe : httpGet : path : /v2/health/live port : http readinessProbe : httpGet : path : /v2/health/ready port : http Tritonがサポートするヘルスチェックエンドポイントを livenessProbe、readinessProbe に設定しています。 /v2/health/live はTritonがリクエストを受け取れる状態かを確認し /v2/health/ready はすべてのモデルが正常にロードされたかを確認します。 セキュリティ設定 securityContext : runAsUser : 1000 fsGroup : 1000 セキュリティの観点からコンテナは非rootユーザーで実行しています。 runAsUser でコンテナ内のプロセスを実行するユーザーIDを指定しrootでの実行を避けています。 fsGroup でマウントされたボリュームのグループ所有権を同じIDに設定することで非rootユーザーでもモデルファイルへアクセスできるようにしています。 Triton Podへの接続設定 APIサーバーからTritonに接続するためのServiceを作成します。 KubernetesのServiceは複数のPodへのアクセスを抽象化するリソースです。通常のClusterIP ServiceではServiceに仮想IP(Cluster IP)が割り当てられ、クライアントはこの単一のIPアドレスにアクセスします。コンテナからの通信は kube-proxy によりDNATされ背後のPodにロードバランシングされます。 一方 Headless Service は clusterIP: None を指定したServiceであり仮想IPが割り当てられません。DNS名前解決ではServiceに紐づくすべてのPodのIPアドレスが直接返されます。これによりクライアント側で接続先のPodを選択できます。 本システムではHeadless Serviceを使用しています。通常のClusterIP Serviceを使用しない理由はgRPCの負荷分散に関係しています。詳細は後述の「 課題1:gRPCで負荷分散されない 」で説明します。 以下がServiceのマニフェストです。 apiVersion : v1 kind : Service metadata : name : triton-inference-server labels : app : triton-inference-server spec : type : ClusterIP clusterIP : None ports : - port : 8000 targetPort : http name : http-inference-server - port : 8001 targetPort : grpc name : grpc-inference-server selector : app : triton-inference-server 前述の通り8000番ポート(HTTP)はヘルスチェック用、8001番ポート(gRPC)は推論リクエスト用に公開しています。 GCSの認証設定 InitコンテナがGCSからモデルをダウンロードするにはGCSへのアクセス権限が必要です。GKEでは Workload Identity を使用することでKubernetesのServiceAccountとGCPのサービスアカウントを紐付けられます。これによりPod内からサービスアカウントキーを管理することなくGCPリソースにアクセスできます。 apiVersion : v1 kind : ServiceAccount metadata : name : triton-inference-server annotations : iam.gke.io/gcp-service-account : <gcp-service-account>@<project-id>.iam.gserviceaccount.com オートスケーリングの設定 KEDAを使用してCPU使用率に基づくオートスケーリングを設定しています。KEDAはKubernetes Event-driven Autoscalingの略で様々なメトリクスに基づいてPod数を自動調整できます。 KEDAについては下記の記事でも触れられていますのでぜひこちらもご参照ください。 techblog.zozo.com apiVersion : keda.sh/v1alpha1 kind : ScaledObject metadata : name : triton-inference-server spec : scaleTargetRef : name : triton-inference-server maxReplicaCount : 5 minReplicaCount : 2 cooldownPeriod : 300 triggers : - type : cpu metricType : Utilization metadata : value : "50" この設定ではCPU使用率50%を超えるとスケールアウトし、最小2台〜最大5台の範囲でPod数を調整します。最小2台としているのは可用性を担保するためです。 モデルのディレクトリ構成 Tritonでモデルを管理するには所定のディレクトリ構成に従う必要があります。本システムではその制約に従い、GCS上のモデルリポジトリを次のような構成にしました。 model_repository/ └── genre_extract_torchscript/ # モデル名(config.pbtxtのnameを指定する場合は一致させる) ├── config.pbtxt # モデルの入出力形式やバッチサイズなどを定義 ├── 1/ # モデルのバージョン │ └── model.pt └── 2/ # モデルのバージョン └── model.pt ディレクトリ名がそのままモデル名として使われます。今回は genre_extract_torchscript です。config.pbtxtで name フィールドを指定する場合はディレクトリ名と一致させる必要があります。 1/ や 2/ はバージョンを示すディレクトリでバージョン番号をディレクトリ名として使用します。この構成により複数バージョンのモデルを同時に管理でき、バージョン切り替えも容易になります。 実際にロードするバージョンはconfig.pbtxtのversion policyで制御でき、デフォルトでは最新バージョンのみがロードされます。config.pbtxtの詳細は「 モデルの設定 」で説明します。 モデルの設定 Tritonではモデルごとに config.pbtxt を用意します。config.pbtxtではモデルの入出力形式やバッチサイズなどを定義します。ジャンル診断モデルで使用しているconfig.pbtxtを以下に示します。 name: "genre_extract_torchscript" platform: "pytorch_libtorch" max_batch_size: 1 input [ { name: "image_input" data_type: TYPE_FP32 format: FORMAT_NCHW dims: [ 3, 224, 224 ] } ] output [ { name: "genre_scores" data_type: TYPE_FP32 dims: [ 12 ] } ] この設定では224×224ピクセルのRGB画像を入力として受け取り、12次元のベクトルを出力します。本システムでは12種類のファッションジャンルに対するスコアに対応しています。 platform フィールドの pytorch_libtorch はTorchScript形式のPyTorchモデルを使用することを示しています。 input[].format フィールドのNCHW形式とは画像データの配列順序を表しており、N(バッチサイズ)、C(チャンネル数=3)、H(高さ=224)、W(幅=224)の順にデータが並びます。 前述の「 推論処理の高速化 」で説明したDynamic Batchingを有効にするには以下のように設定します。 dynamic_batching { max_queue_delay_microseconds: 100 } max_queue_delay_microseconds はバッチを形成するためにリクエストを待機する最大時間をマイクロ秒で指定します。この待機時間内に届いたリクエストをまとめて1回の推論で処理します。 本システムでは max_batch_size: 1 としてDynamic Batchingを無効にしています。リリース直後でリクエスト数が少なくバッチを形成するための待機時間がレイテンシ悪化につながるためです。リクエスト数が増加した場合は max_batch_size を増やし dynamic_batching を有効にすることでスループットを向上できます。 GPUで推論する場合は instance_group で kind: KIND_GPU を指定します。 instance_group [ { kind: KIND_GPU count: 2 } ] count はGPUごとに起動するモデルインスタンス数を指定します。本システムではCPUで推論しています。リリース直後はリクエスト数が少なくCPUでも十分な処理性能を確保できるためです。GPUはCPUと比べてコストが高いため、リクエスト数の増加によりCPUでの処理が追いつかなくなった段階でGPUへの移行を検討します。 推論ゲートウェイAPIの実装 推論ゲートウェイAPIはBackend Serverからのリクエストを受け取り推論結果を返すAPIです。本システムでは tritonclient ライブラリのgRPCクライアントを使用しています。 Triton接続の設定 tritonclientで推論リクエストを送信する際の設定項目は以下の通りです。 設定項目 必須/任意 説明 接続先URL 必須 Tritonサーバーのホストとポート。gRPCのデフォルトポートは8001 モデル名 必須 config.pbtxtの name フィールドと一致させる モデルバージョン 任意 使用するモデルのバージョン番号。省略時は最新バージョンを使用 タイムアウト 任意 リクエストのタイムアウト時間 gRPCチャンネル設定 任意 gRPCチャンネルに渡す引数 本システムではこれらの設定を以下のように定義しています。 # 接続先URL: KubernetesのHeadless Service名とgRPCポートを指定 triton_url = "triton-inference-server.match-genre-extract.svc.cluster.local:8001" # モデル名: config.pbtxtのnameフィールドと一致させる model_name = "genre_extract_torchscript" # モデルバージョン model_version = "1" # タイムアウト timeout_ms = 1000 # gRPCチャンネル設定: ロードバランシングポリシーをround_robinに設定 channel_args = [( "grpc.lb_policy_name" , "round_robin" )] channel_args でgRPCクライアントのロードバランシングポリシーを設定できます。値の指定によりTritonサーバーへの負荷分散の方法を指定できます。この設定の詳細は後述の「 課題1:gRPCで負荷分散されない 」で説明します。 StyleFeatureExtractorクラスの実装 Tritonへの推論リクエストを行うクラスを実装します。なおコード例では入出力名やデータ型といったモデル依存の設定値をハードコーディングしていますが、実際の運用では環境変数から取得しています。 import numpy as np import tritonclient.grpc as grpcclient from opentelemetry.propagate import inject from tritonclient.grpc import InferResult from api.env_settings import settings class StyleFeatureExtractor : _INPUT_NAME = "image_input" _OUTPUT_NAME = "genre_scores" _DATA_TYPE_FP32 = "FP32" def __init__ (self): self.triton_client = grpcclient.InferenceServerClient( url=settings.triton_url, channel_args=settings.channel_args ) def predict_score_and_vector (self, image_data: np.ndarray) -> list [MLGenreScore]: trace_headers = {} inject(trace_headers) inputs = [ grpcclient.InferInput( self._INPUT_NAME, image_data.shape, self._DATA_TYPE_FP32 ) ] inputs[ 0 ].set_data_from_numpy(image_data) outputs = [grpcclient.InferRequestedOutput(self._OUTPUT_NAME)] response: InferResult = self.triton_client.infer( model_name=settings.model_name, inputs=inputs, outputs=outputs, model_version=settings.triton_model_version, headers=trace_headers, timeout=settings.triton_timeout_ms, ) scores = response.as_numpy(self._OUTPUT_NAME) _INPUT_NAME と _OUTPUT_NAME は前述の config.pbtxt で定義した入出力名と一致させます。 __init__ では前述の設定値を使って InferenceServerClient を初期化します。 predict_score_and_vector メソッドではW3C Trace Contextヘッダーを準備しています。これはDatadog APMとの連携に使用するもので、詳細は「 課題2-Datadog APMでトレースが連携されない 」で説明します。 推論リクエスト処理の実装 前述の StyleFeatureExtractor を使用して推論します。まず前処理としてダウンロードした画像をモデルの入力形式に変換します。tritonclientは入力データとしてnumpy配列を受け取るため、PyTorchテンソルから変換して渡します。 # 前処理 PREDICT_IMAGE_TRANSFORM = transforms.Compose([ transforms.Resize(( 224 , 224 )), transforms.ToTensor(), transforms.Normalize(mean=[ 0.485 , 0.456 , 0.406 ], std=[ 0.229 , 0.224 , 0.225 ]), ]) img_tensor = PREDICT_IMAGE_TRANSFORM(img) image_data = img_tensor.unsqueeze( 0 ).numpy() # 推論リクエスト ml_genre_scores = STYLE_FEATURE_EXTRACTOR.predict_score_and_vector(image_data) # 後処理... transforms.Resize で224×224ピクセルにリサイズし、 transforms.ToTensor でPyTorchテンソルに変換します。 transforms.Normalize ではImageNetデータセットの平均と標準偏差で正規化しています。最後にtritonclient用として unsqueeze(0) でバッチ次元を追加し numpy() でnumpy配列へ変換します。 推論リクエストでは前述の StyleFeatureExtractor の predict_score_and_vector メソッドを呼び出してTritonに送信します。 後処理では、Tritonから返却された12種類のファッションジャンルに対するスコアを、サービス要件に合わせたジャンルIDへマッピングして診断結果を生成します。詳細は本記事のスコープ外のため省略します。 以上が推論ゲートウェイAPIの実装です。 導入時の課題と解決策 ここまでTritonの導入方法を説明しましたが、導入にあたりいくつかの課題に直面しました。本節ではそれらの課題とその解決策を説明します。 課題1:gRPCで負荷分散されない Tritonサーバーでデプロイした推論APIの性能を測るために実施した負荷試験においてTriton Podを1台から複数台構成にしても処理できるリクエスト数がほとんど増加しませんでした。Pod数を増やして、適切に負荷分散がされれば、処理できるリクエスト数は増加します。 調査の結果、負荷が均等に分散されておらず、特定のPodのCPU使用率が100%近くに達する一方で他のPodはほとんど使用されていないことが判明しました。 原因 この問題はgRPCの特性およびKubernetesの負荷分散の仕組みに起因していました。 gRPCはHTTP/2上に構築されたL7プロトコルです。HTTP/1.1では1つのTCPコネクション上で同時に1つのリクエストしか処理できないため複数の並行リクエストには複数のTCPコネクションが必要です。一方HTTP/2では単一の長寿命なTCPコネクションを維持しその上で複数のリクエストを多重化します。 しかしこの特性はKubernetesの負荷分散との相性に問題があります。KubernetesにおいてService経由のトラフィックの負荷分散を担うのはkube-proxyです。kube-proxyはiptablesやIPVSを使用してTCPコネクション単位で負荷分散を行いコネクションが確立されるタイミングで振り分け先のPodを決定します。ただしkube-proxyはL4での負荷分散を行うため、L7プロトコルであるgRPCの個々のリクエストを認識できません。HTTP/2では単一のTCPコネクションが長時間維持されるためそのコネクション上のすべてのgRPCリクエストが最初の振り分け先Podへ集中してしまいます。 次の図に問題のある構成を示します。クライアントからのTCPコネクションが1つのPodに集中し他のPodはアイドル状態になっています。 解決策 gRPCの負荷分散の問題に対しては主に2つの解決策があります。 1つ目は Linkerd や Istio などのサービスメッシュの導入です。各Podにサイドカープロキシを注入してL7で負荷分散を行うためアプリケーションコードを変更せずにgRPC負荷分散を実現できます。一方で各Podにサイドカーが必要となりCPUやメモリを消費します。またコントロールプレーンの運用も必要になります。 2つ目はクライアントサイド負荷分散です。gRPCクライアントの設定でHeadless ServiceとDNSベースの負荷分散を利用します。サイドカーを経由しないため高いパフォーマンスを実現できます。一方でクライアント側での設定変更が必要であり負荷分散アルゴリズムはround_robinなど基本的なものに限定されます。 本システムではクライアントサイド負荷分散を採用しました。追加コンポーネントが不要で既存のKubernetesリソースとgRPCクライアントの設定変更のみで実現できるためです。また本システムではAPIサーバーからTritonへの内部通信のみが対象でありクライアント側の設定変更が可能です。推論処理の所要時間がほぼ一定であるためround_robinによる均等分散で十分な負荷分散の効果が得られると判断しました。具体的には次の2つの変更をしました。 1. Headless Serviceの採用 通常のClusterIP Serviceの代わりにHeadless Service( clusterIP: None )を使用しました。Headless ServiceはDNSクエリに対してPodのIPアドレスリストを直接返すためクライアント側で接続先を制御できます。 2. クライアント側でround_robinロードバランシングを設定 gRPCクライアントの設定で round_robin ロードバランシングポリシーを指定しました。これによりDNSで取得した複数のPod IPに対してリクエストを均等に分散できます。 tritonclient ライブラリでは InferenceServerClient の channel_args パラメータでgRPCチャンネルオプションを設定できます。 channel_args: list [ tuple [ str , str ]] = [( "grpc.lb_policy_name" , "round_robin" )] self.triton_client = grpcclient.InferenceServerClient( url=settings.triton_url, channel_args=settings.channel_args # round_robinを設定 ) grpc.lb_policy_name を round_robin に設定することでHeadless ServiceのDNSが返す複数のPod IPに対してリクエストごとに接続先を切り替えます。 次の図に解決後の構成を示します。クライアントがDNSから取得した複数のPod IPに対してround_robinでリクエストを分散しています。 スケールアウト時の負荷分散 上記のクライアントサイド負荷分散により起動時に存在するTriton Pod間での負荷分散は実現できました。しかし HPA などによるスケールアウトで新しいPodを追加した場合、そのPodへトラフィックを分散できない問題が残りました。 gRPCクライアントはすべてのSubchannelが切断された場合のみDNS名を再解決する仕様になっています。本システムではAPIサーバー起動時にTritonクライアントを生成しアプリケーション動作中は同じ接続を維持します。そのため既存の接続が維持されている限りDNS再解決は行われずスケールアウトで追加された新しいPodを検出できません。 この問題に対してはサーバー側で接続が存続できる最大時間を設定することで解決しました。gRPCは接続がクローズされるとDNS名を再解決するためサーバー側で定期的に接続を切断することでクライアントに再接続を促し、新しいPodを含めた負荷分散を実現できます。 具体的にはTritonの起動オプションで以下を設定しました。 --grpc-max-connection-age=30000 --grpc-max-connection-age-grace=10000 grpc-max-connection-age は接続が存在できる最大時間をミリ秒で指定するパラメータです。この時間を超えるとサーバーが接続を終了します。また grpc-max-connection-age-grace は接続終了後に処理中のRPCが完了するまでの猶予時間です。 30秒という値は新しいPodがReadyになってから最大30秒で負荷分散が開始されることを意味します。HPAによるスケールアウト自体に数分かかることを考えると30秒の遅延は許容範囲です。10秒という猶予時間は本システムの推論処理が非機能要件で1秒以内と定義されているため処理中のRPCを完了させるのに十分な時間です。 これらの変更によりPod数の増加に応じて線形にスループットが向上するようになりました。 課題2:Datadog APMでトレースが連携されない APIサーバーとTritonを分離した構成を導入したところDatadog APMでAPIサーバーとTritonのトレースを連携できない問題が発生しました。従来の一体型構成では1本のトレースとして追跡できていましたが、分離した構成では別々のトレースとして表示され同一リクエストの処理として紐付けられませんでした。障害調査時にエンドツーエンドでレイテンシを分析するためにはトレースの連携が必要でした。 原因 この問題はトレースコンテキストの伝播形式の違いに起因していました。 分散トレーシングではサービス間でトレース情報をHTTPヘッダーにより伝播し、複数のサービスを横断するリクエストを1本のトレースとして追跡します。しかしAPIサーバーとTritonでは伝播形式が異なっていました。 APIサーバー側:Datadog独自形式( x-datadog-* ヘッダー) Triton側: OpenTelemetry 形式のみサポート 各トレーシングツールで独自のヘッダー形式を使用していたため、サービス間でトレース情報が正しく伝播されず、トレースの分断が発生していました。 次の図は Monitor OpenTelemetry with Datadog and W3C Trace Context から引用した異なるトレーサーが混在する環境での問題を示しています。OTel TracerとDD Tracerがそれぞれ独自形式のTrace IDを使用するため各サービスのSpanがDatadogに送信されても同一トレースとして紐付けられません。図右側のAPM FlamegraphではDD Tracerを使用するSERVICE BのSpanのみが表示され、OTel Tracerを使用するSERVICE AとCは別トレース扱いになっています。本システムでも同様の問題が発生していました。 解決策 この問題を解決するために次の3つの対応をしました。 W3C Trace Contextによるトレース伝播 Triton専用NodeにDatadog AgentをDaemonSetで配置 Tritonの起動オプションでトレース設定 1. W3C Trace Contextによるトレース伝播 W3C Trace Contextは異なるトレーシングシステム間でトレース情報を共有するための標準規格です。HTTPヘッダー( traceparent 、 tracestate )を通じてトレースIDを伝播します。 APIサーバーからTritonへのリクエスト時にOTelの inject 関数を使用してトレース情報をヘッダーに埋め込みます。 from opentelemetry.propagate import inject def predict_score_and_vector (self, image_data: np.ndarray): # 現在のトレースコンテキストをW3C Trace Context形式でヘッダーに埋め込む trace_headers = {} inject(trace_headers) # Tritonへの推論リクエスト時にヘッダーを付与 response = self.triton_client.infer( model_name=self.model_name, inputs=inputs, outputs=outputs, headers=trace_headers, # W3C Trace Contextヘッダー timeout=settings.triton_timeout_ms, ) inject 関数はddtraceが管理する現在のスパンのトレースIDとスパンIDをW3C Trace Context形式のヘッダーに変換します。Triton側はこのヘッダーを読み取り同じトレースIDで新しいスパンを作成します。これによりddtraceで計装されたAPIサーバーとOTelで計装されたTritonの間でトレースが連携されます。本実装ではddtrace 2.21.8を使用しています 1 。 2. Triton専用NodeにDatadog AgentをDaemonSetで配置 TritonはOTel形式でトレースを出力するためOpenTelemetry Protocol(以下、OTLP)形式を受け取れるDatadog Agentが必要です。 KubernetesにおけるDaemonSetはクラスタ内の各NodeでPodが1つずつ稼働することを保証するリソースです。ログ収集やモニタリングエージェントなど各Nodeで実行が必要なシステム機能のデプロイに適しています。次の図にNode内でのトレース送信の流れを示します。 OTelで計装されたTritonからのトレースは同一Node上のDatadog AgentがOTLP形式で受け取りDatadog Backendに送信します。Tritonが稼働するNodeにDaemonSetでDatadog Agentを配置しport 4318でOTLPリクエストを受け取るよう設定しました。以下はその設定例です。 apiVersion : apps/v1 kind : DaemonSet metadata : name : triton-datadog-agent spec : template : spec : affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - <triton-nodepool-name> containers : - name : datadog-agent image : datadog/agent:7.56.1 env : - name : DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT value : "0.0.0.0:4318" ports : - containerPort : 4318 hostPort : 4318 name : traceporthttp protocol : TCP なお hostPort を使用するとNodeのポートが外部に公開されクラスタへのネットワーク侵入経路となる可能性があるため絶対に必要な場合を除き避けることが推奨されています。本記事の構成ではTritonが稼働するNode上でのみDatadog Agentと通信するために使用しておりプライベートなGKEクラスタ内での利用を想定しています。パブリックなクラスタで利用する場合はファイアウォールルールやネットワークポリシーで適切にアクセス制限を行ってください。 3. Tritonの起動オプションでトレース設定 Triton側では起動オプションでOTelによるトレース出力を有効にしました。 --trace-config level=TIMESTAMPS --trace-config rate=1 --trace-config mode=opentelemetry --trace-config opentelemetry,url=http://$(DD_AGENT_HOST):4318/v1/traces これらの設定によりAPIサーバーからTritonへの流れが1本のトレースとして表示され、スパンごとのボトルネック分析が可能になりました。次の図は本システムで実際にDatadog APMに表示されたトレースです。 APIサーバーとTritonのSpanが1本のトレースとして連携されていることがわかります。 課題3:PyTorchモデルがロードされない Tritonを起動したところPyTorchモデルのロードに失敗してTritonが起動しませんでした。モデルファイルは正しいパスに配置されているにもかかわらず以下のようなエラーが出力されました。 UNAVAILABLE: Internal: failed to load model 'genre_extract_torchscript': PytorchStreamReader failed locating file constants.pkl: file not found このエラーは、通常のPyTorchモデルを、TorchScript形式のモデルとして読み込もうとした場合に発生します。 原因 TritonのPyTorch BackendはTorchScript形式のモデルのみをサポートしています。配置していたモデルはTorchScript形式ではなかったためロードできませんでした。 解決策 PyTorch Backend ではすべてのPyTorchモデルをTorchScript形式に変換する必要があります。本システムでは torch.jit.script を使用してモデルを変換しました。 # Before: PyTorchモデルの重みをそのまま保存 torch.save(model.state_dict(), 'best_model.pt' ) # After: TorchScript形式で保存 jit_script = torch.jit.script(model).eval() jit_script.save(f "{model_dir}/best_model_torchscript.pt" ) この変更によりモデルが正常にロードされました。 導入効果 本節では本番運用を通じて実際に確認できた効果を紹介します。 性能面では同じ総Pod数で目標レイテンシ以内に処理可能なリクエスト数が約38%向上しました。Triton導入前はAPI Pod 4台(CPU)で24rpsが上限でした。導入後はAPI Pod 3台とTriton Pod 1台(計4台、いずれもCPU)で33rpsまで処理できるようになりました。 運用面では config.pbtxt による宣言的な設定管理により設定変更が容易になりました。 項目 Before(Triton導入前) After(Triton導入後) バッチサイズ設定 コード修正が必要 max_batch_size で設定 推論インスタンス数設定 ワーカー数やPod数を調整 instance_group で設定 Dynamic Batching設定 自前実装が必要 dynamic_batching で設定 モデル入出力定義 コード内で暗黙的に定義 input / output で設定 モデルバージョン管理 独自の命名規則で運用 ディレクトリ構造で標準化 またTriton公式のPerf Analyzerにより前処理・後処理の影響を排除した推論単体の性能検証が可能になりました。 スケーラビリティの面ではジャンル診断APIの低レイテンシとスケーラビリティの要件に備えた体制が整いました。APIサーバーとTritonで責務を分離したことで水平スケールや垂直スケールをそれぞれ独立して適用できます。推論がボトルネックになればTritonにGPUを割り当て、前処理がボトルネックになればAPIサーバーのCPUを増強するといった柔軟なリソース配分が可能です。Dynamic Batchingの有効化も設定変更だけで対応できます。 本番稼働から半年以上が経過し大きな障害なく安定稼働しています。社内でもTriton導入を検討するチームがあり本番運用の実績とナレッジを共有できる状態になりました。 導入時の注意点 コスト面の考慮 Tritonを導入する際はコスト面での検討が重要です。リクエスト数が少ない場合、逆にコスト増となる可能性があります。 MLOpsブロックではAPI系サービスの可用性担保のためAPIサーバーを最小3台構成としており、導入前は3台で運用していました。導入後はAPIサーバー3台に加えて可用性担保のためTriton Pod 2台が必要になり合計5台構成となりました。同スペックのPodを使用しているため単純計算で約67%のコスト増です。 一方前述の性能検証で示した通りTriton導入により同Pod数でスループットが約38%向上しています。つまりAPIサーバーのみで同等のスループットを得るには5台以上のPodが必要となる計算です。リクエスト数の増加でスケールアウトが必要な場合、Triton構成はコスト効率が良くなります。 現時点ではサービス開始直後でリクエスト数が少なく最小構成での運用となっているためコスト削減効果があるとは言えません。ただし運用・保守性の向上というメリットは得られています。またリクエスト増加時には同Pod数でスループットが約38%向上している点に加えGPUの活用やDynamic Batchingの有効化によるさらなるコスト効率の改善が見込まれます。 まとめ 本記事ではZOZOマッチのジャンル診断APIにTritonを導入した背景から課題と解決策、導入効果までを紹介しました。 社内初の導入で様々な課題に直面しましたがそれぞれ解決できました。 結果として同Pod数でスループットが約38%向上しconfig.pbtxtによる宣言的管理で運用性が向上しました。またAPIサーバーとTritonで役割を分担することで責務が明確になりました。 導入前は社内での運用実績がなく、トラブル発生時の対応やナレッジ不足を懸念していました。しかし本番稼働から半年以上が経過し大きな障害なく安定稼働しています。社内でもTriton導入を検討するチームがあり本番運用の実績とナレッジを共有できる状態になりました。 本記事で紹介した導入時の知見がこれからTritonの導入を検討している方の参考になれば幸いです。 最後になりますがZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用を行っているためご興味ある方は以下のリンクからぜひご応募ください。 hrmos.co 将来のバージョンではこの手動でのヘッダー埋め込みが不要になる可能性があります。詳細は GitHub Discussion を参照してください。 ↩
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } こんにちは。検索基盤部の橘です。検索基盤部では、ZOZOTOWN検索の品質向上を目指し、検索結果の改善に取り組んでいます。 ZOZOTOWN検索の並び順の精度改善の取り組みについては以下の記事をご参照ください。 techblog.zozo.com 検索基盤部では新しい改善や機能を導入する前にA/Bテストを行い効果を評価しています。A/Bテストの事前評価として、オフラインの定量評価と定性評価を実施しています。これらの評価によりA/Bテストの実施判断をしています。 過去のフィルタリング処理の効果検証として導入したオフライン定量評価の方法については以下の記事をご参照ください。 techblog.zozo.com techblog.zozo.com 本記事では、検索基盤部が最近実践したLLMを用いた新しいオフライン定量評価の手法についてご紹介します。 目次 目次 オフライン定量評価の課題 課題解決のアプローチ LLMによる関連性判定の研究事例とTips プロンプト設計の重要性 マルチモーダル情報の活用 品質保証とモニタリング LLMと人間の判定傾向の違い MLLMを用いて定量評価を実施するまでのフロー ステップ1:関連性の判定基準の改善とゴールドセット作成 ステップ2:ゴールドセットによるラベリングMLLMの評価と最適化 ステップ3:ラベリングMLLMを用いた検索ロジックの定量評価 関連性の判定基準の改善とゴールドセット作成 実施した改善詳細 策定された判定基準の例 デザイン・感性・審美的要素の「程度」の主観的判断に関する基準 Highly relevant Acceptable Substitute Irrelevant ゴールドセットによるラベリングMLLMの評価 評価方法と指標 評価データ 評価対象 評価結果 結果の考察 MLLMモデルの性能と精度 プロンプト改善の効果 ラベリングMLLMを用いた検索ロジックの定量評価 評価フロー 評価対象クエリの選定 検索ロジックの実行 ラベリングMLLMによる関連性判定 評価指標の算出 nDCG@K(normalized Discounted Cumulative Gain) Precision@K ロジック間の性能比較 ラベリングMLLMを用いた評価手法の利点 バイアスの排除 スケーラビリティ まとめ オフライン定量評価の課題 ZOZOTOWN検索の精度評価への取り組み では、検索結果ログを用いた定量評価の手法を紹介しました。ZOZOTOWN検索のオフライン定量評価は、『フィルタリング(絞り込み)ロジック』と『ランキング(並び順)ロジック』を対象としています。 検索結果ログを用いた評価は、ランキングロジックでは有効ですが、フィルタリングロジックでは課題があります。検索結果ログは既存のcontrolロジックの結果にバイアスがかかっているため、treatmentロジックには不利な評価になる傾向があります。上記のテックブログで紹介した手法でも、treatmentロジックがcontrolロジックと同等である可能性は評価できますが、優れているとまでは判断できませんでした。 課題解決のアプローチ 検索クエリとヒットした商品の関連性を判定するアプローチとして、一般的に以下が存在します。 検索結果ログによる判定 :検索クエリとヒットした商品のコンバージョンにより判定 人手による判定 :人間が目視で関連性を評価 LLMによる判定 :大規模言語モデルが関連性を自動判定 近年、eコマースや情報検索の分野では、従来は人手で行っていた関連性判定をLLMに代替させる取り組みが進んでいます。 LLMを利用して関連性を判定するメリットは他の手法に比べて以下があります。 大量データの処理が可能 :人手による判定と比較して、数千から数万件のデータを短時間で処理できる 一貫性のある判定基準 :人によるばらつきを抑え、定義された基準に基づいて一貫した判定が可能である コスト効率の良さ :人手によるアノテーション作業と比較して、コストと時間を大幅に削減できる 柔軟な基準適用 :プロンプトの調整により、複雑な判定基準を容易に適用・変更できる バイアスの軽減 :検索結果ログに含まれる既存ロジックへのバイアスを避けられる ただし、以下のような懸念点も想定されます。 判定精度の不確実性 :人間による判定と同様、LLMの判定精度も100%には達しておらず、誤判定を含む可能性がある ドメイン知識の限界 :ファッションECサイト特有の用語や文脈について、正確に理解できない可能性がある プロンプト依存性 :プロンプトの書き方次第で判定結果は大きく変動する可能性がある これらの懸念点に適切に対処するため、既存の研究事例から得られた知見を参考にしました。 LLMによる関連性判定の研究事例とTips LLMを用いた検索システムの関連性評価は、近年のeコマースや情報検索の分野で活発に研究されています。本取り組みでは、特に以下の研究事例から得られた知見を参考にしました。 Retrieve, Annotate, Evaluate, Repeat: Leveraging Multimodal LLMs for Large-Scale Product Retrieval Evaluation (Zalando, 2024):画像とテキストのマルチモーダルLLMを使用して、大規模な商品検索の関連性評価を自動化した実践研究 Large Language Models can Accurately Predict Searcher Preferences (Microsoft, SIGIR 2024):検索システム評価に必要な関連性ラベルを、LLMで高精度かつ大規模に生成できることを実証した研究 Automated Query-Product Relevance Labeling using Large Language Models for E-commerce Search (Walmart, 2024):Eコマースサイトにおけるクエリと商品の関連性ラベリングをLLMで自動化した実践研究 これらの研究から、LLMによる関連性判定を実用化する上で重要となるTipsを以下にいくつか共有します。 プロンプト設計の重要性 LLMによる判定精度は、プロンプトの設計に大きく依存します。Microsoftの研究では、プロンプトを以下の5つのパートに分けて構成することが効果的であると示されています。 Role(役割) :「あなたは検索結果の評価者です」と役割を明示し、判定基準の定義を提示する Description(クエリの要約) :クエリの意図を簡潔に要約する Narrative(判定基準の詳細) :関連あり/なしの境界条件や、迷った時の優先順位を明示する Aspects(評価観点) :判定を複数の観点に分解して段階的に評価させる。例えば、topicality(クエリとの話題一致度)とtrust(情報の信頼性・整合性)を個別に評価させた上で、overall(最終判定)を出力させる Output(出力形式) :JSONなど構造化された形式で出力させ、自動処理を容易にする 特に、 Aspects(評価観点)を明示的に設計する ことで精度向上に大きく寄与します。例えば、topicality(クエリとの話題一致度)とtrust(情報の信頼性・整合性)を個別に評価させた上で、overall(最終判定)を出力させます。この構成により、判断の安定性と精度の向上を実現できると示されています。 同研究では、プロンプト要素の組み合わせを評価した結果、 Description + Narrative + Aspects(DNA)の構成が最も高い精度 を示しました。また、プロンプトの表現によっても精度が変動するため、少量のゴールドラベルに基づいてプロンプトを最適化することが推奨されています。 マルチモーダル情報の活用 ファッションECサイトにおいては、商品の視覚的特徴(色、形状、デザインなど)に関連性判定で重要な役割を果たします。Zalandoの研究では、 商品のテキスト情報と画像を同時に入力できるマルチモーダルLLM を使用することで、テキストのみの場合と比較して判定精度の向上を示しています。 品質保証とモニタリング LLMによる判定の精度は100%でありません。そのため、 人間による品質保証とモニタリングの重要性 の推奨がなされています。具体的に、以下のような運用方法の効果が示されています。 難例や微妙なケースは人間が判定 :LLMと人手で判定が大きく食い違うケース、主観的で曖昧なクエリなどは、人間が優先的に確認する 継続的なモニタリング :定期的にLLMのラベリング結果をサンプリングし、人手で再ラベリングして精度を監視する ゴールドセットによる検証 :少量の高品質なゴールドセット(人手で慎重にラベリングしたデータ)を作成し、LLMの判定精度を定量的に評価する LLMと人間の判定傾向の違い 関連性判定におけるLLMと人間の判定傾向には特徴的な違いのあることが知られています。 LLMのアノテーションの判断は厳しめに寄りやすく、人間の判定は甘めによる傾向がある と示されています。 具体的には、人間にブランド名やカテゴリの誤判定が多く見られる一方で、LLMには翻訳ミスや視覚的な誤認識の発生があります。この違いを理解した上で、 アノテーションのラベル設計を比較的粗めにする(3段階など) ことが一般的です。粗めのラベル設計には以下のような利点があります。 認知負荷の軽減 :判定者がラベルを選択する際の迷いを減らし、判定作業を効率化できる 曖昧な境界の削減 :細かすぎるラベル分類では判定基準が曖昧になりやすく、一貫性を保つことが困難になる デバッグの容易性 :誤判定が発生した際に、原因を特定しやすく、プロンプトの改善につなげやすい 以上の知見を踏まえ、本取り組みでは マルチモーダルLLM(MLLM) を採用し、以下のアプローチを実施しました。 ラベル設計 :MLLMと人間の判定傾向の違いを考慮し、3段階のラベル(Highly relevant / Acceptable substitute / Irrelevant)を設定(詳細は後述)。認知負荷を軽減し、曖昧な境界を減らして一貫性を確保。 マルチモーダル情報の活用 :商品のテキスト情報だけでなく画像も判定に利用。ファッションECサイト特有の視覚的要素を反映し、より人間に近い精度の関連性評価を実現。 判定基準の明確化 :関連あり/なしの境界条件や季節性、複数基準の優先順位、曖昧なクエリの扱いなど、難しいケースへの対応方針をプロンプトに具体的に記述。 評価と検証のプロセス :検索基盤部チーム作成のゴールドセットでMLLMの判定精度を検証し、最適なプロンプトとモデルを選定。大量データ処理でも判定品質を担保し、チームの判定と同等の精度を目指す。 その上で、選定したMLLMを用いて大量の評価用データを作成し、検索ロジックの定量評価を実施することにしました。 MLLMを用いて定量評価を実施するまでのフロー MLLMによる関連性判定には「判定精度の不確実性」という課題があります。MLLMの判定が100%正確ではないため、まず 信頼できるラベリングMLLMを構築すること が重要です。そのためには、MLLM自体の性能を評価し、検索基盤部チームの判定基準に沿った判定ができるかを検証する必要があります。 本取り組みでは、以下の3段階のアプローチで進めました。 ステップ1:関連性の判定基準の改善とゴールドセット作成 まず、チーム内で暗黙知となっていた判定基準を明確化し、言語化します。ペアラベリング→不一致分析→基準改善を実施することで、判定基準を体系的に整理すると同時に、高品質なゴールドセット(正解データ)を作成します。 このゴールドセットは、次のステップでMLLMの性能を評価するための基準となります。 ステップ2:ゴールドセットによるラベリングMLLMの評価と最適化 作成したゴールドセットを用いて、複数のMLLMモデルとプロンプトの組み合わせを評価します。ゴールドセットに最も近い判定結果を出せる最適な組み合わせを選定することで、検索基盤部チームの判定基準を再現できる信頼性の高いラベリングMLLMを構築します。 この検証により、大量データ処理においても判定品質を担保できることを確認します。 ステップ3:ラベリングMLLMを用いた検索ロジックの定量評価 選定したラベリングMLLMを用いて、数千から数万件のクエリ商品ペアを自動ラベリングし、検索ロジックの定量評価を実施します。従来の検索結果ログに依存した評価と異なり、既存ロジックへのバイアスを排除した公平な評価が可能になります。 以下、各ステップの詳細を説明します。 関連性の判定基準の改善とゴールドセット作成 MLLMに検索クエリと商品の関連性を判定させるためには、明確な判定基準をプロンプトに盛り込むことが重要です。しかし、当初は「関連性の判定基準」がチーム内で完全に言語化・網羅されているわけではなく、曖昧な部分も多く存在していました。 そこで本取り組みでは、 段階的な基準策定プロセス を採用しました。ペアラベリング→不一致分析→基準改善をすることで、判定基準とゴールドセットの品質を同時に高めていきました。 実施した改善詳細 以下に実施した改善の具体的な詳細を示します。 手順1:既存基準の整理 チーム内で共有されていた関連性の判定基準を洗い出し、明文化しました。 手順2:ペアラベリング 既存基準を元に2人1組で同じ検索クエリと商品ペアを独立にラベリング。 手順3:不一致分析 1,000件中293件(29.3%)で判定が不一致。不一致データから不一致原因を特定しました。 手順4:基準追加・修正 不一致原因から11個の新しい基準をチームで相談の上に作成しました。その新基準を既存基準に結合します。 その後不一致データに対して修正された基準を元にラベリングを行い、不一致を解消していきました。 策定された判定基準の例 この改善プロセスを通じて策定された基準の一例として、以下のような「デザイン・感性・審美的要素」に関する判定基準があります。 デザイン・感性・審美的要素の「程度」の主観的判断に関する基準 「デザイン性」「雰囲気」など、数値化できない主観的な要素が、どのレベルなら適合とみなすか。 Highly relevant 以下のいずれかに該当する場合、 Highly relevant と判定する。 1. テキストに明示的に含まれる 商品情報のテキスト(商品名・説明文・タグ等)において、該当キーワードが明示的に含まれている 例: デザイン → デザインニット 例: シアー → シアーシャツ 2. 同義語が含まれる 商品情報のテキストに、該当キーワードの同義語が含まれている 例: デザイン → Patterned knit (patternedということはデザインがあるということ) 3. 画像から明確に読み取れる 商品情報の画像から、該当キーワードの要素が明確かつ視覚的に読み取れる 例: シティボーイ → (シティボーイが着てそうなアイテムの画像) Acceptable Substitute 以下のいずれかに該当する場合、 Acceptable Substitute と判定する。 1. 画像から関連要素が読み取れる テキストには明示されていないが、商品情報の画像から該当キーワードに関連する要素が読み取れる 例: シティボーイ → (シティボーイ好きな人が着そうなアイテムの画像だが、テキストには記載なし) 2. 関連語や関連カテゴリ 該当キーワードそのものではないが、商品情報のテキストに関連語や関連カテゴリの特性が記載されている 例: 柄 → (ワンポイントやラインのみのアイテム) (柄とは言わないが関連するカテゴリ) 3. 周辺カテゴリに属する テキストや画像に明確な該当要素はないものの、該当キーワードの文脈で許容できる周辺カテゴリに属する 例: シアー → (透け感はないが薄手の軽やかな素材のアイテム) Irrelevant 以下の場合、 Irrelevant と判定する。 上記のHighly relevantおよびAcceptable Substituteのいずれの条件にも該当しない 商品情報のテキストや画像において、該当キーワードとの関連性が見出せない 例: 柄 → (無地のTシャツ) 例: デザイン → (シンプルな無地のTシャツで特にデザイン性がないもの) このアプローチにより、検索基盤部チームの知見を体系的な基準として言語化し、MLLMのプロンプトへの反映ができるようになりました。 また、上記の過程で得られた不一致が解消され人間によってラベリングされたデータは『ゴールドセット』としてこの後のラベリングMLLMの評価で利用しました。 ※ただし、すべての不一致を完全に解消することは難しく、一部のデータは判定が分かれたままとなりました。ゴールドセットでは、こうしたケースについては複数のラベルを付与した状態で管理しています。 ゴールドセットによるラベリングMLLMの評価 ゴールドセットを作成した後、次のステップとしてMLLMによる関連性判定の精度を評価しました。この評価の目的は、 最適なMLLMモデルとプロンプトの組み合わせを選定すること です。 評価方法と指標 MLLMによる判定結果と、ゴールドセットのラベルを比較し、精度を算出しました。具体的には、MLLMによる判定が ゴールドセットのラベルにマッチしていれば正解 、そうでなければ不正解としました。 評価データ 評価には、前述の方法で作成したゴールドセットを使用しました。ゴールドセットには検索クエリと商品の組み合わせに対して、ラベルが付与されています。 評価対象 以下のMLLMモデルとプロンプトの組み合わせを評価しました。 評価対象MLLMモデル : Gemini 2.5 Flash Gemini 2.5 Flash Lite Gemini 2.0 Flash Lite 評価対象プロンプト : 改善前プロンプト:初期に作成したラベリング基準のみを含むプロンプト 改善後プロンプト:改善プロセスで追加されたラベリング基準を統合したプロンプト 評価結果 以下に各設定における精度評価の結果を示します。 MLLMモデル プロンプト 精度 Gemini 2.5 Flash 改善後 74.1% (+18.7%) Gemini 2.5 Flash Lite 改善後 66.4% (+11.0%) Gemini 2.0 Flash Lite 改善後 61.7% (+6.3%) Gemini 2.5 Flash 改善前 73.0% (+17.6%) Gemini 2.5 Flash Lite 改善前 66.2% (+10.8%) Gemini 2.0 Flash Lite 改善前(ベースライン) 55.4% 括弧内の数値はGemini 2.0 Flash Liteの改善前プロンプトをベースラインとした際の向上率を示しています。 結果の考察 評価結果から以下の知見が得られました。 MLLMモデルの性能と精度 より高性能なMLLMモデルほど関連性判定の精度が向上する傾向を確認できました。特にGemini 2.5 Flashは 74.1%と最高精度を記録 し、実用的なラベリング精度に到達していると判断できました。 プロンプト改善の効果 改善プロセスによってプロンプトを改善した結果、全モデルで精度の向上が見られました。特にGemini 2.0 Flash Liteでは 6.3ポイントの改善 があり、基準の明確化が効果的であることを確認できました。 ただし、Gemini 2.5 Flash間では1.1ポイントと、改善幅は限定的でした。この結果については、以下のような要因が考えられます。 プロンプトに基準を追加したことで内容が複雑化し、MLLMによる意図通りの判定ができなくなったケースも存在する ゴールドセット自体のラベリングの一貫性や正確性に課題があり、MLLMの判定と正解データの必ずしも一致しない場合も存在する ラベリングMLLMを用いた検索ロジックの定量評価 前節で選定したラベリングMLLMを用いることで、検索ロジックの定量評価ができます。具体的には以下のような評価が可能です。 全文検索のロジックAとロジックBの検索結果の比較 ベクトル検索のロジックAとロジックBの検索結果の比較 本セクションでは、このラベリングMLLMをどのように活用して検索ロジックを評価するかについて説明します。 評価フロー ラベリングMLLMを用いた検索ロジック評価は、以下の流れで実施します。 この流れにより、複数の検索ロジック(例:既存の全文検索と新規のベクトル検索)を公平に比較評価できます。 評価対象クエリの選定 評価に使用するクエリセットを選定します。弊社では実際のユーザー検索ログから代表的なクエリを抽出し、評価対象とするケースが多いです。 検索ロジックの実行 評価対象となる各検索ロジックで検索し、基本的には上位N件の検索結果を取得します。複数のロジックを比較する場合、同じクエリセットで公平に評価することが重要になります。 ラベリングMLLMによる関連性判定 取得した検索結果(クエリと商品のペア)に対して、ラベリングMLLMで関連性ラベルを自動付与します。 入力 :検索クエリ、商品情報(テキスト・画像) 出力 :Highly relevant / Acceptable substitute / Irrelevant 処理方式 :バッチ処理により大量のクエリと商品のペアを効率的にラベリング なお、検索結果の上位N件をラベリングする際の「N」の値は、評価の信頼性・網羅性、MLLMの処理時間・コストに大きく影響するので、よく検討しておくことが必要です。 評価指標の算出 ラベリング結果に基づいて、以下のような評価指標を算出できます。 nDCG@K(normalized Discounted Cumulative Gain) 検索結果の上位K件における関連性の質を評価する指標です。関連性ラベルに応じてスコアを付与します。例えば、Highly relevantは3点、Acceptable substituteは1点、Irrelevantは0点とします。上位の結果ほど重要とみなして順位による割引を適用し、さらにその値を理想的な順位の場合のスコアで割って正規化します。 この指標により、「関連性の高い商品が上位に表示されているか」を定量的に測定できます。 Precision@K Precision@Kは、検索結果の上位K件のうち、関連性があると判定された商品の割合を示す指標です。検索結果の「精度」を定量的に評価できます。 関連商品の定義については、Highly relevantのみを関連商品とする場合と、Highly relevantおよびAcceptable substituteの両方を関連商品とする場合があります。どちらを採用するかは、評価の目的や運用方針に応じて検討が必要です。 ロジック間の性能比較 上記の評価指標を用いて、複数の検索ロジックを比較評価します。例として、このMLLMによる検索ロジック候補群の評価を実施した結果を示します。 ロジック Precision@5 Precision@10 Precision@20 nDCG@5 nDCG@10 nDCG@20 ロジックA 0.8623 0.8507 0.8322 0.9318 0.9322 0.9313 ロジックB 0.8510 0.8389 0.8230 0.9214 0.9222 0.9227 ロジックC 0.8798 0.8696 0.8578 0.9424 0.9416 0.9413 ロジックD 0.8928 0.8846 0.8722 0.9419 0.9431 0.9421 この結果から、ロジックDが最も高いPrecision@Kを示しており、検索精度の向上が確認できます。 ラベリングMLLMを用いた評価手法の利点 バイアスの排除 従来の検索結果ログベースの評価では、既存ロジックの結果にバイアスがかかっていました。ラベリングMLLMを用いることで、ログに依存せず、クエリと商品の本質的な関連性に基づいて評価できるようになりました。 スケーラビリティ 人手によるラベリングと比較して、数千から数万件のクエリと商品のペアを短時間で評価できます。これにより、継続的な評価や複数ロジックの比較が現実的になります。 関連性の判定基準を改善する際に実施したペアラベリングでは、100個の検索クエリと商品のペアを人手でラベリングするのに2時間ぐらいの時間が掛かります。一方、MLLMであれば1分以内でラベリング可能になりました。 まとめ 本記事では、マルチモーダルLLMを用いたZOZOTOWN検索のオフライン定量評価の手法について紹介しました。 このラベリングMLLMを活用することで、検索結果ログに依存せず、クエリと商品の本質的な関連性に基づいた公平な評価が可能になりました。今後は継続的な評価体制の構築を進め、より精度の高い検索システムの開発を推進していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
.table-of-contents > li > ul > li > ul > li { display: none; } はじめに こんにちは、ZOZOMO部FBZブロックの杉田です。普段は Fulfillment by ZOZO が提供するAPIシステムを開発・運用しています。昨年からは、社内における開発者向けAI支援ツール(Claude、Devin、MCPなど)の導入・教育・推進・管理を担う専門チームでも兼務で活動しています。 本記事では、開発ガイドライン準拠チェックをClaude Code Plugins × Atlassian MCPで全社展開した取り組みを紹介します。手作業の確認コストを下げつつ、最新ガイドラインに基づいたレビューを日常的に回せるようにした経緯と、実装・運用のポイントをまとめます。 目次 はじめに 目次 背景・課題 開発ガイドラインについて 開発ガイドラインにまつわる課題 Claude Code Pluginsとは Claude Code Pluginsを採用した理由 実施内容 開発ガイドラインの参照 プラグインの作成 plugin.json guideline-review.md プラグインの登録 プラグインの利用方法 チーム展開時の設定 プラグインの実行結果 関連ガイドラインの特定 適合状況のサマリー 詳細レポート 今後の展望 まとめ 背景・課題 開発ガイドラインについて ZOZOでは、サービス開発における「良い実装」「具体的な実装方法」「実装の目安」を明確にするため、開発ガイドラインをConfluence上で整備・管理しています。開発のガードレールとして機能し、品質の均一化と属人化の防止を目的としています。 執筆時点で、開発ガイドラインは以下のプラットフォーム・領域ごとに整備されています。 API Backend DB ML Infra Frontend iOS Android Q&A Batch Go Java Flutter 共通 各項目には RFC2119 に基づく要請レベル(MUST、RECOMMENDED、MAYなど)が設定され、「必ず従うべき項目」と「推奨事項」が明確に区別されています。また、開発ガイドラインの内容は定期的に見直され、継続的に更新されます。 開発ガイドラインの詳細については以下の記事で解説されていますので、あわせてご参照ください。 techblog.zozo.com 開発ガイドラインにまつわる課題 開発ガイドラインは整備されているものの、実際の運用では次の課題がありました。 人手による確認工数 : プラットフォーム・領域ごとに数十の項目以上のチェックが必要で、レビューの負担が大きい 継続的なチェック体制の不在 : リリース前の目視確認が中心で、更新内容の追従が難しい チーム間のばらつき : 観点の揺れが生まれ、統一した品質基準でのチェックが難しい こうした課題を解消するため、開発ガイドライン準拠チェックを自動化・標準化する仕組みづくりに取り組みました。 Claude Code Pluginsとは Claude Code Plugins は、Claude Codeの機能をチームや組織内部で再利用しやすい形にまとめる拡張機構です。公式ドキュメントに沿って、AgentsやSkillsなどをプラグイン単位で管理・共有できます。 プラグインに含められる代表的な要素は以下の通りです。 構成要素 説明 Skills 1 Claude Codeの機能を拡張する指示セット Agents 特定タスクに特化したサブエージェント Hooks ツール実行前後に介入するフック処理 さらに、プラグインごとに .mcp.json を配置することで、MCPサーバーと連携できます。外部サービス(Confluenceなど)の情報をプラグイン内から参照できる点が大きな特徴です。 プラグインは .claude-plugin/plugin.json を起点として定義され、Skillsは /plugin-name:skill-name のような名前空間付きで提供できます。そのため、開発者が複数のプラグインを導入しても衝突が発生しづらく、チームや組織内での再利用に適した形となります。 Claude Code Pluginsを採用した理由 当初は、開発ガイドラインの内容をGitHubリポジトリに保存して参照する方式を検討していました。具体的には、Confluence上で管理されている開発ガイドラインをMarkdown化し、任意のGitHubリポジトリ配下で管理し参照する方式です。しかし最終的に、Claude Code Plugins × Atlassian MCPの構成に方針を変更しました。 理由は以下の4点です。 1. 情報の鮮度を常に保てる MCPを経由してConfluenceを直接参照することで、実行時点で常に最新の開発ガイドラインを参照できます。 2. 二重管理によるメンテナンスコストを避けられる GitHubリポジトリ配下で管理する場合、Confluenceとの同期が必要になります。MCPで直接参照する方式により、二重管理を回避できました。 3. マーケットプレイス機能による全社配布の容易さ Claude Code Pluginsのマーケットプレイス機能により、各プロダクトチームへの配布が容易になりました。共通のレビュー基準をコマンド1つで導入できます。 4. プラグインの自動更新による最新化の容易さ Claude Code Pluginsはマーケットプレイスとインストール済みプラグインを起動時に自動更新する機能を備えています。また、 /plugin marketplace update marketplace-name コマンドで手動更新も可能です。これにより、プラグインの改善や機能追加を迅速に開発者に展開できます。 実施内容 開発ガイドラインの参照 Confluenceに格納されている開発ガイドラインを参照するため、 Atlassian MCP を活用しました。Atlassian MCPはリモートMCPとして提供されています。ローカルにMCPサーバーを起動せずとも、Atlassianがホストするサーバーに直接接続してConfluenceやJiraのデータにアクセスできます。 プラグイン側で .mcp.json を以下のように設定し、読み取り専用モードでConfluenceにアクセスします。 { " mcpServers ": { " atlassian ": { " command ": " npx ", " args ": [ " -y ", " mcp-remote ", " https://mcp.atlassian.com/v1/mcp " ] , " env ": { " READ_ONLY_MODE ": " true " } } } } ポイントは以下の通りです。 リモートMCP : mcp-remote を使用してAtlassian MCPサーバーに接続 READ_ONLY_MODE : 書き込み操作を無効化し、安全にConfluenceを参照できる 認証 : 初回接続時にブラウザでOAuth 2.1ベースの認証が行われ、以降は自動的に認証情報が管理される プラグインの作成 本セクションで紹介するプラグイン構成は、説明用に簡略化したサンプルです。Claude Code Pluginsの最新の機能や仕様については 公式ドキュメント をご参照ください。 開発ガイドラインレビュー用プラグインは、以下のようなディレクトリ構成で作成しました。 plugins/guideline-review/ ├── .claude-plugin/ │ └── plugin.json # プラグイン設定 ├── .mcp.json # MCP Atlassian統合設定 ├── commands/ │ └── guideline-review.md # ガイドラインレビューコマンド定義 └── README.md # プラグイン詳細ドキュメント plugin.json プラグイン本体の設定ファイルです。プラグインの名前や説明、利用するコマンドを定義します。 { " name ": " guideline-review ", " version ": " 1.0.0 ", " description ": " 開発ガイドラインの準拠状況をレビューするプラグイン ", " commands ": [ " commands/guideline-review.md " ] } guideline-review.md コマンドの振る舞いをMarkdown形式で定義します。ファイル先頭のフロントマターには、 /help で表示されるコマンドの説明( description )や使用可能なツール( allowed-tools )などのメタ情報を記述します。 記述可能なメタ情報の詳細は以下をご参照ください。 code.claude.com フロントマター以降の本文部分がプロンプトとして実行されます。 --- description: 開発ガイドラインの準拠状況をレビューする allowed-tools: - mcp __ atlassian _ _* - Task --- # 開発ガイドラインレビュー 現在のプロジェクトが開発ガイドラインに準拠しているかをチェックします。 ## 手順 1. Atlassian MCPを使用して、Confluenceから開発ガイドラインを取得 2. 関連するプラットフォーム・領域の開発ガイドラインに対して、サブエージェントで並列してチェックを実行 3. 各項目について「適合」「非適合」「確認不可」を判定 4. 非適合の場合は具体的な指摘箇所と改善案を記載 このコマンド定義のポイントは以下の通りです。 allowed-tools : 使用可能なMCPツールを制限(Atlassianへのアクセスとサブエージェント起動のみ許可) サブエージェントによる並列チェック : 開発ガイドラインは種類も多く、各開発ガイドラインの項目も数十に及ぶため、コンテキストウィンドウの消費を抑える観点からもサブエージェントを活用 構造化された出力 : 適合・非適合の判定と詳細を明確に記載 プラグインの登録 作成したプラグインを全社で利用できるようにするため、マーケットプレイスとして登録します。リポジトリのルートに .claude-plugin/marketplace.json を配置します。 { " name ": " xxxxx-marketplace ", " plugins ": [ { " name ": " guideline-review ", " description ": " 開発ガイドラインの準拠状況をレビューします ", " version ": " 1.0.0 ", " source ": " ./plugins/guideline-review " } ] } このリポジトリを公開し、利用者側で extraKnownMarketplaces に追加します。 .claude/settings.json に以下を追加します。 { " extraKnownMarketplaces ": { " guideline-review ": { " source ": { " source ": " github ", " repo ": " your-org/claude-plugins " } } } } プラグインの利用方法 設定が完了したら、以下のコマンドでプラグインをインストールして利用できます。 # マーケットプレイスの追加 /plugin marketplace add your-org/claude-code-plugins-name # プラグインのインストール /plugin install guideline-review-plugin # 開発ガイドラインレビューの実行(プラグイン名:コマンド名の形式) /guideline-review-plugin:guideline-review チーム展開時の設定 チーム全体でプラグインを利用する場合、プロジェクトの .claude/settings.json に許可設定を追加します。 { " plugins ": { " allowed ": [ " guideline-review-plugin " ] } } プラグインの実行結果 コマンドを実行すると、以下のような流れで処理が行われます。 関連ガイドラインの特定 プロジェクトの技術スタックに基づき、関連する開発ガイドラインが自動的に特定されます。以下は、プラグインを実行した結果の抜粋です。 ⏺ 配下のページを確認しました。 このリポジトリ(Python + AWS Lambda + Serverless Framework + DynamoDB)に関連するガイドラインを特定します 関連するガイドライン: 1. 開発ガイドライン - API - APIシステムのため 2. 開発ガイドライン - 共通 - 全プロジェクト共通 3. 開発ガイドライン - Backend - バックエンドシステムのため 4. 開発ガイドライン - Batch - バッチ処理を含むため 5. 開発ガイドライン - Infra - AWS Lambda等のインフラ関連 6. 開発ガイドライン - DB(共通) - DynamoDB使用のため 適合状況のサマリー 確認すべき開発ガイドラインが分かった後は、対象の開発ガイドラインに対するチェックが行われ、結果が以下のようなサマリー形式で出力されます。 状態 件数 項目番号 ✅ 適合 46 1-4, 10-12, 18, 20-23, 26-47, 49-54, 58 ⚠️ 非適合 5 7, 8, 19, 24, 25 ❓ 確認不可/対象外 7 9, 13-17, 48, 55-57 このサマリーはAPIガイドラインに対する適合状況の例です。適合・非適合・確認不可の判定が一覧化されています。 詳細レポート サマリーに続き、各ガイドライン項目の詳細なチェック結果が出力されます。以下、JSON命名規約に関するレポートの抜粋です。 # 項目 必須度 状態 詳細・根拠 18 プロパティ名はASCIIスネークケース MUST ✅ 適合 order_key, member_id等全て準拠 19 enumの値はUPPER_SNAKE_CASE形式 MUST ⚠️ 非適合 new_arrival, soldout等が小文字 20 Booleanのプロパティにnullを使わない MUST ✅ 適合 boolean型にnullable設定なし 21 Booleanのプロパティ名は命名規約に従う RECOMMENDED ✅ 適合 is_main, can_return等準拠 24 日時プロパティ名に_atサフィックス MUST ⚠️ 非適合 order_date等_date使用 25 日付プロパティ名に_onサフィックス MUST ⚠️ 非適合 日付専用プロパティも_date使用 26 日時型・日付型の値はISO 8601に準拠 MUST ✅ 適合 format: date-timeで指定 各項目に対して具体的なコード箇所や根拠が示されます。また、非適合項目については項目別の具体的な修正案も提示されるため、それを参考に修正計画を立てることができます。 今後の展望 本取り組みを通じて、開発ガイドライン準拠チェックの自動化の基盤ができました。今後は以下の拡張を検討しています。 プラグインの拡充 : 開発ガイドラインのレビュー以外にも、コードレビュー支援やテスト生成などのプラグインを追加し、開発フロー全体をカバーする Skills/Agentsの活用 : 任意のタイミングでのチェックだけでなく、開発中も常に開発ガイドラインに準拠した実装が担保される仕組みを展開していく まとめ 本記事では、開発ガイドライン準拠チェックをClaude Code Plugins × Atlassian MCPで全社展開した取り組みを紹介しました。Claude Code Pluginsは、組織固有のワークフローをパッケージ化して容易に配布・メンテナンスできる仕組みです。今回ご紹介した開発ガイドラインレビュー以外にも、さまざまな用途に応用できる可能性があります。また、Claude Codeは継続的にアップデートされているため、最新バージョンに追従することで新機能や改善の恩恵を最大限に受けることができます。AI駆動の開発プロセス標準化に取り組む方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com Claude Code v2.1.3 でCommandsとSkillsが統合されました。本記事の執筆時点ではリリース前だったため、一部サンプルコードはCommands形式で記載していますが、挙動に変わりはありません。 ↩
アバター
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、ZOZOTOWN開発本部リプレイスバックエンドブロックのばやです。普段はZOZOTOWN BFFのリプレイス開発を担当しています。 システムリプレイスのプロジェクトでは、実装に入る前段階として 既存コードの調査 が必ず発生します。特にレガシーシステムの場合、ドキュメントが整備されていなかったり、仕様が暗黙知として埋もれていたりすることが多く、コードを読み解くことでしか仕様を把握できないケースも少なくありません。 一方で、この調査フェーズは成果物の形式や進め方が属人化しやすく、プロジェクト全体の生産性に大きな影響を与えるポイントでもあります。調査に時間がかかればプロジェクト全体のスケジュールに影響しますし、調査品質が低ければ後工程での手戻りにつながります。 本記事では、リプレイスにおける既存コード調査の課題に対し、調査業務をテンプレート化しその後、 Claude CodeでAgent Skillsに落とし込むことで効率化した取り組み をご紹介します。 目次 はじめに 目次 背景 リプレイスにおける既存コード調査の重要性 調査フェーズが抱えていた課題 人による改善:既存コード調査業務の標準化 調査対象コードのコピーによるスコープ明確化 調査テンプレートの設計 テンプレート化だけでは解決できなかった課題 経験者と未経験者の工数差 調査結果の粒度とレビュー工数のバランス AIによる改善 Agent Skillsとは なぜAgent Skillsを選んだのか 「Slash Command + Skills」を採用した理由 常時コンテキストを圧迫することを防ぐ Anthropic公式のSkills作成のSkillsが存在した 専門知識を一般化された状態で格納できる Agent Skillsの実装 ディレクトリ構成 実行フロー この構成にした理由 1. LSPがないレガシー言語への対応 2. コンテキストが膨大な処理の並列化 3. 中間ドキュメントによる精度向上 各Skillの役割 成果物 運用フロー 導入効果 定量的な効果 定性的な効果 見えてきた課題 コンテキストの欠落 ハルシネーションの見落とし 今後の展望 検証プロセスの強化 より人間がわかりやすいドキュメントを再作成する 新規実装やテストコードと旧コードのマッピング Agent Skillsの継続的な改善 他チームへの展開 まとめ 背景 リプレイスにおける既存コード調査の重要性 リプレイスでは、既存システムの仕様や振る舞いを正しく理解することが不可欠です。 特に以下のような情報は、後続の設計・実装の品質を大きく左右します。 実際に使われている機能・使われていない機能 暗黙的な仕様や例外的な処理 データ構造や依存関係 パフォーマンスや運用上の注意点 そのため、既存コード調査は「とりあえず読む」ではなく、 一定の品質を担保した成果物として残す必要 があります。 成果物として残すことで、レビューしプロジェクト全体で品質を担保できます。 調査フェーズが抱えていた課題 一方で、従来の調査フェーズには次のような課題がありました。 調査結果の形式がメンバーごとに異なる どこまで調査すれば十分なのか判断基準が曖昧 調査内容の抜け漏れがレビューまで発覚しない キャッチアップコストが高く、経験者と未経験者で工数差が大きい タスクの見積もりが人によって大きくブレる 人による改善:既存コード調査業務の標準化 これらの課題を解決するために、まず着手したのは 既存コード調査業務の標準化 です。具体的には、以下の2つを行いました。 調査対象コードのコピーによるスコープ明確化 調査対象のコードを、元のリポジトリから現開発環境のリポジトリにコピーします。 単純ではありますが、確認するコードを限定することで作業者とレビュアの双方にとって範囲が明確になり、作業効率が向上しました。 当時はAgent Skillsの導入を意識していたわけではありませんでしたが、結果的にこの運用がAgent Skillsへの移行をスムーズにしました。対象コードが分析用ディレクトリにまとまっていたことで、Agent Skillsが旧コードベースを読み込んで解析できる状態が既に整っていました。 調査テンプレートの設計 私たちのチームでは、BFFを開発する上で最低限必要となる情報の調査に特化したテンプレートを使用しています。 ## API名 ## 概要シーケンス ## リクエスト例 ## リクエストパラメータおよび変換処理 ## コード内での呼び出しAPI / 呼び出しテーブル ## エラーレスポンス ## レスポンス例 ## レスポンスパラメータおよび変換処理 テンプレートにより「何を調べるべきか」「何を書けばよいか」が明確になり、調査の抜け漏れを防げるようになりました。また、成果物の形式が統一されたことで、レビューもしやすくなりました。 テンプレート化だけでは解決できなかった課題 テンプレート化により「何を書けばよいか」は明確になりましたが、以下の課題は依然として残っていました。 経験者と未経験者の工数差 レガシーコードに慣れている経験者は効率的に調査を進められますが、未経験者は同じテンプレートを埋めるのにも時間がかかってしまいます。テンプレートは「何を書くか」を示してくれますが、「どうやって調べるか」までは教えてくれません。 調査結果の粒度とレビュー工数のバランス レビュアの既存コード調査レビューはそれなりに工数を伴います。リプレイスする機能は多岐に渡り、1つの機能がどのような機能かを詳細に把握することは大変です。 一方で一機能のリリースにおける、最序盤のタスクである既存調査に対してプロジェクトとしてはあまり工数をかけたくなく、既存調査の情報粒度は2〜3日程度で完成できる程度としていました。 必然的にレビュアの負担をかけることで、品質を担保するようにしていました。 これにより、レビューが重なったタイミングなどはレビュー遅延による待ちなどが発生して、プロジェクト全体の遅延が懸念されました。 これらの課題を解決するために、次にAgent Skillsの導入を検討しました。 AIによる改善 Agent Skillsとは Agent Skills は、Claudeの機能を拡張するモジュール型の機能です。指示、メタデータ、およびオプションのリソース(スクリプト、テンプレートなど)をパッケージ化し、Claudeが関連するタスクで自動的に使用できるようにします。 Agent Skillsを使う主な利点は以下の3点です。 専門知識を注入し、暗黙知を削減 : 既存知識をドキュメントに移譲することで、暗黙知となっていた業務知識を公開ドキュメントとできる 繰り返しを削減する : 一度作成すれば、複数の会話で同じガイダンスを繰り返す必要がなくなる 機能を組み合わせる : 複数のSkillsを組み合わせて複雑なワークフローを構築できる Agent Skillsは SKILL.md ファイルとして定義します。YAMLフロントマターでメタデータを記述し、本文に具体的な指示を記載します。Claude Codeでは、このファイルを .claude/skills/ ディレクトリに配置することで、Claudeが自動的に検出して使用します。 なぜAgent Skillsを選んだのか Claude Codeには、カスタマイズのための複数の仕組みがあります。今回の用途に適した方式を選ぶため、それぞれの特徴を比較しました。 方式 特徴 用途 CLAUDE.md 起動時に常に読み込まれる 常に必要な情報 Skills 必要に応じて読み込まれる 特定の作業タイミングでのみ必要な指示 Slash Commands ユーザーが明示的に呼び出す 定期的に実行したいタスクのショートカット Subagents 独立したタスクを別エージェントに委譲 調査過程が膨大になる場合の結果のみ取得 現状Agentの呼び出し方法には複数の選択肢があり、チームでは Slash Commands + Skills の組み合わせ を採用しました。 エントリーポイント : Slash Command 業務知識の保存 : Skills(詳細な調査手順とテンプレート) Slash Commandをエントリーポイントとしてのみの薄い層とし、実際の調査テンプレートや調査方法はSkillsに格納しました 1 。 「Slash Command + Skills」を採用した理由 常時コンテキストを圧迫することを防ぐ CLAUDE.mdへの記載は、常時コンテキストを圧迫することになるので避けました。 Anthropic公式のSkills作成のSkillsが存在した 我々のチームでは、CLAUDE.mdなどを読み込ませる際に、どのような文章構成がよりAgentの性能を上げてくれるのかが手探りの状態でした。 導入当時、Skillsは公式が作り方をサポートしてくれるSkillsを作成していました 2 。こちらに沿って作成すればスムーズにSkillsを作成でき、最適な形であることが保証できました。 Anthropic公式の マーケットプレイス も参照ください。 専門知識を一般化された状態で格納できる 作成した既存調査のSkillsは更新・改善を見込んでいます。このように階層に分けたドキュメント配置が公式から指定されていることで、我流のドキュメント配置を抑制できます。一般化された階層を誰もが実現でき、可読性が高い状態を維持できます。 なお、当時はSkillsを明示的に呼び出すことができなかったため、呼び出し部分だけSlash Commandで作成しました。結果的にSkills側に専門知識を寄せたことにより、現状のSlash CommandとSkillsの統合への変更が最小限で済みました。 Slash Command内で並列化することにより、いくつかのSkillsはSubagent的な役割を実現させています。これは個人的な意見にはなりますが、SubagentもいずれSkillsに統合され、Skillsを同期的に呼ぶか非同期で呼ぶかという観点のみが残るような気がしています。 Agent Skillsの実装 調査テンプレートをAgent Skillsとして定義しました。複数のSkillsを組み合わせ、以下の流れで調査を自動化しています。 ディレクトリ構成 {plugin-name}/ ├── commands/ # エントリーポイント │ └── analyze.md # 分析実行コマンド ├── skills/ # Skills本体(詳細な手順) │ ├── find-dependencies/ # 依存関係調査 │ ├── analyze-code/ # コード詳細分析 │ ├── extract-files/ # ファイル抽出 │ └── generate-readme/ # ドキュメント生成 │ └── templates/ # 出力テンプレート └── agents/ # Subagents定義(並列実行用) 実行フロー 1. /analyze を実行(Slash Command) ↓ 2. find-dependencies(依存関係調査) ↓ 3. 並列実行: ├─ analyze-code(コード詳細分析) └─ extract-files(ファイル抽出) ↓ 4. generate-readme(ドキュメント生成) ↓ 5. 成果物出力 この構成にした理由 1. LSPがないレガシー言語への対応 旧コードにはLSP(Language Server Protocol)が存在しないため、「定義へジャンプ」や「参照検索」といった機能が使えません。関数の検索方法や依存関係の追跡方法をドメイン知識としてSkillsに注入する必要がありました。 find-dependencies を最初に実行することで、後続の処理に必要な情報を確実に収集します。 2. コンテキストが膨大な処理の並列化 従来の手作業では、既存コードのコピーとテンプレートへの情報抽出を順番に行っていました。どちらもコードベース全体を読み込む必要があり、コンテキストが膨大になります。これらを analyze-code と extract-files として分離し、並列実行することで処理時間を短縮しています。 3. 中間ドキュメントによる精度向上 依存関係の調査結果を analyze-code で中間ドキュメント(markdown)として出力し、後続のSkillsが参照する構成にしています。一度にすべてを処理するのではなく、段階を踏むことでAIの出力精度が向上します。 各Skillの役割 Skill 役割 find-dependencies 関数検索、依存関数の追跡、グローバル変数の特定 analyze-code 入力パラメータ、出力データ、データアクセス、変換処理を抽出。旧フローのテンプレート化に相当 extract-files メイン関数、依存関数を個別ファイルに分離。旧フローのコードコピーに相当 generate-readme テンプレートに準拠した調査結果ドキュメントを生成。旧フローのテンプレート化に相当 成果物 実行後、以下の成果物が出力されます。 output/{対象関数名}/ ├── README.md # 調査結果(テンプレート準拠) ├── 01.{メイン関数}.ext # メイン関数 ├── 02.{依存関数1}.ext # 依存関数 └── ... 調査結果のREADME.mdは、先述のテンプレートに沿った形式で出力されます。依存関数も個別ファイルとして抽出されるため、レビュー時にコード全体を把握しやすくなります。 運用フロー この仕組みにより、調査担当者は以下の流れで作業できるようになりました。 1. 対象リポジトリやディレクトリを指定 2. Agent Skillsを実行 3. 出力された調査結果を確認・修正 4. レビューを実施 5. 必要に応じて追加調査 従来は「コードを読む → 理解する → まとめる」という一連の作業をすべて人間が行っていました。Agent Skillsを活用することで「AIが下書きを作成 → 人間が確認・修正」という流れに変わりました。 導入効果 定量的な効果 Agent Skillsの導入により、調査作業のリードタイムは大きく改善しました。 項目 従来 導入後 調査の実行時間 数時間〜数日 数分程度 全体のリードタイム 2〜5日 数時間 成果物の形式 バラバラ 統一 調査結果の確認・修正・レビューを含めても 数時間で完結 するようになりました。従来の「数日かかる調査タスク」と比較すると、大幅な短縮です。 また、レビュー体験も改善しました。成果物の形式が統一されているため、レビュアは「何がどこに書いてあるか」を把握しやすくなり、内容の精査へ集中できるようになりました。 特にリードタイムの短縮は顕著で、これまでボトルネックになっていた調査フェーズがスムーズに進むようになりました。 定性的な効果 定量的な効果に加えて、以下のような定性的な効果も得られました。 メンバー間で調査観点が揃い、レビューがしやすくなった 情報粒度が上がったので、細かい内容まで記載があって、レビューがしやすくなった 新しく参加したメンバーでも、同じ品質で調査できるようになった 特に、新規メンバーのキャッチアップコストが下がったことは大きな効果でした。従来は既存システムの知識がないと調査に時間がかかっていましたが、Agent Skillsを使うことで、知識の有無に関わらず一定品質の調査が可能になりました。 見えてきた課題 Agent Skillsの導入により多くの効果が得られた一方で、新たな課題も見えてきました。 コンテキストの欠落 従来は時間をかけて調査することで、各メンバーが暗黙的な知識を蓄積していました。「なぜこのような実装になっているのか」「過去にどのような経緯があったのか」といった背景知識は、コードを読み込む過程で自然と身についていました。 しかし、AIによる高速な調査では「調べた結果」は得られるものの、「理解」を伴わないケースもあります。具体的には以下のような問題に直面しました。 実装フェーズで想定外の考慮漏れが発生する 全体像を把握していないことによる手戻り 調査結果に書いてあることしか把握していない状態 この課題に対しては、調査結果を「読むだけ」で終わらせず、チーム内で共有・議論する時間を設けることが重要だと感じています。 AIが作成した調査結果をベースに、チームで同期的な理解の時間を設けることで後のフェーズである、設計・実装の部分でスムーズな進行ができると感じました。 ハルシネーションの見落とし AIが大量の情報を要約して出力するため、全項目を精査することが難しくなります。結果として、誤った情報がそのままレビューを通過してしまうリスクがあります。 これはLLMを使っている以上避けては通れないリスクではありますが、現状、根本的な解決策は見つかっていません。 レビュイー・レビュア双方による注意深い確認が重要だとされがちです。しかし、それでは業務負荷が作成からレビューに移行しただけで、AI駆動開発の恩恵を受けられません。 今後の展望 見えてきた課題を踏まえ、今後は以下の取り組みを進めていく予定です。 検証プロセスの強化 AIの出力をより効率的に検証するため、現在以下の取り組みを行っています。 より人間がわかりやすいドキュメントを再作成する 既存調査 → 新規設計 → 実装という形でフェーズが進行しており、レビュアが見やすい粒度・構成の設計書を模索しながら作成しています。 新規実装やテストコードと旧コードのマッピング レビュイー本人も漏れを防ぐため、リプレイス後のコードに旧コードのGitHub上での該当箇所URLをコメントすることで擬似チェックリストのようなものを再現しています。愚直ではありますが、これによって実装者が漏れを把握して未然にバグを防げたので、有効性は確認できています。 今後は以下の仕組みも検討しています。 調査結果と実際のコードを自動で照合するスクリプトの作成 重要度の高い項目(データ構造、エラーハンドリングなど)のチェックリスト化 実装フェーズでの差分検出と調査結果へのフィードバックループ Agent Skillsの継続的な改善 プロンプトの精度向上や、新たな観点の追加を継続的に行います。 調査対象のコードパターンに応じたプロンプトの分岐 よくある誤りパターンの収集と、それを防ぐための指示の追加 チームからのフィードバックに基づくテンプレートの更新 他チームへの展開 現在は特定のチームで運用していますが、他のリプレイスプロジェクトへの展開も視野に入れています。 Agent Skillsの汎用化と、プロジェクト固有の設定の分離 導入ガイドやベストプラクティスの整備 成功事例・失敗事例のナレッジ蓄積 まとめ 本記事では、リプレイスにおける既存コード調査の課題に対し、以下のアプローチで業務を効率化した事例を紹介しました。 調査業務をテンプレート化し、「何を調べるか」「何を書くか」を明確にする その手順をAgent Skillsに落とし込み、AIによる調査の自動化を実現する 結果として、調査タスクのリードタイムを大幅に短縮しつつ、品質の安定化も実現できました。 一方で、「コンテキストの欠落」や「ハルシネーションの見落とし」といった新たな課題も見えてきました。これらは、AIを活用した業務効率化において避けて通れない課題であり、運用の工夫や検証プロセスの改善によって対処していく必要があります。 リプレイスでは「調査」は避けて通れない工程だからこそ、人に依存しない形で品質とスピードを両立させることが重要だと考えています。AIの活用はその有力な手段の1つですが、AIの出力を鵜呑みにせず、人間による検証とチームでの知識共有を組み合わせることが成功の鍵です。 既存コード調査やリプレイスに課題を感じている方の参考になれば幸いです。 ZOZOTOWN開発本部リプレイスバックエンドブロックでは、大規模なシステムリプレイスを推進するエンジニアを募集しています。AIを活用した開発プロセスの改善や、レガシーシステムのリプレイスに興味のある方、ぜひご応募ください。 corp.zozo.com 2026/01/21現在、SkillsとSlash Commandは統合され、Skillsを明示的に呼び出せるようになりました。こちらの分割は現在不要となりましたが、実行役割を持つSkillsと知識をもつSkillsの分割はパターンとして未だに有効だと考えています。 ↩ Anthropicの skill-creator というSkillsを参考にしてください。 ↩
アバター