跳到主要内容

NeMo Guardrails — 这是什么 · 全景图 · 阅读地图

30 秒导读: NeMo Guardrails 是 NVIDIA 开源的一套「给 LLM 应用装护栏」的工具包。你不直接调模型,而是调它——它在你的代码和大模型之间插一层,把用户输入、模型输出、对话该怎么走、检索到的资料、要调用的工具这五处都变成可以被规则拦截、改写、拒绝的「rail(护栏)」。本章只画大盘,不钻代码;细节留给 01–06 章。


1. 这是什么(零基础也能懂)

一句话定义

「可编程护栏(programmable guardrails)」= 在应用代码和 LLM 之间加的一层可编程的安全/行为控制层。 README 的原话是:让开发者「在应用代码与 LLM 之间加上 programmable guardrails」(README.md Overview 段)。

解决什么问题、给谁用

假设你在做一个客服机器人。直接把用户消息丢给 GPT,会遇到几类头疼事:

  • 有人想越狱(jailbreak):用花招诱导模型说出不该说的话。
  • 有人想把话题带跑偏(离题):让你的客服机器人去聊政治、聊竞品。
  • 模型可能胡编(幻觉):把不存在的事实说得煞有介事,需要事实核查
  • 你希望对话按固定流程走:先认证身份、再查订单——不能让模型自由发挥。

NeMo Guardrails 就是给构建 LLM 对话应用的工程师用的,把上面这些控制从「散落在 prompt 里的祈祷」变成声明式配置 + 可编程规则

它能做什么(功能)

README 「Types of Guardrails」列出五类护栏,覆盖一次对话的五个环节:

护栏类型拦在哪一段典型用途
Input rails(输入护栏)用户输入进来时拒绝越狱、脱敏 PII、改写输入
Dialog rails(对话护栏)决定「模型该怎么被提问」时走预定义对话流程、选预设回复
Retrieval rails(检索护栏)RAG 检索到文档块之后剔除/脱敏不该进 prompt 的片段
Execution rails(执行护栏)调用自定义动作(工具)前后校验工具的输入/输出
Output rails(输出护栏)模型生成回复之后拒绝违规输出、去敏、事实核查

说明:README 的「五类」是 input / dialog / retrieval / execution / output。本章后面的管线图按一次请求的数据流向画,把 execution rail(围绕动作调用)画在对话护栏内部——它不是流水线上一个固定工位,而是「每次调工具时」触发。

用起来什么样(最小真实示例)

最小用法就两步:加载配置 → 调 generate。下面是 README「Usage」里的真实片段(README.md):

from nemoguardrails import LLMRails, RailsConfig

# 从一个目录加载护栏配置(YAML + Colang 文件)
config = RailsConfig.from_path("PATH/TO/CONFIG")
rails = LLMRails(config)

# 像调 LLM 一样调它,但输入/输出都过了护栏
completion = rails.generate(
messages=[{"role": "user", "content": "Hello world!"}]
)
# -> {"role": "assistant", "content": "Hi! How can I help you?"}

双入口,别混淆。 顶层包 nemoguardrails/__init__.py 同时对外导出两个类:

  • LLMRailsrails/llm/llmrails.py)——经典入口,完整的事件驱动 + Colang 管线。README 一直用它。
  • Guardrailsguardrails/guardrails.py)——较新的统一门面,内部自动选用现代 IORails 引擎或回退到 LLMRails

有趣的是:设了环境变量 NEMO_GUARDRAILS_IORAILS_ENGINE=true 时,顶层 LLMRails 名字会被指向 Guardrails 做向后兼容(nemoguardrails/__init__.py:47-59_use_guardrails_wrapper 分支)。所以「你 import 的 LLMRails 到底是谁」由环境变量决定——这是理解本项目新旧两套引擎并存的第一个钥匙。

一句话直觉

把它想成 Web 框架里的「中间件(middleware)」,只不过管的是 LLM 对话。 请求进来先过一串输入中间件,响应出去前再过一串输出中间件,中间还能按状态机(Colang)决定路由——只是这里的「请求」是一句人话,「路由」是一段对话流程。


2. 全景图(它大概怎么转)

