top of page

LangfuseとLLMを活用したPIIマスキング:トレース保存の詳細と実践的注意点

  • 執筆者の写真: Hiromi Kuwa
    Hiromi Kuwa
  • 11 時間前
  • 読了時間: 6分

PIIフィルター用LLMのトレース保存


こちらの記事で、Langfuse の PII フィルターに LLM を利用する方法について検証しました。

実際の運用では、LLM呼び出しにかかる費用を正確に把握するため、PIIフィルターの実行を含めたトレース記録が求められることがよくあります。ここでは、Langfuse の mask オプションを利用する場合の、PIIフィルターLLMの呼び出しとメインのLLM呼び出しを同一トレース内で記録する方法と、その際の注意点について解説します。


トレース記録の構成イメージ

require_mask = True

def masking_function(data: any, **kwargs) -> any:
    global require_mask
    if require_mask and isinstance(data, str):
        try:
            require_mask = False
            # PII フィルター適用時のトレース保存
            with langfuse.start_as_current_generation(
                name="pii_filter_prompt",
                prompt=[使用するプロンプト]
            ) as generation:
                # LLM呼び出し(マスキング処理)
                generation.update(
                    output=[フィルター済みデータ],
                )
        finally:
            require_mask = True

        return [フィルター済みデータ]

langfuse = Langfuse(
    public_key="****************",
    secret_key="****************",
    host="*****************",
    mask=masking_function,
)

# LLM呼び出しのトレース
with langfuse.start_as_current_generation(
    name="llm_called_trace",
    input=[ユーザの入力],
) as generation:
    # LLM呼び出し(入力に対する回答取得用)
    generation.update(
        output=[LLMの回答],
    )

この構成では、langfuse.start_as_current_generation の input や generation.update の output に対して、自動的に masking_function が呼び出され、PIIがマスキングされた状態でトレースに保存されます。


注意点:再帰呼び出し(無限ループ)への対策


mask オプションで設定した masking_function 内で、さらに Langfuse のトレース保存(例: langfuse.start_as_current_generation)を行おうとすると、無限ループに陥る可能性があります。これは、トレース保存処理自体が mask オプションの対象となり、masking_function が再帰的に呼び出されてしまうためです。

この問題を避けるためには、masking_function の内部でトレースを保存する際に、多重呼び出しを防止する何らかの制御が必要です。

今回のサンプルコードでは、処理を簡潔にするため、グローバル変数 (require_mask) をフラグとして持たせ、masking_function 内部からの呼び出しに対してはフィルターを適用しないようにしています。

実際の運用では、mask オプションを利用せず明示的にマスキング用関数を呼び出す方法や、より堅牢な方法を検討することをお勧めします。


検証結果


今回は、以下の条件でトレースの確認を行いました。

  • 入力値、LLMのプロンプト(PIIフィルター用): こちらの記事でも利用した、以下のダミーデータとプロンプトを利用

入力値

私の個人情報は以下の通りです: 

氏名:山田 太郎
Name: Taro Yamada

生年月日:1985年3月15日
Birthday: March 15, 1985

住所:東京都新宿区西新宿2-8-1
Address: 2-8-1 Nishi-Shinjuku, Shinjuku-ku, Tokyo, Japan

電話番号:03-1234-5678 / 090-9999-8888
Phone: +81-3-1234-5678 / +81-90-9999-8888

メールアドレス:taro.yamada@example.com
Email: taro.yamada@example.com

運転免許証番号:123456789012
Driver's License: DL-123456789012

パスポート番号:TK1234567
Passport Number: TK1234567

社会保障番号:987-65-4321
Social Security Number: 987-65-4321

クレジットカード情報:
- カード番号:4111-2222-3333-4444
- 有効期限:12/25
- セキュリティコード:123

Credit Card Information:
- Card Number: 4111-2222-3333-4444
- Expiry Date: 12/25
- Security Code: 123

銀行口座情報:
- 銀行名:みずほ銀行
- 支店名:新宿支店
- 口座番号:1234567

Bank Account Information:
- Bank Name: Mizuho Bank
- Branch: Shinjuku Branch
- Account Number: 1234567

プロンプト

以下のテキストから、個人情報をマスクしてください。

