agentchief
§ · Nested detail page

三段式路由管线 深度解析

从 stage1 到 stage2 再到 judge,解释为什么 agent-chief 能先便宜过滤,再把少量复杂事件交给模型。

§ 00 · IMPORT CHAIN

导入链与契约

这个子页面聚焦 routing pipeline。关键证据包括 core/scorer.py:72;core/scorer.py:123;core/scorer.py:207;core/brain.py:247;core/scorer.py:90;core/schema.py:52。这些引用不是源码索引,而是帮助定位边界:入口在哪里、数据对象在哪里、失败时由谁兜底。

▸stage1 入口

core/scorer.py · core/scorer.py:72-107Python
72def stage1(
73    event,
74    *,
75    now: datetime,
76    policy: Policy,
77    quiet_hours: str = "23:00-08:00",
78    night_whitelist: list[str] | None = None,
79    recent_dedup_keys: frozenset[str] | set[str] = frozenset(),
80    embedder: Embedder = DEFAULT_EMBEDDER,
81) -> RuleHit | None:
82    """Stage-1 hard rules (SPEC §4.4). Returns a forced route, or None to continue."""
83    night_whitelist = night_whitelist or []
84
85    # Drop rules run before the quiet-hours digest rule: noise must die even at
86    # night, or it would resurface in the morning digest (see §4.7 events 1/24).
87    if policy.is_muted(event.topic):
88        return RuleHit("drop", "muted_topic", f"topic {event.topic} muted in POLICY.md")
89
90    if event.dedup_key and event.dedup_key in recent_dedup_keys:
91        return RuleHit("drop", "dedup", "duplicate of an event seen in the last 24h")
92
93    if is_zero_information(event.summary, embedder):
94        return RuleHit("drop", "zero_information", "zero-information template (empty report)")
95
96    if in_quiet_hours(now, quiet_hours) and not _topic_in_whitelist(
97        event.topic, night_whitelist
98    ):
99        return RuleHit("digest", "quiet_hours", f"quiet hours {quiet_hours}, topic not whitelisted")
100
101    rule = policy.route_for(event.topic)
102    if rule:
103        return RuleHit(
104            rule.route, f"policy:{rule.pattern}", f"POLICY.md rule {rule.pattern} -> {rule.route}"
105        )
106
107    return None
§ 01 · CORE IMPL

核心实现,逐段拆

▸相似度分类

SimilarityClassifier 的核心在 core/scorer.py:143 到 core/scorer.py:166:engaged_hits 能跳过 judge,dismissed_sim 能直接 drop,未知样本才 pass。它让用户历史反馈在毫秒级生效。

core/scorer.py · core/scorer.py:123-166Python
123class SimilarityClassifier:
124    """Compares an event against engaged/dismissed historical vectors (ms).
125
126    dismissed-sim > 0.88 with no engaged record → drop;
127    engaged-sim > 0.88 → skip judge, route by historical same-class mean;
128    otherwise → pass through to stage 3.
129    """
130
131    def __init__(self, embedder: Embedder = DEFAULT_EMBEDDER, threshold: float = STAGE2_THRESHOLD):
132        self.embedder = embedder
133        self.threshold = threshold
134        self._engaged: list[tuple[list[float], str]] = []  # (vector, route)
135        self._dismissed: list[list[float]] = []
136
137    def add_engaged(self, text: str, route: str) -> None:
138        self._engaged.append((self.embedder.embed(text), route))
139
140    def add_dismissed(self, text: str) -> None:
141        self._dismissed.append(self.embedder.embed(text))
142
143    def classify(self, text: str) -> Stage2Verdict:
144        vec = self.embedder.embed(text)
145        engaged_hits = [
146            (cosine(vec, evec), route)
147            for evec, route in self._engaged
148            if cosine(vec, evec) > self.threshold
149        ]
150        if engaged_hits:
151            # "route by historical same-class mean": majority route among the
152            # similar engaged records, nearest-first on ties.
153            engaged_hits.sort(reverse=True)
154            routes = [route for _, route in engaged_hits]
155            best = max(set(routes), key=lambda r: (routes.count(r), -routes.index(r)))
156            sim = engaged_hits[0][0]
157            return Stage2Verdict(
158                "route", best, sim, f"engaged-similar {sim:.2f}, historical route {best}"
159            )
160        dismissed_sim = max((cosine(vec, dvec) for dvec in self._dismissed), default=0.0)
161        if dismissed_sim > self.threshold:
162            return Stage2Verdict(
163                "drop", None, dismissed_sim,
164                f"dismissed-similar {dismissed_sim:.2f} with no engaged record",
165            )
166        return Stage2Verdict("pass", None, dismissed_sim, "unfamiliar; ask the judge")