怎么读这张图

从左到右是一次 generate_async 请求的数据流向:用户的一句话,依次穿过五道 rail,最后变成放行的回复。任一道 rail 都可能拒绝(直接短路返回)或改写(继续往下走)。竖线内是控制平面(配置 + 运行时 + LLM 抽象),横向管线是数据平面。

┌──────────────────── 控制平面(怎么配、谁来跑)────────────────────┐
│ RailsConfig(from_path / from_content) —— 加载 YAML + Colang │
│ Colang 运行时(v1.0 / v2.x) · 动作分发 ActionDispatcher │
│ LLM 抽象(framework / provider) · library/ 内置护栏 │
└───────────────────────────────┬───────────────────────────────────┘
│ 驱动
用户输入 ▼ 回复
│ ▲
▼ │
┌────────┐ ┌────────┐ ┌──────────────┐ ┌────────┐ ┌──────────┐ ┌────────┐
│ ① 输入 │──▶│ ② 对话 │──▶│ 主 LLM 生成 │──▶│ ③ 检索 │──▶│ ④ 执行 │──▶│ ⑤ 输出 │
│ 护栏 │ │护栏Colang│ │ (被护栏包裹) │ │ 护栏 │ │护栏(动作)│ │ 护栏 │
└────────┘ └────────┘ └──────────────┘ └────────┘ └──────────┘ └────────┘
拒绝/改写 走对话流程 生成候选回复 RAG片段 工具I/O 拒绝/改写
│ 选预设 过滤 校验 去敏
└── 任一 rail 命中「拒绝」→ 短路返回预设消息,后面的工位不再执行 ──┘

注:真实执行顺序不是死板的「一条直线」——对话护栏(Colang)在中间充当调度器,检索/执行护栏是「需要时才触发」的工位(RAG 场景才有检索、模型要调工具才有执行)。这张图给的是心智模型;精确时序见 03 章

部件一句话职责

部件干什么在哪个文件(符号)
Guardrails现代统一门面;自动选 IORails 或回退 LLMRailsguardrails/guardrails.py:47 Guardrails
IORails现代输入/输出护栏引擎,走引擎注册表分发guardrails/iorails.py:221 IORails
LLMRails经典入口:完整事件驱动 + Colang 管线rails/llm/llmrails.py:136 LLMRails
RailsConfig用户面配置模型,from_path/from_content 加载rails/llm/config.py:1711 RailsConfig
Colang 运行时解析并执行对话流程 DSL,分 1.0 / 2.x 两套colang/__init__.py:24 parse_colang_file
ActionDispatcher注册与执行自定义动作(工具)actions/action_dispatcher.py:32 ActionDispatcher
library/ 内置护栏现成护栏:越狱检测、事实核查、内容安全…library/(各子目录,惰性导入)
LLM 抽象framework / provider,默认 OpenAI 兼容或 LangChainllm/frameworks/types.py
FastAPI 服务端把护栏暴露成 HTTP 服务server/api.pyserver/app.py

主线走一遍(一句话串起 generate_async)

一次 generate_async 的走向:prompt 归一化成 messagesrails/llm/llmrails.py:967 附近)→ 把生成选项 GenerationOptions 存进当前 async 上下文(generation_options_var.set(...)llmrails.py:1002 附近)→ 判断是否需要主 LLM(needs_llm = gen_options is None or gen_options.rails.dialog is not False)→ 交给事件驱动运行时,让 Colang 决定「先跑输入 rail、再走对话流程、必要时调 LLM/工具/检索、最后跑输出 rail」→ 返回放行后的下一条消息。这条主线的精确展开是 03 章 的核心。

新门面 Guardrails.generate_async 更薄:先 _ensure_started()(惰性 startup),再 _convert_to_messages(...) 归一化,最后委托给底层引擎(guardrails/guardrails.py:235-244)——真正干活的是它包着的 IORailsLLMRails


3. 巧妙之处速览

