Findyの爆速開発を支えるAIフレンドリーなIssue生成カスタムコマンド

こんにちは。

ファインディ株式会社でテックリードマネージャーをやらせてもらってる戸田です。

現在のソフトウェア開発の世界は、生成AIの登場により大きな転換点を迎えています。

GitHub CopilotやClaude Codeなど生成AIを活用した開発支援ツールが次々と登場し、開発者の日常的なワークフローに組み込まれつつあります。

2025年はSpec Driven DevelopmentやAI-DLCなどといった新しい開発手法が注目を集める年になりました。

ファインディでも新しい開発手法への取り組みを進めており、その一環として要件定義、設計、タスク分解、Issue作成までのフローを自動化しました。

そこで今回は、自動化したフローの内容と仕組みを実現したカスタムコマンドについて紹介します。

それでは見ていきましょう!

背景と課題

機能追加の要件から、設計、タスク分解、Issue作成までを行う必要があります。これらを生成AIに任せたいところですが、必要とされる事前知識が多くハードルが高いという課題がありました。

システム全体を把握できていないと適切な設計が難しいケースもあります。また、生成AIに対して効果的な依頼を作成するには明確で簡潔なステップ構造にすることが重要です。

つまり明確な要件を定義して、生成AIが正しい方法と手順を理解して実行できるように、設計図や指示書をAIフレンドリーな形で用意する必要があったのです。

既存の開発手法の検討

2025年はSpec Driven DevelopmentやKiroといった新しい開発手法が注目を集めました。これらは要件定義から実装まで一貫した自動化を目指すアプローチです。

私たちもこれらの手法を検討しましたが、タスク分解とPull requestの粒度に対する考え方に課題を感じました。

Spec Driven Developmentでは、システム全体の仕様を詳細に定義してから実装に進みます。これは大規模な機能開発や新規プロジェクトでは有効ですが、仕様の粒度やSpec/Taskの部分が大きくなりがちです。その結果、実装を進めると大きな変更を含むPull requestが作られることになり、コードレビューの負担が増大する傾向があります。

どちらにも利点がありますが、私たちはレビューの品質とチーム全体の開発速度を重視して、タスク分解と粒度に重点を置いたアプローチが必要だと考えました。

解決策: AIフレンドリーなIssue

この課題を解決するため、システムの現状を把握して、必要な要件をタスク分解してIssueに切り出すカスタムコマンドを構築しました。

重要なのは、Pull requestとタスクの粒度を維持しつつ、生成AIが理解しやすく精度の高いIssueを自動生成することです。この一連の流れをカスタムコマンドで自動化しました。

Issueが作成されたら、その内容や手順が正しいのかどうかチェックして、問題なければそのまま生成AIに渡して実装を進めてもらうようになります。

カスタムコマンドの仕組み

概要とフロー

今回紹介するカスタムコマンドは要件とコードベースを元に、要件の明確化、設計、タスク分解、Issue作成までを自動化します。

特徴的なのはインタラクティブな形式を採用している点です。曖昧な部分や不明点があれば、その都度生成AIが質問を投げかけてきます。人間は質問に答えながら生成AIとの対話を通じて要件を明確化していきます。

このプロセスを経て、生成AIが設計を行い、タスク分解を実施し、最終的にGitHubのIssueを自動で作成します。人間が行うのは作成されたIssueのレビューだけです。

Issueのレビューが完了したら、そのIssueをそのまま生成AIに渡して実装を進めてもらいます。Pull requestの粒度を意識した形でIssueが作成されるため、後続のコードレビューの負担も軽減されます。

このカスタムコマンドの全体フローは次のようなイメージです。

全体のシーケンス図

これら全てのフローを実行するために必要なカスタムコマンドやSkill、Sub Agentを社内Pluginとして開発組織全体に展開しています。

ステップ1: 要件の理解

まずは実現したい要件を生成AIが理解するところから始めます。このステップのフローは次のようなイメージです。

要件理解のシーケンス図

カスタムコマンドを実行して実現したい要件をパラメータ経由で伝えます。この段階では要件がある程度曖昧でも問題ありません。

❯ /plugin:feature-breakdown 複数ユーザーの新規登録を一括で実行する機能を追加したい 

⏺ I'll help you break down the bulk user registration feature into implementation tasks and create GitHub issues. Let me start by creating a TODO list for all phases.

⏺ Phase 1: 要件の理解

  初期リクエスト: 複数ユーザーの新規登録を一括で実行する機能を追加したい

要件の中に曖昧な内容があれば生成AIが確認のための質問を投げかけてきます。対話を通じて徐々に要件を明確にしていきます。

今回は追加したい機能を具体的にするために生成AIから幾つかの質問が投げかけられました。解答を選択式にすることで、スムーズに要件を明確化できます。

  この機能について、以下の点を明確にさせてください。

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
←  ☐ 対象ユーザー  ☐ 入力形式  ☐ 認証フロー  ☐ エラー処理  ✔ Submit  →

この一括登録機能は誰が使うことを想定していますか?

❯ 1. 管理者(AdminUser)のみが使用
     管理画面から管理者が実行する機能として実装。一般ユーザーは使用できない。
  2. 組織の管理者(Org内の特定ロール)
     組織内の権限を持つユーザーが自組織のメンバーを一括登録できる。
  3. 一般ユーザーも使用可能
     全ユーザーが利用可能な機能として実装。
  4. Type something.

解答を選択式にするためにClaude CodeのToolの AskUserQuestion を利用するようにプロンプトに明記すると便利です。 AskUserQuestion は情報を収集または曖昧さを明確にするために、ユーザーに複数選択肢の質問をします。

一通りの質問に回答すると生成AIが要件を整理してくれます。ここまでの選択で要件を理解することができました。認識にズレがなければこのまま次のステップに進みます。何かしらに誤りがあった場合、この段階で軌道修正もできます。

⏺ User answered Claude's questions:
  ⎿  · この一括登録機能は誰が使うことを想定していますか? → 管理者(AdminUser)のみが使用
     · ユーザーデータの入力形式はどのようなものを想定していますか? → CSV/TSVファイルのアップロード
     · 登録時のユーザー認証フローはどうしますか? → 確認メール送信(既存フロー準拠)
     · 登録処理中のエラーハンドリングはどうしますか? → 部分成功を許可

⏺ ありがとうございます。要件を整理します。

⏺ 理解した要件

  機能: 複数ユーザーの新規登録を一括で実行する機能

  要件の詳細:
  - 対象ユーザー: 管理者(AdminUser)のみが使用可能
  - 入力形式: CSV/TSVファイルのアップロード(email, name, org_id等のカラム)
  - 認証フロー: 確認メール送信(既存の単一ユーザー登録フローと同じ)
  - エラー処理: 部分成功を許可(成功したユーザーは登録、失敗したユーザーはエラーレポートで返す)

ステップ2: コードベースの探索

次に、要件を実現させるために既存のコードベースがどうなっているのかを理解する必要があります。

次のようなフローでコードベースを探索していきます。

コード探索のシーケンス図

既存のコードベースを理解するため、Claude Codeの公式Pluginであるfeature-dev にある code-explorerエージェントを活用します。このエージェントはプロジェクトの構造やパターンを自動的に分析して実装に必要な情報を収集します。feature-devプラグインをインストールするだけでcode-explorerエージェントを利用できるようになります。

要件を分割して複数のcode-explorerエージェントを並列で起動します。各エージェントが特定の観点からコードベースを探索して必要な情報を収集します。

⏺ Phase 2: コードベースの探索

  既存のコードとパターンを理解するため、複数のcode-explorerエージェントを並列で起動します。

  Running 3 feature-dev:code-explorer agents… (ctrl+o to expand)
   ├─ Explore user registration architecture · 12 tool uses · 29.1k tokens
   │  ⎿  Search: **/models/admin*.py
   ├─ Explore admin API patterns · 12 tool uses · 27.6k tokens
   │  ⎿  Read: main.py
   └─ Explore CSV and file handling · 10 tool uses · 17.0k tokens
      ⎿  Search: **/api/routes/**/*.py
     ctrl+b to run in background

⏺ Excellent! Now let me read the critical files identified by the code-explorer agents to deepen my understanding.

⏺ Read(src/api/routes/user/users.py)
  ⎿  Read 60 lines

⏺ Read(src/api/routes/user/users.py)
  ⎿  Read 60 lines

⏺ Read(src/api/routes/admin/orgs.py)
  ⎿  Read 70 lines

⏺ Read(src/api/routes/user/orgs/teams.py)
  ⎿  Read 60 lines

⏺ Read(src/services/email/email_service.py)
  ⎿  Read 132 lines

今回は複数のユーザー登録を一括で実行する機能を追加したいので、既存のユーザー登録処理や管理者用APIのパターン、CSVファイルの取り扱い方法などを中心にコードベースを探索しているようです。

要件を実現させるために必要なコードベースの情報が揃ったようなので、次のステップに進みます。

ステップ3: 要件の明確化

次は入力した要件とコードベースの情報を元に、要件を更に明確化していきます。

既存コードと要件を精査して曖昧な内容が残っていれば、追加で質問が投げかけられます。対話を繰り返すことで実装に必要な情報を揃えていきます。

⏺ Phase 3: 明確化のための質問

  コードベースの調査結果から、以下の点について確認させてください。


────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 ☐ CSV Columns 

CSVファイルにはどのような情報を含めますか?

❯ 1. 必須情報:メール、名前、ロール (Recommended)
     email, first_name, last_name, roleの4カラム。GitHubアカウントはオプション
  2. 必須情報:メールと名前のみ
     email, first_name, last_nameの3カラム。ロールはデフォルトでmember
  3. メールアドレスのみ
     emailの1カラムだけ。他の情報は後から追加
  4. Type something.

このステップではエラーハンドリングやエッジケースなど、細かい点についての要件を明確化します。

⏺ Phase 3: 明確化のための質問

  コードベースの調査結果から、以下の点について確認させてください。
                                                                                                 
⏺ User answered Claude's questions:
  ⎿  · CSVファイルにはどのような情報を含めますか? → 必須情報:メール、名前、ロール (Recommended)


────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 ☐ Duplicate Emails 

既存ユーザー(同じメールアドレス)がいた場合、どう処理しますか?

❯ 1. スキップして結果に記録 (Recommended)
     既存ユーザーは登録せず、skipped_emailsリストとして返す
  2. エラーとして返す
     既存ユーザーがいたらエラー情報を返し、その行を登録しない
  3. 情報を更新する
     既存ユーザーの名前やロールをCSVの値で更新
  4. Type something.

