
Developer Engagementブロックの@ikkouです。2026年4月22日から24日の3日間にわたり北海道は函館市の函館サーモン・まるなまアリーナで「RubyKaigi 2026」が開催されました。

今回の函館開催にあわせ、通常の白色のみの五稜郭タワーのライトアップが、Rubyをイメージした特別色のレッドにライトアップされていました。
ZOZOは今年もプラチナスポンサーとして協賛し、スポンサーブースを出展しました。
本記事では、前半はWEARのバックエンドエンジニアが気になったセッションを紹介します。後半では、ZOZOの協賛ブースの様子と各社のブースにおけるコーディネートを写真中心に報告します。
- ZOZOとWEARとRubyKaigi
- WEARのバックエンドエンジニアが気になったセッション
- A Faster FFI
- ruby.wasm can also enable JavaScript to call Ruby libraries.
- The Less-Told Story of Socket Timeouts
- Autoresearching Ruby Performance with LLMs
- Exploring RuboCop with MCP
- The Journey of Box Building
- ZOZOブースの紹介
- 協賛企業ブースのコーディネートまとめ
- RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜を開催します
- おわりに
ZOZOとWEARとRubyKaigi
私たちが運営するファッションコーディネートアプリ「WEAR by ZOZO」のバックエンドはRuby on Railsで開発されています。2013年にVBScriptで構築されたシステムでしたが、2020年頃からコードフリーズし、Rubyへのリプレイスを開始しました。現在もリプレイスを進めながら、新規の機能もRubyで開発しています。また、Matzさんを技術顧問としてお迎えし、毎月Matz MTGと称したオンラインミーティングを実施しています。
ZOZOとRubyKaigiの関係は、ZOZOの前身であるVASILY時代のRubyKaigi 2017に遡ります。コロナ禍を経て再開したRubyKaigi 2022からはWEARのバックエンド開発を担うチームが中心となって協賛とスポンサーブースの出展を続けています。
- RubyKaigi2017参加レポート(全日分)とスライドまとめ
- RubyKaigi2018参加レポート
- RubyKaigi 2019参加レポート〜sonots登壇セッション & エンジニア8名による厳選セッション
- RubyKaigi 2022参加レポート 〜エンジニアによるセッション紹介〜
- RubyKaigi 2023参加レポート 〜エンジニアによるセッション紹介〜
- RubyKaigi 2024 参加レポート
- RubyKaigi 2025 協賛&参加レポート
WEARのバックエンドエンジニアが気になったセッション
今年はWEARチームから6名のバックエンドエンジニアがRubyKaigiに参加しました。本パートでは各エンジニアが特に気になったセッションを個々の視点で紹介します。
A Faster FFI
chikaです。私からはAaron Patterson(@tenderlove)氏の「A Faster FFI」を紹介します。
このセッションでは、「RubyはC言語より速くなるか?」という問いからスタートし、具体的にはRubyのFFIを高速化し、ネイティブのC言語(C拡張)よりもRubyを速く実行できるか? というのがメインの議題でした。
余談ですが、Aaron氏はRubyKaigi 2026の外国人登壇者の中でおそらく唯一日本語でスピーチする方です。流暢な日本語に加えて時折ジョークも交え、大変ユニークなセッションであり毎年楽しみに拝聴しています(最初の挨拶では「外国人みたいな名前ですが、外国人です」と日本語でジョークを言っていて面白かったです)。
そもそもFFIとは?
FFIとはForeign Function Interfaceの略称で、一般的にはRubyのような高水準言語からC言語やRust, Zigなどで書かれた外部の関数を呼び出すための「概念」のことを指します(FFI自体は、あるプログラミング言語から他のプログラミング言語で定義された関数などを利用するための機構)。
Rubyでは主にlibffiというライブラリやfiddle gemを介して利用され、プラットフォームごとの呼び出し規則の違いを吸収してくれます。
例:FFIを使ったhello world
#include <stdio.h> void hello(void) { printf("Hello, World from C!\n"); }
require 'ffi' module Hello extend FFI::Library ffi_lib File.expand_path('libhello.dylib', __dir__) attach_function :hello, [], :void end Hello.hello
cc -shared -o libhello.dylib hello.c && ruby hello.rb
#=> Hello, World from C!
FFIはネイティブC拡張に比べ、CRuby, JRuby, TruffleRubyといった異なるRuby実装でも、ほぼそのまま動くというポータビリティの観点でもメリットがあります。
現状のFFIの課題
一見するとRubyからC言語の関数が直接呼び出せるのはパフォーマンスや移植性などの観点から便利そうに見えますが、実際のところFFIは滅多に使われておらず、その理由としてパフォーマンスが悪いという点が挙げられていました。
ベンチマーク比較では、既存のC拡張とFFIを比較すると、ネイティブC拡張の方が約2.4倍も高速に動作しているとのことでした(FF愛(FFI)してない…)。
なぜFFIは遅いのか?
FFIが遅い主な原因として、以下の三点を挙げていました。
- 余分なフレームプッシュ
- Rubyから目的のC言語の関数を最終的に呼び出すまでに、呼び出し規則の変換などを行う「中間的な関数」がFFIやVM内部で何層も呼び出される。そのたびにコールスタックに余分なフレームが積まれる(プッシュされる)ため、関数呼び出しそのもののオーバーヘッドが大きくなってしまう。
- 値の渡し方の違い
- Rubyはスタックを使って値を渡すが、C言語は(x86_64やARM64などの環境において)CPUのレジスタを使用して値を受け取る。そのため、C言語の呼び出し規約(ABI)に合わせて、スタックからレジスタへ値をコピーして詰め直す操作が発生する。
- タイプ変換
- RubyのオブジェクトをC言語が理解できる型に変換し、Cからの戻り値を再びRubyオブジェクトに変換するオーバーヘッドが発生する。
改善1
この遅い原因を解消するために、JITコンパイラを用いてそれらのコストを削減するアプローチを試みていました。そして、Rubyで書かれたFFIのためのJITコンパイラ「FJIT」が誕生しました。これはfiddle gemなど複数のgemの機能を活用して直接マシンコードを生成することで、既存のFFI実装に比べて約2倍ほど高速化することが可能となりました。
改善2
さらにもう1つの解決案として提案されたのが、CRuby内蔵のJITコンパイラであるZJITと、新しいトランスレータであるFFXでした。
FFXの仕組み
FFXは、Rubyで書かれたFFI拡張のコードを読み込み、自動的にC拡張のコードに変換します。この時、生成されるCコードの中にはZJIT向けのヒント(具体的には、生成されるCコードの中に「この関数の引数はこういう型である」といったメタデータをZJITが読み取れる形で配置する)が意図的に埋め込まれます。ZJITはこのヒントから関数の引数や戻り値の型情報を認識することで、最適化された高速なマシンコードを動的に生成することが可能となります。
最終的なパフォーマンス
これらの改善を導入しベンチマークを測定したところ、なんとC拡張よりも約1.4倍速く動作するという結果になりました。結果的に、「RubyはCよりも速くなる?」という問いに対しては、「(JITとFFXの力を借りれば)イエス」となり、つまり「RubyはCよりも速い」という結論で締め括られていました(笑)
FFIの「遅いから使わない」という常識が覆りつつあり、ZJITやFFXの成熟によってRubyからCライブラリを気軽に・高速に呼べる未来が近づいていると感じました。
ruby.wasm can also enable JavaScript to call Ruby libraries.
小島(@KojimaNaoyuki)です。私からはShigeru Nakajima(@ledsun)さんの「ruby.wasm can also enable JavaScript to call Ruby libraries.」を紹介します。
このセッションでは、ruby.wasmのこれまで積み重ねられてきた改善を振り返りつつ、なぜまだ実務での採用事例が少ないのかの分析とそれを解消するために「JSからRubyを呼び出す」機能を強化していく方針が話されていました。
ruby.wasmとは、ブラウザ環境でRuby実行を可能にするものです。JavaScriptと相互で連携しながらRubyをブラウザ環境で実行できます。
ruby.wasmはこれまでRubyからJavaScriptを呼び出す改善を多く実施しています。しかし、現状、本番環境で利用されている例はあまりないそうです。その原因としてこのセッションでは以下の2点について言及されていました。
- ruby.wasmバイナリが大きすぎる
- JSからRubyを呼ぶサポートが弱い
ruby.wasmバイナリが大きすぎることについてはhttps://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.9.3-2.9.4/dist/ruby+stdlib.wasmを例に出して話されていました。こちらのファイルは非圧縮で31.80MiB、圧縮すると8.84MiBになるそうです。そして、BtoCのブラウザアプリケーションではダウンロードサイズが重要ですが、BtoBの場合はダウンロードサイズは大きな問題ではない可能性があるとセッションでは話されていました。
JSからRubyを呼び出すサポートが弱いことについては、RubyからJSを呼び出す場合とJSからRubyを呼び出す場合を比較して扱いやすさの違いを言及していました。以下がセッションで使用されていた表です。
| Feature | R → J | J → R |
|---|---|---|
| eval | Yes | Yes |
| Method Call | Yes | call |
| Property Access | Yes | N/A |
| Object Conversion | Yes | string |
| Load Ruby Scripts | browser | No |
出典:ruby.wasm can also enable JavaScript to call Ruby libraries. | ドクセル(最終閲覧日:2026/05/07)
こちらを見ると確かにJSからRubyを呼び出す場合にサポートされていないものが多い印象ですね。現状JSからRubyを呼び出すにはevalメソッドを利用して実行できますが、rubyの実行結果の戻り値を扱う適切なAPIが不足していたりと課題があるようです。
これらの現状を受けて、本番環境で利用されている例が少ないのは「JSからRubyを呼ぶ」機能が不十分であるからではないかと分析されていました。そして、もしJSからRubyを呼ぶことが簡単にできたなら、ロジックの二重実装を防ぐことができるため有用ではないかとおっしゃっていました。ロジックをRubyに統一できれば修正も容易になり保守性も向上しそうですね!
「JSからRubyを呼ぶ」機能を強化していくためにはRubyオブジェクトをラップするRbValueを拡張する必要があり、このプロジェクトを「Ruby’s Blanket」と呼ぶと発表がありました。ちなみに、「Ruby’s Blanket」という命名は函館出身の人気バンドGLAYの楽曲に由来しているともおっしゃっていました。
セッションでは、目指していきたい理想の姿も紹介されており、以下のような使用感を想定しているようです。
vm.evalFile("dog.rb"); const dog = vm.eval(“Dog.new”); dog.vow(); dog.vow(); const count = dog.count_of_vows.toInt();
出典:ruby.wasm can also enable JavaScript to call Ruby libraries. | ドクセル(最終閲覧日:2026/05/07)
JSネイティブなプロパティアクセスやメソッド呼び出しでRubyプログラムを扱っているところを示しています。すごく直感的にJSからRubyコードを呼び出せていますね!
セッションの最後にはコーディングエージェントの活用についても語られていて、以前よりも難しい課題に取り組むことができていると語られていました。「Ruby Committers and the World」や「Matz Keynote」でもAI活用が議題に上がっており、多くのコミッターがAIを駆使してRubyの改善に当たっているそうです。 個人的には、言語実装のような「世界的にサンプルが少なそうな低レイヤーの領域」はAIが不得意な分野だと思い込んでいたため、これほど多くのコミッターが活用している事実に驚かされました。 Ruby本体や周辺ライブラリの開発が、AI活用によって今後さらに加速していくのが非常に楽しみです!
このセッションを通して「JSからRubyを呼ぶ」機能が充実することで、Rubyの活用の幅が広がる未来を感じました。実際のプロダクトのブラウザ上でRubyが動いているところを目にすることを期待しています!
The Less-Told Story of Socket Timeouts
WEARのバックエンド開発を担当しているaao4seyです。私からは、@coe401_さんの「The Less-Told Story of Socket Timeouts」を紹介します。
このセッションでは、Ruby 4.0でSocket.tcp / TCPSocket.newに新しく加わったopen_timeoutの導入経緯と、その背後にあるsocketライブラリのタイムアウトの歴史が語られていました。本記事ではopen_timeoutが必要になった経緯を発表内容を踏まえて整理し、続いて各Rubyバージョンで実際にタイムアウトの挙動がどう変わるのかを手元で観察した結果を紹介しようと思います。
なぜopen_timeoutが必要だったのか
net/httpのtimeoutライブラリ依存問題
発表者のもとに「net/httpのtimeoutライブラリ依存を外したい」という相談が届いたことが、open_timeout導入のきっかけでした。スライド内でも紹介されていますが、Ruby 2.7の開発時代にもnet/httpのような標準ライブラリが、標準ではないライブラリに依存しないほうが良いのではという趣旨の提案がなされていました。
加えて、timeoutライブラリは内部でTimeout::State::GLOBAL_STATEという共有状態を持っているため、non-main RactorからこれにアクセスするとRactor::IsolationErrorが発生してしまう状況でした。
resolv_timeout + connect_timeoutでは代替できない
Ruby 4.0開発時点で、Socket.tcp / TCPSocket.newには名前解決のタイムアウトを指定するresolv_timeoutと、接続確立のタイムアウトを指定するconnect_timeoutがすでに用意されていました。これらを組み合わせればtimeoutライブラリの代替になりそうにも思えますが、実際にはメソッド全体の絶対上限時間を制御できません。
ここで前提として1つ補足しておくと、Ruby 3.4でSocket.tcp / TCPSocket.newにはHappy Eyeballs Version 2 (HEv2 / RFC 8305)がfast_fallbackという名前でデフォルト有効として導入されました。HEv2は複数候補のIPアドレスに対して名前解決と接続試行を並行実行するアルゴリズムです(HEv2 自体の解説は同僚が「RubyKaigi 2025 協賛&参加レポート」で書いているので、本記事ではそちらに委譲します)。なお、Ruby 3.4まではfast_fallback: false経路のresolv_timeoutがAPI onlyで実装されておらず、本記事の対象であるRuby 4.0で両経路で機能するように修正されています。
その上で、たとえばfast_fallback有効でresolv_timeout: 2000ms, connect_timeout: 1000msを指定した場合、IPv6が先に解決されてIPv4が解決待ちのまま接続試行が始まると、connect_timeoutの1000msを過ぎてもresolv_timeoutの期限まで待機が続きます。結果、全体タイムアウトは2つの合計でもconnect_timeoutの値でもなく、resolv_timeoutの値である2000msに支配されてしまいます。
HEv2のConnection Attempt Delay (250ms)が絡むケースでは「connect_timeout: 1000msを指定しても全体は1250ms待つ」、fast_fallback無効では「IP数 × connect_timeout」と、いずれもユーザーが指定した値とは別の値で全体時間が決まってしまいます。
open_timeoutのAPI仕様
これらの背景を受けて、発表者ご自身がopen_timeoutをSocket.tcp / TCPSocket.newに追加する提案をされました。open_timeoutはtimeoutライブラリと同じく「名前解決〜接続確立の全体」を1つの期限で管理するオプションです。
Socket.tcp("ruby-lang.org", 80, open_timeout: 1)
設計上の論点として、open_timeoutをresolv_timeout / connect_timeoutと併用された場合にどう振る舞うかを整理する必要がありましたが、複雑になりすぎてしまうという課題があり、最終的にopen_timeoutはconnect_timeout / resolv_timeoutとの同時指定を禁止し、同時指定時はArgumentErrorを投げる仕様となったそうです。
発表内では実装の細かい部分がfast_fallbackありなしごとに説明されていました! スライドのp.224以降に記載されています。
試してみる
ここまで見てきた経緯を踏まえて、open_timeoutがRuby 4.0で動作するのかの検証をしてみます。また、私自身はRuby 3.4で導入されたfast_fallback機能もこの発表内で知ったので、こちらの挙動も合わせて確認してみることにしました。
実験のため/etc/hostsに応答しない3つのIPを割り当てたtest-multi-local.wear.jpを用意しました(記載のIPはdummyです)。
192.168.xx.1 test-multi-local.wear.jp 192.168.xx.2 test-multi-local.wear.jp 192.168.xx.3 test-multi-local.wear.jp
接続先には応答しないポート(50000)を指定し、SYNを送っても応答が返ってこない状況を作ります。
Ruby 3.4: fast_fallback ON
この場合、HEv2のアルゴリズムに従い以下のような挙動になるはずです。
- 約250ms間隔(Connection Attempt Delay)で次のIPへ並行に試行を発射している
- 期待結果:(IP数-1) × 250ms + connect_timeout = 1.5 秒で全体タイムアウト
検証スクリプト
require "socket" t = Time.now begin Socket.tcp("test-multi-local.wear.jp", 50000, connect_timeout: 1, fast_fallback: true) { |s| s.close } rescue => e puts "#{e.class}: #{e.message}" ensure puts "elapsed=#{Time.now - t}s" end
実行結果(Ruby 3.4.9)
Errno::ETIMEDOUT: Operation timed out \- user specified timeout elapsed=1.51281s
tcpdumpの結果抜粋
10:41:18.580 SYN → .1 ← IP1 開始 (t=0) 10:41:18.836 SYN → .2 ← IP2 開始 (t≒0.256) 10:41:19.086 SYN → .3 ← IP3 開始 (t≒0.506)
想定どおり、約1.5秒でtimeoutエラーになることが観測できました! また、tcpdumpでSYNパケットをざっくり観測した結果、約250msごとに異なるIPへSYNパケットが飛んでいることも見て取れました。
Ruby 3.4: fast_fallback OFF
fast_fallback: falseを渡すと、Ruby 3.3以前と同じ直列フォールバックの挙動に戻ります。以下のような挙動になるはずです。
- 各IPに対して
connect_timeout: 1で1秒ずつ順番に試行 - 期待結果:IP数 × connect_timeout = 3 秒で全体タイムアウト
検証スクリプト
前述のスクリプトのSocket.tcp呼び出し行を以下のように変更します。
- Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: true) { |s| s.close } + Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: false) { |s| s.close }
実行結果(Ruby 3.4.9)
Errno::ETIMEDOUT: Operation timed out \- user specified timeout elapsed=3.013505s
tcpdumpの結果抜粋
10:40:20.753 SYN → .1 ← IP1 開始 (t=0) 10:40:21.760 SYN → .2 ← IP2 開始 (t≒1.0) 10:40:22.764 SYN → .3 ← IP3 開始 (t≒2.0)
想定通り、fast_fallbackを無効にするとIP1 → IP2 → IP3と1秒ずつ順番に試行され、合計約3秒で全体タイムアウトすることが観測できました。
Ruby 4.0: open_timeout
ここまでの結果から、connect_timeoutだけではメソッド全体の上限時間を制御できないことが見えました。Ruby 4.0で導入されたopen_timeoutを使うと挙動がどう変わるかを見ていきます。
fast_fallback ON + open_timeout
fast_fallbackONでopen_timeoutが機能するか試します。
- 期待結果:open_timeoutに指定した1秒でタイムアウト
検証スクリプト
前述のスクリプトのSocket.tcp呼び出し行を以下のように変更します。
\- Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: true) { |s| s.close }
\+ Socket.tcp("test-multi-local.wear.jp", 50000, fast\_fallback: true, open\_timeout: 1) { |s| s.close }
実行結果(Ruby 4.0.3)
IO::TimeoutError: user specified timeout for test-multi-local.wear.jp:50000 elapsed=1.004653s
想定通り、約1.0秒でIO::TimeoutErrorが発生し、open_timeout: 1で指定した時間とほぼ同じ時間でタイムアウトすることが観測できました!
fast_fallback OFF + open_timeout
fast_fallbackOFFでopen_timeoutが機能するか試します。
- 期待結果:open_timeoutに指定した1秒でタイムアウト
検証スクリプト
前述のスクリプトのSocket.tcp呼び出し行を以下のように変更します。
\- Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: true) { |s| s.close }
\+ Socket.tcp("test-multi-local.wear.jp", 50000, fast\_fallback: false, open\_timeout: 1\) { |s| s.close }
実行結果(Ruby 4.0.3)
Errno::ETIMEDOUT: Operation timed out \- user specified timeout for 192.168.xx.1:50000 elapsed=1.007804s
こちらも想定通り、約1.0秒でuser specified timeoutが発生し、open_timeout: 1で指定した時間とほぼ同じ時間でタイムアウトすることが観測できました! fast_fallbackの設定有無に関わらずopen_timeoutが機能していることがわかりました。
同時指定はエラー
open_timeoutはconnect_timeout / resolv_timeoutとの同時指定が禁止されており、Socket.tcpの入口でArgumentErrorが即座に発火します。
Socket.tcp("test-multi-local.wear.jp", 50000, connect_timeout: 1, open_timeout: 1) { |s| s.close } # => ArgumentError: Cannot specify open_timeout along with connect_timeout or resolv_timeout
全体の対比
ここまで観測した結果を並べると以下のようになります。
| Ruby | 設定 | elapsed |
|---|---|---|
| 3.4.9 | fast_fallback: false, connect_timeout: 1 |
約 3.01 秒 |
| 3.4.9 | fast_fallback: true, connect_timeout: 1 |
約 1.51 秒 |
| 4.0.3 | fast_fallback: false, open_timeout: 1 |
約 1.01 秒 |
| 4.0.3 | fast_fallback: true, open_timeout: 1 |
約 1.00 秒 |
connect_timeoutだけを指定したケースでは、ユーザーの指定値とは別の値で全体時間が決まっていたのに対し、open_timeoutを指定すれば指定値ぴったりで打ち切られるようになりました。
おわりに
本パートではセッションの内容であるopen_timeoutの導入経緯と、実際にfast_fallbackやopen_timeoutの挙動を観測した結果を紹介しました!
open_timeoutの実装により、接続試行全体のタイムアウトが管理しやすくなったと感じます。実際のセッションではRuby 2.0の開発時代から今に至るまでのsocketライブラリのタイムアウト導入の歴史も詳細に説明されており、各時代ごとにどんな課題がありどう解決してきたのかを知ることができとても興味深かったです。また、このセッションに限らずですが、RubyKaigi全体を通して発表者の方のモチベーションを垣間見ることができたのも自分にとって良い刺激となりました。
Autoresearching Ruby Performance with LLMs
小山です。私からはNate Berkopec(@nateberkopec)さんの「Autoresearching Ruby Performance with LLMs」を紹介します。
Berkopec氏は、Speedshop代表でありPumaメンテナー、Railsパフォーマンスコンサルタントとして広く知られています。本セッションはAutoresearchという先行ツールを参考に、AIエージェントを使って、Rubyのパフォーマンス問題を自動で調査する取り組みとそれにより得られた示唆の発表でした。
発表資料は以下のリポジトリで公開してくださっているためご参照ください。
Autoresearchとは
Autoresearchは、2026年3月にAndrej Karpathy(@karpathy)氏が公開したLLM実験自動化ツールです。仕組みはシンプルです。
- AIエージェントがコードへの変更を提案・適用する
- 一定時間ベンチマークを実行する
- ベースラインより良ければコミット、悪ければリバート
- 1に戻る
これを無限に繰り返します。PyTorchと単一のメインファイル以外に依存関係がなく、1時間あたり約12実験、一晩で約100実験を回せます。Berkopec氏がこのコンセプトをRubyのパフォーマンス改善に応用できるかを探ったのが今回の発表です。
なぜ「ループ」が重要なのか
セッションで繰り返し強調されたのがループという概念です。
AIエージェントは本質的にはループにすぎないと、Berkopec氏は次のコード例で説明しています。
messages = [user_prompt] loop do reply = llm.call(messages, tools: TOOLS) break puts(reply.text) unless reply.tool_call? result = run_tool(reply.tool_name, reply.arguments) messages << reply messages << tool_result(result) end
このループにどのようなゲート(通過条件)を置くかが、ループの性格を決めます。それがセッション全体の一貫したテーマでした。
4つのループパターン
Berkopec氏は、ゲートの種類によってループを4種類に分類しました。
| ループ | ゲート | シグナル | 成果物 |
|---|---|---|---|
| Agents | LLM 自己停止 | 離散 | 最終的な返答 |
| Ralph | ビルド+テスト | 離散 | グリーンなコミット |
| Autoresearch | ベンチマーク差分 | 連続 | 改善した diff |
| Factory | 多数のチェック | 多変数 | マージ可能な PR |
Ralphループ
while :; do cat PROMPT.md | claude-code ./build\_and\_test || continue git add \-A git commit \-m "ralph: passing build" git push done
ビルドとテストをゲートにしたループです。テストが通った変更だけをコミットし続けます。バグへの対処に向いています。
Autoresearchループ
best \= benchmark loop do change \= agent.propose\_optimization apply(change) score \= benchmark if score \> best git\_commit(change.summary) best \= score else git\_revert end end
ゲートがブール値(pass/fail)ではなく連続値(ベンチマークスコアの改善量)である点がRalphとの違いです。
Factoryループ
backlog.each do |spec| loop do code \= agent.implement(spec) gates \= scenarios.map { |s| s.run(code) } break if gates.all?(&:pass?) end ship(code) end
スペックからコードを生成し、複数のゲートを全て通過したら出荷するパターンです。StrongDM社の「Software Factory」が参照されていました。
Sidekiqでの実証実験
セッション中に紹介された実験の1つが、pi-autoresearchを用いた、Sidekiqを対象にした自動最適化です。
実験の背景
SidekiqのProcessor::Counterはアトミックなカウンターで、16行程度のシンプルな実装です。Autoresearchエージェントはここにストライプドロック(各スレッドが自前の状態を持ち、書き込みを安くする仕組み)を適用する変更を提案しました。
また、別のPRではTime.now.to_iをProcess.clock_gettimeに置き換えることも提案されました。
Comparison:
Process.clock_gettime: 17294486.8 i/s
Time.now.to_i: 12698329.6 i/s - 1.36x slower
Timeオブジェクトを生成せずに整数を直接返すことで1.36倍高速化できます。
"マージされないコードを生成することの意味"
これらの変更についてBerkopec氏は「OSSメンテナーは実際にはこれをマージしないだろう」と正直に述べていました。その上で、なぜマージできないコードを生成するのか、という問いを投げかけました。
"Why generate code that you can't merge?"
この問いに対して、セッションはPR レビューの目的を再定義するパートへと展開しました。
PRレビューとは何か?
変更を却下する理由として、Berkopec氏は以下を挙げました。
- バグがある → LLMはある程度得意
- トレードオフがある →「十分に速い」とはどのレベルか?
- 複雑すぎる → Flog/Flayスコア、ABCスコア、LOC
- リスクが高すぎる → キャッシュ無効化など(Autoresearchはキャッシュが大好きで、しかもバグりやすい)
- テストを通るが... → Autoresearchは良いテストがある前提で動く
- GPL 違反など → GitHub外の法的・コンプライアンスチェック
要するに「マージできるか」を判断する能力こそがArchitectであるという主張です。
4つのレッスン
セッションを通じて提示された4つのレッスンをまとめます。
Lesson 1: 「自動調査」であって「自動変更」ではない
Stan Lo氏(Shopify)の取り組みが紹介されました。Ruby-lspのCI時間を33%削減、rubydexのインデックス速度を10%〜50%改善するといった成果を、AIの提案を自身がレビューした上で実現しています。AIの出力を人間が検証・理解して取り込む姿勢が重要だという例です。
Lesson 2: 自分がオーナーで Architect でないものに Autoresearch を適用しない
理解・検証・修正できるコードにのみ適用すべきです。
Lesson 3: ループはビター・レッスンを実践する
Richard Suttonさんの「苦い教訓(Bitter Lesson)」(人間の知識を組み込むより、計算のスケールによって探索・学習する方が長期的に優れるという教訓)をループは体現しています。ループは人間のゲートを担保するバージョンに過ぎず、ゲートをソフトウェアに置き換えることで探索をスケールさせられます。
Lesson 4: 人間のゲートをソフトウェアゲートに変換する
「遅い」という感覚を赤/緑の状態・バグチケットに変換するプロセスがパフォーマンス改善の本質です。たとえAutoresearchが失敗しても、ゲートを定義する過程でソフトウェアの品質自体が上がります。
Software Factory とその課題
セッション後半では、Software Factory(StrongDM社の提唱するコンセプト)が紹介されました。
"Code must not be written by humans. Code must not be reviewed by humans." — StrongDM's "Software Factory"
これは人間のレビューを完全に排除し、多数の自動ゲートを通過したコードのみを出荷する考え方です。セッションではこれを「dark factory」と表現し、Berkopec氏は多くの未解決の問いを並べて紹介しました。
- 形式手法・プロパティベーステストは現実的にスケールするか?
- 既存の大規模コードベース(ブラウンフィールド)に適用できるか?
- LOCの肥大化をどう防ぐか?
- LLMがトレーニングデータの著名な最適化を「再発明」するだけにならないか?
- 決定論的なゲートとLLMジャッジをどう組み合わせるか?
Berkopec氏はこれらに対して現時点で答えはないとしながらも、「ゲート設計こそが重要になる」という方向性を示しました。
まとめ
Berkopec氏のセッションを一言でまとめると、「AI に自律的なループを回させることはできるが、それを意味のある成果に変えるにはゲート設計という人間のスキルが必要」 というメッセージでした。
発表で推奨されていた実践ステップは以下の通りです。
- 自分が十分に理解・レビューできるコードにのみ適用する(Stan Lo氏のアプローチ)
- 成熟した小さなモジュールから始める
- ゲートが最小で済む場所からスタートする
- ゲートを増やせば人間のレビューを減らせる
- Ralph(離散)・Autoresearch(連続)・Factory(多ゲート)のいずれかのループを試してみる
おわりに
今年のRubyKaigiはAIに関するセッションの多さが印象的でした。普段の業務で自身もAIを活用しています。本セッションで紹介されていたゲートの設計・導入に注力して、パフォーマンス改善はもちろんのこと、それ以外のあらゆる業務課題の自動化をより推進していきたいと思いました!
Exploring RuboCop with MCP
伊藤です。今年もRubyKaigiに参加してきました! 私からは@koicさんの「Exploring RuboCop with MCP」をご紹介します。
本セッションは「I. MCP Ruby SDK」「II. RuboCop x MCP」の2部構成で、前半では@koicさんがコミッターを務める公式のMCP Ruby SDK(mcp gemとして公開)の設計や、Streamable HTTPでのセッション管理・Sampling・Elicitationまでが解説されました。後半は、RuboCop 1.85.0で実験的に追加された組み込みMCPサーバーの紹介でした。
ここでは特に後半「RuboCop x MCP」を題材に、WEARのバックエンドエンジニアとして実際に手元で動かしてみた感想を中心にご紹介します。
rubocop --mcpは、RuboCop 1.85.0から導入されたサブコマンドで、AIエージェント(Claude CodeなどのMCPクライアント)からRuboCopを呼び出せるようにする組み込みMCPサーバーを起動します。stdio transportで通信し、以下の2つのツールを公開します。
rubocop_inspection:オフェンス検査(読み取り専用)rubocop_autocorrection:オートコレクト適用(破壊的)
なお、RuboCopではコーディング規約違反のことを「オフェンス(offense)」と呼びます。以降本記事でもこの用語を使います。
rubocop --lsp の内部で使われている診断機構(RuboCop::LSP::Runtime)をそのまま再利用しているのが特徴で、出力されるオフェンスはLSPのDiagnostic形式(JSON)です。Clang形式のテキストよりも構造化されているため、AIエージェントがそのままパースして扱いやすい形になっています。
ここからは実際に手元で動かしてみます。rubocop --mcpを使うには、RuboCop本体に加えてmcp gemが必要です(RuboCopはmcpをruntime dependencyとして持っていないため)。既存のGemfileのdevelopmentグループに以下を追加します。
group :development do gem 'mcp' end
次にClaude Codeに.mcp.jsonでRuboCopのMCPサーバーを登録します(Claude Codeが設定を読み、起動時にbundle exec rubocop --mcpをサブプロセスとして立ち上げてくれます)。
{ "mcpServers": { "rubocop": { "command": "bundle", "args": ["exec", "rubocop", "--mcp"] } } }
Claude Code上で/mcpを叩くと、登録したrubocopサーバーから2つのツールが認識されているのが確認できます。
❯ /mcp ──────────────────────────────────────────────────── Tools for rubocop 2 tools ❯ 1\. RuboCop's inspection read-only 2\. RuboCop's autocorrection destructive
それでは実際にrubocop_inspectionを呼んでみます。オフェンスを意図的に仕込んだ以下のサンプルファイルsample.rbを用意して、検査を呼んでもらいます。
# frozen_string_literal: true def greet( name ) message = "Hello, "+name+"!" puts message end greet("WEAR")
「sample.rbをRuboCopで検査して」とお願いすると、Claude Codeがrubocop_inspectionツールを呼び、結果を以下のように整形して提示してくれました。
❯ sample.rb をRuboCopで検査して Called rubocop (ctrl+o to expand) ⏺ sample.rb の検査結果: 9 件の offense が検出されました(すべて自動修正可能)。 ┌─────┬─────────────────────────────┬─────────────────────────────┐ │ 行 │ Cop │ 内容 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 2 │ Layout/SpaceInsideParens │ 括弧内の余計なスペース(2 │ │ │ │ 箇所) │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Style/StringConcatenation │ \+ 連結より文字列補間を推奨 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Style/StringLiterals │ "Hello, " → │ │ │ │ シングルクォート推奨 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Style/StringLiterals │ "\!" → シングルクォート推奨 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Layout/SpaceAroundOperators │ \+ 演算子の前後にスペース無 │ │ │ │ し(2 箇所) │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 4 │ Layout/IndentationConsisten │ インデントが不揃い │ │ │ cy │ │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 7 │ Style/StringLiterals │ "WEAR" → │ │ │ │ シングルクォート推奨 │ └─────┴─────────────────────────────┴─────────────────────────────┘ すべて rubocop \-A で自動修正できます。自動修正を実行しますか?
「自動修正を実行しますか?」というフォローアップ付きで、続けてrubocop_autocorrectionツールへの切り替えまで自然に提案してくれているのが分かります(行番号がLSPの0-indexed値のまま表示されているのは、RuboCopが返すLSP Diagnosticの値をClaude Code側で1-indexedに変換せずに表示しているためです)。
このとき裏でrubocop_inspectionから返ってきている生のJSONは、紙面の都合でcode_actionsを省略し、検出された9件のうち代表2件のみ抜粋すると以下のようになっています。
{ "files": [ { "path": "sample.rb", "offenses": [ { "range": { "start": { "line": 2, "character": 10 }, "end": { "line": 2, "character": 11 } }, "severity": 3, "code": "Layout/SpaceInsideParens", "codeDescription": { "href": "https://docs.rubocop.org/rubocop/cops_layout.html#layoutspaceinsideparens" }, "source": "RuboCop", "message": "Layout/SpaceInsideParens: Space inside parentheses detected.", "data": { "correctable": true, "code_actions": [/* 省略 */] } }, { "range": { "start": { "line": 3, "character": 12 }, "end": { "line": 3, "character": 30 } }, "severity": 3, "code": "Style/StringConcatenation", "codeDescription": { "href": "https://docs.rubocop.org/rubocop/cops_style.html#stylestringconcatenation" }, "source": "RuboCop", "message": "Style/StringConcatenation: Prefer string interpolation to string concatenation.", "data": { "correctable": true, "code_actions": [/* 省略 */] } } ] } ], "summary": { "target_file_count": 1, "offense_count": 9 } }
LSPの診断と同じ形式なので、各オフェンスにcop名・該当範囲・cop個別のドキュメントURL・自動修正可否(correctable)まで構造化された形で含まれます。さらに省略したcode_actionsには実際のautocorrectで挿入する文字列(例:Style/StringConcatenationなら"Hello, #{name}!"への置換)や、# rubocop:disableを入れて該当行で無効化する案もLSPのWorkspaceEdit形式で含まれており、エージェントから機械的に扱えるようになっています。rubocop_autocorrectionツールに切り替えれば、そのまま破壊的に修正をかけることもできます。
WEARのバックエンドではすでにRuboCop+各種pluginを大規模に運用しているので、生成AIによる開発を取り入れる際にも「RuboCopを通っている」という決定的なゲートはそのまま使い続けたい、と日頃から思っていました。本セッションは、まさにその「決定的なツール(RuboCop)」と「確率的なLLM」をrubocop --mcpというかたちで素直に橋渡ししてくれる発表で、聞きながら自分の開発フローに組み込む絵が具体的に浮かびました。
セッション後半で@koicさんが投げかけられていた "What happens when combined?"(決定的なツールと確率的なLLMを組み合わせると何が起きるか)という問いも印象的でした。例えば、現状の .rubocop.yml で無効化しているcopをLLMに一時的に有効化させ、レビュー観点として「いま無効になっているcopをあえて見たらどう見えるか」を提示してもらう、といった使い方は、ツール単体だと出てこない発想です。Streamable HTTPでのSamplingやElicitationまで含めて、まだ「探索が始まったばかり」という締めくくり通り、これからRuboCop×MCPでどんな試みが出てくるかとても楽しみです。
The Journey of Box Building
坂元(@sakam0cchan)です。私からはSatoshi Tagomori(@tagomoris)さんの「The Journey of Box Building」というセッションを紹介します。
このセッションは、Ruby 4.0で実験的に導入される新機能「Ruby Box」をテーマにしたKeynoteです。Boxの基本概念から、実行中のRubyプログラムの中でBoxを特定する際の難しさ、そしてその実装に至るまでの背景などが語られていました。
私自身が初めてRubyKaigiに参加し、そして初めて聞いたKeynoteがこの内容だったこともあり、これからの3日間がどんなものになるかとてもわくわくしました。そんな思いも込めて、内容を共有します。
Ruby Boxとは何か
Ruby Box(RubyKaigi 2025までは「Namespace」と呼ばれていた機能)は、Ruby 4.0で導入された実験的な機能です。
Ruby Boxは、これまでRubyのプロセス全体で共有されていたクラスやモジュール、モンキーパッチなどを、別の空間(Box)として分離し読み込み・実行できる革新的な機能となっています。たとえば、something.rbに書かれたクラスを、メイン環境とは独立したBoxの中に読み込むイメージです。
# something.rb class Something def hello = "hello from Something" end
# main.rb box = Ruby::Box.new box.require('something') s = box::Something.new p s.hello # => "hello from Something"
Boxの中で読み込んだSomethingはbox::Somethingとしてだけアクセスでき、Boxの外からは見えません。これにより、ライブラリやモンキーパッチをプロセス全体へ漏らさずに使えるのがBoxのコアアイデアです。セッションの中では、以下のような用途がBoxの利用シーンとして挙げられていました。
- 一部のコードにて有効なモンキーパッチの適用
- テスト内でのモック処理
- 異なるバージョンのライブラリに依存する複数のアプリケーションを、同じプロセス内に同居させる
- どちらにも同じリクエストを送ることで、異なるバージョンのライブラリでの挙動を比較する
このように、Boxは「同じプロセス内での隔離された環境」を提供することで、これまでRubyで難しかったことを可能にする機能として期待されています。
Ruby Boxの仕組み
本セッションでは、Ruby Boxを実現する上で必要不可欠となる「現在、実行中のコードがどのBoxに属しているか」を判定する仕組みが詳細に解説されていました。
Ruby Boxでは、「1つのファイルを、別のBoxで同時に読み込む」といったことが可能です。つまり、ファイル自体は特定のBoxに縛られていないため、プログラムが実行されるたびに、Ruby自身が「自分は今、どのBoxの中で動いているのか?」を動的に特定し続けなければなりません。
Boxの特定・決定のプロセスは、以下のようなステップで行われているとのことでした。
- 現在実行中のフレームを特定する
- まず、自分がいま実行しているControl Frameを見つける
- LOCALな変数スコープを探す
- そこから、ブロックや例外処理といった非ローカルなものを無視し、ファイル全体、クラス、メソッドといった「LOCAL(局所的)」なスコープを持つフレームまでさかのぼる
- Boxの情報を引っ張り出す
- そこで、見つけたLOCALなフレームの中に保存されている情報をたどり、そこに紐付いている「Box情報」を取得する
ここで一番興味深かったのは、Control Frameとローカル変数などを管理しているENVとの関係性です。それぞれはRubyを実行する上で、現時点でどのコードが動いているのかを把握するためのものです。Rubyのメモリ上では、Control FrameとENVの2つが両端から内側に向かって伸びていく特殊な構造となっているそうです。
この二つが衝突すると、Stack Overflowが発生するという話をこのセッションで初めて知り、今まであまり意識していなかったRubyの内部構造について、もっとより知って勉強したいと改めて感じました。
Ruby Boxが生まれた歴史
さらに、印象に残っているもう1つのトピックとして、Ruby Boxが生まれた歴史についての話があります。開発者のTagomoriさんがRuby Boxの開発を始めたきっかけは、RubyKaigi 2023で発表された「Multiverse Ruby」のセッションでした。その内容が、過去に自身が抱えていた課題と重なると感じたことで、一気にモチベーションが高まったそうです。
さらに、その会期中に発表者の方と直接話す機会があり、当初は「Hako」という命名にして函館のRubyKaigiで発表しよう、という話もあったそうで…。そして実際に今、こうして発表に至っている、なんと胸が熱くなる展開でしょうか。
こんなエピソードを通して、「みなさんにも、ふとモチベーションが湧き上がる瞬間があるかもしれない。あなたにとっての『Multiverse Ruby』があるかもしれない。そんなプロセスも含めてRubyKaigiを楽しんでほしい」という言葉が印象的でした。最初のセッションでその一言を聞いたとき、これから始まる3日間のRubyKaigiにとてもわくわくしたあの瞬間を、今でも覚えています。そんな開発の裏エピソードも含め、とても印象的なセッションでした。
おわりに
本セッションは「新機能の紹介」というよりも、どんなふうに実現されているのか、さらにその機能が生まれた瞬間を聴ける発表で、聴いた後の余韻が長く残るキーノートでした。
WEARのバックエンドはRuby on Railsで開発・リプレイスを進めており、monkey patchが絡む箇所のスコープ管理に苦労する場面が今後出てくるかもしれません。Boxのように「実行時の境界」を表現できるようになれば、たとえばライブラリの差し込みやテスト環境固有の上書きを、より見通しよく扱えるようになる可能性を感じました。Ruby Boxはまだ実験的な機能ではありますが、今後も引き続き注目していきたいです!
ZOZOブースの紹介
こんにちは。Developer Engagementブロックのwirohaです。ここからはZOZOや各社の協賛ブースの様子を紹介します。
今回は出発当日の朝、管制トラブルの影響で飛行機の運航に乱れが出ている状況からのスタートとなりました。搭乗予定の便も遅延か欠航かの見通しが立たなかったため、早々にキャンセルして新幹線へ切り替えたことが功を奏し、4〜5時間かかったものの比較的早めの時間に函館入りできました。また直前の地震の影響で北海道では配送の遅延が起きていたようですが、幸いブースの荷物は無事に届いており、予定どおり運営できそうでほっとしました。
今回のブースはファッション×AIの2つの新しい体験をご用意しました。ひとつは「Apps in ChatGPT」、もうひとつはWEARアプリ内の「着回し提案」機能です。

