PoC の設計と実行 — 「動く」と「使える」の間にある深い溝
「デモは完璧でした。でも、うちの業務では使えません」
ヤマモト物流の情報システム部長・渡辺さんが、穏やかだが容赦のない一言を放った。会議室の空気が凍る。スクリーンにはソウタが三日間徹夜して作った Arclight AI の配送最適化デモが映っている。サンプルデータで動く美しいダッシュボード。リアルタイムで最適ルートを計算するアニメーション。技術的には申し分ない。
しかし、渡辺さんの目は冷静だった。
「うちの配送ドライバーは一日に平均 87 件の配送をこなします。ルート最適化の前に、まず荷物の積載順序が重要なんです。トラックの荷室は奥から詰めるので、配送順序が逆じゃないと積めない。この制約、デモには入っていますか?」
入っていなかった。
ソウタは隣に座るカイの方をちらりと見た。カイは腕を組んだまま、小さくうなずいた。「想定通りだ」とでも言うように。
会議室を出た後、カイはソウタの肩を叩いた。
「おつかれさん。いい失敗だった」
「いい失敗……ですか」
「デモと PoC は違う。デモは『こんなことができます』を見せるもの。PoC は『あなたの業務でこう使えます』を証明するものだ。今日のは完璧なデモだったけど、PoC ではなかった」
カイはホワイトボードに向かいながら続けた。
「これから本当の PoC のやり方を教える。ヤマモト物流さんには来週、改めて PoC の提案をしよう」
PoC と PoV — 似て非なる二つの検証
PoC(Proof of Concept)と PoV(Proof of Value)。この二つの区別がつかないまま走り出すと、ソウタのように「技術的には動くけど採用されない」という結果に終わる。
PoC — 「できるか?」を問う
PoC は技術的な実現可能性を検証するフェーズだ。「Arclight AI のルート最適化エンジンは、ヤマモト物流の配送データを処理できるか?」「レスポンスは実用的な速度か?」「既存システムと連携可能か?」——こうした問いに答える。
PoV — 「使う価値があるか?」を問う
PoV は一歩先の話だ。技術的に動くことがわかった上で、「導入したら業務がどう改善されるか」を定量的に示す。
INFO
PoC は「動くことの証明」、PoV は「価値があることの証明」だ。FDE が最初に取り組むのは PoC だが、ゴールは常に PoV を見据えている。技術的に動いても業務価値がなければ、顧客は買わない。
ヤマモト物流の渡辺さんが本当に知りたかったのは PoV の領域だった。導入で配送コストは何%下がるのか。ドライバーの残業時間はどれだけ減るのか。毎朝の配車計画に費やす2時間をどこまで短縮できるのか。
ソウタのデモは PoC すら満たしていなかった。積載順序という基本的な業務制約を取り込んでいなかったからだ。
PoC の 4 フェーズ
カイはホワイトボードに PoC の全体像を描いた。
「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
endINFO
成功基準は「ワークフロー単位」で設計する。「ルート最適化 API のレスポンスが速い」ではなく、「朝 7 時の配車会議までに 87 件のルートが計算完了している」のように、顧客の業務フローに即した基準にする。機能ではなくワークフローに焦点を当てることで、技術と業務のギャップを埋められる。
PoC タイムライン — スピードが命
PoC は長引けば長引くほど死ぬ。カイは 3 つのカテゴリに分けたタイムラインの目安を教えてくれた。
| カテゴリ | 期間 | 特徴 | 例 |
|---|---|---|---|
| Simple SaaS | 7 日 | 標準機能の検証。カスタマイズ不要 | API にデータを投げて結果確認 |
| Standard Enterprise | 2-4 週 | 既存システム連携。データ変換あり | SAP TM 連携 + カスタム制約 |
| Complex Integration | 4-6 週 | 複数システム、セキュリティ要件、大規模データ | オンプレ DB + クラウド AI + 基幹連携 |
ヤマモト物流の案件は Standard Enterprise に分類された。SAP TM との連携と積載制約のカスタマイズが必要だが、データ量は中規模で、セキュリティ要件も標準的だった。ソウタは 3 週間の PoC スケジュールを組んだ。
WARNING
PoC のタイムラインを顧客と合意する際、「延長の条件」も事前に定義しておくこと。「SC-001 と SC-002 がパスし、SC-003 のみ未達の場合、追加 1 週間の延長を認める」のように。延長条件がないと、ずるずると「ゾンビ PoC」になる。
マルチテナント PoC 環境の構築
カイが最も力を入れて教えてくれたのが、PoC 環境の設計だった。
「FDE は同時に複数の顧客の PoC を回すことがある。そのとき、顧客ごとにまったく別の環境を立てるとコストが爆発する。かといって同じ環境を共有するとデータが混ざる。マルチテナントの PoC 環境が必要だ」
テナントモデルとスコープ
# テナント管理モデル
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) }
endWARNING
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
endAWS CDK で PoC 環境を即座に立ち上げる
「PoC 環境の構築に 1 週間かけるのは論外だ」とカイは言った。「CDK でテンプレート化しておけば、新しい顧客の PoC 環境を 30 分で立ち上げられる」
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-21INFO
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
endINFO
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
endSMART 基準を渡辺さんと合意し、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 にとって最も技術的にチャレンジングな領域だ。