ここまでで要件と既存コードの変更内容が明確化されました。次のステップで設計に進みます。

ステップ4: 設計

設計フェーズでは、Claude Codeの公式Pluginであるfeature-dev にある code-architectエージェントを活用します。

このエージェントは設計方針を提案してくれます。開発者は提案された選択肢の中からプロジェクトに最適な実装方針を選ぶことができます。この選択により実装の方向性が確定します。

設計に関するフローは次のようなイメージになります。

設計のシーケンス図

今回は最小構成、クリーンアーキテクチャ、実用的バランスの3つのアプローチでcode-architectエージェントを並行稼働して最適な設計方針を提案してもらいました。

⏺ Phase 4: アーキテクチャ設計

  それでは、3つの異なるアプローチでアーキテクチャ設計を行います。

⏺ Running 3 feature-dev:code-architect agents… (ctrl+o to expand)
   ├─ Design minimal change approach · 3 tool uses · 10.1k tokens
   │  ⎿  Search: send_invite_user_email
   ├─ Design clean architecture approach · 4 tool uses · 10.1k tokens
   │  ⎿  Search: src/services/**/*.py
   └─ Design pragmatic balance approach · 0 tool uses
      ⎿  Initializing…
     ctrl+b to run in background

⏺ 3 feature-dev:code-architect agents finished (ctrl+o to expand)
   ├─ Design minimal change approach · 20 tool uses · 25.2k tokens
   │  ⎿  Done
   ├─ Design clean architecture approach · 26 tool uses · 41.0k tokens
   │  ⎿  Done
   └─ Design pragmatic balance approach · 0 tool uses
      ⎿  Done

⏺ Perfect! Now I have three comprehensive architecture designs. Let me present them to the user for selection.

⏺ 3つのアーキテクチャアプローチが完成しました。それぞれの特徴を整理してご提案します。

  3つのアプローチ比較

  1. 最小変更アプローチ (Minimal Change)

  特徴:
  - CSV処理をルートハンドラに直接実装(約150行の単一ファイル)
  - 既存パターンを最大限活用(teams.pyの部分成功パターン)
  - 新規作成ファイル: 2個(エンドポイント + テスト)

  メリット:
  - 最速で実装可能(2 PR、~650行)
  - シンプルで理解しやすい
  - 既存コードとの整合性が高い

  デメリット:
  - ルートハンドラが長くなる(~150行)
  - CSV処理ロジックの再利用が困難
  - 将来の拡張(Excel対応など)に追加実装が必要

  タスク数: 2個

  ---
  2. クリーンアーキテクチャアプローチ (Clean Architecture)

  特徴:
  - サービス層を導入して責務を分離
    - Parser(CSV解析)
    - Validator(バリデーション)
    - UserImportService(オーケストレーション)
  - 新規作成ファイル: 7個

  メリット:
  - 将来の拡張が容易(Excel, JSON対応)
  - テストが容易(各層を独立してテスト可能)
  - ビジネスロジックの再利用性が高い
  - 保守性が高い

  デメリット:
  - 初期実装に時間がかかる(6 PR、~730行)
  - 抽象化により複雑さが増す
  - シンプルなCSV処理に対してはオーバーエンジニアリングの可能性

  タスク数: 6個

  ---
  3. 実用的バランスアプローチ (Pragmatic Balance)

  特徴:
  - サービス層を導入するが、Parser/Validatorは分離しない
  - UserImportServiceで一元管理
  - 新規作成ファイル: 5個

  メリット:
  - 適度な抽象化で保守性を確保
  - 実装速度とコード品質のバランスが取れている
  - 将来の拡張もある程度対応可能

  デメリット:
  - クリーンアーキテクチャほどの柔軟性はない
  - 最小変更アプローチほどシンプルではない

  タスク数: 5個

  ---


────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 ☐ Approach 

どの実装アプローチを採用しますか?

  1. 実用的バランス (Recommended)
     開発速度と品質のバランス。サービス層を導入しつつもシンプルさを維持。タスク数: 5個
  2. 最小変更
     最速で実装。ルートハンドラに直接実装。既存パターンを最大限活用。タスク数: 2個
❯ 3. クリーンアーキテクチャ
     保守性と拡張性を重視。Parser/Validatorを分離し、将来の複数フォーマット対応を考慮。タスク数: 6個
  4. Type something.

様々な設計方針ごとにメリット、デメリットなどが提示されます。要件やプロジェクトの方針にもよりますが、今回はクリーンアーキテクチャを選択します。設計方針が決まったので次のタスク分解のステップに進みます。

ステップ5: タスク分解

確定した設計方針を元に実装に必要なタスクを洗い出して分解します。

今回のカスタムコマンドで特に重要なのは、Pull requestやIssueの粒度を考慮することです。今回のカスタムコマンドの調整により、レビューしやすく適切な粒度でマージしやすい最適な粒度に分解してくれます。

Pull requestやIssueの粒度に関するガイドラインはSkillに切り出しています。 Sub AgentやSkillに切り出すことにより、タスク分解の品質を継続的に改善できるだけでなく、カスタムコマンドの一連の流れのコンテキストを適切に管理できるようになります。

今回はクリーンアーキテクチャを選択しており、最初に各種共通処理の作成とリファクタを行い、その後にAPIを追加するというタスクの分け方になっているようです。

⏺ 承知しました。クリーンアーキテクチャアプローチで進めます。

  それでは Phase 5 に進んで、task-breakdown Skillを使ってタスク分解を評価・調整します。

⏺ Skill(task-breakdown)
  ⎿  Successfully loaded skill

⏺ Task Breakdown Skillがロードされました。それでは、Phase 4で得たクリーンアーキテクチャアプローチのタスク分解を評価します。

  code-architectが提案したタスク分解

  以下は、クリーンアーキテクチャアプローチで提案された6つのタスクです:

  Task 1: [Data] ドメインモデルの追加

  - 変更ファイル:
    - CREATE: src/services/user_import/models.py
    - CREATE: src/services/user_import/__init__.py
    - CREATE: tests/services/user_import/test_models.py
  - 概要: ImportRow, ValidationError, CreatedUser, ImportResult モデルを作成
  - 依存関係: なし

  Task 2: [Logic] CSVパーサーの実装

  - 変更ファイル:
    - CREATE: src/services/user_import/parser.py
    - MODIFY: src/services/user_import/__init__.py
    - CREATE: tests/services/user_import/test_parser.py
  - 概要: CSVParserインターフェースとDefaultCSVParserを実装
  - 依存関係: Task 1(ImportRowモデルが必要)

  Task 3: [Logic] バリデーターの実装

  - 変更ファイル:
    - CREATE: src/services/user_import/validator.py
    - MODIFY: src/services/user_import/__init__.py
    - CREATE: tests/services/user_import/test_validator.py
  - 概要: UserDataValidator(ビジネスルール検証)を実装
  - 依存関係: Task 1(ImportRow, ValidationErrorモデルが必要)

  Task 4: [Logic] ユーザーインポートサービスの実装

  - 変更ファイル:
    - CREATE: src/services/user_import/user_import_service.py
    - MODIFY: src/services/user_import/__init__.py
    - CREATE: tests/services/user_import/test_user_import_service.py
  - 概要: UserImportService(オーケストレーション層)とメール連携
  - 依存関係: Task 1, 2, 3(すべてのコンポーネントが必要)

  Task 5: [API] 管理者用一括登録エンドポイント

  - 変更ファイル:
    - CREATE: src/api/routes/admin/user_import.py
    - MODIFY: main.py
    - CREATE: tests/api/routes/admin/test_user_import.py
  - 概要: POST /api/v1/admin/users/import エンドポイントの実装
  - 依存関係: Task 1, 2, 3, 4(サービス層全体が必要)

  ---
  Task Breakdown Skillによる評価

  1. タスクの粒度チェック

  Task 1: ドメインモデル
  - ファイル数: 3個 ✅ (理想的)
  - 行数: ~80行 ✅ (適切)
  - 評価: 適切なサイズ

  Task 2: CSVパーサー
  - ファイル数: 3個 ✅ (理想的)
  - 行数: ~100行 ✅ (適切)
  - 評価: 適切なサイズ

  Task 3: バリデーター
  - ファイル数: 3個 ✅ (理想的)
  - 行数: ~120行 ✅ (適切)
  - 評価: 適切なサイズ

  Task 4: ユーザーインポートサービス
  - ファイル数: 3個 ✅ (理想的)
  - 行数: ~180行 ✅ (適切)
  - 評価: 許容範囲だが、メール連携部分を分離できる可能性あり

  Task 5: APIエンドポイント
  - ファイル数: 3個 ✅ (理想的)
  - 行数: ~100行 ✅ (適切)
  - 評価: 適切なサイズ

  2. 密結合の識別

  ✅ 密結合が適切に保たれているもの:
  - Task 1: モデル定義 + テスト (実装とテストが同一PR)
  - Task 2: Parser実装 + テスト (実装とテストが同一PR)
  - Task 3: Validator実装 + テスト (実装とテストが同一PR)
  - Task 4: Service実装 + テスト (実装とテストが同一PR)
  - Task 5: APIエンドポイント + ルーティング + テスト (API完全性が保たれている)

  ❌ 潜在的な密結合の問題: なし

  3. 独立性の検証

  独立してrevertできるか?
  - Task 1: ✅ 他のタスクに影響なし(モデルのみ)
  - Task 2: ✅ Task 1に依存するが、独立して削除可能
  - Task 3: ✅ Task 1に依存するが、独立して削除可能
  - Task 4: ⚠️ Task 2, 3に依存するため、revert時は順序が重要
  - Task 5: ✅ Task 4に依存するが、独立して削除可能(ルーター登録を削除するだけ)

  各PRマージ後、アプリケーションは正常動作するか?
  - Task 1マージ後: ✅ モデルのみ追加、既存機能に影響なし
  - Task 2マージ後: ✅ パーサーのみ追加、既存機能に影響なし
  - Task 3マージ後: ✅ バリデーターのみ追加、既存機能に影響なし
  - Task 4マージ後: ✅ サービス層のみ追加、APIエンドポイントがないため既存機能に影響なし
  - Task 5マージ後: ✅ 新規エンドポイント追加、既存機能に影響なし

  4. 依存関係の整理

  Task 1 (モデル)
  ├── Task 2 (Parser) ← Task 1に依存
  └── Task 3 (Validator) ← Task 1に依存
      └── Task 4 (Service) ← Task 2, 3に依存
          └── Task 5 (API) ← Task 4に依存

  並列実装可能なタスク:
  - Task 2とTask 3は並列実装可能(どちらもTask 1にのみ依存)

  5. 推奨事項

  現在のタスク分解は非常に良好です。以下の理由から、このまま進めることを推奨します:

  良い点:
  1. ✅ 各タスクのサイズが適切(3-10ファイル、100-200行)
  2. ✅ 実装とテストが密結合として同一PRに含まれている
  3. ✅ 各タスクが独立してrevert可能
  4. ✅ 依存関係が明確で、並列実装の余地もある
  5. ✅ 各PRマージ後もアプリケーションは正常動作する

  最終判断: 現在のタスク分解をそのまま採用

  ---

