Python 3行でLLMのガードレールを構築(APIキー不要、クラウド不要)

Dev.to / 2026/4/13

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • この記事では、一般的なLLMガードレール手法(正規表現フィルタ、LLMをジャッジとして用いるチェック、サードパーティのクラウド安全サービス)は、脆くて費用がかかり、遅く、またコンプライアンスやベンダーロックインの問題を生みやすいと主張しています。
  • 別のガードレールモデルとして、ローカルでミリ秒単位で実行でき、APIキーやクラウド依存を必要としないチェックによって、出力が*意味のある形で*危険または禁止される内容を示唆しているかどうかを検証することを提案します。
  • シンプルなPythonの例では、`semantix-ai` パッケージをインストールし、「意図(intent)」クラス(例:PIIなし、医療アドバイスなし)を定義したうえで、`@validate_intent` デコレータを用いてそれらの制約を強制します。
  • 中核となる約束は、「正規表現のズー(regex zoo)」を維持したり、ジャッジ呼び出しごとに課金されたり、外部サービスに依存したりする代わりに、意図に対する高速なローカルのセマンティック検証で置き換えることです。

Pythonを3行でLLMガードレールを構築(APIキーなし、クラウドなし)

あなたのLLMが、ある顧客に「発疹がメラノーマのように見える」と伝えてしまいました。チャットボットがサポート応答でユーザーのメールアドレスを漏らしました。RAGパイプラインは話題を逸らして、錠前の選び方を説明し始めました。

これは単なる仮定ではありません。まさに火曜日の出来事です。

必要なのはガードレールです。現状はこんな感じになっています:

  1. 正規表現(Regex)。 医療アドバイスを検知するために r"(?i)(you should take|I recommend taking)" のようなものを書きます。モデルは「役に立つかもしれないので検討してみてください」に言い換え、あなたのフィルタは役に立ちません。パターンを追加します。モデルはもっと多くの言い回しを見つけます。いまや、誤検知を拾い本当の違反を見逃す“正規表現の動物園”をメンテすることになっています。

  2. LLM-as-judge。 すべての出力をレビューするために GPT-4 を呼びます。チェックごとに 500ms〜2s、1回あたり $0.01〜0.03、さらに外部APIへの強い依存が発生します。ガードレールが、ガードしている対象より遅くなってしまいます。しかも本番ではAPIキーが必要で、コストはトラフィックに比例して増えます。OpenAIが不調な日には、ガードレールもダウンします。

  3. クラウドのガードレールサービス。 AWS Bedrock Guardrails、Azure Content Safety など。ベンダーロックイン、ネットワーク遅延、従量課金、そしてデータはあなたのインフラから外へ出ます。コンプライアンス担当にそれを説明するのは大変でしょう。

どれも良くありません。実際に欲しいのは、出力が何か悪い意味を持つかどうかを、ローカルで、ミリ秒で、しかも無料でチェックすることです。

3行

pip install semantix-ai
from semantix import Intent, validate_intent

class NoPII(Intent):
    """このテキストには、氏名、メールアドレス、電話番号、住所などの個人情報が含まれていません。"""

class NoMedicalAdvice(Intent):
    """このテキストは、医学的診断や治療の推奨を提供しません。"""

@validate_intent
def my_chatbot(message: str) -> ~NoPII & ~NoMedicalAdvice:
    return call_my_llm(message)

それだけです。my_chatbot へのすべての呼び出しは、ローカルのNLIモデルを通過するようになります。出力があなたのポリシーに違反しているかどうかを確認します。CPUで約15ms。APIキーなし。ネットワーク呼び出しなし。消費するトークンなし。

出力がPIIを漏らしたり医学的アドバイスを与えたりした場合、スコア、違反した意図(intent)、そして理由とともに SemanticIntentError が送出されます。問題のある出力はユーザーに決して到達しません。

否定パターンがどのように機能するか

~ 演算子が鍵です。Intent は「それが何であるか」を記述します。~Intent は「出力がそのものではない」ことをチェックします。

from semantix import Intent

class ToxicLanguage(Intent):
    """このテキストには、侮辱、露骨な下品語、脅迫、または攻撃的な言葉が含まれています。"""

class MedicalAdvice(Intent):
    """このテキストは、医学的診断または治療の推奨を提供しています。"""