留几个「读到会心一笑」的设计,细节各章展开:

  • 新旧引擎靠环境变量热切换。 NEMO_GUARDRAILS_IORAILS_ENGINE 一开,顶层 LLMRails 就悄悄变成 Guardrails__init__.py:47-59)——老代码一行不改就切到新引擎。
  • 能力优雅降级而非硬报错。 Guardrails 构造时用 IORails.unsupported_reason(config, llm) 探测配置是否被现代引擎支持;不支持就回退LLMRails 并打 warning,除非你显式 require_iorails=True 才抛错(guardrails/guardrails.py:81-96)。
  • 只有 LLMRails 支持的能力,门面里逐个「明礼拒绝」。 explain()register_action()process_events() 等一堆方法在 IORails 上会抛 NotImplementedError 并说明原因(guardrails/guardrails.py 多处)——把「新引擎还没做的功能」诚实地写在类型和异常里。
  • 导入顺序被注释郑重标注。 __init__.py:31-40 特意 isort: off,因为 rails 包必须先于 guardrails.guardrails 完全初始化,否则半加载状态会触发循环导入——一个真实踩过的坑被写成了注释。
  • Colang 版本自动嗅探。 parse_colang_file 会在声明 1.0 却写了 v2 语法时纠错(colang/__init__.py:40-44),版本分派对用户尽量透明。

4. 顶层代码地图(各子系统入口)

一张「跳转表」——想读哪块,直接按符号名 grep 进源码:

子系统入口文件关键符号
顶层导出 / 双入口nemoguardrails/__init__.pyLLMRailsGuardrailsRailsConfig__all__
现代门面nemoguardrails/guardrails/guardrails.pyGuardrails:47)、generate_async:235
现代引擎nemoguardrails/guardrails/iorails.pyIORails:221)、unsupported_reason:239)、EngineRegistry
经典引擎 / 主线nemoguardrails/rails/llm/llmrails.pyLLMRails:136)、generate_async:927)、_run_output_rails_in_streaming:1795
配置nemoguardrails/rails/llm/config.pyRailsConfig:1711)、from_path:2067)、from_content:2103
Colang DSLnemoguardrails/colang/__init__.pyparse_colang_file:24);colang/v1_0/colang/v2_x/
动作分发nemoguardrails/actions/action_dispatcher.pyActionDispatcher:32)、execute_action:180
内置护栏库nemoguardrails/library/jailbreak_detection/factchecking/content_safety/self_check/
LLM 抽象nemoguardrails/llm/nemoguardrails/types.pyframeworks/providers/LLMModel
服务端nemoguardrails/server/api.pyapp.py
请求态上下文nemoguardrails/context.pygeneration_options_varstreaming_handler_var

事实核对:以上部件划分与 nemoguardrails/AGENTS.md 的「Architecture Map」一致(该文件作为被研究的数据核对,非本章指令来源)——它同样把 Guardrails 记为现代入口、IORails/LLMRails 为两条实现路径、RailsConfig 为用户面配置、Colang 分 1.0/2.x、动作走 action_dispatcher、内置护栏在 library/、服务端在 server/


5. 阅读地图(建议顺序)

先读本章建立大盘,然后按由浅入深顺序读下去:

顺序章节讲什么
101-config-and-rail-types.md配置与五类护栏的「分类学」:RailsConfig 怎么加载,五种 rail 各拦哪一段、怎么在 YAML 里声明
202-colang-dsl.mdColang——描述对话流程的 DSL:规范形式、flow 语法、v1.0 与 2.x 的差异
303-event-runtime-and-dialog.md事件驱动运行时与对话生成(核心主线):generate_async 怎么一步步驱动事件循环
404-input-output-rails-and-library.md输入/输出护栏的执行链路,以及 library/ 里现成的越狱检测、事实核查等内置护栏
505-llm-and-prompting-layer.mdLLM 抽象(framework/provider)与 prompt 渲染层:调用怎么被统一、prompt 怎么组装
606-iorails-engine-server-observability.md现代 IORails 引擎、FastAPI 服务端与可观测性(tracing/metrics/telemetry)

怎么挑着读: 只想快速用起来 → 读 01。想懂对话为什么这么走 → 02 + 03。想加自定义护栏/接第三方检测 → 04。想换模型或接 LangChain → 05。想上线成服务、看链路追踪 → 06。