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

AIやツールで運用業務を効率化した先に見えた次のステップ

こんにちは。ファインディのTeam+開発部でエンジニアをしている古田(ryu-furuta)です。
この記事は、ファインディエンジニア #2 Advent Calendar 2025の22日目の記事です。

はじめに

2025年下期、私は「DevとOpsを融合する」というミッションを掲げ、問い合わせやアラートといった運用業務の改善にAIをいくつか活用していきました。

この記事では、Claude Code GitHub ActionsやNotion MCPを使った運用業務改善の具体的な実装方法を紹介します。
また、効率化を実現した先に見えてきた「次にやるべきこと」についても共有します。

AIを活用した運用改善を検討している方や、改善施策を打っても成果が出ないと悩んでいる方に読んでいただけると幸いです。

問い合わせやアラートに生じていた課題

私が携わるFindy Team+はエンジニア増加による開発チームの細分化・多数の機能リリース・連携するサービスの増加といった変化の中にあります。これにより、問い合わせやアラートなどの機能開発以外の運用業務でも複数の課題が生じていました。

  • 問い合わせ/アラートの数が増加して機能開発のボトルネックになっている
  • 問い合わせ/アラートに関連した機能・実装のコンテキストを知らないため調査コストが高い
  • 問い合わせのやりとりはSlackのスレッドで行っているためステータスが分かりにくい
  • 問い合わせのデータが蓄積されないため対策や将来の改善に活かせない

こういった課題を解消すべくAIやツールを活用した改善をこの2025年下期で取り組みました。

Claude Code GitHub Actionsでアラートの初期調査コストを削減

アラート通知はこのようにSentryを経由してSlackへ通知されます。

これだけを見てもどういったエラーでどこで発生したのか分かりません。
そのため従来はSentryのイシュー詳細に遷移し、スタックトレースを確認したり、そこからエディタで関連コードを調査したり、まず 状況把握をするための初期調査コストが高い という問題がありました。

この問題の改善に用いたのがClaude Code GitHub Actionsです。
Claude Code GitHub Actionsは、GitHub ActionsのワークフローからClaude Codeを呼び出せる機能です。

SentryからGitHubのイシューを作成したら自動で次のようなプロンプトのコメントをClaudeにメンションします。

このissueのdescriptionに当リポジトリで発生したエラーが記述されています。
このissueのdescriptionにはSentryのissueのIDが記載されています。
mcp__sentry__get_sentry_issueで記載されたIDのissueの詳細情報を取得してください。
mcp__sentry__get_sentry_issueで取得した結果やエラー発生箇所・周辺ファイルを閲覧し、エラーの発生原因の調査結果をissueのコメントに記載してください。
可能であれば修正対応のプルリクエストを作成してください。
プルリクエストを作成する際は次の3ステップで実行してください:

- エラーに対する初期の対応案(ドラフト)を作成してください。
- そのドラフトに対して、どこが良くてどこが改善できるかをレビューしてください。
- レビュー結果をふまえて、より良い最終的な対応案を提示してください。

これによってClaude Code GitHub Actionsが起動し、自動でエラー発生箇所の周辺調査や状況整理、うまく噛み合えばClaudeが作ったプルリクエストをマージするだけで対応が完了する時もあります。

▼実際にClaudeが作成したコメント例

この取り組みによってアラート発生時の初期調査コストを削減することができました。ケースによっては数時間かかっていた調査が数分で状況把握できるようになり、調査開始までの心理的ハードルも下がっています。

Notionのデータベースで問い合わせのチケット管理とデータ蓄積

前述したようにFindy Team+では数年間に渡ってSlackワークフローでの問い合わせ起票を行ってきました。
問い合わせのコミュニケーションに関してはSlackで過不足無いのですが、複数問い合わせが並行するとやりとりを追うのが大変だったり、現在のタスクの状況が分からなくなる問題が度々発生していました。
また問い合わせはプロダクトの改善に繋がる貴重な情報なのにそのデータが蓄積されずフィードバックループを回すことが出来ない、というのも大きな課題でした。

これを一気に解消したのがNotionのデータベースです。

Slackのワークフローでの問い合わせ起票を全てNotion Formに移行し、フォーム送信と同時に問い合わせの情報がデータベースに蓄積されるようになりました。

またデータベースでもチケット管理を行うようにしました。
対応期限のカラムやいわゆる「To Do」「In Progress」「Done」といった値を持ったステータスのカラムを用意し、Zapierを使ってリマインドの機構を設けました。
例えば対応期限を超過してもステータスが「Done」になっていない場合、Slackで担当者へリマインド通知を飛ばすといった自動化も行いました。

