第二部 原理篇:规则驱动的智能体设计

第 9 章:意图识别与路由:可信的语义分派

9.1 工程问题的本质:语义分派的失败成本

路由解决"这条输入该由谁处理"。语义分派的失败成本并不均衡:把"查库存"路由到下单,会把一次查询变成一次业务操作;把"下单"路由到知识问答,则业务请求悬空。分派错误的代价远高于分派延迟的代价——这一不对称性决定了路由设计的优先级排序。

设计原理: 路由必须同时满足三个性质——可训练(分派规则随业务演化)、可降级(任何输入都有出口)、可审计(分派结果可追溯)。三者缺一,路由就会成为系统中"说不清为什么走这条路"的黑洞。

9.2 上下文隔离:分派的正确性基础

路由正确性的基础是上下文隔离:每个智能体只能看到它该看到的数据。深拷贝保证无共享可变状态;按目标智能体类型裁剪敏感字段(非财务智能体不看到财务明细、非内审智能体不看到内审数据),实现最小权限可见。

原理: 上下文隔离不是"防御性编程",而是信任边界的实现。协作系统的信任不能建立在"每个智能体都很自觉"上,而必须建立在"结构上拿不到"上。隔离之后的显式注入(intent / slots / 流程实例)则是信任的授出——授出哪些、何时授出,由路由层统一决定。

9.3 路由表:可训练与可降级

路由决策分三层:DB 规则路由(intent_rules 表,target_agent 可训练)→ 内置映射(降级)→ 正则表(再降级)。

原理: 三层结构回答了两个问题。可训练:业务调整路由时改库即可,训练后 reload 生效,无需发版;可降级:数据库不可用时逐级回退,语义层永不因依赖缺失而失去出口。路由本身的治理(谁改、改什么)复用于规则审批链,与第 14 章的配置治理一致。

9.4 识别链路:规则优先、模型兜底

意图识别采用四阶段链路:

输入归一化 → DB 高优先级训练规则 → 确定性短语白名单 → LLM+训练样本 → 正则兜底

原理: 四阶段是按"确定性递减、泛化性递增"排列的。规则层命中是零延迟、零成本、完全确定的事实;模型层命中是秒级、消耗 token、有随机性的推断。将规则层置于模型层之前,不是轻视模型,而是让确定的东西先说话——只有当确定的手段无法裁决时,才把不确定性引入。

这一顺序还保证了训练修正的即时性:DB 高优先级规则(priority<10)位于链路第一步,用户训练的正确意图永远优先于白名单与模型。学习的成果(第 14 章)因此能立刻改变行为。

9.5 槽位与参数:事实提取的确定性

意图之后的参数提取同样遵循"确定性优先":槽位定义(正则、排除规则、单位换算、值映射)存于可训练配置,intent 层与智能体层共用同一提取链路,避免双份正则漂移。查询附加词("库存大于 100""近 7 天")由参数解析器提取为结构化条件,规则无法解析的模糊片段才交模型补全,且字段名与操作符均经白名单校验。

原理: 参数是业务事实,事实提取不容许"这次提取到、下次提取不到"的随机性。确定性提取 + 白名单校验,使"用户说什么"到"系统执行什么"的转换链条中,只有语义理解环节允许概率性——参数本身必须是确定的事实。

9.6 消歧:业务策略的编码

多规则命中时按语义优先级链裁决(质量动作优先于质量查询、动作意图优先于查询意图等)。消歧规则集的顺序本身就是业务策略的编码。

原理: 消歧不是技术细节,而是业务风险偏好的形式化。当两条规则冲突时,先满足谁,反映系统对两类错误(漏执行 vs 误执行)的不同容忍度。动作优先于查询,意味着"该执行的操作不能被查询词吞掉"比"查询被误判为操作"更值得避免。

9.7 识别质量度量与经典理论对照

9.7.1 识别质量的度量体系

识别器与路由的质量必须可度量,否则优化无从谈起。四类指标构成最小评估集,见表 9-1:

表 9-1 识别质量的度量指标

