Files
ai-device/docs/nlu_integration_design.md
2026-06-11 16:28:00 +08:00

12 KiB
Raw Blame History

NLU 接入设计方案

状态:已确认,进入实现阶段 关联文档:bert_integration_analysis.mdarchitecture_overview.html


第一部分:概念解释 — 两套术语怎么对应

1.1 Canvas 设计里用的词

architecture_overview.html 的技术流程视图里BERT NLU 节点描述为:

BERT NLU 意图识别
输出 domain / intent / slot / confidence 置信度

这是一套面向业务语义的描述,每个词的含义:

含义 例子
domain 所属业务域,意图的分组 machine_controlequipment_knowledgesmalltalk
intent 用户想做什么,域内的细分动作 wirecut_set_speedwirecut_start_runquery_alarm
slot 动作的具体参数,从句子里提取的关键值 speed=80voltage=90axis=X
confidence 模型对这次识别结果的置信程度0~1 0.94(高)、0.61(中)、0.32(低)

Canvas 的路由逻辑就是:拿到这四个值之后,判断 confidence ≥ 阈值 AND domain = machine_control → 走工具调用路径。


1.2 intelligent_cabin NLU 服务里用的词

intelligent_cabin 后端分两层输出:

层 AJointBertNLU 的原始输出(joint_nlu.py

@dataclass
class JointNluResult:
    intent_id: str | None       # 识别出的意图 id如 "wirecut_set_speed"
    intent_score: float          # softmax 后的概率就是置信度0~1
    candidates: list[JointCandidate]  # Top-K 候选意图及其概率
    slots: dict[str, Any]        # 从句子里提取的 slot如 {"speed": 80}
    slot_items: list[JointSlot]  # slot 在原文中的精确位置和得分

这里的 intent_id + intent_score + slots 对应 Canvas 描述里的 intent + confidence + slotdomain 不是模型直接输出的,而是根据 intent_iddomain.yml 里查到的(wirecut_set_speed → domain machine_control)。

层 BRouter / FusionGrader 的决策输出(router.py

# MultiStageIntentMatcher._build_fusion_stage() 里
decision = "execute" | "clarify" | "route_to_cloud" | "reject"

这是在原始 NLU 结果基础上做的二次路由判断,加入了:

  • 置信度是否够高(score ≥ execute_score_threshold=0.55
  • 头两名候选的分差是否足够大(margin ≥ execute_margin_threshold=0.18
  • 是否有多义性ambiguous

它告诉上层"这个识别结果能不能直接执行",而不只是"模型认为是哪个意图"。


1.3 两套词汇的完整映射关系

Canvas 的描述            ←→    intelligent_cabin 的实际字段
─────────────────────────────────────────────────────────────
domain                    ←→    intent_def.domain  (从 domain.yml 查)
intent                    ←→    JointNluResult.intent_id
slot                      ←→    JointNluResult.slots  (dict)
confidence                ←→    JointNluResult.intent_score  (0~1)

(以上是 NLU 层的概念对应)

Canvas 的路由逻辑          ←→    Router 层的 decision 字段
"高置信 + 设备控制域"      ←→    decision="execute" AND domain="machine_control"
"知识域/低置信兜底"        ←→    decision="route_to_cloud" 或 domain="equipment_knowledge"
"smalltalk"               ←→    decision="reject" 或 social_router 处理

关键点Canvas 当前用 Mock NLUsrc/lib/nlu/mock.ts),它直接输出 domain + intent + confidence + routeHint。 接入真实 NLU 后,两个项目原生打通,当成一个项目,不做兼容适配层,接口可以随时改。


第二部分Canvas ↔ NLU 服务的统一路由方案

2.1 两个项目合并为一个项目(已确认)

intelligent_cabin 不作为独立服务做适配,而是直接作为 ai-canvas 的后端子模块。 原来 src/lib/nlu/mock.ts 的格式可以废弃,不需要保持向后兼容。

2.2 真实 NLU 服务的 HTTP 响应

调用 POST /api/v1/agent/chat 后,服务返回 ChatResponse,与路由相关的核心字段:

{
  "session_id": "xxx",
  "intent": "wirecut_set_speed",
  "domain": "machine_control",
  "decision": "execute",
  "status": "completed",
  "filled_slots": { "speed": 80 },
  "routing_debug": {
    "confidence_grade": "high",
    "stages": [
      { "stage": "classifier", "score": 0.87, "candidates": [...] },
      {
        "stage": "fusion",
        "metadata": { "decision": "execute", "grade": "high",
                      "classifier_signal": 0.87, "classifier_margin": 0.34 }
      }
    ]
  }
}

domain 字段需要在 intelligent_cabin 的 ChatResponse schema 里加上(从 IntentDefinition.domain 填充),改动极小。

2.3 Canvas 侧的 NluResult 类型(替换 mock.ts

// src/lib/nlu/types.ts  (新建,替换 mock.ts 里的类型)

export type RouteHint =
  | "tool_call"       // decision=execute + machine_control 域
  | "knowledge_query" // decision=route_to_cloud 或 equipment_knowledge 域
  | "smalltalk"       // decision=reject
  | "fallback";

export type NluResult = {
  modelVersion: string;
  domain: string;                                        // 后端直接返回
  intent: string;                                        // intent_id
  confidence: number;                                    // classifier stage score
  slots: Record<string, string | number | boolean>;      // filled_slots
  routeHint: RouteHint;
  decisionGrade: "high" | "medium" | "low";
  rawDecision: "execute" | "clarify" | "route_to_cloud" | "reject";
};

export function mapDecisionToRouteHint(
  decision: string,
  domain: string
): RouteHint {
  if (decision === "execute") {
    if (domain === "machine_control") return "tool_call";
    if (domain === "equipment_knowledge") return "knowledge_query";
    return "tool_call";
  }
  if (decision === "route_to_cloud") return "knowledge_query";
  if (decision === "reject") return "smalltalk";
  return "fallback"; // clarify 等待补槽,暂用 fallback
}

2.4 confidence 读取位置

routing_debug.stages 里找 stage === "classifier" 的记录,取其 score 字段。 这是 BERT 分类器 softmax 后的原始概率,等价于 Canvas 描述里的 confidence


第三部分:语音处理前置拦截链路(已定稿)

3.1 设计原则

  • 所有当前可见 UI 组件的按钮文本都参与语音匹配,命中则直接触发点击事件,不调 BERT
  • 调机流程GuidedProcedure当前不在实现范围,相关 1c 拦截逻辑暂不涉及
  • BERT 报错直接抛出,不降级,不用 Mock 兜底

3.2 四阶段处理链路

ASR 文本输入
    │
    ▼
[阶段 0] 停止词检测                          ← 静态词表,构建时嵌入
    ├── 命中 cancel_words → 生成 stop_action链路终止
    └── 未命中 ↓
    │
    ▼
[阶段 1] UI 可见元素语音点击匹配              ← 纯前端规则,<1ms
    ├── 1a. session.status=waiting_confirmation 时的 affirm/deny最高优先级
    └── 1b. 当前可见 Artifact 按钮 text 匹配
    │   命中任意 → 生成 ActionEvent走 Canvas 状态机,链路终止
    │
    │ 全部未命中
    ▼
[阶段 1.5] waiting_slot + inform 检测
    ├── session.status=waiting_slot && 输入为数字/数值类
    └── 命中 → 调 fill_slots 接口,链路终止
    │
    ▼
[阶段 2] BERT NLUintelligent_cabin /api/v1/agent/chat
    ├── 报错 → 直接抛出,不降级
    ├── decision=execute → 工具调用层DBus→ Artifact
    ├── decision=clarify → 渲染补槽卡,等待 waiting_slot
    ├── decision=route_to_cloud → LLM + 知识库 → KnowledgeLessonArtifact
    └── decision=reject → LLM 直接作答,不写 ArtifactStore

3.3 阶段 1 内部优先级说明

// 优先级从高到低1c 调机 textAliases 暂不实现)
const PRIORITY_ORDER = [
  "waiting_confirmation_affirm_deny",  // 1a
  "visible_artifact_button",           // 1b
];

为什么 1a 最高:当高风险操作(如"开始加工")弹出确认卡时, 操作员说"确认"应当触发确认动作,而不是响应画布上同时存在的其他按钮。 状态(session.status)决定优先级,而非文本本身。

3.4 阶段 1 匹配实现pipeline.ts 骨架)

// src/lib/nlu/pipeline.ts

import { AFFIRM_WORDS, CANCEL_WORDS } from "./voice-aliases.gen"; // 构建时生成

type ActionEvent = {
  type: "voice_click_event" | "slot_fill_event" | "stop_action";
  actionId?: string;
  artifactId?: string;
  sourceText: string;
};

export async function processVoiceInput(
  asrText: string,
  session: CanvasSession
): Promise<NluResult | ActionEvent> {

  // 阶段 0停止词
  const norm = normalizeVoice(asrText);
  if (CANCEL_WORDS.some(w => norm.includes(w))) {
    return { type: "stop_action", sourceText: asrText };
  }

  // 阶段 1awaiting_confirmation 状态的 affirm/deny
  if (session.status === "waiting_confirmation") {
    if (AFFIRM_WORDS.some(w => norm.includes(w))) {
      return { type: "voice_click_event", actionId: "confirm", sourceText: asrText };
    }
    if (CANCEL_WORDS.some(w => norm.includes(w))) {
      return { type: "voice_click_event", actionId: "cancel", sourceText: asrText };
    }
  }

  // 阶段 1b当前 Artifact 按钮匹配
  const voiceClick = matchVoiceToAction(asrText, session.visibleActions);
  if (voiceClick) {
    return {
      type: "voice_click_event",
      actionId: voiceClick.actionId,
      artifactId: voiceClick.artifactId,
      sourceText: asrText,
    };
  }

  // 阶段 1.5waiting_slot + 数值输入
  if (session.status === "waiting_slot" && isNumericInput(asrText)) {
    return { type: "slot_fill_event", sourceText: asrText };
  }

  // 阶段 2BERT NLU报错直接抛出
  const response = await callNluService(asrText, session.sessionId);
  return adaptNluResponse(response);
}

3.5 voice_aliases 配置(已确认:静态构建)

词表位置intelligent_cabin/config/voice_aliases.yml(和 dialog_acts.yml 放在一起)

# voice_aliases.yml
affirm_words: ["确认", "好的", "执行", "是", "对", "继续", "好", "ok"]
cancel_words: ["取消", "算了", "不要", "不用", "停止", "停"]

# 工控设备别名(按 intent_id 分组,用于阶段 1b 的 Artifact voiceActions
intent_aliases:
  wirecut_start_run:  ["开始", "启动", "加工", "跑起来"]
  wirecut_stop_run:   ["停", "停机", "急停", "停止"]
  wirecut_home_all:   ["回零", "归零", "回原点"]
  wirecut_pause_run:  ["暂停", "变频暂停"]

构建时生成:构建脚本读取 yml → 生成 src/lib/nlu/voice-aliases.gen.ts TypeScript 侧直接 import不需要运行时 HTTP 请求。


第四部分:下一步实现计划

步骤 位置 内容
1 intelligent_cabin Python 侧 ChatResponse schema 加 domain 字段,agent_service.py 填充
2 intelligent_cabin/config/ 创建 voice_aliases.yml,补充工控别名
3 src/lib/nlu/ 新建 types.ts,废弃 mock.ts 中旧类型
4 src/lib/nlu/ 新建 pipeline.ts,实现四阶段处理链路
5 src/lib/artifacts/types.ts 各 Artifact 类型上加 voiceActions 字段
6 构建配置 添加 yml → ts 生成脚本(voice-aliases.gen.ts