跳到主要内容

Rule Engine:用 AND/OR 条件把 trace 属性映射到上下文/Prompt/评估配置

30 秒导读: OpenLIT 的规则引擎是一个「运行时查表器」。你给它一组 trace 属性(比如"这条调用用的是 openai + gpt-4"),它按你事先配好的 AND/OR 条件组去匹配规则,命中后告诉你"该给这条调用挂哪份 context、哪个 Prompt、哪套评估配置"。它是把评估和 Prompt/context 粘起来的动态层——评估要的"上下文即真相",很多就是它挑出来的。

本章是 OpenLIT 讲解系列的一环,同组还有 index01 一行 init02 span/metric03 落库04 评估05 护栏


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

  • 一句话定义: 规则引擎 = 一张「IF 条件 THEN 挂谁」的动态映射表。条件写在 trace 属性上,结果指向一个实体(context / prompt / evaluation)。
  • 解决什么问题: 你的评估器需要"参考答案 / 领域知识"当上下文,但不同调用需要不同的上下文。硬编码"gpt-4 用这份、claude 用那份"会写死在代码里。规则引擎把这套 IF-THEN 挪到数据里,运行时按 trace 属性动态查。
  • 给谁用: 做 LLM 可观测 + 评估的工程师。埋点已经把 gen_ai.systemgen_ai.request.modeldeployment.environment 这些属性打进了 trace(见 02),规则引擎直接拿它们当匹配输入。
  • 一句话直觉: 像 Web 框架的路由表——URL 进来匹配路由,命中就分派到对应 handler。这里是 trace 属性进来匹配规则,命中就分派到对应的 context/prompt/评估配置

用起来什么样。 Python SDK 一次调用就够(sdk/python/src/openlit/__init__.py:601 evaluate_rule):

import openlit

# 示意,非源码:把当前调用的 trace 属性丢给规则引擎,问"该挂哪份 context"
res = openlit.evaluate_rule(
entity_type="context", # 想要哪类实体:context / prompt / evaluation
fields={ # 拿来匹配的 trace 属性
"gen_ai.system": "openai",
"gen_ai.request.model": "gpt-4",
},
include_entity_data=True, # 顺便把命中的 context 正文也取回来
)
# res = { "matchingRuleIds": [...], "entities": [...], "entity_data": {...} }

返回三样东西:命中了哪些规则(matchingRuleIds)、命中规则挂着哪些实体(entities)、以及(可选)实体的完整数据(entity_data,比如 context 正文、编译好的 prompt)。


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

规则引擎横跨三处:SDK 发起Next.js 路由校验ClickHouse 一条 SQL 算命中。核心是"匹配全在一条 SQL 里做完,应用层不做逐条比对"。

怎么读下图:从左到右是一次 evaluate_rule 的生命周期,方框是部件,方框下小字是文件。

Python SDK Next.js 路由 匹配核心(一条 SQL) 实体解析
┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ ┌──────────────┐
│ evaluate_rule│ POST │ /api/rule- │ │ input_values(把fields │ │ context →查 │
│ fields={...} │ ──────▶ │ engine/ │ ──────▶ │ 变成临时行) │──▶│ contexts表 │
│ entity_type │ JSON │ evaluate │ 校验后 │ condition_matches │ │ prompt →编译 │
└──────────────┘ │ (鉴权+限长) │ 调用 │ group_matches(AND/OR) │ │ prompt │
__init__.py:601 └──────────────┘ │ rule_matches(AND/OR) │ └──────────────┘
evaluate/route.ts │ →join rule_entities │ evaluate.ts:142
└───────────────────────┘ getCompiledPrompt
evaluateRules()
evaluate.ts:14

部件一句话职责:

部件干什么在哪个文件
SDK evaluate_rule组 payload、POST 到 /api/rule-engine/evaluatesdk/python/src/openlit/__init__.py:601
API 路由 POST鉴权(Bearer/session)、校验 entity_type、给 fields 限长限量、防注入src/client/src/app/api/rule-engine/evaluate/route.ts:10
evaluateRules把 fields 拼成 SQL,一条查询算完所有命中,再解析实体数据src/client/src/lib/platform/rule-engine/evaluate.ts:14
4 张规则表存规则 / 条件组 / 条件 / 实体挂载create-rule-engine-migration.ts:15-59
实体 CRUD规则、条件组、实体的增删改查src/client/src/lib/platform/rule-engine/index.ts
评估侧消费评估时按 trace 取 context、把规则和评估配置双向同步rule-engine-context.tssync-rule-entities.ts

