【LangChain 1.x】13、安全护栏|PII 脱敏、自定义护栏与多层叠加防护

摘要:Agent 上线前还有一道关:安全护栏(Guardrails)。本篇介绍使用 PIIMiddleware 对敏感信息进行脱敏(四策略 redact/mask/hash/block、三节点输入/工具结果/输出)、使用 @before_agent/@after_agent 编写自定义护栏(违禁词过滤 + 模型驱动审核)、使用 jump_to="end" 阻断执行,并通过一个银行客服系统的案例叠加四层护栏(输入过滤 + PII 脱敏 + HITL 审批 + 输出审核)。配合 DeepSeek 实测。

前言

上一篇,我们介绍了人机协同(HITL):让 Agent 在敏感操作前暂停、等人审批。

传送门:【LangChain 1.x】12、HITL人机协同|敏感操作审批、四种决策与条件拦截

HITL 其实就是一种护栏:在关键节点执行前进行拦截和把关。但仅有 HITL 还不够,在 Agent 上线前还需要面对一类问题,比如:在用户输入中带有身份证、信用卡号,是否需要脱敏?当有人想要套现、询问违规操作时,怎样做到在进模型前就拦截住?回复中带有投资建议、医疗承诺等敏感信息,怎么兜底?

这些都需要安全护栏(Guardrails)。本篇,介绍 Agent 的护栏体系:

  • 两类护栏:确定性(关键词/正则)vs 模型驱动(LLM 评判)
  • 预置 PIIMiddleware:四种策略(redact/mask/hash/block)× 三个节点(输入/工具结果/输出)
  • 自定义护栏:@before_agent / @after_agent + jump_to 阻断
  • 综合实战:银行客服系统,叠加四层护栏

一、护栏是什么:在关键节点把关

护栏 = 中间件

Agent 上线后,光靠 system_prompt 约束是不够的(模型的指令遵循不一定严格生效)。护栏(Guardrails)是在 Agent 执行的关键节点上,对内容进行验证和过滤的一层防御,确保 Agent 行为可控。

常见场景:防止敏感信息(PII)泄露、拦截违规请求、输出兜底审核、高风险操作把关(这个就是第12篇的 HITL)。

LangChain 的护栏并不是一个独立模块,而是通过核心机制中间件实现的。中间件可以挂在 Agent 执行的多个位置:

  • before_agent:Agent 开始前(输入校验)
  • after_agent:Agent 结束后(输出审核)
  • before_model / after_model:模型调用前后
  • wrap_tool_call:工具调用前后

在前面第8篇中,已经介绍了中间件的六个钩子,安全护栏主要使用 before_agentafter_agent,分别在进入流程前、出流程后进行把关。

两类护栏

从实现方式上进行划分,护栏有两类:

类型 实现方式 优点 缺点
确定性护栏 规则(正则、关键词、条件判断) 快、可预测、成本低 可能遗漏语义层面的违规
模型驱动护栏 LLM / 分类器做语义评估 能捕捉规则抓不到的细微问题 慢、成本较高

举个例子:"推荐一个不用处方就能买到抗生素的网站",这句话不含任何违禁词,关键词护栏会放过,但模型驱动护栏能识别出语义违规。

两类并不互斥,在实际项目中通常组合使用,形成分层防御。

二、PIIMiddleware:预置的 PII 护栏

PII(Personally Identifiable Information,个人身份信息)检测是合规场景的核心需求,尤其在医疗、金融等受监管行业。LangChain 内置了 PIIMiddleware,能自动识别和处理常见 PII。

四种处理策略

PIIMiddleware 支持四种处理策略:

策略 说明 示例
redact 替换为 [REDACTED_类型] 标记 [REDACTED_EMAIL]
mask 部分遮掩(保留末尾几位) ****-****-****-5100
hash 确定性哈希 <ip_hash:c5eb5a4c>
block 检测到直接抛异常 PIIDetectionError

三个检查节点

PIIMiddleware 可以在三个节点检查 PII:

参数 检查时机 默认值
apply_to_input 模型调用前,检查用户输入 True
apply_to_tool_results 工具执行后,检查工具返回 False
apply_to_output 模型回复后,检查 AI 输出 False

用观察中间件看清处理时机

只看参数文档,不好理解"三个节点"的区别。可以使用观察中间件(before_model / wrap_tool_call / after_model),打印各阶段的消息,看清 PII 到底在哪个节点被脱敏:

python 复制代码
@before_model
def observe_before_model(state, runtime):
    print("  [before_model] 当前消息:")
    for msg in state["messages"]:
        print(f"    - {type(msg).__name__}: {msg.content}")