# 条件
- マスク処理以外は行わない
- テキストの前後に余分なテキストを追加しない
- どのようなデータがマスクされたかわかるようにする(例:山田太郎 -> [氏名1]、000-0000-0000 -> [電話番号1])
- 同じ個人情報種別でも、異なる情報の場合は別物であるとわかるようにする(例:山田さんと佐藤さん -> [氏名1]さんと[氏名2]さん)

# 置換対象の個人情報
- 氏名
- 生年月日
- 住所
- 電話番号
- メールアドレス
- 運転免許証番号
- パスポート番号
- 社会保障番号
- クレジットカード情報(カード番号, 有効期限, セキュリティコード)
- 銀行口座情報(銀行名, 支店名, 口座番号)

# 対象のテキスト
{{userMessage}}
  • LLMのプロンプト(問い合わせ用):

以下のテキスト中に「山」が何度出てくるか数えて # 対象の文書 [ 入力値 ]

結果概要


問い合わせ用のLLMの下に、PIIフィルター用のLLM呼び出しがネストする形で保存できました。


Langfuseのトレース上では、LLM呼び出し(問い合わせ1回、input、output のフィルター用各1回ずつ)計3回分のトークン数や費用が確認できます。



LLM呼び出し(llm_called_trace):

このトレースでは、問い合わせの LLM 問い合わせにかかった費用と、一連の流れ(問い合わせ+PIIフィルター)でかかった費用の両方が表示されます。

このLLM への問い合わせ結果を metadata の情報として登録したい場合、mask オプションが適用される作りになっているかを十分に確認する必要があります。上記のコード例では、文字列型以外の場合にマスキング処理が適用されないため、場合によっては metadata に個人情報が除去されていない状態で残ってしまう可能性があります。トレースの output としては個人情報が除去されていても、metadata には生データが残るリスクがあるため、input や output と metadata に保存する形式が異なっている場合は特に、保存内容には注意を払うことが必要です。


冒頭の例のコードでLLM からの結果を metadata に保存(一部抜粋)
冒頭の例のコードでLLM からの結果を metadata に保存(一部抜粋)

LLM呼び出し(1つ目のpii_filter_prompt):

langfuse.start_as_current_generation(
    name="llm_called_trace",
    input=[ユーザの入力],
)

上記で設定している、input に対して masking_function が呼び出された結果です。

with langfuse.start_as_current_generation(
    name="pii_filter_prompt",
    prompt=[使用するプロンプト]
) as generation:
    # LLM呼び出し(マスキング処理)
    generation.update(
        output=[フィルター済みデータ],
    )

今回はPII フィルターに対するトレースを保存する際に、 PIIフィルターを適用しないよう制御しています。そのため、 個人情報が除去されていない生データを残さないよう、input は保存しないようにします。


Langfuse で費用を表示したい場合には、モデルとトークン数の情報も必要になります。

GeminiAPIの場合は、LLM の結果情報に含まれているため、 generation.update の際に model, usage_details もあわせて保存すると今回の例のような形になります。


LLM呼び出し(2つ目のpii_filter_prompt):
# LLM呼び出し(入力に対する回答取得用)
generation.update(
    output=[LLMの回答],
)

上記で設定している、output に対して masking_function が呼び出された結果です。ここでも mask オプションが適用されます。

input に対するトレース保存と同様に、input には何も設定しないことで、誤って生データがトレースに保存されることを防ぎます。


まとめ


この記事では、Langfuseのmaskオプションを活用し、PII(個人特定情報)フィルターにLLMを用いる際のトレース記録方法とその注意点について深掘りしました。LLM呼び出しにかかるコストを正確に把握するためには、PIIフィルターの実行を含む一連の処理を同一トレース内で記録することが重要です。


Langfuseのmaskオプションは、inputやoutputデータがトレースに保存される際に自動的にマスキング関数を適用できる便利な機能です。しかし、オプションで指定した関数内部で再度Langfuseのトレース保存処理を行うと無限ループに陥る可能性があるため、実際に利用する際は適切かつ堅牢な処理となるよう注意が必要です。

また、metadataに個人情報を保存する際の注意点、PIIフィルターLLMのinputには生データを設定しないことの重要性についても解説しました。


これらの注意点を踏まえることで、PIIを適切に処理しつつ、Langfuseの強力なトレース機能を最大限に活用し、LLMアプリケーションの運用コストと安全性を両立させることができます。実際の運用では、本記事で紹介した内容を参考に、トレース設計を検討することをお勧めします。

 
 
 

Commentaires


bottom of page