跳到主要内容

LLM 抽象与 prompt 渲染层

30 秒导读: NeMo Guardrails 里五类护栏(见 01)都要"问模型一句话"——检查输入是否安全、生成 bot 回复、判断有没有幻觉。这一章讲的是它们共用的那块底座:给一个任务名(Task.SELF_CHECK_INPUT),它负责挑对 prompt 模板、按当前 model 填好模板、通过一个统一入口 llm_call 打到真实的 provider,再把模型吐的字符串解析回结构化结果。rail 自己一行 HTTP 都不写。

本章是横切层:不讲某个具体 rail 的判定逻辑(那在 04),只讲"任何 rail 想调模型时,底下发生了什么"。


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

一句话定义: 这是一层"任务导向的 LLM 客户端"——你说"我要做 generate_bot_message 这个任务",它把 prompt 组装、模型调用、输出解析全包了。

它解决的问题。 一个护栏框架要对很多种模型说话:OpenAI 的 GPT、NVIDIA NIM 上的 Llama、DeepSeek、本地 Ollama……而且同一个任务,不同模型需要不同的 prompt 写法(GPT-3.5-instruct 要纯文本补全式 prompt,ChatGPT 要 messages 数组,Llama3 要它自己的 <|begin_of_text|> 格式)。如果每个 rail 都自己拼 prompt、自己调 SDK,代码会爆炸。

这层的价值:把三件事从 rail 里抽走。