主线走一遍(高层): SDK 把 {fields, entity_type} POST 上来 → 路由校验并拿到 databaseConfigIdevaluateRules 把 fields 变成一张临时表,和条件表 JOIN,逐条件算真假,再按组的 AND/OR、规则的 AND/OR 汇总出"哪些规则整体命中" → JOIN 实体表,筛出你要的 entity_type → 命中实体若 include_entity_data 为真,再回表取 context 正文或编译 prompt → 返回。


3. 核心原理

3.1 规则的数据模型:规则 → 条件组 → 条件 → 实体

规则引擎用四张 ClickHouse 表表达一条规则,层层嵌套。理解这四张表的关系,后面的 SQL 就都通了。

建表在 src/client/src/clickhouse/migrations/create-rule-engine-migration.ts:15-59。四张表按包含关系:

存什么关键列定义位置
openlit_rules一条规则的元信息group_operator(组间 AND/OR)、status(ACTIVE/INACTIVE)migration:15
openlit_rule_condition_groups规则下的条件组rule_idcondition_operator(组内 AND/OR)migration:28
openlit_rule_conditions组里的单个条件fieldoperatorvaluedata_typemigration:37
openlit_rule_entities规则命中后挂哪些实体rule_identity_typeentity_idmigration:50

表名常量集中在 src/client/src/lib/platform/rule-engine/table-details.ts:1-4

两级 AND/OR 是关键设计。 一条规则 = 若干条件组,组之间用 group_operator 连;每组 = 若干条件,组内用 condition_operator 连。这就能表达"(A 且 B)或(C)"这种嵌套逻辑:

规则 R (group_operator = OR)
├── 组 G1 (condition_operator = AND)
│ ├── 条件: gen_ai.system equals openai
│ └── 条件: gen_ai.request.model equals gpt-4
└── 组 G2 (condition_operator = AND)
└── 条件: deployment.environment equals prod
命中含义: (system=openai 且 model=gpt-4) 或 (env=prod)
命中后 → 挂在 R 上的所有 entity(某份 context / 某个 prompt / 某套评估配置)生效

一条规则可以挂多个实体、多种 entity_type;evaluate_rule 每次只问一种 entity_type,返回时按它筛。

3.2 SDK 入口:evaluate_rule 只是一个薄 HTTP 封装

SDK 侧没有任何匹配逻辑,纯粹组包发请求。真正的活在服务端。

sdk/python/src/openlit/__init__.py:601 evaluate_rule:它从参数或环境变量拿 OPENLIT_URLOPENLIT_API_KEY,拼出 endpoint = url + "/api/rule-engine/evaluate",组一个 payload,POST 上去:

# 真实源码节选(__init__.py:643-651 附近),payload 组装
payload = {
"entity_type": entity_type,
"fields": fields,
"include_entity_data": include_entity_data,
"entity_inputs": entity_inputs,
"source": "python-sdk",
}
payload = {k: v for k, v in payload.items() if v is not None} # 去掉 None

鉴权走 Authorization: Bearer <api_key>,超时 120s,失败返回 None要点: entity_inputs 是给 prompt 类实体用的——里面能带 variables/version/shouldCompile,让服务端顺便把 prompt 编译好再返回(见 3.5)。

3.3 服务端匹配核心:一条 ClickHouse SQL 算完全部命中

这是全章工程含量最高的地方。匹配不在 JS 里逐条比对,而是把 fields 拼成一张临时表,交给一条 CTE 链式 SQL 一次算完。入口 src/client/src/lib/platform/rule-engine/evaluate.ts:14 evaluateRules

SQL 分四级 CTE,自底向上汇总。怎么读:每一层把下层的布尔结果往上聚合一次。

