mybook

PoC の設計と実行 — 「動く」と「使える」の間にある深い溝

「デモは完璧でした。でも、うちの業務では使えません」

ヤマモト物流の情報システム部長・渡辺さんが、穏やかだが容赦のない一言を放った。会議室の空気が凍る。スクリーンにはソウタが三日間徹夜して作った Arclight AI の配送最適化デモが映っている。サンプルデータで動く美しいダッシュボード。リアルタイムで最適ルートを計算するアニメーション。技術的には申し分ない。

しかし、渡辺さんの目は冷静だった。

「うちの配送ドライバーは一日に平均 87 件の配送をこなします。ルート最適化の前に、まず荷物の積載順序が重要なんです。トラックの荷室は奥から詰めるので、配送順序が逆じゃないと積めない。この制約、デモには入っていますか?」

入っていなかった。

ソウタは隣に座るカイの方をちらりと見た。カイは腕を組んだまま、小さくうなずいた。「想定通りだ」とでも言うように。

会議室を出た後、カイはソウタの肩を叩いた。

「おつかれさん。いい失敗だった」

「いい失敗……ですか」

「デモと PoC は違う。デモは『こんなことができます』を見せるもの。PoC は『あなたの業務でこう使えます』を証明するものだ。今日のは完璧なデモだったけど、PoC ではなかった」

カイはホワイトボードに向かいながら続けた。

「これから本当の PoC のやり方を教える。ヤマモト物流さんには来週、改めて PoC の提案をしよう」

PoC と PoV — 似て非なる二つの検証

PoC(Proof of Concept)と PoV(Proof of Value)。この二つの区別がつかないまま走り出すと、ソウタのように「技術的には動くけど採用されない」という結果に終わる。

Loading diagram...

PoC — 「できるか?」を問う

PoC は技術的な実現可能性を検証するフェーズだ。「Arclight AI のルート最適化エンジンは、ヤマモト物流の配送データを処理できるか?」「レスポンスは実用的な速度か?」「既存システムと連携可能か?」——こうした問いに答える。

PoV — 「使う価値があるか?」を問う

PoV は一歩先の話だ。技術的に動くことがわかった上で、「導入したら業務がどう改善されるか」を定量的に示す。

INFO

PoC は「動くことの証明」、PoV は「価値があることの証明」だ。FDE が最初に取り組むのは PoC だが、ゴールは常に PoV を見据えている。技術的に動いても業務価値がなければ、顧客は買わない。

ヤマモト物流の渡辺さんが本当に知りたかったのは PoV の領域だった。導入で配送コストは何%下がるのか。ドライバーの残業時間はどれだけ減るのか。毎朝の配車計画に費やす2時間をどこまで短縮できるのか。

ソウタのデモは PoC すら満たしていなかった。積載順序という基本的な業務制約を取り込んでいなかったからだ。

PoC の 4 フェーズ

カイはホワイトボードに PoC の全体像を描いた。

Loading diagram...

「PoC は4つのフェーズで進める。それぞれに明確なゴールがあり、フェーズをスキップすると後で必ず痛い目にあう」

Phase 1: 計画策定 — すべてはここで決まる

「PoC の成否の 80% は計画で決まる」とカイは断言した。

最初にやるべきは「何を証明するか」を一文で表現すること。「AI の性能を見せる」ではダメだ。「87 件/日の配送ルートを積載制約付きで 10 秒以内に最適化できることを証明する」——このレベルの具体性が必要だ。

Phase 2: 環境構築 — 本番に近く、しかし軽く

PoC 環境は「本番と同じ」である必要はないが、「本番で再現不可能」では意味がない。この微妙なバランスが FDE の腕の見せ所だ。

Phase 3: 実行・検証 — データで語る

計画した検証項目を一つずつ潰していく。感覚ではなく数値で判断する。

Phase 4: 評価・判定 — Go / No-Go の決断

全ての検証結果を集約し、成功基準と照合して最終判定を下す。「なんとなくうまくいった」は許されない。

SMART 成功基準 — 曖昧さを排除する

「成功基準が曖昧な PoC は、成功も失敗もしない。ゾンビになるだけだ」

カイはそう言いながら、SMART 基準のフレームワークを説明した。

  • Specific(具体的): 何を検証するか明確
  • Measurable(測定可能): 数値で判定できる
  • Achievable(達成可能): PoC 期間内に検証できる
  • Relevant(関連性): 顧客の業務課題に直結
  • Time-bound(期限付き): 検証期限が明確

