Skip to content

05-二级指令系统

二级指令是 LLM 与 DSL 之间的封装层。让 LLM 拿到问题后对照模板填槽,而不是自由发挥写 DSL——这是问数技能能跑稳的另一关键决策。


直接让 LLM 写 DSL 已经比写 SQL 好得多。但 DSL 仍然有自由度——同一个查询可以有 5-10 种写法,LLM 在选择路径时会出错。

用户问:「深圳 10 万平米以上项目的土建单方造价和钢筋含量是多少?」

LLM 写 DSL 直查的路径之一:

{
"cube": "ProjectIndicator",
"dimensions": ["projectName"],
"measures": [
{"member": "indicator", "agg": "avg", "alias": "土建单方", "routedKey": 1,
"filterIf": [
{"member": "bzItemName", "operator": "equals", "values": ["土建工程费"]},
{"member": "isEndCost", "operator": "equals", "values": [0]}
]},
{"member": "indicator", "agg": "avg", "alias": "钢筋含量", "routedKey": 3,
"filterIf": [
{"member": "bzItemName", "operator": "equals", "values": ["钢筋"]},
{"member": "isEndCost", "operator": "equals", "values": [1]}
]}
],
"filters": [
{"member": "cityName", "operator": "contains", "values": ["深圳"]},
{"member": "buildArea", "operator": "gte", "values": [100000]}
]
}

LLM 写这个 DSL 要正确:

  • 知道 cube 是 ProjectIndicator 不是 BqIndicator
  • 知道 routedKey 1/3 各代表什么;
  • 知道每个 measure 要写 filterIf 数组而不是 filters
  • 知道 cityName 是 friendlyAlias 不带前缀;
  • 知道 buildArea 自动 JOIN Project;
  • 知道哪些 measure 需要 isEndCost=0、哪些是 1(父子层级业务规则)。

任一步错都会重试。cost-query 实测:写这条 DSL 单次正确率约 60%,平均 1.7 次重试。

Terminal window
cost-query agg-project-indicator --params '{
"cityName": "深圳",
"buildAreaMin": 100000,
"groupBy": ["projectName"],
"measureGroups": [
{"alias": "土建单方造价", "bzItemName": "土建工程费", "isEndCost": 0, "indicatorType": "建面单方"},
{"alias": "钢筋含量", "bzItemName": "钢筋", "isEndCost": 1, "indicatorType": "含量"}
]
}'

LLM 要正确:

  • 知道这种”区域+面积+多指标”问题对应 agg-project-indicator + measureGroups
  • 知道 indicatorType 写中文别名;
  • 知道每个 measureGroup 的 bzItemName 是 equals 精确(必须用标准词)。

实测:写这条二级指令单次正确率约 90%,平均 0.3 次重试。

核心差异:二级指令吸收了所有结构细节(routedKey、filterIf、cube 选择、friendlyAlias 形式),LLM 只填业务参数。


cost-query 的 15 个二级指令:

VerbEntity指令用途
findprojectfind-project项目画像查询
finddimensionfind-dimension维度探测
findunit-pricefind-unit-price清单价格明细
findtm-pricefind-tm-price人材机价格明细
findbq-indicatorfind-bq-indicator清单指标概览
findbq-bzitem-indicatorfind-bq-bzitem-indicator清单科目指标
findproject-indicator-detailfind-project-indicator-detail项目指标穿透明细
findbq-talent-machinefind-bq-talent-machine清单项×定额×人材机明细行
findproject-module-standardfind-project-module-standard项目建造标准
aggproject-indicatoragg-project-indicator项目指标汇总
aggunit-priceagg-unit-price清单价格汇总
aggtm-priceagg-tm-price人材机价格汇总
aggbq-indicatoragg-bq-indicator清单/科目指标汇总
aggbq-talent-machineagg-bq-talent-machine清单项人材机消耗汇总
batch批量执行

一个事实 cube 通常配 1-2 个二级指令(find + agg),不必每个 verb 都配:

  • find-X:明细查询(不聚合);
  • agg-X:汇总查询(GROUP BY + 聚合);
  • rank-X:cost-query 已下线,用 agg-X + orderBy + limit 替代。

