トレーニング:PoC 構築ワークショップ — ゼロから72時間で動くものを作る
72時間の挑戦
「ソウタ、来週の月曜から水曜まで、3日間を空けておけ」
金曜の夕方、退勤しようとしていたソウタに、カイが声をかけた。手には一枚の資料が握られている。
「中堅 SaaS 企業の TaskFlow から PoC 依頼が来た。プロジェクト管理ツールに AI 機能を統合したいらしい。月曜のキックオフから水曜のデモまで、72時間で動くものを見せる」
ソウタは目を見開いた。「72時間って……3日ですよね? Rails アプリの立ち上げから AI 統合、デモ環境の構築まで?」
「そうだ。FDE にとって PoC ワークショップは最も価値のある72時間だ。顧客が『これは使える』と確信する瞬間を作り出す。今回はお前がリードだ。俺はメンターとして横に付く」
カイは資料をソウタに渡した。「まずは Day 0——準備から始めるぞ」
Day 0:準備(2時間)
顧客ブリーフィング資料の読み込み
カイがソウタに渡した資料には、TaskFlow の概要がまとめられていた。
TaskFlow 社概要
- 従業員数:120名(エンジニア40名)
- プロダクト:プロジェクト管理 SaaS(MAU 5万)
- 技術スタック:Rails 7 / PostgreSQL / Redis / Heroku
- 課題:タスクの手動分類に時間がかかる、進捗リスクの早期検知ができない
- 希望:Arclight AI の API を使って AI 機能を追加したい
「まず顧客のペインポイントを正確に理解する。技術的にできることではなく、顧客が困っていることから入れ」
ソウタは3つの核心課題を整理した。
- タスク分類の自動化 — 毎日100件以上のタスクを手動でカテゴリ分けしている
- 優先度の自動提案 — 締切・依存関係・チームの負荷から優先度を判断したい
- 進捗リスクの早期検知 — 遅延しそうなプロジェクトを事前に警告してほしい
環境構築チェックリスト
「準備段階で環境を完璧にしておけ。Day 1 で環境構築に時間を食うのは最悪のパターンだ」
- ✅ Ruby 3.3 + Rails 7.1 のインストール確認
- ✅ PostgreSQL 16 / Redis 7 のローカルインスタンス起動
- ✅ Arclight AI API キーの発行と
curlでの動作確認 - ✅ AWS CLI + Terraform のセットアップ
- ✅ Docker Desktop の起動確認
- ✅ GitHub リポジトリ
taskflow-ai-pocの作成
WARNING
API キーの動作確認は必ず Day 0 で済ませること。当日になって「API キーが無効でした」は致命的。curl で簡単なリクエストを送り、レスポンスが返ることを確認する。
SMART 成功基準の設計
| 基準 | 内容 |
|---|---|
| Specific | AI 分類・優先度提案・リスク検知の3機能が動作する |
| Measurable | タスク分類の精度 80% 以上、API レスポンス 2秒以内 |
| Achievable | 既存の Arclight AI API の機能範囲内で実現可能 |
| Relevant | TaskFlow の3つの核心課題を直接解決する |
| Time-bound | 72時間以内にデモ可能な状態にする |
INFO
成功基準は顧客と事前に合意しておく。PoC 終了後に「思っていたのと違う」と言われるのを防ぐ。Day 1 のキックオフ冒頭で確認する。
Day 1:スコーピング & プロトタイプ(8時間)
Hour 1-2:テクニカルディスカバリー
月曜日の朝9時。TaskFlow のオフィスで、CTO の田中さんとエンジニアリードの佐藤さんと向き合った。
「最初の2時間は質問の時間です。こちらから提案する前に、御社の現状を正確に理解させてください」
データ構造の質問:
- タスクのカテゴリは何種類か?(→ 8カテゴリ)
- 過去の分類データは何件あるか?(→ 約10万件、2年分)
- タスクの平均テキスト長は?(→ タイトル20文字 + 説明100文字)
インフラ・セキュリティの質問:
- 外部 API 呼び出しの制約は?(→ ユーザー体感3秒以内)
- データの外部送信ポリシーは?(→ タスクタイトルと説明文のみ可、個人情報は除外)
Hour 3-4:アーキテクチャ設計
ディスカバリーの結果を踏まえ、ソウタはホワイトボードにアーキテクチャを描いた。
「Rails アプリから Service クラス経由で Arclight API を呼び、結果を PostgreSQL に保存。頻繁なリクエストは Redis でキャッシュ。フロントは Turbo Streams でリアルタイム反映します」
田中 CTO が頷いた。「うちの技術スタックと親和性が高いな」
WARNING
PoC のアーキテクチャで最も大事なのは既存技術スタックとの親和性。顧客のエンジニアが「これなら自分たちでも保守できる」と思えることが本番採用の鍵。
Hour 5-8:Rails スケルトン構築
午後1時。ソウタはコードを書き始めた。まずはマイグレーションとモデル。
# db/migrate/002_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[7.1]
def change
create_table :tasks do |t|
t.references :project, null: false, foreign_key: true
t.string :title, null: false
t.text :description
t.string :category
t.integer :priority, default: 0
t.string :status, default: "open"
t.string :assigned_to
t.date :due_date
t.timestamps
end
add_index :tasks, %i[project_id status]
end
end
# db/migrate/003_create_ai_suggestions.rb
class CreateAiSuggestions < ActiveRecord::Migration[7.1]
def change
create_table :ai_suggestions do |t|
t.references :task, null: false, foreign_key: true
t.string :suggestion_type, null: false
t.jsonb :payload, default: {}
t.float :confidence, default: 0.0
t.string :status, default: "pending"
t.datetime :accepted_at
t.timestamps
end
end
end# app/models/task.rb
class Task < ApplicationRecord
belongs_to :project
has_many :ai_suggestions, dependent: :destroy
validates :title, presence: true
enum :priority, { low: 0, medium: 1, high: 2, critical: 3 }
scope :open_tasks, -> { where(status: "open") }
scope :overdue, -> { where(status: "open").where("due_date < ?", Date.current) }
end
# app/models/ai_suggestion.rb
class AiSuggestion < ApplicationRecord
belongs_to :task
validates :suggestion_type, inclusion: { in: %w[classification priority risk_alert] }
scope :pending, -> { where(status: "pending") }
def accept!
update!(status: "accepted", accepted_at: Time.current)
end
end次に Arclight AI との接続を Service Object パターンで実装した。
# app/services/arclight_client.rb
class ArclightClient
BASE_URL = ENV.fetch("ARCLIGHT_API_URL", "https://api.arclight.ai/v1")
class ApiError < StandardError; end
class RateLimitError < ApiError; end
def initialize
@api_key = ENV.fetch("ARCLIGHT_API_KEY")
@conn = Faraday.new(url: BASE_URL) do |f|
f.request :json
f.response :json
f.request :retry, max: 2, interval: 0.5
f.options.timeout = 10
end
end
def classify(text, categories:)
request(:post, "/classify", { text: text, categories: categories, language: "ja" })
end
def suggest_priority(task_data)
request(:post, "/analyze/priority", task_data)
end
def detect_risk(project_data)
request(:post, "/analyze/risk", project_data)
end
private
def request(method, path, body)
response = @conn.send(method, path) do |req|
req.headers["Authorization"] = "Bearer #{@api_key}"
req.body = body
end
case response.status
when 200..299 then response.body
when 429 then raise RateLimitError, "レートリミット超過"
else raise ApiError, "API エラー: #{response.status}"
end
rescue Faraday::TimeoutError
raise ApiError, "Arclight API タイムアウト"
end
endAI 機能をまとめるサービスクラスも作成した。
# app/services/task_classifier.rb
class TaskClassifier
CATEGORIES = %w[バグ修正 機能追加 改善 インフラ セキュリティ パフォーマンス リファクタリング ドキュメント].freeze
def initialize(client: ArclightClient.new)
@client = client
end
def classify(task)
cache_key = "classify:#{task.id}:#{task.updated_at.to_i}"
result = Rails.cache.fetch(cache_key, expires_in: 1.hour) do
@client.classify("#{task.title} #{task.description}", categories: CATEGORIES)
end
task.ai_suggestions.create!(
suggestion_type: "classification",
payload: { category: result["category"], alternatives: result["alternatives"] },
confidence: result["confidence"]
)
rescue ArclightClient::ApiError => e
Rails.logger.error("[TaskClassifier] #{e.message}")
nil
end
endDay 1 完了チェックリスト:
- ✅ テクニカルディスカバリー完了
- ✅ アーキテクチャ設計と顧客合意
- ✅ 3つのモデル(Project, Task, AiSuggestion)作成
- ✅ Arclight AI API クライアント実装
- ✅ タスク分類・優先度提案サービス実装
- ✅ シードデータで基本動作確認
Day 2:機能実装 & テスト(8時間)
Hour 1-3:コア機能の実装
火曜日の朝。自然言語タスク作成とリスク検知に取りかかった。
# app/services/natural_language_task_creator.rb
class NaturalLanguageTaskCreator
def initialize(client: ArclightClient.new)
@client = client
end
def create(project:, natural_text:)
parsed = @client.classify(natural_text, categories: ["task_parsing"])
task = project.tasks.create!(
title: parsed.dig("parsed", "title") || natural_text.truncate(100),
description: parsed.dig("parsed", "description"),
due_date: safe_parse_date(parsed.dig("parsed", "due_date"))
)
AiAnalysisJob.perform_later(task.id)
task
end
private
def safe_parse_date(str)
str.present? ? Date.parse(str) : nil
rescue Date::Error
nil
end
end# app/jobs/ai_analysis_job.rb
class AiAnalysisJob < ApplicationJob
queue_as :ai_analysis
retry_on ArclightClient::RateLimitError, wait: 30.seconds, attempts: 3
def perform(task_id)
task = Task.find(task_id)
TaskClassifier.new.classify(task)
Turbo::StreamsChannel.broadcast_replace_to(
"project_#{task.project_id}_tasks",
target: "task_#{task.id}",
partial: "tasks/task",
locals: { task: task.reload }
)
end
endHour 4-6:フロントエンド統合
「Turbo Streams でリアルタイム感を出す。SPA を作らなくても十分だ」
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
before_action :set_project
def create
@task = if params[:natural_text].present?
NaturalLanguageTaskCreator.new.create(
project: @project, natural_text: params[:natural_text]
)
else
@project.tasks.create!(task_params).tap do |t|
AiAnalysisJob.perform_later(t.id)
end
end
respond_to do |format|
format.turbo_stream
format.html { redirect_to project_tasks_path(@project) }
end
end
def accept_suggestion
task = @project.tasks.find(params[:id])
suggestion = task.ai_suggestions.find(params[:suggestion_id])
suggestion.accept!
case suggestion.suggestion_type
when "classification"
task.update!(category: suggestion.payload["category"])
when "priority"
task.update!(priority: suggestion.payload["suggested_priority"])
end
respond_to do |format|
format.turbo_stream
format.html { redirect_to project_task_path(@project, task) }
end
end
private
def set_project = @project = Project.find(params[:project_id])
def task_params = params.require(:task).permit(:title, :description, :due_date)
endINFO
Turbo Streams + Stimulus の組み合わせは PoC で最強の武器。SPA を作る必要がなく、Rails の既存テンプレートエンジンの延長でリアルタイム更新が実現できる。顧客エンジニアにとっても学習コストが低い。
Hour 7-8:テスト & バグ修正
「動くものを作るだけじゃない。壊れないことを証明するテストも PoC に含める」
# spec/services/task_classifier_spec.rb
RSpec.describe TaskClassifier do
let(:client) { instance_double(ArclightClient) }
let(:classifier) { described_class.new(client: client) }
let(:task) { create(:task, title: "ログイン画面が表示されない") }
context "API が正常にレスポンスを返す場合" do
before do
allow(client).to receive(:classify).and_return(
{ "category" => "バグ修正", "confidence" => 0.92, "alternatives" => [] }
)
end
it "AiSuggestion を作成する" do
expect { classifier.classify(task) }.to change { task.ai_suggestions.count }.by(1)
end
it "正しいカテゴリと confidence を保存する" do
suggestion = classifier.classify(task)
expect(suggestion.payload["category"]).to eq("バグ修正")
expect(suggestion.confidence).to eq(0.92)
end
end
context "API がエラーを返す場合" do
before do
allow(client).to receive(:classify)
.and_raise(ArclightClient::ApiError, "Internal Server Error")
end
it "nil を返しクラッシュしない" do
expect(classifier.classify(task)).to be_nil
end
end
endWARNING
PoC のテストはハッピーパス + エラーハンドリングの2軸で書く。カバレッジ100%は不要だが、「API が落ちてもアプリはクラッシュしない」ことを証明するテストは必須。
Day 2 完了チェックリスト:
- ✅ 自然言語タスク作成機能
- ✅ プロジェクトリスク検知機能
- ✅ Turbo Streams によるリアルタイム更新
- ✅ RSpec テスト(サービス層)
- ✅ エッジケース処理(API エラー、タイムアウト、空データ)
Day 3:デモ準備 & プレゼン(8時間)
Hour 1-3:AWS デモ環境の構築
水曜日の朝。ソウタは AWS 上にデモ環境を構築した。
「デモはローカルでやるな。本番に近い環境で動かすことで、『すぐに本番投入できる』と思わせる」
INFO
PoC のデモ環境はコスト最小限に。ECS Fargate は 0.25 vCPU / 512MB、RDS は db.t4g.micro、ElastiCache は cache.t4g.micro で十分。Terraform で管理し、デモ後にすぐ destroy できるようにしておく。
WARNING
デモ環境構築で最もハマりやすいのはセキュリティグループの設定。ECS → RDS、ECS → ElastiCache、ALB → ECS の3つのインバウンドルールを忘れると接続できない。チェックリストにしておく。
Hour 4-5:デモスクリプトの作成
「デモはストーリーだ。機能を羅列するんじゃない。顧客の課題が解決される物語を見せる」
デモスクリプト(15分構成)
Act 1:課題の再確認(2分) 「御社では毎日100件以上のタスクを手動で分類されていると伺いました」 → 顧客に自分の言葉で課題を語ってもらう
Act 2:AI 分類のデモ(4分) タスクを1件作成 → 数秒後に AI が自動分類(Turbo Streams でリアルタイム更新)→ 承認/却下を実演。「手動30秒 × 100件 = 50分が、AI なら5分に短縮」
Act 3:自然言語タスク作成(3分) 「ログイン画面のレスポンスが遅いので来週金曜までに改善してほしい」と入力 → AI がタイトル・期限・カテゴリを自動設定。「Slack の依頼をそのまま貼るだけ」
Act 4:リスク検知(3分) プロジェクトダッシュボード → AI が遅延リスクをハイライト表示。「炎上する前に手を打てます」
Act 5:次のステップ(3分) 本番導入ロードマップ + 質疑応答
Hour 6-7:カイとのリハーサル
「よし、通しでやってみろ。俺が田中 CTO 役をやる」
5分後、カイが止めた。「3つ問題がある」
- 画面遷移が多すぎる — デモは1画面で完結させろ
- 沈黙の時間がある — AI の処理待ち中も「今 AI が分析しています」と実況しろ
- 数字がない — 「速くなります」ではなく具体的な時間短縮を言え
ソウタはスクリプトを修正し、もう一度通した。
「今度はいい。だが最後に——障害シナリオを用意しておけ。デモ中に API が落ちたらどうなるか見せられるようにしろ」
WARNING
デモ中の頻出失敗は「Wi-Fi 切断」「API タイムアウト」「データ不整合」の3つ。全シナリオでグレースフルデグラデーションすることを確認しておく。AI が落ちてもタスク自体は作成できる——その姿を見せられるのは逆に信頼を生む。
Hour 8:振り返りと次のステップ
デモは成功した。田中 CTO は身を乗り出して質問を繰り返し、佐藤エンジニアリードは「この Service Object のパターン、うちのコードベースにもそのまま入れられそう」と笑顔で言った。
KPT 振り返り:
| 区分 | 内容 |
|---|---|
| Keep | Service Object パターンで AI 機能を分離し、顧客エンジニアも理解しやすかった |
| Keep | Turbo Streams のリアルタイム更新がデモのインパクトを高めた |
| Problem | Day 2 のフロントエンドに予想以上の時間がかかった |
| Problem | AWS セキュリティグループ設定で30分ロスした |
| Try | 次回は Terraform モジュールを事前にテンプレート化しておく |
本番移行クオリティチェックリスト
「PoC で終わらせない。本番移行を見据えたチェックリストを顧客に渡せ。これが FDE の信頼を決定づける」
セキュリティ(5項目)
- ✅ API キーは環境変数管理、コードにハードコードなし
- ✅ AI に送信するデータから個人情報(PII)を除外
- ✅ HTTPS のみで通信
- ✅ API キーのローテーション手順を文書化
- ✅ エラーメッセージに内部情報が漏洩しない
パフォーマンス(5項目)
- ✅ AI API 呼び出しはバックグラウンドジョブで非同期処理
- ✅ 頻繁なリクエストは Redis でキャッシュ
- ✅ DB クエリに N+1 問題がない
- ✅ API タイムアウトを適切に設定(10秒以内)
- ✅ 負荷テスト(100 concurrent requests)で 3秒以内
信頼性(5項目)
- ✅ API エラー時にアプリがクラッシュしない
- ✅ リトライロジック実装(指数バックオフ)
- ✅ サーキットブレーカー実装
- ✅ ヘルスチェックエンドポイント設置
- ✅ エラー監視(Sentry / Datadog)設定
運用(5項目)
- ✅ ログに十分なコンテキスト(request_id, user_id)を含む
- ✅ AI 提案の承認/却下率をメトリクスとして記録
- ✅ API 利用量のアラート設定
- ✅ ロールバック手順を文書化
- ✅ オンコール対応のランブック作成
INFO
このチェックリストを PoC 完了時に顧客に渡すと2つの効果がある。①「本番のことまで考えてくれている」という信頼。②本番導入プロジェクトの受注につながるスコープの明確化。
タイムラインで遅れたとき——ショートカット戦略
「完璧な PoC は存在しない。時間が足りないとき、何を切り何を守るかの判断が FDE の腕の見せ所だ」
Day 1 で遅れた場合:
- AiSuggestion を独立テーブルではなく Task の jsonb カラムに統合
- API クライアントのリトライ処理を省略し、最小限のエラーハンドリングのみ
- シードデータは CSV インポートスクリプトで一括投入
Day 2 で遅れた場合:
- Turbo Streams を諦め、通常のフォーム送信 + ページリロードに切り替え
- テストを Service 層のみに絞り、コントローラーテストは省略
- リスク検知を外し、分類と優先度提案の2機能に集中
Day 3 で遅れた場合:
- AWS デプロイを諦め、ngrok 経由のローカルデモに切り替え
- デモスクリプトを15分から10分に短縮
- リハーサルを1回に減らす(ただしゼロにはしない)
WARNING
何を切ってもいいが、デモのリハーサルだけは絶対にゼロにするな。練習なしのデモは事故る。最低1回は通しで練習する。
72時間の全体タイムライン
| フェーズ | 時間 | 内容 | 成果物 |
|---|---|---|---|
| Day 0 | 2h | 準備・環境構築 | チェックリスト、SMART基準 |
| Day 1 H1-2 | 2h | テクニカルディスカバリー | 要件リスト、質問回答記録 |
| Day 1 H3-4 | 2h | アーキテクチャ設計 | 構成図、顧客合意 |
| Day 1 H5-8 | 4h | Rails スケルトン | モデル、API クライアント、サービス |
| Day 2 H1-3 | 3h | コア機能実装 | NL タスク作成、リスク検知 |
| Day 2 H4-6 | 3h | フロントエンド統合 | Turbo Streams、Stimulus |
| Day 2 H7-8 | 2h | テスト & バグ修正 | RSpec テスト |
| Day 3 H1-3 | 3h | AWS デモ環境 | ECS、RDS、ElastiCache |
| Day 3 H4-5 | 2h | デモスクリプト | 15分構成の台本 |
| Day 3 H6-7 | 2h | リハーサル | フィードバック反映 |
| Day 3 H8 | 1h | 振り返り | KPT、チェックリスト |
この章の学び
「72時間で動くものを作る——これが FDE の PoC ワークショップだ」
カイはオフィスの窓から夕焼けを見ながら言った。
「技術力だけでは足りない。時間管理、顧客コミュニケーション、デモのストーリーテリング。全部がひとつになって初めて、顧客の心を動かす PoC になる」
ソウタは今回のワークショップで学んだことを整理した。
- 準備が9割 — Day 0 の2時間が72時間の成否を決める
- Service Object パターン — AI 機能を分離し、テスト容易性と理解しやすさを両立
- Turbo Streams の威力 — SPA なしでリアルタイム感のあるデモが作れる
- グレースフルデグラデーション — AI が落ちてもアプリは動く設計が信頼を生む
- デモはストーリー — 機能の羅列ではなく、課題が解決される物語を見せる
- 完璧より完了 — 何を切るかの判断こそが FDE の価値
「次の PoC は、お前一人でやってもらうぞ」
カイの言葉に、ソウタは静かに頷いた。72時間で動くものを作る力——それは FDE としての自信そのものだった。