input_values 把 fields 的每个 k=v 变成临时表的一行 (field_name, field_value_str)
│ JOIN 条件表(按 field 名对上)

condition_matches 逐条件算真假 → condition_match ∈ {0,1} [multiIf 运算符矩阵]
│ 按 group_id 聚合

group_matches 组内 AND=min / OR=max → group_match ∈ {0,1}
│ 按 rule_id 聚合(且只算 status='ACTIVE' 的规则)

rule_matches 组间 AND=min / OR=max → rule_match ∈ {0,1}
│ JOIN 实体表,筛 entity_type

最终结果 命中规则挂着的 (rule_id, entity_type, entity_id)

第一级 input_values:把 fields 变成临时行。SELECT ... UNION ALL 把每个 k=v 拼成一行(evaluate.ts:25-37)。ClickHouse 里 UNION ALL 造临时表比参数化数组更可靠。

第二级 condition_matches:运算符矩阵。 核心是一个巨大的 multiIf(evaluate.ts:43-64),按 data_type + operator 组合选出对应的 ClickHouse 表达式。这张矩阵是引擎的"指令集":

data_typeoperator底层 ClickHouse 表达式
stringequals / not_equals= / !=
stringcontains / not_containsposition(v, c) > 0 / = 0
stringstarts_with / ends_withstartsWith / endsWith
stringregexmatch(v, c)
stringin / not_inhas(splitByChar(',', c), v) / NOT has(...)
numberequals…ltetoFloat64OrZero(v)= > >= < <=
numberbetween>= 下界 AND <= 上界(splitByChar(',', c)[1]/[2])
booleanequals字符串直接比
其它未匹配0(视为不命中)

number 类一律走 toFloat64OrZero 转换(evaluate.ts:53),解析失败当 0,不抛错。between 的上下界是把 value 用逗号切开取第 1、2 段(ClickHouse 数组下标从 1 开始)。

第三级 group_matches:组内 AND=min / OR=max。 一个巧妙简化——布尔值只有 0/1 时,AND 等价于 min(全 1 才 1),OR 等价于 max(有 1 就 1)。见 evaluate.ts:72-75:

-- 真实源码节选 evaluate.ts:72-75
CASE
WHEN g.condition_operator = 'AND' THEN toUInt8(COALESCE(min(cm.condition_match), 0))
ELSE toUInt8(COALESCE(max(cm.condition_match), 0))
END AS group_match

第四级 rule_matches:组间同样 AND=min / OR=max,且只算 ACTIVE 规则。evaluate.ts:83-89,WHERE r.status = 'ACTIVE' 把停用规则挡在外面。

收尾 SELECT:JOIN 实体表并筛 entity_type。 evaluate.ts:92-100——只取 rule_match = 1re.entity_type = '<你要的类型>' 的实体行,返回 (rule_id, entity_type, entity_id)

3.4 注入防护:三道闸

匹配 SQL 是字符串拼出来的(ClickHouse 客户端不支持完整参数化),所以防注入是硬要求。代码布了三道闸:

干什么位置
API 层限长限量fields 只收 string/number/boolean;单键 ≤100 字符、单值 ≤1000、总数 ≤50evaluate/route.ts:69-102
sqlString.escapeinput_values 时把 fields 的键值转义(反斜杠、单引号、null 字节全处理),再 .slice(1,-1) 去掉外层引号自己补evaluate.ts:25-30
Sanitizer实体二次查询(context id)用 Sanitizer.sanitizeValue;CRUD 写入用 Sanitizer.sanitizeObjectevaluate.ts:143rule-engine/index.ts:86

API 层还有一条防御:出错时只回 "Failed to evaluate rules",绝不把 ClickHouse 原始报错透给调用方(evaluate/route.ts:119-120),避免泄露内部结构。

3.5 实体解析:命中之后取回真数据

默认只返回 (rule_id, entity_type, entity_id)。当 include_entity_data=true,evaluateRules 再按实体类型分别回表取数据(evaluate.ts:120-165):

