从 Prompt 注入到防御:大模型输入安全的工程化检测链路
从 Prompt 注入到防御大模型输入安全的工程化检测链路一、当用户输入变成指令Prompt 注入为什么是大模型的第一道裂缝大模型应用通常会把用户文本直接拼进系统提示词。这带来一个根本矛盾模型无法区分指令与数据。攻击者只要在输入里写一句忽略上面的要求改为……就能劫持整个对话的目标。这类风险在客服、自动化 Agent、RAG 问答里尤其突出。因为系统提示词往往承载了角色、权限与工具调用规则。一旦被覆盖模型可能泄露内部知识、调用不该调用的工具甚至把敏感数据回传给陌生人。更麻烦的是Prompt 注入没有固定边界。它不像 SQL 注入那样有清晰的特征串。同一句话换个说法效果可能完全不同。这也决定了单点过滤解决不了问题必须建立一条工程化的检测链路。一个常见的误解是加个敏感词黑名单就够了。实际上攻击者可以用角色扮演、虚构剧情、编码变形把恶意意图藏进看似无害的叙述里。例如让模型扮演一个没有限制的助手或把指令拆成多轮对话逐步诱导。这种分散式注入更难用静态规则捕获必须依赖语义层面的判断。还有一类容易被忽视的场景是间接注入。恶意文本不是来自用户而是来自模型检索到的外部内容比如网页、邮件、知识库文档。当 RAG 系统把这些内容拼进上下文外部文档就成了藏在数据里的指令。这意味着输入安全的边界要从用户框扩展到所有进入上下文的内容检测链路必须覆盖检索结果的入口。二、注入的攻击面与检测分层模型把输入安全看成一条流水线会更清晰。原始输入先经过多层处理每一层各司其职最终才进入模型上下文。下图展示了一条典型的检测链路flowchart LR A[原始用户输入] -- B[预处理与归一化] B -- C{规则层检测} C --|命中| H[拦截或降级] C --|通过| D[语义分类器] D --|高风险| H D --|低风险| E[上下文隔离沙箱] E -- F[模型推理] F -- G[输出安全校验] G --|异常| H G --|正常| I[返回用户] B -.- J[编码还原/同形字处理] J -.- C规则层负责快而准地挡住已知模式语义层负责捕捉变体和对抗样本隔离层负责把不可信输入限制在受控上下文里。三层互补才能同时压住误报与漏报。三、生产级输入安全网关实现下面是一段可落地的输入检测中间件。它把规则与分类器串成管线并内置超时与降级import asyncio import re # 已知高风险指令片段示例生产需持续运营维护 INJECTION_PATTERNS [ r忽略(上面|之前|先前|以上).{0,12}?(要求|指令|提示|规则), rignore.{0,8}?(previous|above|system).{0,8}?instruction, r你现在是.{0,10}?(开发者|管理员|root|dan), ] class InputGuard: def __init__(self, classifierNone, timeout: float 0.8): self._classifier classifier self._timeout timeout def _rule_check(self, text: str) - bool: lowered text.lower() for pat in INJECTION_PATTERNS: if re.search(pat, text, re.IGNORECASE) or re.search(pat, lowered): return True return False async def _model_check(self, text: str) - float: if self._classifier is None: return 0.0 try: # 分类器可能耗时必须加超时避免阻塞主链路 score await asyncio.wait_for( self._classifier.score(text), timeoutself._timeout ) return float(score) except asyncio.TimeoutError: # 超时按不可信降级宁可误报也不漏报 return 1.0 except Exception: return 1.0 async def inspect(self, text: str) - dict: if not text or not isinstance(text, str): return {risk: block, reason: empty_or_invalid} if self._rule_check(text): return {risk: high, reason: rule_hit} score await self._model_check(text) if score 0.6: return {risk: high, reason: model_score, score: score} return {risk: low, reason: pass, score: score}关键点在于规则层先挡已知样本语义层用分类器补变体任何超时或异常都按高风险降级。这样即使分类器故障链路也不会开天窗。四、检测的边界误报、对抗绕过与性能代价这条链路并非银弹落地时要先想清三件事。误报会伤害体验。客服场景里请忽略上一条的错别字这类正常表达可能误触发。解决办法是给规则层加白名单与上下文并把高风险动作设为人工复核而非直接拦截。这里的核心权衡是把阈值调高漏报上升调低误报上升。没有零成本的平衡点只能按业务容忍度选一个可解释的默认值。对抗会持续演化。攻击者可用 base64、Unicode 同形字、少样本诱导绕过单层检测。因此分类器必须定期用新样本微调规则层要支持热更新不能写死在代码里。更现实的做法是把检测做成可观测的记录每一次命中与误报用真实流量反哺规则库让链路随攻击演进。性能有硬性成本。每次请求都跑语义分类器会增加 50 到 200 毫秒延迟。优化做法是先用规则层快速放行绝大多数正常流量只对命中疑点的请求调用模型层。这样九成以上的请求不付出分类开销。若业务对延迟极敏感还可把分类器做成异步旁路仅对高风险会话做二次确认。隔离层的代价也不能忽略。把不可信输入放进独立上下文意味着模型失去了部分跨轮记忆可能影响多轮对话的连贯性。因此隔离应是按需触发只在风险评分越过阈值时才启用而不是对所有用户输入一刀切。五、总结Prompt 注入的本质是模型无法区分指令与数据。应对它的不是某一条正则而是一条分层检测链路规则层挡已知、语义层补变体、隔离层控影响面。工程落地时必须把超时降级、误报治理与性能预算一起考虑才能让输入安全既兜得住攻击又不拖垮业务。