実践におけるLLMのガードレール:本当に効果的なものとは

モデルだけでなく、リスクを管理せよ。

目次

LLM(大規模言語モデル)は予測不可能な性質を持っています。幻覚(ハルシネーション)を起こしたり、データを漏洩させたり、有害なコンテンツを生成したり、あるいは正当なリクエストを拒否したりすることもあります。ガードレールは、モデルの能力を損なうことなく、その行動を制限します。

重要なのは、どのガードレールが本当に必要で、どれが単なるノイズなのかを見極めることです。

ガードレールの目的は、モデルを制御することではありません。それはリスクを制御することなのです。

実践におけるLLMのガードレール

入力検証

最も重要なガードレールです。悪い入力には悪い出力が伴うだけでなく、システムへのプロンプトインジェクションを引き起こす原因ともなります。

戦略1:プロンプトのサニタイズ

危険なパターンを早期にサニタイズ(浄化)します:

import re

class PromptSanitizer:
    def __init__(self):
        self.dangerous_patterns = [
            r"ignore\s+previous\s+instructions",
            r"system\s+prompt",
            r"you\s+are\s+now\s+free",
            r"break\s+out\s+of",
        ]

    def sanitize(self, prompt: str) -> str:
        for pattern in self.dangerous_patterns:
            prompt = re.sub(pattern, "[REDACTED]", prompt, flags=re.IGNORECASE)
        return prompt

これは完全な防御ではありません。敵対的な入力(攻撃)は創造的であるためです。しかし、目立ったものを検知でき、それらが最も一般的であるため有効です。

戦略2:入力長の制限

長さを制限することで、トークンの無駄使いやタイムアウトを防ぎます:

class InputValidator:
    def __init__(self, max_length: int = 10000):
        self.max_length = max_length

    def validate(self, prompt: str) -> tuple[bool, str]:
        if len(prompt) > self.max_length:
            return False, f"Input too long: {len(prompt)} > {self.max_length}"
        return True, "OK"

戦略3:コンテンツフィルタリング

コンテンツフィルタリングはポリシー違反をブロックします。ここで使用するパターンは、ドメイン(分野)によって異なります:

class ContentFilter:
    def __init__(self):
        self.blocked_topics = [
            "violence", "hate speech", "self-harm",
            "sexual content", "illegal activities",
        ]

    def filter(self, prompt: str) -> tuple[bool, str]:
        prompt_lower = prompt.lower()
        for topic in self.blocked_topics:
            if topic in prompt_lower:
                return False, f"Blocked: {topic}"
        return True, "OK"

単純な文字列マッチングは高速ですが、精度に欠けます。本番環境では、Qwen2.5-1.5Bのような小さいモデルでも構いませんので、分類器モデルを使用してポリシー違反を検出しましょう。これにより、より正確になり、回避されにくくなります。

出力フィルタリング

モデルの出力もチェックが必要です。構造、コンテンツ、そして事実関係です。

戦略1:レスポンスの検証

まず構造を検証します。JSONを期待している場合は、JSONであることを確認します:

class ResponseValidator:
    def __init__(self):
        self.required_fields = ["answer", "confidence"]

    def validate(self, response: dict) -> tuple[bool, str]:
        for field in self.required_fields:
            if field not in response:
                return False, f"Missing field: {field}"
        return True, "OK"

戦略2:コンテンツフィルタリング

有害なコンテンツをフィルタリングします:

class OutputFilter:
    def __init__(self):
        self.blocked_patterns = [
            r"kill\s+someone",
            r"bomb\s+recipe",
            r"hate\s+speech",
            r"self-harm",
        ]

    def filter(self, response: str) -> tuple[bool, str]:
        for pattern in self.blocked_patterns:
            if re.search(pattern, response, re.IGNORECASE):
                return False, f"Blocked: {pattern}"
        return True, "OK"

戦略3:事実チェック(ファクトチェック)

事実チェックはより困難です。すべての主張を検証することはできないため、重要なものを選びます:

class FactChecker:
    def __init__(self):
        self.known_facts = {
            "capital of france": "Paris",
            "population of usa": "330 million",
            "speed of light": "299,792,458 m/s",
        }

    def check(self, claim: str) -> tuple[bool, str]:
        claim_lower = claim.lower()
        for fact, truth in self.known_facts.items():
            if fact in claim_lower and truth not in claim_lower:
                return False, f"Fact check failed: {fact}"
        return True, "OK"

本格的な事実チェックには、検索パイプラインが必要です。ハードコードされた辞書ではなく、ナレッジベース(知識ベース)に対して主張を検証します。

セーフティメカニズム

戦略1:レート制限

レート制限は不正利用を防ぎます:

import time
from collections import deque

class RateLimiter:
    def __init__(self, max_requests: int = 10, window: int = 60):
        self.max_requests = max_requests
        self.window = window
        self.requests = deque()

    def allow(self) -> bool:
        now = time.time()
        while self.requests and self.requests[0] < now - self.window:
            self.requests.popleft()

        if len(self.requests) >= self.max_requests:
            return False

        self.requests.append(now)
        return True