WARNING

「AIの精度を検証する」は SMART ではない。「東京エリアの過去 3 ヶ月分の配送データ(約 7,800 件)に対して、積載制約付きルート最適化の走行距離削減率が 15% 以上であることを、2 週間以内に検証する」が SMART だ。

ソウタはヤマモト物流の PoC 用に、5 つの成功基準を設計した。

# SMART 成功基準の定義
class PocSuccessCriteria
  CRITERIA = [
    {
      id: "SC-001", category: :performance,
      description: "87件の配送ルート最適化を10秒以内に完了",
      target: "P95 ≤ 10,000ms", priority: :must_have
    },
    {
      id: "SC-002", category: :accuracy,
      description: "LIFO積載制約の充足率 100%",
      target: "制約違反 0%", priority: :must_have
    },
    {
      id: "SC-003", category: :efficiency,
      description: "手動配車比で走行距離 15% 以上削減",
      target: "削減率 ≥ 15%", priority: :must_have
    },
    {
      id: "SC-004", category: :integration,
      description: "SAP TM から配送オーダーを自動取得",
      target: "同期成功率 ≥ 99.0%", priority: :should_have
    },
    {
      id: "SC-005", category: :usability,
      description: "オペレーターが30分以内に操作習得",
      target: "初回正答率 ≥ 80%", priority: :nice_to_have
    }
  ].freeze
 
  def evaluate(results)
    must_haves = CRITERIA.select { |c| c[:priority] == :must_have }
    all_must_pass = must_haves.all? { |c| results[c[:id]][:passed] }
 
    { verdict: all_must_pass ? :go : :no_go,
      must_have_pass_rate: calc_rate(must_haves, results) }
  end
 
  private
 
  def calc_rate(criteria, results)
    passed = criteria.count { |c| results[c[:id]][:passed] }
    (passed.to_f / criteria.size * 100).round(1)
  end
end

INFO

成功基準は「ワークフロー単位」で設計する。「ルート最適化 API のレスポンスが速い」ではなく、「朝 7 時の配車会議までに 87 件のルートが計算完了している」のように、顧客の業務フローに即した基準にする。機能ではなくワークフローに焦点を当てることで、技術と業務のギャップを埋められる。

PoC タイムライン — スピードが命

PoC は長引けば長引くほど死ぬ。カイは 3 つのカテゴリに分けたタイムラインの目安を教えてくれた。

カテゴリ期間特徴
Simple SaaS7 日標準機能の検証。カスタマイズ不要API にデータを投げて結果確認
Standard Enterprise2-4 週既存システム連携。データ変換ありSAP TM 連携 + カスタム制約
Complex Integration4-6 週複数システム、セキュリティ要件、大規模データオンプレ DB + クラウド AI + 基幹連携

ヤマモト物流の案件は Standard Enterprise に分類された。SAP TM との連携と積載制約のカスタマイズが必要だが、データ量は中規模で、セキュリティ要件も標準的だった。ソウタは 3 週間の PoC スケジュールを組んだ。

WARNING

PoC のタイムラインを顧客と合意する際、「延長の条件」も事前に定義しておくこと。「SC-001 と SC-002 がパスし、SC-003 のみ未達の場合、追加 1 週間の延長を認める」のように。延長条件がないと、ずるずると「ゾンビ PoC」になる。

マルチテナント PoC 環境の構築

カイが最も力を入れて教えてくれたのが、PoC 環境の設計だった。

「FDE は同時に複数の顧客の PoC を回すことがある。そのとき、顧客ごとにまったく別の環境を立てるとコストが爆発する。かといって同じ環境を共有するとデータが混ざる。マルチテナントの PoC 環境が必要だ」

Loading diagram...

テナントモデルとスコープ

# テナント管理モデル
class PocTenant < ApplicationRecord
  validates :slug, presence: true, uniqueness: true,
                   format: { with: /\A[a-z0-9\-]+\z/ }
  validates :company_name, presence: true
  validates :poc_start_date, presence: true
  validates :poc_end_date, presence: true
 
  has_many :delivery_orders, dependent: :destroy
  has_many :route_optimizations, dependent: :destroy
 
  def active?
    today = Date.current
    poc_start_date <= today && today <= poc_end_date
  end
 
  # テナント固有の最適化設定
  def optimization_config
    config = { constraints: %i[time_window vehicle_capacity] }
    config[:constraints] << :lifo_loading if slug == "yamamoto-logistics"
    config
  end
