05-二级指令系统
Part 5 · 二级指令系统
Section titled “Part 5 · 二级指令系统”二级指令是 LLM 与 DSL 之间的封装层。让 LLM 拿到问题后对照模板填槽,而不是自由发挥写 DSL——这是问数技能能跑稳的另一关键决策。
5.1 为什么要有二级指令
Section titled “5.1 为什么要有二级指令”直接让 LLM 写 DSL 已经比写 SQL 好得多。但 DSL 仍然有自由度——同一个查询可以有 5-10 种写法,LLM 在选择路径时会出错。
一个具体例子
Section titled “一个具体例子”用户问:「深圳 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; - 知道
routedKey1/3 各代表什么; - 知道每个 measure 要写
filterIf数组而不是filters; - 知道
cityName是 friendlyAlias 不带前缀; - 知道
buildArea自动 JOIN Project; - 知道哪些 measure 需要 isEndCost=0、哪些是 1(父子层级业务规则)。
任一步错都会重试。cost-query 实测:写这条 DSL 单次正确率约 60%,平均 1.7 次重试。
二级指令封装后
Section titled “二级指令封装后”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 只填业务参数。
5.2 二级指令的命名与粒度
Section titled “5.2 二级指令的命名与粒度”命名规则:<verb>-<entity>
Section titled “命名规则:<verb>-<entity>”cost-query 的 15 个二级指令:
| Verb | Entity | 指令 | 用途 |
|---|---|---|---|
find | project | find-project | 项目画像查询 |
find | dimension | find-dimension | 维度探测 |
find | unit-price | find-unit-price | 清单价格明细 |
find | tm-price | find-tm-price | 人材机价格明细 |
find | bq-indicator | find-bq-indicator | 清单指标概览 |
find | bq-bzitem-indicator | find-bq-bzitem-indicator | 清单科目指标 |
find | project-indicator-detail | find-project-indicator-detail | 项目指标穿透明细 |
find | bq-talent-machine | find-bq-talent-machine | 清单项×定额×人材机明细行 |
find | project-module-standard | find-project-module-standard | 项目建造标准 |
agg | project-indicator | agg-project-indicator | 项目指标汇总 |
agg | unit-price | agg-unit-price | 清单价格汇总 |
agg | tm-price | agg-tm-price | 人材机价格汇总 |
agg | bq-indicator | agg-bq-indicator | 清单/科目指标汇总 |
agg | bq-talent-machine | agg-bq-talent-machine | 清单项人材机消耗汇总 |
| — | — | batch | 批量执行 |
一个事实 cube 通常配 1-2 个二级指令(find + agg),不必每个 verb 都配:
find-X:明细查询(不聚合);agg-X:汇总查询(GROUP BY + 聚合);:cost-query 已下线,用rank-Xagg-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。
5.3 二级指令 YAML 模板格式
Section titled “5.3 二级指令 YAML 模板格式”commands/agg-project-indicator.yaml 节选:
verb: aggregate # 对应 DSL 的 verbcube: 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 # 自定义编译逻辑(可选)自定义 builder
Section titled “自定义 builder”简单二级指令的 DSL 生成可以完全由 YAML 描述(params → filters → DSL);复杂场景需要自定义 builder。
agg-project-indicator 的自定义 builder 干了什么:
- 展开
measureGroups→ DSLmeasures数组(每个 group 生成一个带 filterIf 的 measure); - 应用 inheritance 规则(顶层 bzItemName → group 默认值);
- 应用 inferredFilters 推断(按 productTypeName 引用情况推 isJianAn);
- 应用 defaultFilters 兜底(未传 bzItemName 时加 isEndCost=1);
- 自动添加 friendlyAlias JOIN(cityName → City)。
完整实现见 ontology-model 仓 _builder.py。
5.4 业务封装样板:measureGroups
Section titled “5.4 业务封装样板:measureGroups”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)。
measureGroups 封装
Section titled “measureGroups 封装”{ "projectName": "X", "measureGroups": [ {"alias": "土建单方", "bzItemName": "土建工程费", "isEndCost": 0, "indicatorType": "建面单方"}, {"alias": "钢筋含量", "bzItemName": "钢筋", "isEndCost": 1, "indicatorType": "含量"}, {"alias": "防水综合单价", "bzItemName": "防水工程", "isEndCost": 1, "indicatorType": "综合单价"} ]}编译器把 measureGroups 展开为 3 个 filterIf measure,一次 SQL 出 3 列。
- 业务化别名
alias:LLM 拼用户原话,输出表头就是用户能看懂的; - 中文枚举
indicatorType:内部映射 routedKey 1/2/3/4,LLM 不需要懂物理列; - 三层语义:
- 顶层
params.bzItemName:LIKE contains,外层 WHERE,口语词容错; measureGroups[i].bzItemName:equals 精确,内层 CASE WHEN,多指标分流;measureGroups[i].bzItemCode:equals 精确,编码精锁;
- 顶层
- inheritance 边界:顶层字段作为 group 默认值,group 显式覆盖;
- 默认排序:v2 起 measureGroups + groupBy 含维度且未传 orderBy 时按 groupBy 首维度 ASC(避免 NULL+LIMIT 截断真数据)。
你的领域怎么找类似封装
Section titled “你的领域怎么找类似封装”观察用户问题特征,找出**「多指标 / 跨 (维度 X, 维度 Y) 组合」类**的高频场景:
| 领域 | 类似封装机会 |
|---|---|
| 售楼 | ”X 项目的认购数 / 签约数 / 备案数”(同一指标按合同状态分流) |
| 工程 | ”X 项目土建/机电/装修的计划进度 vs 实际进度”(同指标按专业 × 阶段分流) |
| HR | ”X 部门各级别的平均薪资 / 平均司龄 / 离职率”(多个指标按级别分流) |
| 财务 | ”X 客户的应收 / 实收 / 已开票”(同金额按结算状态分流) |
封装的判断标准:
- 用户高频问”X 的 a / b / c 是多少”(一次问多指标);
- 各指标共享主筛选条件(同项目 / 同客户 / 同部门);
- 各指标的内层 filter 不同(科目不同 / 状态不同 / 阶段不同);
- 不封装的话 LLM 要发 N 次查询。
满足 3 条以上就值得封装。
5.5 batch:多查询合并
Section titled “5.5 batch:多查询合并”第二个跨领域通用的封装:多查询合并。
LLM 经常需要发多次查询:
- 维度标准化批量探测:先查”建安 → 标准词”、“精装 → 标准词”两个维度;
- 复合问题拆段:先查”广东省有土方开挖的项目”,再用结果查”T-总部大楼的综合单价”;
- 跨 cube 对比:先 BqUnitPrice 查清单价,再 TalentMachinePrice 查市场价;
- 口径并列查询:项目级和业态级两个口径同时看。
反模式:Bash 串行
Section titled “反模式:Bash 串行”# ❌ 反模式cost-query find-dimension --params '{...}' && cost-query find-dimension --params '{...}'问题:Claude Code 的 Bash 工具会截断后段输出,第二条结果丢失。
batch 封装
Section titled “batch 封装”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 即可的能力,你的项目可以原样照搬。
5.6 find-dimension:通用维度探测
Section titled “5.6 find-dimension:通用维度探测”第三个跨领域通用的封装。
用户问”建安成本怎么样”——LLM 不知道”建安”在数据库里的标准词是什么(可能是”建筑安装工程费”)。需要先做维度标准化。
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 看到结果就知道”建安”在不同模板里对应不同标准词,向用户澄清。
自定义 builder
Section titled “自定义 builder”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 清单即可。
5.7 二级指令的何时砍
Section titled “5.7 二级指令的何时砍”cost-query 经历过”指令数膨胀”——V3 阶段有 18 个二级指令(含 3 个 rank-*),V4 砍到 15 个。
砍指令的信号
Section titled “砍指令的信号”- 某 verb 长期低频使用:cost-query rank-* 在测评中调用次数 < 总数的 5%,且能用 agg + orderBy + limit 等价替代 → 砍;
- 指令参数集合高度重叠:A 指令和 B 指令的参数有 80% 重叠,差异只是 1-2 个参数 → 合并;
- LLM 在多个指令间反复选错:测评中 LLM 经常在 A 和 B 之间猜错 → 砍掉差异不显著的一个。
砍指令的好处
Section titled “砍指令的好处”- 决策树缩短:LLM 在选指令时少一层选择;
- 学习曲线降低:模式手册条目少 1/N,更容易记住;
- 维护成本降低:少一个 YAML + builder + 文档。
- 跑测评集确认现有指令的使用频次;
- 替代路径有 100% 等价能力(参数 / 输出都对齐);
- 在 query-guide 反模式中明文标”X 已下线,用 Y 替代”;
- 提交后跑一轮全量测评回归。
5.8 二级指令规划清单(你的领域)
Section titled “5.8 二级指令规划清单(你的领域)”按以下顺序规划你领域的二级指令:
Step 1:收集高频问题
Section titled “Step 1:收集高频问题”从测评集(或业务方访谈)里挑 30-50 道代表性问题,列出每道问题的:
- 主 cube;
- 主 verb(明细 / 汇总 / 排名);
- 关键 filter 维度;
- 关键 measure。
Step 2:聚类找出共性
Section titled “Step 2:聚类找出共性”把问题按”主 cube + verb”分组。每组通常对应 1 个二级指令。
cost-query 实例(按 cube 分组):
| 主 cube | find | agg | 备注 |
|---|---|---|---|
| ProjectIndicator | — | agg-project-indicator | find 没必要建(穿透走 ProjectIndicatorDetail) |
| ProjectIndicatorDetail | find-project-indicator-detail | — | agg 暂无需求 |
| BqUnitPrice | find-unit-price | agg-unit-price | 两个都常用 |
| TalentMachinePrice | find-tm-price | agg-tm-price | 两个都常用 |
| BqIndicator | find-bq-indicator | — | agg 走 agg-bq-indicator(跨 cube) |
| BqBzItemIndicator | find-bq-bzitem-indicator | — | agg 走 agg-bq-indicator(跨 cube) |
| BqUnitTalentMachineDetail | find-bq-talent-machine | agg-bq-talent-machine | 两个都常用 |
| ProjectModuleStandard | find-project-module-standard | — | agg 暂无需求 |
| Project | find-project | — | 项目画像 |
Step 3:识别业务封装机会
Section titled “Step 3:识别业务封装机会”每组指令里,看有没有像 measureGroups 这种业务化封装的机会。判断标准见 §5.4。
Step 4:列参数清单
Section titled “Step 4:列参数清单”每个二级指令的参数清单,按以下分类:
- 必填参数:核心 filter(如 agg-project-indicator 的 measureGroups);
- 可选 filter:如 projectName / cityName / 时间窗;
- 聚合参数:groupBy / orderBy / limit;
- 特殊参数:如 timeGranularity / havingMin/Max。
Step 5:写第一版 YAML,跑测评
Section titled “Step 5:写第一版 YAML,跑测评”每个二级指令上线后立即用 5-10 道相关测评题验证。
5.9 接下来读什么
Section titled “5.9 接下来读什么”- 要看 SKILL.md 怎么承接二级指令 → 读 Part 6 SKILL 业务表达层;
- 要看模式手册怎么把问题映射到二级指令 → 读 Part 7 模式手册;
- 要看脚手架 → 读 Part 9 脚手架与启动模板。
Part 5 完。读完应能回答:“为什么要二级指令、怎么命名、YAML 模板长什么样、measureGroups 这类封装怎么设计、batch / find-dimension 怎么用、什么时候砍指令、怎么规划你领域的指令清单”。