本文档概述了 jinja.cpp 的内部架构和设计决策。
引擎遵循标准的 编译器/解释器 (Compiler/Interpreter) 管道流程:
-
词法分析器 (Lexer -
Lexer类):- 扫描输入字符串。
- tokenize Jinja 定界符
{{ ... }},{% ... %},{# ... #}。 - 处理空白控制修饰符 (
-, 例如{{-),通过跟踪状态并剥离文本 token 的前导/尾随空白。 - 生成一个扁平的
Token列表。
-
解析器 (Parser -
Parser类):- 递归下降解析器 (Recursive Descent Parser)。
- 将有效的 token 转换为抽象语法树 (AST)。
- 处理表达式的运算符优先级。
- 支持:
- 二元运算符 (
+,-,*,/,%,==,!=,<,>,<=,>=,and,or,in,not in,~)。 - 一元运算符 (
not,-)。 - 字面量 (字符串, 数字, 布尔值, 数组, 对象)。
- 变量和属性访问 (
foo.bar,foo['bar'])。 - 函数调用和过滤器 (
foo | filter)。 - 控制结构 (
for,if,set,macro).
- 二元运算符 (
-
抽象语法树 (AST -
Node层次结构):- 基类
Node具有虚函数render(Context&, string& out)。 - 节点类型:
TextNode,PrintNode,ForStmt,IfNode,SetNode,MacroNode。 - 表达式 (
Expr层次结构) 计算结果为nlohmann::json值。
- 基类
-
解释器 / 渲染器 (Interpreter / Renderer -
Template::render):- 遍历根节点并调用
render。 - 管理
Context(作用域, 变量)。
- 遍历根节点并调用
tojson(indent=None): 将变量序列化为 JSON 字符串。支持缩进。safe: 将字符串标记为安全 (在此实现中为无操作,因为默认不强制 HTML 转义,但为了兼容性而支持)。注意:通过透传隐式支持。string: 将值转换为其字符串表示形式。length: 返回列表、字符串或对象的大小。trim: 移除字符串首尾的空白字符。items: 返回字典的[key, value]对列表 (用于遍历对象)。capitalize: 将字符串的首字母大写,其余小写。lower: 将字符串转换为小写。upper: 将字符串转换为大写。map(attribute=name): 从列表中的每个元素提取特定属性 (例如users | map(attribute='name'))。
range([start], stop, [step]): 生成整数序列。namespace(...): 创建一个可变对象,用于在循环内部更新变量 (例如set ns.i = ns.i + 1)。strftime_now(format): 返回按给定字符串格式化的当前时间。
defined: 检查变量是否存在。undefined: 检查变量是否未定义。none: 检查变量是否为 null。boolean: 检查变量是否为布尔值。string: 检查变量是否为字符串。number: 检查变量是否为数字。sequence/iterable: 检查变量是否为列表或字符串。mapping: 检查变量是否为对象/字典。true/false: 检查布尔值。
我们使用 nlohmann::json 作为所有变量的统一数据类型。这简化了类型检查,并允许与基于 JSON 的 LLM API 轻松集成。
- 过滤器 (Filters): 在
FilterExpr中实现。标准的 Jinja2 过滤器如safe,tojson,trim,lower是硬编码的。 - 函数 (Functions):
CallExpr处理全局函数 (range,namespace) 和用户注册的函数。 - 用户钩子 (User Hooks):
Template::add_function允许用户将 C++ lambda 绑定到 Jinja 函数调用。
对 JSON 序列化的严格控制对于对话模板 (例如 Tool 定义) 至关重要。
我们在 src/jinja.cpp 中实现了一个自定义的递归序列化器 to_json_string,它:
- 支持与 Python 通用输出匹配的缩进。
- 针对键进行排序:按照特定顺序 (
type->function->name-> ...) 对键进行排序,以匹配常见的 LLM 训练数据格式,确保高度一致性。
Lexer 部分模拟了 Jinja2 的 lstrip_blocks 和 trim_blocks 行为。手动实现的空白剥离逻辑 (trim_prev, trim_next) 确保生成的 prompt 不含多余的换行符,这可能会影响 LLM 的性能。
为了支持广泛的部署环境:
- 结构化绑定 (Structured bindings) 被标准迭代器取代。
- 为 C++11 使用了
std::make_uniquepolyfill。
- 真实数据: 我们使用
tests/test_chat_template.json,该文件是从官方 Pythontransformers库针对通常支持的模型生成的。 - 模糊匹配: 对于动态内容 (如日期),测试使用正则归一化来确跨时间和环境的通过一致性。
我们在测试套件中自动验证了以下模型:
- Qwen:
Qwen2.5-3B-Instruct,Qwen2.5-VL-3B-Instruct,Qwen2.5-Omni-3B,Qwen2.5-7B-Instruct-1M,Qwen2.5-Math-7B-Instruct,QwQ-32B - Qwen3:
Qwen3-4B,Qwen3-4B-Instruct,Qwen3-4B-Thinking,Qwen3-VL-4B-Instruct,Qwen3-VL-4B-Thinking,Qwen3Guard-Gen-4B,Qwen3-Coder-30B-A3B-Instruct,Qwen3-Omni-30B-A3B-Instruct,Qwen3-Omni-30B-A3B-Thinking - DeepSeek:
DeepSeek-R1-Distill-Qwen-7B,DeepSeek-V3.2,DeepSeek-R1 - GLM:
ZhipuAI/GLM-4.5V,ZhipuAI/GLM-4.6V - Yi:
01ai/Yi-VL-6B,01ai/Yi-1.5-6B-Chat - SmolLM:
HuggingFaceTB/SmolLM-135M-Instruct,HuggingFaceTB/SmolVLM-256M-Instruct,HuggingFaceTB/SmolLM2-135M-Instruct,HuggingFaceTB/SmolLM3-3B - Gemma:
google/gemma-3-4b-it,google/gemma-3n-E4B-it - Mistral:
mistralai/Ministral-3-3B-Instruct-2512 - Llama:
llama-2-7b,Meta-Llama-3-8B-Instruct,Llama-3.2-3B-Instruct - Phi:
Phi-3.5-mini-instruct,Phi-3.5-vision-instruct,phi-4,Phi-4-mini-reasoning - MobileLLM:
LLM-Research/MobileLLM-125M