指标 定义 优化手段
意图识别准确率 命中意图 / 总输入 规则补全、few-shot 增强、训练样本
路由正确率 到达正确 Agent / 总输入 消歧规则、DB 路由训练
规则命中率 规则层命中 / 总输入 高频句式规则化(零延迟收益)
参数提取召回 正确提取槽位 / 应提取槽位 槽位定义、排除规则、单位换算

原理: 度量驱动的改进遵循"先度量、再归因、后优化"——一次误路由的归因链是:输入 → 识别(哪一层命中)→ 消歧(哪条规则优先)→ 路由(哪张表)→ 执行。任一环都可能出错,度量必须能区分"识别错了"与"路由错了",否则优化会打在错误的环节上。

9.7.2 冲突消解策略:经典规则引擎理论的对照

多规则命中时的消歧,与经典规则引擎(如基于 RETE 算法的产生式系统)的冲突消解策略(conflict resolution strategy)是同构问题。经典策略有三类,本框架的消歧链可对照其中两类,见表 9-2:

表 9-2 经典冲突消解策略与框架对应

经典策略 语义 框架中的对应
特殊性优先(specificity) 条件更具体的规则优先 训练规则 priority<10 高于内置规则
新鲜度优先(recency) 最近更新的规则优先 训练数据动态白名单(新样本优先收录)
显著性优先(salience) 显式声明的优先级 消歧链规则 1~9 的显式顺序

对照价值: 这一对照给出一个设计检验:规则冲突的解决必须是确定性、可解释、可训练的。确定性(同样的输入永远同样的结果)靠固定优先级链;可解释性靠"命中哪条规则、为何优先"可追溯;可训练性靠优先级与分类集合存配置(DISAMBIG-CFG)。三者缺一,消歧就会退化为"说不清为什么"的黑盒。

9.7.3 few-shot 与结构化输出的机理

模型识别层以 few-shot + 函数调用(function calling)产出结构化意图。其可靠性来源有三层,缺一不可:

  1. schema 约束:模型被要求输出符合意图 schema 的结构化结果,格式错误即失败——结构化输出把"自由文本"的风险前移到解析层;
  2. 白名单校验:解析结果经已知意图白名单校验,意图名不在名单内一律降级 unknown——模型"发明意图"被拒绝;
  3. 参数合并:规则提取的参数优先,模型提取的补缺失字段——模型不覆盖确定性事实。

原理: 结构化输出的可靠性是"约束叠加"而非"模型自觉":schema 约束格式、白名单约束内容、合并约束事实。每一层都是"模型可能错,但结构上错不出去"的实现。

9.8 失败模式与权衡

表 9-3 意图识别的失败模式与权衡

失败形态 根因 设计应对
查询被路由成操作 查询词触发动作意图 动作/查询分类集合可训练(DISAMBIG-CFG)
主谓宾歧义误判 子串匹配("查看下订单"含"下订单") 白名单整句精确匹配,歧义保留给模型
识别延迟拖垮会话 每轮都走强模型 规则预检 + 多轮补充信息零 LLM(见 17.3 节)
跨智能体数据泄露 共享上下文 深拷贝 + 敏感字段按类型裁剪

权衡: 规则优先牺牲了泛化能力(新颖表达须走模型),换取延迟、成本与确定性;可训练的消歧集合在"稳定"与"灵活"之间给了业务方调节权——默认行为保守(规则优先),业务需要时训练放开。

9.9 实现剖析:上下文隔离、指代消解与参数确定性

本节从 runtime/coordinator.py(4,262 行)与 runtime/intent_recognition.py(3,861 行)摘取三段,印证 9.2 节的上下文隔离、9.2 节的指代消解与 9.5 节的参数提取确定性。

9.9.1 上下文隔离:深拷贝 + 按 Agent 类型裁剪

def _isolate_context(self, user_context):
    """为目标 Agent 构造隔离的上下文副本(深拷贝并裁剪敏感字段)"""
    import copy
    isolated = copy.deepcopy(user_context) if user_context else {}   # ★ 无共享可变状态

    isolated.setdefault("user", {})
    isolated.setdefault("history", [])
    isolated.setdefault("slots", {})
    isolated.setdefault("data", {})

    agent_type = isolated.get("agent_type", "")
    if agent_type != "audit":        # 非内审 Agent 不应看到内审数据
        isolated.pop("audit_data", None)
    if agent_type != "finance":      # 非财务 Agent 不应看到财务明细
        isolated.pop("financial_data", None)
    return isolated