entity_type怎么解析位置
contextSELECT * FROM openlit_contexts WHERE id = '<sanitized>',取正文/元数据evaluate.ts:142-151
promptgetCompiledPromptByDbConfig,按 entity_inputsversion/variables/shouldCompile 编译好返回evaluate.ts:152-159
evaluation不在这里解析——evaluate.ts 只有 context/prompt 两个分支,evaluation 实体只回 id,由评估侧另行消费(见 3.6)

解析用 Promise.all 并发、用 seen 去重(同一实体只取一次),任一实体取失败就把它的值置 null 而非整体失败(evaluate.ts:161-163)。这是"部分降级"设计:一份 context 挂了不拖垮其它命中。

3.6 评估侧如何消费规则产出

规则引擎的价值在于被评估用。两条粘合线:

线一:评估时按 trace 取 context。 src/client/src/lib/platform/evaluation/rule-engine-context.ts 是评估和规则引擎的桥。它先把一条 trace 的属性抽成规则 fields——extractRuleEngineFieldsFromTrace(rule-engine-context.ts:45)按 TRACE_TO_RULE_FIELDS(rule-engine-context.ts:13)把 SpanAttributes['gen_ai.system'] 这类 OTel 属性映射成规则字段名。这张映射表必须和 api/rule-engine/field-values/route.tsFIELD_COLUMN_MAP 对齐,条件构造器里能选的字段才和运行时匹配的字段一致。

然后 getContextFromRuleEngineForTrace(rule-engine-context.ts:74)调 evaluateRules({entity_type:"context", include_entity_data:true}),把命中的 context 正文拼起来,交给评估当"上下文即真相"(04)。还支持按优先级排序取多条 context(getContextFromRulesWithPriority,rule-engine-context.ts:129,priority 降序)。调用点在 evaluation/index.ts:181:233:455,以及"改进建议"链路 chat/improvement.ts:768

线二:evaluation 实体与评估配置双向同步。 evaluation 类实体不走 entity_data 解析(3.5),而是靠 src/client/src/lib/platform/evaluation/sync-rule-entities.ts 在两处存储间对账:

  • 规则挂载存在 ClickHouse 的 openlit_rule_entities 表;
  • 评估类型配置(哪条规则、什么优先级)存在 Prisma 的 evaluationConfigs.meta JSON 里。

syncRuleEntitiesFromConfig(sync-rule-entities.ts:76)算出"配置里想要的挂载集合" desired,再把 ClickHouse 里多余的删掉、缺的补上(sync-rule-entities.ts:94-119)。addRuleToEvaluationType / removeRuleFromEvaluationType(sync-rule-entities.ts:25/:52)则处理从规则详情页反向改评估配置。两条路径保证"从规则页改"和"从评估页改"最终一致。


4. 巧妙之处(可借鉴)

  • AND/OR 退化成 min/max。 布尔值 0/1 下,AND=min、OR=max,一行 CASE 就把两级逻辑汇总干净,无需在 JS 里递归求值(evaluate.ts:72-75:83-85)。
  • 匹配下推到数据库。 不把规则拉到应用层逐条比对,而是把输入 fields 拼成临时表和条件表 JOIN,一条 SQL 算完所有规则的命中——规则再多也是一次查询(evaluate.ts:33-101)。
  • 拼 SQL 也能防注入。 ClickHouse 客户端无完整参数化,于是 sqlString.escape + .slice(1,-1) 自管引号 + API 层限长限量 + 出错吞细节,四招叠起来(evaluate.ts:25-30evaluate/route.ts:69-120)。
  • 部分降级。 实体解析 Promise.all 并发、单个失败置 null 不拖垮整体(evaluate.ts:161-163);评估侧取 context 出错则返回空数组而非抛错(rule-engine-context.ts:115-117)。
  • 两存储对账。 规则挂载(ClickHouse)和评估配置(Prisma)分家,用 desired 集合做删补对账保持一致(sync-rule-entities.ts:87-119)。