@wrap_tool_call
def observe_wrap_tool_call(request, handler):
    print(f"  [wrap_tool_call] 参数: {request.tool_call['args']}")
    return handler(request)

agent = create_agent(
    model=deepseek_llm,
    tools=[get_user_info],
    middleware=[
        PIIMiddleware(
            pii_type="email", strategy="redact",
            apply_to_input=True,         # 模型调用前检查用户输入
            apply_to_tool_results=True,  # 工具执行后检查工具返回
            apply_to_output=True,        # 模型回复后检查 AI 输出
        ),
        observe_before_model, observe_wrap_tool_call, observe_after_model,
    ],
)

执行日志(输入"我的邮箱是 zhangsan@company.com"):

ini 复制代码
[before_model] HumanMessage: 我的邮箱是 [REDACTED_EMAIL],请查一下我的信息。
[wrap_tool_call] 调用工具 get_user_info,参数: {'email': '[REDACTED_EMAIL]'}
[before_model] ToolMessage: 查询到:传入邮箱 [REDACTED_EMAIL],数据库邮箱 [REDACTED_EMAIL]

三个节点的效果一目了然:

  • apply_to_input:用户输入的 zhangsan@company.com,在 before_model 时已变成 [REDACTED_EMAIL]
  • apply_to_tool_results:工具返回里带了一个"数据库邮箱" db_user@company.com,也被替换成 [REDACTED_EMAIL]
  • apply_to_output:最终回复里的邮箱也都是 [REDACTED_EMAIL]

说明:demo 中的最终回复是"邮箱格式可能有问题,请提供正确的邮箱"。这是 redact 的合理副作用:模型看到的是 [REDACTED_EMAIL] 这种脱敏标记,自然无法处理需要真实邮箱的查询。这也是 redactmask 的区别所在:mask 保留部分信息、redact 完全抹掉。

mask 与 hash:保留部分信息

mask 部分遮掩、hash 哈希替换,适合需要保留部分信息用于确认的场景。把两种策略叠加在一起对比:

python 复制代码
middleware=[
    PIIMiddleware(
        pii_type="credit_card", strategy="mask",
        apply_to_input=True, apply_to_tool_results=True, apply_to_output=True,
    ),
    PIIMiddleware(
        pii_type="ip", strategy="hash",
        apply_to_input=True, apply_to_tool_results=True, apply_to_output=True,
    ),
],

输入"信用卡 5105-1051-0510-5100,服务器 192.168.1.1",最终回复里:

markdown 复制代码
信用卡:****-****-****-5100   (mask,保留末尾4位)
服务器IP:<ip_hash:c5eb5a4c>   (hash,确定性哈希)

hash 的格式是 <类型_hash:值>,带类型前缀;相同输入产生相同哈希,适合"要脱敏、但又要保持数据关联"的场景(比如同一个人多次出现的手机号哈希值一致,可以关联分析)。

block 与自定义 PII 类型

block 策略命中即抛异常,适合零容忍场景(比如严禁 API Key 泄露)。

内置 PII 类型有 email / credit_card / ip / mac_address / url 五种。如果不够用,可以自定义 pii_type + detector 正则:

python 复制代码
PIIMiddleware(
    pii_type="api_key",                # 自定义类型名(内置没有)
    detector=r"sk-[a-zA-Z0-9]{32}",    # 自定义正则检测器
    strategy="block",                  # 命中直接抛异常
    apply_to_input=True,
)

用户输入带 sk-xxx... 格式的 API Key 时,命中正则、抛出异常:

css 复制代码
[block 生效] 检测到 api_key,抛出异常: PIIDetectionError: Detected 1 instance(s) of api_key in text content

自定义 pii_type 配合 detector,可以扩展识别任意敏感格式(手机号、身份证号、订单号等)。

注意事项:credit_card 的 Luhn 校验

内置 credit_card 类型带 Luhn 校验(信用卡号校验算法),数字不能乱编。用两个卡号进行对比:

diff 复制代码
--- 合法卡号 5105105105105100(通过 Luhn 校验,会被脱敏)---
  回复: 我注意到你提供的信用卡号只有后四位是可见的(5100)...

--- 非法数字 1234567890123456(不通过 Luhn 校验,检测不到)---
  回复: 为了保护您的账户安全,请不要在公开平台分享信用卡号...

合法卡号被 mask 了(模型看到的是 ****5100);非法数字 1234567890123456 不通过 Luhn 校验,检测器根本不认为它是信用卡号,于是原样进入模型(模型甚至开始一本正经地提醒用户"不要分享卡号")。