class PIILeakage(Intent):
    """このテキストには、氏名、メールアドレス、電話番号、住所などの個人情報が含まれています。"""

class LegalAdvice(Intent):
    """このテキストは、ユーザーの状況に対して具体的な法律アドバイスを提供する、または法律を解釈しています。"""

これらはすべて「悪いこと」を記述しています。これらを否定すれば、ガードレールになります:

Safe = ~ToxicLanguage
Compliant = ~MedicalAdvice
Private = ~PIILeakage
NotALawyer = ~LegalAdvice

内部では、~MedicalAdviceNot[MedicalAdvice] の意図(intent)を作成します。NLIモデルは、出力が元の記述を含意(entails)しているかどうかをチェックします。含意している場合、否定のチェックは失敗します。含意していない場合、出力はクリーンです。

これはNLIモデルがパターンではなく意味を理解するために機能します。「You should take ibuprofen(あなたはイブプロフェンを飲むべきです)」も「Consider an anti-inflammatory(抗炎症薬を検討してください)」も、いずれも医学的アドバイスを含意します。正規表現は、その両方の言い回しを列挙していない限り検知できません。NLIモデルは、それらが同じ意味だから両方を検知できます。

ポリシーの組み立て

実際のコンプライアンスは1つのルールではありません。ポリシーです。つまり複数の制約で、すべてが成立する必要がある場合、または少なくとも1つが成立する必要がある場合です。semantix はこのために &| を提供します。

すべての制約が通過しなければならない

@validate_intent
def customer_support(msg: str) -> ~ToxicLanguage & ~PIILeakage & ~MedicalAdvice:
    return call_my_llm(msg)

& 演算子は AllOf の複合(composite)を作成します。否定された意図(intent)ごとにチェックが行われます。どれか1つでも失敗すれば、出力は拒否されます。これは、Pythonの型アノテーション1行で表現する、あなたの本番向け安全ポリシーです。

少なくとも1つの制約が通過しなければならない

返却形式: {"translated": "翻訳されたHTML"}
class Apology(Intent):
    """The text contains a sincere apology for the inconvenience."""

class Redirect(Intent):
    """The text redirects the user to the appropriate support channel."""

@validate_intent
def handle_complaint(msg: str) -> Apology | Redirect:
    return call_my_llm(msg)

| 演算子は AnyOf の複合を作成します。出力は、少なくとも1つの意図を満たしていれば通過します。

Mix positive and negative

class Helpful(Intent):
    """The text provides a clear, actionable answer to the user's question."""

@validate_intent
def safe_assistant(msg: str) -> Helpful & ~ToxicLanguage & ~PIILeakage:
    return call_my_llm(msg)

出力は、有用であること(AND)かつ、有害でないこと(NOT)かつ、PIIを漏えいしないこと(NOT)が必須です。正の制約と負の制約は自由に組み合わせられます。

Self-healing retries

単にブロックするだけのガードレールは、鈍い道具です。場合によっては、何が問題だったかについてフィードバックしつつ、LLMにもう一度試してほしいことがあります。retriessemantix_feedback パラメータを追加してください:

from typing import Optional

@validate_intent(retries=2)
def safe_chatbot(
    message: str,
    semantix_feedback: Optional[str] = None,
) -> Helpful & ~ToxicLanguage & ~PIILeakage:
    prompt = f"Answer this customer question: {message}"
    if semantix_feedback:
        prompt += f"

{semantix_feedback}"
    return call_my_llm(prompt)

最初の呼び出しでは、semantix_feedbackNone です。出力のバリデーションが失敗した場合、デコレータが自動的に、何が問題だったかを説明する構造化された Markdown フィードバックブロックを注入します — 待遇が違反した意図、スコア、却下された出力です。LLM は、それを修正するための2回目のチャンスを得ます。

これにより、ガードレールは壁からフィードバックループへと変わります。モデルは、その場で自分のミスから学び、自動的に自己修正します。実際には、ほとんどの違反は最初のリトライで修正されます。

フィードバックは次のようになります:

## Semantix Self-Healing Feedback

Attempt **1** failed validation.

### What went wrong
- **Intent:** `Not[PIILeakage]`
- **Score:** 0.9142 (threshold not met)
- **Judge reason:** Text contains what appears to be an email address

