业务特殊逻辑配置

功能介绍

业务特殊逻辑配置(enrich)是在语义层专家模式中配置的一个函数,作用是开发特殊业务逻辑。当用户提问后,系统会将问题转换为 Logicform,如果有业务特殊的逻辑需要添加,就可以通过 enrich 功能修改,返回一个新的 Logicform。

Tip

enrich 功能的使用需要有 JavaScript 基础。

场景举例

以下场景都比较适合使用 enrich 功能:

  • 你希望当提问者问到【上个月业绩】的时候,这个【上个月】指的是上上个月的 28 号~上个月的 27 号
  • 你希望当提问者问到【商品销售额】的时候,默认加上【是否退货 = false】,但是其他场景下,不要加上此条件。

总结来说,遇到需要逻辑判断的特殊业务规则,都可以用 enrich 功能实现。

使用方式

系统搭建 → 语义建模 → Schema 列表 → 点击具体 Schema → 业务逻辑定制 处打开。

业务特殊逻辑配置(enrich) 本质是一个函数。以下是该函数的签名:

(logicform, normedLogicform, helperFunctions) => {
   // 各种修改logicform,如logicform.query.日期 = {year: 2024}
   return logicform;
}

该函数接受 3 个参数,分别是:

  • logicform: 代表用户的提问意图。可直接在 logicform 上进行修改。
  • normedLogicform: 规范化后的 logicform,方便用于各种 if 条件判断。尤其是关于日期,logicform 变量
  • helperFunctions: 一些帮助函数,典型的有 moment。可以用如下形式调出 moment
(logicform, normedLogicform, helperFunctions) => {
   const {moment} = helperFunctions;
   // 各种修改logicform,如logicform.query.日期 = {year: 2024}
   return logicform;
}

将 logicform 修改后,直接返回 logicform 即可。

注意:enrich 函数也支持 async 版本,只要在函数开头写上 async 就好

async (logicform, normedLogicform, helperFunctions) => {
   const {moment} = helperFunctions;
   // 各种修改logicform,如logicform.query.日期 = {year: 2024}
   return logicform;
}

helperFunctions 里面所有支持的函数

常用函数:

  • moment: momentjs 库
  • _: underscore 库
  • execute: logicform 的 execute 函数。和自定义指标里面 execute 是一样的。
  • getGranuality: 传入日期参数,如 {$gte:'xxx', $lte: 'xx'},返回日期的粒度,如 yearmonthday
  • isDimensionInQuery: (query: object, dimension: string)。返回 true/false,判断某个 query 里是否包含指定维度。
  • isDimensionInLogicform: (logicform: LogicformType, dimension: string)。返回 true/false,判断整个 logicform 里是否包含指定维度。
  • getQueryWithKey: (key: string)。查找 logicform 中所有包含指定字段名的 query 节点,适合批量修改时间条件、业务条件等。

可用上下文:

  • graph: 当前解析过程中的 graph 对象。
  • schemas: 当前可用的 schema 集合。
  • customFunctions: 当前加载的自定义函数集合。
  • locale: 当前语言环境。
  • schema: 当前 schema 的 enrich 中可用。
  • renamePredItem: 当前 schema 的 enrich 中可用,用于重命名 predItem

此外,helperFunctions 还会透出一部分 Logicform.util 中的工具函数。不同版本里可用项可能略有差异,实际请以运行环境为准。

详细说明

moment

moment 是标准的时间处理库,可用于日期计算、格式化、偏移和区间处理。

适合场景:

  • 计算相对日期,例如上个月、近 7 天、最近一个完整财月
  • 对日期进行格式化,例如输出为 YYYY-MM-DD
  • 做日期偏移,例如加减天、月、年

使用示意:

async (logicform, normedLogicform, helperFunctions) => {
  const { moment } = helperFunctions;
  const start = moment().subtract(1, 'month').startOf('month').format('YYYY-MM-DD');
  const end = moment().subtract(1, 'month').endOf('month').format('YYYY-MM-DD');

  logicform.query.日期 = { $gte: start, $lte: end };
  return logicform;
}

_

_underscore 工具库,适合做数组、对象的遍历、过滤、分组和转换。

适合场景:

  • 批量处理 groupbypredsquery 中的字段
  • 做简单的数据整理和条件筛选

execute

execute 是 logicform 的执行函数,可以在 enrich 过程中执行一个 logicform,拿到查询结果后再继续改写当前 logicform。

函数特征:

  • 支持 async enrich
  • 适合”先查一遍,再决定怎么改 logicform”的场景

使用时需要注意:

  • 这类写法会在 enrich 阶段额外执行查询
  • 如果逻辑复杂或调用频繁,要注意性能影响

getGranuality

getGranuality 用于判断一个日期 query 的粒度。

函数签名如下:

getGranuality(query: object) => string

典型返回值包括:

  • year
  • quarter
  • month
  • day

适合场景:

  • 先判断用户问的是按年、按月还是按天,再决定 enrich 逻辑

isDimensionInQuery

isDimensionInQuery 用于判断某个 query 对象里是否出现了指定维度。

函数签名如下:

isDimensionInQuery(query: object, dimension: string) => boolean

适合场景:

  • 只想判断某一段 query,而不是整个 logicform
  • 结合 $and$or 结构做更细粒度判断

isDimensionInLogicform

isDimensionInLogicform 用于判断整个 logicform 中是否包含某个维度。

函数签名如下:

isDimensionInLogicform(logicform: object, dimension: string) => boolean

适合场景:

  • 如果用户问到了某个维度,就追加特定业务规则
  • 根据是否存在某个维度,选择不同的查询口径

使用示意:

async (logicform, normedLogicform, helperFunctions) => {
  const { isDimensionInLogicform } = helperFunctions;

  if (isDimensionInLogicform(logicform, '门店')) {
    logicform.query.是否闭店 = false;
  }

  return logicform;
}

getQueryWithKey

getQueryWithKey 用来在当前 logicform 中递归查找所有包含某个字段名的 query 节点。

这个函数适合以下场景:

  • 你需要找到所有包含某个字段的筛选条件,例如 日期
  • 这个字段可能出现在主 querypreds[].queryfrom.query,甚至更深层的 $and$or 条件里
  • 你希望批量修改这些 query,而不是只处理某一个固定位置

函数签名如下:

getQueryWithKey(key: string) => Array<{
  queryFromLogicform: object,
  queryFromNormed: object
}>

参数说明:

  • key: 要查找的字段名,例如 日期

返回值说明:

  • 返回一个数组
  • 每一项都表示一个命中的 query 节点
  • queryFromLogicform: 原始 logicform 中命中的 query 对象,可直接修改
  • queryFromNormed: 对应的规范化 query 对象,适合配合条件判断使用

使用示意:

async (logicform, normedLogicform, helperFunctions) => {
  const { getQueryWithKey } = helperFunctions;
  const timestampQueryValues = getQueryWithKey('日期');

  for (const timestampQuery of timestampQueryValues) {
    const timestampQueryValue = timestampQuery.queryFromLogicform;
    // 在这里直接修改命中的 query
    delete timestampQueryValue.日期;
  }

  return logicform;
}

完整示例可参考财年场景解决方案,其中使用 getQueryWithKey('日期') 批量找到所有日期条件,并改写为财年日历查询。


renamePredItem

renamePredItem 用于重新生成或覆盖某个 predItem 的名称。这个能力通常只在 schema 级 enrich 中可用。

函数签名如下:

renamePredItem(predItem: object, logicformToRename: object, newName?: string) => void

适合场景:

  • 你修改了某个指标或谓词的结构,想同步更新它展示给用户的名称
  • 你新增了衍生 predItem,希望它有可读名称

graph / schemas / customFunctions / locale / schema

这些不是函数,而是 enrich 执行时注入的上下文对象。

常见用途:

  • graph: 读取当前图解析上下文
  • schemas: 获取全部 schema 定义
  • customFunctions: 获取当前自定义函数
  • locale: 根据语言环境做差异化逻辑
  • schema: 获取当前 schema 的定义,只在 schema 级 enrich 中可用

完整使用示例

以下是一个医院满意度调查场景的真实案例,展示了如何根据用户问题的维度组合,动态追加筛选条件。

背景

某医院使用 Data Agent 分析患者满意度数据,其中:

  • 医生 是一个维度字段,用户可以按医生筛选或分组
  • 十分满意度 是一个自定义指标,计算公式为评分≥10 分的问卷数 / 总问卷数
  • 科室 是一个维度字段
  • 问题类型 是一个分类字段,也是本次需要根据不同条件设置不同逻辑的字段

业务需求

  1. 当提问涉及「医生」+「十分满意度」指标时,自动限定数据范围为「问题类型 = 医生」(即只看医生相关的满意度条目)
  2. 当科室为「医美中心」时,问题类型需要进一步细分为「外科医生」或「皮肤科医生」,并覆盖上一条的默认规则

完整代码

