RAKSUL TechBlog

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

JSAI2026 – 2026年度 人工知能学会全国大会(第40回)発表報告

はじめに

こんにちは。ラクスルのデータ戦略部でデータサイエンティストを務めている野中です。 2026年6月8日〜12日に開催された「JSAI2026 – 2026年度 人工知能学会全国大会(第40回)」に参加し、ラクスルのデータ戦略部として発表を行いました。 本記事では、発表内容の概要と、人工知能学会全国大会(JSAI2026)に参加して感じたことについてご紹介します。

群馬県の高崎にあるGメッセ群馬で行われました

JSAI2026とは

JSAI2026(2026年度 人工知能学会全国大会 第40回)は、一般社団法人 人工知能学会が主催する国内最大規模のAI関連学会です。 大学・研究機関・企業など幅広い参加者が集まり、機械学習、画像処理・自然言語処理、生成AI、マルチモーダルAIなど、多様なテーマについて研究発表や議論が行われます。 近年は、研究領域だけでなく実サービスへのAI活用/LLM活用・AIエージェント・業務改善など実践寄りのテーマも増えており、産学双方から大きな注目を集めています。 2026年度大会も、現地・オンラインのハイブリッド形式で開催され、多くの研究者・エンジニア・企業関係者が参加しました。

発表セッションとテーマ

本セッションでは、実企業で得られたデータを活用し、マーケティング上の課題解決に向けて統計・機械学習による予測を行う研究が数多く発表されました。 我々が取り組んだのは、各顧客のLTV(顧客生涯価値)を予測するというマーケティング上の重要な課題です。一般に、事前に顧客ごとのLTVを推定できれば、LTVの高い顧客に対して重点的にマーケティング投資を行うことができ、施策全体の収益性向上にもつながります。このテーマは、ラクスルが日々向き合っているマーケティング上の課題とも重なるものであり、産業現場における実課題と学術研究をつなぐという観点でも意義のあるテーマだと考えています。

LTVを予測するためには、まず各顧客の将来の購買回数を予測する必要があります。その代表的な手法が BTYD(Buy Till You Die)モデル と呼ばれる一群のモデルです。なかでも、 BG/NBDモデルとGamma-Gammaモデルを組み合わせた手法は広く利用されています。我々の発表は2件ともこのモデルを前提とした分析や手法改良の提案を行なっています。

B2Bプラットフォームにおける法人単位LTV予測

こちらは弊チームの磯と筒井の共著によるものです。本研究では、弊社の購買プラットフォームを対象に、BTYDモデル(BG/NBD・Gamma-Gamma)による法人単位のLTV予測に取り組みました。 まず背景として、従来のBTYD研究の多くはB2C(個人向け)を対象としており、B2B(法人向け)への適用は限定的でした。B2Bには固有の難しさがあります。BTYDモデルは本来一人の顧客に対して、1つのポアソン過程を仮定し個人の購買行動をモデル化する手法です。法人には複数のユーザーが所属するため、あるユーザー個人では離反したように見えても、別のユーザーが購買を続けていて法人としては取引が継続しているという事象が起こります。このとき法人単位のLTVを予測するうえでモデルをどの粒度で当てはめるべきかが自明ではなく、各ユーザーを独立顧客として適用し法人に足し上げる「個人粒度アプローチ(アプローチA)」と、法人内の全ユーザーの購買を1つの主体に統合して直接適用する「法人粒度アプローチ(アプローチB)」の2通りが考えられます。

そこで本研究では、この2つのアプローチによる法人LTVの予測精度を実データで比較検証し、B2B事業におけるBTYDモデルの適用可能性を明らかにすることを目的としました。具体的には、次の2つのリサーチクエスチョンを設定しています。RQ1は「個人粒度アプローチと法人粒度アプローチで、法人LTVの予測精度に差はあるか」、RQ2は「その精度差は、法人の所属ユーザー数やユーザーの購買頻度特性によってどう変化するか」です。結果及び考察に関しては、リンク先論文PDFをご参照ください。

商品カテゴリ別の購買回数予測のための階層ベイズBG/NBDモデル

こちらの研究は、私が単著で発表いたしました。本研究では、多様な商材を扱うラクスルのようなECサイトを対象に、「商品カテゴリごとの将来購買回数を予測する」という課題設定のもと、BG/NBDモデルの適用を考えました。BG/NBDモデルによる商品カテゴリごとの将来購買回数の予測には、大きく2つの課題があります。

まず第一に、チラシとのぼりを例に挙げても、カテゴリによって購買パターンは大きく異なります。チラシは、日常業務で利用されるため、毎月あるいは隔週で継続的に購入するユーザーが多く存在します。一方で、のぼりは店舗リニューアルなど特定のタイミングで購入されることが多く、需要構造が異なります。こうした違いを適切に表現するためには、カテゴリごとに異なるパラメータを持たせる必要があります。 第二の課題として、ECサイトの商品カテゴリ別の購買数は、典型的な ロングテール(裾の重い分布) を示します。一部の主力カテゴリには多くの購入者が存在する一方で、大半のカテゴリでは購入者数が限られています。このようなデータが少ないカテゴリでは、パラメータ推定が不安定りやすく、たまたま含まれた少数の特徴的な顧客の影響を強く受けてしまうため、予測精度が低下するという問題があります。

