RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

イベントレポート:AIを“組み込む” ─ 大規模開発における推進と統制、現場の実情と次の一手【後編】

本レポートは、2025年12月17日に開催したイベント「AIを“組み込む” ─ 大規模開発における推進と統制、現場の実情と次の一手」の内容を、前編・後編の2回構成でお届けしています。

前編では、AI活用を本格的に進めるにあたっての導入設計、ヒトとAIの役割分担の考え方について、現場の取り組みを中心に整理しました。(前編はこちら
後編となる今回は、そうした取り組みを一部の現場にとどめず、組織全体へと広げていくフェーズに焦点を当てます。
ガバナンスやガードレールの設計、そして成果をどう捉え、どう説明していくのか。AI活用を継続的な取り組みにしていくための、より実践的な論点を見ていきます。

登壇者は、組織横断でAI活用を支援する KINTOテクノロジーズ の生成AIエバンジェリスト・和田氏、 印刷ECという巨大なシステムの現場でAIネイティブ化を進める ラクスル 事業 VPoE・箱崎。 モデレーターは ラクスル事業 CTO・岸野 が務めました。

Theme 2:ガバナンス/ガードレール設計

ルールの目的は「統制」ではなく「試せる状態を継続する」こと

ガバナンスの議論は、「縛る/縛らない」では終わりません。現場の自由が無くなると学習が止まり、統制が無くなると運用が破綻する。両者を同時に満たす設計が必要です。

ラクスル:基本は自由。ただし“推奨ライン”は提示する

ラクスルのスタンスは「まずはご自由にお試しください」。新しいツールやモデルも最初から強く制限しない。
ただし、本番開発では推奨ラインを提示し、日常的に使うツールは結果として2〜3パターンに収束していく。
自由の中に“収束圧”を自然に生む設計。
厳密に縛らないが、野放しにしない。この中間のバランスが現場の実務に耐える、という話でした。

KINTOテクノロジーズ:スピードと安全性を両輪で実現

KINTOテクノロジーズでは、AIファーストグループ、コーポレートIT、セキュリティ・プライバシーが密に連携してルールをつくる。 重要なのは「どちらが正しいか」ではなく、イノベーションとガバナンスが互いに支え合う構造を持つこと。

また、SaaSをレイヤー分けして運用する話は、明日から使える具体論でした。

  • 入社初日から使える標準ツール
  • 申請すれば使えるツール(上長承認不要)
  • 申請+上長承認が必要なツール
  • 検証・研究用途のツール(未整理なものは厳しめに)

この区分があることで、現場から「これ使っていい?」が来たときに、“まず試せるプレイグラウンド”を提示できる。そしてツール側の進化で、気づけば標準ツールで事足りるケースも増える。

「AIだから特別扱いしない」— ルールが破綻しないための前提

このテーマの核心として語られたのは3つ。

  • AIツールだから特別な審査フローをつくると、ほぼ確実に破綻する
  • これからAIが含まれていないSaaSはほとんどなくなる
  • AIも通常のSaaSと同じく「入力があって、出力がある」だけ

クリエイティブ系ツールの著作権リスクも、「AIだから起きる」のではなく、表現手段が変わっただけ。恒久的な“特別扱い”にしないことが重要だ、と。

この視点は、生成AI導入を「新ルール作り」から解放します。
むしろ、既存のSaaSガバナンスを“AI時代に耐える形へ再設計する”ことが本質だと気づかされました。

“迷っている呟き”を拾う。運用は大きな制度より先に、日々の応答から始まる

岸野が触れた「使っていいか分からず躊躇して終わる」問題に対し、和田氏は社内チャット(Times)の小さな投稿を例に挙げました。

  • 「このツール使っていいんだっけ?」
  • 「申請したら通るんでしたっけ?」

温度の低い呟きを見て見ぬふりをしない。誰かが反応し、そのやり取りを見た別の人が「聞いてみようかな」と思える。ガバナンスは制度設計だけでなく、“心理的ハードルを下げる運用”で広がっていく。この話は、現場感がありながら本質的でした。

“めんどくささ”は悪ではない。覚悟を問う最小のハードル

一方で、何でもノーチェックが理想かというと、そうではない。 箱崎は、最低限見るべき項目(運営主体、準拠法など)を挙げつつ、運用を軽くしすぎるとリスクが入り込むと語ります。

和田氏も「一定のめんどくささは必要」と同意します。それは「それでも使いたいですか?」という覚悟を問う最小の仕掛けであり、越えたからこそ“使い切る”姿勢が生まれる。ここがこのテーマの着地点でした。

最低限のハードルは設ける。ただし、そのハードルはできる限り越えやすく設計する。

Theme 3:メトリクス設計と説明責任

数字はゴールではなく「行動と空気を変えるための材料」

AI導入が進むほど、経営や周辺組織からの問いは変わります。

  • 「で、何がどれだけ良くなったの?」
  • 「生産性は上がった?」
  • 「事業へのインパクトは?」

ここで“都合のいい数字”をつくると、いずれ破綻します。両社が選んだのは、AI特有の指標に寄せるのではなく、継続できるオーソドックスな指標に立ち戻ることでした。

ラクスル:定点で見続けられる指標を選ぶ

箱崎が見ているのは、マージされたPR数などのオーソドックスな指標。重要なのは、厳密さを追い求めすぎると計測も運用も複雑になり、続かないこと。続かない指標ほど意味がない。

開発には季節要因もあれば、フェーズごとの波もある。短期の数値だけで語れないからこそ、簡単に取れて、定点で、長期に見続けられることを重視する。
そして次に出てくる問いが、「PRが増えたとして、それはアウトカムにどう結びつくのか?」。これはAIだから新しく生まれた問いではなく、開発が事業価値をどう生むかという古くて新しい問いに立ち戻るものだ、と整理されました。

KINTOテクノロジーズ:「Vibe Coding Week」で“説明責任の壁”を越える

和田氏は、Copilotのアクセプタンスレートのような“AIっぽい数字”が、意思決定にあまり効かない実感を語りました。結局、昔から参考になる指標に戻ってくる。

そのうえで、管理職・経営層の認知を更新する施策として実施したのが 「Vibe Coding Week」。
1週間、原則手作業コーディングをせず、AIツール前提で開発する。実験としての意味合いも含めつつ、結果としてPRのオープンからマージまでのサイクルタイムが約35.5%改善しました。

ここで大事なのは、「厳密な比較」よりも目的の置き方です。

  • これだけ変わる可能性がある、という感覚を持ってもらう
  • エンジニアが安心してツールに没頭できる空気をつくる
  • AIツールの“社内での地位”を引き上げる

数字は「儲け」や「人員削減」のためではなく、挑戦しやすい空気と行動をつくる補助線。 この位置づけは、AI導入のメトリクス議論が泥沼化しがちな組織にとって、強い処方箋になります。

まとめ:AIを“組み込む”ための3つの実践原則

最後に、今回の議論を整理します。

1) 使い方を揃えるのではなく、分布をずらす