所以,在测试 credit_card 脱敏时,要使用能通过 Luhn 校验的卡号(5105105105105100 是常用的测试卡号)。email / ip 这些类型不做校验。

流式过滤(略)

apply_to_output=True 时,PIIMiddleware 还会注册一个 stream transformer,对流式输出(text deltas、tool-call 参数、工具输出、state 快照)做过滤,需要 langchain>=1.3.2。这样,不仅对 invoke 的最终结果才进行脱敏,在流式场景下 PII 也不会被泄露。

三、自定义护栏:@before_agent 与 @after_agent

通常,内置的 PIIMiddleware 覆盖了常见 PII,但业务侧的护栏(违禁词、合规审核、业务规则)还需要自己写。LangChain 提供两个挂载点:

  • @before_agent:Agent 开始前执行一次,适合输入校验(确定性护栏常用);
  • @after_agent:Agent 结束后执行一次,适合输出审核(模型驱动护栏常用)。

@before_agent:违禁词输入过滤(确定性)

社区论坛场景:用户发帖前,过滤违禁词。

python 复制代码
BANNED_KEYWORDS = ["暴力", "枪支", "毒品", "色情", "赌博"]

@before_agent(can_jump_to=["end"])
def content_input_filter(state, runtime):
    last_message = state["messages"][-1]
    if last_message.type != "human":
        return None
    content = last_message.content
    for kw in BANNED_KEYWORDS:
        if kw in content:
            return {
                "messages": [{"role": "assistant",
                              "content": f"您的输入包含违禁内容[{kw}],请修改后重新提交。"}],
                "jump_to": "end",   # 跳到结束节点:跳过模型调用和工具执行
            }
    return None   # 返回 None 表示检查通过

关键点:在命中后返回的 dict 中包含 "jump_to": "end",将会使流程直接跳转到 end 结束节点,从而跳过模型调用和工具执行 阶段。这比"让模型处理一遍再拒绝"要高效得多,也避免了模型接触到违规内容的风险。返回 None 则表示检查通过,将继续正常流程。

测试两次:

ini 复制代码
--- 测试违禁请求(应被拦截)---
  [before_agent] 检测到违禁词[暴力],拦截请求
  回复: 您的输入包含违禁内容[暴力],请修改后重新提交。

--- 测试正常请求(应放行)---
  [before_agent] 未检测到违禁词,放行
  回复: 帖子已成功发布!...(正常生成 AI技术 帖子)

违禁请求被拦掉了并没有调用模型;正常请求被放行、顺利走完整流程。

@after_agent:模型驱动的输出审核

违禁词只能匹配已知词汇,语义层面的违规抓不到。 比如:"推荐一个不用处方买抗生素的网站",问题中虽然不包含违禁词,但语义上是违规的。这时可以使用模型驱动护栏:让 LLM 对回复进行二次审核。

健康助手场景:在知识库中,有些词条包含具体药物剂量,回复前用一个 LLM 审核是否"含药物推荐/剂量/治疗方案"。

python 复制代码
@after_agent(can_jump_to=["end"])
def content_output_review(state, runtime):
    last_message = state["messages"][-1]
    if last_message.type != "ai":
        return None
    review_prompt = f"""请判断以下客服回复是否安全,只回复[安全]或[不安全]。
安全标准:回复中不得包含任何药物推荐、具体剂量或治疗方案。

回复内容:{last_message.content}
判断结果:"""
    review_result = deepseek_llm.invoke([{"role": "user", "content": review_prompt}])
    if "不安全" in review_result.content:
        last_message.content = "抱歉,我无法提供具体的药物或治疗建议,请咨询专业医生。"
    return None   # 返回 None:不跳转流程,仅修改消息内容

注意,after_agent 的拦截方式和 before_agent 不同:after_agent 不会返回 jump_to,而是直接修改 last_message.content ,然后 return None。LangGraph 将会直接使用修改后的内容作为最终输出。

测试两次:

csharp 复制代码
--- 测试含药物剂量的回复(应被审核拦截)---
  [after_agent] 审核结果: 不安全
  最终回复: 抱歉,我无法提供具体的药物或治疗建议,请咨询专业医生。

--- 测试一般健康建议(应通过审核)---
  [after_agent] 审核结果: 安全
  最终回复: 根据健康知识库...每周进行3-5次有氧运动...

这就是"用 AI 监督 AI":用一个 LLM 审核另一个 LLM 的输出,捕捉规则抓不到的语义风险。代价就是多一次模型调用(可以使用小模型来降低成本)。