ここまでで要件を正しい方法と手順で実現するために必要な準備が整いました。最後のステップをこれらを元にIssueを作成していきます。

ステップ6: Issue作成

最後のステップで、分解したタスクを元にGitHubのIssueを自動で作成します。

作成前には確認ステップを踏み、認識がズレていないか最終チェックを行います。問題がなければ、そのままIssueを作成してSub Issueの紐付けとrelationshipの設定まで完了させます。

これにより親Issueと子Issueが適切に構造化された状態で、すぐに実装に取り掛かれるようになります。

GitHubのIssueを作成する処理もSub Agentに切り出しています。Issueを作成するために必要な情報量が多く、すぐにコンテキストが肥大化してしまうからです。そのためIssue作成専用のSub Agentを用意して必要な情報だけ渡してIssueを作成する形にしています。

⏺ Phase 7: GitHub Issueの作成

  github-issue-creator agentを起動してGitHub issueを作成します。

⏺ plugin:github-issue-creator(Create GitHub issues)
  ⎿  Done (25 tool uses · 117.1k tokens · 8m 35s)                                                                                                                                                                                                                                                  

⏺ ✅ Feature Breakdown 完了!

  GitHub Issueの作成が完了しました。以下が作成されたIssueです。

  📋 親Issue

  #1 - Feature: 複数ユーザーの一括登録機能
  https://github.com/Findy/Example/issues/1

  📦 子Issue(実装タスク)

  1. #2 - [Data] ドメインモデルの追加 + テスト
  https://github.com/Findy/Example/issues/2
  依存関係: なし
  2. #3 - [Logic] CSVパーサーの実装 + テスト
  https://github.com/Findy/Example/issues/3
  依存関係: #2
  3. #4 - [Logic] バリデーターの実装 + テスト
  https://github.com/Findy/Example/issues/4
  依存関係: #2
  4. #5 - [Logic] ユーザーインポートサービスの実装 + テスト
  https://github.com/Findy/Example/issues/5
  依存関係: #2, #3, #4
  5. #6 - [API] 管理者用一括登録エンドポイント + テスト
  https://github.com/Findy/Example/issues/6
  依存関係: #5

  ---
  📊 Issue構造

  📋 #1 Feature: 複数ユーザーの一括登録機能
  ├── 📦 #2 [Data] ドメインモデルの追加 + テスト
  │   ├── ⚙️  #3 [Logic] CSVパーサーの実装 + テスト
  │   └── ⚙️  #4 [Logic] バリデーターの実装 + テスト
  │       └── ⚙️  #5 [Logic] ユーザーインポートサービスの実装 + テスト
  │           └── 🔌 #6 [API] 管理者用一括登録エンドポイント + テスト

  ---
  🎯 次のステップ

  1. 実装開始: #2から順番に実装してください
  2. 並列実装可能: #3と#4は#2完了後、並列で実装可能です
  3. 進捗管理: 親Issue #1で全体の進捗を確認できます
  4. PRの作成: 各Issueごとに1つのPRを作成してください

  ---
  📝 設計サマリー

  採用アーキテクチャ: クリーンアーキテクチャ
  - Parser層(CSV解析)
  - Validator層(ビジネスルール検証)
  - Service層(オーケストレーション)
  - API層(HTTPハンドリング)

  主な技術要素:
  - CSVパース: Python標準ライブラリ csv.DictReader
  - バリデーション: Pydantic + カスタムロジック
  - トランザクション: ユーザー単位のコミット
  - メール送信: 既存のEmailService再利用
  - エラー処理: 部分成功パターン

  受入基準:
  - テストカバレッジ90%以上
  - 各PRは3-10ファイル、200-500行の変更
  - すべてのPRは独立してrevert可能

  Feature Breakdown コマンドの全フェーズが完了しました!

これで要件の策定から適切な粒度でのIssue作成までの一連の流れが完了しました。Issueの内容をチームでレビューして、実装を生成AIに任せましょう。適切な粒度でタスクが分解されているので、Pull requestの粒度も適切になり、レビューの負担が大幅に軽減されます。

カスタムコマンド作成のコツ

様々な開発タスクを効率化するためには必要な情報を用意する必要があります。その結果、コンテキストが肥大化してしまい精度が一気に落ちてしまうことがよくあります。

まずは一通りの流れをカスタムコマンドで作成して、内容やスコープに応じてSkillやSub Agentに切り出していくのが良いです。特定の知識やガイドラインなどはSkillに切り出しましょう。メインのコンテキストには不要で結果のみ必要な情報を扱う場合はSub Agentに切り出すのが良いです。

いかにしてコンテキストを最小限に保つかがコツです。 カスタムコマンドに限らず、コンテキストが肥大化すると生成AIの精度は一気に落ちてしまいます。 必要な情報だけを適切に渡して維持し続けることが、生成AIを活用する上で非常に重要です。

まとめ

今回紹介したカスタムコマンドは、誰でも高品質で適切な粒度のAIフレンドリーなIssueを作成できるようにするものです。

生成AIを活用した開発では、生成AIが理解しやすい形で要件と設計を伝えることが重要です。要件定義から設計、タスク分解、Issue作成までのフローを自動化することで、開発者はより本質的な開発業務に集中できるようになります。生成AIとの協業による開発スタイルは、今後ますます重要になっていくでしょう。

最後になりますが、このたび 2026年2月18日(水)にFindy AI Meetup in Fukuoka #4の開催が決定 しました!

findy-inc.connpass.com

会場へのアクセスは天神駅から徒歩3分となっています。またTVCM公開記念ノベルティや、イベント後半には懇親タイムもご用意しています。

申込みの先着順となっておりますので、気になっている方は早めにお申し込みいただくことをおすすめします。生成AIの活用事例に興味のある方は奮ってご参加ください!

みなさんにお会いできることを楽しみにしています!

採用情報

現在、ファインディでは一緒に働くメンバーを募集中です。

興味がある方はこちらから。 herp.careers

Findyの爆速開発を支えるAI×チェックリスト型セルフレビュー

こんにちは。

ファインディ株式会社でテックリードマネージャーをやらせてもらってる戸田です。

現在のソフトウェア開発の世界は、生成AIの登場により大きな転換点を迎えています。

GitHub CopilotやClaude Codeなど生成AIを活用した開発支援ツールが次々と登場し、開発者の日常的なワークフローに組み込まれつつあります。

弊社でも例に漏れず、生成AIを活用して開発効率の向上に取り組んでいます。その中でFindy Team+で開発組織の生産性をチェックしていたところ、Pull requestの質が落ちているのでは?という仮説が浮かび上がりました。

今回は仮説が浮かんできた経緯と、その対策として導入したセルフレビューの仕組みについて紹介します。

それでは見ていきましょう!

思っていたよりPull requestの数が増えていなかった

稼働人数は昨年比で1.5倍程度増えており、それと比例する形でPull requestの作成数も単純に1.5倍に増えていました。この図から、人数の増加とPull request作成数が概ね比例していることが分かります。

Pull request作成数の推移

しかし、1人あたりのPull requestの作成数は、昨年とほぼ変わらずでした。この図を見ると、人数が増えても1人あたりのPull request作成数はほぼフラットであることが分かります。

1人あたりのPull request作成数の推移

生成AIにPull requestを作成してもらうのであれば、1人あたりの作成数も伸びて、増えた人数以上に総数も伸びるはずでは?ここが疑問ポイントでした。

Pull requestの数が多ければ必ずしも良いわけではありませんが、AIを導入しても1人あたりの数値が伸びていないということは、どこかしらに問題があるはずです。

Pull requestの作成数だけでは判断することが出来ないので、他の数値に目を向けてみました。すると、各種リードタイムが昨年比で悪化していることがわかりました。

その中でも私が特に目を付けたのが「レビューからApproveまでの平均時間」です。この数値が昨年比で平均約20分近くも伸びていたのです。

更に「平均コメント数」と「平均レビュー数」にも目を付けました。こちらも昨年比で30%近くも増えていたのです。

これらの数値の変化から、1つの仮説が浮かび上がりました。それは「レビューで指摘された内容の対応で各種リードタイムが伸びており、マージまでの時間が伸びて結果的にPull requestの作成数が伸びていないのでは?」ということです。

この仮説を一言で言うと、「作成されているPull requestの質が落ちているのでは?」と言い換えることもできます。仮説を整理できたので、その仮説が正しいのかどうかを検証することにしました。

理解しないままレビュー依頼を出していた

AIが作成したPull requestを幾つか確認してみたところ、確かにセルフレビューである程度防げるような指摘が多く見受けられました。

例えばAIが次のようなテストコードを追加しているシーンがありました。

import { render, screen } from '@testing-library/react';

it('should render with isHiddenTitle', () => {
    render(<HogeComponent isHiddenTitle />);

    expect(screen.queryByText("Title")).not.toBeInTheDocument();
});

一見特に問題なさそうなテストコードですが、仮にテキストが常に非表示になっている状態だった場合、このテストコードは常に成功してしまいます。つまり、テストコードとしての意味を成していないのです。

特定の条件下で「表示されないこと」を守りたいのあれば、その条件下以外で「表示される」ことも同時に守る必要があります。この変更に対してはリードクラスのエンジニアからレビューで指摘が入り、次のようなテストコードになりました。

import { render, screen } from '@testing-library/react';

it('should render', () => {
    render(<HogeComponent />);

    expect(screen.getByText("Title")).toBeInTheDocument();
});

it('should render with isHiddenTitle', () => {
    render(<HogeComponent isHiddenTitle />);

    expect(screen.queryByText("Title")).not.toBeInTheDocument();
});

このテストコードで文言が表示されること、特定の条件下のみで表示されないことの両方を守ることができるようになりました。

このように、AIが出力したコードを依頼主が理解しないままレビュー依頼を出しているケースが少なからず見受けられました。そしてそれらをレビューするリードクラスのエンジニアのレビュー負担が上がっており、結果的にマージまでのリードタイムが伸びている。という状態に陥っていたのです。