全員を均一にする発想はコストが高い。
まずは上位の使い方を再現できる層を増やし、可視化し、関心を伝播させる。誰が動くかは固定ではなく、ツール成熟と環境で入れ替わる。

2) ガバナンスは“統制”ではなく“試行を継続する設計”

自由と統制の二項対立ではなく、

  • 推奨ラインの提示(自然な収束)
  • SaaSレイヤー分け(プレイグラウンドの明示)
  • 迷いの呟きに反応する運用(心理的ハードルの低下)で、組織として前に進むためのガードレールをつくる。
3) メトリクスは「正しい数字」より「続く指標」+「空気を変える材料」

AI特有の指標に寄せすぎない。
継続できるオーソドックスな指標で定点観測しつつ、成功事例で認知を更新し、挑戦できる空気をつくる。数字はゴールではなく行動を変えるための材料。

おわりに:次の一手は「開発全体のバリューストリーム最適化」へ

生成AIによって実装スピードが上がると、ボトルネックは前後工程へ移動します。コードを書く工程が速くなる一方で、要件の詰め方やレビュー待ちといった前後工程が、相対的にボトルネックになる。今回の議論では、まさにその変化が、両社の事例として共有されていました。

ラクスルが進める仕様駆動開発(Spec-Driven Development, SDD)へのシフトも、KINTOテクノロジーズが向き合っているレビュー待ちの課題も、背景にあるのは同じ構造です。
AIによって「つくる部分」が加速した結果、開発全体をどう設計し直すかが、次のテーマとして立ち上がってきたと言えます。 このことは、AI導入の成果が「コードが速く書けるようになったかどうか」だけでは測れない、という点も示しています。
要求定義、実装、レビュー、リリースまでを含めた一連の流れの中で、どこが詰まり、どこを変えるべきか。そこに向き合い続けること自体が、AIを“組み込む”という取り組みの本質なのかもしれません。

Difyで実装するAnthropicの「Building Effective Agents」

こんにちは。ノバセルCTOの磯部です。

この記事はノバセル テクノ場 出張版2025 Advent Calendar 2025の最終日になります。これまで長らくお付き合い頂き、本当にありがとうございます。

生成AIを活用したシステム開発において、Agentという言葉は日常的に使われるようになりました。しかし、その言葉の定義や実装方法はしばらく曖昧だったかと思います。

もう1年前のことになりますが、2024年12月にAnthropicがBuilding Effective Agentsを公開し、どこまでをWorkflowと呼び、どこからをAgentと呼ぶべきかを提案してくれました。

この記事の最大の貢献は、AgentとWorkflowを二項対立ではなく、自律性のスペクトラムとして定義したことにあると私は考えています。

今回は、ノーコードLLM開発プラットフォームであるDify(ver. 1.11.1)を使い、この記事で紹介されているEvaluator-Optimizerパターンを実装してみます。

また、Workflowだけでなく記事の後半ではAgentによる実装も行い、Workflow型とAgent型で挙動がどう変わるのか、その違いをハンズオン形式で比較検証していきます。

AgentとWorkflowのスペクトラム

実装に入る前に、Anthropicによる定義を整理しておきましょう。これらは0か1かのバイナリではなく、以下のようなスペクトラムの中に存在します。

  • Workflow: 事前に定義されたコードやパスに従ってシステムが動作します。分岐やループが存在しても、その条件は人間が設計したロジックに基づきます。「予測可能性」と「確実性」が高いのが特徴です。
  • Agent: LLMが自らプロセスを制御します。どのツールを使うか、次に何をするかを動的に決定します。「柔軟性」が高い反面、挙動の予測が難しくなる場合があります。

本記事では、この中間〜右側に位置するパターンを実際にDifyで作っていきます。

Difyのチャットフローとワークフローの違い

Difyにはアプリケーションの種類としてチャットフローとワークフローがありますが、これらは以下のように使い分けます。

特徴 チャットフロー ワークフロー
主な用途 ユーザーとの対話型アプリ。 バックエンド処理、バッチAPI。
メモリ あり。会話履歴を保持し、文脈を踏まえた応答が可能。 なし。1回のリクエストで完結するステートレスな処理。
対話機能 中間ステップで「回答」ノードを使い、ストリーミング応答が可能。 基本的に最終結果のみを出力する(Functionに近い)。


実際のところ、両者で使えるノードにほとんど違いはありません。どちらを使っても複雑なフローを組むことはできます。

今回は、ユーザーと対話しながら文章を作成する対話型アプリを作成するため、メインの実装にはチャットフローを使用します。

以降、ワークフロー(Workflow)という語はAnthropicの概念(Workflow/Agent) とDifyのアプリ種別(チャットフロー/ワークフロー) の2つの意味で登場するため、混同しないように注意してください。

実装パターンの選定

Anthropicの記事では、エージェントシステムを構築するビルディングブロックとして以下の5つが紹介されています。

  1. Prompt Chaining: LLMの出力を次の入力にする基本形。
  2. Routing: 入力を分析し、適切なパスに振り分ける。
  3. Parallelization: 複数のタスクを並列実行し、統合する。
  4. Orchestrator-Workers: 指揮者がタスクを分解し、作業者に割り振る。
  5. Evaluator-Optimizer: 生成と評価をループさせ、品質を高める。

今回はEvaluator-Optimizerを採用します。 理由は、このパターンを実装するには「生成結果を次の入力にする(Prompt Chaining)」と「評価結果で分岐する(Routing)」という要素の両方が含まれており、他の基本的なパターンの要素も同時に学習できるためです。

本稿では、執筆時点でDifyから利用可能な選択肢の中から用途に合わせて、生成にはGemini 2.5 Pro、評価にはGemini 2.5 Flashを使います。

実践1:Workflowアプローチ(明示的なループ制御)

まずは、チャットフロー機能を使って「指定回数まで推敲を繰り返す」フローを構築します。 AI任せにするのではなく、人間が「ループ」ノードを使ってループ回数を制御します。

アーキテクチャ

