大模型输出护栏(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 只是起点,护栏才是底线。