end

テナント分離ミドルウェア

# リクエストごとにテナントを識別・設定するミドルウェア
class TenantMiddleware
  def initialize(app)
    @app = app
  end
 
  def call(env)
    request = ActionDispatch::Request.new(env)
    tenant = resolve_tenant(request)
 
    return tenant_not_found if tenant.nil?
    return poc_expired unless tenant.active?
 
    Current.tenant = tenant
    @app.call(env)
  ensure
    Current.tenant = nil
  end
 
  private
 
  def resolve_tenant(request)
    slug = request.headers["X-Tenant-ID"]
    slug ||= request.host.split(".").first if request.host.split(".").length > 2
    PocTenant.find_by(slug: slug)
  end
 
  def tenant_not_found
    [404, json_header, ['{"error":"Tenant not found"}']]
  end
 
  def poc_expired
    [403, json_header, ['{"error":"PoC period has expired"}']]
  end
 
  def json_header
    { "Content-Type" => "application/json" }
  end
end

テナントスコープの自動適用

# 全モデルに適用するテナントスコープ
module TenantScoped
  extend ActiveSupport::Concern
 
  included do
    belongs_to :poc_tenant
    default_scope -> { where(poc_tenant: Current.tenant) if Current.tenant }
    validates :poc_tenant, presence: true
    before_validation :set_tenant, on: :create
  end
 
  private
 
  def set_tenant
    self.poc_tenant ||= Current.tenant
  end
end
 
class Current < ActiveSupport::CurrentAttributes
  attribute :tenant
end
 
class DeliveryOrder < ApplicationRecord
  include TenantScoped
 
  has_many :delivery_items, dependent: :destroy
  validates :scheduled_date, presence: true
  validates :destination_address, presence: true
 
  scope :for_date, ->(date) { where(scheduled_date: date) }
  scope :unassigned, -> { where(driver: nil) }
end

WARNING

default_scope はテナント分離には便利だが、Rails では副作用が多い。unscoped で意図せずスコープが外れたり、joins で期待通りに動かないことがある。本番環境では acts_as_tenant gem や PostgreSQL の Row Level Security を検討すること。PoC 段階ではシンプルに進め、本番移行時にリファクタリングする。

PoC 用 API コントローラー

class Api::V1::RouteOptimizationsController < ApplicationController
  before_action :verify_poc_active!
 
  def create
    orders = DeliveryOrder.for_date(params[:date]).unassigned
    config = Current.tenant.optimization_config
 
    result = ArclightClient.optimize(
      orders: orders.map(&:to_optimization_params),
      constraints: config[:constraints]
    )
 
    # PoC 結果の記録(Phase 4 の評価に使う)
    PocResult.create!(
      poc_tenant: Current.tenant,
      request_params: { order_count: orders.size, date: params[:date] },
      response_time_ms: result.elapsed_ms,
      optimization_result: result.to_h,
      success: result.success?
    )
 
    render json: {
      optimization: result.routes,
      metrics: {
        total_distance_km: result.total_distance_km,
        reduction_rate: result.distance_reduction_rate,
        constraint_violations: result.violations.count,
        response_time_ms: result.elapsed_ms
      }
    }, status: :created
  end
 
  private
 
  def verify_poc_active!
    return if Current.tenant&.active?
    render json: { error: "PoC period is not active" }, status: :forbidden
  end
end

AWS CDK で PoC 環境を即座に立ち上げる

「PoC 環境の構築に 1 週間かけるのは論外だ」とカイは言った。「CDK でテンプレート化しておけば、新しい顧客の PoC 環境を 30 分で立ち上げられる」

Loading diagram...
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as rds from "aws-cdk-lib/aws-rds";
 
interface PocStackProps extends cdk.StackProps {
  tenantSlug: string;
  pocEndDate: string;
}
 
class PocEnvironmentStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props: PocStackProps) {
    super(scope, id, props);
 
    const vpc = new ec2.Vpc(this, "PocVpc", {
      maxAzs: 2,
      natGateways: 1, // コスト削減
    });
 