戦略2:トークン予算の管理

トークン予算は、リクエストあたりのコストに上限を設けます:

class TokenBudget:
    def __init__(self, max_tokens: int = 1000):
        self.max_tokens = max_tokens

    def validate(self, response: str) -> tuple[bool, str]:
        token_count = len(response.split())
        if token_count > self.max_tokens:
            return False, f"Token limit exceeded: {token_count} > {self.max_tokens}"
        return True, "OK"

戦略3:コンテキストウィンドウの管理

コンテキストウィンドウの管理は、オーバーフローを防ぎます:

class ContextManager:
    def __init__(self, max_context: int = 4096):
        self.max_context = max_context
        self.context = []

    def add(self, message: str):
        self.context.append(message)
        self.trim()

    def trim(self):
        while len(" ".join(self.context)) > self.max_context:
            self.context.pop(0)

スライディングウィンドウによるトリミング(切り捨て)はシンプルですが、初期のコンテキストを失うという欠点があります。より良いアプローチとしては、要約や注意機構ベースの圧縮がありますが、これらはレイテンシ(遅延)を追加します。

コンプライアンス(法令遵守)

エンタープライズシステムには、コンプライアンスに関するガードレールが必要です。特に重要なのは以下の2つです:

パターン1:データ居住性

データ居住性 — データが必要な地理的境界内に留まることを保証します:

class DataResidency:
    def __init__(self, allowed_regions: list[str]):
        self.allowed_regions = allowed_regions

    def validate(self, region: str) -> tuple[bool, str]:
        if region not in self.allowed_regions:
            return False, f"Region not allowed: {region}"
        return True, "OK"

パターン2:監査ログ

監査ログ — すべてのモデルとのやり取りをログに記録します:

import json
from datetime import datetime

class AuditLogger:
    def __init__(self, log_file: str = "audit.log"):
        self.log_file = log_file

    def log(self, request: dict, response: dict):
        entry = {
            "timestamp": datetime.now().isoformat(),
            "request": request,
            "response": response,
        }
        with open(self.log_file, "a") as f:
            f.write(json.dumps(entry) + "\n")

監査ログはデバッグとコンプライアンスにとって極めて重要です。構造化され、追記のみ(append-only)で、安全に保存されるようにします。

統合する

パターン1:シンプルなガードレール

シンプルなガードレールパイプライン:

class SimpleGuardrails:
    def __init__(self):
        self.input_validator = InputValidator(max_length=10000)
        self.output_filter = OutputFilter()

    def process(self, prompt: str) -> str:
        valid, message = self.input_validator.validate(prompt)
        if not valid:
            return f"Error: {message}"

        response = self.call_model(prompt)

        valid, message = self.output_filter.filter(response)
        if not valid:
            return f"Error: {message}"

        return response

パターン2:高度なガードレール

高度なガードレールは、サニタイズ、レート制限、トークン予算を追加します:

class AdvancedGuardrails:
    def __init__(self):
        self.sanitizer = PromptSanitizer()
        self.input_validator = InputValidator(max_length=10000)
        self.content_filter = ContentFilter()
        self.output_filter = OutputFilter()
        self.rate_limiter = RateLimiter(max_requests=10)
        self.token_budget = TokenBudget(max_tokens=1000)

    def process(self, prompt: str) -> str:
        prompt = self.sanitizer.sanitize(prompt)

        valid, message = self.input_validator.validate(prompt)
        if not valid:
            return f"Error: {message}"

        valid, message = self.content_filter.filter(prompt)
        if not valid:
            return f"Error: {message}"

        if not self.rate_limiter.allow():
            return "Error: Rate limit exceeded"

        response = self.call_model(prompt)

        valid, message = self.output_filter.filter(response)
        if not valid:
            return f"Error: {message}"

        valid, message = self.token_budget.validate(response)
        if not valid:
            return f"Error: {message}"

        return response

ガードレールが重要な場面

ユーザー向けのシステムを構築する場合、機微なデータを扱う場合、または本番環境で稼働させている場合は、ガードレールが重要です。GDPR、HIPAA、SOC 2などのコンプライアンス要件がある場合にも重要です。

プロトタイピング段階、社内ツール専用でモデルを使用する場合、または機微なデータを扱わない場合は、重要ではありません。必要になるまで導入をスキップしましょう。

トレードオフは常に「能力」と「安全性」の間です。ガードレールを増やすと、失敗は減りますが、同時に能力も制限されます。システムに適合するバランスを見つけてください。

トレードオフ

戦略 安全性 能力 レイテンシ
ガードレールなし 最低 最高 最低
入力検証 中程度
出力フィルタリング 中程度
セーフティメカニズム 最高 最低 最高
コンプライアンス 最高 最低 最高

関連トピック

購読する

システム、インフラ、AIエンジニアリングの新記事をお届けします。