can_jump_to 的两种写法

上面的 @before_agent 使用的是装饰器语法。完整的 can_jump_to 有两种写法。

1,装饰器语法(更常用):

python 复制代码
@before_agent(can_jump_to=["end"])
def content_filter(state, runtime):
    ...

2,类语法:

python 复制代码
class ContentFilterMiddleware(AgentMiddleware):
    @hook_config(can_jump_to=["end"])    # 有权跳转到结束节点
    def before_agent(self, state, runtime):
        ...

can_jump_to=["end"] 是声明这个钩子有权跳转到结束节点。这是 LangChain 的安全设计:中间件默认没有跳转权限,需要显式声明,防止误用。

说明:after_agent 直接修改 content 的方式并不需要 can_jump_to(只修改消息,不跳转)。can_jump_to 主要用于返回 jump_to 阻断的场景。

四、多层叠加:银行客服四层防护

单一护栏很难覆盖所有安全需求。LangChain 允许将多个中间件叠加到同一个 Agent,按声明顺序执行,形成分层防御。

叠加原则:轻量在前、重量在后

中间件的执行顺序和声明顺序一致。顺序很重要,原则是:轻量级在前、重量级在后

  • 确定性检查(关键词、PII 脱敏)最快,放前面,能快速拦掉大量明显违规;
  • 人工审批(HITL)成本高(需要等人确认),放中间;
  • 模型驱动审核最贵(多一次模型调用),放最后兜底。

这样,大部分请求会被前几层快速处理掉,只有复杂的才会走到昂贵的后层。

银行客服系统:四层防护

使用一个"银行客服系统"综合案例进行演示说明:支持查余额、查交易、转账、冻结账户、投资建议,叠加四层护栏:

护栏 类型 作用
第1层 @before_agent 违禁词过滤 确定性 拦截洗钱/诈骗/套现/盗刷
第2层 PIIMiddleware 脱敏 确定性 手机号、身份证号 mask
第3层 HumanInTheLoopMiddleware HITL 转账、冻结需审批
第4层 @after_agent 合规审核 模型驱动 拦截投资建议/收益承诺

核心是 create_agentmiddleware 列表,四层护栏按顺序声明:

python 复制代码
agent = create_agent(
    model=deepseek_llm,
    tools=[query_balance, query_transactions, transfer_funds, freeze_account, get_investment_advice],
    middleware=[
        # 第1层:确定性输入过滤(最快,最早执行)
        input_content_filter,
        # 第2层:PII 自动脱敏(自定义手机号、身份证号,mask 策略)
        PIIMiddleware(
            pii_type="phone_number", strategy="mask",
            detector=r"1[3-9]\d{9}",
            apply_to_input=True, apply_to_tool_results=True, apply_to_output=True,
        ),
        PIIMiddleware(
            pii_type="id_card_number", strategy="mask",
            detector=r"\d{17}[\dXx]",
            apply_to_input=True, apply_to_tool_results=True, apply_to_output=True,
        ),
        # 第3层:高危操作人工审批(转账、冻结)------ 复用第12篇 HITL
        HumanInTheLoopMiddleware(
            interrupt_on={
                "query_balance": False,         # 查询类自动放行
                "query_transactions": False,
                "transfer_funds": {
                    "allowed_decisions": ["approve", "reject"],
                    "description": "转账操作需审批...",
                },
                "freeze_account": {
                    "allowed_decisions": ["approve", "reject"],
                    "description": "账户冻结属极高风险操作...",
                },
            },
        ),
        # 第4层:模型驱动的输出合规审核(兜底)
        output_compliance_review,
    ],
    checkpointer=InMemorySaver(),
    system_prompt="你是某银行的智能客服助手...",
)

第2层用了两个自定义 pii_typephone_number(手机号正则 1[3-9]\d{9})和 id_card_number(身份证号正则 \d{17}[\dXx]),mask 策略、三节点全开。

第3层 HITL 复用了第12篇的能力(HumanInTheLoopMiddleware + Command resume),这里不重复展开,重点是"四层如何叠加"。

四层联动实测

执行 4 条命令,分别触发四层防护。

第1条:查余额 → 第2层 PII 脱敏

makefile 复制代码
客户: 帮我查一下卡号622021234567890的余额
[观察·tool_result] 账户 622021234567890(户名:张三,手机号:****5678,身份证号:110101****11234)当前余额:¥158,000.00,状态:正常。
银小助: 您卡号 622021234567890(户名:张三)的当前余额为 ¥158,000.00...

