mybook

プロローグ — 「コードは完璧なのに、なぜ顧客は使ってくれないのか」

「テストカバレッジ95%。重大バグ0件。アーキテクチャスコアA。——なのに、なぜ解約されるんだ」

深夜1時を回ったオフィスで、シニアエンジニアのソウタはダッシュボードを睨んでいた。画面に映るのは、TechNova の主要KPIだ。技術指標はすべて緑。だが、チャーンレートだけが赤く点滅している。

月次チャーンレート: 8.2%

SaaS の健全なチャーンレートは3%以下と言われる。8.2%は危険水域だ。ソウタは数字の推移を思い返した。3ヶ月前は5.1%だった。2ヶ月前に6.8%。先月7.4%。そして今月、8.2%。折れ線グラフは右肩上がりの一直線を描いている。上がってほしくない唯一の数字が、上がり続けている。

しかもソウタが最も力を注いだエンタープライズ顧客「メリディアン・フィナンシャル」が、今日正式に解約を通知してきた。契約から6ヶ月。年間契約の更新を待たずに離脱した。ARR(年間経常収益)で1,800万円。TechNovaにとって5番目に大きい顧客だった。

ソウタはSlackのログを遡った。メリディアンのCS担当からの最後のメッセージが目に入る。

「先方の担当者いわく、"機能は素晴らしいが、うちの業務に合わない" とのことです」

合わない。ソウタには理解できなかった。メリディアンが要望した機能は、すべて実装した。API連携、カスタムレポート、承認ワークフロー。要件定義書に書かれた通りに、一行の漏れもなく。

ソウタのコード、ソウタの自信

ソウタが所属するTechNovaは、企業向けデータ分析SaaSを提供するスタートアップだ。シリーズBで12億円を調達し、エンジニアは15名。ソウタはその中でもトップクラスの実力を持つシニアエンジニアだった。

彼のコードは、チーム内で「教科書」と呼ばれていた。

# app/services/reports/generator.rb
# ソウタが設計したレポート生成サービス
module Reports
  class Generator
    Result = Data.define(:report, :metadata)
 
    def initialize(account:, template:, date_range:)
      @account = account
      @template = template
      @date_range = date_range
    end
 
    def call
      validate_permissions!
      data = fetch_aggregated_data
      report = build_report(data)
      Result.new(report:, metadata: build_metadata(data))
    end
 
    private
 
    def validate_permissions!
      unless @account.can_generate_report?(@template.report_type)
        raise Reports::UnauthorizedError, "Account lacks permission for #{@template.report_type}"
      end
    end
 
    def fetch_aggregated_data
      DataAggregator.new(
        account: @account,
        metrics: @template.required_metrics,
        date_range: @date_range
      ).aggregate
    end
 
    def build_report(data)
      Report.create!(
        account: @account,
        template: @template,
        data_snapshot: data.to_json,
        generated_at: Time.current
      )
    end
 
    def build_metadata(data)
      { row_count: data.size, generated_at: Time.current, template_version: @template.version }
    end
  end
end

美しい単一責務。明確な命名。イミュータブルな Data.define。テストカバレッジは98%。コードレビューで指摘が入ることはほとんどなかった。

# spec/services/reports/generator_spec.rb
RSpec.describe Reports::Generator do
  let(:account) { create(:account, :with_report_permissions) }
  let(:template) { create(:report_template, report_type: :monthly_summary) }
  let(:date_range) { Date.new(2026, 1, 1)..Date.new(2026, 1, 31) }
 
  subject { described_class.new(account:, template:, date_range:) }
 
  describe "#call" do
    it "generates a report with correct metadata" do
      result = subject.call
 
      expect(result.report).to be_persisted
      expect(result.metadata[:row_count]).to be_positive
      expect(result.metadata[:template_version]).to eq(template.version)
    end
 
    context "when account lacks permissions" do
      let(:account) { create(:account) }
 
      it "raises UnauthorizedError" do
        expect { subject.call }.to raise_error(Reports::UnauthorizedError)
      end
    end
  end