これらの多くは難しい問題点ではなく、セルフレビューの時点で気づくことが出来るような内容が大半でした。

AIが出力したコードの責任は人間にあります。 レビュー依頼を出す前に、まずセルフレビューをして、早い段階で問題点に気付けるように仕組みを作ることにしました。

AIでセルフレビューを支援する仕組みを導入

自動でレビューをしてくれるサービスも利用したのですが、汎用的な内容でのレビューなのでドメイン知識や個人の癖などを考慮出来ておらず、指摘内容としても薄いものが多く、レビュー依頼する前のセルフチェックという意味合いでは不十分でした。

そこで、自分自身にカスタマイズされたセルフレビューのチェックリストを作成する仕組みを内製することにしました。

流れとしてはこうです。

まず直近数カ月で自分が作ったPull requestの一覧を取得します。そのPull requestに対して作成されたレビューコメントを全て取得します。それらレビューコメントの全てをLLMに渡して、どういう内容や傾向で自分自身が指摘されているのかを分析後にチェックリストを作成します。

作成されたチェックリストに沿って、Claude Codeにセルフレビューしてもらいます。

一連の流れはこのようになります。

セルフレビューの仕組みのシーケンス図

このシーケンス図から、チェックリストの生成からセルフレビュー、修正の反映までが一連のフローとして自動化されていることが読み取れます。

この一連の流れを全て自動で行うカスタムコマンドを作成し、Pluginsに入れて社内に展開しました。

チェックリストを更新するカスタムコマンドと、そのチェックリストを使ってセルフレビューを実行するカスタムコマンドは分けています。チェックリストは定期的に更新すれば良いので、セルフレビューの度に更新する必要はないからです。

まずチェックリストを作成するコマンドを実行して、次のようなファイルが出力されます。

# Self-Review Checklist for hoge

このチェックリストは、過去3ヶ月間(2025年9月30日〜2025年11月26日)にマージされたPRとレビューコメントを分析して作成されました。

## 🧪 テストコード

### アサーションの品質
- [ ] `toEqual` の代わりに `toStrictEqual` を使用しているか
  - より厳密な型チェックのため
- [ ] テストケースは実装の意図を正確に検証しているか
- [ ] スナップショットテストは適切に使用されているか

### テストの網羅性
- [ ] 主要な機能パスはすべてカバーされているか
- [ ] エラーハンドリングのテストは含まれているか
- [ ] エッジケースを考慮しているか

## 🎨 UI/UX 設計

### アクセシビリティ
- [ ] aria-label は適切に設定されているか
- [ ] キーボード操作に対応しているか
- [ ] カラーコントラストは十分か

## 🎯 コミットメッセージ

### Conventional Commits
- [ ] プロジェクトのコミットメッセージ規約に従っているか
- [ ] 適切な type を使用しているか(feat, fix, refactor, chore, etc.)
- [ ] scope は正しく指定されているか
---

**最終更新日**: 2025-11-28
**分析期間**: 2025-09-30 〜 2025-11-26

次にセルフレビューのコマンドを実行します。出力されたチェックリストの内容に沿って、変更箇所をレビューしてもらいます。実際の出力例は次のようになります。

> 変更箇所に対して、`self-review-check-list.md` の内容を元にレビューしてください

⏺ 変更箇所をレビューするために、まず self-review-check-list.md の内容を確認します。

⏺ Search(pattern: "**/self-review-check-list.md")
  ⎿  Found 1 file (ctrl+o to expand)

⏺ Read(self-review-check-list.md)
  ⎿  Read 33 lines

⏺ Read(packages/feature-server/src/clients/github/github-client.spec.ts)
  ⎿  Read 863 lines

⏺ self-review-check-list.md の内容に基づいて、変更ファイルのレビューを実施しました。

  レビュー結果

  ✅ コード品質

  命名の明確さ
  - 関数名や変数名が実際の動作を正確に表現しています
  - ドメイン用語(GitHub API関連)を正しく使用しています

  ✅ テストコード

  テストの網羅性
  - 各関数に対して以下のテストケースを網羅しています:
    - すべてのパラメータを指定した成功ケース
    - 必須パラメータのみの成功ケース
    - エラーケース
  - toStrictEqual を使用してオブジェクト全体の検証を適切に行っています

  テスト依存関係
  - 不要なテスト依存関係は追加されていません
  - プロジェクト標準のテストライブラリのみを使用しています

  ✅ プロジェクト固有のルール

  - TypeScript Deep Dive のスタイルガイドに従っています
  - ファイル名がケバブケース(github-client.spec.ts)になっています

  ⚠️ 改善提案

  テストアサーションの統一性

  packages/feature-server/src/clients/github/github-client.spec.ts:307

  expect(response.data).toEqual(mockData);

  他のテストケースでは toStrictEqual を使用していますが、ここだけ toEqual を使用しています。toStrictEqual に変更することを推奨します:

  expect(response.data).toStrictEqual(mockData);

  toStrictEqual は toEqual よりも厳密なチェック(undefined
  プロパティも検証)を行うため、より堅牢なテストになります。

  ---
  全体的に非常に高品質なテストコードです。上記の1点のみ修正すれば、チェックリストのすべての項目を満たしま
  す。

テストコードを変更した場合のセルフレビュー例ですが、コード品質やテストコード、プロジェクト固有のルールに関しては基本的にクリアしていましたが、改善提案として1点のみ指摘がありました。

このレビュー結果を元に、修正も自動で行ってもらいましょう。続いてプロンプトを実行します。

> レビュー結果を元に、変更箇所を修正してください 

⏺ レビュー結果に基づいて修正します。