    // PoC はスモールインスタンスで十分
    const db = new rds.DatabaseInstance(this, "PocDb", {
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_16,
      }),
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL),
      vpc,
      databaseName: `poc_${props.tenantSlug.replace(/-/g, "_")}`,
      backupRetention: cdk.Duration.days(3),
      removalPolicy: cdk.RemovalPolicy.DESTROY, // PoC 終了後に削除可能
    });
 
    // PoC 終了日に自動削除するタグ
    cdk.Tags.of(this).add("poc:tenant", props.tenantSlug);
    cdk.Tags.of(this).add("poc:end-date", props.pocEndDate);
    cdk.Tags.of(this).add("auto-delete", "true");
  }
}

デプロイは 1 コマンドだ。

cdk deploy PocStack-yamamoto \
  --context tenantSlug=yamamoto-logistics \
  --context pocEndDate=2026-07-21

INFO

PoC 環境には必ず auto-delete タグと終了日を設定する。AWS の予算アラートと組み合わせて、終了後に放置された環境を自動検出・削除する仕組みを作っておく。FDE チームで月 50 万円のクラウド費用が「終わった PoC の残骸」だったという悲劇は珍しくない。

PoC のアンチパターン — 失敗から学ぶ

カイは過去の失敗事例を 4 つ共有してくれた。どれもソウタにとって耳が痛い話だった。

アンチパターン 1: スコープクリープ

最初は配送ルート最適化だけの PoC だったのに、途中で在庫管理も倉庫内動線もやりたいと言われ、全部引き受けた。結局どれも中途半端になった。

対策: スコープ変更は必ず書面で合意する。追加要望は「次の PoC」としてバックログに入れ、現在の PoC の成功基準は変えない。

アンチパターン 2: ゾンビ PoC

期限が来ても「もう少しで結果が出るから」と延長を繰り返し、3 ヶ月経っても終わらない PoC。ゾンビ PoC は FDE の時間を食い尽くし、他の有望な案件に手が回らなくなる。

対策: PoC 開始時に「最大延長は 1 回、1 週間まで」と合意する。延長条件(MUST 基準の 2/3 以上をパスしている場合のみ)も事前に決める。

アンチパターン 3: 成功基準の未定義

「うまくいったらわかるよ」——PoC 開始時の最も危険なセリフだ。成功基準がないと、顧客は「期待と違った」と言い、ベンダーは「技術的には成功した」と主張する。誰も幸せにならない。

対策: Phase 1 で SMART 基準を定義し、顧客と書面で合意してから Phase 2 に進む。

アンチパターン 4: 放置された PoC

PoC が終わった後、結果を誰もまとめず、学びが組織に残らない。次の PoC でまた同じ失敗をする。

対策: PoC 完了後に必ずポストモーテムを実施し、学びをチーム Wiki に記録する。

# PoC 完了後のナレッジ記録
class PocPostMortem
  def initialize(poc_plan:, results:)
    @poc_plan = poc_plan
    @results = results
    @learnings = []
  end
 
  def record_learning(category:, description:, action:)
    @learnings << { category: category, description: description,
                    action: action, recorded_at: Time.current }
  end
 
  def recommendation
    if @results[:verdict] == :go
      "PoV フェーズへの移行を推奨。本番環境の要件定義を開始すること。"
    else
      failed = @results[:details].reject { |d| d[:passed] }
      "未達基準: #{failed.map { |d| d[:id] }.join(', ')}。再 PoC または代替アプローチの検討を推奨。"
    end
  end
end

INFO

PoC のナレッジは「個人の経験」にとどめず、チーム全体で共有する。Arclight AI の FDE チームでは全 PoC の結果を社内 Wiki に記録し、月次の振り返り会を実施している。「あの顧客ではこの制約が問題になった」という知見が、次の PoC の計画精度を上げる。

PoC から本番への転換率

カイは FDE チームのダッシュボードを見せてくれた。

「PoC の成功率だけを見ても意味がない。重要なのは、PoC が本番契約に繋がったかどうかだ」

アプローチPoC 成功率本番転換率総合転換率
非構造化 PoC(基準なし)45%30%13.5%
構造化 PoC(SMART基準あり)60%80%48.0%
構造化 PoC + PoV 連携55%85%46.8%