end

テストは網羅的で、エッジケースも漏らさない。CI は常にグリーン。ソウタにとって、コード品質は誇りだった。

Rubocopの違反は0件。Brakemanのセキュリティ警告も0件。デプロイパイプラインは完全に自動化されており、mainブランチにマージされると5分以内に本番環境に反映される。技術的負債の指標であるCodeClimateのGPAは4.0満点中3.8。

ソウタは「良いコードを書けば顧客は満足する」と信じていた。いや、信じているというよりも、それ以外の可能性を考えたことがなかった。

だが、その信念が今夜、音を立てて崩れようとしていた。

深夜の来訪者

「——ソウタ? まだこんな時間にオフィスにいるのか」

振り返ると、見覚えのある顔がドアの前に立っていた。カイ。大学時代の同期で、卒業以来まともに連絡を取っていなかった。

「カイ? なんで……」

「うちの会社がTechNovaとパートナーシップを検討していてね。明日のミーティングの下見がてら寄ったんだが、ビルの明かりが見えたから」

カイは今、AIプラットフォーム企業「Arclight AI」でFDEチームのリードを務めている。ソウタはそれを噂で聞いていたが、FDEが何の略かすら知らなかった。

「何を睨んでるんだ」カイはソウタの画面を覗き込んだ。

「チャーンレート。メリディアン・フィナンシャルが解約した。6ヶ月で。——俺は彼らが要望した機能を全部作ったのに」

カイは静かにうなずいた。そして、ソウタが予想もしなかった一言を放った。

「お前のコードは完璧だよ。でも、解いている問題が違う

「ラストマイル問題」

ソウタは眉をひそめた。「解いている問題が違う? 要件定義書通りに作ったんだぞ」

「それが問題なんだ」カイはソウタの隣に座った。「メリディアンのレポート機能、見せてもらっていいか?」

ソウタはメリディアン向けに構築したAPIエンドポイントを開いた。

# app/controllers/api/v1/reports_controller.rb
module Api
  module V1
    class ReportsController < ApplicationController
      def create
        generator = Reports::Generator.new(
          account: current_account,
          template: ReportTemplate.find(params[:template_id]),
          date_range: parse_date_range(params[:start_date], params[:end_date])
        )
        result = generator.call
        render json: ReportSerializer.new(result.report).as_json, status: :created
      end
 
      private
 
      def parse_date_range(start_date, end_date)
        Date.parse(start_date)..Date.parse(end_date)
      end
    end
  end
end

「綺麗なAPIだな」カイは認めた。「で、メリディアンの担当者は、このAPIをどう使ってた?」

ソウタは一瞬黙った。「……使ってなかった。正確に言うと、使おうとしたが、途中でやめたらしい。CS経由で聞いた」

「なぜやめたか、聞いたか?」

「"うちの業務フローに合わない"と」

カイは画面を指差した。「ソウタ。メリディアンは金融機関だ。彼らのレポートは四半期単位で、しかも監査証跡が必要だ。お前のAPIは start_dateend_date を自由に指定できる——柔軟だが、彼らにとっては『毎回手動で四半期の開始日と終了日を計算して入力する手間』になる。しかも監査証跡がない」

ソウタは言葉を失った。

「もうひとつ。メリディアンのデータ分析チームは5人いるが、全員がAPIを直接叩けるわけじゃない。彼らが欲しかったのは、ボタン一つで『2026年Q1レポート』が出る画面と、『誰がいつ生成したか』の記録だ」

INFO

**ラストマイル問題(Last Mile Problem)**とは、プロダクトが技術的に完成していても、顧客の実際の業務環境で「使える」状態になるまでの最後の距離(ラストマイル)を埋められない問題のことだ。ネットワーク業界の「ラストワンマイル」(基地局からユーザー宅までの最後の回線)に由来する。

