一张路由表少写一行,装好的 tri-loop 就成了死 skill:我把 tri-intent 的 27 个落点扒了一遍

0 阅读10分钟

先把话撂这儿:Agent skill 生态里最贵的一类 bug,不在任何一个 skill 自己的执行逻辑里,而在上游那张路由表少写的一行。代码全对、目录结构没毛病、用户也老老实实把 skill 装上了,它就是不动弹——因为压根没人给它派活。

这不是我瞎编的架构洁癖。tri-loop 的激活条件白纸黑字写在 doing/loop-design.md 里,三条,缺一不可:

# tri-loop 激活条件(下游 skill 侧校验)
intent.L2_核心意图: I14
下游路由建议: tri-loop
任务要点:  loop/domain 创建语义

三者同时满足才启动,L2 越界就停下来回退给 tri-intent 重新路由。设计得挺严谨。问题在于——上游那阵子根本不会产出第二条。第二个条件恒为假,后面两条对得再准也白搭。

要讲明白这事儿怎么发生的,得先把 tri-intent 这个东西铺开说。

它不是一个干活的 skill,是个收费站

大部分人写 skill 的思路是「这个 skill 能帮我做什么」。tri-intent 反着来,它啥也不做。SKILL.md 的执行契约里第四条写得很硬:

本 skill 仅负责「识别意图分类(L1/L2)→ 标注正交维度(D1–D5)→ 需求模糊时主动澄清 → 产出快照 → 交接下游 skill」。设计、拆解、执行、测试、审批等一切下游职责均不属于本 skill。

还专门列了禁止清单——不准产出 requirements.md、design.md、tasks.md、implements.md。我个人挺吃这一套。见过太多 skill 写着写着就膨胀成万能工具,一个 skill 里塞需求分析塞代码生成塞测试报告,到头来谁都不知道它到底该在什么时候被激活。tri-intent 把自己钉死在「分类」这一件事上,反而让整条链路的边界变得可推理。

它的定位类似高速收费站:任何一次用户提问进来,必须先过站,打上标签,再放行到对应的匝道。

三分法先切一刀,再切 27 刀

第一层是四选一,依据 OpenAI《How People Use ChatGPT》的三分法加了个 Meta 补全:

大类用户心智占比
A. Asking 咨询求解把 AI 当顾问~49%
B. Doing 委托执行把 AI 当执行者~40%
C. Expressing 表达陪伴把 AI 当倾听者~11%
Meta 元操作针对上一轮回复本身边缘补全

判定主键只有一个——核心动词。「想让 AI 做什么动作」,抽出来就是分类键。这条约束是 MECE 能立住的关键,因为一旦允许多主键判定,类别重叠就是必然的。

第二层下钻到 27 个落点:Asking 下面 I01–I05,Doing 下面 I06–I16 再加个 I21 蒸馏造物和 1 个代码审查特殊路由,Expressing 是 I17–I20,Meta 是 M01–M05(I01–I21 共 21 个 I 类 + 代码审查 1 个 + M 类 5 个 = 27)。说实话第一眼看到这个数量我是嫌多的,二十几个分类谁记得住。但真去读 doing/SKILL.md 的消歧要点,会发现它切得挺克制:

有没有原文,无就是 I06 生成,有就分 I07 改写(同语言)和 I08 翻译(跨语言格式)。代码类四分——新增 I11、修故障 I12、只出方案 I13、真跑起来 I14。压缩和洞察也分开,只提要点归 I09,要评判结论归 I10。

每一刀都对应一个真实存在的执行差异,不是为了凑数。

兜底也想过了。I19 闲聊娱乐是 Expressing 的兜底项,无法归入任何 I/M 的输入都落这儿,保证零覆盖空白。还有条我觉得挺聪明的降级规则:判为 Doing 但核心动作过于泛化(「帮我搞定那件事」「搞一下」),映射不到任何具体产出动作时,降级归 I03 建议咨询,再强制走澄清门。而且它明确了降级判定的唯一标准是「动作能否映射」,不是「对象是否缺失」——「优化一下」这种动作明确对象缺失的,不降级,直接补齐对象就行。这个边界不写清楚,实际跑起来一半的输入都会被误降级。

快照是它唯一的产出

识别完了怎么交接?不靠上下文,不靠对话记忆,靠一个落盘文件。

路径是 .tribro/snapshots/<问题类型>_<日期>_<时间>_<会话ID>.md,会话 ID 取 chat_session_id 前 8 位,用来区分同一秒内的多次提问。永不覆盖。

快照分三段:用户原始提问原文(逐字粘贴不加工)、意图分析过程(L1/L2/D1–D5 逐步推导)、结构化结论。前两段给人看,用来溯源;第三段给机器读,下游 skill 直接执行,不用重新识别一遍意图。

拿刚才那个 loop 场景举例,真实产出的 §三 长这样:

一句话复述: 你想让我在知识库里建一个每周自动跑的 research loop

intent:
  L1_交互类型: B.Doing
  L2_核心意图: I14 操作执行(loop/domain 创建子类)
  辅助意图: [I13]

dimensions:
  D1_任务领域: 编程
  D2_输入形态: 
  D3_交互轮次: 长程Agent
  D4_输出期望: 文件产物
  D4_格式约束: 
  D5_确定性: 明确

任务要点:
  - 在知识库中创建一个 research loop
  - cadence 为每周一次
  - 需要 charter 收集与 README scaffold
  - 建成后执行一次真实测试运行

澄清门状态: 已跳过(需求清晰)
交付预期: 一个可验证运行的知识库 loop,附 Timeline  LOG.md 记录
下游路由建议: tri-loop

