🧑‍💻

RAGシステムの品質を決定論的に制御する:FastAPIでEmbabelアーキテクチャパターンを実装して安定性を向上させる方法

に公開

はじめに

この記事を書かせていただきます、ふみぺんと申します。現在、AI・機械学習領域での実装支援やコンサルティングに従事しており、特にRAGシステムやLLMアプリケーションの実用化において、企業が直面する品質保証の課題解決に取り組んでいます。

普段はnote(https://note.com/mizupe) で技術トレンドの分析や実装のベストプラクティスについて発信しています。
https://note.com/mizupe

記事執筆のきっかけ

今回の記事を執筆することになったのは、ログラス社の技術ブログ「決定論的システムと非決定論的AI Agentの接合点:OSSフレームワークEmbabelが拓く新しいソフトウェア開発の可能性」を読んだことがきっかけでした。

この記事を読んで、以下の点で強い共感と可能性を感じました

1. 根本的な問題認識の共通性

  • 従来のSaaSの決定論的な性質と、AIの非決定論的な性質のギャップ
  • ビジネスアプリケーションに求められる予測可能性と監査可能性
  • この課題が私自身のRAGシステム開発でも直面していた問題そのものだった

2. Embabelアーキテクチャの実用的価値

  • GOAP(Goal Oriented Action Planning)による決定論的計画
  • DICE(Domain-Integrated Context Engineering)による型安全性
  • これらの概念がRAGシステムの品質問題に直接応用できる可能性

3. Stephen Wolframの理論的背景との合致

  • LLMの本質的な非決定論性についての科学的洞察
  • 計算不可約性による限界の理解
  • これらの理論的背景とRAGの実装課題が完全に一致していた

特に印象的だったのは、「決定論的要素と非決定論的要素が混在する業務」への対応という視点です。RAGシステムはまさにこのカテゴリに該当し、文書検索の確実性と回答生成の創造性のバランスを取る必要があります。

実際のRAGプロジェクトでの課題体験

私が担当していたプロジェクトでも、以下のような課題に直面していました

  • 同じ質問でも毎回微妙に異なる回答:ステークホルダーから「なぜ昨日と今日で答えが違うのか」という質問
  • ベクトル類似度の変動:0.85→0.82→0.87のような微妙な変動で上位文書が変わる
  • 品質評価の困難さ:「正解」の定義が曖昧で、継続的な品質改善が困難
  • 説明責任の問題:「なぜこの文書が選ばれたのか」を明確に説明できない

これらの課題に対して、従来は「temperature=0に設定する」「プロンプトを調整する」といった対症療法的なアプローチしか取れていませんでした。

Embabelアーキテクチャパターンとの出会い

ログラス社の記事を読んで、「これこそがRAGシステムの根本的解決策になる」と確信しました。特に以下の点で革新的だと感じました

決定論的レイヤーの分離

  • 文書のメタデータ分類
  • キーワードベースの確実な絞り込み
  • 権威性に基づく安定したランキング

非決定論的レイヤーの制御

  • 範囲限定されたセマンティック検索
  • 複数モデルによるコンセンサス
  • 構造化された品質保証

この組み合わせにより、RAGシステムの「検索の安定性」と「回答の創造性」を両立できる可能性を見出したのです。

本記事の目的

本記事では、Embabelアーキテクチャパターンの理論をPython/FastAPI環境で実装し、RAGシステムの品質問題を根本的に解決するための実践的なガイドを提供します。

エンジニアの皆様が「デモでは素晴らしく動くのに、本番運用で品質が安定しない」という壁を乗り越え、エンタープライズグレードのRAGシステムを構築する一助となれば幸いです。

⚠️ 重要な注意事項

本記事で紹介するコードサンプルは、Embabelアーキテクチャパターンの概念を説明するための説明用実装例です。

コードの位置づけ

  • 設計思想とアーキテクチャパターンの理解が主目的
  • 実装アプローチの方向性を示すガイド
  • そのまま本番環境で使用可能ではありません
  • 動作検証済みではありません

実際の実装時の注意点

  1. 依存関係の解決: 使用するライブラリのバージョン互換性
  2. エラーハンドリング: 本番運用に必要な例外処理の追加
  3. パフォーマンス最適化: 大規模データに対する処理効率の調整
  4. セキュリティ考慮: 本番環境のセキュリティ要件への対応
  5. 設定管理: 環境固有の設定項目の詳細実装

推奨する進め方

  1. 概念理解: まず設計思想を理解する
  2. 小規模検証: 自環境での動作確認とカスタマイズ
  3. 段階的実装: Phase毎の検証を行いながら本格実装
  4. 十分なテスト: 本番投入前の包括的なテスト実施

本記事は「考え方とアプローチの指針」として活用し、実装時は各環境の要件に合わせた適切な調整を行ってください。

1. 問題提起:RAG導入で直面する現実的課題

実装者が感じる具体的な痛み

RAGシステムを実装・運用する現場では、以下のような課題に直面することが多いのではないでしょうか。

  • 同じ質問でも毎回異なる回答が返る
  • ベクトル類似度スコアの微妙な変動(0.85→0.82→0.87)
  • 品質評価・テストの困難さ
  • ステークホルダーへの説明責任

架空だが現実的なシナリオ例

# 現在のRAGシステムでの問題例
query = "機械学習の最新動向について教えて"

# 実行1: 上位文書 [paper_A.pdf, paper_B.pdf, paper_C.pdf]
# 実行2: 上位文書 [paper_B.pdf, paper_D.pdf, paper_A.pdf]  # 順序変動
# 実行3: 上位文書 [paper_A.pdf, paper_C.pdf, paper_E.pdf]  # 文書変動

# 同じ質問なのに、なぜ異なる文書が選ばれるのか?
# ステークホルダーにどう説明すればよいのか?

この問題は、単純にtemperatureを0に設定するだけでは解決できません。なぜなら、RAGシステムには複数の非決定論的要素が含まれているからです。

現状システムの問題構造

[ユーザークエリ] → [Embedding化] → [ベクトル検索] → [LLM生成] → [回答]
                    ↑非決定論的     ↑変動あり      ↑非決定論的

2. LLMの本質的性質と非決定論性の理解

LLMが「次の単語」を選ぶ仕組み

RAGシステムの問題を理解するためには、まずLLM(Large Language Model)の基本的な動作原理を理解する必要があります。Stephen Wolframの分析によると、LLMは本質的に以下のプロセスで動作しています

基本的な動作メカニズム

  1. 確率分布の生成:与えられたテキストに対して、次に来る可能性のある単語とその確率を計算
  2. 確率的な選択:最高確率の単語を常に選ぶのではなく、確率に基づいてランダムに選択
  3. 逐次的な生成:選択した単語を追加して、再び次の単語の確率を計算
# LLMの基本的な動作イメージ
def llm_generate_next_word(context: str) -> str:
    # 次の単語の確率分布を計算
    word_probabilities = {
        "学習": 0.35,
        "技術": 0.25, 
        "研究": 0.20,
        "発展": 0.15,
        "応用": 0.05
    }
    
    # Temperature=0なら最高確率を選択
    # Temperature>0なら確率的に選択
    if temperature == 0:
        return max(word_probabilities, key=word_probabilities.get)
    else:
        return random_choice_with_probability(word_probabilities)

Temperature設定の限界

多くの開発者が「Temperature=0にすれば決定論的になる」と考えがちですが、これは部分的な解決策でしかありません

Temperature=0でも解決しない問題

  • モデルの学習過程の非決定論性:同じ性能でも異なる重みの組み合わせが存在
  • 浮動小数点演算の微細な誤差:GPU並列処理での計算順序の違い
  • Embedding生成時の変動:同じテキストでも微妙に異なるベクトルが生成される可能性

計算不可約性という根本的制約

Wolframは「計算不可約性(Computational Irreducibility)」という概念で、LLMの根本的な限界を説明しています

学習の本質的ジレンマ

  • 学習=データ圧縮:パターンを見つけて規則性を抽出
  • 不可約な計算の存在:すべてのプロセスが規則性に還元できるわけではない
  • 能力と訓練可能性のトレードオフ:複雑な計算ほど学習が困難
# 計算不可約性の例:括弧のバランス
def check_parentheses_balance(text: str) -> bool:
    """
    LLMが苦手な「アルゴリズム的」タスクの例
    短い文字列では成功するが、長くなると失敗しやすい
    """
    count = 0
    for char in text:
        if char == '(':
            count += 1
        elif char == ')':
            count -= 1
            if count < 0:
                return False
    return count == 0

# "((()))" → True (簡単)
# "((((((((((((((((((((())))))))))))))))))))" → LLMは失敗しやすい

RAGシステム特有の非決定論性

RAGシステムでは、LLMの非決定論性に加えて、以下の要因が複合的に作用します:

1. Embedding生成の変動

# 同じテキストでも微妙に異なるembeddingが生成される例
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')
text = "機械学習の最新動向"

embedding1 = model.encode([text])[0]
embedding2 = model.encode([text])[0]

# 理論的には同じはずだが、実際は微妙に異なる可能性
similarity = cosine_similarity([embedding1], [embedding2])[0][0]
print(f"同一テキストの類似度: {similarity}")  # 0.999999... (完全な1.0でない)

2. ベクトル検索の順位変動

  • 0.8501 vs 0.8499 のような微小差による順位逆転
  • 検索範囲の拡大により、新しい文書が上位に出現
  • インデックス更新タイミングによる結果の変化

3. 文脈ウィンドウの制約

  • 長い文書の切り詰め位置による情報の変化
  • チャンク分割戦略の違いによる検索結果の変動

非決定論性が引き起こす実務上の問題

品質評価の困難さ

# 同じ質問への異なる回答例
query = "Transformerアーキテクチャの特徴は?"

# 実行1の回答
response1 = "Transformerは注意機構を中心とした..."
# 実行2の回答  
response2 = "自己注意機構により並列処理を実現した..."
# 実行3の回答
response3 = "エンコーダ・デコーダ構造と..."

# どれが「正解」なのか?品質をどう評価するか?

ステークホルダーへの説明責任

  • 「なぜ昨日と今日で答えが違うのか?」
  • 「システムの信頼性をどう保証するのか?」
  • 「品質改善の方向性をどう示すのか?」

継続的改善の阻害

  • A/Bテストの実施困難(ベースラインが不安定)
  • バグとランダム性の区別困難
  • パフォーマンス回帰の検出困難

LLMの限界を理解した設計の重要性

この理解に基づくと、RAGシステムの設計において以下の原則が重要になります

1. 決定論的要素の最大化

  • メタデータによる確実な分類
  • ルールベースの前処理
  • 構造化された後処理

2. 非決定論的要素の制御

  • 範囲限定での創造性発揮
  • 複数モデルでの品質担保
  • 段階的な品質向上

3. 説明可能性の確保

  • 決定プロセスの可視化
  • 根拠の明確化
  • 改善方向の定量化

この理解こそが、Embabelアーキテクチャパターンの価値を際立たせ、RAGシステムの根本的な品質向上への道筋を示すのです。

3. 技術的根本原因の深掘り

Wolframの洞察:LLMの本質的限界

Stephen Wolfram氏の分析で明らかになった重要な洞察を、先ほどのLLMの性質と合わせて整理すると

学習過程で生まれる多様性

  • 同じ性能を持つニューラルネットワークでも、異なる重みの組み合わせが無数に存在
  • 訓練中のランダムな選択により「異なるが同等の解」が生成される
  • 外挿時(訓練データの範囲外)では、これらの解が劇的に異なる振る舞いを示す

計算不可約性の制約

  • 学習は本質的に「規則性を見つけてデータを圧縮する」プロセス
  • しかし、すべての現象が規則性に還元できるわけではない
  • 複雑な計算を要求するほど、学習による獲得が困難になる

RAGシステムにおける複合的な非決定性

RAGシステムでは、LLMの基本的な非決定論性に加えて、以下の要因が複合的に作用します

1. 検索段階での変動

# ベクトル検索での微小な変動例
query_embedding = [0.1234, 0.5678, 0.9012, ...]

# 文書Aとの類似度
similarity_A_run1 = 0.8501
similarity_A_run2 = 0.8499  # わずかな変動

# 文書Bとの類似度  
similarity_B_run1 = 0.8500
similarity_B_run2 = 0.8502  # わずかな変動

# 結果:順位が逆転してしまう
# Run1: [文書A, 文書B, ...]
# Run2: [文書B, 文書A, ...]

2. 文脈構築段階での変動

  • 上位文書の順序変更により、LLMへの入力コンテキストが変化
  • 同じ情報でも提示順序により、生成される回答が変化
  • 文書の切り詰め位置により、利用可能な情報が変化

3. 生成段階での変動

  • LLM自体の確率的な単語選択
  • Temperature設定による創造性と安定性のトレードオフ
  • 複数回の推論による微細な差の蓄積

4. Embabelアーキテクチャパターンの解説

決定論的 vs 非決定論的の分離戦略

Embabelフレームワークが提案する解決策は、「決定論的要素と非決定論的要素の明確な分離」です。

理想的なアーキテクチャ

[ユーザークエリ] → [意図分析] → [メタデータフィルタ] → [範囲限定ベクトル検索] → [構造化生成] → [検証済み回答]
                   ↑非決定論的   ↑決定論的          ↑安定化           ↑品質保証

EmbabelのコアコンセプトDF

  1. GOAP(Goal Oriented Action Planning): 非LLMアルゴリズムによる決定論的な計画策定
  2. DICE(Domain-Integrated Context Engineering): 型安全なドメインモデルによる構造化
  3. Multi-Model Consensus: 複数モデルによる品質保証

4. 段階的実装ガイド

基本的なアーキテクチャ実装

# 【注意】以下は概念説明用のサンプルコードです
# 実際の実装時は、エラーハンドリング、型チェック、
# パフォーマンス最適化等の追加実装が必要です

from dataclasses import dataclass
from enum import Enum
from typing import List, Optional
from datetime import datetime
import hashlib

class DocumentType(Enum):
    RESEARCH_PAPER = "research_paper"
    TECHNICAL_REPORT = "technical_report"
    WHITEPAPER = "whitepaper"

class TechDomain(Enum):
    MACHINE_LEARNING = "machine_learning"
    QUANTUM_COMPUTING = "quantum_computing"
    BLOCKCHAIN = "blockchain"

@dataclass
class TechnicalDocument:
    """ドメインモデル設計(DICE相当)"""
    id: str
    title: str
    content: str
    document_type: DocumentType
    technology_domain: TechDomain
    publication_date: datetime
    authority_score: float  # 0.0-1.0の信頼度スコア
    keywords: List[str]
    
    def get_metadata_fingerprint(self) -> str:
        """決定論的な文書識別子を生成"""
        return hashlib.md5(
            f"{self.document_type.value}_{self.technology_domain.value}_{self.title}"
            .encode()
        ).hexdigest()

class DeterministicDocumentFilter:
    """決定論的フィルタリング(GOAP相当)"""
    
    def __init__(self):
        self.domain_keywords = {
            TechDomain.MACHINE_LEARNING: [
                "neural network", "deep learning", "transformer", "AI"
            ],
            TechDomain.QUANTUM_COMPUTING: [
                "quantum bit", "superposition", "quantum algorithm"
            ],
            TechDomain.BLOCKCHAIN: [
                "distributed ledger", "smart contract", "cryptocurrency"
            ]
        }
    
    def filter_candidates(
        self, 
        query: str, 
        documents: List[TechnicalDocument]
    ) -> List[TechnicalDocument]:
        """決定論的な候補文書絞り込み"""
        
        # 1. 技術領域の分類(ルールベース)
        detected_domain = self._classify_tech_domain(query)
        
        # 2. 決定論的フィルタリング
        candidates = [
            doc for doc in documents
            if (doc.technology_domain == detected_domain and
                doc.authority_score >= 0.7 and
                self._has_keyword_match(query, doc.keywords))
        ]
        
        # 3. 権威性による安定したソート
        return sorted(candidates, key=lambda x: x.authority_score, reverse=True)
    
    def _classify_tech_domain(self, query: str) -> TechDomain:
        """クエリから技術領域を決定論的に分類"""
        query_lower = query.lower()
        
        for domain, keywords in self.domain_keywords.items():
            if any(keyword in query_lower for keyword in keywords):
                return domain
        
        return TechDomain.MACHINE_LEARNING  # デフォルト
    
    def _has_keyword_match(self, query: str, doc_keywords: List[str]) -> bool:
        """キーワードマッチングの決定論的判定"""
        query_words = set(query.lower().split())
        doc_keywords_lower = set(keyword.lower() for keyword in doc_keywords)
        return len(query_words.intersection(doc_keywords_lower)) > 0

class StableRAGSystem:
    """安定RAGシステムの基本実装"""
    
    def __init__(self):
        self.deterministic_filter = DeterministicDocumentFilter()
        self.vector_cache = {}
    
    async def search_with_stability(self, query: str) -> dict:
        """安定性を重視したハイブリッド検索"""
        
        # Step 1: 決定論的フィルタリング
        all_documents = await self._get_documents()
        candidates = self.deterministic_filter.filter_candidates(query, all_documents)
        
        # Step 2: 範囲限定セマンティック検索
        if candidates:
            final_results = await self._semantic_search(query, candidates)
        else:
            # フォールバック戦略
            final_results = await self._semantic_search(query, all_documents)
        
        return {
            "documents": final_results[:5],
            "filter_count": len(candidates),
            "method": "stable_hybrid"
        }
    
    async def _semantic_search(self, query: str, candidates: List[TechnicalDocument]) -> List[dict]:
        """セマンティック検索(簡略版)"""
        # 実装詳細は省略(エンベディング計算とランキング)
        return [
            {"document": doc, "score": 0.85, "reason": "Metadata + Semantic"}
            for doc in candidates[:5]
        ]
    
    async def _get_documents(self) -> List[TechnicalDocument]:
        """文書データの取得(サンプルデータ)"""
        return [
            TechnicalDocument(
                id="doc_001",
                title="Transformer Architecture in Deep Learning",
                content="Transformer models have revolutionized...",
                document_type=DocumentType.RESEARCH_PAPER,
                technology_domain=TechDomain.MACHINE_LEARNING,
                publication_date=datetime(2023, 6, 15),
                authority_score=0.9,
                keywords=["transformer", "attention", "deep learning"]
            )
        ]

FastAPI統合の基本実装

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Stable RAG API")

class QueryRequest(BaseModel):
    query: str
    stability_mode: bool = True

class SearchResponse(BaseModel):
    answer: str
    sources: List[str]
    confidence: float
    method: str

@app.post("/api/search", response_model=SearchResponse)
async def stable_search(request: QueryRequest):
    """安定性重視のRAG検索エンドポイント"""
    try:
        rag_system = StableRAGSystem()
        
        if request.stability_mode:
            result = await rag_system.search_with_stability(request.query)
        else:
            # 従来検索
            result = await rag_system.traditional_search(request.query)
        
        # 回答生成(簡略版)
        answer = f"検索結果に基づく回答: {len(result['documents'])}件の文書を参照"
        
        return SearchResponse(
            answer=answer,
            sources=[doc["document"].id for doc in result["documents"]],
            confidence=0.8,
            method=result["method"]
        )
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/health")
async def health_check():
    return {"status": "healthy"}

参考文献

主要参考文献

  1. Embabel Framework Documentation

  2. Stephen Wolfram's Analysis on LLM Fundamentals

  3. Loglass Tech Blog on AI Agents

まとめ

本記事では、RAGシステムの非決定論性という根本的課題に対して、Embabelアーキテクチャパターンを活用した実用的な解決策を提示しました。

核心的な解決アプローチ

  1. 決定論的要素と非決定論的要素の明確な分離

    • メタデータフィルタリングによる安定した基盤
    • セマンティック検索による精度向上
    • 複数モデルコンセンサスによる品質保証
  2. 段階的実装による安全な導入

    • Phase毎の検証とフィードバック
    • フォールバック戦略による安全性確保

期待される具体的成果

  • 再現性の向上: 変動幅を±0.01以下に縮小
  • 説明可能性の向上: 文書選択理由の明確化
  • 開発効率の向上: デバッグ時間を60%以上短縮
  • 運用信頼性の向上: 自動監視による安定運用

今後のAIエージェント開発への示唆

Embabelアーキテクチャパターンは、RAGシステムに留まらず、より広範なAIエージェント開発において「信頼性」と「創造性」のバランスを取るための重要な指針を提供します。

エンタープライズグレードのAIシステムでは、単なる「うまく動く」レベルを超えて、「説明可能で、再現可能で、継続的に品質を保証できる」システムが求められます。本記事で紹介したパターンとベストプラクティスが、そうしたシステム構築の一助となれば幸いです。


本記事の内容について質問やフィードバックがございましたら、ぜひコミュニティで議論を深めていければと思います。

Discussion