跳到主要内容

配置与五类护栏(rails 分类学)

30 秒导读: 用 NeMo Guardrails 给一个 LLM 应用加安全护栏,第一步不是写代码,而是写一份 config.yml——在里面声明"哪些检查挂在用户输入上、哪些挂在模型输出上"。这一章讲清楚这份声明长什么样、它被解析成哪个 Python 对象(RailsConfig)、五类护栏各自是什么、以及一个 config 目录如何从 YAML + .co 文件被加载起来。只讲声明,不讲执行——真正的运行时在 03 / 04

本章属于 NeMo Guardrails 系列。如果你还不知道这个项目是什么、整体怎么转,先读 index.md。Colang 语法本身(.co 文件里 flow/define 怎么写)在 02,本章只把 Colang 当作"被挂上去的黑盒 flow"。


1. 先建立直觉:config 是一张"挂载表"

假设你有一个客服机器人,你希望:

  • 用户说的话先过一遍"越权/攻击检测",不通过就直接拒绝;
  • 机器人要回的话,发出去之前先过一遍"敏感词/事实核查"。

在 NeMo Guardrails 里,你不写 if-else。你写一份配置,告诉引擎:

rails:
input:
flows:
- self check input # ← 挂到"输入"阶段的一个检查
output:
flows:
- self check output # ← 挂到"输出"阶段的一个检查

这就是核心心智模型:

config = 一张挂载表。 每一行是"把某个名叫 X 的 flow,挂到某个管线阶段(input / output / …)"。config 本身不实现任何检查逻辑;它只说"哪个检查挂在哪个阶段"。检查逻辑写在 .co(Colang flow)或内置护栏库里,真正被触发是在运行时。

一句类比:config 像电路板上的插槽标签——它规定 input 插槽、output 插槽各接了哪几个模块;模块里的电路(真正干活的逻辑)是另一回事,通电(执行)更是另一回事。


2. 顶层全景:从文件到对象到运行时

一个 guardrails 配置在磁盘上通常是一个目录,里面混着 YAML 和 Colang:

my_config/
├── config.yml # 主配置:models、rails 挂载表、各种开关
├── rails.co # Colang flow 定义(被 config.yml 里的名字引用)
├── prompts.yml # 可选:自定义任务 prompt
└── kb/ # 可选:知识库文档(*.md)

加载这个目录,会经历三段:

目录 (YAML + .co + kb/)

│ RailsConfig.from_path(path) ← config.py:2066