カイは続けた。「これはお前だけの問題じゃない。業界全体の構造的な課題だ」

Loading diagram...

「プロダクトチームは機能を作る。顧客は受け取る。だがその間に巨大な溝がある。要件定義書には書かれない、顧客の日常業務のコンテキストだ。承認フロー、社内ルール、データの粒度、セキュリティ要件。それを埋めない限り、どんなに美しいコードも使われない」

90%のAIプロジェクトが失敗する理由

「AI の世界では、この問題がさらに深刻だ」カイは自分のスマートフォンを取り出し、一つの数字を見せた。

WARNING

Gartner の調査(2025年)によれば、AIおよびMLプロジェクトの約90%がPoCから本番環境への移行に失敗している。技術的な問題ではなく、組織・業務・データ環境の壁が原因だ。

「うちのArclight AIは、企業向けにAI分析プラットフォームを提供している。モデルの精度は高い。デモでは顧客が感動する。だが、いざ本番環境にデプロイすると——」

「動かない?」

「動くんだ。動くけど、使えない

カイはTechNovaのコードを例に取った。「たとえばお前のところのAI予測機能。デモではサンプルデータで見事に動いただろう。だが、メリディアンの実データで動かしたらどうなる?」

ソウタは思い出した。メリディアンのデータチームから「予測結果がおかしい」と何度か問い合わせがあった。調査したが、モデル自体には問題がなかった。

# app/services/predictions/engine.rb
# TechNovaのAI予測エンジン — 技術的には正しい
module Predictions
  class Engine
    def initialize(account:, model_type: :default)
      @account = account
      @model = load_model(model_type)
    end
 
    def predict(dataset)
      validated = validate_schema(dataset)
      normalized = normalize(validated)
      @model.predict(normalized)
    end
 
    private
 
    def validate_schema(dataset)
      # 標準スキーマに準拠しているかチェック
      schema = Predictions::Schema.for(@model)
      schema.validate!(dataset)
      dataset
    end
 
    def normalize(dataset)
      # 標準的な正規化処理
      Predictions::Normalizer.new(
        strategy: :z_score,
        handle_missing: :mean_imputation
      ).transform(dataset)
    end
  end
end

「このコード、何が問題かわかるか?」カイが問いかけた。

ソウタは首をかしげた。「設計上の問題は見当たらないが……」

「メリディアンのデータには、日本の会計基準特有のカラムが含まれている。税区分コード消費税端数処理区分インボイス登録番号。お前の validate_schema は標準スキーマで検証するから、これらを『不明なカラム』として弾く。メリディアンの分析チームは毎回、カラムを手動で削除してからアップロードしている」

「……そんな話は聞いてなかった」

「聞いてないんじゃない。聞きに行ってないんだ」

本当に必要だったコード

カイは自分のラップトップを開いた。「うちのFDEチームがメリディアンのような金融機関に入ったとき、最初に書くコードはこういうものだ」

# FDEが顧客環境で書くアダプター層の例
module CustomerAdapters
  class FinancialInstitution
    FISCAL_QUARTER_MAP = {
      q1: { start_month: 4, end_month: 6 },   # 日本の会計年度
      q2: { start_month: 7, end_month: 9 },
      q3: { start_month: 10, end_month: 12 },
      q4: { start_month: 1, end_month: 3 }
    }.freeze
 
    def initialize(account:, fiscal_year_start: 4)
      @account = account
      @fiscal_year_start = fiscal_year_start
    end
 
    def quarterly_report(quarter:, fiscal_year:)
      range = resolve_quarter(quarter, fiscal_year)
 
      Reports::Generator.new(
        account: @account,
        template: find_or_create_audit_template,
        date_range: range
      ).call.tap do |result|
        AuditLog.record!(
          actor: Current.user,
          action: :report_generated,
          resource: result.report,
          metadata: { quarter:, fiscal_year: }
        )
      end
    end
 
    private
 
    def resolve_quarter(quarter, fiscal_year)
      mapping = FISCAL_QUARTER_MAP.fetch(quarter)
      start_date = Date.new(fiscal_year, mapping[:start_month], 1)
      end_date = start_date.end_of_month + (mapping[:end_month] - mapping[:start_month]).months
      start_date..end_date.end_of_month
    end
 
    def find_or_create_audit_template
      ReportTemplate.find_or_create_by!(
        account: @account,
        report_type: :quarterly_audit,
        compliance_standard: :j_sox
      )
    end
  end
