在处理敏感数据的 LLM 场景中,隐私脱敏(Redaction)是一个绕不开的话题。传统的脱敏方案通常是单向的:把用户输入中的邮箱、手机号或密钥替换成不可逆的占位符(比如 [邮箱] ),然后发送给大模型。这种做法在保护隐私的同时,也带来了一个痛点:当大模型需要调用外部工具,或者在多轮对话中需要引用这些具体信息时,单向脱敏会让模型 “失忆”,导致工具调用失败或上下文断裂。
最近,我在开源的 privacy-filter(一个基于 Go 语言、毫秒级延迟的 LLM 隐私脱敏网关核心库)基础上做了一些改进。通过引入可逆脱敏和一个简单的反向代理网关,我们不仅能保护敏感数据,还能让大模型在不获取敏感数据的前提下,具有操作数据的能力。
本文将详细介绍这次提交(Commit e10cd42)的核心设计与技术实现。
# 痛点:单向脱敏的局限性
原版的 privacy-filter 提供了非常高效的单向脱敏能力。它通过正则表达式和 gitleaks 规则,能够在毫秒级内将结构化 PII(如身份证、银行卡)和高熵凭证(如 API Key)替换为带有实体类型的占位符。
然而,在 Agent 或 Tool-call(工具调用)场景下,这种单向性会成为障碍。例如:
- 用户输入:
发邮件给 a@b.com - 单向脱敏后:
发邮件给 [邮箱] - 大模型生成工具调用参数:
{"to": "[邮箱]"}
此时,如果系统直接将 {"to": "[邮箱]"} 传递给邮件发送工具,显然会报错。我们需要一种机制,能在请求发往大模型前将敏感信息替换为唯一占位符,并在大模型响应后,将这些占位符精准地还原为原始数据。
# 核心机制:可逆脱敏
为了解决上述问题,我在 filter 核心包中引入了可逆脱敏功能。
# 占位符映射与存储
在 RedactReversible 方法中,系统在检测到敏感信息时,不再仅仅替换为通用的 [邮箱] ,而是生成带有索引的唯一占位符,例如 [邮箱_0] 。同时,系统会在内存中维护一个映射表(Mapping):
map[string]string{ | |
"[邮箱_0]": "a@b.com", | |
} |
为了管理这些短期存在的映射关系,我实现了一个基于内存的并发安全会话存储(Session Store)。每次可逆脱敏请求都会生成一个唯一的 session_id ,并将映射表存入内存,默认有效期(TTL)为 5 分钟。
# 递归还原
当大模型返回响应时,系统通过 session_id 取回映射表,并使用 RestoreText 或 RestoreJSON 方法将占位符替换回原文。特别是对于 Tool-call 场景, RestoreJSON 能够递归地遍历 JSON 结构,精准还原字符串值中的占位符,同时保持数字和布尔类型不变。
# 工程实践:示例反向代理网关(Gateway)
有了底层的可逆脱敏能力,我进一步实现了一个完整的示例反向代理网关(位于 cmd/gateway )。这个网关可以直接部署在客户端与 OpenAI 兼容的 LLM API 之间,实现透明的隐私保护。
| 处理阶段 | 网关行为 | 技术细节 |
|---|---|---|
| 请求拦截(Request) | 解析请求体,执行可逆脱敏 | 提取文本字段,生成占位符映射并存入 Session,修改请求体后转发给上游。 |
| 流式响应(SSE) | 实时还原 Delta 事件中的占位符 | 感知 text/event-stream ,在流式返回过程中实时替换 *.delta 事件中的占位符。 |
| 普通响应(JSON) | 全量还原响应体 | 拦截上游返回的 JSON,递归还原占位符后返回给客户端。 |
# 应对流式输出的挑战:后缀缓冲
在处理大模型的流式响应(Server-Sent Events, SSE)时,我们遇到了一个经典问题:大模型可能会将一个占位符(如 [邮箱_0] )拆分在多个 Chunk 中返回。
为了确保占位符能够被正确识别和还原,我在网关中实现了一个微小的后缀缓冲区。系统会缓存不超过一个占位符长度的后缀字符串,等待后续 Chunk 到达并拼接完整后,再进行正则匹配和还原。这种设计在保证流式输出低延迟的同时,完美解决了占位符被截断的问题。
# 安全与扩展性考量
在设计这套机制时,我特别关注了安全性:
- 内存级短期存储:可逆会话映射仅存在于内存中,默认 5 分钟后自动过期销毁,绝不落盘。
- 禁止原始映射外泄:HTTP 和 gRPC 接口绝不会直接返回原始的 Mapping,避免敏感数据在网络中二次暴露。
- 日志脱敏:网关默认不记录请求和响应的原始 Body,仅记录请求元数据、脱敏计数和错误信息。
此外,我还更新了 Protocol Buffers 定义,增加了 RedactReversible 和 Restore 的 gRPC 接口,并提供了详尽的单元测试(新增了近千行测试代码,覆盖了网关的核心逻辑和边界情况)。
# 为 Agent 编写操作 Skill
实现了网关之后,随之而来的问题是:运行在网关后方的 Agent 本身,是否知道该如何处理这些占位符?
大模型并不天然了解 [邮箱_0] 这类标记的含义。如果 Agent 在推理过程中试图猜测或还原占位符背后的原始值,或者在向用户确认时刻意回避这些标记,反而会破坏网关的透明还原机制。
为此,我在仓库中新增了一份 Agent Skill(Commit 4c72d6f),专门用于告知 Agent 以下几件事。
# 占位符速查表
Skill 列出了全部 6 种占位符类型及其对应的实体类别:
| 占位符格式 | 实体类型 | 示例原始值 |
|---|---|---|
| [邮箱_N] | 电子邮件地址 | alice@example.com |
| [电话_N] | 手机 / 电话号码 | 13900001111 |
| [身份证_N] | 中国居民身份证号 | 110101199001011234 |
| [银行卡_N] | 银行卡号(Luhn 校验) | 6222021234567890 |
| [IP_N] | IPv4 地址 | 192.168.1.1 |
| [密钥_N] | 密钥 / 凭证 / API Key / 密码 | sk-abc123... |
索引规则:相同原始值 → 相同占位符;同类型不同值 → 不同下标( [邮箱_0] 、 [邮箱_1] ……)。
# Agent 的核心行为准则
Skill 向 Agent 传达的最关键一点是:占位符的还原是网关的职责,不是 Agent 的。 Agent 只需要将占位符原样透传,网关会在响应出站时自动完成替换。
具体来说:
- 工具调用:直接将占位符写入工具参数,例如
{"to": "[邮箱_0]"}—— 网关会在响应中还原它。 - 用户追问原始值:直接回复占位符,例如您的邮箱是 [邮箱_0]—— 用户看到的将是真实邮箱地址。
- 等值比较:
[邮箱_0]不等于[邮箱_1]意味着两个不同的邮箱地址,可以直接用占位符做逻辑判断。 - 永远不要猜测或重建原始值:网关的 Session 映射对 Agent 不可见,任何推断都是错误的。
这份 Skill 也随 .gitignore 的更新一并提交进了仓库,方便后续接入该网关的 Agent 直接引用。
# 已知局限与兼容性说明
# 当前仅支持 OpenAI Responses API 格式
目前网关的 SSE 流式还原逻辑是针对 OpenAI Responses API( /v1/responses )格式实现的 —— 具体来说,是识别事件体中 type 字段以 .delta 结尾或包含 _delta 的事件,并对其中的 delta 字段执行占位符还原。因此,理论上当前版本仅对 Codex 有完整支持。
对于使用其他格式的客户端,流式还原尚未覆盖,非流式 JSON 响应的还原可能能用。
# Coding 环境下的兼容性问题与 Bypass 方案
即便是在 Codex 场景下,反向代理也可能在某些情况下出现报错 —— 脱敏后的占位符有时会触发客户端或上游 API 的参数校验逻辑,导致请求失败。
网关为此提供了一个 Session 级别的脱敏旁路(Bypass) 机制,可以在不重启网关的情况下,针对特定会话临时禁用脱敏。启用方式是在启动网关时设置环境变量:
PF_GATEWAY_BYPASS_MARKER=<你选择的标记字符串>(我建议你选一个uuid) |
设置后,当某个请求体中包含该标记字符串时,网关会将该请求对应的 Session ID 记录为旁路会话。此后,来自同一 Session 的所有后续请求都会跳过脱敏,直接透传给上游 LLM,直到该 Session 结束。
旁路触发的完整逻辑如下:
| 条件 | 网关行为 |
|---|---|
| 请求体包含 Bypass Marker,且能识别到 Session ID | 将该 Session 标记为旁路,本次及后续请求均跳过脱敏 |
| 请求体包含 Bypass Marker,但无法识别 Session ID | 仅记录日志警告,本次请求仍正常脱敏 |
| Session 已被标记为旁路 | 后续所有请求直接透传,不再脱敏 |
| 未设置 PF_GATEWAY_BYPASS_MARKER 环境变量 | Bypass 机制完全禁用,所有请求均正常脱敏 |
注意:Bypass 状态仅存在于内存中,网关重启后自动清除。这一机制的设计初衷是作为调试和兼容性应急手段,不建议在生产环境中长期开启。
# 结语
通过为 privacy-filter 引入可逆脱敏和反向代理网关,我们展示了如何在不牺牲隐私的前提下,保持 LLM Agent 在复杂任务中的上下文连贯性。这不仅是一个功能增强,更是对隐私计算与 AI 工程结合的一次有益探索。
如果你对这个项目感兴趣,欢迎访问 Tritium0041/privacy-filter 查看完整的源码和中英文文档。
致谢:感谢原作者 PackyCode 开源了如此优秀的单向脱敏底层框架,为本次扩展提供了坚实的基础。