「Apps in ChatGPT」の体験では、技術カンファレンスに合うコーディネートを相談する方や、自分の写真を撮って「もっとおしゃれにするにはどうしたらいい?」と質問する方など、それぞれ自由な対話を楽しんでいる様子が見られました。

「着回し提案」は特定のアイテムを選択すると、ファッションに特化したAIがユーザーの好みに合わせた着回しを提案します。「知らなかった、便利そう!」という感想を多くいただきました。

いずれかの体験をしていただいた方に、ZOZOらしいノベルティの「洗濯ネット」をプレゼントしました。「ちょうど今日洗濯しようと思ってました」「洗濯ネットが欲しくて体験しに来ました」といった声があり、日常的に使えるアイテムとして多くの方に喜んでいただけました!

協賛企業ブースのコーディネートまとめ
assu_ です。函館へはすんなり辿り着く──はずでした。伊丹空港で突如届く欠航の知らせ、満席の函館便、迫られる決断。滑り込んだ新千歳行き、やけくそで高い寿司を買い、特急北斗で函館へ。家を出てから実に13時間、極寒の函館に到着──虚無の心を救ったのは、函館駅前のハセガワストア。波乱の移動を経て辿り着いた会場には、同じように大変な思いをした人も少なくありませんでした。
そんな会場から、前回のRubyKaigi 2025同様、素敵なコーディネートの協賛企業の皆様を撮影させていただきました!