end

「お前の Reports::Generator はそのまま使ってる。中核のコードは変えない。ただ、顧客の業務コンテキストを手前で吸収するアダプター層を加えるんだ。四半期の自動計算、監査証跡の記録、日本の会計基準への対応。——こういうコードは、顧客のオフィスに座らないと書けない」

ソウタは画面を見つめた。わずか50行にも満たないコードだが、ここには自分が6ヶ月間見落としていたものが詰まっていた。

INFO

ラストマイル問題の本質は「技術力の不足」ではない。顧客の業務コンテキストの不在だ。どれだけ優れたAPIを設計しても、顧客が日々どのようにデータを扱い、どのような承認フローを経て、どのような規制に従っているかを知らなければ、「使われるソフトウェア」にはならない。

FDEとは何か

「カイ、お前の肩書き——FDEって何なんだ?」

「Forward Deployed Engineer。日本語にすると前方展開エンジニアだ」

「前方展開……軍事用語か?」

「そうだ。軍事で『前方展開(Forward Deployment)』とは、部隊を前線に配置して迅速に対応できる態勢を作ることを指す。FDEは、エンジニアを顧客の現場に『前方展開』させる。2010年代にPalantirが始めたモデルだ」

INFO

FDE(Forward Deployed Engineer) はPalantir Technologies が2010年代初頭に確立した職種だ。データ分析プラットフォームを政府機関や大企業に導入する際、エンジニアを顧客先に常駐させ、プロダクトのカスタマイズ・導入・定着を推進した。「Forward Deployed(前方展開)」という名称は、米軍の前方展開戦略に由来する。

「ただのコンサルタントとは違うのか?」

「まったく違う。コンサルタントは提案書を書く。FDEは本番コードを書く。ただの技術サポートとも違う。サポートエンジニアは問い合わせに対応する。FDEはプロダクトのロードマップを動かす

カイは紙ナプキンに図を描き始めた。

Loading diagram...

「ソフトウェアエンジニア(SWE)はプロダクトを作る。セールスエンジニア(SE)は顧客に価値を伝えて売る。FDEは顧客の現場に入り込んで、プロダクトを実際に動く状態にする。そして、そこで得た知見をプロダクトチームにフィードバックして、プロダクト自体を進化させる」

「つまり、お前はエンジニアでありながら、顧客の業務を理解して、プロダクトと現場の橋渡しをする……」

「そうだ。FDEはプロダクトと顧客の間に立つ唯一のエンジニア職だ」

なぜ今、FDEが急増しているのか

カイはコーヒーを淹れながら続けた。

「FDEという職種は2010年代からあったが、2025年から2026年にかけて需要が爆発した。LinkedIn のデータでは、FDE関連の求人が前年比729%増だ」

「729%? バグじゃないのか」ソウタは信じられない顔をした。

「バグじゃない。理由は明白だ——AIだ」

カイの説明はこうだった。2024年から2025年にかけて、あらゆる企業がAIを導入しようとした。McKinseyの調査では、2025年時点でFortune 500企業の78%がAI導入プロジェクトを1つ以上走らせていた。だが、そのうち本番環境で継続的に使われているのは12%に過ぎなかった。残りの88%は、PoCで終わったか、導入後に放棄されたかのどちらかだ。

