NeMo Guardrails 事实核查实战:用 ask_llm 模式构建基于知识库的问答 Bot(以 2023 年 3 月美国就业报告为例)
NeMo Guardrails 事实核查实战:用 ask_llm 模式构建基于知识库的问答 Bot(以 2023 年 3 月美国就业报告为例)
在 NeMo Guardrails 的 qa/bots 系列示例中,latency_5_fact_checking_ask_llm 是一个专门演示"基于知识库(KB)的事实核查"能力的问答机器人:它把美国劳工统计局(BLS)发布的 2023 年 3 月《就业形势报告》全文作为知识库文档,让 LLM 在回答报告相关问题时,先经过 ask_llm 方式的事实核查(Fact Checking)再给出答复。读完本文,你将掌握 NeMo Guardrails 中 fact_checking 配置的完整用法、$check_facts 上下文变量的触发机制、self_check_facts 动作的底层实现,以及如何复现该示例的验证流程。
一、示例的定位:仓库中的"事实核查"演示单元
latency_5_fact_checking_ask_llm 位于仓库的 qa/bots/latency_5_fact_checking_ask_llm 目录,它是 QA 延迟对比系列(latency_0_baseline 至 latency_6_fact_checking_align_score)中的一员。整个目录只有 4 个文件,职责非常清晰:
| 文件 | 角色 |
|---|---|
| config.yml | 机器人主配置:指定主模型、开启 ask_llm 事实核查 provider、注册输出 rail |
| general.co | Colang 对话流程:问候、能力介绍、知识库介绍、通用话题 |
| report.co | Colang 报告问答流程:识别"报告问题"并设置 $check_facts = True |
| kb/report.md | 知识库文档:2023 年 3 月美国就业形势报告全文 |
该系列的其他示例(latency_1_normal、latency_2_single_call、latency_3_embeddings_only、latency_4_compact)共用同一份 kb/report.md 和几乎相同的 general.co / report.co,差异仅在 config.yml 中体现(single_call 开关、embeddings_only 开关、prompting_mode、是否启用事实核查)。因此 latency_5 的价值在于:在同等知识库与对话流程的基础上,单独叠加"LLM 事实核查"这一能力,用于测量它带来的额外延迟与 LLM 调用开销。
二、知识库文档:2023 年 3 月美国就业报告(report.md)
kb/report.md 是 BLS 官方发布的《The Employment Situation -- March 2023》新闻稿全文,共 135 行,直接作为该示例的知识库语料。从 qa/latency_report.py 的测试问题可以看到,这份文档被用于两类验证:回答报告内事实(触发事实核查)与回答报告外内容(检验幻觉检测 / 拒绝能力)。
2.1 文档结构与总体结论
文档标题为 Jobs Report - March 2023,正文按 BLS 新闻稿的固定格式组织:开篇给出总体结论,随后分 Household Survey Data(家庭调查数据) 与 Establishment Survey Data(企业调查数据) 两大板块。核心总览如下:
- 2023 年 3 月非农就业总人数增加 236,000,失业率基本持平于 3.5%;
- 就业继续增长的行业:休闲与酒店业、政府、专业和商业服务、医疗保健;
- 该新闻稿基于两项月度调查:家庭调查衡量劳动力状态(含按人口特征划分的失业情况),企业调查衡量按行业划分的非农就业、工时与收入。
2.2 家庭调查数据要点
| 指标 | 数值 |
|---|---|
| 失业率 | 3.5%,失业人数 580 万,均较上月变化不大 |
| 西班牙裔失业率 | 降至 4.6% |
| 成年男性 / 成年女性 / 青少年失业率 | 3.4% / 3.1% / 9.8% |
| 白人 / 黑人 / 亚裔失业率 | 3.2% / 5.0% / 2.8% |
| 永久失业者 | 增加 172,000,至 160 万 |
| 重新进入劳动力市场者 | 减少 182,000,至 170 万 |
| 长期失业者(失业 27 周及以上) | 110 万,占全部失业者的 18.9% |
| 劳动参与率 | 62.6%,持续上行 |
| 就业人口比 | 60.4% |
| 因经济原因兼职者 | 410 万,基本持平 |
| 想工作但不在劳动力队伍者 | 490 万 |
| 边际附着劳动力 | 130 万 |
| 灰心丧气的求职者 | 351,000 |
报告同时指出:劳动参与率与就业人口比仍低于疫情前(2020 年 2 月)的 63.3% 与 61.1%。
2.3 企业调查数据要点
| 指标 | 数值 |
|---|---|
| 3 月非农就业增加 | 236,000(前 6 个月月均 +334,000) |
| 休闲与酒店业 | +72,000(食品服务与饮吧 +50,000;仍低于疫情前 368,000,即 2.2%) |
| 政府 | +47,000(低于疫情前 314,000,即 1.4%) |
| 专业和商业服务 | +39,000 |
| 医疗保健 | +34,000(家庭保健服务 +15,000、医院 +11,000、护理与住宿护理设施 +8,000) |
| 社会救助 | +17,000 |
| 运输与仓储 | +10,000(快递 +7,000、航空运输 +6,000、仓储 -12,000) |
| 零售贸易 | -15,000(建材与园艺设备 -9,000、家具/家装/电子电器零售 -9,000、百货商店 +15,000) |
| 平均时薪 | +9 美分(+0.3%)至 $33.18;近 12 个月累计 +4.2% |
| 生产与非主管雇员平均时薪 | +9 美分至 $28.50 |
| 平均每周工时 | 34.4 小时(-0.1);制造业 40.3 小时、加班 3.0 小时 |
报告末尾还给出了数据修订说明(1 月从 +504,000 下修至 +472,000,2 月从 +311,000 上修至 +326,000),并预告 4 月就业形势报告将于 2023 年 5 月 5 日发布。这些细节在知识库中都是可被 LLM 检索并接受事实核查的"证据"(evidence)。
三、配置解析:如何开启 ask_llm 事实核查
latency_5 的核心配置在 config.yml 中,全文如下:
models:
- type: main
engine: openai
model: gpt-3.5-turbo-instruct
rails:
config:
fact_checking:
provider: "ask_llm"
output:
flows:
- check facts
逐段说明:
- models 段:声明主模型为 OpenAI 的
gpt-3.5-turbo-instruct。事实核查动作复用的正是这个主模型(见下文self_check_facts的llm参数),因此该示例的核查开销是"在同一个模型上多发起一次专门的任务调用"。 - rails.config.fact_checking:
provider: "ask_llm"指明事实核查采用"让 LLM 自己判断回答是否与证据一致"的方式。与之相对的另一个 provider 是align_score(见第六节)。 - rails.output.flows:
- check facts将self check facts输出 rail 注册为输出方向的可用流程。
从源码看,fact_checking 配置项由 nemoguardrails/library/factchecking/rail_config.py 中的 FactCheckingRailConfig 模型定义,除 parameters 外还支持 fallback_to_self_check: bool(默认 False,表示当其他核查方式失败时是否回退到 self-check);而 factchecking rail 本身在 nemoguardrails/library/factchecking/rail.py 中以 manifest 形式注册,其 capabilities 标注为 fact_check,分类为 config 与 output。
四、Colang 流程编排:何时才真正触发核查
事实核查并不是对每条消息都执行,而是由业务流程显式开启,这正是该示例最有教学意义的地方。
4.1 报告问答流程(report.co)
report.co 定义了"报告问题"的识别与触发逻辑:
define user ask about report
"What was last month's unemployment rate?"
"Which industry added the most jobs?"
"How many jobs were added in the transportation industry?"
define flow answer report question
user ask about report
# We enable fact checking for questions about the report
$check_facts = True
bot provide report answer
关键点:当用户消息命中 ask about report 意图后,流程在生成回答前将上下文变量 $check_facts 置为 True。这个变量是事实核查输出 rail 的"开关"——只有它为真时,check facts 流程才会真正调用 LLM 进行核查。
4.2 通用对话流程(general.co)
general.co 提供了其余对话能力:
- 问候流程:
user express greeting→bot express greeting("Hello! How can I assist you today?"); - 能力介绍流程:告诉用户"我是一个演示事实核查与幻觉检测能力的示例 bot,可以问我知识库中的文档来测试事实核查,或问其他话题来测试幻觉检测";
- 知识库介绍流程:明确告知"我的知识库包含 2023 年 3 月美国就业报告的信息,可用于事实核查";
- 通用问题意图(
ask general question):股票推荐、餐厅推荐、写邮件、选举等与报告无关的问题。
注意 report.co 与 general.co 的意图是互斥的:只有命中 ask about report 才会设置 $check_facts = True,通用问题不触发核查,从而可以对比"带核查"与"不带核查"两类回答的差异。
4.3 输出 rail 的内部流程
"check facts" 对应的底层流程定义在 nemoguardrails/library/self_check/facts/flows.co:
flow self check facts
"""Check if the previous answer is accurate w.r.t. the relevant chunks.
This output rail must be enabled explicitly per output message by setting
the $check_facts context variable to True.
"""
if $check_facts == True
global $check_facts
$check_facts = False
$response = await SelfCheckFactsAction
if $response.is_blocked
if $system.config.enable_rails_exceptions
send FactCheckRailException(message="Fact check failed. The accuracy of the previous answer was below the required threshold.")
else
bot refuse to respond
abort
这段流程说明了三点:其一,rail 每次执行后会把 $check_facts 复位为 False,避免重复核查;其二,核查动作返回的 RailOutcome 若为 is_blocked,则在开启 enable_rails_exceptions 时抛出 FactCheckRailException,否则让 bot 拒绝回答并 abort;其三,该 rail 必须在 config.yml 的 output.flows 中注册后才可用。
五、底层实现:self_check_facts 动作如何判断"事实是否准确"
事实核查的核心动作是 self_check_facts,完整实现位于 nemoguardrails/library/self_check/facts/actions.py。其工作流程可概括为:
- 取证据与回答:
evidence取自relevant_chunks(未显式传入时从上下文的relevant_chunks读取),response取自bot_message。如果没有任何证据(not evidence),直接返回_fact_check_outcome(1.0),即"无证据可查,视为通过"。 - 渲染任务提示:使用
LLMTaskManager.render_task_prompt(task=Task.SELF_CHECK_FACTS, ...)渲染核查提示,任务标识SELF_CHECK_FACTS = "self_check_facts"定义于 nemoguardrails/llm/types.py。 - 发起 LLM 调用:通过
llm_call调用主模型,temperature取配置中的最低温度(无配置时为 0.0),max_tokens上限 1024;若响应被截断(warn_if_truncated),直接按"不通过"处理(accuracy 0.0)。 - 解析输出:优先使用任务注册的输出解析器;没有时强制使用
is_content_safe解析器,把模型回答解析为布尔结果。 - 判定与放行:
result = float(not is_not_safe)得到 0 或 1 的准确率,交给_fact_check_outcome与阈值比较。阈值常量定义在文件顶部:
FACT_CHECK_THRESHOLD = 0.5
def _fact_check_outcome(accuracy: float) -> RailOutcome:
if accuracy < FACT_CHECK_THRESHOLD:
return RailOutcome.block(metadata={"accuracy": accuracy})
return RailOutcome.allow(metadata={"accuracy": accuracy})
也就是说,只要 LLM 判定回答与证据不一致(accuracy 为 0,低于 0.5 阈值),该回答就会被 block,并在 metadata 中记录实际 accuracy 供观测。这也解释了为什么"幻觉检测"与"事实核查"在这个示例中被放在一起介绍:对知识库外的问题(如 general.co 中的股票推荐、总统选举等),没有相关证据时流程会直接放行,行为差异可以通过测试观察。
另外,nemoguardrails/rails/llm/config.py 在配置校验阶段要求:当输出 rail 包含 self check facts 时,必须提供 self_check_facts 提示模板,否则抛出 InvalidRailsConfigurationError——这说明"提示模板"是启用该 rail 的硬性前置条件。
六、与 align_score provider 的对比
同一个知识库与流程,也可以换用 align_score provider。对照示例位于 qa/bots/latency_6_fact_checking_align_score/config.yml:
rails:
config:
fact_checking:
provider: "align_score"
parameters:
endpoint: "http://localhost:5000/alignscore_large"
与 ask_llm 的差别在于:align_score 需要额外部署一个 AlignScore HTTP 服务(见 nemoguardrails/library/factchecking/align_score/),由该服务对"回答 vs 检索片段"进行对齐打分,而不是让主 LLM 自查。从 align_score/actions.py 的实现看,当 AlignScore 端点不可用时会记录警告,若配置了 fallback_to_self_check 则回退到 self_check_facts——这正是上一节提到的 FactCheckingRailConfig.fallback_to_self_check 字段的用途。两个示例共享同一份 kb/report.md 与 report.co,便于直接对比两种 provider 的延迟与调用次数差异。
七、如何复现与验证:延迟对比实验
该示例还承担着量化"事实核查带来多少额外开销"的任务,验证入口是 qa/latency_report.py。脚本将 7 个配置(含 latency_5_fact_checking_ask_llm)与一组固定问题做交叉测量,每个问题重复 30 次(NUM_TEST_RUNS = 30),并记录 total_overall_time、total_llm_calls_time、num_llm_calls、token 用量等指标,输出到 latency_report_detailed_openai.tsv 与 latency_report_openai.tsv。测试问题分三类:
- 闲聊类:
"Hi"、"Hey, how's the weather in Santa Clara today?"、"Hello! What can you do for me?"; - 报告类(触发事实核查):
"What was the unemployment rate in March?"、"What was the unemployment rate among Asians?"、"Has the labor force participation rate been trending up according to the US jobs report?"、"How many people remained discouraged by the job market?"、"What percentage of unemployed people have been jobless for a long time?"、"What industries have shown a continued increase in employment?"; - 报告外事实类(验证幻觉检测/拒绝能力):
"What was Barack Obama's age when he was appointed the President of the United States?"、"What is the recipe to make Aglio Olio pasta?"。
运行方式(需要先按 qa/README.md 安装依赖并设置 OPENAI_API_KEY):
# 在仓库根目录执行
pip install -U scikit-learn
export OPENAI_API_KEY=...
python qa/latency_report.py
脚本内部通过 RailsConfig.from_path 加载每个配置目录、LLMRails(config) 构建应用、app.generate(messages=...) 发起对话,并用 llm_stats 统计每次运行的 LLM 调用次数与耗时;两次请求之间 time.sleep(1) 以避免触发 API 限流影响测量。观察 latency_5 与 latency_0_baseline / latency_4_compact 在同一组报告问题上的 num_llm_calls 与总耗时,即可直观看到 ask_llm 事实核查为每条报告类回答额外增加的 LLM 调用成本。
八、小结
latency_5_fact_checking_ask_llm 用一份真实、结构化的知识库文档(2023 年 3 月美国就业报告)完整演示了 NeMo Guardrails 的事实核查闭环:通过 fact_checking.provider: "ask_llm" 注册核查能力,通过 report.co 中的 $check_facts = True 精确控制触发范围,由 self_check_facts 动作调用主 LLM 比对"回答与证据"并依据 0.5 阈值决定放行或拦截,最终由 qa/latency_report.py 量化验证其开销。掌握这一模式后,你可以把任意领域文档放入 kb/,用同样的配置与流程为自己的 LLM 应用加上基于知识库的事实核查能力。