北海道・函館らしいデザインや、変化する気温に対応しやすそうな形が目立った回でしたね。お忙しい中ご協力いただいたブースの皆さん、本当にありがとうございました!
RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜を開催します

5月13日(水)にRubyKaigi 2026スポンサー企業の株式会社ZOZO、株式会社リブセンス、株式会社TOKIUM、株式会社マイベストでRubyKaigi 2026のアフターイベント「RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜」を開催します。
RubyKaigi 2026に参加した方も、参加できなかった方も、ぜひお気軽にご参加ください!
おわりに

ZOZOは毎年RubyKaigiに協賛し、ブースを出展しており、今年も多くの方々との交流を通じて有意義な時間を過ごすことができました。実行委員会の皆様、そして温かく迎えてくださった函館市の皆様に感謝申し上げます。来年も再び素晴らしい時間を共有できることを楽しみにしております!

RubyKaigi 2027 #rubykaigi
— RubyKaigi (@rubykaigi) 2026年4月24日
🗓️ 14..16 Apr 2027
🥭 Miyazaki, Japan
そして、次回の開催地は私たちZOZOの宮崎オフィスがある宮崎県です。宮崎在住エンジニアも数名在籍しているので、地元を生かしたイベントなどができると良いですね。それではまた来年、RubyKaigi 2027でお会いしましょう。現場からは以上です!
ZOZOでは、来年のRubyKaigi 2027を一緒に盛り上げるエンジニアを募集しています。ご興味のある方は以下のリンクからご応募ください。