AI 時代の FDE — エージェント、RAG、Eval の最前線
帝都生命の会議室
「ソウタさん、来週から帝都生命のプロジェクトに入ってもらいます」
Arclight AI のオフィスで、カイがプロジェクト資料を広げた。国内有数の生命保険会社「帝都生命」が、保険金請求処理を AI で自動化したいという。年間 80 万件の請求書類を、現在は 300 人の査定担当者が手作業で処理している。
「PoC はもう終わってるんですよね?デモ動画も見ました。かなり精度が高そうでしたが」
ソウタの言葉に、カイは苦笑した。
「AI の PoC は誰でも作れる。本番で動かし続けるのが地獄だ」
カイはホワイトボードに「PoC」と「Production」の間に巨大な谷を描いた。
「この谷を"死の谷"と呼ぶ。AI プロジェクトの 90% 以上がここで死ぬ」
WARNING
Gartner の調査によると、AI プロジェクトの 85% が本番投入に至らない。さらに本番投入できたプロジェクトの半数が 1 年以内に運用停止される。技術的な問題よりも、業務プロセスとの不整合やエッジケースの処理不足が主因だ。
PoC と本番の間にある "死の谷"
カイはソウタに、過去の失敗プロジェクトの事例を見せた。
「PoC では準備されたデータで 95% の精度が出る。でも本番データは汚い。手書きの文字は潰れているし、FAX で送られてきた書類はノイズだらけ。"その他"欄に自由記述で書かれた内容を AI が誤読して、本来 100 万円の保険金を 1,000 万円と判定したこともある」
FDE が埋めるラストマイル
PoC と本番の間のギャップは、純粋な技術の問題ではない。顧客のデータ、業務プロセス、エッジケースを深く理解した上で、AI を「使い物になる」レベルまで磨き上げる必要がある。
- データの現実を知る -- 顧客が持つデータは PoC 用のクリーンなデータとはまるで違う
- 業務フローに組み込む -- AI の出力を既存の承認プロセスにどう接続するか
- エッジケースを潰す -- 「99% 正しい」では保険業務は回らない。残り 1% が訴訟リスクになる
- 運用を設計する -- モデルの劣化検知、再学習パイプライン、障害時のフォールバック
INFO
AI 時代の FDE は「ML エンジニア」ではない。最先端の AI 技術を、顧客の泥臭い現実で動くようにする「技術と現実の翻訳者」だ。
RAG アーキテクチャの設計
帝都生命の最初の要件は「過去の査定事例を検索して、類似案件の判断根拠を提示する」機能だった。ソウタは RAG(Retrieval-Augmented Generation)アーキテクチャの設計に取りかかった。
チャンキング戦略
「まず文書をどう分割するかが最初の関門だ」とカイが言った。保険約款は数百ページに及ぶ法律文書だ。単純に 500 文字で区切ると、条項の途中で切れてしまい意味が失われる。
# app/services/document_chunker.rb
class DocumentChunker
STRATEGIES = { fixed: FixedChunker, semantic: SemanticChunker,
recursive: RecursiveChunker }.freeze
def initialize(strategy: :recursive, chunk_size: 512, overlap: 64)
@strategy = STRATEGIES.fetch(strategy)
@chunk_size = chunk_size
@overlap = overlap
end
def chunk(document)
@strategy.new(chunk_size: @chunk_size, overlap: @overlap)
.call(document.content)
end
end
# 再帰的チャンキング -- 構造を保持しながら分割
class RecursiveChunker
SEPARATORS = ["\n## ", "\n### ", "\n\n", "\n", "。", ""].freeze
def initialize(chunk_size:, overlap:)
@chunk_size = chunk_size
@overlap = overlap
end
def call(text)
split_recursive(text, SEPARATORS)
end
private
def split_recursive(text, separators)
return [text] if text.length <= @chunk_size || separators.empty?
sep = separators.first
parts = text.split(sep).reject(&:empty?)
chunks = []
current = ""
parts.each do |part|
candidate = current.empty? ? part : "#{current}#{sep}#{part}"
if candidate.length <= @chunk_size
current = candidate
else
chunks << current unless current.empty?
current = part
end
end
chunks << current unless current.empty?
chunks.flat_map do |chunk|
chunk.length > @chunk_size ? split_recursive(chunk, separators[1..]) : chunk
end
end
end「保険約款のような法律文書には再帰的チャンキングが効く。見出し、段落、文の順に分割を試みるから、意味の区切りが保たれる」とカイは説明した。
ベクトル DB の選定
「帝都生命は既に PostgreSQL を使っている。pgvector から始めよう」とカイが判断した。
INFO
Rails エンジニアにとって pgvector は最も自然な選択肢だ。既存の PostgreSQL にエクステンションを追加するだけで、Active Record からベクトル検索が使える。数百万件規模まではこれで十分に戦える。
# db/migrate/20260601000000_create_document_chunks.rb
class CreateDocumentChunks < ActiveRecord::Migration[8.0]
def up
enable_extension "vector"
create_table :document_chunks do |t|
t.references :document, null: false, foreign_key: true
t.text :content, null: false
t.integer :position, null: false
t.column :embedding, :vector, limit: 1024
t.jsonb :metadata, default: {}
t.timestamps
end
add_index :document_chunks, :embedding,
using: :hnsw, opclass: :vector_cosine_ops,
name: "index_document_chunks_on_embedding"
end
end# app/models/document_chunk.rb
class DocumentChunk < ApplicationRecord
belongs_to :document
has_neighbors :embedding # pgvector の neighbor gem
scope :search_by_similarity, ->(query_embedding, limit: 5) {
nearest_neighbors(:embedding, query_embedding, distance: "cosine")
.limit(limit)
}
endソウタは将来のスケーリングに備えて、他のベクトル DB も調査した。
- pgvector -- 既存の PostgreSQL に追加するだけ。Rails との親和性が最高。数百万件まで
- Pinecone -- フルマネージド。スケーリングに強いが、ベンダーロックインあり
- Weaviate -- ハイブリッド検索(ベクトル + キーワード)が強力。自前運用も可能
- Qdrant -- 高性能でフィルタリング機能が充実。Rust 製で軽量
RAG クエリサービスの実装
ソウタは RAG のコア部分を Rails のサービスオブジェクトとして実装した。Embedding 生成には AWS Bedrock の Titan モデルを使い、回答生成には Claude を利用する。
# app/services/rag_query_service.rb
class RagQueryService
MAX_CONTEXT_TOKENS = 4000
def initialize(embedding_service: EmbeddingService.new,
llm_client: BedrockLlmClient.new)
@embedding_service = embedding_service
@llm_client = llm_client
end
def query(question, filters: {})
query_embedding = @embedding_service.embed(question)
chunks = DocumentChunk.search_by_similarity(query_embedding, limit: 20)
chunks = chunks.where("metadata @> ?", filters.to_json) if filters.any?
ranked = rerank(question, chunks.first(10))
context = build_context(ranked.first(5))
answer = generate(question, context)
Conversation.create!(
question: question, context_used: context,
answer: answer, chunk_ids: ranked.map(&:id),
model_version: @llm_client.model_id
)
answer
end
private
def rerank(question, chunks)
chunks.sort_by { |c| -compute_relevance_score(question, c.content) }
end
def build_context(chunks)
parts, total = [], 0
chunks.each do |chunk|
tokens = chunk.content.length / 2 # 日本語の概算
break if total + tokens > MAX_CONTEXT_TOKENS
parts << "[出典: #{chunk.document.title}]\n#{chunk.content}"
total += tokens
end
parts.join("\n\n---\n\n")
end
def generate(question, context)
@llm_client.generate(
system: system_prompt,
messages: [{ role: "user",
content: "参照文書:\n#{context}\n\n質問: #{question}" }]
)
end
def system_prompt
<<~PROMPT
あなたは帝都生命の保険査定アシスタントです。
1. 参照文書に記載されている情報のみに基づいて回答する
2. 参照文書にない情報は「該当する情報が見つかりません」と回答する
3. 金額や日付は参照文書の記載を正確に引用する
4. 法的な判断や最終的な査定結果は提示しない
5. 回答の根拠となる出典を必ず明記する
PROMPT
end
endAgent オーケストレーション
帝都生命の要件が複雑化していった。請求書類の種類を判定し、必要な情報を抽出し、過去の類似事例を検索し、査定基準に照らして判断を提示する -- 複数のステップを連携させる必要がある。
「これは Agent の出番だ」とカイが言った。
# app/services/claims_agent_orchestrator.rb
class ClaimsAgentOrchestrator
def initialize(claim)
@claim = claim
@context = { claim_id: claim.id, steps: [] }
end
def process
doc_type = ClassifyDocumentAgent.new(@claim.document).call
extracted = ExtractInformationAgent.new(
document: @claim.document, doc_type: doc_type,
schema: extraction_schema_for(doc_type)
).call
query_embedding = EmbeddingService.new.embed(build_search_query(extracted))
similar = DocumentChunk.search_by_similarity(query_embedding, limit: 10)
assessment = GenerateAssessmentAgent.new(
claim_data: extracted, similar_cases: similar,
guidelines: load_guidelines
).call
validated = GuardrailService.new.validate(assessment, context: @context)
ClaimAssessmentResult.create!(
claim: @claim,
assessment: validated[:assessment],
confidence: validated[:confidence],
similar_case_ids: similar.map(&:id),
requires_human_review: validated[:confidence] < 0.85,
processing_context: @context
)
end
end「ポイントは requires_human_review だ。AI が自信のない案件は必ず人間に回す。保険業務で AI が暴走したら取り返しがつかない」
Eval フレームワーク -- AI を「テスト」する
ソウタが最も衝撃を受けたのは、カイが Eval(評価)に割く時間の多さだった。
「Eval は AI プロジェクトで最も重要で、最も軽視されている部分だ」
従来のソフトウェアテストは「入力 A に対して出力 B が返ること」を検証する。だが AI の出力は毎回異なる。同じ質問をしても、微妙に違う文章が返ってくる。
# app/services/eval_runner.rb
class EvalRunner
EVAL_PROMPT = <<~PROMPT
あなたは AI の回答品質を評価する審査員です。
0.0〜1.0 のスコアを付けてください。
## 評価基準
- faithfulness: 参照文書に忠実か
- relevance: 質問に適切に答えているか
- completeness: 必要な情報が網羅されているか
- safety: 不適切な内容を含んでいないか
質問: %{question}
参照文書: %{context}
AI の回答: %{answer}
JSON で回答: {"faithfulness":0.0,"relevance":0.0,"completeness":0.0,"safety":0.0,"reasoning":""}
PROMPT
def initialize(llm_client: BedrockLlmClient.new)
@llm_client = llm_client
end
def evaluate(question:, context:, answer:)
prompt = format(EVAL_PROMPT, question: question,
context: context, answer: answer)
scores = JSON.parse(@llm_client.generate(prompt))
EvalResult.create!(
question: question, context: context, answer: answer,
faithfulness: scores["faithfulness"],
relevance: scores["relevance"],
completeness: scores["completeness"],
safety: scores["safety"],
reasoning: scores["reasoning"]
)
end
def run_suite(test_cases)
results = test_cases.map do |tc|
answer = RagQueryService.new.query(tc[:question])
evaluate(question: tc[:question], context: tc[:context], answer: answer)
end
EvalSuiteReport.new(results)
end
endWARNING
Eval は「一度作って終わり」ではない。顧客の業務が変わるたびにテストケースも更新が必要だ。FDE は顧客と定期的にレビューして、Eval スイートを進化させ続ける。これが AI の品質を維持する唯一の方法だ。
AWS Bedrock + OpenSearch のエンタープライズ RAG 構成
帝都生命は AWS を全面採用している。ソウタはエンタープライズグレードの RAG アーキテクチャを設計した。
# app/clients/bedrock_llm_client.rb
class BedrockLlmClient
MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0"
def initialize(region: "ap-northeast-1")
@client = Aws::BedrockRuntime::Client.new(region: region)
end
def model_id = MODEL_ID
def generate(system: nil, messages:, max_tokens: 2048)
body = { anthropic_version: "bedrock-2023-05-31",
max_tokens: max_tokens, messages: messages }
body[:system] = system if system
response = @client.invoke_model(
model_id: MODEL_ID, content_type: "application/json",
accept: "application/json", body: body.to_json
)
JSON.parse(response.body.read).dig("content", 0, "text")
end
end文書がアップロードされると、バックグラウンドジョブがチャンキング、Embedding 生成、インデックス登録を自動的に行う。
# app/jobs/document_indexing_job.rb
class DocumentIndexingJob < ApplicationJob
queue_as :ai_indexing
retry_on Aws::BedrockRuntime::Errors::ThrottlingException,
wait: :polynomially_longer, attempts: 5
def perform(document_id)
document = Document.find(document_id)
document.update!(indexing_status: "processing")
chunks = DocumentChunker.new(strategy: :recursive).chunk(document)
service = EmbeddingService.new(provider: :bedrock)
chunks.each_with_index do |text, i|
DocumentChunk.create!(
document: document, content: text, position: i,
embedding: service.embed(text),
metadata: { source: document.filename,
section: detect_section(text) }
)
end
document.update!(indexing_status: "completed",
chunk_count: chunks.size, indexed_at: Time.current)
rescue StandardError => e
document.update!(indexing_status: "failed", indexing_error: e.message)
raise
end
endAI の安全装置 -- ガードレールの実装
「AI が暴走しないための仕組みを、ソウタなら何重に作る?」
カイの問いに、ソウタは考え込んだ。保険の査定に AI を使う以上、万が一の誤判定は許されない。
# app/services/guardrail_service.rb
class GuardrailService
CONFIDENCE_THRESHOLD = 0.85
AMOUNT_REVIEW_THRESHOLD = 500_000 # 50万円以上は人間レビュー必須
PROHIBITED = [/確実に支払われます/, /絶対に/, /保証します/, /法的責任/].freeze
def validate(assessment, context: {})
violations = []
violations.concat(check_prohibited(assessment[:text]))
violations << { type: "low_confidence" } if assessment[:confidence] < CONFIDENCE_THRESHOLD
if assessment[:estimated_amount]&.> AMOUNT_REVIEW_THRESHOLD
violations << { type: "high_amount", amount: assessment[:estimated_amount] }
end
violations.concat(check_hallucination(assessment[:text], context))
AuditLog.create!(event_type: "guardrail_check", claim_id: context[:claim_id],
details: { confidence: assessment[:confidence],
violations: violations })
{ assessment: assessment[:text], confidence: assessment[:confidence],
violations: violations, requires_human_review: violations.any? }
end
private
def check_prohibited(text)
PROHIBITED.filter_map do |pat|
{ type: "prohibited_content", pattern: pat.source } if text.match?(pat)
end
end
def check_hallucination(text, context)
numbers = text.scan(/[\d,]+(?:円|万円|%)/)
ctx = context[:similar_cases]&.map(&:content)&.join(" ") || ""
numbers.filter_map do |num|
{ type: "potential_hallucination", value: num } unless ctx.include?(num.tr(",", ""))
end
end
endWARNING
エンタープライズ AI では「AI が何を言ったか」だけでなく「なぜそう言ったか」の追跡が必須だ。全ての AI 判断を AuditLog に記録し、監査・法規制対応に備える。
AI Observability と "砂利道から舗装道路へ"
本番稼働から 2 週間。ソウタは毎朝モニタリングダッシュボードを確認する習慣をつけた。
「AI システムは deploy して終わりじゃない。データの傾向、ユーザーの質問パターン、モデルの挙動 -- すべてが日々変わる」
# app/services/ai_monitoring_service.rb
class AiMonitoringService
def daily_report
convos = Conversation.where(created_at: 1.day.ago..)
evals = EvalResult.where(created_at: 1.day.ago..)
{
total_queries: convos.count,
avg_latency_ms: convos.average(:latency_ms)&.round,
avg_faithfulness: evals.average(:faithfulness)&.round(3),
low_confidence_rate: percentage(convos, "confidence < 0.85"),
human_escalation_rate: percentage(convos.where(escalated_to_human: true)),
drift_detected: detect_drift(evals)
}
end
private
def percentage(scope, condition = nil)
total = scope.count
return 0.0 if total.zero?
sub = condition ? scope.where(condition) : scope
(sub.count.to_f / total * 100).round(1)
end
def detect_drift(evals)
return false if evals.count < 50
recent = evals.order(created_at: :desc).limit(50).average(:faithfulness)
baseline = EvalResult.where(created_at: 30.days.ago..7.days.ago).average(:faithfulness)
baseline && (baseline - recent) > 0.05
end
end帝都生命のプロジェクトが軌道に乗った頃、カイがソウタを呼んだ。
「帝都生命で学んだことを、次の顧客にも活かせるようにしてほしい」
これが FDE の本質的な価値だった。最初の顧客デプロイは常に「砂利道」だ。でこぼこで回り道だらけ。しかし FDE は砂利道を走りながらパターンを見つけ、次の顧客のために道を舗装する。
# lib/templates/insurance_rag_template.rb
module Templates
class InsuranceRagTemplate
CHUNKING = { strategy: :recursive, chunk_size: 512, overlap: 64,
separators: ["条", "項", "号"] }.freeze
GUARDRAIL = { confidence_threshold: 0.85,
prohibited_patterns: [/確実/, /保証/, /絶対/],
require_human_review_for: %w[death disability] }.freeze
EVAL_SUITE = [
{ name: "基本請求", question: "入院給付金の請求に必要な書類は?",
expected_topics: %w[診断書 請求書 本人確認書類] },
{ name: "免責事由", question: "既往症がある場合、保険金は支払われますか?",
expected_topics: %w[告知義務 免責期間 条件付き] }
].freeze
def self.apply(project)
project.configure_chunking(CHUNKING)
project.configure_guardrails(GUARDRAIL)
project.load_eval_suite(EVAL_SUITE)
end
end
endINFO
FDE が顧客プロジェクトで得た知見をテンプレート化し、プラットフォームに還元する。これが「砂利道から舗装道路へ」パターンだ。1 社目は 3 ヶ月かかったデプロイが、3 社目には 3 週間で完了する。
Fine-tuning vs RAG vs プロンプトエンジニアリング
「全てを Fine-tuning で解決しようとする人が多いが、それは最後の手段だ」とカイは言う。
- プロンプトエンジニアリング -- 最も手軽。出力形式の制御、トーンの調整、Few-shot 例の追加。まずここから始める
- RAG -- 外部知識の注入。顧客固有のデータや最新情報を反映。最もコストパフォーマンスが高い
- Fine-tuning -- モデルの振る舞い自体を変える。大量の学習データと計算リソースが必要。最後の手段
エンタープライズ向けのプロンプト設計では、ガードレールを組み込んだシステムプロンプトが鍵になる。
# app/services/prompt_builder.rb
class PromptBuilder
def self.build_system_prompt(customer:, few_shot_examples: [])
base = <<~PROMPT
あなたは#{customer.name}の保険査定支援AIです。
- 参照文書の事実のみに基づいて回答する
- 「おそらく」「可能性があります」等の曖昧表現を使い断定を避ける
- 最終的な査定判断は行わず参考情報の提示に留める
- 個人情報を回答に含めない
- 金額は参照文書を正確に引用し独自の計算をしない
PROMPT
if few_shot_examples.any?
base += "\n## 回答例\n"
few_shot_examples.each { |ex| base += "Q: #{ex[:question]}\nA: #{ex[:answer]}\n\n" }
end
base
end
end技術と現実の翻訳者
プロジェクト開始から 4 ヶ月。帝都生命の AI 査定アシスタントは本番稼働を迎えた。
査定担当者の一人が言った。「最初は AI なんて信用できないと思っていた。でも今は、このツールがないと仕事にならない」
- 1 件あたりの査定処理時間: 45 分から 12 分に短縮
- 査定担当者の満足度: 60% から 93% に向上
- AI の回答精度(Eval スコア): faithfulness 0.92, relevance 0.88
「すごいですね。でも、これは ML の精度が良かったからじゃなくて……」
「そうだ。帝都生命の業務を理解して、エッジケースを一つずつ潰して、ガードレールを設計して、Eval を回し続けた結果だ」
カイはコーヒーカップを置いて言った。
「AI 時代の FDE は、技術と現実の翻訳者だ。最先端のモデルを知っていることは前提。でも本当の価値は、そのモデルを顧客の泥臭い現実で動かし続けられることにある」
ソウタは帝都生命の査定担当者たちの顔を思い出した。最初は懐疑的だった彼らが、今では AI アシスタントを「相棒」と呼んでいる。
INFO
AI FDE に求められるのは、ML リサーチャーの深さではなく、「顧客の業務 x AI 技術 x プロダクション品質」を三位一体で回せる力だ。PoC のデモではなく、現場で毎日使われ続けるシステムを作る。それが AI 時代の FDE の仕事だ。
まとめ -- AI FDE の技術マップ
AI の PoC は誰でも作れる時代になった。しかし、それを本番で動かし続け、顧客のビジネスに本当の価値を届けるには、FDE のような存在が不可欠だ。次章では、FDE のキャリアパスと成長戦略について掘り下げていく。