graph LR
    Start[開始] --> Gen["LLM<br>Generator"]
    Gen --> LoopStart{"Loop<br>(最大3回)"}
    
    subgraph Loop
        LoopStart --> Eval["LLM<br>Evaluator"]
        Eval --> Parser["コード<br>JSONパース"]
        Parser --> Check{"Router<br>PASS or FAIL?"}
        Check -- FAIL --> Opt["LLM<br>Optimizer"]
    end
    
    Opt --> Finish["回答"]
    Check -- PASS --> Finish["回答"]
  1. Generator: 初稿を作成。
  2. Loop: 最大3回まで以下のプロセスを回す。
  3. Evaluator: 現在の記事を評価。
  4. Router: PASSなら何もしない。
  5. Optimizer: FAILなら記事をリライトし、変数を上書きして次のループへ。

STEP 1: 初稿作成

  1. Difyで「最初から作成」→「チャットフロー」を選択
    • アプリ名: Optimized Writer Flow
  2. 「スタート」ノードの次に「LLM」ノードを追加し、名前をGeneratorとします。
    • モデル: 「Gemini 2.5 Pro」(論理的思考に強いモデル)

システムプロンプト:

あなたはプロのライターです。ユーザーの依頼に基づいて記事の初稿を作成してください。

ユーザープロンプト:

