大模型输出护栏:用 Guardrails 做结构化校验与自愈

大模型输出护栏(Guardrails)是 LLM 应用上线前绕不开的一环。哪怕你用 JSON Mode 拿到了看似规整的结果,字段缺失、类型错乱、枚举越界、PII 泄露依然会悄悄溜进生产。本文用 Guardrails 与 Pydantic 搭一层可重试、可自愈的输出校验护栏,让模型返回真正可信。

一、为什么 JSON Mode 不等于安全

很多团队把”开了 JSON Mode / Structured Output”等同于”模型输出可控”,这是一个危险的误判。结构化约束只保证语法层面的可解析,并不保证语义正确。我们至少会踩到四类坑:字段缺失(模型偷懒省略了可选字段)、类型错乱(本该是数字却返回了字符串)、枚举越界(状态值超出了约定集合)、敏感信息泄露(在回答里带出了手机号或身份证)。这些问题在测试集上偶尔出现,到了生产流量里就是接口 500 或合规事故。结构化输出解决了”能不能解析”,而护栏要解决”解析出来能不能用”。

二、护栏的三层能力:校验、约束、自愈

一个合格的输出护栏应该同时具备三种能力。第一是校验:对返回结果做类型、范围、必填校验;第二是约束:在生成阶段就注入业务规则(例如”评分必须是 0–1 之间的浮点数”);第三是自愈:校验失败时不是直接报错,而是把错误回灌给模型,让它修正后重试。这三层合起来,才把 LLM 从”概率生成器”变成”可信服务”。

方案能拦住的拦不住的
JSON Mode语法可解析字段缺失、枚举越界、PII 泄露
Pydantic 校验类型、必填、范围语义合理性、敏感信息
Guardrails 护栏类型+语义+PII+自愈需要自定义 validator

三、快速上手:用 Pydantic 定义输出契约

最轻量的护栏就是一份数据契约。下面用 Pydantic 描述一次”工单分类”模型的输出:必须带置信度、分类必须落在约定枚举内、摘要不能为空。任何不符合契约的结果都会在解析阶段被抛出。

在正式接入前,记得把 Pydantic 升级到 2.x 版本,老版本 v1 的写法略有差异。契约定义越贴近业务语义(而不是简单留一个”任意字符串”),护栏的拦截价值就越高——例如把”状态码”显式写成枚举,比自由字符串能多拦下大量脏数据,也让下游消费方更省心。

from pydantic import BaseModel, Field, ValidationError
from typing import Literal

class TicketResult(BaseModel):
    category: Literal["账单", "故障", "咨询", "投诉"] = Field(..., description="工单分类")
    confidence: float = Field(..., ge=0.0, le=1.0, description="置信度 0-1")
    summary: str = Field(..., min_length=5, description="不少于5字摘要")

try:
    obj = TicketResult.model_validate(model_json)
except ValidationError as e:
    print("护栏拦截:", e)

四、用 Guardrails 加语义校验与 PII 过滤

Pydantic 管”形状”,Guardrails 还能管”内容”。它可以定义正则、长度、禁止词,以及最常见的 PIR(敏感信息)过滤。下面是一段用 guardrails 框架定义的校验:要求输出为合法 JSON,且对 email 字段做脱敏,禁止出现手机号。

from guardrails import Guard
from guardrails.validators import ValidChoices, RegexMatch

guard = Guard.from_string(
    '''<rail version="0.1">
      <output>
        <string name="category" format="valid-choices:账单,故障,咨询,投诉"/>
        <float name="confidence" format="range:0,1"/>
        <string name="email" format="pii:redact"/>
      </output>
    </rail>'''
)

validated = guard(
    model_api=call_llm,
    prompt_params={"text": user_text},
)

五、自愈循环:校验失败自动重试与修复

护栏的价值不只在于”拦”,更在于”修”。当校验失败时,把错误信息和原始输出拼回 prompt,让模型在下一轮修正。下面这段自愈循环最多重试三次,并把每次失败原因累加进上下文,避免模型重复犯错。

自愈虽好,也有副作用:当模型能力本身不足时,重试只会反复撞墙并叠加延迟。因此要把重试上限设小(3 次以内),并在连续失败时快速降级到”人工审核”或”安全默认值”,而不是让用户一直等待。护栏的目标是”可控地失败”,而不是”无限地死循环”。

def safe_generate(text, max_retry=3):
    history = ""
    for i in range(max_retry):
        raw = call_llm(text + history)
        try:
            return TicketResult.model_validate(raw)
        except ValidationError as e:
            history += f"\n上次结果不合法:{e},请严格按契约修正后重发。"
    raise RuntimeError("护栏重试耗尽,仍有非法输出")

result = safe_generate(user_text)

六、在 LangChain / LiteLLM 中接入护栏

护栏不应该散落在业务代码里,而应该作为一个可复用的中间件。无论你用 LangChain Agent 编排工作流,还是用 LiteLLM 多模型网关 统一接入,都可以在拿到模型返回后统一过一遍护栏。与 Function Calling 配合时,护栏还能先约束工具参数再执行动作,避免脏参数触发错误调用。下面是在 LiteLLM 调用外面包一层校验的示例。

import litellm
from guardrails import Guard

def guarded_chat(messages):
    raw = litellm.completion(model="gpt-4o", messages=messages)
    text = raw.choices[0].message.content
    return guard.parse(text)   # 解析+校验+必要时重试

# 在 Agent 工作流里,护栏可与 Function Calling 配合:
# 先约束工具参数,再执行动作,避免脏参数触发错误调用。

七、生产落地 Checklist

把护栏接进生产前,建议对照下面这张清单逐项确认。它覆盖了从契约定义到可观测性的关键环节,缺一项都可能在上线后付出代价。

检查项说明
输出契约每个 LLM 接口都定义了 Pydantic / JSON Schema
失败策略明确校验失败时重试、降级还是返回友好错误
PII 处理对手机号、身份证、邮箱做脱敏或拒绝
成本上限自愈重试次数设上限,避免无限循环烧钱
可观测记录护栏拦截率,定位模型能力短板

八、性能与成本权衡

护栏不是免费的:每次自愈重试都意味着一次额外的模型调用,直接推高 token 成本。经验法则是——把”便宜的校验”放在前面(本地 Pydantic 秒级完成),把”贵的重试”放在后面,并对重试次数设硬上限。当某个接口的护栏拦截率长期偏高,说明模型本身能力不足,应该回头优化 prompt 或换更强模型,而不是靠无限重试掩盖问题。结合推理成本优化的思路,可以显著提升护栏的性价比。

九、小结

大模型输出护栏(Guardrails)是 LLM 应用从 Demo 走向生产的必选项。用 Pydantic 定义契约、用 Guardrails 补语义与 PII 校验、用自愈循环兜底,再配合LangChain Agent与LiteLLM 多模型网关做统一接入,你就能把”概率生成”收敛成”可信服务”。记住:JSON Mode 只是起点,护栏才是底线。

上一篇 CSS 嵌套实战:用原生嵌套语法重构你的样式表
下一篇 Java 单例模式实战:DCL、静态内部类与枚举的安全写法
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]