⏺ Update(packages/feature-server/src/clients/github/github-client.spec.ts)
  ⎿  Updated packages/feature-server/src/clients/github/github-client.spec.ts with 1 addition and 1
     removal

       304            }
       305          );
       306          expect(response.status).toBe(HttpStatusCode.OK);
       307 -        expect(response.data).toEqual(mockData);
       307 +        expect(response.data).toStrictEqual(mockData);
       308        });
       309    
       310        it('fetch data with only required params', async () => {

⏺ 修正が完了しました。

  修正内容

  packages/feature-server/src/clients/github/github-client.spec.ts:307

  変更前:
  expect(response.data).toEqual(mockData);

  変更後:
  expect(response.data).toStrictEqual(mockData);

  この修正により、テストアサーションが統一され、すべてのテストケースで toStrictEqual
  を使用するようになりました。toStrictEqual は toEqual よりも厳密なチェック(undefined
  プロパティも検証)を行うため、より堅牢なテストになります。

  これで self-review-check-list.md のすべての項目を満たすコードになりました。

この仕組みにより、定期的にチェックリストのファイルを更新することによって、セルフレビューの精度を上げ続けることが可能となります。

まとめ

今回はジュニアエンジニアがAIを使ってPull requestを作成した場合に、セルフレビューでのチェックが甘くなり、レビュワーの負担が上がっているといったケースでした。

「AIに使われる」のではなく、「AIを使う」ためにも、AIが出力したコードを理解することが必要不可欠です。AIが出力したコードの責任は人間にあります。自分自身の責任を果たす意味でも、生成AI時代のセルフレビューが持つ意味合いは、これまで以上に重要になってくるでしょう。

現在、ファインディでは一緒に働くメンバーを募集中です。

興味がある方はこちらから ↓ herp.careers

LangChainのastreamでLLM分析をストリーミング対応してUXを改善した話

こんにちは。

ファインディ株式会社でFindy AI+の開発をしているdanです。

Findy AI+ではLLMを活用した分析機能を提供しています。 分析対象は個人・チーム・組織と幅広く、データ量に応じて分析に時間がかかることがあります。分析が完了するまで画面に何も表示されないと、ユーザーは処理が進んでいるのか分からず、待ち時間が長く感じられてしまいます。

この課題を解消するため、LLM分析結果の表示にストリーミング出力を導入しました。

今回は、実装内容とどの程度待ち時間が改善されたのかについてお話しします。

Findy AI+とは

Findy AI+は、GitHub連携やプロンプト指示を通じて生成AIアクティビティを可視化し、生成AIの利活用向上を支援するサービスです。

人と生成AIの協働を後押しし、開発組織の変革をサポートします。

Claude Code、GitHub Copilot、Codex、Devinなど様々なAIツールの利活用を横断的に分析しており、分析基盤にはLangChainを採用しています。また、日報やチーム分析などの機能でもLLMを活用しています。

LLM分析に使用するプロンプト調整についても記事を公開していますので、よかったらご覧ください。

tech.findy.co.jp

ストリーミング対応前は何が問題だった?

当初の設計

当初の設計

分析結果を見るのにどのくらいの時間を要していたのか

冒頭で述べた通り、Findy AI+では個人・チーム・組織と幅広いデータを分析対象としています。

分析に必要なデータ作成のAPIを例にあげます。

@router.post("/api/v1/hoge")
def create_hoge(
    request: CreateHogeRequest,
    db: Session = Depends(get_db),
):
    # 1. データ取得
    hoge = get_hoge(db, request.hoge_id)

    # 2. LLM呼び出し(全レスポンス待ち)
    client = Anthropic(api_key="xxx")
    message = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=4096,
        messages=[{"role": "user", "content": f"分析して: {hoge.content}"}],
    )
    analysis_result = message.content[0].text

    # 3. 分析結果保存
    save_analysis(db, hoge.id, analysis_result)

    # 4. レスポンス返却
    return {"id": hoge.id, "analysis": analysis_result}

やっていることは次の2つです。

  1. LLM分析を行い完了後に分析結果を保存
  2. フロントへデータを返す

APIを呼び出してみると平均して30~40秒ほど時間がかかっていることが分かりました。

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100  7341  100  6673  100   668    193     19  0:00:35  0:00:34  0:00:01  2059

実行してから分析結果を見るのに30~40秒程度かかるほど、遅すぎて使いものにならない状態でした。

どのように対応を進めたか

ストリーミング対応の設計

新たに作成したストリーミングのAPIは下記の通りです。 ストリーミングAPIのシーケンス図

肝となる実装の全体像

ストリーミング対応したサンプルコードは下記になります。

@router.post("/api/v1/hoge/analysis/streaming")
async def create_hoge_analysis_streaming(
    request: AnalysisRequest,
    db: Session = Depends(get_db),
):
    # 1. データ取得
    hoge = get_hoge(db, request.hoge_id)

    # 2. LangChainエージェント作成
    agent = ChatAnthropic(
        model="claude-sonnet-4-20250514",
        anthropic_api_key="xxx",
        max_tokens=4096,
    )

    # 3. StreamingResponse返却
    return StreamingResponse(
        generate_streaming_response(agent, hoge, db),
        media_type="text/event-stream",
    )


async def generate_streaming_response(
    agent: ChatAnthropic,
    hoge: Hoge,
    db: Session,
) -> AsyncGenerator[str, None]:
    """SSE形式でストリーミングレスポンスを生成"""
    accumulated_content = ""
    messages = [HumanMessage(content=f"分析して: {hoge.content}")]

    # チャンクごとに送信
    async for chunk in agent.astream(messages):
        if chunk.content:
            accumulated_content += chunk.content
            data = json.dumps({"type": "content", "content": chunk.content})
            yield f"data: {data}\n\n"

    # 完了後にDB保存
    save_analysis(db, hoge.id, accumulated_content)

    # 完了イベント送信
    data = json.dumps({"type": "complete", "content": accumulated_content})
    yield f"data: {data}\n\n"

やっていることは次の2つです。

  1. 分析結果がチャンクで返ってくるので、そのままフロントに渡す
  2. 分析完了後に分析結果を保存・フロントに返す

どのように分析結果をチャンク(断片したテキスト)形式で受け取るのかについて説明します。 これは、LangChainのストリーミングメソッドを使用することで実現可能です。

LangChainの astream() メソッド

async for chunk in agent.astream(messages):
    print(chunk.content)  # "Find" → "y AI+" → "AI利活用" のように少しずつ届く

astream() は非同期ジェネレータを返し、LLM APIからのレスポンスをチャンク単位で受信できます。

通常の invoke() との違いは全文が返らないことです。

# invoke(): 全文が返るまで待つ
result = agent.invoke(messages)  # 数秒〜数十秒ブロック

# astream(): チャンクごとに即座に処理できる
async for chunk in agent.astream(messages):  # 即座に開始
    process(chunk.content)

FastAPIでのストリーミング実装

@router.post("/api/v1/hoge/analysis/streaming")
async def create_hoge_analysis_streaming(request: AnalysisRequest):
    hoge = get_hoge(request.hoge_id)
    agent = ChatAnthropic(model="claude-sonnet-4-20250514", ...)

    return StreamingResponse(
        generate_streaming_response(agent, hoge),
        media_type="text/event-stream",
    )

StreamingResponse に非同期ジェネレータを渡すことで、yield するたびにクライアントへデータが送信されます。

ストリーミングレスポンスの生成

async def generate_streaming_response(agent, hoge) -> AsyncGenerator[str, None]:
    accumulated_content = ""
    messages = [HumanMessage(content=f"分析して: {hoge.content}")]

    # チャンクごとにフロントへ送信
    async for chunk in agent.astream(messages):
        if chunk.content:
            accumulated_content += chunk.content
            data = json.dumps({"type": "content", "content": chunk.content})
            yield f"data: {data}\n\n"

    # 完了後にDB保存 & 完了イベント送信
    save_analysis(hoge.id, accumulated_content)
    data = json.dumps({"type": "complete", "content": accumulated_content})
    yield f"data: {data}\n\n"
  • accumulated_content で全文を蓄積しDB保存の準備を行います
  • yield f"data: {data}\n\n" でSSE形式でフロントへ送信することでストリーミング対応を行います

SSE(Server-Sent Events)の仕組み

StreamingResponse で media_type="text/event-stream" を指定することで、SSE形式でデータを送信できます。

SSEでは data: {JSON}\n\n という形式でイベントを送信します。クライアントはこのイベントを受信するたびに画面を更新できるため、LLMの出力をリアルタイムで表示できます。

data: {"type": "content", "content": "Findy "}\n\n
data: {"type": "content", "content": "AI+では"}\n\n
data: {"type": "content", "content": "..."}\n\n
data: {"type": "complete", "content": "Findy AI+では..."}\n\n

フロントエンドでの受信処理

フロントエンドでは fetchReadableStream を使ってストリーミングデータを受信します。

const response = await fetch('/api/v1/hoge/analysis/streaming', {
  method: 'POST',
  body: JSON.stringify({ hoge_id: 123 }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const chunk = decoder.decode(value);
  // "data: {...}\n\n" 形式のイベントをパース
  const events = chunk.split('\n\n').filter(e => e.startsWith('data: '));

  for (const event of events) {
    const data = JSON.parse(event.replace('data: ', ''));
    if (data.type === 'content') {
      // チャンクを画面に追加表示
      appendText(data.content);
    } else if (data.type === 'complete') {
      // 完了処理
    }
  }
}

このように、サーバー側でチャンクを yield するたびにフロントエンドで受信・表示することで、ユーザーは分析結果をリアルタイムで確認できます。

ストリーミング対応後に得られた効果

画面遷移から3秒程度で読み始めることができるので非常に使い勝手が良くなりました。

ストリーミングで表示されている画像

おわりに

ストリーミング対応をしたことで分析に使用するデータ量が多くても逐一分析結果が表示されるようになりました。

LangChainを使用したストリーミング対応について少しでも参考になれば幸いです。

現在、ファインディでは一緒に働くメンバーを募集中です。

興味がある方はこちらからご応募ください。 herp.careers

AI時代のDependabot対応。手動からDevin、そしてClaude Code Actionへ

こんにちは、Findy Freelanceでフロントエンドエンジニアをしている主計(かずえ)です。

この記事は、ファインディエンジニア #3 Advent Calendar 2025の25日目の記事です。

adventar.org

Dependabotが作成するPRの対応、皆さんはどのように運用していますか?依存パッケージの更新は地味ながら継続的に発生する作業で、特に少人数チームでは対応工数の比率が無視できません。

この記事では、私たちのチームがDependabot PR対応を手動運用からDevin、そしてClaude Code Actionへと段階的に改善してきた過程を紹介します。それぞれのアプローチで得られた知見と、最終的にClaude Code Actionに落ち着いた理由をお伝えします。

Dependabot PRの対応フローを効率化したい方、AIツールを活用したコードレビューの自動化に興味がある方の参考になれば幸いです。2025年の変遷を書いているのでDependabot PRのAIレビューをこれから実施したい場合はClaude Code Actionのセクションを中心に見ていただければと思います。

手動時代

フロー

Dependabot PRへの対応は、次のような流れで行っていました。

  1. Dependabotが作成したPRを確認する
  2. CIの実行結果を待ち、成功・失敗を確認する
  3. 更新されたパッケージのリリースノートを確認し、Breaking Changesがないか調べる
  4. CIが失敗している場合は原因を調査し、必要に応じてコードを修正する
  5. 問題がなければレビューを行う
  6. マージする

一見シンプルなフローですが、実際に運用してみると様々な課題があります。

課題

まず、コンテキストスイッチのコストが大きいという問題がありました。Dependabot PRを順番にマージしていくと、他のPRとコンフリクトが発生してrebaseが必要になります。CIの実行を待っている間に別の作業を始め、CIが完了したら戻ってきて次のPRを処理する、という流れで作業が細切れになりがちでした。

リリースノートの確認も手間のかかる作業でした。Breaking Changesは基本的にCIが失敗するので気付けますが、稀にruntimeで問題が発覚することがあります。そのため、念のためリリースノートを確認してからマージするようにしていました。

また、人によってマージの判断基準にズレがあるという問題もありました。あるメンバーはCIが通っていればすぐにマージし、別のメンバーはリリースノートを細かく確認してからマージするといった具合です。リリースノートを確認するというルールは作れば防げますが、どれくらい詳細に確認するかなど個人の感覚に依存してしまいます。

少人数チームでは、こうした定型作業の工数比率が相対的に高くなります。週に数時間をDependabot PR対応に費やしていると、本来注力すべき開発作業に割ける時間が減ってしまいます。

Devinへ移行

こうした課題を解決するため、導入済みだったDevinのPlaybookをDependabot PRのレビューに活用してみることにしました。

Playbookとは

DevinにはPlaybookという機能があります。これは繰り返し行うタスクの手順を定義しておき、再利用できる仕組みです。「Dependabot PRをレビューする」というPlaybookを作成しておけば、毎回同じ手順でレビューを実行できます。

次のような形でPlaybookに記載して繰り返し利用しておりました。 Playbookの例

SlackからPlaybookを呼び出して実行

DevinはSlackと連携できるため、Slackから直接Playbookを呼び出すことができます。 「@Devin playbook:!hoge」とメンションするだけで、定義したPlaybookが実行される仕組みです。

SlackからDevinのPlaybookを実行

レビューが実施されると各PRにコメントを残してくれます。

承認時

Devin承認時

非承認時

Devin非承認時

良かった点

Devinを導入して良かった点は、1つのアクションで各PRのレビュー依頼が完結することでした。Slackでメンションするだけでレビューが始まるため、Devinの作業が完了した後は、各PRについたレビューを確認し、マージするだけで済むようになりました。

運用していく中で見えてきたこと

複数のDependabot PRが同時に作成された場合、一部のPRが漏れてしまうケースがありました。また、すでにレビュー済みのPRを再度レビューしてしまうこともありました。

これらはDevin自体の問題ではなく、1回の依頼で複数PRをまとめて処理させていた運用方法に起因する課題でした。DevinにはAPIが提供されているため、GitHub ActionsからPRごとにDevin APIを呼び出す仕組みを作れば解決できる課題と考えました。

Claude Code Actionへ移行

GitHub ActionsからDevin APIを呼び出す仕組みの構築を検討していたところ、Claude Code Actionがリリースされました。

Claude Code Actionは、名前の通りGitHub Actions上でClaude Codeを実行できるActionです。PRの作成やコメントをトリガーにして、自動でコードレビューやタスクの実行ができます。

PRごとにワークフローが実行されるため、「このPRに対してレビューを行う」というスコープが明確になります。公式のGitHub Actionとして提供されていることと、AIモデルを選択できることが決め手となり、Claude Code Actionへ移行することとしました。

bot作成PRでは動作しないためClaude Code Base Actionを利用

導入当初、Claude Code ActionにはDependabotのようなbotが作成したPRでは実行できないという制約がありました。

この制約を回避するため、Claude Code Base Actionを利用することにしました。Base Actionはbot作成のPRでも動作させることができ、機能的にもClaude Code Actionと同等のことができます。

allowed_botsでClaude Code Actionへ移行

その後、Claude Code Actionにallowed_botsオプションが追加されました。これにより、Dependabotが作成したPRでもClaude Code Actionを実行できるようになりました。

Claude Code Base Actionからの移行を行った理由は、機能的な差はほとんどないものの、他のCIワークフローとの一貫性を保つためでした。チーム内で複数のActionを使い分けるよりも、統一した方がメンテナンス性が高くなります。

各CI完了後にレビューを実施

Claude Code Base Actionのワークフローはlint、test、typecheckなどのCIジョブが完了した後にClaude Code Base Actionを実行するようにしました。そうすることで、CIの結果を踏まえたレビューができます。

CIの流れは次のようなイメージです。

jobs:
  test:
    ...(省略)...
  lint:
    ...(省略)...
  typecheck:
    ...(省略)...
  claude-review-for-dependabot-pr:
    name: Claude Review for Dependabot PR
    # `always()`を指定しCIが失敗しても実行
    if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && always() }}
    # 各CIの結果が欲しいので終了してから実行
    needs: [test, lint, typecheck]
    ...(省略)...
    steps:
      - name: Checkout repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - name: Claude Review for Dependabot PR
        uses: anthropics/claude-code-action@f0c8eb29807907de7f5412d04afceb5e24817127 # v1.0.23
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          github_token: ${{ github.token }}
          show_full_output: false #デバッグしたいときはshow_full_outputをtrueにして確認
          allowed_bots: dependabot
          claude_args: |
            --allowedTools "View,GlobTool,GrepTool,BatchTool,Bash(gh auth status),Bash(gh pr view:*),Bash(gh run list:*),Bash(gh pr comment:*),Bash(gh pr review:*),Bash(gh pr diff:*),Bash(gh pr checks:*),Bash(gh run view:*),Bash(gh release view:*),Bash(git log:*),Bash(git status:*),Bash(git diff:*)"
            --model claude-opus-4-5-20251101
          prompt: |
            (プロンプト例は下記に記載)

プロンプト例

## 命令
CI実行中のDependabotのプルリクエスト #${{ github.event.pull_request.number }} を評価しマージして問題ないか判断してください

## 手順
1. プルリクエストを確認(gh pr view ${{ github.event.pull_request.number }})
2. package.jsonとpackage-lock.jsonの差分を確認
3. バージョン差分から公式リリースノートとGitHubのリリースを確認しライブラリの変更内容を把握(gh release view)
4. test, lint, typecheckの実行結果を確認(gh pr checks ${{ github.event.pull_request.number }})
5. test, lint, typecheckが失敗していた場合、ログを確認(gh run view)
6. 既存コードや他ライブラリのバージョンとの整合性を確認
7. 変更内容を評価しフォーマットに沿ってプルリクエストにレビューコメント(日本語)を1件投稿

## フォーマット
```
# {ライブラリ名} x.x.x -> x.x.x
## 評価結果
✅ 承認 / ⚠️ 確認が必要 / ❌ 要対応 / etc...
## ライブラリの変更内容
- 変更内容1
- 変更内容2
## リスク、注意事項
- リスク1
- リスク2
## 必要な対応
- 対応1
- 対応2
```

## 注意事項
- 承認する場合:`gh pr review {プルリクエスト番号} --approve --body "{BODY}"`
- 承認しない場合:`gh pr comment {プルリクエスト番号} --body "{BODY}"`
- 実行時に足りない権限があった場合はコメント

## 追加確認項目
- Breaking Changesの有無
- セキュリティ脆弱性の修正状況
- 他の依存関係への影響

他のCIジョブの完了を待ってからActionを実行することで、CIが失敗している場合は「なぜ失敗したか」の分析をさせることができます。失敗ログから原因を特定し、次に取るべきアクションをPRコメントとして残すことができます。 対応すべきことがわかれば対応時の工数感がわかりすぐ取り掛かるかタスクとして積んでおくかの判断がしやすいです。

現在のフロー

  1. DependabotがPRを作成する
  2. lint、test、typecheckのCIジョブが実行される
  3. CIジョブ完了後、Claude Code Actionが実行される
  4. Claude Code Actionが更新内容の要約、リリースノートの重要ポイント、CI失敗時の原因分析と修正提案をPRコメントとして残す
  5. 人間がコメントを確認し、最終判断を行う
  6. 問題なければマージする

人間が行う作業は「Claude Code Actionのコメントを確認して最終判断する」ことに集中できるようになりました。

承認時

Claude Code Action承認時

非承認時

Claude Code Action非承認時

効果

この改善により、Dependabot PR対応にかかる工数が週あたり約30分〜1時間削減されました。 削減される主な内訳としては、リリース内容の確認と対応要否の調査にかかる時間です。 必要な対応が出力されるのですぐ対応するかタスクとして積むかの判断がしやすくなったのは良かったと感じます。

また、レビューの観点がプロンプトで統一されているため、属人性が低下しました。誰が対応しても同じ基準でレビューが行われます。

コンテキストスイッチのコストも低下しました。コンフリクトしたPRは自動でrebaseと再レビューがされるため、自分の切りがいいタイミングでマージするだけで済むようになりました。

注意点

ANTHROPIC_API_KEYはDependabot用のSecretに設定

ANTHROPIC_API_KEYはGitHub ActionsのSecretsだけでなく、Dependabot側のSecretsにも設定する必要があります。Dependabotが作成したPRでは、通常のRepository Secretsにアクセスできないためです。

設定場所はhttps://github.com/{Org}/{Repo}/settings/secrets/dependabotです。これを忘れると、Dependabot PRでClaude Code Actionが動作しません。

WebFetchの不安定さ

当初、リリースノートの取得にWebFetch機能を使っていましたが、フリーズすることがありました。(2025年11月ごろ)

対策として、外部情報の取得はghコマンドを使う方針に変更しました。GitHub CLIを使ってリリース情報を取得する方が安定性と再現性が高く、トラブルが減りました。

Claude Code側でWebFetchがフリーズするissueがいくつかあります。show_full_outputを有効化してもログが表示されないため直接的な原因は掴めていませんが、現状ではghコマンドで必要な情報は取得できているのでallowedToolsからWebFetchは一時的に除外しています。 github.com

まとめ

Dependabot PR対応を手動運用からDevin、そしてClaude Code Actionへと改善してきた過程を紹介しました。

定期的に発生する作業は自動化すると効果が大きいです。特にDependabot PRのような「PRごとに自動でトリガーされる」タイプのタスクには、GitHub Actionsと連携するClaude Code Actionが適していました。

今後の課題としては、Claude Code ActionがApproveしたPRを自動マージする仕組みの導入を検討しています。現在nxを利用したモノレポで運用しており、ユニットテストやVRTは各アプリで整備できています。ただし、管理画面など一部のアプリではE2Eテストが未整備のため、まだ自動マージには踏み切れていません。E2Eテストを拡充し、自動マージしても安全という確信を持てる体制を整えていきたいと考えています。

上記のように、AIのガードレール整備が重要なのでテスト等をしっかりやっていくことは今後さらに重要になってくると再認識しました。


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。 興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers

DuckDB as a Pipeline: Findyデータ基盤におけるDuckDBの活用事例

この記事は「ファインディエンジニア #1 Advent Calendar 2025」の24日目の記事です。

沢山のアドベントカレンダー記事が執筆されていますので、年末のお供に是非読んでみてください。

adventar.org

はじめに

ソフトウェアエンジニアの土屋(@shunsock)です。私の所属するデータソリューションチームでは、ファインディ全体のデータ活用を推進するためのデータ基盤を構築しています。

今回、我々はデータ基盤のRDSとBigQueryのテーブル同期システム (EL Pipeline) のリプレースを行い、DuckDBを本番導入しました。本稿では、活用に至った経緯と実際に組みこむにあたる課題、および成果を紹介します。

ファインディにおけるテーブル同期システムの立ち位置

ファインディでは、ウェブアプリケーションをAWS上のECSとRDS、データ基盤をGoogle CloudのBigQueryで作成しています。

このような構成を取っているため、AWSのRDSとGoogle CloudのBigQueryを同期してテーブルを最新にする必要があります。

次の図は、Findy Tools事業部における、現在のデータフローの概念図です。AWS上に存在するRDSのデータをBigQueryに転送していることが分かります。

リプレイスの背景

弊社では従来、OSSのEL(Extract Load) ツール Embulk をECSに載せて長期間運用していました。弊社で利用しているRDBMSやデータウェアハウスに対応している他、社内に知見を持った方が在籍しているためです。

しかし、近年では、 Embulkのエコシステムのレガシー化や長期的なメンテナが不足が課題 となっています。特に、 将来のメンテナンスが不透明な点は、セキュリティインシデントに繋がりかねない ため危惧していました。

また、 Embulkの起動の遅さも課題 にしていました。我々はBigQueryプラグインなどを利用していたため、JVM上でさらにJRuby VMを立ちあげます。このような構成は テーブル同期の遅さに繋がり、ECSの課金額を増やす要因 となっていました。

このように、システムを堅牢にすることと処理スピード向上による料金のコストダウンが今回のプロジェクトの目的でした。

補足

Embulkのメンテナーの方も「オープンソース・プロジェクトのたたみ方」というブログ記事で脆弱性について次のように述べています。

おそらくいくつかの攻撃は既に成功していて、私たちのソフトウェア・サプライチェーンには、悪意のあるコードがとっくに入り込んでいる、と認識しておくべきでしょう。

技術選定

Datastream, Spark, その他 ELTツールなど、複数の移行先候補がありました。その中で、データ規模に応じて次の2つから選定することにしました。

  • Datastream: ニアリアルタイムでの更新が欲しい場合や大規模データの場合
  • DuckDB: 小規模データの場合

Datastream

Datastream は Google Cloudが提供するサーバーレスのCDC (Change Data Capture), Replicationツールです。

CDCは、あるソースのシステムを監視し、そのシステムに対する操作をニアリアルタイムで、ターゲットとなるシステムに反映する仕組みのことです。これによりAWSのRDSに対する変更を即座にBigQueryに反映可能です。

DuckDB

DuckDBは高速なアナリティカルデータベースです。s3などのストレージサービスに出力されたログ分析やファイルフォーマットの変換、wasmによるフロントエンドでの活用など広い用途で活用されています。

接続先や出力フォーマットが非常に豊富な他、C++製のマルチスレッドランタイムにより、高速に動作する点が魅力です。

次の写真はDuckDBのPoC時に行なったベンチマークです。小さなテーブルで転送を試したところ、1.5倍程度の高速でした

ソフトウェア名称 平均 標準偏差 最速 最遅
Embulk 253秒 8秒 242秒 261秒
DuckBD 176秒 30秒 137秒 209秒

補足: 実際にパフォーマンステストを行ったときの様子

Datastream, DuckDB両採用の理由

今回のリプレイスでは、コスト最適化を軸に Datastream と DuckDB の2種類のアプローチを使い分ける構成を採用しました。

DatastreamはフルマネージドでサーバーレスなCDCサービスと強力です。一方で、ニアリアルタイム性が不要な小規模データに対しては機能過多となり、費用面でも割高になります。そこで、リアルタイム性を求めない領域では、より軽量でシンプルに扱えるDuckDBを使って同期を行う方針を取りました。

本記事の以降では、上記のうち、DuckDBによってどのようにテーブル同期システムを構築したか、開発運用で見えた知見を説明します。

システム設計

概要

次の画像は我々のDuckDBによるテーブル同期システムの概念図です。

次のように各種ソフトウェアが起動します。

  1. GitHub Actionsのon_schedule でワークフローが起動
  2. ワークフローがECS Fartate Taskを起動
  3. Fargate Taskがコンテナランタイムを起動
  4. コンテナランタイムの中でCLIアプリケーションが起動
  5. CLIアプリケーションが引数と設定ファイルからSQLを生成
  6. CLIアプリケーションがDuckDBでSQLを実行

CLIを挟む理由

DuckDBを直接起動しない理由は、1回の実行で1テーブルずつ送信できるようにするためと、SQLを直接書かずに設定ファイルをインターフェースにするためです。

実際のユーザーの入力インターフェースは次のようなYAMLです。

dataset_id: lake...
table_name: table_name
select_statement: "hoge, fuga, ..."

GitHub Actionsからの起動にした理由

元々のワークフローはEventBridge Schedulerだったのですが、システム障害時にEventBridgeのcronを変更するなど運用負荷が重い状態でした。DispatcherをGitHub Actionsにすることでボタン操作だけで検証可能にしました。

また、1テーブルずつの送信にしたので、ステージング環境での動作検証も簡単かつ軽量です。ユーザーは次のようなWorkflow Dispatchを起動するだけで動作検証が完了します。

複数のRDSを転送する

現在のFindy Tools事業部のワークフローを見ると分かる通り、複数のRDSを転送する必要がありました。そこで開発用スクリプトを汎用化して動的なビルドやawsコマンドの発火をしています。

開発運用と成果

開発は、私1人で1か月弱でしました。最初の1プロジェクトこそ時間がかかったものの、モノレポ構成にしたおかげで従来1か月かかった新規データソースの追加が1週間程度になりました。

処理速度については、直列稼動から並列稼動へ変更となったため単純な比較は難しいのですが、1テーブルあたり約30秒から約10秒に短縮できました。

すでに他のメンバーからもプルリクエストが届いており、社内でも手応えのある反応を得ています。

開発・運用してみた感想

可読性と拡張性が高い

今回作成したCLIでは次のようなSQLを生成しています。高い拡張性や可読性が良いと改めて感じました。

INSTALL mysql;
LOAD mysql;
ATTACH '' AS mysqldb (TYPE mysql); -- 環境変数から取ってくる
CREATE TABLE users AS
SELECT *
FROM mysqldb.table_name;

INSTALL bigquery FROM community;
LOAD bigquery;
ATTACH '' as bq (TYPE bigquery);
DROP TABLE IF EXISTS bq.lake__system_name.table_name;
CREATE TABLE bq.lake__system_name.table_name AS SELECT * FROM table_name;
DROP TABLE table_name;

拡張についても、次のCore Extensionsの他にCommunity Extensionsがあります。DB以外にもSpreadSheetなど幅広いツールが対応しているので、興味を持った方は確認してみると良いと思います。

duckdb.org

とはいえまだまだ新興のソフトウェア

DuckDBは新興のソフトウェアということもあり、普通にバグがあったりします。例えば次のIssueは、私がDuckDBのMySQLのプラグインのATTACH句に存在したバグを報告したものです。(既に解決済みです)

github.com

また、拡張によっては、サポートしているOSが限られていることがあります。私が作成した時期では、BigQuery拡張でarm64 linuxがサポートされておらず、Fargateをamd64で立てていました。なお、こちらも現在は対応しているようです。

github.com

まとめ

今回の取り組みで、我々のテーブル同期システムはより高速、堅牢になりました。さらに、ユーザーインターフェースが洗練され、チームメンバーの利用しやすいソフトウェアとなりました。

データソリューションチームでは一緒に事業部横断データ基盤を作る仲間を募集しています。気になる方は是非次のフォームからカジュアル面談に応募してみてください!!

herp.careers

瞬間的なアクセス集中はオートスケールに検知されない ― GitHub Actionsでコンテナ事前調整を自動化

こんにちは。

2025 年 9 月にファインディに入社し、 Platform 開発チームで SRE を担当している富田(@Cooking_ENG)です。

この記事は、ファインディエンジニア #2 Advent Calendar 2025の 23 日目の記事になります。

adventar.org

今回は、ファインディのサービスの1つである「Findy Conference」のインフラ環境の運用トイルを改善した話を紹介します。

Findy Conference とは

Findy Conference とは、テックカンファレンスに特化したプラットフォームサービスです。

国内外のカンファレンスに関する情報・体験を一元化し、主催者・参加者・スポンサーをつなぐことで、テックカンファレンスの体験を最大化することを目指します。

参加者は関心のあるイベント情報や CFP(発表募集)、イベントのタイムテーブルを見逃さずに把握でき、主催者は集客や受付管理、データ活用といった運営にかかるコスト・工数を最小化できるようになります。

conference.findy-code.io

Findy Conference のトラフィックの特徴

Findy Conference のトラフィックには、一般的な Web サービスとは異なる、カンファレンス特有の瞬間的なスパイクが多く発生するという特徴があります。

特にスパイクが起こりやすいタイミングは次の 2 点でした。

  • 受付が始まったタイミング
  • セッションの始まりと終わりのタイミング

このうち、特に負荷が高かったのがセッションの始まりと終わりのタイミングです。

原因は次のような、オンライン配信におけるカンファレンス参加者の方々の動きによって起こるものでした。

  1. セッション中は参加者の方々は配信セッションを視聴しているため、Findy Conference のポータルサイトなどにアクセスすることは少ない。

  2. セッションが終了するタイミングで、次のセッションの配信場所やチャンネル切り替えを行うため、セッションを視聴していた方々が一斉に Findy Conference のポータルサイトにアクセス。

  3. その結果、一気にアクセスが集中し、スパイクが発生!

この一連の流れにより、インフラに瞬間的な高負荷がかかっていました。

オートスケールが発動しない

Findy Conference の環境は Amazon ECS と AWS Fargate (以降 ECS/Fargate) を使って構築しています。

ECS/Fargate であれば、オートスケールが発動するのではないか?という疑問を持つ方もいらっしゃると思います。実際に、Findy Conference の環境でも、CPU 使用率が設定している閾値を超えたらオートスケールが発動するように設定していました。

しかし、実際の運用では CPU 使用率が設定している閾値を超えてもオートスケールが発動せず、高負荷時にユーザー体験を維持できないリスクが生じていました。

オートスケールが発動しなかった原因は「アクセス集中が瞬間的すぎる」という点にありました。

  • 短時間の高トラフィック・スパイクにより、オートスケールが発動に必要な時間を満たせず、新しいコンテナが立ち上がる前に CPU 使用率が下がってしまう
  • Fargate の起動が少し遅いという特性も影響

結果として、カンファレンス中に上記のような瞬間的な高負荷がかかっても、コンテナ数はスケールせず終わってしまうという状況でした。

(参考までに、こちらは過去のオブザーバビリティカンファレンスでの CPU グラフです。同時刻にオートスケールが発動していないことがわかります。)

CPU image containers image

これまでのカンファレンス開催時の SRE の対応

上記の問題を回避するため、以前は「カンファレンス開催前に、Platform 開発チーム SRE メンバー(以降、SRE チーム)が AWS の Production 環境に入り、手動でコンテナ数を増やし、カンファレンス終了後にコンテナ数を元に戻す」という対応をしていました。

スパイク時の対策としては、当時はコンテナ台数を増やす以外に現実的な選択肢がなく、人手によるスケール対応に頼らざるを得ない状況でした。

この手動オペレーションは、次のようなトイルを生み出していました。

  • カンファレンス運営の方との連携が必須: カンファレンスが開催される日程共有の段階でコミュニケーションミスが発生した場合、急いで対応する必要がある。

  • ペアオペ作業のため 2 人分の工数が発生する: Production 環境で作業をするセンシティブな作業のため、ペアオペが必須となり、2 人分の時間が取られる。

  • 複数環境の調整: フロントエンドとバックエンドなど、複数の環境で調整が必要なため、作業量、作業時間が多くなる。

  • オペレーション・リスク: そもそも Production 環境を手作業で触るので、作業ミスが発生するリスクがある。

実際、最近開催されたアーキテクチャカンファレンスでは、関連する複数環境のコンテナ調整に 2 時間近く要してしまいました。 カンファレンスが開催されるたびに、この手動作業の負荷が大きいと考え、トイルを抜本的に改善する必要があると判断しました。

GitHub Actions の workflow と AWS CLI で自動化

このトイルを撲滅すべく SRE 以外でも素早くコンテナ調整をできるようにするために、今回は AWS CLI と GitHub Actions の workflow_dispatch を組み合わせて、Production のマネジメントコンソールに入らなくても GitHub 上からコンテナ数を調整できるようにしました。

これにより、必要な権限を持ったユーザーが、安全かつ簡単にコンテナ調整を行えるようになりました。

コードの全体像 以下が、コンテナ数をスケールさせるための GitHub Actions のワークフロー全体像です。

scale-containers.yml

name: Scale Containers

run-name: Conference Scale Containers to ${{ github.event.inputs.container_count }} in ${{ github.event.inputs.environment }}

on:
  workflow_dispatch:
    inputs:
      environment:
        type: environment
        required: true
        default: staging
      container_count:
        description: "containers_count"
        type: choice
        required: true
        options:
          - xx
          - yy
          - zz

permissions:
  id-token: write
  contents: read

jobs:
  scale-containers:
    runs-on: ubuntu-slim
    environment: ${{ github.event.inputs.environment }}
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - name: Scale Backend ECS containers
        run: |
          CLUSTER_NAME="backend-${{ github.event.inputs.environment }}"
          SERVICE_NAME="backend-${{ github.event.inputs.environment }}"

          echo "🔄 Scaling Backend  containers to ${{ github.event.inputs.container_count }}"

          # 1. ECSサービスのDesired Countを更新
          aws ecs update-service \
            --cluster "$CLUSTER_NAME" \
            --service "$SERVICE_NAME" \
            --desired-count ${{ github.event.inputs.container_count }}

          # 2.  オートスケールの最小キャパシティもワークフローでの設定値に合わせる
          aws application-autoscaling register-scalable-target \
            --service-namespace ecs \
            --scalable-dimension ecs:service:DesiredCount \
            --resource-id "service/$CLUSTER_NAME/$SERVICE_NAME" \
            --min-capacity ${{ github.event.inputs.container_count }} \
            --max-capacity [xx]

          echo "✅ Backend containers scaled successfully"

      - name: Scale Frontend ECS containers
        run: |
          CLUSTER_NAME="frontend-${{ github.event.inputs.environment }}"
          SERVICE_NAME="frontend-${{ github.event.inputs.environment }}"

          echo "🔄 Scaling Frontend containers to ${{ github.event.inputs.container_count }}"

          aws ecs update-service \
            --cluster "$CLUSTER_NAME" \
            --service "$SERVICE_NAME" \
            --desired-count ${{ github.event.inputs.container_count }}

          aws application-autoscaling register-scalable-target \
            --service-namespace ecs \
            --scalable-dimension ecs:service:DesiredCount \
            --resource-id "service/$CLUSTER_NAME/$SERVICE_NAME" \
            --min-capacity ${{ github.event.inputs.container_count }} \
            --max-capacity [xx]

          echo "✅ Frontend containers scaled successfully"

scal-container-workflow

※実際のワークフローの画面です。

ワークフローの実装ポイント

  • workflow_dispatch による手動実行

    inputs で environment(環境名)と container_count(コンテナ数)を入力値として受け付けます。container_countchoiceにすることで、設定可能な値に制限を設け、誤入力を防いでいます。

  • コンテナ数と同時に最小コンテナ数の設定値も合わせる

    aws application-autoscaling register-scalable-targetを実行し、オートスケールの--min-capacityもワークフローでの設定値に合わせるようにしています。この設定はコンテナ数を増やしても、オートスケールが最小キャパシティまでコンテナ数を減らしてしまう可能性を防ぐためです。

本ワークフローの導入により、Findy Conference のコンテナ調整は、誰でも GitHub Actions の画面から数クリックで実行できるようになり、SRE チームの負荷が軽減されました。また、既にカンファレンス開催に関わる複数の環境全てに横展開を完了しています。

この改善によって、SRE チームが約 2 時間かけて行っていたペアオペ作業は解消されました。

加えて、手動オペレーションのリスクも排除され、必要な権限を持った開発者やカンファレンス担当者が安全にコンテナ調整を行える運用体制を整えることができました。

おわりに

今回は Findy Conference のスパイクの起こりやすいトラフィック課題に対して、GitHub Actions と AWS CLI を用いて運用オペレーションを自動化・トイル削減した事例をご紹介しました。

トイル削減は SRE の永遠のテーマですが、エンジニアが気持ちよく開発に取り組める環境づくりや、安心して運用できる体制に繋がるため、これからも積極的に進めていきたいと思います。

最後までお読みいただきありがとうございました!

ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers

LLMに時間変換を任せてはいけない:Findy AI+の開発で学んだ反省と改善策

こんにちは。

ファインディ株式会社でFindy AI+の開発をしているdanです。

今回は、プロンプトにどのようなデータや指示内容を与えるとLLMが誤った出力をしやすいのかについてお話しします。

プロンプトには何を書くべきで、何を書かないべきなのか。また、LLMに渡すデータはどのような形であるべきなのか。私自身が経験した実際の例をあげて解消までのアプローチ方法をご紹介します。

分析の精度をあげるにはここで紹介する内容では不十分ですが、入門編として参考になれば幸いです。

この記事はファインディエンジニア #3 Advent Calendar 2025 23日目の記事です。今月から、たくさんのアドベントカレンダー記事が執筆される予定ですので、ぜひ読んでみてください。

adventar.org

Findy AI+とは

Findy AI+は、GitHub連携やプロンプト指示を通じて生成AIアクティビティを可視化し、生成AIの利活用向上を支援するサービスです。

人と生成AIの協働を後押しし、開発組織の変革をサポートします。

Claude Code、GitHub Copilot、Codex、Devinなど様々なAIツールの利活用を横断的に分析しており、分析基盤にはLangChainを採用しています。また、日報やチーム分析などの機能でもLLMを活用しています。

こうした特性から、Findy AI+ では「何をどう分析するか」を定義するためのプロンプトが、サービスの価値を左右する重要な要素となります。

誤解していたプロンプト調整

良くない手法1: システムプロンプトにUTCから日本時間に変換する指示を与える

初期のシステムプロンプトは次の通りです。

# System Prompt
## Overview
You are expert of developer experience and developer productivity.

Your Timezone is Asia/Tokyo (UTC+9).
Your Language is Japanese.

The Timezone for the creation datetime of the Pull request is UTC.

上記はすごくシンプルなシステムプロンプトです。 このコードでは次のことを書いています。

  • LLMがどのような役割を担うのか
  • タイムゾーンはどこを使用するのか
  • どの言語で出力するのか
  • プルリクエストの作成日時がUTCであること

このシステムプロンプトを使用してプロダクトから分析ツールを実行すると次のような出力結果になりました。

指定した期間で取得されている画像 去年の日付で出力されている画像

システムプロンプトには使用してほしいタイムゾーンがUTC+9(日本時間)であることと、LLM分析に使用するデータはUTCであることを明記しています。

その結果、LLMは分析中に与えられたUTCのデータをJSTに変換する推論処理を行う必要があり、意図しない日付のズレが発生しました。

良くない手法2: 分析に使用するデータを加工する指示をシステムプロンプトに与える

ユーザーからの自由入力による追加分析を行うと実際とは異なる日時が分析結果として返ってきました。 そのため次のようにシステムプロンプトを調整しました。

# System Prompt
## Overview
You are expert of developer experience and developer productivity.

## Current Date
Today's date is {current_date} (YYYY-MM-DD format in UTC timezone).
When you see dates in the data provided, interpret them according to this current date.
For example, if today is 2025-10-08 and the data shows "2025-10-06 to 2025-10-12", this is the current week, not a future or past week.

Your Timezone is Asia/Tokyo (UTC+9).
Your Language is Japanese.

The Timezone for the creation datetime of the Pull request is UTC.

このコードでは次のことを書いています。

  • LLMがどのような役割を担うのか
  • 現在日時の明記
  • 現在日時は明記されたものを使用し過去を参照しないこと
  • タイムゾーンはどこを使用するのか
  • どの言語で出力するのか
  • プルリクエストの作成日時がUTCであること

このシステムプロンプトを使用してPRの分析を行った後、「日曜日に稼働したりしてないよね?」と追加で質問しました。LLMはGitの活動ログから曜日を判定して回答しますが、次のような出力結果になりました。

現実には存在しない日付を返している画像

実際の曜日とLLMが出力した曜日を比較すると、2025年11月17日は月曜日ですが日曜日で出力されてしまっていることが分かります。

システムプロンプトに現在日時の明記と過去を参照しない旨を明記しています。 また期間指定の範囲についても具体的に指示しています。

その結果LLMは現実と過去について言及されている部分で混乱し、誤った推論による出力ミスが起きてしまいました。

あるべき手法

システムプロンプトはどのような振る舞いで分析をしてほしいのかを書く

システムプロンプトにデータの加工、時間変換など処理の具体的な内容を書いてしまうと上記で紹介したような誤った推論を誘発してしまう可能性が高いです。

そのため、役割・言語・フォーマットなどシンプルな内容で最低限のみ書くのが良いです。

最終的には次のようなシンプルなシステムプロンプトになりました。

# System Prompt
## Overview
You are expert of developer experience and developer productivity.

## Current DateTime
Current datetime is {current_datetime_jst}

## Important Rules
- When data includes weekday information (e.g., "月曜日", "火曜日"), use it as-is. Do NOT recalculate weekdays yourself.

Your Language is Japanese.

このコードでは次のことを書いています。

  • LLMがどのような役割を担うのか
  • 現在日時は変換済みのJSTで渡す
  • 渡されたデータをそのまま使うよう指示する
  • どの言語で出力するのか

分析に使用するデータを加工してからLLMに渡す

分析に使用するデータは、システムプロンプトへ渡す前に加工済みのものを用意した方が良いです。

システムプロンプトにUTCからUTC+9(日本時間)の変換をするように書くべきではありません。 LLMに渡すデータは変換済みのデータにしておきましょう。

例えば、PRのデータをLLMに渡す場合を見てみましょう。 GitHubのPR情報をAPIから取得する場合、日時に関するレスポンスはUTCで返ってきます。

次の手順で、LLMに渡すデータを変換し分析を行います。

・GitHubのAPIから取得したPR作成日時をUTCからUTC+9(日本時間)に変換する

def format_datetime_jst(pr_utc_datetime: str) -> str:
  # UTCの日時文字列をJSTに変換
  utc_dt = datetime.fromisoformat(pr_utc_datetime.replace("Z", "+00:00"))
  jst_dt = utc_dt.astimezone(JST)
  # 曜日も付与してフォーマット
  weekday_jp = WEEKDAYS_JP[jst_dt.weekday()]
  return jst_dt.strftime(f"%Y/%m/%d ({weekday_jp}) %H:%M:%S UTC+9")

# 実行例
format_datetime_jst("2025-11-17T08:53:32Z")
# => "2025/11/17 (月曜日) 17:53:32 UTC+9"

・変換したデータを分析コンテンツに格納する

analysis_content = f"""
# Pull Request Analysis

- PR1 Created: {format_datetime_jst(pr1.get("created_at", ""))}
- PR2 Created: {format_datetime_jst(pr2.get("created_at", ""))}
- PR3 Created: {format_datetime_jst(pr3.get("created_at", ""))}
"""

・分析コンテンツを使用してLLMに分析を依頼する

llm.invoke([
  # 「システムプロンプトはどのような振る舞いで分析をしてほしいのかを書く」で紹介したシステムプロンプト
  {"role": "system", "content": system_prompt},
  {"role": "user", "content": analysis_content}
])

このように変換済みのデータをLLMに渡すことで、LLMが日時の計算や曜日の判定を行う必要がなくなり、誤った出力を防ぐことができます。

おわりに

今回は、LLMにデータ加工や時間変換を任せると誤った推論を誘発しやすいことをご紹介しました。システムプロンプトには役割や出力形式などシンプルな指示のみを書き、データはあらかじめ加工してから渡すことで、分析精度を向上させることができます。

プロンプトの書き方ひとつでLLMの出力は大きく変わります。この記事が、皆さんのLLM活用の参考になれば幸いです。

現在、ファインディでは一緒に働くメンバーを募集中です。

興味がある方はこちらからご応募ください。 herp.careers