
敏感数据治理与 Agent 工具调用之间天生存在一对矛盾Agent 要完成业务操作通常需要读取订单号、用户手机号、内部账号、数据库连接参数等信息但把这些信息原样灌进 LLM 的上下文又会造成数据泄露、日志留痕、越权推理等多重风险。实际项目里最常见的失败不是模型答不出来而是脱敏做得太粗暴把工具调用参数一起洗掉了导致 Agent 拿到一个被替换过的占位符去查库返回结果要么为空要么报错。这篇文章从一条完整的 LLM Agent 调用链路出发先说明敏感数据在哪个环节容易被泄露再给出“上下文脱敏、数据最小化、令牌化恢复”这三条核心策略最后用一个可运行的 Python 示例演示如何让脱敏和工具调用同时成立。文章会给出代码、参数说明、验证方法和排查清单适合正在做 Agent 开发、RAG 应用或企业内部 LLM 工具的开发者参考。1. 先摸清敏感数据在 Agent 工作流里的流动路径1.1 一条典型 Agent 调用链路中有哪些环节一个标准的 LLM Agent 工作流看起来是“用户提问 - 模型理解 - 模型决定调用哪个工具 - 工具执行 - 结果返回给模型 - 模型组织回答”。但拆开看数据会在五个位置出现用户输入本身可能包含手机号、身份证号、公司内部项目名。系统提示词与上下文构建开发者可能在 system prompt 里写入了内部接口地址、账号信息。模型生成的工具调用参数例如query_order(order_id...)中的参数。工具执行结果数据库返回的完整记录、第三方 API 响应体。链路日志与回调数据LangChain 等框架默认会把每次模型请求、工具输入输出写入 trace。很多团队只在第 3 步做了简单的“不让模型返回密码字段”忽略了第 1、2、4、5 步结果敏感数据依然从日志和上下文中泄露。1.2 风险面要按“可见性”分三层看待下面这张表展示了敏感数据在不同层级的可见程度数据位置对谁可见典型风险LLM 上下文prompt response模型服务商、本地模型进程上下文被记录、模型被诱导输出敏感内容工具调用参数工具系统、日志系统、调用链追踪参数明文进入日志或消息队列工具返回结果Agent 编排层、模型上下文、外部系统返回结果被模型进一步加工后写回日志日志与监控运维、日志平台、第三方分析工具脱敏之前就落盘事后无法撤回这里要特别提醒如果接的是外部 LLM APIprompt 和 response 是否被用于训练、是否会被服务商留存取决于服务协议。即使协议写明不用于训练也不代表没有日志留存。对敏感业务来说默认应该假设“发送给模型的内容都等于半公开内容”。1.3 常见的敏感数据场景分类结合 Agent 开发实际敏感数据通常分成三类身份类手机号、邮箱、身份证号、地址、姓名。这类数据主要出现在用户输入和工具查询结果里。凭证类API Key、token、数据库密码、私钥。这类数据主要出现在系统配置、环境变量、工具调用鉴权环节。业务机密类内部项目代号、财务数据、未公开的产品策略。这类数据最容易被人忽略因为它不是明显的 PII但泄露后果同样严重。处理策略不同身份类适合脱敏和令牌化凭证类应该完全不进入 LLM 上下文只保存在工具执行环境里业务机密类则依赖权限隔离和访问控制不能只靠关键词替换。2. 三条核心策略让 LLM 只看到“够用”的数据2.1 方案一上下文脱敏进入模型前完成清洗脱敏的核心思路是在构建 prompt 之前把敏感字段替换成占位符确保送入模型的文本不包含真实值。例如用户问“查询手机号 138****1234 的订单”系统把手机号替换为[PHONE_1]然后把原始手机号保存在当前请求的内存映射中。模型的上下文里只出现[PHONE_1]模型可以理解这是一个需要查询的标识但看不到明文。这种方式的优点是实现简单不改变原有工具接口缺点是脱敏规则要覆盖所有敏感类型且模型如果被要求解释占位符含义可能会靠猜测补全。2.2 方案二数据最小化工具参数不传完整内容很多工具调用失败的根因是开发者把“用户问题”和“工具入参”混在一起。实际上Agent 并不需要知道数据库密码、不需要知道用户完整身份证号。工具调用完全可以是query_order(order_id2024-0815-001)而不是query_order(user_phone13812341234, user_id_card..., db_password...)最小化原则落地时要对工具接口做一次“入参审计”每个参数是否真的是执行该操作所必需的如果不需要就不应该出现在工具 schema 描述里更不应该由模型随意填写。2.3 方案三令牌化与引用传递工具侧恢复真实值这是解决“脱敏后工具调用失败”的关键。流程是用户输入中的敏感值被替换为令牌如[TOKEN_ORDER_USER_0032]。LLM 上下文只包含令牌。模型生成工具调用时参数里出现的是令牌。Agent 编排层在真正调用工具之前把令牌替换回真实值。工具执行完成后返回结果再次被脱敏然后才送回模型上下文。这样做的好处是模型、日志、追踪系统只看到令牌工具却能获得完整参数。坏处是令牌映射表必须生命周期极短并且不能把映射表写进日志。2.4 三种方案对比方案优点缺点适用场景上下文脱敏实现简单改动小规则覆盖有限模型可能推断原文常见 PII 字段如手机号、邮箱数据最小化从源头减少敏感数据量需要重新设计工具接口和 schema工具入参设计阶段令牌化恢复既保护上下文又能保证工具执行成功需要维护映射表增加编排复杂度必须使用真实参数查询的工具实际项目通常组合使用先做数据最小化再对不可避免进入上下文的字段做脱敏最后在工具调用边界做令牌恢复。3. 环境准备与最小项目结构3.1 技术选型说明下面示例使用 Python LangChain 风格的抽象但实现不绑定具体框架。你完全可以用 LlamaIndex、自研编排代码或 TypeScript 版本复现同样的思路。关键不是框架而是四个组件脱敏器负责识别和替换敏感字段。令牌映射器维护“占位符 - 真实值”的双向映射。工具调用拦截器在执行工具前恢复真实值在执行后重新脱敏。日志过滤器防止脱敏前的数据落盘。建议环境依赖项说明Python 3.10类型注解和异步支持更友好langchain-core工具抽象与消息封装可选pydantic配置校验与工具参数模型tiktoken估算 token 长度便于检查脱敏后上下文大小如果原始项目没有固定版本落地前先确认langchain或llama_index的版本兼容关系避免 API 变更导致示例无法运行。3.2 项目目录结构sensitive_agent/ ├── config.py # 配置项与敏感规则 ├── redactor.py # 脱敏器与令牌映射 ├── tool_interceptor.py # 工具调用拦截器 ├── tools.py # 业务工具定义 ├── agent.py # Agent 编排主流程 ├── logger_filter.py # 日志脱敏过滤器 └── tests/ └── test_redaction.py # 验证用例这个结构把“脱敏”作为一个独立横切关注点而不是分散在各工具函数里。后面新增工具时不需要改脱敏逻辑。3.3 定义模拟场景为了演示我们构造一个常见场景用户查询订单状态。订单数据里包含手机号、收货地址、内部备注等敏感字段。工具需要真实订单号执行查询但模型得到的上下文不能有手机号和完整地址。假设用户输入帮我查一下订单 20240815001 的状态下单手机号是 13812341234。预期行为模型上下文里手机号被替换为[PHONE_0]工具调用时恢复真实手机号查询成功后返回给模型的结果里地址和手机号再次被脱敏。4. 代码实现脱敏和工具调用同时成立的完整示例4.1 先写配置与敏感规则config.py里定义需要识别的敏感字段类型和占位符前缀。用正则匹配是为了演示生产环境建议结合实体识别模型或专用 DLP 服务。# config.py from dataclasses import dataclass import re dataclass(frozenTrue) class SensitiveRule: name: str placeholder_prefix: str pattern: re.Pattern # 演示用规则生产环境请根据真实数据范围扩展 SENSITIVE_RULES [ SensitiveRule( namephone, placeholder_prefixPHONE, patternre.compile(r1[3-9]\d{9}), ), SensitiveRule( nameaddress, placeholder_prefixADDR, patternre.compile(r(?地址[:])[^\n,。;]), ), SensitiveRule( nameid_card, placeholder_prefixIDCARD, patternre.compile(r\d{17}[\dXx]), ), ] TOKEN_PATTERN re.compile(r\[([A-Z])_(\d)\])这里的TOKEN_PATTERN用来识别脱敏后的占位符后续工具调用拦截器会用它做反向替换。注意正则规则只适合演示。手机号、身份证号、地址在真实文本里形态复杂建议用专门的敏感数据识别库或模型。4.2 实现脱敏器与令牌映射器redactor.py是核心模块它维护一个请求级的双向映射redact(text)把原文中的敏感值替换为[TYPE_INDEX]形式。restore(text)把占位符还原为真实值。to_public(text)把工具返回值脱敏成可送回模型的文本不保留映射。# redactor.py from typing import Dict, Tuple from config import SENSITIVE_RULES, TOKEN_PATTERN class TokenMapper: 维护占位符与真实值的双向映射每次请求结束后清理。 def __init__(self): self._token_to_secret: Dict[str, str] {} self._secret_to_token: Dict[str, str] {} self._counters: Dict[str, int] {} def register(self, secret_value: str, rule_name: str) - str: 为真实值分配一个占位符若已存在则复用。 if secret_value in self._secret_to_token: return self._secret_to_token[secret_value] index self._counters.get(rule_name, 0) token f[{rule_name.upper()}_{index}] self._counters[rule_name] index 1 self._token_to_secret[token] secret_value self._secret_to_token[secret_value] token return token def restore(self, text: str) - str: 把文本中的占位符替换回真实值。 def _replace(match): token match.group(0) return self._token_to_secret.get(token, token) return TOKEN_PATTERN.sub(_replace, text) def clear(self): self._token_to_secret.clear() self._secret_to_token.clear() self._counters.clear() class Redactor: def __init__(self, mapper: TokenMapper): self.mapper mapper def redact(self, text: str) - str: 按规则替换敏感值。注意规则顺序先长后短避免误覆盖。 for rule in SENSITIVE_RULES: def _replace(match, _rulerule): value match.group(0) return self.mapper.register(value, _rule.name) text rule.pattern.sub(_replace, text) return text def restore(self, text: str) - str: return self.mapper.restore(text) def publicize(self, text: str) - Tuple[str, Dict[str, str]]: 把工具结果脱敏成可公开文本并返回本次公开字段的令牌表。 # 模拟对结果字段做脱敏生产环境应根据返回结构逐字段处理 result self.redact(text) token_map { k: v for k, v in self.mapper._token_to_secret.items() if k in result } return result, token_map这里要解释几个易错点为什么redact要“先长后短”假设地址文本里恰好包含手机号如果地址规则先命中手机号就不会被单独替换。实际项目中规则按“最长优先、最具体优先”排序。为什么restore只能用于工具调用前不能用于返回给模型的文本因为返回给模型的文本即使恢复了真实值也会成为模型上下文的一部分必须用publicize再次脱敏。mapper必须按请求隔离。多用户并发时不能共享同一个映射表否则 A 用户的占位符可能被 B 用户恢复成 A 的数据。4.3 工具调用拦截器tool_interceptor.py负责在工具函数执行前做反向恢复执行后再脱敏。这样工具函数本身保持纯粹的“拿真实参数干活”不需要感知脱敏逻辑。# tool_interceptor.py import inspect from typing import Callable, Any from redactor import TokenMapper, Redactor class ToolInterceptor: def __init__(self, redactor: Redactor): self.redactor redactor def invoke(self, func: Callable, raw_args: dict) - Any: raw_args 是模型生成的工具参数含占位符。 restored_args { key: self.redactor.restore(value) if isinstance(value, str) else value for key, value in raw_args.items() } # 记录一次“脱敏前 - 脱敏后”的调用摘要用于审计但不落盘敏感值 audit_summary { tool: func.__name__, restored_keys: [ k for k, v in raw_args.items() if isinstance(v, str) and [PHONE in v or [ADDR in v or [IDCARD in v ], } print(f[audit] {audit_summary}) result func(**restored_args) # 如果结果是字符串类型直接脱敏如果是结构化数据再递归处理 if isinstance(result, str): return self.redactor.publicize(result)[0] return result注意示例里的audit_summary只记录哪些字段是敏感占位符不记录真实值。生产环境应该把它写入独立的审计日志并配上访问权限控制。4.4 定义业务工具tools.py里定义两个工具一个模拟查询订单一个模拟查询用户。工具函数签名要尽量精简非必要参数不要出现在入参里。# tools.py from datetime import datetime def query_order(order_id: str): 查询订单状态。真实项目中这里会访问数据库或调用内部服务。 order_id 是业务主键不属于敏感数据但返回值中包含敏感字段。 # 模拟数据库返回 order_data { order_id: order_id, status: 已发货, phone: 13812341234, address: 北京市朝阳区某街道 100 号, internal_note: VIP 客户优先处理, } return f订单 {order_data[order_id]} 状态{order_data[status]}收货人电话{order_data[phone]}地址{order_data[address]} def query_user(user_id: str): 根据用户 ID 查询基本信息。真实场景中 user_id 可能来自登录态而不是模型猜测。 profile { user_id: user_id, phone: 13900001111, email: userexample.com, } return f用户 {profile[user_id]} 的电话{profile[phone]}邮箱{profile[email]}这两个工具故意在返回值里放进敏感字段目的是演示“工具结果再脱敏”的必要性。真实项目中更推荐让数据库查询直接返回脱敏后的视图例如 SQL 层只查脱敏字段而不是先查全量再脱敏。4.5 组装 Agent 主流程agent.py演示完整链路。这里不依赖具体 LLM 调用库而是把“模型输出”拆成两段第一段模拟模型识别到需要调用query_order第二段模拟模型根据工具结果生成回答。# agent.py from redactor import Redactor, TokenMapper from tool_interceptor import ToolInterceptor from tools import query_order, query_user def fake_llm_plan(user_message: str): 模拟 LLM 的意图识别与工具选择。 真实项目中这里会调用 OpenAI / 本地模型 / LangChain Agent。 注意此处收到的 user_message 已经是脱敏后的文本。 if 订单 in user_message: # 模拟模型从脱敏文本中提取订单号 return {tool: query_order, args: {order_id: 20240815001}} if 用户 in user_message: return {tool: query_user, args: {user_id: U10001}} return None def fake_llm_respond(public_result: str): 模拟模型根据脱敏后的工具结果组织回答。 回答中不能出现敏感明文因为 public_result 已经脱敏。 return f根据查询结果{public_result}。如需更多帮助请继续提问。 def main(): mapper TokenMapper() redactor Redactor(mapper) interceptor ToolInterceptor(redactor) tools { query_order: query_order, query_user: query_user, } # 1. 用户原始输入 user_message 帮我查一下订单 20240815001 的状态下单手机号是 13812341234。 # 2. 进入模型前脱敏 redacted_message redactor.redact(user_message) print(脱敏后的用户输入:, redacted_message) # 3. LLM 做意图识别和工具调用决策 plan fake_llm_plan(redacted_message) if not plan: print(模型未识别到工具调用) return # 4. 调用工具前恢复映射执行后脱敏返回值 raw_result interceptor.invoke(tools[plan[tool]], plan[args]) public_result, token_map redactor.publicize(raw_result) print(脱敏后的工具结果:, public_result) # 5. 模型根据公开结果生成回答 answer fake_llm_respond(public_result) print(模型回答:, answer) # 6. 请求结束清理映射 mapper.clear() if __name__ __main__: main()运行这段代码正常输出应该是脱敏后的用户输入: 帮我查一下订单 20240815001 的状态下单手机号是 [PHONE_0]。 [audit] {tool: query_order, restored_keys: []} 脱敏后的工具结果: 订单 20240815001 状态已发货收货人电话[PHONE_0]地址[ADDR_1] 模型回答: 根据查询结果订单 20240815001 状态已发货收货人电话[PHONE_0]地址[ADDR_1]。如需更多帮助请继续提问。代码里的audit_summary打印的restored_keys为空是因为query_order的入参只有order_id没有包含手机号。这恰恰说明模型并不需要手机号才能查订单手机号只是用户输入里的冗余信息。这是一种典型的“数据最小化”效果。4.6 关键点解析模型上下文与工具执行上下文分离这段代码的核心是把 Agent 的执行拆成两个上下文模型上下文只能看到占位符和脱敏后的结果。工具执行上下文才能接触真实数据。实现这个隔离的关键是redactor.publicize()在工具返回结果后再次脱敏。很多团队只处理了入参漏了返回值导致模型在“拿到工具结果后生成回答”这一环泄露了敏感数据。另一个关键点是入参里即使包含手机号也不一定需要透传给工具。上面query_order的 schema 只有order_id模型自然无法把手机号作为参数传进去。这比“传进去再脱敏”更安全因为工具函数根本不接收手机号。5. 怎么验证脱敏没有破坏工具调用5.1 验证点一模型输入里不存在敏感明文在 Agent 编排层增加一个检查点构建完 prompt 后扫描一遍敏感正则如果命中则抛错。# validation.py from config import SENSITIVE_RULES def assert_no_sensitive(text: str, context: str unknown): for rule in SENSITIVE_RULES: matches rule.pattern.findall(text) if matches: raise ValueError( f[{context}] 发现敏感字段 {rule.name}: {matches} )这个断言应该放在两个位置一是模型请求发出前二是工具结果返回给模型前。也就是说凡是进入模型的文本都要先经过这道检查。不要依赖模型本身不输出敏感信息模型没有那么可靠。5.2 验证点二工具调用返回结果是否符合预期脱敏最容易造成的问题是占位符被当作真实参数传给工具。所以要验证两条路径正常路径工具收到真实参数后能正确执行。异常路径如果模型把占位符原样传给工具拦截器是否能在执行前恢复。写测试用例时建议模拟这三种情况测试场景输入预期用户输入含手机号查订单 20240815001手机号 13812341234模型上下文无明文手机号模型参数含占位符{order_id: [ORDER_0]}工具执行前恢复为真实值工具返回含地址电话返回字符串含13812341234返回模型前被替换为占位符5.3 验证点三日志与 trace 中不出现敏感值日志脱敏是另一个独立检查点。以 Python 标准库为例可以给logging.Formatter增加过滤逻辑# logger_filter.py import logging from config import TOKEN_PATTERN, SENSITIVE_RULES class SensitiveDataFilter(logging.Filter): def __init__(self): super().__init__() self._patterns [rule.pattern for rule in SENSITIVE_RULES] def filter(self, record: logging.LogRecord) - bool: if isinstance(record.msg, str): for pattern in self._patterns: record.msg pattern.sub(REDACTED, record.msg) elif isinstance(record.args, dict): record.args { k: pattern.sub(REDACTED, str(v)) for k, v in record.args.items() } return True # 使用示例 def setup_logger(): logger logging.getLogger(agent) handler logging.StreamHandler() handler.addFilter(SensitiveDataFilter()) logger.addHandler(handler) logger.setLevel(logging.DEBUG) return logger这里有个原则日志过滤不能只放在打印语句里要放在Handler层。因为框架内部的 trace、回调、第三方库都可能直接打日志只有统一在输出端过滤才能覆盖。6. 常见坑与排查链路6.1 坑一脱敏规则破坏了工具参数现象工具调用经常失败报“参数不存在”或“校验失败”排查发现模型传入的是[PHONE_0]而不是真实手机号。原因脱敏发生在工具调用之前但拦截器没有把占位符恢复成真实值或者工具本身接收了包含占位符的参数。排查链路先看工具调用参数是否含[XXX_n]格式。再看拦截器是否注册到了所有工具上有没有漏补的入口。看restore()是否确实映射到了真实值。看映射表是否在请求结束后被清理导致后续请求无法恢复。解决方案把restore放到统一的工具执行边界不要在每个工具内部手动恢复。6.2 坑二日志比脱敏更早落盘现象日志平台里能看到完整手机号程序代码明明写了脱敏。原因日志打印发生在脱敏前的原始数据阶段或者第三方 SDK 在调用时自己写了日志。日志过滤只覆盖了自己写的 logger没覆盖框架内部日志。排查链路查看日志里敏感值出现的上下文判断是哪个模块打的。检查 stdout 输出、traceback、异常信息是否包含原始入参。检查框架的 verbose / debug 开关必要时关闭或重定向。解决方案日志过滤放在 Handler 层同时关闭框架的 verbose 日志异常信息不要直接打印包含完整入参的对象先做脱敏再记录。6.3 坑三模型通过推理“猜出”敏感信息现象不直接泄露明文但模型根据订单号、地址片段、上下文线索推断出了个人信息。例如用户输入“我在北京朝阳区”工具结果显示地址被脱敏成[ADDR_1]模型却在回答里提示“根据您的地址信息……”这可能是因为模型在 chain-of-thought 里保留了推理痕迹。原因LLM 具备模式补全能力。脱敏只解决了“输入不包含明文”没有解决“模型从其他字段推断”。排查链路检查模型输出是否包含与占位符对应但不存在的值。检查 prompt 里是否允许模型输出链式推理。检查 system prompt 是否明确告诉模型“不要猜测脱敏字段”。解决方案在 system prompt 中加入“如果某个字段显示为占位符不要尝试猜测或补全其真实内容”对高风险场景关闭思维链输出或对敏感字段脱敏到不可逆级别如只保留前三位区号。6.4 常用排查对照表问题现象可能原因检查方式处理建议工具调用失败参数含占位符拦截器未做 restore打印 raw_args 与 restored_args在统一边界调用 restore日志明文泄露日志先于脱敏检索日志平台敏感值过滤下沉到 Handler 层返回结果模型仍能“猜”出脱敏粒度不够查看 prompt 与输出增强脱敏限制推理输出多用户串数据映射表跨请求共享检查 Redis 或内存对象生命周期按请求隔离映射规则误伤正常文本正则过于宽泛审计脱敏前后 diff增加白名单和上下文判断7. 生产环境最佳实践与检查清单7.1 工具权限模型要在编排层做不能靠模型自觉模型是否调用某个工具、能否访问某个字段应该由权限策略控制而不是模型自己决定。建议给每个工具加“可见字段白名单”工具返回结果经过字段投影后再脱敏。例如query_order在内部返回了internal_note字段但在对外 schema 上不体现模型永远看不到这个字段。工具设计上尽量遵循“一工具一职责”query_order_status(order_id)只返回状态和时间不返回用户手机号。query_order_user_profile(order_id)单独返回脱敏后的用户信息并校验调用方权限。这样即使模型误调用也不会一次性拿到多个敏感字段。7.2 区分学习环境与生产环境配置项学习环境生产环境脱敏规则正则即可正规 DLP 服务或实体识别模型令牌映射存储内存字典Redis带过期时间禁用持久化日志可打印调试信息全量脱敏审计日志单独隔离模型选择任意 LLM本地部署或合规模型服务工具权限全部放行按用户、角色、租户隔离异常处理打印堆栈不输出原始入参统一错误码生产环境特别要注意令牌映射表不要写入 Redis 的持久化文件。一旦落盘等于把敏感数据以“映射表”的形式存了一份。Redis 缓存要设置较短的 TTL例如 60 秒请求结束后尽快清理。7.3 发布前检查清单下面是可直接使用的最小检查清单工具 schema 里是否还有多余参数尤其是带敏感含义的参数每个工具的返回值是否都经过字段投影和脱敏日志 Handler 是否过滤了所有输出通道stdout、文件、第三方上报异常信息是否会包含工具入参、数据库连接串或 token令牌映射表是否按请求隔离并设置 TTL模型 prompt 是否明确禁止猜测脱敏字段测试用例是否覆盖了“入参含占位符”和“返回值含敏感字段”两条路径是否保留工具调用审计日志但不记录敏感值7.4 扩展方向如果你的项目已经跑通了上述最小方案下一步可以按这几个方向深入引入专门的数据分类和实体识别能力替代手写正则。建议从presidio、Microsoft Presidio这类开源组件开始评估根据业务数据形态决定是否自训练模型。把脱敏封装成可复用的 middleware 或 decorator。LangChain 的callback机制、自定义Tool包装器都可以承载这套逻辑让业务代码不感知脱敏过程。对接统一的密钥管理和策略中心。工具所需的数据库密码、API Key 统一从密钥管理系统读取不进入环境变量明文更不进入模型上下文。对工具调用链路做 trace 脱敏。LangSmith、Langfuse 等链路追踪产品都支持自定义before/after回调可以在数据上报前做一次过滤。8. 收尾建议敏感数据治理在 Agent 工作流里的核心判断是模型上下文、工具执行上下文、日志审计上下文必须完全隔离脱敏不是一次性动作而是贯穿“输入清洗、工具调用、返回处理、日志输出”四个边界。令牌化方案能保证工具收到真实参数同时让模型和日志只看到占位符这是解决“脱敏不破坏工具调用”的关键。对于正在做 Agent 项目的开发者建议先从工具 schema 的“数据最小化”开始把每个工具入参里非必要的敏感字段删除往往比写一堆脱敏规则更有效。等最小化做到位再补上下文脱敏和日志过滤。最后的难关一定是模型输出侧需要在 prompt 设计和输出校验上同时加约束。把这个最小示例跑通之后把它变成你自己的“脱敏测试台”每新增一个工具都先在这个测试台里验证入参恢复、返回脱敏、日志过滤三个环节再进入正式业务流程。这样积累出来的代码会比临时补丁可靠得多。