Redact PII Before Calling Claude or GPT — Python & TypeScript Code

Model providers still process what you send. Replace emails, phone numbers, card numbers and IPs with reversible placeholders locally, then restore them in the reply. Ready-to-run Python and TypeScript.

English Implementation & Architecture SummaryBase URL: https://api.callaiapi.com/v1

Model providers still process what you send. Replace emails, phone numbers, card numbers and IPs with reversible placeholders locally, then restore them in the reply. Ready-to-run Python and TypeScript.

Auth Header: Bearer <YOUR_API_KEY>•Billing: TRON USDT from $1 · Pay-as-you-go•Compatibility: 100% OpenAI SDK Drop-in

隐私保护最可靠的一层,永远在你自己的机器上。无论网关承诺多少,只要真实的客户邮箱、电话出现在 Prompt 里,它们就会被发送给模型服务商。发送前脱敏可以让敏感信息完全不离开本机,而模型照样能完成写邮件、做总结、翻译等任务。

一、思路:可还原的占位符

原始内容发送给模型的内容
[email protected][EMAIL_1]
+1 415-555-0132[PHONE_1]
4111 1111 1111 1111[CARD_1]
10.0.8.21[IP_1]

同一个值始终映射为同一个占位符,模型可以正常理解上下文(例如"再发一次给 [EMAIL_1]")。映射表只保存在本地内存里,收到回复后替换回真实值。

二、Python 实现

import re

# Order matters: longer / more specific patterns first
PATTERNS = {
    "EMAIL": re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+"),
    "CARD": re.compile(r"(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)"),
    "IP": re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)"),
    "PHONE": re.compile(r"(?<![\w])\+?\d[\d\s-]{6,15}\d(?!\d)"),
}


def redact(text: str) -> tuple[str, dict[str, str]]:
    """Replace sensitive values with stable placeholders; keep the mapping locally."""
    mapping: dict[str, str] = {}
    for label, pattern in PATTERNS.items():
        def replace(match: re.Match, label: str = label) -> str:
            value = match.group(0)
            for key, original in mapping.items():
                if original == value:
                    return key
            key = f"[{label}_{sum(k.startswith('[' + label) for k in mapping) + 1}]"
            mapping[key] = value
            return key
        text = pattern.sub(replace, text)
    return text, mapping


def restore(text: str, mapping: dict[str, str]) -> str:
    # Longest placeholder first so [PHONE_1] never clobbers [PHONE_10]
    for key in sorted(mapping, key=len, reverse=True):
        text = text.replace(key, mapping[key])
    return text

与 OpenAI SDK 配合使用,只需把 base_url 指向青柠AI:

from openai import OpenAI

client = OpenAI(base_url="https://api.callaiapi.com/v1", api_key="sk-live-your-key")

raw = "客户 Anna 的邮箱 [email protected],电话 +1 415-555-0132,请帮我写一封跟进邮件。"
safe, mapping = redact(raw)          # 发送前:真实信息只留在本地内存

resp = client.chat.completions.create(
    model="claude-sonnet-5",
    messages=[{"role": "user", "content": safe}],
)
print(restore(resp.choices[0].message.content, mapping))  # 收到后:本地还原

三、TypeScript 实现

// Order matters: longer / more specific patterns first
const PATTERNS: Array<[string, RegExp]> = [
  ['EMAIL', /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g],
  ['CARD', /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g],
  ['IP', /(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)/g],
  ['PHONE', /(?<![\w])\+?\d[\d\s-]{6,15}\d(?!\d)/g],
];

export function redact(text: string): { safe: string; mapping: Map<string, string> } {
  const mapping = new Map<string, string>();
  let safe = text;
  for (const [label, pattern] of PATTERNS) {
    safe = safe.replace(pattern, (value) => {
      for (const [key, original] of mapping) if (original === value) return key;
      const count = [...mapping.keys()].filter((k) => k.startsWith(`[${label}_`)).length;
      const key = `[${label}_${count + 1}]`;
      mapping.set(key, value);
      return key;
    });
  }
  return { safe, mapping };
}

export function restore(text: string, mapping: Map<string, string>): string {
  // Longest placeholder first so [PHONE_1] never clobbers [PHONE_10]
  return [...mapping.keys()]
    .sort((a, b) => b.length - a.length)
    .reduce((out, key) => out.split(key).join(mapping.get(key)!), text);
}

四、局限与建议

正则不是万能的

正则适合格式固定的信息(邮箱、电话、卡号、IP),但识别不了姓名、公司名、地址这类自由文本。处理高度敏感的数据时,建议叠加命名实体识别(NER)模型,或在发送前人工复核。

  • 先脱敏再记日志:你自己的应用日志同样可能泄露数据,日志里只记录脱敏后的文本。
  • 占位符格式保持稳定:让模型在回复里原样保留 [EMAIL_1] 这类标记,还原才会可靠。
  • 按客户拆分 API Key:即使出现问题,也能快速定位并删除对应的 Key。