特殊场景才单独建指令

  • find-dimension:覆盖所有维度 cube 的探测,不需要每个维度建一个 find-city / find-bzitem
  • batch:批量执行框架,不属于任何 cube;
  • find-project:除了主 cube 查询还包含组织层级 / 业态切换逻辑,比 find 一般 cube 重,单独建。
  • 用 kebab-case(find-unit-price 不是 findUnitPrice);
  • 业务实体名优先用领域术语简称(tm = talent-machine、bq = bq-name);
  • 避免缩写歧义:find-project-indicator-detail 而不是 find-pi-detail

commands/agg-project-indicator.yaml 节选:

verb: aggregate # 对应 DSL 的 verb
cube: ProjectIndicator # 主 cube
description: 汇总项目指标(建面单方 / 单位造价 / 含量 / 综合单价)
params: # 参数清单(--info 时打印)
measureGroups:
required: true
type: array
description: 指标分组(必填)
schema:
type: object
required: [alias, indicatorType]
properties:
alias: { type: string, description: 输出列名 }
indicatorType: { type: string, description: 指标类型(中文枚举)}
bzItemName: { type: string, description: 科目名(equals 精确)}
isEndCost: { type: number, enum: [0, 1] }
projectName:
type: string | array
description: 项目名(contains / 数组自动 in)
cityName:
type: string | array
description: 城市名(contains / 数组自动 in)
productTypeName:
type: string
description: 业态名(contains)
bzItemName:
type: string | array
description: 顶层科目名(LIKE contains,作外层 WHERE)
isEndCost:
type: number
enum: [0, 1]
buildAreaMin:
type: number
buildAreaMax:
type: number
buName:
type: string
description: 公司名(contains 跨层级模糊)
groupBy:
type: array
orderBy:
type: array
limit:
type: number
default: 20
filters: # 顶层参数 → DSL filters 的映射
- param: cityName
member: cityName # friendlyAlias 短名
operator: contains
array_op: in
- param: projectName
member: projectName
operator: contains
array_op: in
- param: bzItemName
member: bzItemName
operator: contains
array_op: in
- param: isEndCost
member: isEndCost
operator: equals
- param: buildAreaMin
member: buildArea
operator: gte
- param: buildAreaMax
member: buildArea
operator: lte
builder: _builder.build_agg_project_indicator # 自定义编译逻辑(可选)

简单二级指令的 DSL 生成可以完全由 YAML 描述(params → filters → DSL);复杂场景需要自定义 builder。

agg-project-indicator 的自定义 builder 干了什么:

  1. 展开 measureGroups → DSL measures 数组(每个 group 生成一个带 filterIf 的 measure);
  2. 应用 inheritance 规则(顶层 bzItemName → group 默认值);
  3. 应用 inferredFilters 推断(按 productTypeName 引用情况推 isJianAn);
  4. 应用 defaultFilters 兜底(未传 bzItemName 时加 isEndCost=1);
  5. 自动添加 friendlyAlias JOIN(cityName → City)。

完整实现见 ontology-model 仓 _builder.py


cost-query 最有价值的二级指令封装。来看它解决什么问题。

用户问:「X 项目的土建单方 + 钢筋含量 + 防水综合单价分别是多少?」

不同指标属于不同的 (bzItemName, isEndCost, indicatorType) 组合:

  • 土建单方 → bzItemName="土建工程费", isEndCost=0, routedKey=1
  • 钢筋含量 → bzItemName="钢筋", isEndCost=1, routedKey=3
  • 防水综合单价 → bzItemName="防水工程", isEndCost=1, routedKey=4

传统做法:

  • 发 3 次查询:耗时 × 3,且 LLM 要处理 3 次结果合并;
  • 写 DSL filterIf:一次出 3 列,但 LLM 写 filterIf 错误率高(每个 measure 要写 2 条 filterIf 条件 + 自己懂 routedKey)。
{
"projectName": "X",
"measureGroups": [
{"alias": "土建单方", "bzItemName": "土建工程费", "isEndCost": 0, "indicatorType": "建面单方"},
{"alias": "钢筋含量", "bzItemName": "钢筋", "isEndCost": 1, "indicatorType": "含量"},
{"alias": "防水综合单价", "bzItemName": "防水工程", "isEndCost": 1, "indicatorType": "综合单价"}
]
}