「AIのデモは簡単だ。整形済みのサンプルデータを食わせれば、見栄えの良い結果が出る。だが本番環境は違う。データは汚い。フォーマットはバラバラ。社内の承認フローが絡む。既存システムとの連携が必要だ。セキュリティ要件がある。——これを全部乗り越えないと、AIは『動いている』だけで『使われている』にはならない」

ソウタはTechNovaのAI機能を思い出した。デモ環境ではサンプルデータで完璧に動く。だが、メリディアンの実データ環境では——。

# TechNovaのAI機能 — デモでは動くが本番で失敗するパターン
module Ai
  class AnalysisJob < ApplicationJob
    def perform(account_id, dataset_id)
      account = Account.find(account_id)
      dataset = Dataset.find(dataset_id)
 
      # デモ環境: 整形済みCSV、カラム名は英語、欠損値なし
      # 本番環境の現実:
      #   - カラム名が日本語("売上高", "経常利益")
      #   - Excel由来の数値に全角文字混入("1,234")
      #   - 日付フォーマットが和暦("R6.4.1")
      #   - 機密カラムが含まれる(マスキング必要)
      result = Predictions::Engine.new(account:).predict(dataset.records)
 
      AnalysisResult.create!(
        account:,
        dataset:,
        predictions: result,
        status: :completed
      )
    end
  end
end

WARNING

デモ環境と本番環境の乖離は「AIデプロイメントの死の谷」と呼ばれる。サンプルデータでの成功は、本番環境での成功を一切保証しない。データの品質、フォーマット、セキュリティ要件、既存システムとの統合——これらすべてが「デモでは見えない壁」として立ちはだかる。

「メリディアンの場合、お前のAI機能を使うために、データチームが毎回2時間かけてExcelを整形していたそうだ」カイが言った。

「2時間……? なぜそれを言ってくれなかったんだ」

「言ったんだよ。CSチーム経由で。『データの前処理が大変』という問い合わせが3回あった。だが、お前のチームは『ドキュメントに前処理の手順を追記しました』と返した」

ソウタの顔が強張った。確かに、そのチケットは覚えている。Jiraのステータスは「Resolved」。対応内容は「ドキュメントを更新し、データ前処理の手順をFAQに追加」。自分がレビューして承認した。

だが、顧客が本当に求めていたのはドキュメントではなく、前処理を不要にする仕組みだった。手順書を渡されても、毎回2時間の手作業が消えるわけではない。顧客は黙って離れる。不満を言い続ける顧客は少数派だ。多くは静かに解約する。メリディアンもそうだった。

FDEの仕事のサイクル

「じゃあ、FDEは具体的にどう動くんだ?」ソウタが聞いた。

「5つのフェーズを回す」

Loading diagram...

カイは各フェーズを説明した。

スコーピング。まず顧客の現場に入り、業務フローを理解する。要件定義書ではなく、実際にユーザーが画面を操作している様子を観察する。『何を作ってほしいか』ではなく、『何に困っているか』を聞く」

プロトタイプ。顧客の環境で動くプロトタイプを、数日〜数週間で作る。完璧である必要はない。顧客のデータ、顧客のワークフロー、顧客のインフラで動くことが最優先だ」

デプロイ。プロトタイプを顧客の本番環境に載せる。ここでインフラ、セキュリティ、パフォーマンスの壁とぶつかる。FDEはこの壁を自分で乗り越える」

フィードバック収集。デプロイ後、ユーザーがどう使っているかを観察する。使われていない機能、想定外の使い方、新たな課題。これを構造化して記録する」

プロダクト改善提案。フィードバックを本社のプロダクトチームに持ち帰り、汎用的な改善として提案する。FDEが顧客Aで解決した問題は、顧客B・Cでも発生している可能性が高いからだ。実際、うちのFDEチームが金融顧客向けに作った和暦変換モジュールは、今ではプロダクトのコア機能として20社以上で使われている」

