news 2026/8/31 7:46:18

LLM Agent敏感数据治理:脱敏与工具调用如何同时成立?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM Agent敏感数据治理:脱敏与工具调用如何同时成立?

敏感数据治理与 Agent 工具调用之间,天生存在一对矛盾:Agent 要完成业务操作,通常需要读取订单号、用户手机号、内部账号、数据库连接参数等信息;但把这些信息原样灌进 LLM 的上下文,又会造成数据泄露、日志留痕、越权推理等多重风险。实际项目里最常见的失败,不是模型答不出来,而是脱敏做得太粗暴,把工具调用参数一起洗掉了,导致 Agent 拿到一个被替换过的占位符去查库,返回结果要么为空,要么报错。

这篇文章从一条完整的 LLM Agent 调用链路出发,先说明敏感数据在哪个环节容易被泄露,再给出“上下文脱敏、数据最小化、令牌化恢复”这三条核心策略,最后用一个可运行的 Python 示例演示如何让脱敏和工具调用同时成立。文章会给出代码、参数说明、验证方法和排查清单,适合正在做 Agent 开发、RAG 应用或企业内部 LLM 工具的开发者参考。

1. 先摸清敏感数据在 Agent 工作流里的流动路径

1.1 一条典型 Agent 调用链路中有哪些环节

一个标准的 LLM Agent 工作流,看起来是“用户提问 -> 模型理解 -> 模型决定调用哪个工具 -> 工具执行 -> 结果返回给模型 -> 模型组织回答”。但拆开看,数据会在五个位置出现:

  1. 用户输入本身:可能包含手机号、身份证号、公司内部项目名。
  2. 系统提示词与上下文构建:开发者可能在 system prompt 里写入了内部接口地址、账号信息。
  3. 模型生成的工具调用参数:例如query_order(order_id="...")中的参数。
  4. 工具执行结果:数据库返回的完整记录、第三方 API 响应体。
  5. 链路日志与回调数据:LangChain 等框架默认会把每次模型请求、工具输入输出写入 trace。

很多团队只在第 3 步做了简单的“不让模型返回密码字段”,忽略了第 1、2、4、5 步,结果敏感数据依然从日志和上下文中泄露。

1.2 风险面要按“可见性”分三层看待

下面这张表展示了敏感数据在不同层级的可见程度:

数据位置对谁可见典型风险
LLM 上下文(prompt + response)模型服务商、本地模型进程上下文被记录、模型被诱导输出敏感内容
工具调用参数工具系统、日志系统、调用链追踪参数明文进入日志或消息队列
工具返回结果Agent 编排层、模型上下文、外部系统返回结果被模型进一步加工后写回日志
日志与监控运维、日志平台、第三方分析工具脱敏之前就落盘,事后无法撤回

这里要特别提醒:如果接的是外部 LLM API,prompt 和 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_id="2024-0815-001")

而不是:

query_order(user_phone="13812341234", user_id_card="...", db_password="...")

最小化原则落地时,要对工具接口做一次“入参审计”:每个参数是否真的是执行该操作所必需的?如果不需要,就不应该出现在工具 schema 描述里,更不应该由模型随意填写。

2.3 方案三:令牌化与引用传递,工具侧恢复真实值

这是解决“脱敏后工具调用失败”的关键。流程是:

  1. 用户输入中的敏感值被替换为令牌,如[TOKEN_ORDER_USER_0032]
  2. LLM 上下文只包含令牌。
  3. 模型生成工具调用时,参数里出现的是令牌。
  4. Agent 编排层在真正调用工具之前,把令牌替换回真实值。
  5. 工具执行完成后,返回结果再次被脱敏,然后才送回模型上下文。

这样做的好处是:模型、日志、追踪系统只看到令牌,工具却能获得完整参数。坏处是:令牌映射表必须生命周期极短,并且不能把映射表写进日志。

2.4 三种方案对比

方案优点缺点适用场景
上下文脱敏实现简单,改动小规则覆盖有限,模型可能推断原文常见 PII 字段,如手机号、邮箱
数据最小化从源头减少敏感数据量需要重新设计工具接口和 schema工具入参设计阶段
令牌化恢复既保护上下文又能保证工具执行成功需要维护映射表,增加编排复杂度必须使用真实参数查询的工具

实际项目通常组合使用:先做数据最小化,再对不可避免进入上下文的字段做脱敏,最后在工具调用边界做令牌恢复。

3. 环境准备与最小项目结构