{{#sys.query#}}

STEP 2: ループの設定

  1. 「LLM」ノードの後に「ループ」ノードを追加し、名前をLoopとします。
    • ループ変数:
      • 変数名: current_draft
      • 変数型: 「String」
      • 入力モード: 「Variable」
      • : {{#Generator.text#}}
    • 最大ループ回数: 3

最大ループ回数を1-2回など少なめに設定すると決定論的なWorkflow寄りになりますし、10回など多めに設定すると自律的なAgent寄りになると捉えることができます。

STEP 3: 評価

  1. 反復ブロック内の開始点に「LLM」ノードを追加し、名前をEvaluatorとします。
    • モデル: 「Gemini 2.5 Flash」(高速なモデル)

システムプロンプト:

あなたは厳格な編集者です。入力された記事を評価し、JSON形式で結果を出力してください。
合格基準: ユーザーの依頼を満たし、論理的で、誤字脱字がないこと。

ユーザープロンプト:

ユーザーの依頼: {{#sys.query#}}
現在の原稿: {{#Loop.current_draft#}}

以下のJSONフォーマットのみを返してください:
{
  "status": "PASS" または "FAIL",
  "reason": "評価理由と、FAILの場合は具体的な修正指示"
}

STEP 4: JSONパース

  1. Evaluatorの後に「コード」ノードを追加し、名前をJSON_Parserとします。
    • 入力変数: json_str(String)← 値に {{#Evaluator.text#}} を設定
    • 出力変数: status(String), reason(String)

コード (Python 3):

import json
import re

def main(json_str: str) -> dict:
    try:
        # Markdownコードブロック記法への対策
        clean_str = re.sub(r'```json\s*|\s*```', '', json_str).strip()
        data = json.loads(clean_str)
        return {
            "status": data.get("status", "FAIL"),
            "reason": data.get("reason", "Unknown Error")
        }
    except Exception as e:
        return {
            "status": "FAIL", 
            "reason": f"JSON Parse Error: {str(e)}"
        }

STEP 5: 条件分岐

  1. JSON_Parserの後に「IF/ELSE」ノードを追加します。
    • 条件: 変数 {{#JSON_Parser.status#}}PASS である場合。

STEP 6: 最適化とループ制御

  1. True (PASS) の場合:
    • 「ループ完了」ノードを追加。
  2. False (FAIL) の場合:
    • 「LLM」ノードを追加し、名前をOptimizerとします。
    • モデル: 「Gemini 2.5 Pro」

システムプロンプト:

あなたはライターです。編集者の指摘に従って、記事をリライトしてください。

ユーザープロンプト:

元の依頼: {{#sys.query#}}
現在の原稿: {{#Loop.current_draft#}}
編集者の指摘: {{#JSON_Parser.reason#}}

指摘を反映した完成稿のみを出力してください。

Optimizerの後に「変数代入」ノードを置き、{{#Loop.current_draft#}}{{#Optimizer.text#}} (リライト後の文章) で上書き更新します。

STEP 7: 最終出力

反復ノードを抜け出した後に、「回答」ノードを配置し、{{#Loop.current_draft#}} を表示します。

これで、「最大3回まで、ダメ出しがある限りループして品質を高め続ける」ワークフローが完成しました。

実践2:Agentアプローチ(自律的)

次は、同じことをDifyの「エージェント」ノードを使って実装します。 こちらはフローチャートでループを組むのではなく、評価ツールをAIに渡し、AI自身に納得いくまで修正させるアプローチです。

アーキテクチャ

graph LR
    User["スタート"] --> Agent["エージェント"]
    
    subgraph "External Tool(ワークフロー)"
        ToolInput["Start"] --> ToolLLM["LLM<br>Evaluator"]
        ToolLLM --> ToolOutput["出力"]
    end

    Agent --> ToolInput
    ToolOutput --> Agent
    Agent -->|"自律判断<br>リライト or 終了"| Agent
    Agent --> Output[回答]

評価ツールの作成

まず、評価ロジックを独立したツールとして作成します。Agentが正確に判断できるよう、構造化されたデータ(statusreason)を返すように作り込みます。

  1. Difyで「最初から作成」→ 「ワークフロー」を選択します
    • 名前: Article Quality Check Tool
    • 「ユーザー入力(元の開始ノード)」を選択
  2. 「スタート」ノード
    • 入力変数: draft(Paragraph, 必須), requirements(Paragraph, 必須)
  3. 「LLM」ノードを追加し、名前をEvaluatorとします。
    • モデル: 「Gemini 2.5 Flash」

システムプロンプト:

あなたは記事の品質チェッカーです。

ユーザープロンプト:

要件: {{#requirements#}}
原稿: {{#draft#}}

以下のJSONフォーマットのみを返してください:
{
  "status": "PASS" または "FAIL",
  "reason": "評価理由と、FAILの場合は具体的な修正指示"
}
  1. 「コード」ノードを追加、名前をJSON_Parserとします。
    • LLMの出力をパースします。実践1と同じPythonコードを使用してください。
    • 入力変数: json_str(String)← 値に {{#Evaluator.text#}} を設定
    • 出力変数: status(String), reason(String)
  2. 「出力」ノード
    • 出力変数:
      • status (String) ← 値: {{#JSON_Parser.status#}}
      • reason (String) ← 値: {{#JSON_Parser.reason#}}
  3. 画面右上の「公開する」→「ワークフローをツールとして公開する」をクリック。
    • ツールコールの名前: check_article_quality
    • 説明: Checks if the draft meets requirements. Returns status (PASS/FAIL) and reason.

チャットフローでのAgent実装

ここからが本番です。Workflow版とは異なり、フロー自体は非常にシンプルです。

  1. チャットフローを新規作成します
    • 名前: Autonomous Writer Agent
  2. 開始ノードの後ろに「エージェント」ノードを追加、名前をAgentとします。
    • エージェンティック戦略: 「FunctionCalling」
    • MODEL: 「Gemini 2.5 Pro」 (Function Calling性能が高いモデル必須)
    • TOOL LIST: 先ほど作成した「Article Quality Check Tool」を追加します。
    • MAXIMUM ITERATIONS: 6

INSTRUCTION:

あなたは妥協を許さないプロのライターです。
以下の手順を自律的に実行し、最高品質の記事を作成してください。

1. ユーザーの依頼に基づいて初稿を執筆する。
2. `check_article_quality` ツールを使用して、自分の記事を評価する。
3. ツールの結果(`status`)を確認する:
   - "FAIL" の場合: `reason` を読んで記事を修正し、**再度**ツールで評価を受ける。
   - "PASS" の場合: その記事を最終回答とする。
4. "PASS" が出るまで最大5回繰り返す。

Query:

{{#sys.query#}}

最後に「回答」ノードを配置し、{{#Agent.text#}} を表示します。

比較と考察

観点 Workflow Agent
制御性 。設計図通りに動く。 。AIの判断に委ねられる。
予測可能性 。コストや時間が読める。 。ループ回数により変動する。
実装 ロジックの組み立てが必要。 プロンプトとツールの定義が主。
自己決定 なし。 あり(終了タイミング等を決定)。


Anthropicの記事にある通り、ビジネスアプリケーションでは予測可能性が重視されるため、まずはWorkflowから構築し、必要に応じてAgent的な自律性を取り入れるのが成功への近道です。

アプリケーションへの統合

最後に、Difyで作成したワークフローは、APIとして即座に利用可能です。これにより、ノーコードで検証したロジックを、そのまま自社プロダクトのバックエンドとして組み込むことができます。

例えば、今回作成したワークフローは、以下のようなリクエストで実行できます。

curl -X POST 'https://{ENDPOINT}/v1/chat-messages' \
--header 'Authorization: Bearer {API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
  "inputs": {},
  "query": "AIについて短くまとめて",
  "user": "abc-123",
  "response_mode": "blocking"
}'

レスポンス例:

{
  "event": "message",
  "task_id": "f6a55377-9e2f-47c3-b20a-6ca35122fb5b",
  "id": "3c8aae35-b1f5-486a-bdf6-9ac920747f56",
  "message_id": "3c8aae35-b1f5-486a-bdf6-9ac920747f56",
  "conversation_id": "3397c79e-f5a4-44bf-9142-c36381118cd6",
  "mode": "advanced-chat",
  "answer": "AI(人工知能)とは、学習・推論・判断といった...",
  "created_at": 1766620800
}

このように、「Difyでロジックをカプセル化し、アプリからはAPIを叩くだけ」という疎結合なアーキテクチャは、変化の速いAI開発において非常に有効です。ぜひ、皆さんの現場でもEffective Agents(効果的なAIエージェント)を実装してみてください。

イベントレポート:AIを“組み込む” ─ 大規模開発における推進と統制、現場の実情と次の一手【前編】

「生成AIを使う」から、一歩先へ。
いま開発現場で問われているのは、AIを組織とプロセスの中にどう“組み込む”かです。

2025年12月17日に開催したイベント「AIを“組み込む” ─ 大規模開発における推進と統制、現場の実情と次の一手」では、複雑なドメインや長い歴史を持つシステムを前提に、推進と統制のリアルを掘り下げました。
登壇者は、組織横断でAI活用を支援する KINTOテクノロジーズ 生成AIエバンジェリスト・和田氏、印刷ECという巨大かつ歴史あるシステムの現場でAIネイティブ化を進めるラクスル事業 VPoE・箱崎。モデレーターは ラクスル事業 CTO・岸野が務めました。

本レポートは、前編・後編の2回構成でお届けします。
前編では、AI活用を本格的に進めるにあたって最初に直面する、「どこから組み込み、どう広げていくのか」という導入設計と、ヒトとAIの役割分担の現在地に焦点を当てます。
後編では、取り組みを一部の現場にとどめず、組織全体へ広げていくために欠かせないガバナンスやガードレールの考え方、そして成果をどう捉え、どう説明していくのかといった、より実践的な論点を整理していきます。

オープニング:「どう使うか」ではなく「どう組み込むか」

岸野(モデレーター)が最初に提示したのは、現場が直面する“次の難所”でした。

  • どこから/どの工程に入れるかという導入設計の難しさ
  • 自由に試したい現場と、全社としての統制をどう両立させるか
  • 「結局、何がどれだけ変わったのか」をどう示すかという説明責任

生成AIの進化速度が加速するほど、現場は“やりたい”に傾き、組織は“守りたい”に傾きます。両者のギャップを放置すると、最終的に起きるのは「使ってる人だけ使ってる」「怖いから止まる」「説明できずに縮む」のいずれか。
このイベントは、その分岐を越えるための“設計”が主題でした。

各社の現在地:推進の狙いは「利用」ではなく「再現性」

KINTOテクノロジーズ:CCoE的な立ち位置で“循環”をつくる

和田氏が所属するAIファーストグループは、特定プロダクトに紐づかず、会社全体でAIがうまく使われる状態をつくる組織です。活動は大きく3つ。

  1. 教育・研修
  2. ユースケース開発
  3. 技術手の内化

印象的だったのは、これらを「個別施策」ではなく循環構造として捉えていた点です。研修があるからアイデアが出る。知見があるから素早く検証できる。成果が次のユースケースを生む。AIファーストグループは、その“循環のハブ”になる。

また、生成AI活用を考える際に、4象限(効率化/価値創出 × 業務をAIに寄せる/AIを業務に寄せる)で整理している点も示唆的でした。

「既存業務の効率化」は取り組みやすい一方で、本当に難しいのはアウトカム(売上・事業インパクト)へ転換する領域。ここは“放っておいてもうまくいかない”という認識が共有されました。

ラクスル:AIネイティブ化は「混沌を一周してからが本番」

箱崎は、ラクスル事業(印刷EC)を中心に、AIを普及・浸透させ、エンジニア以外にも「何がどう良くなったか」を説明できる状態を役割としています。

AIネイティブ化に本格的に舵を切ったのは直近6〜7か月。環境整備が一気に進む一方で、「AIを入れたなら開発スピードも倍だよね」といった期待値も飛んでくる。 そこで重要だったのは、短期で“成果っぽい何か”を出すことよりも、混沌とした試行錯誤を一度通り抜け、地に足のついた運用へ落とすことでした。

効果の兆しとしては、新規に近いプロダクトで生成AIによるコード支援率が9割近くに達し、巨大な既存リポジトリでも生成比率が3割弱まで来ている。 一方で、実装が速くなった結果、ボトルネックがレビューや要件定義など前後工程へ移り、「レビュー地獄」も発生。ここから仕様駆動開発(Spec-Driven Development, SDD)へのシフトへ話がつながっていきました。

Theme 1:導入設計と進捗

「まず全員に配ってみる」vs「手を挙げた人から」— 重要なのは“順番”より“学習の設計”

ラクスル:「本丸」に向かう前に、まず現場で探索する

立ち上げ当初は、「この機能に入れる」「このリポジトリを対象にする」といった形で導入範囲を細かく決めていたわけではありませんでした。
まず行ったのは、臨時予算を確保してエンジニア全員にAIツールを配布し、「とにかく使い倒してください」という期間を設けることです。最初の1〜2か月は、あえて細かな制約を設けず、探索を優先したといいます。

好奇心の強いエンジニアがさまざまな使い方を試す中で、「これはいけそうだ」という感覚が立ち上がってくる。この“感覚の芽”が生まれること自体が、導入の出発点になっていました。
新規プロダクトをAIで高速に立ち上げること自体は、比較的やりやすいだろう、という認識も当初からありました。実際、そうした成果が経営会議などで共有された場面もあったといいます。

ポイントはここです。
箱崎が「本丸」と表現したのは、そうした新規の取り組みではありませんでした。 真に向き合うべき対象は、ドメイン知識と技術的な積み重ねを多く抱えた、巨大な既存システムです。
その“本丸”にいきなり攻め込むのではなく、まずは現場の探索を最大化し、どこで手応えが生まれるのかを見極める。そのうえで、実装可能性と価値の両立が見えた地点から、段階的に踏み込んでいく。

この初期の判断が、その後の技術的な広がり方を大きく左右するのかもしれません。

KINTOテクノロジーズ:ボトムアップで“濃い使い方”から広げる

一方、KINTOテクノロジーズでは、全員に一律で配布するのではなく、「使いたい」と手を挙げたエンジニアから順にツールを展開していきました。
前提として、生成AIの活用には必ず使い方の濃淡が生まれるため、まずは実際によく使っている人の存在を捉え、その具体的な活用方法を丁寧に言語化し、組織全体に還元することを意識していたといいます。

また、もう一つ共有されたのが、ツールの進化スピードと、組織内での認識のアップデートには時間差が生じやすいという点です。 2023〜2024年頃のGitHub Copilotの印象が、その後の判断の前提として残るケースもあり、ツールが進化しても、関係者にその変化が十分に伝わらなければ、評価や意思決定は当時のイメージに引きずられてしまうことがあります。

生成AIに限らず、新しい技術を組織に定着させていく過程では、こうした「情報の非対称性」や「認識のギャップ」が自然に生まれます。 そのギャップをどう埋めていくかもまた、導入を進める側にとって重要なテーマであることが、今回の議論から浮かび上がってきました。

“ばらつき”は悪ではない。分布をどう動かすか

岸野から投げられた問いは、「全員を均一にするのか? ばらつきを前提に設計するのか?」。

和田氏の答えは明快でした。
全員を同じレベルにするのは難しい。できることはせいぜい、正規分布の“山”を少し右に動かすこと。そのために狙うのは“底上げ”ではなく、“上位の使い方の再現”だといいます。

上位5%の人がやっていることを、上位20〜30%が再現できる状態へ。

そしてレイトマジョリティが動くきっかけは「置いていかれるかも」と本人が気づく瞬間。だから、上を引き上げ、その様子が見える環境をつくることで関心が伝播する。

箱崎もこれに強く同意しつつ、もう一段リアルな補足をします。
一見、静観しているように見える層が存在します。彼らはAIが嫌いなのではなく、ドメインや既存リポジトリの複雑さを深く理解しているからこそ、任せきれないと判断していた。 しかし周囲が使い始め、会話や成果物がAI前提になってくると腰が上がり、動き出した途端に一気にキャッチアップして推進側に回ることも多い。

ここで得られる学びは、単なる「推進あるある」ではありません。 “誰が動くか”は固定ではない。ツール成熟と環境設計によって入れ替わる。だから、いま動く層に時間を投下し、分布をずらす。この発想が、AI導入の現実解として提示されました。

まとめ

前編では、AI導入の初期フェーズにあたる導入設計と、 ヒトとAIの役割分担の現在地について、両社の取り組みを見てきました。
AI活用を進めるうえで重要なのは、 最初から完成形を目指すことではなく、 試しながら学び、その学びを次につなげていくプロセスをどうつくるか、という点であることが改めて共有されたように思います。
後編では、こうした取り組みを全社スケールで進めていくためのガバナンス設計やメトリクスの捉え方について、より実践的な視点から整理していきます。

Modin vs Polars on a Mac: A Single-Node Benchmark

こんにちは、ラクスルベトナムTech LeadのMinhです。 本記事はノバセル テクノ場 出張版2025 Advent Calendar 2025の24日目の記事になります。 私は日本語が得意ではないので英語での投稿とさせてください。

Introduction

DataFrame computation is the workhorse of Python analytics, and pandas remains the default thanks to its familiar API and ecosystem. But even when data fits comfortably in RAM, single-process pandas can become the bottleneck: filters, groupby/aggregations, joins, sorts, and feature-style transforms often spend more time in execution than in analysis.

This article targets a practical end-user question: on a local Mac laptop, for in-memory datasets and analytic workloads, which library delivers better performance without sacrificing day-to-day productivity? Today the choice increasingly falls into two camps:

  • Pandas-compatible parallelism for minimal code churn (Modin)
  • Native columnar engines optimized for analytic execution (Polars).

To make that tradeoff concrete, I benchmark Modin and Polars side by side on the same Mac using representative, RAM-resident workloads. The goal is not to declare a universal winner—performance depends on data shape, operation mix, and execution model—but to surface where each approach wins, and what you pay in compatibility and complexity.

Modin and Polars at a glance

Modin (official: https://modin.org) is a drop-in acceleration layer for pandas. It aims to speed up pandas-style code by parallelizing many DataFrame operations across CPU cores using configurable execution backends (commonly Ray or Dask), making it attractive when you want faster runs with minimal refactoring.

Polars (official: https://pola.rs) is a modern DataFrame library built around a Rust-based, columnar query engine. It emphasizes vectorized columnar processing, parallel execution, and an expression API (including optional lazy execution) designed to maximize performance and memory efficiency on analytics-heavy workloads.

Experimental Setup

This section documents the hardware, software, dataset I used to benchmark Modin and Polars on a single local machine. My intent is to make the results reproducible and to keep the benchmark focused on in-memory analytics execution rather than distributed orchestration or I/O quirks.

Hardware and OS

  • Machine: MacBook Pro (Model Identifier: Mac16,8) with Apple M4 Pro
  • CPU: 12 cores total (8 performance + 4 efficiency)
  • Memory: 24 GB unified memory
  • OS: macOS (arm64). Record the exact version from sw_vers for reproducibility.

Software Stack

I ran all benchmarks under native arm64.

  • Python: CPython 3.9.6 (arm64)
  • Libraries:
    • pandas: 2.3.3
    • Modin: 0.37.1
    • Ray: 2.52.1
    • Polars: 1.36.1
    • PyArrow: 22.0.0
    • NumPy: 2.3.5
    • pyperf: 2.9.0

Where relevant, thread/CPU usage was explicitly controlled to avoid “unfair” defaults:

  • Polars: POLARS_MAX_THREADS= set before importing Polars.
  • Modin: MODIN_CPUS= and a consistent engine selection (Ray) for all runs.

Data

Dataset

  • Primary dataset: TPC-H — a decision-support benchmark defined by the Transaction Processing Performance Council (official: https://www.tpc.org/tpch/) — with tables generated via this tpch benchmark kit: https://github.com/gregrahn/tpch-kit.
  • Scale factor (SF): SF controls the size of the generated dataset—roughly, higher SF means proportionally more rows and larger tables. As a sanity check, at SF=1 row counts are on the order of customer ~150k, orders ~1.5M, and lineitem ~6M (with the remaining tables scaled accordingly).
  • Tables: lineitem, orders, customer, supplier, nation, region, part, partsupp

Data generation procedure (tpch-kit dbgen on macOS)

The raw TPC-H data is generated using the reference dbgen implementation from tpch-kit, then converted to Parquet for repeatable benchmarking.

1) Install build tools (one-time)

xcode-select --install

2) Clone tpch-kit and build dbgen

mkdir -p ~/bench/tpch

cd ~/bench/tpch

git clone https://github.com/gregrahn/tpch-kit.git

cd tpch-kit/dbgen

make MACHINE=MACOS DATABASE=POSTGRESQL

Verify the binary exists:

ls -l dbgen

3) Choose an output folder and scale factor

IMPORTANT: run from tpch-kit/dbgen

cd ~/bench/tpch/tpch-kit/dbgen

SF=1

export DSS_PATH="$HOME/dfbench/tpch/output_sf${SF}"

mkdir -p "$DSS_PATH"

export DSS_CONFIG="$PWD"

export DSS_QUERY="$PWD/queries"

4) Generate all tables

./dbgen -s "$SF" -f -v

5) Verify output files and row counts

ls -lh "$DSS_PATH"

Expected .tbl files:

  • customer.tbl
  • lineitem.tbl
  • nation.tbl
  • orders.tbl
  • part.tbl
  • partsupp.tbl
  • region.tbl
  • supplier.tbl

6) Convert .tbl to typed Parquet (one-time per SF)

Finally, I convert the .tbl files to typed Parquet because Parquet preserves the schema (column types and metadata), which reduces the risk of benchmarking text parsing and type inference rather than execution performance. It also creates a stable, reusable dataset snapshot so repeated runs (and different libraries) read the same data with the same types.

erDiagram
    REGION {
        INT R_REGIONKEY PK
        CHAR R_NAME
        VARCHAR R_COMMENT
    }
    NATION {
        INT N_NATIONKEY PK
        CHAR N_NAME
        INT N_REGIONKEY FK
        VARCHAR N_COMMENT
    }
    CUSTOMER {
        INT C_CUSTKEY PK
        VARCHAR C_NAME
        VARCHAR C_ADDRESS
        INT C_NATIONKEY FK
        CHAR C_PHONE
        DECIMAL C_ACCTBAL
        CHAR C_MKTSEGMENT
        VARCHAR C_COMMENT
    }
    SUPPLIER {
        INT S_SUPPKEY PK
        CHAR S_NAME
        VARCHAR S_ADDRESS
        INT S_NATIONKEY FK
        CHAR S_PHONE
        DECIMAL S_ACCTBAL
        VARCHAR S_COMMENT
    }
    PART {
        INT P_PARTKEY PK
        VARCHAR P_NAME
        CHAR P_MFGR
        CHAR P_BRAND
        VARCHAR P_TYPE
        INT P_SIZE
        CHAR P_CONTAINER
        DECIMAL P_RETAILPRICE
        VARCHAR P_COMMENT
    }
    PARTSUPP {
        INT PS_PARTKEY FK
        INT PS_SUPPKEY FK
        INT PS_AVAILQTY
        DECIMAL PS_SUPPLYCOST
        VARCHAR PS_COMMENT
    }
    ORDERS {
        INT O_ORDERKEY PK
        INT O_CUSTKEY FK
        CHAR O_ORDERSTATUS
        DECIMAL O_TOTALPRICE
        DATE O_ORDERDATE
        CHAR O_ORDERPRIORITY
        CHAR O_CLERK
        INT O_SHIPPRIORITY
        VARCHAR O_COMMENT
    }
    LINEITEM {
        INT L_ORDERKEY FK
        INT L_PARTKEY FK
        INT L_SUPPKEY FK
        INT L_LINENUMBER
        DECIMAL L_QUANTITY
        DECIMAL L_EXTENDEDPRICE
        DECIMAL L_DISCOUNT
        DECIMAL L_TAX
        CHAR L_RETURNFLAG
        CHAR L_LINESTATUS
        DATE L_SHIPDATE
        DATE L_COMMITDATE
        DATE L_RECEIPTDATE
        CHAR L_SHIPINSTRUCT
        CHAR L_SHIPMODE
        VARCHAR L_COMMENT
    }

    REGION ||--o{ NATION : "N_REGIONKEY -> R_REGIONKEY"
    NATION ||--o{ CUSTOMER : "C_NATIONKEY -> N_NATIONKEY"
    NATION ||--o{ SUPPLIER : "S_NATIONKEY -> N_NATIONKEY"
    CUSTOMER ||--o{ ORDERS : "O_CUSTKEY -> C_CUSTKEY"
    ORDERS ||--o{ LINEITEM : "L_ORDERKEY -> O_ORDERKEY"
    PART ||--o{ LINEITEM : "L_PARTKEY -> P_PARTKEY"
    SUPPLIER ||--o{ LINEITEM : "L_SUPPKEY -> S_SUPPKEY"
    PART ||--o{ PARTSUPP : "PS_PARTKEY -> P_PARTKEY"
    SUPPLIER ||--o{ PARTSUPP : "PS_SUPPKEY -> S_SUPPKEY"

Workloads / Queries

I use TPC-H–like queries because they represent realistic decision-support analytics, reliably stressing the operations that dominate laptop-scale workloads: multi-table joins, grouped aggregations, sorting/top-k, and selective filtering. The workload also scales naturally with the dataset scale factor (SF), making it easy to increase pressure on CPU, memory bandwidth, and execution strategy while keeping the schema and query semantics fixed.

Concretely, the suite is derived from the canonical TPC-H query set (Q1–Q22) as defined in the official specification (see the query templates and definitions in https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v3.0.1.pdf). I implement a representative subset in both libraries, mapping each query to equivalent DataFrame operations and keeping the logical plan consistent (same filters, join keys, aggregation expressions, and output schema).

Benchmark Runs Configuration

Datasets. I benchmark two dataset sizes: SF=1 and SF=5. This gives a small and medium in-memory footprint while preserving the same schema and query semantics.

Parallelism / CPU settings. For each scale factor, I run the same workload suite under three controlled configurations to separate engine efficiency from parallel scaling:

  1. Polars (thread scaling): vary POLARS_MAX_THREADS ∈ {1, 2, 4, 8}.
  2. Modin A: Modin + Ray (worker scaling): vary MODIN_CPUS ∈ {1, 2, 4, 8} while keeping each worker effectively single-threaded (cap BLAS/OpenMP threads to 1). This makes speedups primarily reflect Modin/Ray parallel execution rather than nested threading.
  3. Modin B: Modin + Ray (intra-process threading sensitivity): fix MODIN_CPUS=1 and vary the numeric-kernel thread caps ∈ {1, 2, 4, 8} to quantify how much performance comes from multi-threaded kernels versus higher-level DataFrame parallelism.

Across all configurations, I pin versions and environment variables and report results per (SF × workload × configuration) to make scaling behavior explicit.

Results

TPC benchmark materials are distributed under the TPC End User License Agreement (EULA), which places conditions on public disclosure of performance results produced using the software and associated materials (see the current EULA text: https://www.tpc.org/TPC_Documents_Current_Versions/txt/EULA_v2.2.0.txt).

This article uses data and query logic derived from TPC-H for research/educational purposes. It does not claim compliance with any official TPC benchmark standard, is not a “TPC Benchmark Result,” and has not been audited or reviewed by the TPC. The numbers reported here should be treated as non-comparable to published TPC results and should not be interpreted as an official TPC performance claim.

To keep the comparison interpretable, I report results grouped by dataset scale factor and by a shared parallelism level N ∈ {1, 2, 4, 8}. For each N, the three configurations are aligned as follows:

  • Polars: POLARS_MAX_THREADS = N
  • Modin A: MODIN_CPUS = N with numeric-kernel threads capped to 1 (to avoid nested parallelism)
  • Modin B: MODIN_CPUS = 1 with numeric-kernel thread caps set to N

Note: the values in the tables below are in seconds.

This layout makes it explicit whether gains come from (a) the engine’s per-core efficiency, (b) scaling out across workers/cores, or (c) multi-threaded numeric kernels.

Dataset: SF=1

Report the SF=1 results for each workload under the aligned configurations below:

N = 1

  • Polars: POLARS_MAX_THREADS=1
  • Modin A: MODIN_CPUS=1, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=1

N = 2

  • Polars: POLARS_MAX_THREADS=2
  • Modin A: MODIN_CPUS=2, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=2

N = 4

  • Polars: POLARS_MAX_THREADS=4
  • Modin A: MODIN_CPUS=4, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=4

N = 8

  • Polars: POLARS_MAX_THREADS=8
  • Modin A: MODIN_CPUS=8, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=8

Dataset: SF=5

Repeat the same reporting structure for SF=5:

N = 1

  • Polars: POLARS_MAX_THREADS=1
  • Modin A: MODIN_CPUS=1, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=1

Note: The Ray engine crashed at ~minute 30 for both Modin A & Modin B, so there are no results for the two setups

N = 2

  • Polars: POLARS_MAX_THREADS=2
  • Modin A: MODIN_CPUS=2, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=2

Note: I cancelled the runs for Modin A & Modin B at ~60 minutes, so there are no results for the two setups

N = 4

  • Polars: POLARS_MAX_THREADS=4
  • Modin A: MODIN_CPUS=4, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=4

N = 8

  • Polars: POLARS_MAX_THREADS=8
  • Modin A: MODIN_CPUS=8, kernel threads=1
  • Modin B: MODIN_CPUS=1, kernel threads=8

Key Takeaways

  • Measured from the recorded per-query runtimes: On SF1 (N=1/2/4/8), Polars is faster than Modin Setup A and Setup B on 22/22 queries in every configuration, with geometric-mean speedups ranging from 47× to 62×. On SF5, Modin produced results only at N=4 and N=8; in those cases Polars is again faster on 22/22 queries, with geometric-mean speedups of 108×–113× vs Modin A and 93×–109× vs Modin B. Total suite time shows the same pattern (e.g., SF5 N=4: 7.199s Polars vs 658.6s Modin A vs 668.5s Modin B). So, it's clear to say Polars is consistently faster in this benchmark. Across the workloads and scale factors I ran, Polars outperformed Modin by a clear margin, suggesting the difference is structural (execution model and engine design) rather than an outlier tied to a single query.
  • On a single machine, Modin’s overhead dominates. In both Modin modes I tested—(a) one worker with more intra-process threading and (b) multiple Ray workers with single-threaded kernels—execution still flows through Ray’s task scheduling and object store. For these TPC-H–style analytics on one node, the end-to-end runtime appears more sensitive to orchestration, scheduling, and garbage-collection overhead than to raw compute throughput.
  • This setup favors a single-process analytic engine. The benchmark is explicitly single-node and memory-resident. In that regime, Polars’ columnar, multi-threaded engine maps naturally to the hardware, while Modin’s primary advantages (cluster-scale parallelism and distributed memory) are not exercised.

Limitations

  • Single-node, in-memory focus. The results reflect a laptop-scale, single-machine scenario with RAM-resident analytics. They should not be extrapolated to multi-node clusters or workloads that exceed local memory—where Modin’s distributed design is intended to shine.
  • Polars benefits from a native columnar execution engine and (optionally) lazy query planning, while Modin executes a pandas-compatible API on top of Ray. The benchmark therefore measures end-to-end systems (including orchestration and execution strategy), not only “parallelism knobs.”
  • I/O is intentionally de-emphasized. Data is standardized as typed Parquet to reduce parsing/type inference costs. This makes the benchmark more representative of execution-heavy analytics, but it may understate scenarios where CSV/text ingestion is the dominant cost.

Code Repo

https://github.com/minhnguyenth/modin_polars_benchmark.git

レビュー漏れ防止のためにRuboCopカスタムCopを作った

この記事はノバセル テクノ場 出張版2025 Advent Calendar 2025の23日目の記事です。

はじめに

ノバセルでエンジニアをしている原です。

本記事では、マイグレーションファイルで外部キー制約が使われていないかをチェックするRuboCopカスタムCopの実装方法を紹介します。

なぜ作ったのか

今開発しているプロダクトでは外部キー制約をつけないルールにしています(外部キー制約の是非についてはここでは語りません)。

コードレビューの際に気をつけてはいるものの、うっかり見逃してしまうケースがありました。RuboCopでチェックできないかと探してみたのですが、ちょうどいいCopが見つかりませんでした(実はあるかもしれませんが)。

ないなら作ってしまえ、ということで自作しました。

やりたいこと

プロジェクトのルールとして、マイグレーションファイルで以下を禁止したいケースがあります。

  • add_foreign_key メソッドの使用
  • referencesadd_referenceforeign_key: true オプション

これをコードレビューで毎回チェックするのは手間なので、RuboCopで自動検出できるようにします。

RuboCop Copの仕組み

実装に入る前に、RuboCopのカスタムCopがどう動くか簡単に説明します。

ASTとノード

RuboCopはRubyのソースコードをAST(抽象構文木)に変換して解析します。例えば以下のコード:

t.references :user, foreign_key: true

は、こんなASTになります:

(send
  (lvar :t) :references
  (sym :user)
  (hash
    (pair
      (sym :foreign_key)
      (true))))

sendはメソッド呼び出しを表すノードタイプです。

on_sendコールバック

on_sendメソッドを定義すると、RuboCopがASTを走査する際に、sendノード(メソッド呼び出し)を見つけるたびに呼び出されます。

def on_send(node)
  # nodeにはメソッド呼び出しの情報が入っている
  node.method_name  # => :references
  node.arguments    # => 引数のノード配列
end

他にも on_classon_defon_hash など、ノードタイプごとにコールバックがあります。

def_node_matcher

RuboCopには def_node_matcher というDSLがあり、ASTのパターンマッチングを簡潔に書けます。

def_node_matcher :add_foreign_key?, '(send nil? :add_foreign_key ...)'

このパターンの意味:

部分 意味
send メソッド呼び出しノード
nil? レシーバがない(add_foreign_key :table のような形式)
:add_foreign_key メソッド名が add_foreign_key
... 引数は何でもOK

定義すると add_foreign_key?(node) というメソッドが使えるようになり、マッチすれば truthy、しなければ nil を返します。

これを踏まえて実装を見ていきます。

実装

# frozen_string_literal: true

module RuboCop
  module Cop
    module Custom
      class MigrationForeignKey < ::RuboCop::Cop::Base
        # Node Patternの定義
        # nil? はレシーバなし(add_foreign_key :table)、... は引数不問
        def_node_matcher :add_foreign_key?, '(send nil? :add_foreign_key ...)'

        def on_send(node)
          # .rubocop.yml の Include で対象を絞っているなら
          # processed_source のチェックは削除してOKです。

          if add_foreign_key?(node)
            add_offense(node, message: 'add_foreign_keyメソッドの使用は禁止されています。')
            return
          end

          check_foreign_key_option(node)
        end

        private

        def check_foreign_key_option(node)
          # references や add_reference などのメソッドの引数を走査
          node.arguments.each do |arg|
            next unless arg.hash_type?

            arg.pairs.each do |pair|
              # key が :foreign_key かつ value が false 以外なら違反
              next unless pair.key.sym_type? && pair.key.value == :foreign_key
              next if pair.value.false_type?

              add_offense(pair, message: 'foreign_keyオプションはfalseにする必要があります。')
            end
          end
        end
      end
    end
  end
end

設定

ファイル配置

lib/rubocop/cop/custom/migration_foreign_key.rb に配置します。

.rubocop.yml

require:
  - ./lib/rubocop/cop/custom/migration_foreign_key

Custom/MigrationForeignKey:
  Enabled: true
  Include:
    - db/migrate/**/*

検出例

以下のようなマイグレーションファイルがあった場合:

class CreateOrders < ActiveRecord::Migration[7.0]
  def change
    create_table :orders do |t|
      t.references :user, foreign_key: true  # ← 検出される
      t.timestamps
    end

    add_foreign_key :orders, :shops  # ← 検出される
  end
end

rubocop を実行すると警告が出ます。

db/migrate/20241223000000_create_orders.rb:4:29: C: Custom/MigrationForeignKey: foreign_keyオプションはfalseにしてください。
      t.references :user, foreign_key: true
                          ^^^^^^^^^^^^^^^^^
db/migrate/20241223000000_create_orders.rb:8:5: C: Custom/MigrationForeignKey: add_foreign_keyメソッドの使用は禁止されています。
    add_foreign_key :orders, :shops
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

まとめ

RuboCopを動かすたびに内部でどうやってチェックしているのか疑問でしたが、思いのほか簡単に自作できました。ASTとコールバックの仕組みさえ理解すれば、プロジェクト固有のルールもすぐに実装できます。

他にもレビュー漏れが発生しやすいものについては、同様にカスタムCopを作ってチェックするようにしています。

最近はAIに「こういうチェックをするRuboCop Copを作って」と頼めばサクッと作ってくれるので、欲しいCopが見つからなければ自分で作ってみるのもおすすめです。