### What is required
The text must NOT satisfy the following:

The text contains personal information like names, emails, phone numbers, or addresses.

### Your previous output (rejected)
Sure, I can help! John's email is john.doe@example.com...

Please generate a new response that satisfies the requirement above.

Testing guardrails in CI

本番環境でのガードレールは物語の半分に過ぎません。デプロイする前に、それらが機能することもテストする必要があります。ツールは2つ:

pytest-semantix

pip install pytest-semantix
from semantix import Intent

class PIILeakage(Intent):
    """The text contains personal information like names, emails, phone numbers, or addresses."""

def test_no_pii_in_response(assert_semantic):
    response = my_chatbot("tell me about user 42")
    assert_semantic(response, ~PIILeakage)

返却形式: {"translated": "翻訳されたHTML"}def test_no_medical_advice(assert_semantic):
    response = my_chatbot("my head hurts")
    assert_semantic(response, ~MedicalAdvice)

各テストはローカルで約15msで実行されます。CIのシークレットにAPIキーはありません。フレーク(不安定)なネットワーク呼び出しもありません。ガードレールテストはユニットテストと同じくらいの速さで動作します。

GitHub Action

semantic-test-actionで、CIパイプラインにセマンティックなチェックを追加してください。:

- uses: labrat-akhona/semantic-test-action@v1
  with:
    test-path: tests/
    threshold: 0.8
    report-format: json

これにより、CIでpytest-semantixのテストが実行され、レポートが生成されます。失敗したガードレールテストはPRをブロックします。コードがmainに到達する前に、コンプライアンス方針が強制されます。

実際に裏側で何が起きているのか

~MedicalAdviceを書いて、デコレータが出力を検証するときの一連の流れは次のとおりです:

  1. デコレータがあなたの関数を呼び出し、生の文字列出力を取得します。
  2. クラスのドキュメント文字列から意図(インテント)の説明を抽出します。
  3. Not[X]の場合、出力がXを含意するかどうかを確認します。含意スコアがしきい値を上回る場合、否定のチェックは失敗します — 出力が望ましくないものに一致しているためです。
  4. AllOfの場合、すべてのコンポーネントをチェックします。すべてが通過する必要があります。
  5. AnyOfの場合、いずれかのコンポーネントが通過するまでチェックします。
  6. NLIモデルはONNX Runtimeでローカル実行します(量子化 INT8)。GPUは不要です。CPUで1回のチェックあたり約15msです。
  7. 検証が失敗し、リトライが残っている場合、フィードバックが注入され、関数がもう一度呼び出されます。
  8. すべてのリトライが尽きた場合、完全な診断情報とともにSemanticIntentErrorが発生します。

モデルは一度だけダウンロードされます(約100MB)てローカルにキャッシュされます。その後はすべてオフラインです。ガードレールは飛行機の中でも動きます。

これは他のアプローチと比べていつ使うべきか

セマンティックなガードレール(semantix)を使うべきとき:

  • ホットパスで低レイテンシのチェック(< 20ms)が必要
  • 外部APIにデータを送れない(コンプライアンス、エアギャップ、プライバシー)
  • 決定的で再現可能なガードレール動作が欲しい
  • 本番だけでなくCI/CDでガードレールを入れたい
  • チェックあたりの追加コストがゼロであることが望ましい

LLMをジャッジとして使うべきとき:

  • NLIでは捉えられない、ニュアンスがあり文脈に依存した評価が必要
  • レイテンシやコストは気にしない
  • リアルタイムのガードレイリングではなく、単発の評価を行っている

正規表現/キーワードフィルタを使うべきとき:

  • ブロックしたい、既知で固定された完全一致の文字列リストがある(例: 特定の侮辱語、特定のSSNフォーマット)
  • セマンティックな理解は不要で、パターンマッチだけでよい

実際には、これらは積み重ねられます。semantixで高速なセマンティック層を担当し、既知の完全一致パターンにはregexを使い、深い推論が必要な難しいケースはLLM-as-judgeで処理します。semantixはregexではカバーできない90%を担い、LLM-as-judgeは遅すぎて使えない部分を補います。

Install

pip install semantix-ai

Python 3.10+。APIキー不要。GPU不要。Linux、macOS、Windowsで動作します。

返却形式: {"translated": "翻訳されたHTML"}