复杂度信号
概览
complexity 通过与已配置示例集比较,估计请求是 easy、medium 还是 hard。它独立于主题:同一领域内的两个请求仍可能需要不同模型档。
主要优势
- 将估计难度与主题分类分开。
- 同一 easy/medium/hard 策略可被多条决策复用。
- 用示例调路由,而不需要自定义分类器 schema。
解决什么问题?
仅靠领域路由无法区分简短事实请求与多步分析请求。Complexity 提供可复用的难度信号,使决策只升级真正需要的流量。
何时使用
当模型成本或推理模式应随估计任务难度变化时,使用 complexity。不要把它当作正确性或安全保证;那些问题请用领域特定评估与安全信号。
配置
routing:
signals:
complexity:
- name: needs_reasoning
threshold: 0.10
description: Escalate multi-step reasoning or synthesis-heavy prompts.
hard:
candidates:
- solve this step by step
- compare multiple tradeoffs
- analyze the root cause
easy:
candidates:
- answer briefly
- quick summary
- simple rewrite
threshold 是裕量,不是相似度截止。Router 分别对 hard 与 easy 候选库打分,再比较两者:
signal = hard_bank_score - easy_bank_score
signal > threshold -> hard
signal < -threshold -> easy
otherwise -> medium
因为两个库可能给出相近的基线分数,相减得到的裕量可能远小于任一单独分数。在一次使用上述候选库的校准中,八条提示上观察到的最大绝对裕量是 0.197;阈值为 0.75 时没有一条进入 hard 或 easy 档。
把 0.10 当作起点,而不是通用默认。在代表性流量上测量裕量,再为已配置的候选库与嵌入模型调阈值。
规则会发出带后缀的名称。决策必须引用 <rule>:easy、<rule>:medium 或 <rule>:hard:
routing:
decisions:
- name: escalate-hard-prompts
description: Route hard prompts to the reasoning model.
priority: 150
rules:
operator: AND
conditions:
- type: complexity
name: needs_reasoning:hard
modelRefs:
- model: reasoning-model
use_reasoning: true
如需可选的原型库调参,在族级模块上配置一次:
global:
model_catalog:
modules:
complexity:
prototype_scoring:
enabled: true
max_prototypes: 8
top_m: 2
复杂度规则也可在 hard 和 easy 旁声明 prototype_scoring。
省略时继承上述族级配置。声明的对象会完整覆盖:未填写的字段,包括空对象 {}
中的所有字段,使用内置默认值,不继承族级覆盖值。这样,规则的原始配置会随配方
导出或初始化一同保留。
如需保留两个候选库中的所有不同候选:
prototype_scoring:
enabled: false
best_weight: 0.75
top_m: 2
此设置关闭聚类和原型数量上限,保留最高相似度与支持分数的聚合。
文本和图像的 hard/easy 候选库及其评分使用同一份解析后的配置。
启用压缩时,max_prototypes: 0 使用默认上限 8。这些设置影响本地原型评分,
不影响远程打分器返回的分数;阈值和显式难度边界保持不变。
判断模型评分
已有 hard/easy 样例库默认继续使用嵌入比较。要改为判断任务,设置 routing.model_bindings.complexity: {deployment: primary, contract: decision.v1}。没有样例库的规则也可使用默认判断模型部署;部署必须支持 score 问题。
任务提出 easy、medium、hard 等级问题,将原生等级分数归一化到 [0, 1] 后应用显式边界;仅设置 threshold 时先映射到 [-1, 1] 再应用对称区间。这是模型判断,不是余弦差值或校准后的正确率;切换任务或模型时应重新评估边界。
本地与远程打分
没有 backend 时,complexity 保 持现有本地行为:hard 与 easy 候选列表在启动时嵌入一次,每个请求按它更像难示例还是易示例的程度打分。该差值是以零为中心的有符号裕量,因此 threshold 是对称的——高于 +threshold 为 hard,低于 -threshold 为 easy,中间档为 medium。
远程打分器使用共享 backend 块,声明在 prototype_scoring 旁边,而不是写在规则上。每个路由配方会整段替换 routing.signals,因此写在规则上的 backend 会在未重复声明它的配方下消失,留下仍在运行但已悄悄退回本地的信号。其 model 是 global.model_catalog.external[] 中的显式名称,该条目需要 model_role: classification。
Complexity 读取两种约定,选哪一种取决于模型返回什么。因为有两种,contract 不能默认,必须写明。
返回分数的模型:score.v1
回归模型——例如查询难度打分器——返回一个数字,Router 用每条规则的边界把它变成判定。无论多少规则读取它,一次请求只调用一次。
分数以模型自身单位到达,不是裕量,因此 threshold 不适用。改为写明两个边界,所用的一对字段表示难度方向:
global:
model_catalog:
external:
- name: difficulty-scorer
model_role: classification
llm_endpoint:
address: difficulty-scorer.default.svc
port: 8080
llm_model_name: query-difficulty-v1
modules:
complexity:
backend:
protocol: http_classify
contract: score.v1
model: difficulty-scorer
deadline_ms: 5000
routing:
signals:
complexity:
- name: needs_reasoning
hard_above: 0.85 # 分数越高越难
easy_below: 0.60
- name: extreme
hard_above: 0.95 # 同一分数,更严的边界
easy_below: 0.30
对于分数随难度上升而下降的模型——例如预测正确答案概率的模型——改用 hard_below 与 easy_above。把方向编码在字段名里,就不必再维护单独的方向设置,也不会意外写出重叠档。这两个字段适用于评分后端或判断模型评分。本地样例库的裕量是 hard 减 easy,因此更高值本身就更难;要在本地反转它,交换候选列表即可。
想要比三个判定更细的分级?对同一分数写更多规则,各有自己的边界。判定词表仍是 hard|easy|medium,因为决策匹配 <rule>:<verdict>,规则仍可通过各自的 composer 条件区分。
上面两条规则不是互斥替代:打分器只调用一次,每条规则用自己的边界读同一分数,因此一次请求为每条规则产生一个判定。
| 分数 | needs_reasoning(0.85 / 0.60) | extreme(0.95 / 0.30) |
|---|---|---|
| 0.20 | easy | easy |
| 0.50 | easy | medium |
| 0.70 | medium | medium |
| 0.90 | hard | medium |
| 0.99 | hard | hard |
这给决策一条可匹配的阶梯——最顶端用 extreme:hard,超过 0.85 用 needs_reasoning:hard,底部用 needs_reasoning:easy。分数 0.99 同时满足两个 hard 条件,因此 extreme:hard 的决策需要更高 priority,否则更松的规则会拿走请求。
给每一档显式 priority,并把更严的一档设为更大数字。priority 可以省略;两条决策在同一优先级都匹配时, 按置信度分开——而 score.v1 从不报告置信度,因此比较会落到决策名的字母序。重命名决策就会改变请求到达哪个模型,且没有任何报告说明发生了这件事——#3658 跟踪让最终比较可观测。
score.v1 不报告置信度。刚低于 hard_above 的分数是最不确定的位置,而不是强位置,因此不会从中推导置信度,任何以该规则为门控的决策会按引擎的结构默认排序,而不是报告分数。Router 在启动时对这种情况发出警告。
端点可以用两种形状回答:恰好一条的 HuggingFace 文本分类数组 [{"label": "difficulty", "score": 0.73}],或裸对象 {"score": 0.73}。缺失或空分数、多于一条,或非有限值都是错误,绝不是零——零是 [0,1] 范围易端的真实分数,故障不得按零路由。
返回判定的模型:label_distribution.v1
三分类模型直接返回 hard、easy 与 medium。不查阅边界——获胜标签就是判定——其概率是真实置信度,因此基于置信度的排序不受影响。标签是固定判定词表,不可配置:
modules:
complexity:
backend:
protocol: http_classify
contract: label_distribution.v1
model: difficulty-classifier
deadline_ms: 5000
无论哪种方式,一旦 backend 提供分数,hard 与 easy 候选列表就不再被读取,Router 会在启动时说明,而不是让你去编辑已无效果的示例。
观察远程打分器
网络依赖会以本地模型不会的方式失败,因此远程路径会报告它们做了什么:
llm_remote_connector_requests_total{operation, outcome}与llm_remote_connector_request_duration_seconds{operation}统计并计时共享连接器上的每次调用。outcome是success或错误种类(transport、status、authorization、request、response),llm_remote_connector_retries_total统计重试次数。llm_complexity_verdict_total{rule, verdict, source}统计判定,其中source是local、remote_score或remote_labels——与失败计数器携带的同一标签,因此两者可以一起读。该分布的形状是尺度不匹配的检查:hard_above: 0.85后面的[1,10]打分器会表现为该规则的每个请求都是hard,单条日志行看不出来。llm_complexity_evaluation_failures_total{source}统计未产生判定的评估。
打分器不可达时,请求仍会路由。complexity 信号缺失,每条 complexity 规则在请求的信号错误中标记为 complexity_evaluation_failed,并由 llm_complexity_evaluation_failures_total 计数。
默认情况下,条件读取失败 complexity 规则的决策把它当作未匹配,因此请求落到下一条匹配。要有意决定,请在该决策的根 rules 节点上设置 on_unknown:
decisions:
- name: deep-reasoning
priority: 100
rules:
on_unknown: match # no_match(默认)| match | fail_request
operator: AND
conditions:
- type: complexity
name: needs_reasoning:hard
match 把无法评级的请求送到更强模型,通常是更安全的默认;fail_request 直接拒绝。on_unknown 属于根 rules 节点,并作用于整条决策——逐条件 on_error 字段只在 classifier 和 decision 条件上接受,因此把它放在 complexity 条件上会在配置加载时被拒绝。
打分器的数字以模型自身单位发布为 complexity:<rule>:score,而本地打分发布 complexity:<rule>:margin 及其文本/图像分量。这些到达回 放记录与可观测性;决策条件匹配判定(<rule>:<verdict>),而不是数字,因此对 :score 的数值谓词不是路由机制。
依赖与限制
- Complexity 使用已配置的语义嵌入运行时。远程嵌入提供方会收到用于分类的请求文本。
- 候选短语与阈值必须对照已标注流量一起校准。嵌入模型变更时请重新评估。
- 模糊提示可能落到
medium档;请为你依赖的每一档定义路由或回退。 - 完整示例见:
config/fragments/signal/complexity/escalation.yaml。