编译器把 measureGroups 展开为 3 个 filterIf measure,一次 SQL 出 3 列。

  1. 业务化别名 alias:LLM 拼用户原话,输出表头就是用户能看懂的;
  2. 中文枚举 indicatorType:内部映射 routedKey 1/2/3/4,LLM 不需要懂物理列;
  3. 三层语义
    • 顶层 params.bzItemName:LIKE contains,外层 WHERE,口语词容错;
    • measureGroups[i].bzItemName:equals 精确,内层 CASE WHEN,多指标分流;
    • measureGroups[i].bzItemCode:equals 精确,编码精锁;
  4. inheritance 边界:顶层字段作为 group 默认值,group 显式覆盖;
  5. 默认排序:v2 起 measureGroups + groupBy 含维度且未传 orderBy 时按 groupBy 首维度 ASC(避免 NULL+LIMIT 截断真数据)。

观察用户问题特征,找出**「多指标 / 跨 (维度 X, 维度 Y) 组合」类**的高频场景:

领域类似封装机会
售楼”X 项目的认购数 / 签约数 / 备案数”(同一指标按合同状态分流)
工程”X 项目土建/机电/装修的计划进度 vs 实际进度”(同指标按专业 × 阶段分流)
HR”X 部门各级别的平均薪资 / 平均司龄 / 离职率”(多个指标按级别分流)
财务”X 客户的应收 / 实收 / 已开票”(同金额按结算状态分流)

封装的判断标准:

  • 用户高频问”X 的 a / b / c 是多少”(一次问多指标);
  • 各指标共享主筛选条件(同项目 / 同客户 / 同部门);
  • 各指标的内层 filter 不同(科目不同 / 状态不同 / 阶段不同);
  • 不封装的话 LLM 要发 N 次查询。

满足 3 条以上就值得封装。


第二个跨领域通用的封装:多查询合并

LLM 经常需要发多次查询:

  • 维度标准化批量探测:先查”建安 → 标准词”、“精装 → 标准词”两个维度;
  • 复合问题拆段:先查”广东省有土方开挖的项目”,再用结果查”T-总部大楼的综合单价”;
  • 跨 cube 对比:先 BqUnitPrice 查清单价,再 TalentMachinePrice 查市场价;
  • 口径并列查询:项目级和业态级两个口径同时看。
Terminal window
# ❌ 反模式
cost-query find-dimension --params '{...}' && cost-query find-dimension --params '{...}'

问题:Claude Code 的 Bash 工具会截断后段输出,第二条结果丢失。

Terminal window
cost-query batch --params '{
"queries": [
{"cmd": "find-dimension", "params": {"dimensionName": "BzItem", "keyword": "建安"}},
{"cmd": "find-dimension", "params": {"dimensionName": "BzItem", "keyword": "精装"}},
{"cmd": "agg-project-indicator", "params": {...}},
{"cmd": "aggregate", "dsl": {...}}
]
}'
  • 子查询字段:二级指令用 {cmd, params},DSL 直查用 {cmd, dsl}不能同传
  • 按队列顺序执行,结果统一返回;
  • 子查询任意失败时报错但已执行的不回滚;
  • 支持 DSL + 二级指令混排。

commands/_ext/batch.py

def build_batch(params: dict) -> list[dict]:
"""
输入:{"queries": [{"cmd": ..., "params"|"dsl": ...}, ...]}
输出:每个子查询的执行结果数组
"""
queries = params.get("queries", [])
results = []
for i, q in enumerate(queries):
cmd = q.get("cmd")
# 同传校验
if "params" in q and "dsl" in q:
raise ValueError(f"queries[{i}]: 不能同时传 params 与 dsl")
# 路由到二级指令或 DSL
if "params" in q:
r = execute_template(cmd, q["params"])
else:
r = _compile_and_execute(cmd, q["dsl"])
results.append({"index": i, "cmd": cmd, "result": r})
return results

这是个跨领域直接 fork 即可的能力,你的项目可以原样照搬。


第三个跨领域通用的封装。

用户问”建安成本怎么样”——LLM 不知道”建安”在数据库里的标准词是什么(可能是”建筑安装工程费”)。需要先做维度标准化。

Terminal window
cost-query find-dimension --params '{
"dimensionName": "BzItem",
"keyword": "建安",
"projectName": "X" # 收敛到项目所属指标模板
}'

返回候选列表:

