在处理敏感数据的 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 取回映射表,并使用 RestoreTextRestoreJSON 方法将占位符替换回原文。特别是对于 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 到达并拼接完整后,再进行正则匹配和还原。这种设计在保证流式输出低延迟的同时,完美解决了占位符被截断的问题。


# 安全与扩展性考量

在设计这套机制时,我特别关注了安全性:

  1. 内存级短期存储:可逆会话映射仅存在于内存中,默认 5 分钟后自动过期销毁,绝不落盘。
  2. 禁止原始映射外泄:HTTP 和 gRPC 接口绝不会直接返回原始的 Mapping,避免敏感数据在网络中二次暴露。
  3. 日志脱敏:网关默认不记录请求和响应的原始 Body,仅记录请求元数据、脱敏计数和错误信息。

此外,我还更新了 Protocol Buffers 定义,增加了 RedactReversibleRestore 的 gRPC 接口,并提供了详尽的单元测试(新增了近千行测试代码,覆盖了网关的核心逻辑和边界情况)。


# 为 Agent 编写操作 Skill

实现了网关之后,随之而来的问题是:运行在网关后方的 Agent 本身,是否知道该如何处理这些占位符?

大模型并不天然了解 [邮箱_0] 这类标记的含义。如果 Agent 在推理过程中试图猜测或还原占位符背后的原始值,或者在向用户确认时刻意回避这些标记,反而会破坏网关的透明还原机制。

为此,我在仓库中新增了一份 Agent SkillCommit 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 开源了如此优秀的单向脱敏底层框架,为本次扩展提供了坚实的基础。