そこで本発表では、これらの課題を解決するためにBG/NBDモデルを階層ベイズモデルへと拡張し、カテゴリごとの購買行動の違いを捉えつつ、データの少ないカテゴリにおいて予測精度を安定化させることを目指しました。より技術的な詳細に関しては、リンク先論文PDFを参照いただければ幸いです。

おわりに

今回のJSAI2026への参加を通じて、AI活用に関するさまざまな知見や刺激を得ることができ、非常に有意義な機会となりました。 ラクスルとしても、今後もAIやデータ活用に関する研究・発信へ継続的に取り組んでいきたいと考えています。最後までお読みいただき、ありがとうございました。

【RubyKaigi 2026】『Faster Bundler, Happier Developers』振り返り — 実プロダクトで bundle install を計測してみた

はじめに

こんにちは、ラクスルのエンタープライズ事業部でエンジニアをしているマナティです。

RubyKaigiは2025年が初参加で、今回が2回目になります。函館は食べ物も美味しく、桜が綺麗で非常に楽しかったです!!

このブログでは、RubyKaigi 2026で発表された『Faster Bundler, Happier Developers』の内容を振り返りながら、実際のプロダクトでどれだけ速くなったかを計測してみます。

bundle installが遅い

bundle installを実行すると、思ったより時間がかかると感じたことはないでしょうか。登壇者の環境では、125個のgemで36MBのデータを持つ新規Railsアプリで、bundle installに30秒以上かかっていたとのことです。

36MBというのは、普通のインターネット接続なら2秒程度でダウンロードできる量です。なぜ30秒もかかるのでしょうか?

Pythonコミュニティではuvというパッケージマネージャーが登場し、高速なインストールが実現されています。これを受けて「BundlerやRubyGemsも、もっと速くできないか?」というモチベーションでこの改善が始まりました。

Bundlerとは

Bundlerはプロジェクトの依存関係を管理するパッケージマネージャーです。主に3つの役割を担っています。

  1. 依存関係の解決
  2. 解決したGemとそのバージョンをダウンロード
  3. インストール

RubyGemsと混同されやすいですが、RubyGemsはgemのインストールツールのことです。BundlerはRubyGemsの仕組みを内部で使っており、現在はRubyGemsリポジトリの中にBundlerが統合されています。

bundle installの処理の流れ

bundle installを実行すると、内部では以下の処理が行われます。

1. Gemfileの評価

まず、Gemfileを読み込み、直接依存(Direct Dependency)のGemとバージョン制約をメモします。

# Gemfile
gem 'rails', '7.1.4'# bundler 内部で依存関係をメモ
add_dependency("rails", ["7.1.4"], {})

2. gemspecを取得