3.1 技术选型说明

下面示例使用 Python + LangChain 风格的抽象,但实现不绑定具体框架。你完全可以用 LlamaIndex、自研编排代码或 TypeScript 版本复现同样的思路。关键不是框架,而是四个组件:

  • 脱敏器:负责识别和替换敏感字段。
  • 令牌映射器:维护“占位符 -> 真实值”的双向映射。
  • 工具调用拦截器:在执行工具前恢复真实值,在执行后重新脱敏。
  • 日志过滤器:防止脱敏前的数据落盘。

建议环境:

依赖项说明
Python 3.10+类型注解和异步支持更友好
langchain-core工具抽象与消息封装,可选
pydantic配置校验与工具参数模型
tiktoken估算 token 长度,便于检查脱敏后上下文大小

如果原始项目没有固定版本,落地前先确认langchainllama_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(frozen=True) class SensitiveRule: name: str placeholder_prefix: str pattern: re.Pattern # 演示用规则,生产环境请根据真实数据范围扩展 SENSITIVE_RULES = [ SensitiveRule( name="phone", placeholder_prefix="PHONE", pattern=re.compile(r"1[3-9]\d{9}"), ), SensitiveRule( name="address", placeholder_prefix="ADDR", pattern=re.compile(r"(?<=地址[::])[^\n,,。;;]+"), ), SensitiveRule( name="id_card", placeholder_prefix="IDCARD", pattern=re.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, _rule=rule): 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

这里要解释几个易错点:

  1. 为什么redact要“先长后短”?假设地址文本里恰好包含手机号,如果地址规则先命中,手机号就不会被单独替换。实际项目中,规则按“最长优先、最具体优先”排序。
  2. 为什么restore只能用于工具调用前,不能用于返回给模型的文本?因为返回给模型的文本即使恢复了真实值,也会成为模型上下文的一部分,必须用publicize再次脱敏。
  3. 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": "user@example.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]而不是真实手机号。

原因:脱敏发生在工具调用之前,但拦截器没有把占位符恢复成真实值,或者工具本身接收了包含占位符的参数。

排查链路:

  1. 先看工具调用参数是否含[XXX_n]格式。
  2. 再看拦截器是否注册到了所有工具上,有没有漏补的入口。
  3. restore()是否确实映射到了真实值。
  4. 看映射表是否在请求结束后被清理,导致后续请求无法恢复。

解决方案:把restore放到统一的工具执行边界,不要在每个工具内部手动恢复。

6.2 坑二:日志比脱敏更早落盘

现象:日志平台里能看到完整手机号,程序代码明明写了脱敏。

原因:日志打印发生在脱敏前的原始数据阶段,或者第三方 SDK 在调用时自己写了日志。日志过滤只覆盖了自己写的 logger,没覆盖框架内部日志。

排查链路:

  1. 查看日志里敏感值出现的上下文,判断是哪个模块打的。
  2. 检查 stdout 输出、traceback、异常信息是否包含原始入参。
  3. 检查框架的 verbose / debug 开关,必要时关闭或重定向。

解决方案:日志过滤放在 Handler 层,同时关闭框架的 verbose 日志;异常信息不要直接打印包含完整入参的对象,先做脱敏再记录。

6.3 坑三:模型通过推理“猜出”敏感信息

现象:不直接泄露明文,但模型根据订单号、地址片段、上下文线索推断出了个人信息。例如用户输入“我在北京朝阳区”,工具结果显示地址被脱敏成[ADDR_1],模型却在回答里提示“根据您的地址信息……”,这可能是因为模型在 chain-of-thought 里保留了推理痕迹。

原因:LLM 具备模式补全能力。脱敏只解决了“输入不包含明文”,没有解决“模型从其他字段推断”。

排查链路:

  1. 检查模型输出是否包含与占位符对应但不存在的值。
  2. 检查 prompt 里是否允许模型输出链式推理。
  3. 检查 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 发布前检查清单

下面是可直接使用的最小检查清单:

  1. 工具 schema 里是否还有多余参数,尤其是带敏感含义的参数?
  2. 每个工具的返回值是否都经过字段投影和脱敏?
  3. 日志 Handler 是否过滤了所有输出通道(stdout、文件、第三方上报)?
  4. 异常信息是否会包含工具入参、数据库连接串或 token?
  5. 令牌映射表是否按请求隔离并设置 TTL?
  6. 模型 prompt 是否明确禁止猜测脱敏字段?
  7. 测试用例是否覆盖了“入参含占位符”和“返回值含敏感字段”两条路径?
  8. 是否保留工具调用审计日志,但不记录敏感值?

