【Gemini】クラウドベースLLMとのメッセージの個人情報をタグに置き換える

# タグに置き換えるとは
個人情報をタグに置き換えたメッセージでGeminiとやり取りします。
LLMのパーソナライズが進化する中で、個人情報の取り扱いが重要な課題です。LLMのパーソナライズ化はさらに進展しています。ChatGPT は「ユーザーについて知っておくべきこと」「名前」「職業」など入力する欄が増え、より個人に特化したAIへ進化しています。
クラウドベースのLLMにおいて、個人情報保護と効率的なパーソナライズを両立させるために、メッセージ内の**個人情報をタグに置き換える手法**を考え、試します。匿名化、マスキングをするイメージです。
この手法は、当初の目的であった個人情報の漏洩リスク低減に加え、スケーラビリティや信頼性の向上など、さまざまなメリットをもたらすと気付きました。また運用側はクラウドAIにデータを学習させても、リスクを抑えられる設計になると考えています。

今回はクラウドLLMをGemini APIの無料枠にして試します。
GeminiのAPIは無料枠であると、やり取りしたデータがモデルの改善に使用されます。
学習前にGoogleがデータを事前に処理するとは思いますが、それでも心配ですので、事前に一部をタグに置換したデータでやり取りすれば確実です。

簡単まとめると、**Geminiが個人情報を直接把握せずに、ユーザーと会話する**ということです。

# メリット

- **情報漏洩リスクの低減**: 個人情報をデータベースで管理し、LLMとのやり取りではタグを使用することで、情報漏洩のリスクを大幅に低減します
- **スケーラブルなパーソナライズ**: LLMは、ID(ユーザーの仮の名前)と種類(氏名、メアド、電話番号、住所など)をタグで指定するだけなので、LLMがそれぞれのユーザーの情報をあらかじめ把握する必要がなくなり、ユーザーが増えてもデータベースを更新するだけで対応可能です
- **信頼性の向上**: データベースの情報を直接タグで参照するため、ハルシネーションを防止し、正確な情報提供を実現します
- **処理の効率化**: LLMによる関数呼び出しを使わない処理となり、コンテキストを減らして処理負荷を軽減します
- **長い個人情報との相性が良い**: 住所などの長い個人情報でも、プログラムがタグをそのまま置き換えてくれるので、LLMが間違えるリスクを減らせます

system instructionで命令するのではなく、ファインチューニングでタグを生成するように調整すれば、テキスト数を減らせるでしょう。

住所など**長い個人情報テキスト**はこの方法がより適していると考えます。なぜなら、そのままタグがデータベース上の個人情報と置き換わるので、LLMが間違えるということがないからです。

**モデルを大規模に個別で調整せず、個人情報が確実でスケーラブルにパーソナライズでき、リスクを抑えられる**と考えています。

# デメリット
- **柔軟性に欠ける**: タグによる置換は、LLMの自由な言語生成を制限して自然な会話の流れを阻害する可能性があり、複雑な感情表現やニュアンスを伝えるのが難しくなる場合があります。データベース上でのバリエーションを増やさなければなりません
- **Geminiのタグ生成ミス**: system instructionを無視してタグの使用方法を間違えられると使えない。LLMがタグを正しく生成できるかに依存する
- **データベースの管理**: やり取りに必要なデータをデータベース上で管理し、運用しなければならない

また、**発言スタイルのパーソナライズはできない**ので、別の手法(ファインチューニングやFew-shotプロンプティング、system instruction)を組み合わせて使う必要があります。

# 仕組み
データベースで個人情報とタグを紐付け、Geminiとの間で テキスト処理 を介して情報の置き換えを行います。
- ユーザーからLLMへのメッセージでは、登録済みの個人情報をタグに変換します(後ほど説明するソースコードでは実装していません)
- LLMからユーザーへのメッセージでは、タグを登録済みの個人情報に置き換えて表示します(**今回はこちらに焦点を当てています**)