5. 边界与局限(诚实)

  • fields 硬上限。 单键 ≤100、单值 ≤1000 字符、总数 ≤50;超限的键值被静默丢弃而非报错(evaluate/route.ts:69-102)。
  • 可匹配字段是白名单。 条件构造器只认 FIELD_COLUMN_MAP 里那几个字段(ServiceName、SpanName、gen_ai.systemgen_ai.request.modeldeployment.environment 等,field-values/route.ts:8),自定义任意 SpanAttribute 不在其中。
  • evaluate.ts 只解析 context/prompt。 evaluation 实体不返回 entity_data,需走同步机制间接消费(3.6)。类型里 dataset/meta_config 是注释掉的占位(types/rule-engine.tsevaluate/route.ts:48),尚未启用。
  • number 转换不报错。 toFloat64OrZero 把非数字当 0(evaluate.ts:53),脏数据可能悄悄命中数值条件。
  • 无组合爆炸保护之外的复杂度控制。 规则/条件数量本身没有软上限,匹配开销随条件表规模线性增长。

6. 收尾:护栏(05)× 评估(04)× 规则引擎(06)是怎么串起来的

这三章是 OpenLIT 评估侧的三件套,各管一段,别混淆:

干什么什么时机对流量的作用
护栏(05)输入/输出安全检查请求前 preflight / 响应后 postflight同步阻断——不合规就拦
规则引擎(06)按 trace 属性挑该挂哪份 context/prompt/评估配置运行时查表动态选料——决定用什么上下文
评估(04)LLM-as-a-Judge 给质量打分事后(auto/manual)打分——不拦流量,产出分数

怎么读下图:一条真实调用从左往右流过三层,规则引擎在中间把"该用什么上下文"喂给评估。

一次 LLM 调用


┌───────────┐ 不合规 → 拦
│ 护栏 (05) │ preflight / postflight
│ 同步阻断 │ → 05-guardrails.md
└─────┬─────┘
│ 放行,埋点落库(trace 属性:gen_ai.system / model / env …)

┌───────────────────────────┐
│ 规则引擎 (06) 本章 │ IF trace 属性匹配 AND/OR 条件组
│ 按属性查表,动态选上下文 │ THEN 返回 context / prompt / 评估配置
└─────┬──────────────────────┘
│ 把命中的 context 正文当"上下文即真相"喂下去

┌───────────┐
│ 评估 (04) │ LLM-as-a-Judge 打分(不拦流量)
│ 事后打分 │ → 04-evaluations.md
└───────────┘

一句话记住三者关系:护栏是同步阻断,规则是动态选上下文,评估是事后打分;规则引擎是把可观测数据(trace 属性)反哺给评估与 Prompt 的那根粘合剂。


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

主题文件路径符号名
SDK 入口sdk/python/src/openlit/__init__.py:601evaluate_rule
API 路由(鉴权+校验+防注入)src/client/src/app/api/rule-engine/evaluate/route.ts:10POST
匹配核心(CTE 链式 SQL)src/client/src/lib/platform/rule-engine/evaluate.ts:14evaluateRules
运算符矩阵src/client/src/lib/platform/rule-engine/evaluate.ts:43-64condition_matches / multiIf
组内/组间 AND=min·OR=maxsrc/client/src/lib/platform/rule-engine/evaluate.ts:72-89group_matches / rule_matches
实体解析(context/prompt)src/client/src/lib/platform/rule-engine/evaluate.ts:120-165include_entity_data 分支
4 张规则表建表src/client/src/clickhouse/migrations/create-rule-engine-migration.ts:15-59CreateRuleEngineMigration
表名常量src/client/src/lib/platform/rule-engine/table-details.ts:1-4OPENLIT_RULES_TABLE_NAME
规则/条件/实体 CRUDsrc/client/src/lib/platform/rule-engine/index.tscreateRule / addRuleEntity / getRuleEntities
trace→规则字段映射 + 取 contextsrc/client/src/lib/platform/evaluation/rule-engine-context.ts:13,45,74TRACE_TO_RULE_FIELDS / extractRuleEngineFieldsFromTrace / getContextFromRuleEngineForTrace
优先级排序取 contextsrc/client/src/lib/platform/evaluation/rule-engine-context.ts:129getContextFromRulesWithPriority
规则↔评估配置双向同步src/client/src/lib/platform/evaluation/sync-rule-entities.ts:76syncRuleEntitiesFromConfig
可匹配字段白名单src/client/src/app/api/rule-engine/field-values/route.ts:8FIELD_COLUMN_MAP