非構造化の PoC では、全体の転換率がわずか 13.5% だ。SMART 基準を導入するだけで 48% に跳ね上がる。

INFO

構造化 PoC で転換率が劇的に上がる理由は、「失敗の早期検出」にある。SMART 基準があれば、Phase 3 の途中で No-Go と判断できる。非構造化 PoC は「なんとなく続けた挙句、なんとなく終わる」ので、顧客もベンダーも判断を先延ばしにし、結局は自然消滅する。明確な Go/No-Go は、Go の確度も上げるのだ。

ヤマモト物流 PoC — リベンジ

最初のデモ失敗から 1 週間。ソウタは改めてヤマモト物流に PoC の提案を持っていった。今回は違った。

まず、渡辺さんと現場のオペレーター 2 名を交えて、1 日かけて業務ヒアリングを行った。トラックの荷室サイズ、積み込み手順、配送先の時間指定、ドライバーの休憩パターン——すべてを聞き取り、制約条件として整理した。

# ヤマモト物流の業務制約をコード化
class YamamotoConstraints
  CONSTRAINTS = {
    lifo_loading: {
      type: :hard,  # 必ず満たす
      description: "積載順序はLIFO(後入れ先出し)"
    },
    time_window: {
      type: :hard,
      description: "配送先の時間指定を厳守"
    },
    max_work_hours: {
      type: :hard,
      description: "1日の稼働時間 ≤ 10時間(労基法準拠)"
    },
    refrigerated: {
      type: :hard,
      description: "冷蔵品は冷蔵車に限定"
    },
    lunch_break: {
      type: :soft,  # できれば満たす
      description: "12:00-13:00 に 45 分以上の休憩",
      penalty_weight: 0.8
    }
  }.freeze
end

SMART 基準を渡辺さんと合意し、3 週間の PoC を開始した。CDK で環境を立ち上げ、SAP TM からデータを連携し、制約付きのルート最適化を回した。

2 週間目、最初の中間報告。

「走行距離の削減率、現時点で 18.3% です」

渡辺さんの表情が変わった。

「目標の 15% を超えているのか。しかも積載順序も守られている?」

「はい。全 1,247 件のテストケースで制約違反はゼロです」

3 週間の PoC が終わった時点で、5 つの SMART 基準のうち 4 つをクリア。唯一 SC-004(SAP TM 同期成功率)が 98.7% で目標の 99.0% にわずかに届かなかったが、原因は SAP 側のタイムアウト設定にあることが特定できていた。

渡辺さんは言った。

「次のステップに進みましょう。年間の配送コスト削減効果を試算したいです」

PoV への移行が決まった瞬間だった。

ソウタの学び

オフィスに戻る電車の中で、ソウタは今回の PoC で学んだことを整理した。

最初のデモで失敗したとき、自分は「技術を見せること」に夢中だった。サンプルデータで動く美しいダッシュボード。リアルタイムのアニメーション。でも、それは顧客の業務とは無関係だった。

PoC は「技術のショーケース」ではない。「顧客の業務課題を解決できることの証明」だ。

カイが教えてくれた最も重要なことは、「PoC の設計に時間をかけろ」ということだった。SMART 基準を定義し、タイムラインを合意し、成功と失敗の境界線を明確にする。この「つまらない準備」が、PoC の成否を分ける。

そして、PoC は終わりではなく始まりだ。技術的に「動く」ことを証明した後に、「使える」ことを証明する PoV が待っている。「動く」と「使える」の間には、業務理解という深い溝がある。

FDE の仕事は、その溝を埋めることだ。

INFO

PoC で最も大切なのは、「失敗を歓迎する」姿勢だ。PoC の段階で「この技術はこの業務には合わない」とわかるのは成功だ。本番導入してから気づくよりも、はるかに安い。No-Go の判断ができることが、構造化 PoC の最大の価値である。

ソウタの手帳には、カイの言葉がメモされていた。

「完璧なデモを作るな。不完全だが正直な PoC を作れ。顧客が本当に知りたいのは、お前の技術力じゃない。自分たちの課題が解決されるかどうかだ」

次の章では、PoC を経て顧客との信頼関係を構築したソウタが、カスタム統合(Custom Integration)の世界に足を踏み入れる。顧客の既存システムと自社プロダクトをどう繋ぐのか——FDE にとって最も技術的にチャレンジングな領域だ。