▼対応期限超過のリマインドメッセージ

Notionのデータベースを用いることでステータス管理や情報の蓄積が可能になり、問い合わせが抱えていた複数の課題を一気に解消することができました。
また、データが蓄積されることで次に紹介する取り組みにも繋がりました。

Notion MCPを使って過去事例の検索と自動調査

問い合わせ起票時には起票するメンバーに問い合わせに関連した「画面機能」「連携サービス」「メトリクス」といった情報をデータベースに入力してもらうようにしています。
さらに問い合わせの対応が完了したらLLMに問い合わせのやりとりを要約してもらい、その内容もデータベースに保存しています。
これにより半自動的に問い合わせについての情報を拡充していくことができています。

そしてアプリケーションのリポジトリに次の内容のClaude Codeのスラッシュコマンドを作成しました。

---
description: Notion上の問い合わせDBから類似の問い合わせを検索し、原因を調査する
---

- $ARGUMENTSは `collection://***` の問い合わせデータベースの中の問い合わせ(ページ)です。
- notion-fetchを使って $ARGUMENTS の問い合わせ内容を確認してください。
  - $ARGUMENTS の問い合わせと類似の過去の問い合わせを `collection://***` のデータベースから検索してください。
  - 検索の際は`summary`,`detail`,`tag`の各プロパティから類似の問い合わせを検索してください。
- この検索結果から $ARGUMENTS の問い合わせを当リポジトリのコードから調査してください。
- 最終的に次の結果を出力してください。
  1. 類似の問い合わせが見つかったかどうか
  2. 類似の問い合わせが見つかった場合、その問い合わせIDと概要(summary)
  3. $ARGUMENTS の問い合わせの原因調査結果

このスラッシュコマンドは内部でNotion MCPのtoolを利用しています。
これにより過去の問い合わせ情報というコンテキストを持ちながらClaude Codeがより詳細にコード情報を調査することを可能にしています。

出力サンプルを本記事に掲載したいところですが、あまりにも詳細に情報を出力しすぎてしまうため残念ながらここでの掲載は控えさせていただきます。

ただ私自身何度かこのスラッシュコマンドを問い合わせに実行してみて過去に類似問い合わせがあると問い合わせの要因等をかなり詳細に調査してくれることを確認しています。
また過去に類似問い合わせが無いとしても、問い合わせに関連した周辺処理の概況を説明してくれるので全くコンテキストを把握していない機能の問い合わせの調査コストを軽減できていると感じています。

取り組みの成果

今回紹介した取り組みにより、次のような成果を得ることができました。

  • 問い合わせやアラートの状況が可視化され、分析可能な基盤が整った
  • AIによる初期調査で調査の効率化が実現し、状況把握までの時間を大幅に短縮できた
  • チケット管理によりステータスが明確になり、対応漏れを防げるようになった

一方で、可視化や効率化を行うだけでは問い合わせやアラートの件数自体は減少しませんでした。
機能開発が進み、利用者数も増加している中での自然な傾向でもあります。
可視化や効率化は部分的に対応コストを下げるものであり、それだけでは件数を減らす根本的な解決にはなりません。

今後の取り組み:根本原因の特定と改善の仕組みづくり

蓄積された問い合わせデータやアラート傾向を分析し、頻出する問題の根本原因を特定していきます。
暫定対処ではなく恒久対応を行うことで、問い合わせやアラートの件数自体を減らすことを目指します。
たとえば、問い合わせデータを「画面機能」や「連携サービス」ごとに集計し、特定の領域に問い合わせが集中していないかを可視化します。
集中している領域があればUIの改善やドキュメントの拡充、場合によっては機能自体の見直しを検討します。

また、これまでは改善活動に対する明確な行動指針がなく、暫定対処に留まりがちでした。
今後はインセンティブの設計をしっかり行い、改善活動が継続して実施される仕組みを作ります。
具体的には、来期から私も機能開発チームに合流し、改善対応もミッションの一部として進めていきます。
運用業務の当事者として改善に取り組むことで、フィードバックループを確実に回していきます。

まとめ

2025年下期開始当初、自分は昨今のAIの潮流もあって「AIがあれば運用業務の全てを改善できる!」と意気込んでいました。
実際に取り組んだ結果、AIやツールを活用していくらかの問い合わせやアラート対応の「可視化」と「効率化」を実現しました。
しかしこれらは対応を効率化するものであり、件数を減らす根本解決ではありません。今後は蓄積したデータを活用して恒久対応に繋げ、問い合わせやアラートの根本原因を取り除いていきます。

引き続きAIを活用しつつ、改善活動が継続する仕組みを作りながら本質的な改善に挑戦していきます!

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

herp.careers