主动性 Agent 工程周 · Day 2(周二)。昨天拆了 dsh-superagent 的整体架构,今天进入最容易被做歪的一环:一条环境事件从进来到"开工"或"闭嘴",中间到底发生了什么。
本文含一个 80 行、纯标准库、可离线复跑的两级过滤模拟器,文末输出经实际运行验证。
一、为什么不能每条消息都过模型
接入真实群聊和代码平台后,事件流有个残酷特征:高噪音、低触发率。一个研发群一天几百条消息,闲聊占九成以上;真正该让 Agent 动手的触发,一天可能一两次。
如果每条消息都调 LLM 从头判断"要不要动",延迟和成本都不可接受。DeepSeek 在《Super-agent 设计探索》里搬来了计算机系统的经典思路 Fast Path / Slow Path:
- Fast Path(程序层策略):把明确、稳定的高频判断固化成确定性代码——事件来源、群组范围、用户/Bot 白名单,直接代码准入检查。毫秒级、零模型成本,在调模型前滤掉大部分噪音。
- Slow Path(语义层策略):通过程序层准入的候选事件,才交给 LLM,按提示词里的策略判断"随口吐槽还是真问题"。
工程上的好处很实际:Fast Path 改代码,Slow Path 改提示词。想调整语义策略,改一句话就行,不用发版。
二、准入矩阵:连"谁有资格 @ 它"都是策略
程序层不只是关键词过滤。dsh-superagent 的授权文档把"谁能触发它"写成三列映射(按官方 docs/002-authorization.md):
| 来源 | 默认情况 | 特殊情况 |
|---|---|---|
| 名单内用户 | 不 @ 时进入推断候选 | @ 则算正式请求 |
| 陌生人(群聊) | @ 也不理 | —— |
| Bot | 不接任何请求 | 仅名单内 Bot 可产生推断候选 |
两个容易漏的细节:
- 陌生人 @ 它也默认不接——显式呼叫不等于授权,防止任何人拉群就能指挥你的 Agent。
- 鉴权贯穿任务全生命周期:挂起的任务唤醒时重新拉取主体状态,触发者若被移出名单,执行权限即刻失效。授权不是一次性门票。
三、订阅直通:Router 不能成为堵点
过滤之后是路由。如果已分配任务的每个后续事件都要经过全局 Router 转发,这个常驻节点就是单点瓶颈。DeepSeek 的方案是 Thread 订阅 + 直通:
- 同一个 GitHub PR、同一封邮件往来、同一个 IM 话题下的连续事件构成一条 Thread;
- Worker 订阅 Thread 后,后续事件由程序直接送达对应 Worker,不再绕 Router;
- 全局 Router 只管两件事:新任务冷启动、跨任务协调。
代价账目(官方自报的模拟实验,未经独立审计):去掉订阅直通后,Router 决定数增加约 30%,读缓存 token 从 2323 万增至 3177 万;批处理能把单个决定分摊的推理时间从 6.4 秒压到 1.6 秒。结论直白:架构省下的每一秒,都是用 token 买的。
四、可跑模拟器:两级过滤 + observe/auto 双模式
下面是本次的复现代码(gate_sim.py),纯标准库。Slow Path 用确定性关键词规则"扮演"LLM,以便离线复跑;真实系统里换成模型调用即可:
# gate_sim.py —— 两级过滤 + 订阅直通 + 观察模式 最小可跑模拟
# 用法: python gate_sim.py
from dataclasses import dataclass, field
@dataclass
class Event:
source: str # 渠道: dev群 / 闲聊群 / github / ci
sender: str # 发件人
sender_type: str # human / bot / ci
text: str
thread_id: str = "" # Thread 订阅: 同一话题的连续事件直通 Worker
# ---------- 程序层策略 (Fast Path, 对应 docs/002 的三列准入映射) ----------
ALLOW_USERS = {"张三", "李四"} # 名单内用户: 不@也进推断候选
ALLOW_BOTS = set() # 名单内 Bot: 允许产生推断候选
CHANNEL_POLICY = { # 渠道策略: infer=推断候选 / ignore=直接丢弃
"dev群": "infer",
"github": "infer",
"ci": "ignore", # 无明确发件人的 CI 信号只开兜底槽位
}
def fast_path(ev: Event, mentioned: bool) -> str:
"""返回: direct=正式请求 / infer=推断候选 / ignore=丢弃"""
if ev.sender_type == "human" and ev.sender in ALLOW_USERS:
return "direct" if mentioned else CHANNEL_POLICY.get(ev.source, "ignore")
if mentioned and ev.sender_type == "human" and ev.sender not in ALLOW_USERS:
return "ignore" # 陌生人@也默认不接
if ev.sender_type == "bot" and ev.sender in ALLOW_BOTS:
return "infer"
return "ignore" # 名单外Bot / CI 等一律丢弃
# ---------- 语义层策略 (Slow Path, 真实系统=LLM 按提示词裁决) ----------
TRIGGER_WORDS = ("bug", "报错", "复现", "崩溃")
def slow_path(ev: Event) -> bool:
"""团队预写的『静默复现并修复』策略的最简替身"""
return any(w in ev.text.lower() for w in TRIGGER_WORDS)
# ---------- 接受模式: observe 只记录, auto 才派工 ----------
@dataclass
class Gate:
mode: str = "observe" # observe / auto
would_accept: list = field(default_factory=list) # observe 模式的『假定接受』日志
dispatched: list = field(default_factory=list)
def eval_ticket(self, ev: Event, verdict: str) -> str:
if verdict != "infer":
return verdict # direct=正式请求直接开工; ignore 丢弃
if self.mode == "auto":
self.dispatched.append(ev)
return "dispatch"
self.would_accept.append(ev) # observe: 结论可行也只记录, 绝不执行
return "observe"
# ---------- 主流程: 一批事件过闸门 ----------
def run(events, mentioned_flags, mode="observe"):
gate = Gate(mode=mode)
stats = {"总事件": len(events), "FastPath丢弃": 0, "SlowPath拒绝": 0,
"正式请求": 0, "派发/假定接受": 0}
for ev, mentioned in zip(events, mentioned_flags):
v = fast_path(ev, mentioned)
if v == "ignore":
stats["FastPath丢弃"] += 1
continue
if v == "direct":
stats["正式请求"] += 1
gate.eval_ticket(ev, "direct")
continue
if slow_path(ev): # infer: 过 Slow Path 再裁决
result = gate.eval_ticket(ev, "infer")
if result in ("dispatch", "observe"):
stats["派发/假定接受"] += 1
else:
stats["SlowPath拒绝"] += 1 # 随口吐槽, 模型判不动
return gate, stats
if __name__ == "__main__":
events = [
Event("dev群", "张三", "human", "这个接口在并发下会报错, 谁看下?"),
Event("dev群", "王五", "human", "今天中午吃什么"), # 陌生人@
Event("dev群", "李四", "human", "周五团建报名接龙啦"), # 名单内但纯闲聊
Event("闲聊群", "张三", "human", "有人养猫吗"), # 渠道策略 ignore
Event("github", "张三", "human", "PR#42 的 CI 挂了, 疑似 bug"), # 不@也进候选
Event("dev群", "helper_bot", "bot", "构建失败, 请检查"), # 名单外Bot
]
mentioned = [False, True, False, False, False, False]
gate_ob, stats_ob = run(events, mentioned, mode="observe")
gate_au, stats_au = run(events, mentioned, mode="auto")
print("== observe 模式 (默认) ==")
for k, v in stats_ob.items():
print(f" {k}: {v}")
print(" 假定接受清单:", [f"{e.sender}:{e.text[:12]}" for e in gate_ob.would_accept])
print("== auto 模式 (灰度后) ==")
for k, v in stats_au.items():
print(f" {k}: {v}")
print(" 派发清单:", [f"{e.sender}:{e.text[:12]}" for e in gate_au.dispatched])
# 断言: 行为契约
assert stats_ob["FastPath丢弃"] == 3, "陌生人@/闲聊群/名单外Bot 共 3 条应被 Fast Path 滤掉"
assert stats_ob["SlowPath拒绝"] == 1, "团建接龙应被语义层拒绝"
assert len(gate_ob.would_accept) == 2, "observe 下应有 2 条假定接受(报错+PR bug)"
assert len(gate_au.dispatched) == 2, "auto 下应有 2 条真派发"
assert gate_au.dispatched == gate_ob.would_accept, "auto 派发与 observe 记录应一一对应"
print("全部断言 PASS")
实际运行输出(Python 3, 两次运行结果逐字一致):
== observe 模式 (默认) ==
总事件: 6
FastPath丢弃: 3
SlowPath拒绝: 1
正式请求: 0
派发/假定接受: 2
假定接受清单: ['张三:这个接口在并发下会报错,', '张三:PR#42 的 CI 挂']
== auto 模式 (灰度后) ==
总事件: 6
FastPath丢弃: 3
SlowPath拒绝: 1
正式请求: 0
派发/假定接受: 2
派发清单: ['张三:这个接口在并发下会报错,', '张三:PR#42 的 CI 挂']
全部断言 PASS
看输出:6 条事件里,3 条死在 Fast Path(零模型成本),1 条死在 Slow Path,只有 2 条有资格进入"派发或记录"——这就是"高噪音、低触发率"下的漏斗形状。observe 与 auto 的唯一差别是最后一行的动作,两份清单一一对应,这正是官方"先 observe 看日志、顺眼了再开 auto"灰度路径的可运行版本。
五、踩坑记录(本次实测真实踩到)
- 断言里的期望值是拍脑袋写的。初版我预设"Fast Path 应滤掉 4 条",跑完才发现实际是 3 条——写用例时没逐条推演 fast_path 的分支。教训:先手推每个用例的预期路径,再写断言,否则测试只是"复述代码"。
- 关键词规则会误伤语义。
TRIGGER_WORDS里如果放"挂","团建报名接龙啦"里含"接龙"倒没事,但"进度挂了没"这类闲聊会被误触发。真实系统里 Slow Path 是 LLM,门槛高一些,但误报率的压力会转嫁给提示词质量——这就是官方说"规则是人写的,写歪了它就照着歪做"的代码级验证。 @不是授权。模拟器里王五 @ 了 Agent 但被ignore,对应官方准入矩阵"陌生人 @ 也不理"。很多自建 Agent 的第一版漏洞就是把mentioned当唯一门控。- CI 信号没有发件人。
sender_type == "ci"的事件在准入映射里没有自然归属,官方也只给兜底槽位。自建系统里要显式决定:CI 信号走白名单仓库过滤,还是直接忽略。
六、局限说明
- 模拟器的 Slow Path 是关键词替身,不含真实 LLM 的语义泛化能力;误报率数字不具外推性。
- 官方所有性能数字(批处理 6.4→1.6 秒、直通 +30% 决策、token 2323 万→3177 万等)为团队自报的模拟/合成负载测试结果,未经独立审计。
- dsh-superagent-prototype 是研究原型,README 明言非官方产品、非生产就绪,部分论文设计未实现。
七、延伸
明天(Day 3)讲权限与治理:Mandate 为什么必须不可变、"消息是决策输入不能变更权限"这条防注入的架构级答案。给读者的作业:把 slow_path 换成你们团队的触发词表,跑一遍自己群的真实消息导出,数数漏斗每层的量——你的 Agent 该沉默的时刻,比你想的多。