Liquid 模板引擎在 AI Agent 中的深度集成
Liquid 模板引擎在 AI Agent 中的深度集成
学习目标
读完本章后,你将能够:
- 理解 Liquid 模板从视图层扩展到配置层的设计动机
- 掌握自定义过滤器、标签和块的注册方法
- 设计上下文栈系统支持动态插值
- 识别模板注入攻击的防御策略
核心概念
为什么在 Agent 系统中引入模板引擎
AI Agent 系统通常面临一个问题:如何让用户在不编写代码的情况下,灵活定义 Agent 的行为参数?
Liquid 模板引擎的优势:
- 安全沙箱:不能执行任意代码,只能访问显式暴露的变量和方法
- 用户友好:
{{ variable }}和{% if condition %}语法直观 - 可扩展:支持自定义过滤器、标签和块
- 纯字符串操作:输出总是字符串,不会意外执行副作用
三层扩展模式
将模板引擎引入 Agent 系统需要三个层次的扩展:
上下文系统设计
插值上下文栈
模板引擎的上下文不是一个扁平的字典,而是一个栈结构,支持动态推送和弹出作用域:
关键设计决策:
- Agent 始终可用:
_agent_变量在所有上下文中都指向当前 Agent,可在模板中引用{{ _agent_.name }} - 事件驱动作用域:
interpolate_with(event)将事件 payload 推入环境栈顶部,使{{ field_name }}直接访问事件字段 - 批量迭代:
interpolate_with_each(array)遍历事件数组,每次迭代自动设置current_event - 共享状态:通过 KeyValueStoreAgent 注入的变量在所有模板中可用
递归插值
当 Agent 的 options 是嵌套的 Hash/Array 结构时,interpolate_options 递归遍历所有层级,对字符串值应用模板解析:
原始 options:{ "url": "https://api.example.com/{{ city }}/weather", "headers": { "Authorization": "Bearer {{ api_key }}" }, "extract": ["title", "body"]}
插值后 (假设 city=Tokyo, api_key=abc123):{ "url": "https://api.example.com/Tokyo/weather", "headers": { "Authorization": "Bearer abc123" }, "extract": ["title", "body"]}自定义扩展
自定义过滤器
# 注册到 Liquid::Environmentmodule Filters def uri_escape(string) CGI.escape(string) rescue StandardError string end
def hmac_sha256(input, key) OpenSSL::HMAC.hexdigest('sha256', key.to_s, input.to_s) end
def as_object(object) # 通过 throw/catch 返回非字符串对象 throw :as_object, object.as_json endend
Liquid::Environment.default.register_filter(Filters)as_object 的特殊处理:
Liquid 默认总是返回字符串。as_object 过滤器通过 throw :as_object 跳出模板渲染,将原始 Ruby 对象(如 Hash/Array)返回给调用者。这是必须作为最后一个过滤器的原因——后续的字符串操作会破坏对象结构。
自定义标签
class Credential < Liquid::Tag def render(context) # 通过 context.registers[:agent] 访问当前 Agent context.registers[:agent].credential(@credential_name) || "" endend
Liquid::Environment.default.register_tag('credential', Credential)自定义块
正则替换块是典型的自定义块实现,包含两个子块(in 和 with):
{% regex_replace "\w+" in %}Use me like this.{% with %}{{ match | capitalize }}{% endregex_replace %}实现要点:
parse方法分别解析in块和with块的主体render方法对in块渲染结果应用正则匹配- 每次匹配时将
match变量推入上下文栈,渲染with块 - 支持命名捕获组自动暴露为变量
安全考量
模板注入防御
Liquid 模板天然安全,但仍需注意:
| 风险 | 防御策略 |
|---|---|
| 访问不应该的数据 | 只通过 to_liquid 暴露安全的属性和方法 |
| 无限循环 | Liquid 的循环次数限制 |
| 拒绝服务(复杂正则) | regex_replace 在 Agent 端执行,受 Agent 超时限制 |
| 凭据泄露 | credential 标签绑定到当前用户的凭据,无法跨用户访问 |
验证前置
模板错误应在保存时捕获,而非运行时:
validate :validate_interpolation
def validate_interpolation interpolated # 尝试插值,抛出 Liquid::Error 则添加验证错误rescue Liquid::Error => e errors.add(:options, "has an error with Liquid templating: #{e.message}")end设计取舍
Liquid vs 其他模板引擎
| 维度 | Liquid | ERB | Jinja2 | 自定义 DSL |
|---|---|---|---|---|
| 安全性 | 沙箱隔离 | 可执行任意 Ruby | 沙箱(需配置) | 取决于实现 |
| 扩展性 | 过滤器/标签/块 | Ruby 方法 | 过滤器/测试 | 完全自定义 |
| 用户学习成本 | 低 | 中 | 中 | 高 |
| 性能 | 中 | 高 | 高 | 取决于实现 |
| 生态 | Shopify 社区 | Ruby 社区 | Python 社区 | 无 |
选择 Liquid 的原因:安全沙箱是核心需求——Agent 的配置由用户定义,不能允许执行任意代码。
模板引擎 vs 代码编辑
| 维度 | 模板引擎 | 代码编辑(JavaScriptAgent) |
|---|---|---|
| 安全边界 | 沙箱限制 | 完整语言访问权限 |
| 灵活性 | 有限(字符串操作为主) | 完全(任意逻辑) |
| 适用场景 | 配置参数、简单转换 | 复杂业务逻辑 |
最佳实践:模板引擎处理 80% 的配置需求,代码编辑处理剩余 20% 的复杂场景。
参考来源
- Liquid Template Language — Shopify 开源的安全模板引擎
- RFC 3986 — URI 编码标准,
uri_escape过滤器的实现依据 - Shopify/liquid GitHub — Liquid 的源码与扩展文档