7.4 扩展方向

如果你的项目已经跑通了上述最小方案,下一步可以按这几个方向深入:

  • 引入专门的数据分类和实体识别能力,替代手写正则。建议从presidioMicrosoft Presidio这类开源组件开始评估,根据业务数据形态决定是否自训练模型。
  • 把脱敏封装成可复用的 middleware 或 decorator。LangChain 的callback机制、自定义Tool包装器都可以承载这套逻辑,让业务代码不感知脱敏过程。
  • 对接统一的密钥管理和策略中心。工具所需的数据库密码、API Key 统一从密钥管理系统读取,不进入环境变量明文,更不进入模型上下文。
  • 对工具调用链路做 trace 脱敏。LangSmith、Langfuse 等链路追踪产品都支持自定义before/after回调,可以在数据上报前做一次过滤。

8. 收尾建议

敏感数据治理在 Agent 工作流里的核心判断是:模型上下文、工具执行上下文、日志审计上下文必须完全隔离,脱敏不是一次性动作,而是贯穿“输入清洗、工具调用、返回处理、日志输出”四个边界。令牌化方案能保证工具收到真实参数,同时让模型和日志只看到占位符,这是解决“脱敏不破坏工具调用”的关键。

对于正在做 Agent 项目的开发者,建议先从工具 schema 的“数据最小化”开始:把每个工具入参里非必要的敏感字段删除,往往比写一堆脱敏规则更有效。等最小化做到位,再补上下文脱敏和日志过滤。最后的难关一定是模型输出侧,需要在 prompt 设计和输出校验上同时加约束。

把这个最小示例跑通之后,把它变成你自己的“脱敏测试台”:每新增一个工具,都先在这个测试台里验证入参恢复、返回脱敏、日志过滤三个环节,再进入正式业务流程。这样积累出来的代码,会比临时补丁可靠得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 7:45:50

AI调试实战:结构化上下文让Grok与GPT成为得力助手

有一回我帮朋友看一个 Java 服务偶发超时的问题。他开了一下午 Debug&#xff0c;断点打在服务入口和数据库查询之间&#xff0c;发现有时能跳进去&#xff0c;有时直接卡住。他把日志贴给 AI 助手问怎么回事&#xff0c;对方只回了一句&#xff1a;可能是数据库连接池耗尽。他…

作者头像 李华
网站建设 2026/8/31 7:41:50

Umi-OCR完整上手指南:离线OCR截图与批量识别安装教程

Umi-OCR完整上手指南&#xff1a;离线OCR截图与批量识别安装教程 【免费下载链接】Umi-OCR OCR software, free and offline. 开源、免费的离线OCR软件。支持截屏/批量导入图片&#xff0c;PDF文档识别&#xff0c;排除水印/页眉页脚&#xff0c;扫描/生成二维码。内置多国语言…

作者头像 李华
网站建设 2026/8/31 7:37:52

电机产线检测:PWM调速、转速测量与NVH分析实战

在电机产线检测中&#xff0c;PWM 信号、转速测量、NVH 声学分析这三件事经常被放在同一个工位完成。生产一台直流电机&#xff0c;从装配完成到下线&#xff0c;通常要验证它在给定 PWM 占空比下能否达到目标转速&#xff0c;同时监听它在运行过程中的振动和噪声是否超标。难点…

作者头像 李华
网站建设 2026/8/31 7:28:01

牛客模考一模实战解析:程序员笔试避坑与提分策略

每年到了春招和秋招的节点&#xff0c;牛客网上的模考活动就成了程序员求职者的“练兵场”。我自己经历了两次校招、又帮团队做过几次笔试题筛选&#xff0c;对“牛客模考&#xff08;一模&#xff09;”这类线上模拟笔试的含金量还是比较认可的。它最大的价值不是押中原题&…

作者头像 李华
网站建设 2026/8/31 7:27:16

B站2019秋招笔试编程题解析:字符串与动态规划实战指南

1. 2019秋招笔试全景&#xff1a;题型分布与考察逻辑先交代一下背景。2019年那会儿&#xff0c;B站的秋招笔试还远没有现在这么卷&#xff0c;但已经能看出这家公司的出题口味和一般互联网大厂不太一样。我当时完整刷过那一批题目&#xff0c;也帮学弟学妹做过复盘&#xff0c;…

作者头像 李华