この構造により、LLMは個人情報を直接把握することはないものの、会話に支障が出ることもありません。
ユーザーからLLMへのメッセージの場合、固有表現抽出(個人情報を検出することに特化したモデル等)を使えば、より柔軟に個人情報をタグに置き換えられると思います。
![個人情報タグ置換フロー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2464900/be2b7327-aa5b-4958-97ed-73bc23dc87e7.png)
多くのユーザがいる場合、Geminiのメッセージからタグを取得した後、認証する仕組み(発言者にアクセス権限があるか)を入れることで機密性も維持できます。※権限がなかった場合、LLMにそのことを伝えて再生成させることで、ユーザーからは違和感なくLLMとやり取りできると考えています。

### メッセージの例
以下にメッセージの例を提示します。
ユーザーが入力 
`こんばんは！`
↓ 
機械的にIDをつける
`user_1 : こんばんは！`
↓ 
これをinputとして、タグを使うことをあらかじめ把握しているLLMが以下を生成
`<<name: user_1>>さん、こんばんは！`
↓
機械的にテキストからタグを抽出、<<name: user_1>>からnameでIDがuser_1のものをデータベースから参照し、置き換える
`太郎さんこんばんは！`
これをユーザーに表示

# コードを作成する
機能を簡単に作成しました。
genaiを使ってGemini APIを操作し、sqlite3を使ってdbファイルにアクセスします。

:::note warn
- エラー処理が完全でないことに注意してください
- 今回は、ユーザーからLLMへのメッセージはタグの処理をしないということに注意してください
- APIキーを変えてください
:::
```python
import os
import re
from google import genai
from google.genai import types
import sqlite3

# API key
GEMINI_API_KEY = "## YOUR_API_KEY ##"
client = genai.Client(
        api_key=GEMINI_API_KEY,
    )

model = "gemini-2.0-flash"

generate_content_config = types.GenerateContentConfig(
    temperature=1,
    top_p=0.95,
    top_k=40,
    max_output_tokens=8192,
    response_mime_type="text/plain",
    system_instruction=[
        types.Part.from_text(
            text="""There are multiple users.
Input dialogues are formatted as "ID: utterance,"(ex. "3: Hello, how are you?") so you can identify conversation partners by their ID.
Output Gemini generates should be in plain sentences.(ex. "I'm fine, thank you.")
You have access to users' personal information, which should be enclosed in <> tags.
The available subjects and their tag formats are as follows (replace ID with the target's ID):
Name: <<name:ID>>
Email Address: <<email:ID>>
Address: <<address:ID>>
Phone Number: <<phone:ID>>"""
        ),
    ],
)

contents = []

# ユーザー情報を登録する関数
def create_and_insert_user_table(users):
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS user_table (
        id INTEGER PRIMARY KEY,
        name TEXT,
        role TEXT,
        email TEXT,
        phone TEXT,
        address TEXT
    )
    ''')
    
    inserted_ids = []
    for user in users:
        cursor.execute("INSERT INTO user_table (name, role, email, phone, address) VALUES (?, ?, ?, ?, ?)", user)
        inserted_ids.append(cursor.lastrowid)
    
    conn.commit()
    conn.close()
    return inserted_ids

# ユーザー情報を取得する関数
def select_user_by_id_and_name(type,id):
    conn = sqlite3.connect('test.db')
    cursor = conn.cursor()
    
    cursor.execute(f"SELECT {type} FROM user_table WHERE id = ?", (id,))
    
    result = cursor.fetchone()[0]
    conn.close()
    return result

# 会話の内容を追加する関数
def add_content(id:int, text:str):
    get_role = select_user_by_id_and_name("role",id)
    for_add_content = types.Content(
        role=get_role,
        parts=[
            types.Part.from_text(
                text= f"{str(id)}: {text}" if get_role == "user" else text
            ),
        ],
    )
    contents.append(for_add_content)

# テキストからタグを抽出する関数
def extract_tags(text;str):
    return re.findall(r'<<([^:]+):(\d+)>>', text)

# タグを置換する関数
def replacement_tags(text:str):
    tags = extract_tags(text)
    if tags:
        for tag in tags:
            type =  str(tag[0])
            id = int(tag[1])
            replacement_info = select_user_by_id_and_name(type,id)
            text = text.replace(f"<<{type}:{id}>>", replacement_info)
    return text

def console_chat():
    print("ユーザーのメッセージを入力してください(終了はqでEnter)")
    generate()

# 会話を生成する関数
def generate():
    user_text = str(input("User: "))
    if user_text == "q":
        return
    
    add_content(3, user_text) # 3はユーザーのID

    llm_output = client.models.generate_content(model=model,contents=contents,config=generate_content_config)
    llm_output_display = replacement_tags(llm_output.text)

    print("Gemini's output: ",llm_output.text)
    print("display: ",llm_output_display)

    add_content(1, llm_output.text) # 1はGeminiのID

    generate() #再帰的に呼び出して連続で会話を続ける

if __name__ == "__main__":

    # console_chat()する前に以下のコードを実行してデータベースの作成、ユーザー情報を登録しておく！2回以上実行しても問題はないが、実行毎にレコードが増える。

    #users = [ # ダミーのデータ
    #    ('Gemini', 'model', None, None, None),
    #    ('Bot', 'user', None, None, None),
    #    ('Jane Smith', 'user', 'jane@example.com', '098-765-4321', '虹色市夢見が丘1丁目2番3号 ファンタジーマンション405号室')
    #]
    #inserted_ids = create_and_insert_user_table(users)
    #print("Inserted IDs:", inserted_ids)

    console_chat()
```
最初、if __name__ == "__main__":以下にあるコメントアウトを外して実行してください。