抽走的事谁负责白话
挑 + 填 promptLLMTaskManager + prompts/*.yml按 task 名和当前 model 选对模板,用 Jinja 填进对话历史
真正调模型llm_call + framework/client统一入口,底下是 OpenAI 兼容 HTTP 或 LangChain
解析输出output parsers"User intent: ..." 这种文本解析回结构

用起来什么样。 一个 rail 动作里的真实调用序列长这样(来自 library/content_safety/actions.py:76-113,已简化):

# 示意,非源码:一个 rail 想问模型"这段输入安全吗"
prompt = task_manager.render_task_prompt(task, context, events) # ① 组装
response = await llm_call(model, prompt, stop=stop_tokens) # ② 调用
result = task_manager.parse_task_output(task, response.content) # ③ 解析

一句话直觉/类比。 把它当成一个"翻译中转站":rail 只会说抽象的"任务语",中转站负责翻译成每个模型听得懂的方言,把话递过去,再把回话翻译回"任务语"。rail 永远不需要学任何一门模型方言。


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

主线一句话: Task 枚举 → 选模板 → Jinja 渲染 → llm_call → framework/client → provider → 解析回结果。

下面这张图从上到下就是一次任务调用的完整数据流。左边是"组装 prompt",中间是"打出去",右边是"收回来解析"。

┌─────────────────────────────────────────────┐
rail 动作 │ LLMTaskManager (taskmanager.py) │
传入一个 Task ──────────▶│ │
(如 SELF_CHECK_INPUT) │ render_task_prompt() │
│ │ │
│ ├─▶ get_prompt(config, task) ◀── prompts.py
│ │ 按 model 打分选模板 ◀── prompts/*.yml
│ │ │
│ └─▶ _render_string() │
│ Jinja 沙箱 + 过滤器 ◀── filters.py │
└────────────────┬────────────────────────────┘
│ 渲染好的 str 或 messages[]

┌─────────────────────────────────────────────┐
│ llm_call() (actions/llm/utils.py) │
│ 统一入口:日志/tracing/token 统计/异常包装 │
└────────────────┬────────────────────────────┘
│ model.generate_async(...)

┌──────────────────────────────────────────┐
│ LLMModel 抽象 (types.py Protocol) │
│ │
┌─────┴─────┐ ┌─────────┴─────────┐
▼ default 框架 ▼ langchain 框架
OpenAIChatModel LangChain adapter
→ OpenAICompatibleClient → 任意 langchain-<provider>
→ HTTP /chat/completions (需要 LangChain 的引擎才走)

▼ LLMResponse(content, usage, ...)
┌─────────────────────────────────────────────┐
│ parse_task_output() ◀── output_parsers.py │
│ "User intent: ..." → 结构化 │
└─────────────────────────────────────────────┘

部件一句话职责。

部件干什么在哪个文件
Task(枚举)给每个"要问模型的事"起个稳定名字llm/types.py:19
LLMTaskManager渲染 prompt、解析输出、注册过滤器/解析器llm/taskmanager.py:58
get_prompt按 task 名 + 当前 model 打分选模板llm/prompts.py:146
prompts/*.yml按模型分文件的 prompt 模板库llm/prompts/
TaskPrompt / MessageTemplate模板的 Pydantic 结构rails/llm/config.py:555 / :548
过滤器Jinja 里把事件流转成 colang 文本等llm/filters.py
llm_call统一调用入口(日志/统计/异常/流式)actions/llm/utils.py:52
LLMModel / LLMResponseprovider 无关的调用协议与返回类型llm/types.pytypes.py
DefaultFrameworkOpenAI 兼容的默认后端工厂llm/frameworks/default.py:65
output parsers把模型文本解析回结构llm/output_parsers.py
LFUCache缓存模型调用结果llm/cache/lfu.py:80

3. 核心原理(逐个机制,由浅入深)

3.1 Task 枚举——把"要问模型的事"变成稳定名字

它要解决的小问题: 框架里有几十处地方要调模型(生成用户意图、生成 bot 回复、自检输入、Llama Guard 检查……)。每处都需要不同的 prompt 和不同的解析。怎么让它们各自"认领"到对的配置?

思路: 用一个枚举当"主键"。每个任务一个枚举值,枚举的 .value(字符串)同时是 prompt 模板里 task: 字段的值,把代码里的调用点和 YAML 里的模板一一对应起来。

Task 枚举分两组(llm/types.py:19 class Task):核心对话任务(GENERATE_USER_INTENTGENERATE_BOT_MESSAGEGENERAL 等,对应 Colang 主线,见 03),以及各 rail 专用任务(SELF_CHECK_INPUTLLAMA_GUARD_CHECK_INPUTSELF_CHECK_FACTS 等)。

# 示意,非源码:枚举值就是模板里的 task 名
class Task(Enum):
GENERATE_BOT_MESSAGE = "generate_bot_message" # ← 对应 general.yml 里 task: generate_bot_message
SELF_CHECK_INPUT = "self_check_input"

关键细节: 几乎所有入口方法都接受 Union[str, Task](如 render_task_prompt 的签名 taskmanager.py:281),内部统一 str(task.value) if isinstance(task, Task) else task 归一(prompts.py:133)。所以自定义 rail 可以用任意字符串任务名,不必登记进枚举。

3.2 按模型选模板——同一个任务,不同模型给不同 prompt

它要解决的小问题: generate_user_intent 这个任务,给 GPT-3.5-instruct 该用补全式纯文本 prompt,给 ChatGPT 该用 messages 数组,给 Llama3 又该用它的特殊标记格式。一个任务名,要能路由到"最合适的那份模板"。

思路:打分匹配,分数最高者胜。 启动时 _load_prompts()(prompts.py:29)遍历 llm/prompts/ 下所有 .yml,把每个 TaskPrompt 读进内存。选模板时 _get_prompt()(prompts.py:57)对每个候选模板算一个 0~1 的匹配分,取最高。

模板用 models: 列表声明它"为哪些引擎/模型而写"(如 openai/gpt-3.5-turbo-instruct)。当前 model 由 get_prompt 拼成 engine[/model] 字符串(prompts.py:152-157),再去和每个模板的 models: 比。打分规则(prompts.py:71-107)大致:

情形分数
模板没写 models(通用模板)0.2general.yml 兜底
_model in model(子串)0.4弱匹配
model_model/ 开头0.5引擎前缀命中
provider/base_model 是前缀0.9nvidia/llama-3.1-nemotron
全串精确相等1.0完全命中

分数相等时"取后一个"(prompts.py:120 if _score >= matching_score),这让用户在自己 config 里追加的模板能覆盖内置同分模板(config.prompts 拼在内置之后,prompts.py:164)。

第二维度:prompting mode。 除了 model,还有一个 mode 维度(standard / compact),对应 config.prompting_mode。mode 不匹配会打折:standard 模板打五折仍可用(_score *= 0.5,prompts.py:112-116),非 standard 的不匹配 mode 直接丢弃。实际例子:openai-chatgpt.yml:57mode: compact 的紧凑版模板。

选不到任何模板就抛 ValueError(prompts.py:127)——宁可报错不静默

两种模板体裁。 TaskPrompt(config.py:555)要么给 content(字符串,补全模型用),要么给 messages(MessageTemplate 列表,chat 模型用),二选一(校验器 config.py:593)。对比:

  • general.yml:5general 任务用 content: 纯文本;
  • openai-chatgpt.yml:7 同名任务用 messages: 数组,每条是 type: system/userMessageTemplate,或一个像 "{{ sample_conversation | to_messages }}" 的字符串(字符串会被 literal_eval 成 messages,见 3.3)。

3.3 Jinja 渲染——把对话历史填进模板

它要解决的小问题: 模板里写着 {{ history | colang | verbose_v1 }},得把运行时的事件流转成模型看得懂的对话文本,填进去。

思路:沙箱化的 Jinja + 一组领域过滤器。 LLMTaskManager.__init__(taskmanager.py:61)建了一个 SandboxedEnvironment(Jinja 的沙箱环境,禁止模板访问危险属性——因为模板可能来自用户 config),然后注册十几个自定义过滤器(taskmanager.py:68-81)。

渲染主流程 _render_string(taskmanager.py:113):

  1. meta.find_undeclared_variables 先扫出模板里用到哪些变量(taskmanager.py:130);
  2. 组一个 render_context,默认塞入 history(事件流)、general_instructionssample_conversation 等(taskmanager.py:133);
  3. 把调用方传的 context 和注册进来的 prompt_context 里、且模板真的用到的变量补进去(prompt_context 里的值若是 callable 会现算,taskmanager.py:155);
  4. template.render(...)

general_instructions 从 config 的 instructions 里取第一条 type == "general" 的内容(_get_general_instructions,taskmanager.py:99)——这就是所有 prompt 顶部那段"你是一个 helpful bot"的来源。

过滤器就是"事件流 → prompt 文本"的翻译器。 关键几个(filters.py):

过滤器作用位置
colang事件数组 → colang 格式对话历史filters.py:26
verbose_v1colang → User message:/User intent: 冗长格式filters.py:254
to_messagescolang → chat messages 数组filters.py:133
first_turns/last_turns截取前/后 n 轮filters.py:348/:363

colang 过滤器底层调 get_colang_history(actions/llm/utils.py:464),它能自动嗅探 Colang 1.0 还是 2.x 的事件并分别渲染。

两条渲染出口。 render_task_prompt(taskmanager.py:281)按模板体裁分流:有 content 就走 _render_string 返回字符串;否则走 _render_messages(taskmanager.py:162)返回 messages 列表。字符串模板里若整体是 [{...}] 形式会被 literal_eval 解成 messages(taskmanager.py:184)。

超长自动截断(巧妙处)。 渲染完若超过 prompt.max_length(默认 16000 字符,config.py:573),就从历史最前面逐个删事件重渲染,直到装得下(taskmanager.py:305-310)。连空历史都超长才抛异常。多模态图片的 base64 在长度计算时只按占位符算(_get_messages_text_length,taskmanager.py:242),避免一张大图撑爆长度检查。

3.4 统一的 llm_call 入口——所有模型调用的唯一门

它要解决的小问题: 每次调模型都要做一堆横切的事:记 prompt/completion 日志、累加 token 统计、抽取推理轨迹(reasoning)、包装异常、支持流式。不能让每个 rail 各写一遍。

思路:一个装饰过的异步函数,把横切全收进来。 llm_call(actions/llm/utils.py:52)被 @track_llm_call(utils.py:51)装饰(负责 tracing span)。它的骨架:

# 示意,非源码:llm_call 的主干
async def llm_call(llm, prompt, ...):
if not isinstance(llm, LLMModel): # 只认 LLMModel 协议
raise TypeError(...)
_setup_llm_call_info(model, ...) # 初始化 context 里的 LLMCallInfo
_log_prompt(prompt) # 记 prompt 日志
chat_prompt = _ensure_chat_messages(prompt) # dict[] → ChatMessage[]
if streaming_handler: # 流式分支
return await _stream_llm_call(...)
try:
response = await model.generate_async(chat_prompt, stop=stop, **params)
except Exception as e:
_raise_llm_call_exception(e, model) # 统一包成 LLMCallException
_store_reasoning_traces(response) # 抽 <think> 或 reasoning
_update_token_stats(response) # 累加 token
return response

几个值得记住的细节:

  • 入参强类型收口。 llm_call 只接受实现了 LLMModel 协议的对象,否则直接 TypeError(utils.py:65-71)——把"传错东西"挡在最外层。传进来的 prompt 若是 dict 列表,会先转成 ChatMessage(_ensure_chat_messages,utils.py:40)。
  • 异常统一域化。 任何 provider 报错都被 _raise_llm_call_exception(utils.py:314)包成 LLMCallException,并带上 model=/provider=/endpoint= 上下文,用 raise ... from 保留原始栈。
  • 推理模型两条抽取路径。 有的模型在结构化字段里给 reasoning,有的把它塞在正文的 <think>...</think> 里。_store_reasoning_traces(utils.py:338)先看 response.reasoning,没有再调 _extract_and_remove_think_tags(utils.py:347)从正文里挖出来并从正文删掉。标记不成对时只告警不乱切(utils.py:367)。
  • 静默截断预警。 warn_if_truncated(utils.py:399)专门处理"推理模型把 token 预算全花在思考上、正文为空、finish_reason="length""这个坑——正文空但调用"成功",下游解析器可能误判,于是给调用方一个显式的 fail-safe 信号。

流式路径。 _stream_llm_call(utils.py:93)逐块消费 model.stream_async,一边把内容推给 StreamingHandler,一边累加 reasoning / tool_calls / usage / provider_metadata,最后拼成一个完整 LLMResponse 返回——确保流式和非流式对下游是同一种返回类型

3.5 输出解析与多行提取——把模型文本翻回结构

它要解决的小问题: 补全模型返回的是一坨文本,比如 User intent: express greeting,或安全检查模型返回 unsafe\nS1, S2。得解析回结构化的东西。

思路:每个 TaskPrompt 可声明一个 output_parser 名,manager 里有个名字 → 函数的注册表。 LLMTaskManager__init__ 里建了 output_parsers 字典(taskmanager.py:83),parse_task_output(taskmanager.py:339)按模板的 output_parser 字段查函数、调用;查不到就原样返回并记一条 info。

内置解析器(llm/output_parsers.py):

解析器干什么位置
verbose_v1_parserUser message:/intent: 格式 → colangoutput_parsers.py:43
user_intent_parser剥掉 User intent: 前缀output_parsers.py:28
is_content_safe看头两个词是 safe/unsafe/yes/no → 布尔+违规类output_parsers.py:92
nemoguard_parse_prompt_safety解析 NemoGuard 的 JSON 安全输出output_parsers.py:141
nemotron_reasoning_parse_*先剥 <think> 再抽 harmful/unharmfuloutput_parsers.py:243

安全解析器的"疑罪从有"原则(巧妙处): nemoguard_parse_prompt_safety 里 JSON 解析失败时不是当成安全,而是直接判 unsafe 并标 "JSON parsing failed"(output_parsers.py:167-170)——因为解析失败本身可能就是越狱尝试制造的畸形输出。安全默认值永远偏向"拦下"。

另一类"提取"而非"解析": 对话主线常需要从模型自由文本里抠出"第一段 bot 回复"。get_multiline_response(actions/llm/utils.py:795)做这个:遇到换行后的 user 标记就截断(那是模型幻想出的下一轮),否则一直收非空行直到某行以引号结尾。配套还有 get_first_bot_action(utils.py:867)等一堆游标式提取器,generation.py 在生成 bot 消息后会调用(如 generation.py:983)。


4. 深入实现:framework / provider 抽象

这一节是"llm_call 里那句 model.generate_async 到底打到哪去了"的展开。给要读源码的人。

4.1 两个协议:LLMModel 与 LLMFramework

整个后端建立在两个 Protocol 上(types.py,注意是包根的 nemoguardrails/types.py,不是 llm/types.py):

  • LLMModel(types.py:245):任何模型后端都要实现 generate_async / stream_async(异步生成器)/ model_name / provider_name / provider_urlllm_call 只依赖这个协议,所以核心管线对 provider 完全无感
  • LLMFramework(types.py:286):一个"造 LLMModel 的工厂",要实现 create_model / register_provider / get_provider_names / reset

配套的纯数据类(全是 @dataclass,刻意不依赖 Pydantic 以保持轻量,见文件顶注 types.py:16-27):

类型是什么位置
ChatMessage一条消息(role + content + tool_calls...)types.py:103
LLMResponse一次完整响应(content/reasoning/usage/finish_reason...)types.py:219
LLMResponseChunk流式增量块(delta_content/delta_reasoning...)types.py:232
UsageInfo / ToolCalltoken 用量 / 工具调用types.py:79 / :61

ChatMessage.from_dict(types.py:149)很能容错:接受 roletype、把 bot/human/developer 这类别名映射到规范 role(_ROLE_ALIASES,types.py:92)、兼容嵌套和扁平两种 tool_call 格式、未知键收进 provider_metadata——这让来自 Colang 事件、用户 config、provider 回包的各种 dict 都能落进同一个类型。

4.2 两条路径:OpenAI 兼容(默认) vs LangChain

框架注册表 frameworks/registry.py 靠环境变量 NEMOGUARDRAILS_LLM_FRAMEWORK 选默认框架(registry.py:27,默认 "default")。get_framework(registry.py:61)懒加载:要 langchain 才 import LangChain 适配器,要 default 才建 DefaultFramework——没装 LangChain 的用户也能跑默认路径

NEMOGUARDRAILS_LLM_FRAMEWORK

┌───────────────┴───────────────┐
"default" "langchain"
│ │
DefaultFramework LangChainFramework
create_model() (integrations/langchain/…)
│ │
OpenAIChatModel 包住任意 langchain-<provider>
│ 用于必须走 LangChain 的引擎
OpenAICompatibleClient

HTTP POST /chat/completions

默认框架的模型工厂 DefaultFramework.create_model(frameworks/default.py:104):

  1. 若 provider 是注册过的自定义 provider,直接用它;
  2. 否则按 provider 名解析 base_url(内置 openai/nim/nvidia_ai_endpoints/ollama 的默认 URL,default.py:28)和 API key 环境变量(default.py:35);
  3. Azure 系走一段专门的 kwargs 重塑(_prepare_azure_kwargs,default.py:140)——把 azure_deployment/api_version 拼成部署 URL、用 api-key 头而非 Bearer;
  4. 造(或从池里取)一个 OpenAICompatibleClient,包进 OpenAIChatModel 返回。

解析不到默认 URL 的 provider 会抛错并提示改用 LangChain 框架(default.py:50-55)——两条路径的边界写在错误信息里。

HTTP 客户端按 (base_url, api_key, timeout, ...) 做键缓存复用(_get_or_create_client,default.py:70),aclose() 负责把连接池优雅关掉(default.py:228)。

4.3 OpenAI 兼容客户端:重试、SSE、鉴权

OpenAICompatibleClient(clients/openai_compatible.py:21)很薄:_build_payload(:28)拼 OpenAI 格式请求体(流式时自动加 stream_options.include_usage),chat_completion(:51)和 stream_chat_completion(:62)分别走非流式/流式。

厚重逻辑在基类 BaseClient(clients/base.py:82),用 httpx.AsyncClient:

  • 重试 _apost(base.py:185):对超时、网络错、可重试状态码做带抖动的指数退避;还专门识别 asyncio "stale event loop" 错误并重试(_is_stale_loop_error,base.py:55)——这是跨事件循环复用 httpx 时的经典坑。退避时长优先读 Retry-After 头(_calculate_retry_delay,base.py:166)。
  • 流式 _apost_stream(base.py:242):用 SSEDecoder 逐行解 Server-Sent Events,遇 [DONE] 收尾;已经吐过第一块之后就不再重试(first_yielded 守卫,base.py:303),避免重复内容。
  • 鉴权 _build_headers(base.py:145):默认 Authorization: Bearer <key>,但 custom_headers 可覆盖(大小写不敏感地去重),支持 Azure api-key 等非 Bearer 方案。往明文 HTTP 发 key 会告警(base.py:110)。

OpenAIChatModel(llm/models/openai_chat.py:48)是把这个客户端适配成 LLMModel 协议的薄壳:generate_async(:105)把 ChatMessage 转 OpenAI messages、调 chat_completion、把回包组装成 LLMResponse;stream_async(:120)对应流式。


5. LLM 调用缓存(LFU)

它要解决的小问题: 内容安全这类检查,同样的输入会被反复问同一个模型。重复的网络往返又慢又贵。

思路:按归一化 prompt 做 LFU 缓存。 缓存键由 create_normalized_cache_key(cache/utils.py:54)生成:把 prompt(字符串或 messages 列表)规范化(折叠空白)后取 SHA-256。这样语义等价、只差空白的 prompt 命中同一键。

缓存容器是 LFUCache(cache/lfu.py:80)——Least Frequently Used(最少使用优先淘汰),满了淘汰访问频次最低者,频次相同时淘最久未用者。实现用"频次桶 + 每桶一个双向链表 + key_map"(lfu.py:130 _update_node_freq),get/put 都 O(1),threading.RLock 保线程安全。

并发去重(巧妙处): get_or_compute(lfu.py:339)保证同一个键在并发请求下只计算一次——用一个 _computing future 表,后到的协程 await 前一个的 future 而不是重复打模型。

缓存怎么接进 rail。 以内容安全为例(library/content_safety/actions.py:91-120):渲染完 prompt → 先查缓存,命中就 get_from_cache_and_restore_stats(cache/utils.py:155)把结果连同 token 统计、模型元数据一起恢复回 context(这样即使走缓存,统计和可观测性数据也不丢);未命中才真调 llm_call,再 cache.put 存回(结果 + llm_stats + llm_metadata,即 CacheEntry,cache/utils.py:48)。缓存是 per-model 配的(Model.cache,config.py:132),主要给内容安全模型用。


6. 巧妙之处(可借鉴的技术)

  • 任务名当主键,把"调用点"和"模板"解耦。 代码里一个 Task 枚举值,YAML 里一个 task: 字段,中间靠打分匹配连起来——加模型/换 prompt 完全不动 rail 代码。见 prompts.py:57 _get_prompt
  • 同分取后者 = 天然的覆盖机制。 用户 config 的模板拼在内置之后且 >= 判定,不需要显式"override"语法就能盖掉内置模板。prompts.py:120,164
  • 两条抽取路径兜住 reasoning 模型。 既认结构化 reasoning 字段,也认正文里的 <think> 标记,标记不成对时保守不切。actions/llm/utils.py:347
  • 安全解析器"疑罪从有"。 解析失败即判 unsafe,而不是放行——把"畸形输出即攻击"的直觉编码进默认值。output_parsers.py:167
  • 流式已吐首块后不重试。first_yielded 守卫避免重试造成内容重复,是流式 HTTP 客户端常被忽视的正确性细节。clients/base.py:303
  • 缓存命中也恢复统计。 走缓存不代表可观测性数据消失——token/模型元数据被显式还原回 context,tracing 依旧完整。cache/utils.py:103,155

7. 边界与局限(诚实)

  • 默认框架只覆盖 OpenAI 兼容端点。 provider 若不 OpenAI 兼容(需要 LangChain 的引擎),必须设 NEMOGUARDRAILS_LLM_FRAMEWORK=langchain 并装对应 langchain-<provider>;默认路径解析不到 base_url 会直接抛错(frameworks/default.py:50)。
  • prompt 打分是启发式,不是精确路由。 子串命中给 0.4 这类规则可能对某些模型名产生意外匹配;选不到就 ValueError,没有"最相似模板"回退。
  • Colang 1.0/2.x 的历史嗅探是启发式。 get_colang_history 靠"事件里出现 2.x 内部事件"来判版本(actions/llm/utils.py:488),注释自承"想个更稳健的办法?"。
  • 多行提取器是纯文本启发式。 get_multiline_response 等靠"引号结尾""\nuser 标记"切分模型输出(utils.py:795),模型不按套路输出时可能截错。
  • max_length 截断是按字符数、从最旧事件删起。 不是 token 精确计数;多模态图片按占位符估长(taskmanager.py:242),极端情况下仍可能与真实 token 预算有偏差。
  • LFU 缓存主要服务内容安全类模型,不是对话主线的通用缓存;键只基于 prompt 文本,采样温度等参数不进键。

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

主题文件路径符号名
任务枚举nemoguardrails/llm/types.pyTask
Prompt 渲染 / 输出解析入口nemoguardrails/llm/taskmanager.pyLLMTaskManager, render_task_prompt, parse_task_output, _render_string, _get_general_instructions
按模型选模板nemoguardrails/llm/prompts.pyget_prompt, _get_prompt, _load_prompts, get_task_model
模板结构nemoguardrails/rails/llm/config.pyTaskPrompt, MessageTemplate, Model, Instruction
模板库(按模型分文件)nemoguardrails/llm/prompts/*.ymlgeneral.yml, openai.yml, openai-chatgpt.yml, llama3.yml, deepseek.yml
Jinja 过滤器nemoguardrails/llm/filters.pycolang, verbose_v1, to_messages, first_turns, last_turns
统一调用入口nemoguardrails/actions/llm/utils.pyllm_call, _stream_llm_call, get_colang_history, get_multiline_response, warn_if_truncated
输出解析器nemoguardrails/llm/output_parsers.pyverbose_v1_parser, is_content_safe, nemoguard_parse_prompt_safety, nemotron_reasoning_parse_prompt_safety
调用协议与返回类型nemoguardrails/types.pyLLMModel, LLMFramework, LLMResponse, LLMResponseChunk, ChatMessage, UsageInfo
框架注册表nemoguardrails/llm/frameworks/registry.pyget_framework, register_framework, set_default_framework
默认(OpenAI 兼容)框架nemoguardrails/llm/frameworks/default.pyDefaultFramework, create_model, _resolve_base_url, _prepare_azure_kwargs
HTTP 客户端(重试/SSE/鉴权)nemoguardrails/llm/clients/base.pyBaseClient, _apost, _apost_stream, _build_headers, _calculate_retry_delay
OpenAI 兼容客户端nemoguardrails/llm/clients/openai_compatible.pyOpenAICompatibleClient, chat_completion, stream_chat_completion
OpenAI 模型适配器nemoguardrails/llm/models/openai_chat.pyOpenAIChatModel, generate_async, stream_async
LFU 缓存nemoguardrails/llm/cache/lfu.pyLFUCache, get, put, _evict_lfu, get_or_compute
缓存键与统计恢复nemoguardrails/llm/cache/utils.pycreate_normalized_cache_key, get_from_cache_and_restore_stats, restore_llm_stats_from_cache

相邻章节: index · 01 配置与五类护栏 · 02 Colang DSL · 03 事件运行时与对话生成 · 04 输入/输出护栏与内置库 · 06 IORails 引擎、服务端与可观测性