LLMのガードレール実践ガイド:実際に効果的な手法とは

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

目次

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

重要なのは、どのガードレールが本質的であり、どれが単なるノイズであるかを理解することです。

ガードレールはモデルを制御するためのものではありません。リスクを制御するためのものです。

LLM guardrails in practice

入力検証

最も重要なガードレールです。入力が悪いと出力も悪くなり、さらに悪い入力はプロンプトインジェクションのきっかけとなる可能性があります。

戦略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"

単純な文字列マッチングは高速ですが不正確です。本番環境では、ポリシー違反を検出するために分類モデル(qwen3-1.7bのような小規模モデルでも可)を使用します。これにより、精度が向上し、回避されにくくなります。

出力フィルタリング

モデルの出力もチェックが必要です。構造、コンテンツ、事実の観点から検証します。

戦略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: トークン予算管理

トークン予算管理は、1リクエストあたりのコストを上限設定します:

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")

監査ログはデバッグとコンプライアンスに不可欠です。構造化された、追記専用のログとし、安全に保存します。

組み合わせる

パターン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エンジニアリングの新記事をお届けします。