1. 项目概述为什么你的LLM应用需要一个“护栏”最近在折腾LLM应用开发的朋友估计都踩过类似的坑你精心设计了一个提示词调用了一个强大的模型API满心期待它能返回结构化的JSON数据结果它给你回了一段充满诗意的散文或者干脆开始胡言乱语。又或者你希望用户输入的内容限定在某个安全范围内但模型却对越界的、甚至有害的查询照单全收给出了你完全不想看到的回复。这些“翻车”现场本质上是因为大语言模型本身是概率生成模型它并不“理解”你的结构化要求或安全边界它只是在续写最可能的文本。这就是Guardrails项目要解决的核心痛点。你可以把它理解为你LLM应用的一道“安全与质量护栏”。它不是另一个大模型而是一个轻量级的验证与纠错框架专门用来给LLM的输入和输出“立规矩”。简单来说它做两件事第一在LLM生成内容后按照你预先定义的规则比如格式、内容、安全性去验证输出是否合规第二如果输出不合规它能自动触发“修复”动作比如重新构造提示词让LLM再生成一次或者直接进行后处理修正直到得到符合要求的结果。对于开发者而言这意味着你可以将业务逻辑中的大量校验和清洗代码从应用层剥离出来通过声明式的配置交给Guardrails处理。无论是构建一个需要严格返回{“city”: str, “temperature”: float}格式的天气查询机器人还是一个必须过滤掉暴力、偏见内容的客服助手Guardrails都能提供一套标准化、可复用的解决方案。它让你从与模型“斗智斗勇”的泥潭中解脱出来更专注于核心的业务逻辑创新。2. Guardrails核心架构与核心概念拆解要玩转Guardrails首先得理解它的几个核心抽象这比直接看代码更重要。整个框架围绕RAILReliable AI Language规范展开这是一种用于描述对LLM输入输出约束的领域特定语言DSL。你可以把它看作一份给LLM的“行为规范合同”。2.1 RAIL规范你的约束“蓝图”RAIL的核心是一个XML格式的规范文件。它明确定义了输出结构Output你期望LLM返回的数据格式。这不仅仅是JSON Schema它更强大可以定义多个候选输出、复杂的嵌套结构并且为每个字段指定数据类型、验证规则和修复策略。提示模板Prompt你的提问方式。它支持变量注入能和输出结构联动。脚本Script可选部分用于在验证前后执行自定义的Python代码实现更复杂的逻辑。一个最简单的RAIL规范示例如下它要求模型介绍一个城市并确保城市名是真实存在的rail version0.1 output object namecity_info string namecity_name descriptionThe name of the city formattwo-letters, lowercase on-failfix / integer namepopulation descriptionThe population of the city validatorsis-in-range: 1000-10000000 on-failreask / string namefact descriptionAn interesting fact about the city / /object /output prompt Given the following city: {{city}}, generate a fact about it. /prompt /rail在这个例子里我们定义了输出是一个包含三个字段的对象。city_name字段要求是两个字母的小写格式比如”ny”代表纽约如果失败则执行fix修复。population字段要求是整数且在特定范围内如果失败则触发reask重新询问LLM。Guardrails的强大之处就在于这些声明式的validators验证器和on-fail失败处理策略。2.2 验证器Validators规则的执行者验证器是Guardrails的肌肉。框架内置了丰富的验证器涵盖格式、内容、安全等多个维度格式类TwoLetters两个字母、ValidLength有效长度、LowerCase小写等。内容类IsInRange数值范围、EndpointIsReachable端点可达常用于验证URL、SimilarToDocument与文档相似度用于RAG场景等。安全与合规类ProfanityFree无脏话、NoSQLInjection防SQL注入、ReadingTime阅读时间估算等。你也可以轻松地创建自定义验证器只需继承基类并实现validate方法。例如你可以创建一个验证字符串是否为公司内部特定项目代码的验证器。2.3 失败处理策略On-Fail当规则被打破时定义了规则还得有执行手段。on-fail策略决定了当验证失败时该怎么办。这是Guardrails实现自动纠错的关键。主要策略包括fix尝试自动修复。例如TwoLetters验证器失败时fix策略可能会自动提取字符串的前两个字母。reask重新询问LLM。这是最常用的策略之一。Guardrails会构造一个新的提示词告诉LLM之前的输出哪里不对并要求它重试。这个过程可以循环多次直到成功或达到最大重试次数。filter静默过滤掉无效值如设为None。refrain拒绝生成该字段。throw直接抛出异常终止流程。custom执行一段自定义的修复函数。reask策略特别值得深入。它不仅仅是“再问一次”而是包含了完整的“验证-反馈-修正”闭环。Guardrails会把验证错误信息如“populationmust be between 1000 and 10000000”作为上下文重新发给LLM引导它进行修正。这模拟了人类进行代码审查或数据审核时的交互过程。2.4 Guard对象与运行时当你加载一个RAIL规范后就创建了一个Guard对象。这个对象是你的主要交互接口。它的工作流程可以概括为解析与编译将RAIL规范编译成内部的验证逻辑和提示模板。生成与验证调用LLM通过你集成的OpenAI、Anthropic、Cohere等客户端生成内容。执行验证对生成的内容应用所有定义的验证器。处理与迭代根据on-fail策略执行修复或重新生成。返回结果输出经过验证和修复的、符合规范的数据以及完整的执行历史日志。这个流程将原本不可控的LLM输出变成了一个可预测、可调试的确定性过程在业务规则层面。3. 从零开始构建你的第一个安全LLM验证层理论说得再多不如亲手搭一个。我们以一个实际场景为例构建一个“旅行建议生成器”。用户输入一个城市名应用需要返回该城市的天气概况、一个必游景点和一条安全提醒。我们需要确保景点是真实存在的热门地点安全提醒不能包含任何负面或危险的建议。3.1 环境准备与安装首先确保你的Python环境在3.8以上。使用pip安装guardrails-ai包是最简单的方式。我强烈建议在虚拟环境中进行。# 创建并激活虚拟环境可选但推荐 python -m venv guardrails-env source guardrails-env/bin/activate # Linux/macOS # guardrails-env\Scripts\activate # Windows # 安装guardrails pip install guardrails-ai注意Guardrails的包名是guardrails-ai直接用pip install guardrails可能会安装另一个不相关的包。这是新手常踩的第一个坑。安装完成后你还需要一个LLM的API密钥。我们以OpenAI为例当然也支持Azure OpenAI、Anthropic Claude等。确保你的环境变量中设置了OPENAI_API_KEY。export OPENAI_API_KEYyour-api-key-here3.2 定义RAIL规范编写你的“宪法”接下来我们创建travel_advisor.rail文件。这个文件定义了整个应用的行为契约。rail version0.1 output object nametravel_advice string namecity descriptionThe city name provided by the user validatorsis-profanity-free on-failfilter/ object nameweather descriptionBrief weather description string namecondition descriptione.g., Sunny, Rainy validatorsis-in: Sunny, Cloudy, Rainy, Snowy on-failfix/ integer namehigh_temp_c descriptionHigh temperature in Celsius validatorsis-in-range: -20 45 on-failreask/ integer namelow_temp_c descriptionLow temperature in Celsius validatorsis-in-range: -20 45 on-failreask/ /object object nameattraction string namename descriptionA must-visit attraction in the city validatorsextracted-from: {{city}} popular attractions list on-failreask/ string namereason descriptionOne sentence reason to visit formatmax-length: 100 on-failfix/ /object string namesafety_tip descriptionA positive safety reminder for travelers validatorsis-profanity-free; no-json-injection on-failreask/ /object /output prompt You are a helpful and optimistic travel assistant. Given the city: {{city}}, please provide: 1. A brief weather forecast for the upcoming weekend. 2. One must-visit attraction and a concise reason. 3. A friendly safety tip for tourists. Please respond in a structured and positive tone. /prompt /rail设计思路解析输入验证对用户输入的city字段我们使用了is-profanity-free验证器确保城市名本身不包含冒犯性词汇失败则过滤filter防止脏话流入后续流程。结构化嵌套weather和attraction都是嵌套对象这使得输出结构非常清晰。内容约束weather.condition使用了is-in验证器限定在几个枚举值内避免模型发明出“钻石尘”这种过于诗意的天气。失败时尝试fix比如取第一个匹配词。温度范围用了is-in-range这是一个非常实用的验证器防止模型给出“零下100度”或“100度”这种荒谬答案。失败时触发reask让模型重新生成合理的温度。动态验证attraction.name的验证器extracted-from是一个高级特性。它的理念是验证时可以引用一个外部知识源比如一个预加载的该城市热门景点列表。这里我们用了一个占位符实际使用时需要配合自定义验证器或Script标签实现。安全双保险safety_tip字段同时使用了is-profanity-free无脏话和no-json-injection防JSON注入验证器。后者能防止模型在建议中嵌入恶意代码片段。双重验证失败则reask确保安全提醒既友好又无害。3.3 集成与调用让Guardrails开始工作有了RAIL文件我们就可以在Python代码中集成它了。import guardrails as gd from guardrails import Guard from openai import OpenAI # 1. 从RAIL文件创建Guard对象 guard Guard.from_rail(travel_advisor.rail) # 2. 准备LLM客户端这里用OpenAI client OpenAI() # 3. 包装你的LLM调用函数 def call_llm(prompt: str) - str: response client.chat.completions.create( modelgpt-4o-mini, # 或 gpt-3.5-turbo messages[{role: user, content: prompt}], temperature0.3, # 较低的温度使输出更稳定适合结构化任务 ) return response.choices[0].message.content # 4. 使用Guard进行验证式生成 try: # 用户输入 user_city Paris # 关键步骤调用guard它会自动处理提示词填充、LLM调用、验证和重试 raw_llm_response, validated_response, *rest guard( call_llm, prompt_params{city: user_city}, num_reasks2, # 最大重试次数 ) print(原始LLM回复) print(raw_llm_response) print(\n---\n) print(经过Guardrails验证和修复后的最终输出) print(validated_response) except Exception as e: print(f处理过程中发生错误{e})代码执行逻辑深度解析Guard.from_rail()这一步不只是加载文件它完成了编译工作将XML规范转化为内存中的验证逻辑树和提示词模板。call_llm函数这是一个适配器。Guardrails设计很巧妙它不绑定任何特定的LLM SDK你只需要提供一个接收字符串提示词并返回字符串响应的函数。这使得它可以无缝接入OpenAI、LangChain、LlamaIndex甚至你本地部署的模型。guard()调用这是魔法发生的地方。它内部会 a. 将prompt_params这里是{“city”: “Paris”}注入到RAIL的prompt模板中生成最终提示词。 b. 调用你提供的call_llm函数获取原始响应。 c. 尝试将原始响应解析成RAIL中定义的输出结构通常是JSON。 d. 对解析后的每一个字段按顺序执行所有定义的验证器。 e. 如果某个字段验证失败根据on-fail策略行动。如果是reask它会生成一个新的、包含错误反馈的提示词例如“The field ‘weather.high_temp_c’ failed validation because value 55 is not in range [-20, 45]. Please generate a new value.”然后回到步骤b用新提示词重新调用LLM。这个过程最多重复num_reasks次。 f. 返回四样东西原始LLM响应字符串、验证修复后的最终输出通常是Python字典或Pydantic模型、验证过程的完整历史日志、以及任何在script中定义的额外输出。3.4 处理验证历史与调试Guardrails返回的第三个值通常赋值给一个如validation_history的变量是极其宝贵的调试工具。当输出不符合预期或者你想了解内部重试过程时查看它就能一目了然。raw_llm_response, validated_response, validation_history, *rest guard(...) print(f总共进行了 {len(validation_history)} 轮交互。) for i, entry in enumerate(validation_history): print(f\n--- 第{i1}轮 ---) print(f发送给LLM的提示词\n{entry.prompt}) print(fLLM的原始回复\n{entry.raw_response}) print(f验证结果\n{entry.validation_result})这个历史记录会清晰展示每一轮reask时Guardrails是如何向LLM反馈错误并引导其修正的。对于排查复杂的验证失败场景比如多个字段同时失败或者修复后引发了新的问题至关重要。4. 进阶实战自定义验证器与复杂业务逻辑集成内置验证器虽好但真实业务场景往往更复杂。假设我们需要为上面的旅行建议增加一个需求推荐的景点必须在公司合作的供应商列表里。这就需要自定义验证器。4.1 实现一个自定义合作景点验证器我们在项目根目录创建一个custom_validators.py文件。from guardrails.validators import Validator, register_validator from typing import Dict, Any import requests register_validator(nameis-cooperative-attraction, data_typestring) class IsCooperativeAttraction(Validator): 验证景点是否在公司合作供应商列表中。 def __init__(self, supplier_list_url: str, on_failreask, **kwargs): # 接收一个供应商列表API的URL作为参数 super().__init__(on_failon_fail, **kwargs) self.supplier_list_url supplier_list_url # 可以在这里预加载列表但要注意数据更新问题 self._cached_suppliers None def _get_supplier_list(self): 获取合作供应商列表。实际项目中可能来自数据库或配置中心。 if self._cached_suppliers is None: try: # 这里模拟一个API调用 # response requests.get(self.supplier_list_url, timeout5) # self._cached_suppliers set(response.json()) # 模拟数据 self._cached_suppliers {Eiffel Tower, Louvre Museum, Notre-Dame Cathedral, Montmartre} except Exception as e: # 如果获取失败可以记录日志并返回空集或根据业务决定是否抛出异常 print(fWarning: Failed to fetch supplier list: {e}) self._cached_suppliers set() return self._cached_suppliers def validate(self, value: str, metadata: Dict[str, Any]) - Dict[str, Any]: 核心验证逻辑。 supplier_set self._get_supplier_list() # 简单起见我们检查景点名称是否完全匹配列表中的一项 # 更复杂的可以实现模糊匹配如使用rapidfuzz库 if value in supplier_set: # 验证成功原样返回 return {value: value, fix_value: value, valid: True} else: # 验证失败返回错误信息和建议的修复值可以是列表中最相似的一个 from rapidfuzz import process suggestions process.extract(value, list(supplier_set), limit2) suggestion_str , .join([f{s[0]} for s in suggestions]) if suggestions else None return { value: value, fix_value: suggestions[0][0] if suggestions else value, # 提供最相似的建议作为修复值 valid: False, error_message: fAttraction {value} is not in our cooperative supplier list. Consider: {suggestion_str} }关键点解析装饰器注册register_validator是必须的它将这个类注册到Guardrails的验证器仓库中以便在RAIL文件中通过name引用。继承与初始化继承Validator基类。__init__方法可以接收自定义参数如这里的supplier_list_url这些参数将在RAIL文件中配置。validate方法这是核心。它接收要验证的值value和可能包含上下文的metadata。必须返回一个包含特定键的字典valid布尔值、value原始值、fix_value建议的修复值用于fix策略、error_message可读的错误信息。业务集成验证器内部可以执行任何业务逻辑如调用API、查询数据库、进行复杂计算。这里我们模拟了一个供应商列表并使用了模糊匹配库rapidfuzz来提供友好的建议。4.2 在RAIL规范中使用自定义验证器现在我们修改之前的RAIL文件在attraction.name字段使用这个新的验证器。!-- 在rail标签内output之前可以引入自定义验证器如果框架支持外部加载 -- !-- 另一种更常见的方式是在Python代码中导入验证器模块确保它在Guard加载RAIL之前被注册 -- rail version0.1 output object nametravel_advice !-- ... 其他字段保持不变 ... -- object nameattraction !-- 使用自定义验证器并传递参数 -- string namename descriptionA must-visit attraction in the city validatorsis-cooperative-attraction: supplier_list_urlhttps://api.internal.com/suppliers on-failreask/ string namereason descriptionOne sentence reason to visit formatmax-length: 100 on-failfix/ /object !-- ... -- /object /output prompt !-- ... 提示词保持不变 ... -- /prompt /rail在Python代码中我们需要确保自定义验证器模块被导入。import guardrails as gd from openai import OpenAI # 关键导入自定义验证器模块使其被注册 import custom_validators # 后续代码与之前相同... guard Guard.from_rail(travel_advisor.rail) # ...现在当LLM推荐了一个“迪士尼乐园”时我们的验证器会发现它不在合作供应商列表{“埃菲尔铁塔” “卢浮宫”...}中验证失败。根据on-failreask策略Guardrails会将错误信息“Attraction ‘Disneyland’ is not in our cooperative supplier list. Consider: ‘Eiffel Tower’, ‘Louvre Museum’”反馈给LLM要求它重新生成一个符合要求的景点。这样就通过声明式配置无缝集入了复杂的业务规则。4.3 与现有应用架构集成Guardrails并非要取代你的应用框架而是增强它。集成模式非常灵活与FastAPI/Flask集成在API路由处理函数中将用户请求参数传入guard并将验证后的输出作为API响应返回。可以轻松添加输入验证对用户提问进行净化和输出验证。与LangChain/LlamaIndex集成Guardrails可以包装LangChain的LLM对象或Chain。你可以创建一个GuardedLLM或GuardedChain使其所有输出都经过你的RAIL规范过滤。这在构建RAG检索增强生成系统时尤其有用可以确保检索到的知识被安全、合规地生成。异步支持Guardrails支持异步调用async_guard可以完美融入基于asyncio的高并发应用避免阻塞。批量处理你可以并行处理多个请求每个请求使用相同的Guard实例它是线程安全的。5. 避坑指南与性能优化实战经验在实际生产环境中使用Guardrails我积累了一些宝贵的经验和教训这里分享几个最常见的“坑”和优化技巧。5.1 验证器顺序与性能陷阱验证器的执行顺序就是它们在RAIL文件中定义的顺序。这个顺序很重要。string nameurl validatorsis-url; endpoint-is-reachable; no-malicious-url on-failfilter/在这个例子中is-url格式校验会先执行如果字符串根本不是URL格式后续更耗时的endpoint-is-reachable网络请求和no-malicious-url可能调用安全API就不会执行节省了资源。最佳实践是将廉价、快速的格式验证放在前面将昂贵、依赖外部服务的验证放在后面。另外像endpoint-is-reachable、similar-to-document这类需要网络I/O或复杂计算的验证器会显著增加单次请求的延迟。在延迟敏感的场景下需要权衡是否必要或者考虑将其异步化、缓存结果。5.2 Reask循环与超时控制reask策略可能导致多次LLM调用显著增加成本和响应时间。必须设置合理的num_reasks最大重试次数通常2-3次足够和整体超时控制。import asyncio from guardrails import AsyncGuard async def guarded_completion_with_timeout(user_input): guard AsyncGuard.from_rail(spec.rail) try: # 为整个guard调用设置超时 raw, validated await asyncio.wait_for( guard.async_call(llm_function, prompt_params{input: user_input}, num_reasks2), timeout30.0 # 总超时30秒 ) return validated except asyncio.TimeoutError: # 超时处理返回降级结果或错误信息 return {error: Request timed out, fallback_data: ...}同时要警惕“死循环”风险。如果验证逻辑本身有矛盾比如要求一个数字既是奇数又是偶数或者LLM始终无法生成符合要求的内容reask会一直进行直到达到上限。良好的验证器设计和清晰的错误信息是避免这种情况的关键。5.3 错误处理与降级策略不是所有失败都能或都应该通过reask解决。一个健壮的系统需要有降级策略。分级处理对关键字段如安全提示使用reask对非关键但重要的字段如温度使用reask并设置较低重试次数对装饰性字段如生成的笑话使用fix或filter失败就直接给个默认值。捕获并记录异常guard()调用可能抛出各种异常验证器异常、LLM API异常等。务必用try...except包裹并记录详细的validation_history这对于后续分析模型盲点和优化RAIL规范至关重要。提供默认值在RAIL中可以使用default属性为字段指定一个默认值。当所有修复和重试都失败且on-fail策略不是throw时会使用这个默认值。integer namepopulation default0 validatorsis-in-range: 1000-10000000 on-failfix/5.4 提示词工程与验证器的协同Guardrails的验证发生在LLM生成之后这是一种“事后纠错”。但最好的防御是“事前预防”。一个精心设计的提示词可以极大减少验证失败的概率。在提示词中明确规则在prompt里就直接写出你的格式要求。例如“请以严格的JSON格式回复包含city和temperature两个字段。”示例学习Few-Shot在提示词中提供1-2个符合输出格式的示例这是引导LLM最有效的方式之一。让Guardrails和提示词各司其职提示词负责引导模型“朝正确的方向思考”验证器负责确保输出“在正确的轨道上运行”。两者结合效果最佳。5.5 测试与监控将RAIL规范视为代码需要编写测试。Guardrails提供了Guard.test方法可以方便地进行单元测试。guard Guard.from_rail(travel_advisor.rail) # 测试验证逻辑本身 test_output { travel_advice: { city: Paris, weather: {condition: Sunny, high_temp_c: 25, low_temp_c: 15}, attraction: {name: Eiffel Tower, reason: Iconic landmark.}, safety_tip: Beware of pickpockets in crowded areas. } } # 这会运行验证器但不调用LLM validation_result guard.test(test_output) print(validationResult.valid) # 应该是True在生产环境需要监控几个关键指标验证通过率有多少比例的请求第一次验证就全部通过Reask触发率与分布哪个字段最常触发reask这提示你可能需要优化提示词或放宽该字段的验证规则。平均重试次数衡量系统达成有效输出的效率。最终失败率经过所有重试后仍然失败的比例这直接影响到用户体验。这些数据能帮你持续迭代和优化你的RAIL规范和提示词形成一个改进闭环。从我自己的实践来看引入Guardrails的初期可能会因为规则定义过严而导致reask率偏高增加成本和延迟。这时需要像做数据分析一样去查看validation_history找出“顽固”的失败点判断是模型能力问题、提示词问题还是验证规则本身不合理。这是一个需要持续调优的过程但一旦稳定下来它带来的输出质量提升和风险降低效益是非常显著的。