▸score_and_route

score_and_route 在 core/scorer.py:207 到 core/scorer.py:256 合成五维分数、场景阈值和 dispatchable 信号。这里是 route 从数字变成产品动作的地方。

core/scorer.py · core/scorer.py:207-256Python
207def score_and_route(
208    result,
209    scene: SceneState,
210    *,
211    topic_weights: dict[str, float] | None = None,
212    scene_cost: float = 0.0,
213    threshold_overrides: dict[str, float] | None = None,
214    threshold_adjust: float = 0.0,
215    memory_hit: bool = False,
216) -> tuple[str, float, dict[str, float], str]:
217    """Compose `score = Σ(w_topic[dim]·comp[dim]) − scene_cost` and route.
218
219    Returns (route, score, components-after-boost, reason).
220    """
221    from context.infer import downgrade_low_confidence, interrupt_threshold
222
223    weights = {**DEFAULT_WEIGHTS, **(topic_weights or {})}
224    comps = {dim: getattr(result, dim) for dim in DIMS}
225    if memory_hit:
226        comps["relevance"] = min(1.0, comps["relevance"] * 1.2)
227
228    score = sum(weights[dim] * comps[dim] for dim in DIMS) - scene_cost
229    threshold = interrupt_threshold(scene.scene, threshold_overrides)
230    if threshold_adjust:
231        # learned global adjustment, clamped (SPEC §4.6 bounds [0.35, 0.95])
232        threshold = min(0.95, max(0.35, threshold + threshold_adjust))
233
234    if score >= threshold:
235        route = "interrupt"
236        reason = f"score {score:.2f} ≥ {scene.scene} threshold {threshold:.2f}"
237    elif score >= DIGEST_FLOOR:
238        route = "digest"
239        reason = f"score {score:.2f} below {scene.scene} threshold {threshold:.2f}"
240    elif result.memorize:
241        route = "curate"
242        reason = f"score {score:.2f} low but worth remembering"
243    else:
244        route = "drop"
245        reason = f"score {score:.2f} with no lasting value"
246
247    route = downgrade_low_confidence(route, scene)
248    if route == "digest" and score >= threshold:
249        reason += f"; downgraded (scene confidence {scene.confidence:.2f} < 0.6)"
250
251    # SPEC §4.4: dispatchable prep work runs first, then delivery ("arrive with a plan")
252    if result.dispatchable and route in ("interrupt", "digest"):
253        route = "dispatch"
254        reason += "; dispatchable prep work available"
255
256    return route, score, comps, reason
§ 02 · KEY FLOW

关键流程

routing pipeline input → normalize state → score / verify → persisted Decision

流程上,routing pipeline 并不是一个孤立函数。它会和 core/scorer.py:72、core/scorer.py:123、core/scorer.py:207 共同形成一个闭环:先产生可解释中间状态,再把 route、score、cost 或 rejected reason 存回系统。

§ 03 · EDGE CASES

边界 / 失败模式

▸失败不是异常,而是产品路径

边界处理在源码里是显式产品逻辑:core/brain.py:247 负责保守降级,core/scorer.py:90 负责避免重复或噪声,core/schema.py:52 负责留下可追踪状态。这让 agent-chief 的可信度来自失败路径,而不是只来自 happy path。

  • judge 不可用时不得盲目 interrupt。
  • 历史相似事件只能在阈值内复用,不应跳过新奇事件。
  • 派发结果必须验证,不能信任“done”的自述。
Generated · 2026-07-09Source · SmileLikeYe/agent-chief