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。