[
{"bzItemName": "建筑安装工程费", "bzItemCode": "A.03", "isEndCost": 0, "templateName": "房开模板"},
{"bzItemName": "建筑工程", "bzItemCode": "B.01", "isEndCost": 0, "templateName": "东航模板"},
{"bzItemName": "安装工程", "bzItemCode": "B.02", "isEndCost": 0, "templateName": "东航模板"}
]

LLM 看到结果就知道”建安”在不同模板里对应不同标准词,向用户澄清。

commands/find-dimension.yaml + commands/_ext/find_dimension.py

def build_find_dimension(params: dict) -> dict:
dim_name = params["dimensionName"] # 如 "BzItem"
keyword = params.get("keyword")
# 按 dimensionName 路由到对应维度 cube
cube_map = {
"BzItem": "BzItem",
"ProductType": "ProductType",
"City": "City",
"TalentMachineType": "TalentMachineType",
# ...
}
cube = cube_map.get(dim_name)
if not cube:
raise ValueError(f"未知 dimensionName: {dim_name}")
# 构造 DSL find 查询
return {
"verb": "find",
"cube": cube,
"dimensions": [...], # 该 cube 的展示字段
"filters": [{"member": "name", "operator": "contains", "values": [keyword]}] if keyword else [],
"limit": 50
}

跨领域通用:你只需要把 cube_map 改成自己领域的维度 cube 清单即可。


cost-query 经历过”指令数膨胀”——V3 阶段有 18 个二级指令(含 3 个 rank-*),V4 砍到 15 个。

  • 某 verb 长期低频使用:cost-query rank-* 在测评中调用次数 < 总数的 5%,且能用 agg + orderBy + limit 等价替代 → 砍;
  • 指令参数集合高度重叠:A 指令和 B 指令的参数有 80% 重叠,差异只是 1-2 个参数 → 合并;
  • LLM 在多个指令间反复选错:测评中 LLM 经常在 A 和 B 之间猜错 → 砍掉差异不显著的一个。
  • 决策树缩短:LLM 在选指令时少一层选择;
  • 学习曲线降低:模式手册条目少 1/N,更容易记住;
  • 维护成本降低:少一个 YAML + builder + 文档。
  1. 跑测评集确认现有指令的使用频次;
  2. 替代路径有 100% 等价能力(参数 / 输出都对齐);
  3. 在 query-guide 反模式中明文标”X 已下线,用 Y 替代”;
  4. 提交后跑一轮全量测评回归。

5.8 二级指令规划清单(你的领域)

Section titled “5.8 二级指令规划清单(你的领域)”

按以下顺序规划你领域的二级指令:

从测评集(或业务方访谈)里挑 30-50 道代表性问题,列出每道问题的:

  • 主 cube;
  • 主 verb(明细 / 汇总 / 排名);
  • 关键 filter 维度;
  • 关键 measure。

把问题按”主 cube + verb”分组。每组通常对应 1 个二级指令。

cost-query 实例(按 cube 分组):

主 cubefindagg备注
ProjectIndicatoragg-project-indicatorfind 没必要建(穿透走 ProjectIndicatorDetail)
ProjectIndicatorDetailfind-project-indicator-detailagg 暂无需求
BqUnitPricefind-unit-priceagg-unit-price两个都常用
TalentMachinePricefind-tm-priceagg-tm-price两个都常用
BqIndicatorfind-bq-indicatoragg 走 agg-bq-indicator(跨 cube)
BqBzItemIndicatorfind-bq-bzitem-indicatoragg 走 agg-bq-indicator(跨 cube)
BqUnitTalentMachineDetailfind-bq-talent-machineagg-bq-talent-machine两个都常用
ProjectModuleStandardfind-project-module-standardagg 暂无需求
Projectfind-project项目画像

每组指令里,看有没有像 measureGroups 这种业务化封装的机会。判断标准见 §5.4。

每个二级指令的参数清单,按以下分类:

  • 必填参数:核心 filter(如 agg-project-indicator 的 measureGroups);
  • 可选 filter:如 projectName / cityName / 时间窗;
  • 聚合参数:groupBy / orderBy / limit;
  • 特殊参数:如 timeGranularity / havingMin/Max。

每个二级指令上线后立即用 5-10 道相关测评题验证。


Part 5 完。读完应能回答:“为什么要二级指令、怎么命名、YAML 模板长什么样、measureGroups 这类封装怎么设计、batch / find-dimension 怎么用、什么时候砍指令、怎么规划你领域的指令清单”。