refactor(dispatcher): compose 翻默认 —— 补平三缺口后退役 graph.go 在望

EINO_COMPOSE 默认改为开(留 =0 逃生舱回退权威 graph.go)。翻默认前补平
compose 路径三个会静默吃掉治理能力的缺口:

- HITL 审批:execDSLNode 无 approval 分支 → 审批节点被当未识别跳过、下游照跑;
  补 case 调 approvalNode。
- 忠实度评测:executeGraph 把 refs 硬写 nil → 评测静默失效;runComposeGraph
  改签名回传 refsOf(b)。
- 终态传播:rejected/fatalErr 不传播也不阻断下游 → 任务误判 done-空;加节点
  lambda 入口守卫 + branch cond 守卫 + 终态上抛 errRejected/fatalErr,并让
  runComposeConversation 模型失败置 fatalErr(对齐 graph.go 的 break 语义)。

新增等价测试:审批拒绝停下游、refs 回流。go test ./... 全绿。
soak 无回归后即可物理退役 graph.go。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-29 11:13:46 +08:00
parent d2662a1f37
commit 03225e31a9
3 changed files with 103 additions and 13 deletions
@@ -24,11 +24,10 @@ func registerFlowMerge() {
}
// executeGraph 按灰度开关选编排实现:compose.GraphPhase C)或自研 graph.go(默认/权威)。
// 返回 (成稿, 检索来源, error);来源供忠实度评测(compose 路径暂不提供来源 → 返回 nil)。
// 返回 (成稿, 检索来源, error);来源供忠实度评测,两条路径都回传compose 已对齐 graph.go)。
func (o *Orchestrator) executeGraph(ctx context.Context, t *contract.Task, tr *execTracer) (string, []string, error) {
if composeEnabled() {
ans, err := o.runComposeGraph(ctx, t, tr)
return ans, nil, err
return o.runComposeGraph(ctx, t, tr)
}
return o.runGraph(ctx, t, tr)
}
@@ -36,7 +35,9 @@ func (o *Orchestrator) executeGraph(ctx context.Context, t *contract.Task, tr *e
// runComposeGraph 把 DSL 图编译为 Eino compose.Graph 并执行(Phase C 编排归一):
// 节点体复用现有 execDSLNode;黑板进 compose 本地状态;branch 走 AddBranch
// DAG 触发模式让无依赖节点并行调度(效率)。编译失败即降级回自研 graph.go(安全网)。
func (o *Orchestrator) runComposeGraph(ctx context.Context, t *contract.Task, tr *execTracer) (string, error) {
// 返回 (成稿, 检索来源, error):审批拒绝→errRejected、预算触顶/模型失败→fatalErr,
// 与 graph.go 终态严格对齐,否则任务会被误判 done-空。
func (o *Orchestrator) runComposeGraph(ctx context.Context, t *contract.Task, tr *execTracer) (string, []string, error) {
registerFlowMerge()
flow, ferr := dsl.Parse(t.Graph)
plan := dsl.Compile(t.Graph)
@@ -52,7 +53,7 @@ func (o *Orchestrator) runComposeGraph(ctx context.Context, t *contract.Task, tr
b.profile = o.fetchMemory(ctx, b.uid, b.query)
b.history = o.fetchHistory(ctx, b.sid)
o.runComposeConversation(ctx, t.ID, b, plan.System, tr, "agent")
return b.answer, nil
return b.answer, refsOf(b), b.fatalErr // 模型失败 → 上抛判 failed(对齐 graph.go
}
// 邻接 + 入度(只认两端都存在的边)。
@@ -104,6 +105,10 @@ func (o *Orchestrator) runComposeGraph(ctx context.Context, t *contract.Task, tr
_ = g.AddLambdaNode(key(node.ID), compose.InvokableLambda(
func(c context.Context, _ flowSignal) (flowSignal, error) {
perr := compose.ProcessState(c, func(sc context.Context, bd *board) error {
// 上游审批拒绝 / 预算触顶 / 模型失败 → 跳过下游(对齐 graph.go 的 break 中止语义)。
if bd.rejected || bd.fatalErr != nil {
return nil
}
o.execDSLNode(sc, t, node, bd, plan, tr)
return nil
})
@@ -126,6 +131,9 @@ func (o *Orchestrator) runComposeGraph(ctx context.Context, t *contract.Task, tr
cond := func(c context.Context, _ flowSignal) (map[string]bool, error) {
chosen := map[string]bool{}
_ = compose.ProcessState(c, func(sc context.Context, bd *board) error {
if bd.rejected || bd.fatalErr != nil {
return nil // 已中止 → 不选任何下游,下方收口到 END
}
for _, tgt := range o.branchNode(brn, bd, outE[brn.ID], nodeByID, tr) {
chosen[key(tgt)] = true
}
@@ -161,18 +169,28 @@ func (o *Orchestrator) runComposeGraph(ctx context.Context, t *contract.Task, tr
r, cerr := g.Compile(ctx, compose.WithNodeTriggerMode(compose.AllPredecessor))
if cerr != nil {
tr.info("task", "system", "compose 编译失败", "退回自研 graph.go"+cerr.Error())
ans, _, gerr := o.runGraph(ctx, t, tr) // 降级路径丢弃 refs(compose 路径暂不评忠实度
return ans, gerr
return o.runGraph(ctx, t, tr) // 降级回权威实现(带 refs / 终态
}
if _, ierr := r.Invoke(ctx, flowSignal{}); ierr != nil {
tr.info("task", "system", "compose 执行告警", ierr.Error()) // 副作用已落 board;下方按需补一段答复
tr.info("task", "system", "compose 执行告警", ierr.Error()) // 副作用已落 board;下方按终态收尾
}
// 终态对齐 graph.go:审批拒绝 / 预算触顶 / 模型失败要显式上抛,否则任务误判 done-空。
if b.rejected {
return b.answer, nil, errRejected // 合法终态,Handle 据此判 rejected 并优雅收尾
}
if b.fatalErr != nil {
return b.answer, nil, b.fatalErr // 上抛 → Handle 判 failed(带原因)
}
// 图里无 agent 节点(纯工具/检索图)也要出一段答复。
if b.answer == "" {
o.runComposeConversation(ctx, t.ID, b, plan.System, tr, "agent")
if b.fatalErr != nil { // 兜底对话也可能触预算顶 / 模型失败
return b.answer, nil, b.fatalErr
}
}
return b.answer, nil
return b.answer, refsOf(b), nil // 成功:带回检索来源供忠实度评测
}
// execDSLNode 执行一个非 branch 的 DSL 节点(compose 编译器用;节点体与 graph.go 一致,
@@ -208,6 +226,10 @@ func (o *Orchestrator) execDSLNode(ctx context.Context, t *contract.Task, n dsl.
merged := aggregate(cstr(n.Config, "strategy"), append(append([]string{}, b.refs...), b.toolOut...))
b.refs, b.toolOut = merged, nil
tr.info("aggregate:"+n.ID, "system", labelOf(n, "汇聚"), "策略:"+firstNonEmpty(cstr(n.Config, "strategy"), "拼接"))
case "approval":
// HITL 人工审批中断:阻塞等批准/拒绝,拒绝/超时置 b.rejected(节点入口守卫据此中止下游)。
// 返回的「放行下游」清单在 compose 由静态边 + rejected 守卫接管,这里丢弃。
o.approvalNode(ctx, t.ID, n, b, tr, nil)
case "render":
o.renderNode(ctx, t.ID, n, b, tr)
case "map":