注意 任务要点 里那几个词——loop、cadence、charter、scaffold。loop-design.md 明确要求把这些语义关键词保留在快照里,因为 tri-loop 激活时要拿它们做校验。上游写快照的时候顺手保留,下游才能自证「这活确实是派给我的」。

那个恒为假的条件

事故点在这儿。

修复前,路由映射表里 I14 只有一行:I14 → tri-action。意图判定完,下游路由建议 一律填 tri-action。哪怕用户说的是「帮我在知识库起一个 monitoring loop」,L2 判 I14 没错,任务要点里 loop、charter 全在,但第二条——下游路由建议: tri-loop——上游根本不产出这个值。

tri-loop 的三个激活条件,第二个永远为假。

于是就出现了那个最让人上火的现象:skill 装了,目录在,SKILL.md 能读,双向检测也不报错(因为它确实装了),但你怎么提问它都不激活。我排查这玩意儿花了大半天,一开始盯着 tri-loop 自己的激活逻辑翻来覆去看,觉得肯定是校验条件写错了。翻到第三遍才反应过来——问题不在这边,是上游压根没往这个方向发过一次货。

死 skill。装了等于没装。

修一行的事,但得修对地方

修法是加一个子类路由项,也就是现在的 doing/loop-design.md。关键在于它不动 L2。

L2 还是 I14,只覆写 下游路由建议。因为 loop 创建在语义上确实是「操作执行」,硬给它开一个新的 L2 编码会破坏 MECE——它和 I14 不是并列关系,是包含关系。同理,工作流设计子类(tri-workflow)也是这个套路,I13/I14 保持不变,只改路由建议。

判定逻辑落成代码大概是这样:

#!/usr/bin/env python3
"""tri-intent 子类路由覆写 + 下游安装检测(可直接运行)"""
from pathlib import Path

DEFAULT_ROUTE = {
    "I11": "tri-coding", "I12": "tri-fix", "I13": "tri-plan",
    "I14": "tri-action", "I15": "tri-mm",  "I16": "tri-bs",
    "I21": "tri-god",
}

LOOP_KEYWORDS = {"loop", "domain", "循环", "知识库",
                 "beat", "workstream", "charter", "cadence"}
WORKFLOW_KEYWORDS = {"工作流", "流水线", "审批流", "自动化流程",
                     "CI-CD", "编排", "pipeline"}


def route(l2: str, task_points: list[str]) -> str:
    """按 L2 取默认路由,再按任务要点语义做子类覆写"""
    downstream = DEFAULT_ROUTE.get(l2, "tri-ask")
    blob = " ".join(task_points).lower()

    if l2 == "I14" and any(k.lower() in blob for k in LOOP_KEYWORDS):
        return "tri-loop"
    if l2 in ("I13", "I14") and any(k.lower() in blob for k in WORKFLOW_KEYWORDS):
        return "tri-workflow"
    return downstream


def is_installed(slug: str, skills_dir: str = ".") -> bool:
    return (Path(skills_dir) / slug / "SKILL.md").is_file()


if __name__ == "__main__":
    cases = [
        ("I14", ["在知识库中创建一个 research loop", "cadence 为每周一次"]),
        ("I14", ["把生产环境的缓存清一遍"]),
        ("I13", ["设计一条 CI-CD 流水线"]),
    ]
    for l2, points in cases:
        target = route(l2, points)
        state = "已安装" if is_installed(target) else "未安装-需提示"
        print(f"{l2:4} -> {target:12} [{state}]  {points[0]}")

跑出来:

I14  -> tri-loop     [未安装-需提示]  在知识库中创建一个 research loop
I14  -> tri-action   [未安装-需提示]  把生产环境的缓存清一遍
I13  -> tri-workflow [未安装-需提示]  设计一条 CI-CD 流水线

同样是 I14,任务要点一变,落点就换了。这才是子类路由该有的样子。

双向检测能抓什么,抓不住什么

tri-intent 有个我觉得设计得挺到位的机制:产出快照后,MUST 检测 下游路由建议 指向的 skill 装没装,没装就给出安装引导。下游那 11 个 skill 反过来也会在激活时检测上游 tri-intent 在不在,高频的还支持降级模式跑。两头对着测,无论用户先装哪一端,缺的另一端都会被逮住。

只对「落盘快照」类意图做检测,Expressing 和 M05 这些不落盘的直接跳过——没有下游要交接,检测个啥。这个剪枝也挺干净。

但这机制有个天然盲区,正是我踩的那个坑:它只能回答「路由建议指向的 skill 装了没」,回答不了「路由建议本身对不对」。路由表少写一行的时候,建议值填的是 tri-action,检测器一看 tri-action 装着呢,一路绿灯放行。全链路没有任何一个环节会告诉你,有个 skill 正躺在目录里等一个永远不会来的快照。

要真想兜住这类问题,靠运行时检测没戏,得靠测试用例——每新增一个下游 skill,就在 tests/ 里补一条「什么样的输入应该路由到它」的断言。这活儿没有银弹,就是笨办法。

路由表这东西平时最不起眼,写的时候觉得就是几行映射,出了事才发现它是整条链路唯一的单点。

顺带说一句,我自己那个叫雷达鸭的 App——收录中国一人公司和超级个体真实赚钱案例的那个——内容更新链路也是这套快照交接跑的,鸿蒙版在华为应用市场,微信小程序同步上了。这次的坑就是在给它加自动化 loop 的时候撞上的。


关于我

我是老三,写了十来年代码,软件设计师、人工智能应用工程师,主职鸿蒙 ArkTS 北向开发和 Web 前端。业余折腾 AI 自动化,偶尔在 CSDN 记点鸿蒙和 AI 方向的东西。

本文遵循 MIT 协议,转载请注明出处。

安装

请根据 https://skillhub.cn/install/skillhub.md,安装 tri-intent。