Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Claude Codeのskillから、Railsアプリへ

Claude Codeのskillから、Railsアプリへ

RailsTokyo #5(2026-07-16)の登壇資料です。Active Agent(gem)を使って、採用業務の Claude Code skill を社内向け Slack bot「Heron」に育てた話。

Avatar for Yuki Minamiya

Yuki Minamiya

July 16, 2026

More Decks by Yuki Minamiya

Other Decks in Programming

Transcript

  1. RAILSTOKYO #5 — 2026.07.16 Claude Codeのskillから、 Railsアプリへ Active Agentを使ってSlack botを作ったら

    思ったより頼りになる相棒ができた 南谷 祐貴 RailsTokyo Organizer @yuki3738 @yuki3738 — RailsTokyo #5 1 / 31
  2. WHO AM I 南谷 祐貴 @yuki3738 — 株式会社mov で Engineering

    Manager。開発と並行してエンジニア採用も担当 — Claude のお陰で PR を沢山作れるようになった(merge できてるとは言ってない) — Claude Code の skills / plugin を整備して、組織に組み込み中 — Omotesando.rb 所属 @yuki3738 — RailsTokyo #5 2 / 31
  3. きっかけ この繰り返しを、skill にした skill = 手順や判断基準を書いた Markdown。Claude Code が読み込んで、その通りに動く(ご存知の方が多 いと思いますが、念のため)

    — skill がやるのは、候補者のプロフィールと自社の求人票を突き合わせて、判断しやすいよう に情報を整えてもらうところまで — 決めるのは自分 — 使いながら気づいたことをフィードバックして、育てていける — かなりの業務効率化ができた — いい判断のためには求人票が超重要。ここで刷新が活きた @yuki3738 — RailsTokyo #5 8 / 31
  4. 転機 毎朝 Claude Code を開いて、同じ手順で skill を回す日々が続く── 「Slack に通知が来た瞬間に動いていて、 人間が見に行ったときには結果がまとまっていてほしい」

    skill は自分が起動したときにしか動かない。 通知は拾えない。外部システムへの書き込みも自動では回らない。 @yuki3738 — RailsTokyo #5 9 / 31
  5. 昇華 Rails アプリにした Slack 通知 → Rails controller → Solid

    Queue job → Active Agent → Claude API — Rails 8.1 の社内向け Slack bot。実際に稼働中 — エージェント層は Active Agent(gem)、LLM は Anthropic Claude API — 通知が来た瞬間に動く。見に行くと結果がまとまっている — Rails じゃなくても実現可能だけど、Active Agent を学びたかった @yuki3738 — RailsTokyo #5 10 / 31
  6. 本編 相棒のつくり方 Active Agent の使い方 01 Agent クラスと action の生やし方

    業務のまとまりごとに action を定義する感覚 02 プロンプトは view template 業務知識が Rails の view 資産になる 頼れるやつにするための設計 03 会話の記憶を Slack スレッドに置く セッション管理なしのマルチターン 04 全部を LLM に投げない 実行判断は正規表現、LLM は読む・書くに専念 @yuki3738 — RailsTokyo #5 12 / 31
  7. 01 / Agent クラスと action 業務のまとまりごとに action を定義する class HeronAgent

    < ApplicationAgent def screen(candidate_profile:, job_postings:, notification_text:, ...) @candidate_profile = candidate_profile @job_postings = job_postings # インスタンス変数に詰めて @notification_text = notification_text prompt # view end を render して LLM へ スレッドで相談 def reply_draft(...) # 文章の下書き def discuss(...) # end インスタンス変数に詰めて prompt ── controller が view を render するのと同じ形 @yuki3738 — RailsTokyo #5 14 / 31
  8. 01 / Agent クラスと action 呼び出す側(Solid Queue の job から)

    response = HeronAgent.screen( candidate_profile: profile.data, job_postings: postings, notification_text: parent_text ).generate_now # ここで LLM 呼び出し reply(channel:, thread_ts:, text: response.message.content) — action の呼び出しは組み立てまで。 generate_now で初めて LLM が呼ばれる。戻り値から本文を取り出 して Slack へ — Action Mailer の deliver_now を知っていれば、そのままの感覚 @yuki3738 — RailsTokyo #5 15 / 31
  9. 01 / Agent クラスと action プロバイダ設定は YAML 1枚 # config/active_agent.yml

    development: anthropic: service: "Anthropic" api_key: <%= Rails.application.credentials.dig(:anthropic, :api_key) %> model: "claude-opus-4-8" — database.yml と同じノリで環境ごとに書ける — ここまでで「LLM を呼べる Rails アプリ」はもう動く @yuki3738 — RailsTokyo #5 16 / 31
  10. 02 / プロンプトは view template プロンプトの置き場所は app/views app/views/agents/heron/ ├── instructions.md

    ├── screen.md.erb ├── discuss.md.erb 業務の判断基準 # action ごとの user プロンプト # system prompt = ├── reply_draft.md.erb └── ... view と同じ規約で、action 名のテンプレートが render される。 つまりプロンプトもリポジトリの中のただのファイル ── 変更は diff で見えて、ふつうの PR レビューに乗る @yuki3738 — RailsTokyo #5 18 / 31
  11. 02 / プロンプトは view template ERB でコンテキストを埋め込む (抜粋) ## Slack

    通知本文 # screen.md.erb ``` <%= @notification_text %> ``` ## 候補者プロフィール ```json <%== JSON.pretty_generate(@candidate_profile) %> ``` — インスタンス変数がそのまま見える ── controller → view と同じデータの流れ — ⚠️ ハマりポイント: <%= だと JSON が HTML エスケープされて &quot; になる。 <%== で raw 出力 @yuki3738 — RailsTokyo #5 19 / 31
  12. 02 / プロンプトは view template skill で磨いた判断基準を、そのまま持ち込む SKILL.md(Claude Code /

    人が回す) 判断基準 - 面談メモの第三者評価を必ず反映 - 総合評価は3軸で見る (深さ・条件・コミット志向) - 迷ったら「面談で確認」に倒す ## instructions.md(bot / 自動で回る) 判断基準 - 面談メモの第三者評価を必ず反映 - 総合評価は3軸で見る (深さ・条件・コミット志向) - 迷ったら「面談で確認」に倒す ## skill 時代に業務の中で磨いた判断基準が、view 資産としてアプリに受け継がれる @yuki3738 — RailsTokyo #5 20 / 31
  13. 03 / 会話の記憶を Slack スレッドに置く セッション管理も履歴テーブルも、作らない スレッドの全メッセージ replies.drop(1).map do |msg|

    # 先頭は親メッセージなので除外 # bot の発言なら assistant、人間なら user def build_turns(replies) # role = msg.user == HERON_USER_ID ? "assistant" : "user" { role: role, content: msg.text.to_s } end end turns = build_turns(replies) # initial = view # 戻り値 = role つき会話履歴の配列 を render した最初のプロンプト(プロフィール・求人票入り) messages = [ { role: "user", content: initial } ] + turns 話しかけられるたびに、スレッドを丸ごと読み直して LLM に渡す。それだけ @yuki3738 — RailsTokyo #5 22 / 31
  14. 03 / 会話の記憶を Slack スレッドに置く 会話の記憶は、Slack が持っててくれる @メンション → スレッドを読み直す

    → bot/人間の発言に分ける → LLM へ — 「前回どこまで話したか」を覚えるのは Slack。アプリは何も覚えない — 覚えないから、個人情報も残らない。再起動しても会話は壊れない — 仕組みはさっきの小さなメソッド1個。会話の続きが必要な機能は、いまのところこれで足 りている @yuki3738 — RailsTokyo #5 23 / 31
  15. 04 / 全部を LLM に投げない 優先順位つきの regex ルーティング 文末の決定表現だけ拾う。つぶやきでは発火しない REGISTER_PATTERN

    = /(登録して|登録お願い)\z/ # \z = 末尾アンカー DRAFT_PATTERN = /下書き(つくって|作って|書いて)/ # 動詞つきのみ # case mention_text when REGISTER_PATTERN then run_register when DRAFT_PATTERN else # 書き込み系ほど上に # どれでもなければ相談 then run_draft run_discuss end ※ コマンド語は実際の業務のものから改変しています @yuki3738 — RailsTokyo #5 26 / 31
  16. 04 / 全部を LLM に投げない 振り分けはただの Ruby。だから RSpec で固定できる 「登録して」なら実行される"

    do expect(route("@bot 登録して")).to eq(:register) it " end 「登録しようかなぁ」では実行されない" do expect(route("@bot 登録しようかなぁ")).to eq(:discuss) it " end 「この文言なら実行される/されない」の境界そのものをテストに固定。LLM 呼び出しを含 まないので、速くて確実 @yuki3738 — RailsTokyo #5 27 / 31
  17. 04 / 全部を LLM に投げない もうひとつの守り ─ 重複配送を防ぐ # Slack

    は同じ event を最大3回リトライしてくる def self.claim!(slack_channel_id:, slack_message_ts:, event_type:) create!(...) # (channel, ts) true rescue ActiveRecord::RecordNotUnique false end # 2 に unique index 回目以降は静かに skip ここも LLM ではなくDB の unique 制約が守る @yuki3738 — RailsTokyo #5 28 / 31
  18. 実際の見え方 10:02 📮 採用媒体の通知 山田太郎さんから応募が届きました! Heron 10:02 受信したよ。山田太郎さんをスクリーニング中… Heron 10:03

    スクリーニング結果: 山田太郎 候補者サマリー Rails 8年。直近はテックリードとして… Senior Backend Engineer ✅ 面談推奨: Yes 強み … 懸念点 … ⚠️ 確認事項 … 祐貴 10:10 🙎 南谷 @Heron カジュ面する Heron 10:10 🤝 面談枠を作りました。候補者向けメッセージ(コピーして送付してください): … ※ 候補者・IDは架空のものです @yuki3738 — RailsTokyo #5 29 / 31
  19. おわりに この会場のみなさんは、 既に Rails という慣れた道具を持っています そして Active Agent という gem

    が、そこに「エージェント層」という可能性に満ちた抽 象化を持ち込んでくれました。使わない手はない。 聞き終わったあと「自分もやってみるか」と思ってもらえたら嬉しいです。 @yuki3738 — RailsTokyo #5 31 / 31