┌─────────────────────────┐
│ _load_path: 遍历目录 │ config.py:1528
│ · *.yml → 合进 raw dict │
│ · *.co → 收集待解析 │
│ · kb/*.md → docs │
└───────────┬─────────────┘
│ _parse_colang_files_recursively config.py:1642

┌─────────────────────────┐
│ RailsConfig (Pydantic) │ config.py:1711
│ · models: [...] │ ← 用哪些模型
│ · flows: [...] │ ← 所有 flow 定义
│ · rails: Rails{...} │ ← 五类护栏的挂载表
└───────────┬─────────────┘
│ 交给引擎(本章到此为止)

LLMRails(config) / IORails → 运行时把挂上的 flow 真正跑起来(见 03/04/06)

怎么读这张图(从上到下是加载顺序,命中即合并): 目录里的每个文件被读进一个 raw_config 字典,Colang 文件延后解析(因为要先知道 colang_version),最后整体喂给 RailsConfig 这个 Pydantic 模型做校验。得到的 RailsConfig 对象就是"声明"的最终形态;它被 LLMRails 拿去,在 __init__ 里把每个挂上的 flow 名字核对、标记、注册——但执行发生在后续的对话回合里。

各部件一句话职责:

部件干什么在哪
config.yml / *.yaml声明 models、rails 挂载表、各类开关用户目录
*.co定义被挂载的 flow(Colang)用户目录,见 02
RailsConfig承载全部配置的 Pydantic 对象config.py:1711
Rails五类护栏的挂载表(input/output/…)config.py:1401
RailsConfigData内置护栏的参数(阈值/端点/实体表…)config.py:1297
from_path / from_content两个加载入口config.py:2066 / config.py:2102
LLMRails.__init__把 config 装配进运行时(本章边界)rails/llm/llmrails.py:352

3. 五类护栏(rails 分类学)

NeMo Guardrails 把一次对话拆成若干管线阶段,每个阶段可以挂 0 到多个 flow。这就是"护栏分类"的由来——护栏是按它在管线里的位置分类的,不是按它检查什么内容。

3.1 五类一览表

从"用户说话"到"机器人回话",护栏依次可能在这些位置介入:

rail 类别在管线的位置由哪个配置段驱动配置字段/类型典型用途
input(输入)用户消息进来、还没进对话逻辑之前rails.input.flowsInputRailsconfig.py:716越权/攻击/PII 检测,不通过就拒绝
dialog(对话/话题)决定"用户意图 → 下一步做什么"rails.dialogDialogRailsconfig.py:865话题边界、按意图走预设流程
retrieval(检索)RAG 取回知识块之后、喂给 LLM 之前rails.retrieval.flowsRetrievalRailsconfig.py:777过滤/改写检索到的 chunk
execution(执行/工具)调用自定义 action 或工具的前后rails.actions / rails.tool_input / rails.tool_outputActionRailsconfig.py:786)、ToolInputRailsconfig.py:819)、ToolOutputRailsconfig.py:802校验工具名/参数,过滤工具返回
output(输出)机器人生成回复之后、返回用户之前rails.output.flowsOutputRailsconfig.py:758敏感词、事实核查、自检、脱敏

注:官方常说的"五类"以 input / dialog / retrieval / execution / output 为准。代码里 execution(执行护栏)细分成两块——旧的 ActionRailsconfig.py:786,只有 instant_actions 一个选项)和较新的 tool 护栏 ToolInputRails / ToolOutputRailsconfig.py:819 / config.py:802),它们是"工具调用"这条现代执行路径上的护栏。

3.2 管线里它们的先后(一次回合的护栏顺序)

用户输入

▼ ① input rails —— rails.input.flows 不过→拒绝/改写

▼ ② dialog rails —— rails.dialog 判定意图、选下一步

├── (若走 RAG) ③ retrieval rails —— rails.retrieval.flows 过滤知识块

├── (若调工具) ④ execution/tool rails —— rails.tool_input/tool_output

▼ ⑤ output rails —— rails.output.flows 不过→拦截/脱敏

机器人回复

这张图只表达"声明的阶段各自落在管线哪一段"。 谁真正调度这些阶段、事件如何驱动,是运行时的事——见 03(事件驱动主线)和 04(输入/输出护栏的执行细节)。

3.3 每类的关键字段(只看声明面)

input(InputRails, config.py:716 三个字段:

  • flowsconfig.py:733)——挂载的 flow 名字列表;
  • parallelconfig.py:719)——是否并行跑多个输入护栏;
  • speculative_generationconfig.py:724)——是否让输入护栏与 LLM 生成并发(仅非流式 generate_async 支持)。

output(OutputRails, config.py:758 除了 flowsconfig.py:766)和 parallel,还有一个 streaming 子配置 OutputRailsStreamingConfigconfig.py:739)——用于在流式输出时按 token 块(chunk_size / context_size)施加输出护栏。

dialog(DialogRails, config.py:865 和前两者不同,它不是一个 flows 列表,而是"对话生成怎么进行"的开关:

  • single_callSingleCallConfig, config.py:836)——是否用一次 LLM 调用同时完成"意图识别 + 下一步 + 生成"(拓扑护栏的单次调用优化);
  • user_messages——是否只用 embedding 来匹配用户意图。

retrieval(RetrievalRails, config.py:777 最简单,只有一个 flows 列表。

execution / tool 三个:

  • ActionRailsconfig.py:786)——目前只有 instant_actions(哪些 action 立即返回、不进事件循环);
  • ToolInputRails / ToolOutputRailsconfig.py:819 / config.py:802)——各有 flows + parallel,分别护栏"工具结果进来"和"工具调用出去"。

4. RailsConfig:承载一切的顶层对象

所有配置最终都变成一个 RailsConfig 实例(config.py:1711,Pydantic BaseModel)。它字段很多,但骨架就三块:用什么模型、有哪些 flow、护栏怎么挂

4.1 models 段——用哪些模型

RailsConfig.modelsconfig.py:1717)是一个 Modelconfig.py:103)列表。每个 Model 声明一个被引擎使用的模型:

models:
- type: main # 用途标签:main 是主对话模型
engine: openai # 提供方/框架
model: gpt-4o # 模型名

Model 的关键字段:typeconfig.py:114,用途,如 mainembeddings、内容安全模型自定义类型)、engineconfig.py:115)、modelconfig.py:116)、parametersconfig.py:124,透传给底层)、modeconfig.py:126chattext)。同一个 config 可以声明多个模型,护栏 flow 可以用 $model=<type> 语法引用非 main 的模型(校验见 config.py:1846check_model_exists_for_input_rails)。模型抽象与调用见 05

4.2 flows 段与 colang_version

RailsConfig.flowsconfig.py:1729)是所有 flow 定义的汇总——它们来自目录里的 .co 文件(解析后合并进来),也可能内联在 YAML 里。config 里的 rails.*.flows 只是"名字",真正的 flow 定义在这个 flows 列表里,靠名字对应。

colang_versionconfig.py:1790,默认 "1.0")决定 .co 用哪套 Colang 语法/运行时解析——1.02.x 是两套(llmrails.py:376colang_version_to_runtime 据此选运行时)。这个字段必须在解析 .co 之前就知道,所以加载器会先扫 YAML 拿到它(config.py:1651)。

4.3 rails 段——挂载表本身

RailsConfig.railsconfig.py:1807)是一个 Rails 对象(config.py:1401),它把五类护栏聚在一起:

Rails 字段类型说明
inputInputRails输入护栏挂载config.py:1408
outputOutputRails输出护栏挂载config.py:1409
retrievalRetrievalRails检索护栏挂载config.py:1410
dialogDialogRails对话生成开关config.py:1414
actionsActionRails执行护栏(instant_actions)config.py:1415
tool_outputToolOutputRails工具调用出口护栏config.py:1416
tool_inputToolInputRails工具结果入口护栏config.py:1420
configRailsConfigData内置护栏的参数config.py:1404

4.4 RailsConfigData:内置护栏的"参数",不是"挂载"

要区分两件事:

  • rails.input.flows: [self check input] —— 挂载(把这个护栏挂到 input 阶段);
  • rails.config.jailbreak_detection: {...} —— 参数(这个护栏用什么端点/阈值)。

RailsConfigDataconfig.py:1297)就是后者:一大堆内置护栏(越权检测、敏感数据、正则、注入检测、各家第三方 API…)各自的配置块。它只在对应护栏被挂到某个阶段时才生效——例如 jailbreak_detectionconfig.py:1325)配了但没有任何 input flow 引用它,加载时会告警(config.py:1994)。内置护栏库本身见 04


5. 加载:from_path / from_content 如何把目录变成对象

两个入口,殊途同归到 parse_objectconfig.py:2157model_validate)。

5.1 from_path:从目录/文件加载

RailsConfig.from_pathconfig.py:2066)流程(简化演示,示意,非源码):

# 演示 from_path 的三段式加载
def from_path(config_path):
raw_config, colang_files = _load_path(config_path) # 1. 扫目录:YAML 合并,.co 收集
if raw_config.get("import_paths"):
_load_imported_paths(raw_config, colang_files) # 2. 递归拉入 import 的配置
_parse_colang_files_recursively( # 3. 按 colang_version 解析 .co
raw_config, colang_files, parsed_colang_files=[])
return cls.parse_object(raw_config) # → RailsConfig

重点看:YAML 先合并、Colang 后解析——因为解析 .co 需要先从 YAML 里读到 colang_version

_load_pathconfig.py:1528)是那台"扫目录机器":遍历目录(os.walk),.yml/.yamlyaml.safe_load 读进来(config.py:1576),.co 只登记路径待解析(config.py:1580),kb/ 下的 .md 收进 docsconfig.py:1572)。多个 YAML 文件之间由 _join_configconfig.py:1442)按字段规则合并(flows/models/prompts 拼接,标量字段覆盖)。

5.2 from_content:从字符串加载

RailsConfig.from_contentconfig.py:2102)接三个可选参数:yaml_contentcolang_contentconfig(dict)。它不碰磁盘,直接把传入的 YAML/Colang 字符串走同一套解析。这条路径常用于测试和内嵌场景:

# 演示:不落盘,直接用字符串构造配置 # 示意,非源码
config = RailsConfig.from_content(
yaml_content="""
models:
- type: main
engine: openai
model: gpt-4o
rails:
input:
flows: [self check input]
""",
colang_content='define flow self check input\n ...',
)

5.3 装配进运行时(本章边界)

RailsConfig 造好后交给 LLMRailsrails/llm/llmrails.py)。在 __init__ 里,config 只被声明性地装配——把挂在各阶段的 flow 名字收集起来、标记、核对,但不执行:

# llmrails.py:352 —— 把所有被 rails 引用的 flow 标为 system flow
rail_flow_ids = config.rails.input.flows + config.rails.output.flows + config.rails.retrieval.flows
for flow_config in self.config.flows:
if flow_config.get("id") in rail_flow_ids:
flow_config["is_system_flow"] = True # 不进 prompt
flow_config["is_subflow"] = True

这段(llmrails.py:352-359)体现了本章的核心论点:config 到运行时,做的仍是"登记"——把声明里引用的 flow 打上标记。紧接着的 _validate_configllmrails.py:478)逐一核对 rails.input/output/retrieval.flows 里的每个名字是否真有对应 flow 定义,没有就报 InvalidRailsConfigurationErrorllmrails.py:490)。至此声明装配完成;这些 flow 何时、如何被真正跑起来,是运行时的主线——见 0304


6. 一个最小可用 config(真实示例,示意标注)

下面是仓库里 ABC 示例机器人的 config.ymlexamples/bots/abc/config.yml),去掉冗长的对话样例后,恰好覆盖本章讲的三块:

# models 段:声明用哪些模型(§4.1)
models:
- type: main # 主对话模型
engine: openai
model: gpt-4

# rails 段:五类护栏的挂载表(§4.3)
rails:
input: # ① 输入阶段
flows:
- self check input # 挂一个"输入自检"护栏(名字→对应某个 .co flow)
output: # ⑤ 输出阶段
flows:
- self check output # 挂一个"输出自检"护栏
dialog: # ② 对话生成开关(不是 flows 列表,§3.3)
single_call:
enabled: False # 关闭"单次 LLM 调用"优化

配套的 .co(此处不展开,见 02)会定义名为 self check input / self check output 的 flow。整份 config 的意思用一句话说:

"用 gpt-4 当主模型;用户输入先过 self check input,机器人输出先过 self check output;对话生成用多次调用。" —— 全是声明,没有一行实现逻辑。

注意这份 config 里没有 colang_version,所以默认 1.0config.py:1790)。若要用 Colang 2.x,需显式写 colang_version: "2.x",且在 2.x 下用 rails.input/output 挂载会触发弃用告警(config.py:1695,改用 flow-based 配置)。


7. 边界与常见坑

  • config 不执行任何东西。 本章反复强调:它只是声明"挂什么到哪"。看到 rails.output.flows 里一个名字,不代表你能在这里读到它的检查逻辑——逻辑在 .co02)或内置库(04),执行在运行时(03)。
  • 挂载名字必须有对应 flow 定义。 rails.input/output/retrieval.flows 里每个名字都会被 _validate_configllmrails.py:478)核对;打错字 = 启动即报错。
  • dialog 段不是 flows 列表。 它是对话生成的开关(single_calluser_messages),容易和 input/output 的挂载语义混淆。
  • rails.config(参数)配了但没挂载 = 白配。 例如 jailbreak_detection 有配置但没在 input flows 里引用,只会告警不生效(config.py:1994)。
  • streaming 字段已弃用。 RailsConfig.streamingconfig.py:1812)被标记 deprecated,改用 stream_async();输出流式护栏走 rails.output.streamingconfig.py:771)。
  • 两套 Colang 不通用。 colang_version 选错,.co 会解析失败(config.py:1662ColangParsingError)。

8. 代码地图(导航索引)

主题文件路径符号名
顶层配置对象nemoguardrails/rails/llm/config.py:1711RailsConfig
从目录/文件加载nemoguardrails/rails/llm/config.py:2066RailsConfig.from_path
从字符串加载nemoguardrails/rails/llm/config.py:2102RailsConfig.from_content
最终校验入口nemoguardrails/rails/llm/config.py:2157RailsConfig.parse_object
扫目录加载器nemoguardrails/rails/llm/config.py:1528_load_path
多 YAML 合并规则nemoguardrails/rails/llm/config.py:1442_join_config
Colang 递归解析nemoguardrails/rails/llm/config.py:1642_parse_colang_files_recursively
五类护栏挂载表nemoguardrails/rails/llm/config.py:1401Rails
内置护栏参数nemoguardrails/rails/llm/config.py:1297RailsConfigData
input 护栏配置nemoguardrails/rails/llm/config.py:716InputRails
output 护栏配置nemoguardrails/rails/llm/config.py:758OutputRails
output 流式配置nemoguardrails/rails/llm/config.py:739OutputRailsStreamingConfig
dialog 护栏配置nemoguardrails/rails/llm/config.py:865DialogRails
单次 LLM 调用配置nemoguardrails/rails/llm/config.py:836SingleCallConfig
retrieval 护栏配置nemoguardrails/rails/llm/config.py:777RetrievalRails
执行护栏(action)nemoguardrails/rails/llm/config.py:786ActionRails
工具入口/出口护栏nemoguardrails/rails/llm/config.py:819 / config.py:802ToolInputRails / ToolOutputRails
模型声明nemoguardrails/rails/llm/config.py:103Model
config→运行时装配nemoguardrails/rails/llm/llmrails.py:352LLMRails.__init__
挂载名字校验nemoguardrails/rails/llm/llmrails.py:478LLMRails._validate_config
Colang 版本选运行时nemoguardrails/rails/llm/llmrails.py:376colang_version_to_runtime
最小真实示例examples/bots/abc/config.yml