先把话撂这儿: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。