这段代码印证 9.2 节:隔离靠"结构上拿不到",而非"自觉不去看"。

copy.deepcopy 切断共享可变状态,杜绝"A 智能体改了上下文、B 智能体看到脏数据";按 agent_type 裁剪则是最小权限可见的实现——audit_data 只对 audit 可见、financial_data 只对 finance 可见。裁剪发生在分发之前,因此下游智能体即使被提示注入诱导,也拿不到无权字段。

9.9.2 指代消解:把"它/那个"还原成上一轮的实体

def _resolve_pronouns(self, user_input, session_context):
    """多轮指代消解:检测代词并用会话上下文中的实体替换"""
    if not session_context or not isinstance(session_context, dict):
        return user_input

    pronouns = ["它", "那个", "这个", "上面的", "刚才的", "之前那个", "该"]
    if not any(p in user_input for p in pronouns):
        return user_input                       # 无代词,原样返回

    slots = session_context.get("slots", {})
    if not slots:                               # 回退:从历史轮次里取最近实体
        for turn in reversed(session_context.get("history", [])):
            if isinstance(turn, dict) and turn.get("slots"):
                slots = turn["slots"]
                break
    if not slots:
        return user_input

    replacements = []
    for key in ("product_code", "order_id", "work_order_id", "po_id",
                "customer_name", "supplier"):   # ★ 实体优先级固定
        if slots.get(key):
            replacements.append((key, str(slots[key])))
    if not replacements:
        return user_input

    resolved = user_input
    for pronoun in pronouns:
        if pronoun in resolved:
            resolved = resolved.replace(pronoun, replacements[0][1], 1)
            break                               # 只替换第一个匹配的代词
    return resolved

这段代码印证 9.2 节与第 10 章的交界:指代消解是"上下文隔离"之后的一次显式授出。隔离保证智能体拿不到多余数据;消解则把上一轮槽位中的实体,按固定优先级(产品 > 订单 > 工单 > 采购单 > 客户 > 供应商)注入本轮输入。优先级的固定是关键——否则"它"指向哪个实体将随会话状态漂移,路由结果不可复现。代词表与优先级都是确定性规则,不含概率成分。

9.9.3 参数确定性:合并优先级 + exclude_regexes 后过滤

@staticmethod
def _merge_params(base, extra):
    """合并参数槽位:base 优先,extra 仅补充 base 缺失的键"""
    merged = dict(base or {})
    for k, v in (extra or {}).items():
        if v is not None and merged.get(k) is None:   # None 不覆盖已有值
            merged[k] = v
    return merged


@staticmethod
def _filter_by_exclude_regexes(params):
    """用 SLOT-DEFS.exclude_regexes 过滤合并后的槽位值"""
    defs = get_slot_defs(use_cache=True)      # 槽位定义存 DB,可训练热生效
    filtered = {}
    for k, v in params.items():
        if v is None:
            continue
        excludes = (defs.get(k) or {}).get("exclude_regexes") or []
        if not excludes:
            filtered[k] = v
            continue
        if not any(re.search(pat, str(v)) for pat in excludes):
            filtered[k] = v                   # ★ 命中排除规则的槽位被丢弃
    return filtered

这段代码印证 9.5 节:参数提取的确定性由"明确的合并优先级 + 可训练的排除规则"共同保证。_merge_params 定义"谁覆盖谁"——调用约定为 LLM 结构化输出优先、正则提取补缺,且 None 不覆盖已有值(避免 LLM 显式 null 抹掉正则结果)。

_filter_by_exclude_regexes 则解决"属性定语被误当实体名"的问题(如"状态为待发布"被当作 product_name):无论值来自 LLM 还是正则,只要命中该槽位的 exclude_regexes 即丢弃。

关键是 exclude_regexes 存于 SLOT-DEFS(数据库、可训练),因此口径修正不需要改代码,与第 14 章的配置治理一脉相承。

9.10 本章小结