上記のコードで実装されている通り、コンテキストにタグを個人情報に変換したものを追加しないようにすることです。追加してしまうと結局個人情報をコンテキストとしてGeminiに送信してしまうからです。変換前のllm_output.textをコンテキストにしましょう。

使用するクラウドLLM(Geminiの有料枠など)が学習しないと保証されているのであれば問題ないです。コンテキストにタグが置き換えられたテキストを追加することで、先ほど提示したデメリットである会話の柔軟性を、ある程度改善することができます。LLMは**後から**ユーザーの名前や年齢などの個人情報を把握するような会話形式になります。

# 結果
コンソールのスクリーンショットです。(改行で見づらくてすみません)
![クラウドLLM(Gemini)とのメッセージの個人情報をタグに置き換える1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2464900/ccf3b50f-99a1-46db-a8ab-c4adc3596cfd.png)
Gemini’s outputはGeminiが生成したテキストです。displayはGemini’s outputのタグを置き変えた実際に表示されるテキストです。問題なくメーセージをやり取りできています(最後なぜか「少々お待ちください」になってますが…)

![スクリーンショット 2025-03-06 17.24.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2464900/7d4d5973-c1ae-4184-9f45-72d9f8242900.png)
どんなに会話を重ねていたとしても、住所を間違えることはありません。

仮にユーザーが複数人存在するチャットであっても、LLMはこのような長い住所を全員把握しておく必要はありません。また、発言者の認証を組み込むことで個人情報を出力させるのを防ぐことができるでしょう。

# まとめ
このように、メッセージ内の個人情報をタグに置き換える手法は、情報漏洩リスクの低減だけでなく、スケーラブルなパーソナライズ、テキストと処理の削減、信頼性の向上、をもたらすことがわかりました。一種の手法として確立したシステムであると思います。

ここまでご覧いただき、ありがとうございました！
不備などありましたらご連絡ください！

# 参考と使用サービス
- [Google AI Studio](https://aistudio.google.com/app/prompts/new_chat) (system instructionを調整、作成するために使用)
- [Whimsical](https://whimsical.com) (フローチャートを作成するために使用)

※この記事は、[Gemini](https://gemini.google.com/app) (モデルは2.0-Flash)と共同で作成しました。