(logicform, normedLogicform, helperFunctions) => {

  // ========== 第一步:获取基本信息 ==========

  // 问题中涉及的分组维度列表(如按医生分组看各医生的表现)
  const groupbyList = logicform?.groupby || [];

  // ========== 第二步:判断用户意图 ==========

  // 1. 判断问题中是否筛选了"医生"维度
  //    例如"张三的十分满意度" → logicform.query = { "医生": "张三" }
  const hasDoctorInQuery = !!logicform.query.医生;

  // 2. 判断问题中是否按"医生"分组
  //    例如"各医生的十分满意度" → logicform.groupby = [{ "_id": "医生" }]
  const hasDoctorInGroupby = groupbyList.some(item => item?._id === "医生");

  // 3. 判断问题是否问到了"十分满意度"这个自定义指标
  //    自定义指标在 preds 中用 item.operator 判断
  //    Schema 原生字段用 item.pred 判断,如 item.pred === "amount"
  const hasTargetOperator = logicform.preds.some(
    item => item.operator === "十分满意度"
  );

  // ========== 第三步:根据条件组合追加筛选 ==========

  // 规则1:问到了"十分满意度" AND(筛选了医生 OR 按医生分组)
  //       → 自动限定只看"医生"问题类型的数据
  if (hasTargetOperator && (hasDoctorInQuery || hasDoctorInGroupby)) {
    logicform.query["问题类型"] = "医生";
  }

  // 规则2:科室为"医美中心"时,覆盖规则1
  //       将问题类型从"医生"细化为"外科医生"和"皮肤科医生"
  //       例如"医美中心的医生十分满意度"
  //       → logicform.query.就诊科室.query.科室名称 = "医美中心"
  if (logicform?.query?.就诊科室?.query?.科室名称 === "医美中心") {
    logicform.query["问题类型"] = {
      "$in": ["外科医生", "皮肤科医生"]
    };
  }

  return logicform;
}

效果对照

用户提问触发规则最终追加的筛选条件
「张三的十分满意度」规则1:命中了指标 + 医生筛选"问题类型": "医生"
「各医生的十分满意度」规则1:命中了指标 + 医生分组"问题类型": "医生"
「医美中心各医生的十分满意度」规则2:科室为医美中心,覆盖规则1"问题类型": {"$in": ["外科医生","皮肤科医生"]}
「上个月的患者满意度」无:未命中"十分满意度"指标不追加任何条件

要点总结

核心思路:先判断,再修改

整个 enrich 函数的本质就是 「在什么条件下,修改什么」。写 enrich 之前,先把业务规则转换成下面这个模板:

用户问题中同时出现 A 和 B,追加筛选条件 C;但如果出现了 D,把 C 替换为 E。

上例中:

  • A = 问到了「十分满意度」指标
  • B = 筛选或分组的维度包含「医生」
  • C = 追加 "问题类型": "医生"
  • D = 科室为「医美中心」
  • E = 覆盖为 {"$in": ["外科医生","皮肤科医生"]}

判断维度的三种方式

根据用户问题中维度出现的位置,判断方式不同:

维度出现位置示例问题logicform 中的形态判断代码
筛选条件(query)「张三的十分满意度」logicform.query = { "医生": "张三" }!!logicform.query.医生
分组维度(groupby)「各医生的十分满意度」logicform.groupby = [{ "_id": "医生" }]groupbyList.some(item => item?._id === "医生")
指标名称(preds)「十分满意度是多少」logicform.preds = [{ "operator": "十分满意度" }]logicform.preds.some(item => item.operator === "十分满意度")
注意

item.operator 判断自定义指标,用 item.pred 判断 Schema 原生字段(如 item.pred === "amount")。两者不可混用。

理解"何时触发修改"

enrich 的核心价值在于只在特定条件下才干预 logicform,而不是对所有问题都无差别修改。判断条件可以从以下角度考虑:

  1. 指标条件 — 只有问到了特定指标才触发(如上例中的「十分满意度」)。通过 logicform.preds 判断,避免影响无关问题的查询结果。

  2. 维度条件 — 只有涉及特定维度才触发(如上例中的「医生」)。可以同时检查 query 和 groupby,确保不遗漏「按某维度分组」这种问法。

  3. 值条件 — 当某个字段的值为特定内容时才触发(如上例中科室名称 == "医美中心")。通过访问深层嵌套的 query 结构来判断。

  4. 组合条件 — 用 &&(且)和 ||(或)组合以上条件,精确控制触发范围。例如上例的:

    hasTargetOperator && (hasDoctorInQuery || hasDoctorInGroupby)

    含义:指标必须匹配,且维度在筛选或分组中至少命中一个

理解"修改的优先级"

当一条 enrich 函数里有多条规则时,代码的书写顺序决定优先级

规则1: 打标记 → 规则2: 如果满足更特殊条件就覆盖标记 → 规则3: ...

上例中规则2在规则1之后,因此当科室 = 医美中心时,规则2的赋值会直接覆盖规则1的赋值。如果反过来写,规则1就会错误地覆盖规则2的细分结果。

实际应用建议

  • 先写通用规则(覆盖面广的),再写特殊规则(覆盖面窄但优先级高的)
  • 每条规则用注释标明「什么条件下触发」,方便后续维护
  • $in 运算符表示「多选一」,比写多个 $or 更简洁