依存関係の解決のために、「どのGemのどのバージョンが、何に依存しているのか」というメタデータが必要です。 そのメタデータはgemspecファイルに記載されており、以下のような内容となっています。例)sidekiq gem v.8.1.5(GitHub

# 例:sidekiq.gemspec
Gem::Specification.new do |gem|
  # gemの名前
  gem.name = "sidekiq"

  # バージョン
  gem.version = Sidekiq::VERSION

  # 必要なRubyバージョン
  gem.required_ruby_version = ">= 3.2.0"

  # gemが依存しているgemとバージョン制約
  gem.add_dependency "redis-client", ">= 0.29.0"
end

gemspecを取得するGemは2種類あります。

  • Direct Dependency(直接依存): Gemfileに書かれたGem
  • Transitive Dependency(推移依存): 直接依存のGemが必要としているGem。gemspecのadd_dependencyに記載されている。
rails 7.1.4           # 直接依存
  ├─ actionpack       # 推移依存
  └─ activerecord     # 推移依存
       └─ activemodel # 推移依存の推移依存

ここでは、Gemfileに記載された直接依存のGemのgemspecを取得します。 推移依存のGemは、解決を進めながら必要になったタイミングでgemspecが取得されます。

gemspecの取得方法は、以下の表のようにGemのソースによって異なります。 Git gemのみ、APIではなくリポジトリを直接cloneしてgemspecを取得します。

ソース 取得先
rubygems.org Compact Index API
プライベートのGem(GitHub Packagesなど) そのサーバのCompact Index API
Git gem(git@github.com:のもの) git cloneし、.gemspecを読む

3. 依存関係の解決

集めたGemのメタデータをPubGrubアルゴリズムのソルバーに渡して、インストールすべきGemのバージョンを決定します。 PubGrubは、パッケージの依存関係の解決に特化したRust製のライブラリです。

gemのバージョンを1つ1つ仮で決めながら、全てのバージョン要求を満たすかバージョン比較しながら検証していきます。 バージョン要求を満たさなかった場合は、戻って別のバージョンを試していくことで、全てのgemのバージョン要求を満たす組み合わせを見つけます。

そして、解決した結果をGemfile.lockに書き込みます。

詳細なアルゴリズムの説明は、PubGrubの作者であるNatalie Weizenbaumさんのブログがわかりやすいです。 https://nex3.medium.com/pubgrub-2fb6470504f

4. ダウンロード

依存関係が解決したら、ワーカースレッドを立ち上げ、.gemファイルをダウンロードします。 .gemファイルの中に、gemの実体やメタデータ情報、チェックサムなどが入っています。

次に、.gemファイルを展開し、Rubyのgemインストールディレクトリにコピーします。 この時、Cネイティブ拡張を持つGemの場合、コンパイルします。

Bundlerやbundle installの内部処理については、RubyKaigi 2023でHiroshi SHIBATAさん(@hsbt)が発表した『How resolve Gem dependencies in your code?』が参考になりました。

ボトルネックと改善

1. バージョン比較のsort keyを改善

PubGrubでは仮のバージョンを決め、そのバージョンで問題がないかを試し、だめであれば別のバージョンを試すということを行うため、バージョン比較が大量に行われます。 また、そのバージョン比較がGem::Versionの<=>演算子を使用しており、ループや条件分岐が複数ありました。

Shopifyのアプリケーションでは、バージョン比較が1400万回呼ばれていたため、時間がかかっていました。

改善:Gemのバージョンオブジェクトを生成する際に、整数の安定ソートキー(Stable Sort Key)を作成することで、整数同士でバージョン比較を行うようにした

結果:バージョン比較が5倍高速化

2. Git gemのダウンロードを並列化

Git gemはgit cloneをしてgemspecを取得する必要があります。 以前は、このcloneが直列で行われていたため、複数のGit gemがある場合に時間がかかっていました。

改善:git cloneを並列化

結果:Git gemのダウンロードが10〜12秒 → 2秒に短縮され、約5倍高速化

3. HTTPコネクションプールの拡張

Bundlerは複数スレッドを立ち上げ、複数ワーカがgemのダウンロード・インストールを行っています。

しかし、複数ワーカーがいるにもかかわらず、HTTPコネクションプールが1本しかなく、全ワーカーが奪い合っていました。 1ワーカーが接続している間、他のワーカーは待機するしかない状態でした。

改善:BUNDLE_JOBSの数に合わせてプール接続を増やす

4. Gemインストールの並列化

以前は、gemを依存関係の順にダウンロードしており、依存があるgemは前のgemのインストール完了を待つ必要がありました。 そのため、実質直列でのインストールとなっていました。

改善:Cネイティブ拡張を持たないgemは依存を気にすることなく、並列してインストールする。Cネイティブ拡張を持たない純粋なRubyで書かれたGemの場合、gemファイルをインストールするだけで良く、依存関係などの考慮はアプリケーションの実行時に行われるためbundle installで行う必要がないため。

一方で、Cネイティブ拡張を持つGemは引き続き依存があるGemは直列でインストールする必要があります。 理由は、Cネイティブ拡張を持つGemでは、CコードをRubyから呼べるようにextconf.rbというファイルをコンパイルします。その際、requireで依存したGemがある場合、依存したGemがインストールされていないとエラーになるからです。

5. global gem cache

※ 登壇者の改善ではなく、RubyGemsコミュニティによる改善

以前は、Gemをダウンロードした時に、Rubyバージョンごとにフォルダが分かれて.gemファイルが置かれてしまい、Rubyバージョンを上げると同じGemを再ダウンロードする必要がありました。

.bundle/ruby/3.3.0/actionview-7.0.8.4.gem
.bundle/ruby/4.0.0/actionview-7.0.8.4.gem  # 同じファイルなのに再DL

改善:Rubyバージョンを問わず、共有できるglobal gem cacheを導入。Rubyバージョンが異なってもGemのキャッシュが効くようになった。

現在はデフォルトが無効のopt-in機能ですが、次のRubyGemsのメジャーリリースでデフォルト有効になる予定です。

ベンチマーク結果

登壇者は bundler-perf-toolkit を用いて計測した結果、以下の結果となり大きく高速化がされました。

シナリオ 結果
直接依存249個、ネイティブ拡張なし 37%高速化
RubyのGemfile 2.6倍速
Git gem 40個 6倍速(2秒未満に)
rails newしたRailsアプリ 2倍速(20秒→10秒)

これらの改善は全て Bundler & RubyGems 4.0.10 で利用できます。

さらなる高速化のために

さらにbundle installを高速化するための最大のボトルネックはCネイティブ拡張のコンパイルです。 Cネイティブ拡張のGemはコンパイルが必要で、Rubyは本質的にコンパイルが遅いため、どうしても時間がかかってしまいます。

解決策は、precompiled gem(事前にコンパイル済みのgem)です。 nokogiriやsqlite3、ffiはこれに対応しています。

しかし、コンパイル済みにするには、全てのプラットフォーム・バージョンごとにコンパイルする必要があります。クロスコンパイルとしてrake-compiler-dockがありますが、デバッグや保守のしづらさが課題となっています。

RubyKaigi 2025でも、Sutou Kouheiさん(@ktou)による発表でCネイティブ拡張のgemのメンテナンスが辛いという話がありました。Goodbye fat gem 2025

その解決策として、登壇者は cibuildgem というGemを作成し、紹介していました。 cibuildgemを使うと、ネイティブにGemをコンパイルし、compiled gemとして公開できます。

  1. CIプロバイダー上でマシンを立ち上げる(macOS / Windows / Linux / ARM64)
  2. ターゲットのプラットフォーム上でネイティブにコンパイルする
  3. コンパイルしたバイナリでテストを実行
  4. precompiled gemとしてRubyGems.orgにリリース(trusted publishing)

登壇者が全gemをprecompile済みにしたRailsアプリを試したところ、20秒 → 3.8秒 になったとのことです。

実際のプロダクトで試してみた

ここまで、bundle installの処理の流れとどういう高速化の改善がされたのかを説明しました。 この改善が実際のプロダクトにどれほど影響を与えたのか、開発に携わっているRailsアプリケーションで実際に試してみました。

Railsアプリケーションの構成

項目
gem総数 275個
直接依存 87個
推移依存 188個
Git gem 5個
Cネイティブ拡張gem(precompiled gemを除く) 22個
precompiled gem 3個(nokogiri, ffi, google-protobuf)
Rubyバージョン 3.3.10
gemの取得元 ・rubygems.org(252個)
・GitHub Packages(3個)

比較するバージョン

セッションで紹介された改善は、4.0.3〜4.0.10にかけて導入されました。4.0.2はこれら全ての改善が入る直前のバージョンです。

そのため、改善前のバージョンとして4.0.2、改善後のバージョンとして4.0.10を比較することにしました。

改善内容 導入バージョン
HTTPコネクションプール拡張 4.0.3
Git clone並列化 4.0.9
並列インストール 4.0.9
stable sort key 4.0.10

計測手法

ベンチマークツールは hyperfine を使用しました。 複数回実行して、平均・標準偏差・最小/最大を出力してくれるCLIツールです。

hyperfine \
  --runs 5 \
  --prepare 'rm -rf vendor/bundle' \
  --export-markdown benchmark_result.md \
  'BUNDLE_IGNORE_BUNDLER_VERSION=true bundle _4.0.2_ install' \
  'BUNDLE_IGNORE_BUNDLER_VERSION=true bundle _4.0.10_ install'

結果

ベンチマーク結果

平均時間 標準偏差
Bundler 4.0.2 113.3秒 ±3.4秒
Bundler 4.0.10 88.7秒 ±1.6秒

結果、4.0.10は4.0.2より約1.28倍(25秒)速くなりました。

また、標準偏差が4.0.10の方が小さくなっています。 これは並列インストールによって、個々の処理のばらつきが平均化されたためだと考えられます。

まとめ

Bundlerの処理の流れと高速化の内容を振り返り、業務アプリで Bundler 4.0.10 にアップグレードしたところ、bundle install が113秒から89秒へ短縮され、約1.28倍速くなることを確認しました。

BundlerやRubyGemsのバージョンアップは後回しにしがちでしたが、CI/CDやDocker buildでも bundle install は頻繁に実行されるため、積み重なって効いてきそうな改善だと思いました。

さらなる高速化のためには、Cネイティブ拡張のコンパイルが依然としてボトルネックとなるようです。 これは、昨年のRubyKaigi 2025でも、Sutou Kouheiさん(@ktou)による『Goodbye fat gem 2025』で触れられた課題感とも通じる話で、2年連続でRubyKaigiを通じてエコシステムの動きが見えてきたのが非常に興味深かったです。

型破りな未来へテイクオフ 〜TSKaigi 2026に参加しています〜

こんにちは、ラクスル事業部 Web エンジニアの長澤、酒井です。

私たちは今、5/22(金)・23(土)の2日間にわたって開催される「TSKaigi 2026」に来ています!

ラクスルでは、祖業である印刷EC「ラクスル」だけでなく、日々非常に多くの多様なサービスを開発・運営しています。そして、それらのプロダクトの多くで、フロントエンドを中心にTypeScriptが標準技術として深く根付いています。

私たちラクスルのエンジニアの多くが、日々の開発においてTypeScriptがもたらす堅牢性や開発スピードの恩恵を大いに受けており、私たちもその中の1人です。だからこそ、世界最大規模となった今回のTypeScriptの祭典にリアル参加できることを、楽しみにしていました!

本記事では、速報として熱気あふれる現地の空気感と、私たちラクスルメンバーがどのような視点を持って今回のTSKaigiに臨んでいるのかをリアルタイムにご紹介します!

はじめに:TSKaigiとは

TSKaigiは、プログラミング言語・TypeScriptに関するあらゆるテーマを扱う、国内最大級のテックカンファレンスです。 3回目の開催となる今年のミッションは、『学び、繋がり、“型”を破ろう』

単に誰かの発表を聴くだけでなく、関わるすべての人たちが互いに学び合い、新たな繋がりを生み出すことで、既存の「型」にとらわれないエンジニアとして生き生きと活躍できる世界を目指しています。日本の、そして世界のTypeScriptの未来を切り拓く「型破り」なイベントです。

今回は簡単に現地の雰囲気と、ラクスルメンバーが何を期待して TSKaigi 2026 に参加しているかをお伝えできればと思います!

現地の様子

今年の会場は、羽田空港第3ターミナル直結の「羽田エアポートガーデン」内にある、ベルサール羽田空港です!

空港の到着ロビーから連絡通路を渡ってすぐ会場にアクセスできるのですが、空港ならではの旅立ちのワクワク感を感じながら会場に向かいました。会場に足を踏み入れると、そんな雰囲気とは一転してTSKaigiならではの熱気に包まれていました。

今回のTSKaigi 2026は、現地参加者が1,000人規模。オフラインとしては、世界最大のTypeScriptカンファレンスとなるそうです!(オープニングトークで代表の方が言っていました)

注目しているセッション

そんな、熱気あふれるTSKaigi 2026ですが、ラクスルメンバーが期待しているセッションを紹介します!

長澤

  • アンチパターンを避ける型駆動React最適化(Kazuya Serizawa)
    • 私が所属している開発チームではNext.jsを使用しており、現在React Compilerの導入を計画中です。アプリケーションコードをどのように記述すると最適化されやすいのか、それをTSの型システムなどの仕組みによって、どのように維持すべきなのかについて学べたらと思います。
  • ビジネスモデルから紐解く、AI+型駆動開発(omote)
    • 仕様やドメインを型で表現するということを意識し日々の開発を行っていますが、複雑なドメインを扱う場合などでは難しいことがあります。型駆動開発をどのように捉えて設計の意思決定を行っているのかについて知見を得られればと思います。

酒井

  • 業務に残された「よくない型」で考える「TypeScriptの難しさ」(Saji)
    • 私が普段携わっている印刷EC「ラクスル」はラクスルの中でも特に大規模かつ歴史あるコードベースであり、やむを得ずts-ignoreなどの妥協を迫られるケースもよくあります。そうした環境下において少しでも型安全なコードを書くためのヒントを得られればと思います。
  • Stage 3 Decorators でできること / できないこと(susisu)
    • 数年前にGoogle Apps Scriptでスプレッドシートに対するCRUDを書けるORMパッケージを個人的に開発したことがあり、その際にTypeORMのような書き方を目指して当時既に実装されていたStage 3 Decoratorsを採用しました。そうした経験から今回改めてデコレーターについて学べればと思います。

おわりに

現在1日目の真っ最中ですが、この後も気になるセッションが盛りだくさんです。最後まで楽しみ、多くの学びを持ち帰りたいと思います!

【RubyKaigi 2026】「Ruby Releases Ruby」セッションレポート ― リリースを支える自動化の設計思想

ラクスル事業部 Web エンジニアの森田です。 RubyKaigi 2026 のうち、参加前から特に楽しみにしていた「Ruby Releases Ruby」セッションについてご紹介します。

かつて Ruby のリリースは重い作業でしたが、さまざまな取り組みを経て、2022年には9回だったリリースが2025年には14回まで増えています。このセッションでは、その裏側にある自動化の設計思想と具体的な事例が紹介されました。

本記事では、セッションの内容をベースに紹介されたスクリプトやワークフローを実際に調査し、読み取った設計の意図も含めながら、特に印象に残った3つの事例を取り上げます。


リリース高速化のためのワークフロー設計

リリースを高速化するにあたって、まず「何を人間がやり、何を自動化するか」を定義することから始まったと紹介されていました。

人間がやることの例 - ビルドパターンの決定 - タグを打つタイミング・アナウンス・公開のタイミング判断

自動化することの例 - リリースノートの生成 - Docker イメージ作成など

事例1. ビルドの自動実行と可視化

Ruby のビルドやテストには、GitHub Actions(out-source)と chkbuild(in-source)の2種類のCI環境が使われています。

この2つは単なる冗長構成ではなく、それぞれ「コミットごとの組み合わせ爆発を並列処理する」、「クラウドでは再現できない実機互換性を担保する」という独立した役割を担っています。

out-source CI:GitHub Actions

OS × コンパイラ × ビルドオプションの膨大な組み合わせ(約120ジョブ)を並列で網羅しています。OS だけでなく実行環境のレイヤまで含めた組み合わせも対象で、例えば「Ubuntu on WSL」専用のワークフロー(wsl.yml)も存在します。

バリエーション例
OS / OS バージョン Ubuntu 22.04, Ubuntu 24.04, macOS 14, macOS 15, Windows 2022
コンパイラ GCC, Clang, MSVC
ビルドオプション YJIT有効, YJIT無効, デバッグフラグ有効
プロセスモデル 32bit, 64bit, 共有ライブラリ有効(--enable-shared

in-source CI:chkbuild

chkbuild は cron で定期実行されており、Amazon Linux・macOS・OpenBSD・Android・s390x・riscv64 など、多種多様な実機・OS環境でのビルドを担います。結果は rubyci.org に集約されており、前回ビルドとの差分やコミットハッシュとの紐付けまで整形されているため、コアメンテナが分散した環境の状態を1画面で追えます。


事例2. リリースノートの自動生成

リリースノートの生成には tool/gen-github-release.rb が使われています。前タグ〜今タグ間のコミットを取得し、各コミットメッセージを2パターンでスキャンします。

  • [Backport/Feature/Bug #1234]bugs.ruby-lang.org からタイトルをスクレイプ
  • (#1234) → GitHub PR ページからタイトルをスクレイプ

重複除去して箇条書きに整形し、GitHub Release を作成します。実際のリリースノートはこちらで確認できます。

AI や複雑な判定ロジックは使っておらず、実装は正規表現によるスキャンとスクレイピングのみです。

また、コミットメッセージをそのまま転記するのではなく、チケット番号をインデックスとして外部トラッカーからタイトルを取得するため、何度リトライコミットが打たれていても、リリースノートに集約されるのはチケット単位の1行のみです。


事例3. RubyGems / Bundler のバックポート自動化

RubyGems / Bundler は CRuby とは独立したリポジトリで管理されており、Ruby 本体より一歩進んだリリース自動化が実現されています。tool/release.rb が cherry-pick からリリース PR 作成まで一連の作業を自動化しています。

master でマージされたコミットを、リリース時にまとめて stable ブランチへ cherry-pick するバックポート方式をとっています。スクリプトは以下を自動で行います。

  1. 前回リリースタグを起点に master の未リリースコミットから対象 PR を特定
  2. 各 PR のマージ戦略(Merge commit / Squash / Rebase)を判定し、適切な引数で git cherry-pick を実行
  3. CHANGELOG 生成・バージョンバンプ・コミット
  4. stable ブランチへのリリース PR(#9554)と、master への CHANGELOG 書き戻し PR(#9555)を同時に作成

4番目の「書き戻し PR の同時生成」が設計上のポイントとみられます。一般的なリリースフローでは stable 側で CHANGELOG を確定した後、人間が手動で master へマージします。その間に master 側で開発が進むと、同じファイルの同じ箇所でコンフリクトが発生します。一方このスクリプトは、リリースと同時に書き戻し PR を生成することで、両ブランチの乖離時間をゼロにし、コンフリクトを構造的に防いでいます。


感想

今回の事例の中では、特に事例1のビルドシステムの設計が印象に残りました。

エッジな CPU アーキテクチャ・特殊な OS 環境を網羅した CI 環境を20年以上前から維持し続けている点はもちろん、GitHub Actions(out-source)では可視化の仕組みが標準で備わっているのに対し、chkbuild(in-source)側でもコミットハッシュへのリンクや差分を含む独自の可視化を整備している点が印象的でした。どちらの環境でも「何がどのコミットで壊れたか」を追える状態を維持していることになります。 一方で効果的なビルドマトリクスの選定や構成管理はメンテナーの判断に委ねられており、その設計方針の背景や意思決定プロセスも気になるところです。

また一連の事例を通じて、単にすべてを自動化するのではなく、何を自動化し何を人間が決定するかをあらかじめ定義してからワークフローを設計することの大切さを実感しました。このアプローチは、昨今の AI 活用や効率的な開発プロセスを考える上でも、そのまま役立つ考え方だと感じています。

【RubyKaigi 2026】AIのバグをテストで検知!形式仕様からPBTを生成する spec-to-pbt で組む堅牢な開発ワークフロー

こんにちは!ラクスル事業部でWebエンジニアをしています井口です。

今回、2026/4/22~4/24にかけて開催されたRubyKaigi 2026に参加してきました。

私自身、今回が初めてのRubyKaigi参加だったのですが、新たな交流や、難しくも面白い新技術が数多くあり、非常に刺激を受けた3日間でした。 このブログでは、そんな数あるセッションの中から、特に今のAI時代だからこそ必要になってくる「生成コードの検証」に焦点を当てたセッションの内容をまとめ、深堀りしたいと思います。

そのセッションは @ohbarye さんの From Formal Specification to Property Based Testです。 実は社内でも、Coding Agentが良い成果物を作るためには検証ループを回す必要があり、検証方法の次なる打ち手として「Property Based Testing」(以降、PBTとします。)が提唱され始めています。事前に社内でも用語が出始めていたからこそ気になっていたセッションでもありました。

なぜ PBT なのか

旧来からあるソフトウェア開発のワークフローとして、プログラマーが仕様・実装・テストを書き、テストが実装を検証するという流れが一般的です。

そこで、重要なことは

  • 書き起こされている仕様が正しいか、正しいことをどのように検証するのか
  • テストが実装をどうやって検証するのか

というところです。

確かに、AIやCoding Agentで指示する際に渡す情報である仕様が間違っていれば、当然、生成される成果物も誤ったものになります。また、テストもフレーキーなものであれば、実装が誤ったものであることを検出できず、バグを本番環境にデプロイしてしまうことに繋がりかねません。 これらを解決し、品質保証しつつ、より良い開発サイクルを回すための方法の1つがPBTになるわけです。

実際、ラクスルの開発現場でもAIによるコード生成を試す中で、「動くコードはすぐに生成されるが、複雑な業務ロジックにおいてAIが織り交ぜてくるバグをどのように見抜くのか」が課題になりつつあります。AIへの指示は自然言語で行っている以上、細かいエッジケースの仕様は曖昧になりがちであり、AIが「よしなに」判断しているケースがよくあります。

形式仕様とPBTの概要

こちらでは聞き馴染みのない形式仕様とPBTについて説明します。(既にご存知の方は読み飛ばしていただいて大丈夫です!)

1. 形式仕様

形式手法は、数学や論理学をベースにして、ソフトウェアやハードウェアシステムの仕様記述や検証を行う手法の総称です。そして、その中でAlloyといったプログラム言語を用いて、システムの振る舞いを厳密に記したものを形式仕様といいます。

例えば、ECサイトでよくある仕様である「商品の在庫引き当てロジック(購入ボタンが押されたら販売可能在庫を1つ減らして出荷待ち在庫を1つ増やす)」をAlloyで表現すると、以下のようになります。

// 販売可能在庫(available)と、出荷待ち在庫(allocated)を持つInventoryモデルの定義
sig Inventory { available: Int, allocated: Int } 

// 在庫を引き当てる(Allocate)時の仕様(ビジネスロジック)
pred Allocate [i, i': Inventory] {
  // 【ガード条件】販売可能在庫が0より大きい場合のみ、この処理が実行できる
  i.available > 0 implies 
  
  // 【状態遷移】販売可能在庫を1つ減らし、出荷待ち在庫を1つ増やす
  i'.available = i.available - 1 and
  i'.allocated = i.allocated + 1
}

自然言語で記述された仕様には、例外パターンと言ったエッジケースで矛盾が生じたり、解釈が曖昧になりがちです。 しかし、このような形式仕様で記述すると、数学的な検証(モデル検査)ができるため、「コードを1行も書く前の段階」で、この仕様自体に矛盾やバグ(例:在庫がマイナスになってしまうケースがないか等)がないかをチェックできるのが最大の強みです。

2. PBT

形式仕様に基づき、実装されたプログラムを検証するのがPBTです。 従来のテスト(RSpecの it ブロックなど)は「在庫数1のとき購入したら在庫数0になる」という個別の具体例を検証しますが、PBTは「そのプログラムが常に満たすべき普遍的な特性(Property)」を記述します。

例えば先ほどの「在庫引き当て機能」のテストであれば、以下のように定義できます。

何度在庫引き当てを繰り返しても、『販売可能在庫 + 出荷待ち在庫』の合計値は、最初の総在庫数と常に完全に一致すること

あとはテストツールが、人間が思いつかないようなランダムな入力値や操作順序(在庫がゼロの状態でさらにキャンセルが走る、大量の注文が同時に来るなど)を数百〜数千通り生成して、実装を徹底的に検証してくれます。 また、バグを見つけた場合はそれを再現する「最小の条件(反例)」を自動で探索して報告してくれるShrinkingという機能もあります。

PBTの手法自体のメリットは以前から語られてきましたが、今、重要性が高まってきている背景には、AIやCoding Agentの台頭があります。 TDDのように先にテストを書いてからAIにアルゴリズムの実装を依頼する場合、従来の具体例を並べたテストだけだと、AIが「その特定のテストデータだけをパスするような実装」を出力してくるリスクがあります。 「本当に仕様の特性を満たした正しい実装になっているか」を検証するためのテストハーネスとして、今PBTの価値が高まってきています。

品質保証されたソフトウェア開発方法

形式仕様とPBTを組み合わせることで、より堅牢なソフトウェア開発ができそうに感じます。しかし、AIがPBTを記述する場合、間違ったテストコードを生成したときに悪い実装が生成され、結局バグを見逃すことになってしまいかねません。

そこで、あるべき理想のフローとしては@ohbaryeさんのスライドにもあるように、以下になります。

  • 人間が仕様を厳密に考え、形式仕様に起こす(仕様の正しさを検証)
  • その形式仕様からPBTのテストコードを自動生成する
  • 生成されたPBTを使って、AIや人間が書いた実装コードを厳密に検証する

この「形式仕様からPBTを自動生成する」という部分を解決するために開発されたツールが spec-to-pbt です。spec-to-pbtの内部仕様や設計思想については、スライド資料や後に公開される発表動画をぜひご覧ください!

Railsでspec-to-pbtを試してみる

先ほどの形式仕様を説明したセクションで示した「商品の在庫引き当てロジック」を用いて、RailsのService ObjectをPBTで検証してみます。

形式仕様を書く

商品の在庫引き当てロジックを、Alloyで記述すると、次のようになります。形式仕様を説明したセクションで示されたものから、状態を追加してみました。 Allocate は販売可能在庫を1減らし、出荷待ち在庫を1増やします。Deallocate はその逆です。どちらの操作でも available + allocated の合計は変わらない、というのが今回検証したい性質です。 人間はこの部分の本質的な仕様を厳密に考えることに集中します。

module inventory_allocation

sig Inventory {
  available: one Int,
  allocated: one Int
}

-- Purchase allocation moves one unit from available stock to allocated stock.
pred Allocate[i, i': Inventory] {
  #i.available > 0 implies
    #i'.available = sub[#i.available, 1] and
    #i'.allocated = add[#i.allocated, 1]
}

-- Cancellation returns one allocated unit to available stock.
pred Deallocate[i, i': Inventory] {
  #i.allocated > 0 implies
    #i'.available = add[#i.available, 1] and
    #i'.allocated = sub[#i.allocated, 1]
}

fact NonNegative {
  all i: Inventory | #i.available >= 0 and #i.allocated >= 0
}

assert TotalConserved {
  all i, i': Inventory |
    (Allocate[i, i'] or Deallocate[i, i']) implies
      add[#i.available, #i.allocated] = add[#i'.available, #i'.allocated]
}

Railsアプリケーションを用意する

最小構成のRailsアプリを作り、テスト用にpbtrspec-railsを入れます。 pbtというGemは、@ohbarye さんが開発しているRuby向けのPBT実行ツールです。こちらのツールの詳細は、RubyKaigi 2024の講演Unlocking Potential of Property Based Testing with Ractorをご確認ください。

rails new inventory-pbt-demo --minimal --skip-javascript --skip-hotwire
ruby "4.0.0"

group :development, :test do
  gem "rspec-rails"
end

group :test do
  gem "pbt", "0.6.0"
end

scaffold(ひな形)を生成する

spec-to-pbtのリポジトリから、Railsアプリのspec/pbt配下にscaffoldを生成します。 spec-to-pbtは記事公開時点ではRubyGems未公開のため、GitHubからcloneしてCLIを使いました。

bin/spec_to_pbt /path/to/inventory_allocation.als \
  --stateful --with-config \
  -o /path/to/inventory-pbt-demo/spec/pbt

生成される主なファイルは次の2つです。

  • inventory_allocation_pbt.rb: 再生成可能なPBT scaffold
  • inventory_allocation_pbt_config.rb: Rails側の実装に接続するための設定

spec-to-pbtでは、生成されたscaffoldを直接作り込むより、*_pbt_config.rb*_impl.rbで実装への接続を管理するのが基本方針です。

RailsのService Objectに接続する

Rails側の実装は、通常のService ObjectとしてAIに実装してもらいました。

module Inventory
  class Stock
    attr_reader :available, :allocated

    def initialize(available:, allocated: 0)
      @available = available
      @allocated = allocated
    end

    def allocate!
      raise "no available stock" if @available <= 0

      @available -= 1
      @allocated += 1
      nil
    end

    def deallocate!
      raise "nothing allocated" if @allocated <= 0

      @available += 1
      @allocated -= 1
      nil
    end

    def snapshot
      { available: @available, allocated: @allocated }
    end
  end
end

Rails側の実装(Service Object)とAlloyのcommandとの対応付けは、人間ないしAIが*_pbt_config.rbで行う必要があります。

InventoryAllocationPbtConfig = {
  sut_factory: -> { Inventory::Stock.new(available: 10, allocated: 0) },
  initial_state: { available: 10, allocated: 0 },
  command_mappings: {
    allocate: {
      method: :allocate!,
      verify_override: ->(after_state:, observed_state:, **) do
        raise "Expected observed inventory after allocate to match model" unless observed_state == after_state
      end
    },
    deallocate: {
      method: :deallocate!,
      verify_override: ->(after_state:, observed_state:, **) do
        raise "Expected observed inventory after deallocate to match model" unless observed_state == after_state
      end
    }
  },
  verify_context: {
    state_reader: ->(sut) { sut.snapshot },
  }
}

これで、Alloyから推論されたモデル状態とRailsのService Objectの実状態を比較できます。 そうして、RSpecを実行してみます。

bundle exec rspec spec/pbt/inventory_allocation_pbt.rb

正しい実装では green になりました。

1 example, 0 failures

バグを入れてみる

次に、allocate!available だけを減らし、allocated を増やし忘れる実装にしてみます。

def allocate!
  raise "no available stock" if @available <= 0

  @available -= 1
  nil
end

この状態で同じ PBT を実行すると、1回目の操作列で失敗しました。

Pbt::PropertyFailure:
  Property failed after 1 test(s)
    seed: 1
    counterexample: [#<Pbt::Stateful::Step command=:allocate, args=nil>]
    Shrunk 4 time(s)
    Got RuntimeError: stateful step 0 (allocate): Expected observed inventory after allocate to match model [args=nil]

通常の具体例に基づくテストでもこのバグは見つけられますが、ここで重要なのは「人間が具体例を並べた」のではなく、「形式仕様から作られたモデル」と「実装の状態」がズレたことをPBTが検出している点です。

AI に実装を依頼した場合でも、availableだけを減らすような「それっぽく動くが仕様を満たしていない実装」を、この検証ループに通すことで検出できます。

試してみた感想

思っていたよりも簡単にRailsアプリケーションにPBTを組み込むことができました。通常の開発では具体例に基づくテストを主に書いていますが、プロダクトのメインドメインやステータスフローではPBTで堅牢にテストし、それ以外は具体例に基づくテストと併用すればよさそうだと感じました。 また、生成されるのは完成品のテストではなくPBTのひな形ですが、その接続作業は想定より少なく、検証ループの土台として十分使えそうだと感じました。

まとめ

このAI時代だからこそ、生成されたコードへの検証はより良いものにしていく必要があります。 今回のセッションでは、その一例として、検証済みの形式仕様・実装を検証するために生成されたPBT・AI Coding Agentを用いた、より品質保証されたソフトウェア開発方法が紹介されました。

PBTは、旧来だと「人間が頑張って厳密な仕様を書き、マシンにチェックさせる」というストイックな開発手法であったと思います。しかし今回の発表を聞いて、「人間は本質的な仕様を厳密に考えることに集中し、そこからツールが決定論的にテスト(PBT)を生成し、AIが生成した実装のバグを徹底的に検証する」開発効率と品質保証の両方を兼ね備えた手法へと進化していくところに未来を感じました。

本ブログで用いたspec-to-pbtは、Rails側の実装との接続はConfigで人間ないしAIが行わないといけません。stateless 向けの自動生成もありますが、対象は限定的なパターンに留まります。今後の進化にも注目していきたいと思います。

参考