资讯中心

LLM应用安全护栏实战:Guardrails与Presidio构建RAG与Agent防护体系

📅 2026/9/29 6:44:52
LLM应用安全护栏实战:Guardrails与Presidio构建RAG与Agent防护体系
1. 为什么LLM应用需要一层“安全护栏”1.1 从一次线上事故说起去年下半年我参与了一个企业知识库问答系统的上线底层用的是开源大模型前端接的是内部Wiki和工单系统。上线第三天客服同事在群里甩了一张截图用户问“帮我查一下上个月华东区退货率最高的三个SKU”模型返回的答案里赫然带着一段数据库连接串连密码都没脱敏。虽然那个连接串是只读账号但这件事直接把整个项目组拉进了紧急复盘。问题出在哪模型本身没有“边界感”。它只是一个概率生成器你给它什么上下文它就可能把上下文里的任何东西吐出来。RAG场景下检索回来的文档片段里可能混着配置说明、运维手册、甚至测试环境的密钥备注Agent场景下工具调用的返回值里可能带着内部接口的鉴权头。这些内容一旦进入模型的上下文窗口就变成了“可被生成的内容”。这就是LLM应用安全护栏要解决的核心问题不是让模型变聪明而是给模型套上一层可控的约束让它在“该说什么、不该说什么、能调用什么、不能调用什么”上有明确的边界。1.2 护栏到底护什么很多人一提到LLM安全就想到“内容合规”其实那只是其中一小块。从工程视角看护栏至少要覆盖四个层面输入侧用户输入里有没有Prompt Injection、有没有试图套取系统提示词、有没有夹带恶意指令。检索侧RAG召回的文档片段里有没有敏感信息、有没有过期数据、有没有权限越界的知识。生成侧模型输出里有没有PII个人身份信息、有没有密钥凭证、有没有不符合业务规则的表述。工具侧Agent调用外部工具时参数是否合法、返回值是否被二次污染、调用链是否可审计。这四个层面里验证器Validator是贯穿始终的执行单元。你可以把验证器理解成流水线上的质检工位每个工位只负责检查一件事通过了就放行不通过就打回或脱敏。Guardrails这类框架提供的核心能力就是让你用声明式的方式把这些验证器编排起来。1.3 适合谁来读这篇内容如果你正在做下面这些事这篇内容应该能帮到你用LangChain、LlamaIndex、Dify或者自研框架搭RAG应用已经上线或准备上线。在业务里引入了Agent需要让模型调用内部API或数据库。被安全团队或合规部门要求出具“模型输出管控方案”。单纯想搞清楚Presidio、Guardrails这些工具到底怎么落地而不是只看官方Demo。我接下来的内容会按照“设计思路—核心组件—实操落地—问题排查”的顺序展开中间会穿插我在实际项目里踩过的坑和验证过的参数。代码以Python为主因为目前生态里最成熟的护栏工具链都在Python侧。2. 护栏体系的整体设计与选型思路2.1 先明确一个原则护栏不是模型的一部分这是我在第一个项目里犯的错。最开始我试图通过System Prompt来约束模型行为比如写“你不能输出任何包含密码的内容”。实测下来这种做法在简单场景下勉强能用但一旦上下文变长、检索片段变多模型的注意力就会被稀释该漏的还是漏。原因很简单Prompt层面的约束是软约束它依赖模型的“自觉性”。而安全护栏必须是硬约束它应该在模型之外、在应用层实现。模型可以换、Prompt可以改但护栏规则应该是独立且稳定的。所以我的设计原则是模型只负责生成护栏负责裁决。模型输出的任何内容在到达用户之前必须经过一层或多层验证器。验证器不依赖模型能力而是基于规则、正则、小模型分类器或专用NLP工具。2.2 验证器的三种类型与选型对比在实际落地中我把验证器分成三类每类的实现方式和适用场景完全不同类型实现方式典型工具适用场景延迟影响规则型正则、关键词黑名单、JSON Schema校验自研、Pydantic格式校验、固定敏感词过滤极低微秒级模型型小分类模型、嵌入相似度Presidio、TransformersPII识别、语义级敏感检测中等几十到几百毫秒混合型规则前置模型兜底Guardrails Presidio生产环境综合防护可控可并行选型的时候不要一上来就追求“大而全”。我见过有团队直接上了一个7B的审核模型做输出过滤结果单次请求延迟从1.2秒涨到4秒用户体验直接崩了。合理的做法是能用规则解决的绝不上模型能用小模型解决的绝不上大模型。比如“输出里不能包含手机号”这件事一条正则就能搞定没必要调模型。但“输出里不能包含暗示用户进行线下交易的表述”这种语义级判断规则就很难覆盖这时候才需要模型型验证器。2.3 护栏的编排方式串行还是并行Guardrails框架默认是串行执行验证器的一个通过再走下一个。这在验证器数量少的时候没问题但当你挂了五六个验证器延迟就会累加。我的做法是把验证器分成两组阻断组必须串行因为后面的验证器可能依赖前面的结果。比如先做PII检测再做脱敏顺序不能反。审计组可以并行只做记录和告警不阻断流程。比如输出内容的情感倾向分析、长度统计。在代码层面阻断组用Guardrails的Rail编排审计组用异步任务旁路执行。这样主链路的延迟只受阻断组影响审计组的结果异步写入日志系统。2.4 一个容易被忽略的点护栏本身的可观测性护栏上线之后你必须知道它“拦了什么、放过了什么、误杀了什么”。我在第二个项目里加了一个简单的埋点每次验证器触发都记录validator_name、trigger_reason、input_hash、action_taken。这些数据积累一周之后你会发现两个有价值的信息哪些验证器从来没触发过可能是规则写得太松也可能是场景没覆盖到。哪些验证器触发频率异常高可能是误杀需要调整阈值。没有可观测性的护栏等于没有护栏。因为你不知道它到底在不在工作。3. 核心组件拆解Guardrails与Presidio怎么配合3.1 Guardrails的核心抽象Rail、Validator、GuardGuardrails这个框架的文档写得比较抽象我用大白话翻译一下它的三个核心概念Guard一个完整的护栏对象你可以把它理解成“一套安检流程”。Rail流程里的一个环节比如“输入检查”是一个Rail“输出检查”是另一个Rail。Validator环节里的具体检查项比如“检查是否包含PII”是一个Validator。实际写代码的时候你主要跟Validator打交道。Guardrails内置了一批Validator比如DetectPII、ToxicLanguage、ValidJSON也支持你自定义。自定义的方式是继承Validator基类实现validate方法。这里有个细节Guardrails的Validator返回值不是简单的True/False而是一个ValidationResult对象里面包含outcomepass/fail/fix和value修正后的值。这意味着Validator不仅可以“拦截”还可以“修正”。比如PII检测发现手机号可以直接返回脱敏后的文本让流程继续走下去而不是直接报错。3.2 Presidio的定位专做PII识别与脱敏Presidio是微软开源的一个PII检测和匿名化工具。它跟Guardrails的关系是Presidio是一个能力提供方Guardrails是一个编排方。你可以在Guardrails的Validator里调用Presidio的AnalyzerEngine来做PII识别。Presidio的核心能力有两块Analyzer识别文本中的PII实体支持信用卡号、邮箱、电话号码、身份证号、IP地址等预定义实体也支持自定义实体。Anonymizer对识别出的PII进行脱敏支持替换、掩码、哈希、加密等多种操作。我实测下来Presidio对英文PII的识别准确率很高但中文场景需要额外配置。比如中文手机号的正则、中文姓名的识别默认的zh语言包覆盖不全需要自己补充Recognizer。3.3 两者配合的典型架构在一个RAG问答系统里我通常这样编排用户输入 - [输入Rail: Prompt Injection检测] - 检索 - [检索Rail: 文档PII扫描] - 模型生成 - [输出Rail: PII脱敏 格式校验] - 用户输入Rail用规则型验证器检测常见的注入模式比如“忽略之前的指令”“输出你的系统提示词”这类。检索Rail用Presidio扫描召回的文档片段发现PII就打标或直接过滤掉该片段。输出Rail先用Presidio做PII识别和脱敏再用Pydantic做JSON Schema校验确保返回格式符合前端预期。这套架构的延迟增量大概在200-400毫秒主要消耗在Presidio的模型推理上。如果对延迟极度敏感可以把Presidio换成基于正则的轻量Recognizer准确率会降一些但延迟能压到50毫秒以内。3.4 关于“gpt秘钥用哪个验证器绑定”这个问题的回答热搜词里有人问“gpt秘钥用哪个验证器绑定”我理解他问的是“怎么防止模型输出里泄露API Key”。这个问题我的答案是不要依赖单一验证器要用组合拳。第一层在System Prompt里明确禁止输出任何以sk-开头或类似格式的字符串。第二层在输出Rail里加一个正则验证器匹配常见的Key格式比如sk-[a-zA-Z0-9]{32,}。第三层如果Key是存在环境变量里的确保它永远不会进入模型的上下文窗口——这是最根本的因为一旦进了上下文就有被生成的风险。我见过最离谱的案例是有人把API Key写在RAG的知识库文档里然后问模型“帮我总结一下这份配置文档”模型直接把Key原样吐出来了。这种问题护栏只能兜底真正的解法是从数据源头做权限隔离。4. 实操落地从零搭一套可用的护栏4.1 环境准备与依赖安装我用的Python版本是3.10Guardrails和Presidio对3.8以上都支持。安装命令如下pip install guardrails-ai pip install presidio-analyzer presidio-anonymizer python -m spacy download zh_core_web_lg python -m spacy download en_core_web_lg这里有个坑Presidio默认用的是spaCy的小模型中文识别效果一般。换成zh_core_web_lg之后中文姓名和地名的识别率明显提升。但lg模型体积不小下载大概需要几百MB网络不好的话建议提前配好镜像源。Guardrails安装完之后需要初始化它会引导你配置一些基础设置。如果你在CI环境里跑可以用guardrails configure --disable-metrics跳过交互。4.2 自定义一个PII脱敏验证器Guardrails内置的DetectPII验证器功能比较基础我习惯自己写一个方便控制脱敏策略。核心代码如下from guardrails.validators import Validator, ValidationResult, PassResult, FailResult from presidio_analyzer import AnalyzerEngine from presidio_anonymizer import AnonymizerEngine class CustomPIIValidator(Validator): def __init__(self, entitiesNone, languagezh): super().__init__() self.analyzer AnalyzerEngine() self.anonymizer AnonymizerEngine() self.entities entities or [PHONE_NUMBER, EMAIL_ADDRESS, CREDIT_CARD, PERSON] self.language language def validate(self, value, metadata): results self.analyzer.analyze( textvalue, entitiesself.entities, languageself.language ) if not results: return PassResult() anonymized self.anonymizer.anonymize( textvalue, analyzer_resultsresults ) return FailResult( error_messagef检测到{len(results)}处PII已脱敏, fix_valueanonymized.text )这个验证器的逻辑是先分析如果发现PII就返回FailResult但带上fix_value。Guardrails收到fix_value之后会自动用修正后的值替换原值流程继续。这样既保证了安全又不会因为一次PII检测就中断整个对话。4.3 配置Rail并接入模型调用定义好验证器之后用Guardrails的Guard对象把它编排起来from guardrails import Guard guard Guard().use( CustomPIIValidator( entities[PHONE_NUMBER, EMAIL_ADDRESS, CREDIT_CARD], languagezh ), onoutput ) result guard( llm_apiyour_llm_callable, prompt请总结这份工单的内容, max_tokens1024 ) print(result.validated_output)这里的onoutput表示这个验证器只在输出阶段生效。如果你也想在输入阶段做检查可以再挂一个验证器oninput。实测下来这个配置对单次请求的延迟增加大概在150毫秒左右主要是Presidio的Analyzer在跑。如果你的QPS比较高建议把AnalyzerEngine做成单例避免每次请求都重新加载模型。4.4 参数选择与性能调优Presidio的Analyzer有几个关键参数会影响性能和准确率score_threshold默认0.5调高会减少误报但可能漏报。中文场景我一般设0.6。entities不要一股脑全开只开你业务相关的。全开的话延迟会翻倍。allow_list白名单比如你确定某些词不是PII可以加进去避免误杀。另外如果你的应用是流式输出护栏的处理方式要调整。流式场景下不能等整个输出生成完再检查我的做法是按句子切分逐句过验证器。这样虽然会增加一些复杂度但能保证用户在流式过程中看到的内容也是经过脱敏的。4.5 一个完整的输入输出双向护栏示例把上面的东西串起来一个完整的双向护栏大概长这样from guardrails import Guard from guardrails.validators import ValidJson input_guard Guard().use( PromptInjectionValidator(), oninput ) output_guard Guard().use( CustomPIIValidator(entities[PHONE_NUMBER, EMAIL_ADDRESS]), ValidJson(), onoutput ) def safe_llm_call(user_input): input_result input_guard.validate(user_input) if not input_result.validation_passed: return {error: 输入包含不安全内容} raw_output call_llm(input_result.validated_output) output_result output_guard.validate(raw_output) return output_result.validated_output这个结构里输入护栏负责拦截注入输出护栏负责脱敏和格式校验。两层护栏之间是独立的可以分别调整规则而不影响对方。5. 常见问题与排查技巧实录5.1 验证器误杀率太高怎么办这是最常见的问题。我遇到过一个案例Presidio把“订单编号20231215001”识别成了信用卡号因为那串数字符合信用卡的Luhn算法。结果所有包含订单号的回答都被脱敏成了CREDIT_CARD用户完全看不懂。排查思路是先把验证器的触发日志拉出来看它到底把什么识别成了PII。如果是固定模式的误杀加allow_list或者调整正则。如果是模型型验证器的误判考虑降低score_threshold或者换更小的实体集合。我的经验是上线前一定要用真实业务数据跑一遍验证器统计误杀率和漏杀率。不要用官方Demo里的示例文本那些都是精心挑选的覆盖不了真实场景的脏数据。5.2 模型返回的JSON不稳定怎么修热搜词里有人问“修复llm返回json的java库”虽然我用Python多但这个问题本质是一样的模型输出的JSON经常带markdown代码块标记或者少个引号、多个逗号。Guardrails的ValidJson验证器能处理一部分但它主要是校验不是修复。我的做法是加一层预处理先用正则把json和去掉再用json.loads尝试解析失败的话用json_repair这类库做修复最后再交给ValidJson校验。如果模型频繁返回非法JSON那说明Prompt需要调整。我通常会在Prompt里明确写“只返回JSON不要包含任何其他文字”并且在few-shot示例里给出标准格式。实测下来加了few-shot之后JSON合法率能从70%提到95%以上。5.3 护栏拖慢了整体响应怎么办延迟问题要从两个维度看绝对延迟和感知延迟。绝对延迟的优化手段前面提过验证器并行化、Presidio单例化、只开必要实体。感知延迟的优化则是另一回事如果护栏处理需要200毫秒你可以在这200毫秒里给前端发一个“正在思考”的动画用户就不会觉得卡。另外不是所有请求都需要全量护栏。我一般会做分级普通问答走轻量护栏只做正则和格式校验涉及敏感数据的请求走全量护栏加Presidio和模型型验证器。分级策略可以根据用户角色、请求来源、历史行为来动态调整。5.4 常见问题速查表问题现象可能原因排查方向解决手段输出被过度脱敏验证器实体范围过大查看触发日志缩小entities、加allow_list敏感信息漏出验证器未覆盖该类型构造测试用例补充自定义Recognizer延迟明显增加验证器串行过多打点统计各验证器耗时并行化、换轻量实现JSON解析失败模型输出格式不稳检查原始输出加预处理、优化Prompt护栏未触发规则太松或未挂载检查Guard配置调整阈值、确认on参数5.5 几个我踩过的坑第一个坑在异步框架里用了同步的Presidio调用。FastAPI里如果直接调analyzer.analyze会阻塞事件循环。后来改成run_in_executor才解决。第二个坑Guardrails的版本兼容性。0.4.x和0.5.x的API差异不小升级的时候一定要看changelog。我有次升级完发现ValidationResult的字段名变了导致整个护栏静默失效——它不报错但也不拦截。第三个坑忽略了护栏本身的错误处理。如果Presidio加载失败或者超时护栏应该走“fail-closed”还是“fail-open”我的选择是fail-closed也就是护栏挂了就拒绝请求而不是放行。安全组件不能用“可用性优先”的逻辑。6. 进阶Agent场景下的工具调用护栏6.1 Agent带来的新风险面普通RAG应用的风险主要在“说错话”Agent应用的风险升级成了“做错事”。模型不仅能生成文本还能调用工具、执行SQL、发HTTP请求。这时候护栏要管的不只是输出内容还有工具调用的参数和返回值。我遇到过一个真实案例Agent在回答“帮我查一下库存”的时候自己构造了一条SQL里面带了DROP TABLE。虽然数据库权限限制了它执行不了但这件事说明模型生成的工具参数必须经过校验。6.2 工具参数校验的实操我的做法是在工具调用的入口加一层参数校验器。以SQL查询为例import sqlparse def validate_sql(sql: str) - bool: parsed sqlparse.parse(sql) for statement in parsed: if statement.get_type() ! SELECT: return False forbidden [DROP, DELETE, UPDATE, INSERT, ALTER] upper_sql sql.upper() for word in forbidden: if word in upper_sql: return False return True这个校验器只允许SELECT并且用关键词黑名单兜底。更严格的做法是用SQL解析器做AST级别的校验但实现成本更高。对于大多数业务场景关键词黑名单已经能挡住大部分误操作。6.3 工具返回值的二次污染问题工具返回的内容会进入模型上下文所以它也可能携带敏感信息。比如你调了一个内部API返回的JSON里带了internal_token字段模型下一轮生成时可能就把它吐出来了。我的处理方式是在工具返回值和模型之间再加一层过滤把返回值里已知的敏感字段比如token、secret、password直接剔除只保留业务需要的字段。这层过滤用Pydantic的model_validate配合字段白名单就能实现不需要上Presidio。6.4 Agent护栏的审计要求Agent场景下护栏不仅要拦截还要记录完整的调用链。我一般会记录用户输入、模型生成的工具调用参数、工具实际返回值、护栏的裁决结果。这些数据在出问题的时候是唯一的追溯依据。存储上不要只存最终结果要存每一步的中间状态。我见过有团队只存了最终回答结果出了事故根本查不到是哪一步出的问题。7. 关于护栏效果评估的一点经验护栏上线之后怎么衡量它有没有用我一般看三个指标拦截率触发拦截的请求占总请求的比例。太低说明规则太松太高说明误杀严重。误杀率被拦截但实际无害的请求占比。这个需要人工抽样评估我一般每周抽100条。漏杀率没被拦截但实际有害的请求占比。这个更难统计通常靠用户反馈和安全团队的渗透测试来发现。这三个指标里漏杀率是最危险的因为它意味着护栏形同虚设。我建议在项目初期就建立一套红队测试用例定期跑一遍确保新增的护栏规则没有引入回归问题。另外护栏规则不是写完就完了。业务在变、模型在变、攻击手法也在变。我现在的习惯是每个月review一次验证器的触发日志把长期不触发的规则清理掉把新出现的风险模式补进去。护栏是一个持续迭代的东西不是一次性的交付物。最后分享一个我在实际项目里验证过的小技巧把护栏的配置做成可热更新的。不要硬编码在代码里而是放在配置中心或者数据库里。这样发现新风险的时候改配置就能生效不用重新发版。这个在应急响应的时候特别有用。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案