INFO

FDEは単なる「導入支援エンジニア」ではない。顧客の現場で得た知見をプロダクトに還元することで、プロダクト全体の価値を底上げする役割を担う。一社のカスタマイズが、全顧客の体験を改善する——このフィードバックループがFDEの真価だ。

「つまりFDEは、顧客と一緒に仕事をしながらプロダクトを進化させる存在なのか」

「そうだ。デプロイするだけじゃない。現場のインテリジェンスを持ち帰って、プロダクト全体をよくする。それがFDEとカスタマーサポートの決定的な違いだ」

メリディアンを救えたかもしれない世界線

ソウタはしばらく黙っていた。そして、ぽつりと言った。

「もしFDEがメリディアンに入っていたら、最初の数週間で気づいていたのか。四半期レポートの件も、データ前処理の件も」

「おそらくな。FDEはまず現場に座る。メリディアンのデータチームの隣で、彼らがどうデータを触っているかを見る。全角数字の混入も、和暦の問題も、初日に気づいただろう。そして翌週にはアダプターを書いて、翌月にはその知見をプロダクトのコアに提案している」

カイはラップトップを閉じた。

「ソウタ。お前は優秀なエンジニアだ。コードの品質は俺が保証する。だが、お前に欠けているのは顧客の現場で得る解像度だ。要件定義書は地図だが、FDEは現地を歩く

ソウタはもう一度ダッシュボードを見た。チャーンレート8.2%。メリディアンの名前が解約リストに並んでいる。

「お前に提案がある」カイが立ち上がった。「うちのArclight AIに1ヶ月来い。FDEチームに入って、実際の顧客デプロイメントを経験してみろ。前方展開とは何か、自分の目で見ろ

「1ヶ月……」

「お前のCTOには俺から話す。パートナーシップの一環として、エンジニア交換プログラムということにすればいい。——お前が今のまま最高のコードを書き続けても、チャーンレートは下がらないぞ」

ソウタは迷った。自分のコードには自信がある。だが、メリディアンの解約は事実だ。95%のテストカバレッジが守れなかったものがある。コードの外側に。

「……わかった。行く」

カイは薄く笑った。

「いい判断だ。俺がFDEに転向したのは3年前だ。それまでは、お前と同じようにプロダクトチームで『完璧なコード』を書いていた。だが、初めて顧客のオフィスに座ったとき、自分がいかに想像の中の顧客を相手に仕事をしていたかを思い知った」

ソウタは黙ってうなずいた。

「その前に、FDEが業界のどこに位置づけられるか、整理しておこう。明日のミーティングの前に。——お前が思っている以上に、エンジニアの世界は広い」


ソウタはオフィスの電気を消した。深夜2時。窓の外には六本木の夜景が広がっている。いつもはこの景色を見る余裕もなく帰っていた。

エレベーターを待つ間、スマートフォンでメリディアンの担当者——田中さんのプロフィールを見た。LinkedInには「データ分析チームリーダー」とある。田中さんは毎朝8時にオフィスに来て、Excelのデータを2時間かけて整形し、TechNovaのプラットフォームにアップロードしていた。その事実を、ソウタは今日初めて知った。

テストカバレッジ95%。重大バグ0件。アーキテクチャスコアA。

——それでも、田中さんの2時間は返ってこない。メリディアンは去った。

明日からソウタは、コードの「外側」を学び始める。FDE——前方展開エンジニアという、自分が知らなかった世界へ。

INFO

次章予告: 第2章「FDEの座標 — エンジニアリングの地図を広げる」では、FDEがSWE・SE・SRE・TAMなどの既存職種とどう異なるのかを体系的に整理する。ソウタがArclight AIに足を踏み入れる初日、カイが見せる「エンジニアリングの全体地図」とは——。