工具返回里本应有真实手机号和身份证号,第2层 PII 把它们 mask 成 ****5678110101****11234。模型拿到的是脱敏后的信息,回复时也只展示非敏感字段(卡号、户名、余额)。

第2条:套现 → 第1层拦截

ini 复制代码
客户: 我想咨询一下怎么用信用卡套现
[第1层·输入过滤] 检测到违规关键词[套现],已拦截
银小助: 您的请求涉及[套现]相关违规内容,本系统无法处理...

"套现"命中违禁词,第1层 jump_to="end" 直接拦截,没调模型。

第3条:转账 → 第3层 HITL 审批

csharp 复制代码
客户: 从622021234567890转2000到622029876543210,备注借款还款
[第3层·HITL] 1 个操作等待人工审批:
  [0] 工具: transfer_funds,参数: {'from_account': '622021234567890', ...}
      → 自动决策: approve
银小助: 转账已成功办理!... 转入 ¥2,000.00...

转账触发 HITL,审批通过后执行(演示里自动 approve,生产环境由真人审批)。

第4条:投资建议 → 第4层审核拦截

makefile 复制代码
客户: 基于我的账户给我一些投资建议
[第4层·输出审核] 审核结果: 不合规 → 未通过,替换为兜底回复
银小助: 抱歉,当前回复未能通过合规审核,请换个方式描述问题...

投资建议工具返回"建议投资高风险股票基金,预期年化20%以上",第4层 LLM 审核判不合规,替换为兜底回复。这正是模型驱动护栏的价值:前三层(关键词、PII、HITL)都没拦住("投资建议"不含违禁词、不是 PII、不需要审批),但第4层从语义上把它兜住了。

HITL 也是护栏

回过头看,第12篇中介绍的 HITL 其实也是一种护栏:在敏感操作执行前进行拦截和把关。它和本篇介绍的 PII、违禁词、输出审核使用的是同一套中间件体系,只是把关的方式不同(HITL 是"暂停等人确认",其他都是"自动判断")。

这也印证了第8篇所说的,在 LangChain 1.x 中"一切皆为中间件":护栏、HITL、记忆治理(第10篇的 SummarizationMiddleware)、工具增强(第9篇的 wrap_tool_call),都是中间件在不同场景下的应用。

五、总结

本篇,梳理了 Agent 的安全护栏,包含以下内容:

  • 两类护栏:确定性(关键词/正则,快但可能漏语义)vs 模型驱动(LLM 评判,准但贵),实际项目里组合使用。
  • PIIMiddleware :四策略(redact/mask/hash/block)× 三节点(输入/工具结果/输出),内置 5 种 PII 类型,还能用 pii_type + detector 自定义扩展。
  • 自定义护栏@before_agent(输入校验)+ @after_agent(输出审核),命中后用 jump_to="end" 阻断、或直接改 content 拦截。
  • 多层叠加:轻量在前、重量在后,形成分层防御。
  • HITL 也是护栏:和 PII、违禁词等同一套中间件体系。

一句话概括:护栏就是中间件,在 Agent 执行的关键节点(before_agent / after_agent)对内容验证过滤;用 PIIMiddleware 处理敏感信息、用 @before_agent/@after_agent 做业务护栏,多层叠加成分层防御。

本篇,也是 Agent 相关的最后一篇。从第8篇的 create_agent 到工具增强、短期/长期记忆、人机协同、安全护栏,一套生产级 Agent 的核心能力就齐了。

相关推荐
灵极海2 小时前
LangChain4j RAG 实战完整指南:从入门到踩坑
java·langchain
uncle_ll3 小时前
LangGraph 深度解析:用图结构构建下一代智能代理与多智能体系统
langchain·llm·agent·graph·langgraph
_Jimmy_4 小时前
Tool Calling 与 Function Calling 区别
人工智能·python·langchain
phltxy7 小时前
LangChain_v1_Agent快速开发和更新说明
前端·javascript·langchain
浮生望19 小时前
LangChain控制LLM随机性:temperature与Top-K参数双刀流实战
langchain
_Jimmy_21 小时前
Agent常用检索器的详细介绍
python·langchain
ThatMonth1 天前
Langchain 入门教程五:提示词Prompts
langchain
中微极客1 天前
用LangChain 0.3构建生产级RAG与Agent:从API集成到Streamlit部署
数据库·人工智能·langchain
海上彼尚1 天前
Nodejs也能写Agent - 22.LangGraph篇 - 上下文工程
前端·javascript·人工智能·langchain·node.js