refactor(dispatcher): T1.1 退役 graph.go —— 编排引擎收成单一 compose
compose 已默认数天、HITL/多智能体/评测全在其上真实跑过,soak 充分。删掉自研拓扑 解释器这第二套引擎,消灭"双实现 drift"税: - 删 runGraph(graph.go 的自研解释器)+ composeEnabled/EINO_COMPOSE 逃生舱开关 + runConversation(仅 runGraph 用的死代码)。 - executeGraph 直接走 runComposeGraph;compose 编译失败兜底改单轮对话(不再回退 graph.go);清掉仅 runGraph 用的 import(otel attribute/trace/otelx)。 - 保留 board / 各节点执行器(retriever/tool/agent/branch/approval/map/render/aggregate) / 工具函数 —— 它们是 compose 各节点 lambda 复用的,非 graph.go 专属。 - 测试:8 处 runGraph→runComposeGraph;等价测试(对照两引擎)转为 compose 正确性测试; runConversation 的开关测试转为「无 ChatModel 降级 runAgent」。 go test ./... 全绿 + vet 干净;冒烟 简单 agent/分支图 跑通。此后每个编排改动不再两边 对齐,成本减半。DEPTH_ROADMAP T1.1 ✅。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,13 +8,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/dsl"
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
"github.com/sundynix/sundynix-shared/otelx"
|
||||
)
|
||||
|
||||
// defaultAgentSystem 是 agent 节点未填系统提示词时的兜底。
|
||||
@@ -32,156 +29,12 @@ type board struct {
|
||||
sections []reportSection // map 并行 fan-out 产出的分项成稿(供 render 多章渲染)
|
||||
answer string // 当前成稿(多 agent 协作时 = 最近一个 agent 的产出 = 成品)
|
||||
agentOut []string // 各上游 agent 的产出(按序),注入下游 agent 上下文以实现接力协作
|
||||
rejected bool // HITL 审批节点拒绝/超时 → 置位,runGraph 中止并返回 errRejected
|
||||
fatalErr error // agent 节点 LLM 调用失败 → 置位,runGraph 中止并上抛 → 任务判 failed(而非 done-空)
|
||||
rejected bool // HITL 审批节点拒绝/超时 → 置位,执行中止并返回 errRejected
|
||||
fatalErr error // agent 节点 LLM 调用失败 → 置位,执行中止并上抛 → 任务判 failed(而非 done-空)
|
||||
}
|
||||
|
||||
// runGraph 按 DSL 图的真实拓扑与连线执行(替代旧的线性拍平 compileFlow)。
|
||||
//
|
||||
// 入度0 入口 → 沿连线传播 active → 每个节点按 kind 执行真实行为 →
|
||||
// branch 按条件只激活选中的下游(剪枝)→ agent 节点流式回流 token。
|
||||
//
|
||||
// 逐节点点亮"运行·观测"。返回终端 agent 的完整产出(供写回历史)。
|
||||
func (o *Orchestrator) runGraph(ctx context.Context, t *contract.Task, tr *execTracer) (string, []string, error) {
|
||||
flow, ferr := dsl.Parse(t.Graph)
|
||||
plan := dsl.Compile(t.Graph)
|
||||
b := &board{
|
||||
uid: meta(t, contract.MetaUserID),
|
||||
sid: meta(t, contract.MetaSessionID),
|
||||
query: plan.Query,
|
||||
}
|
||||
|
||||
// 无法解析或空图:退化为"无图单轮对话"(注入默认记忆 + 直接出模型)。
|
||||
if ferr != nil || flow == nil || len(flow.Nodes) == 0 {
|
||||
tr.info("task", "system", "无结构化图", "按单轮对话执行")
|
||||
b.profile = o.fetchMemory(ctx, b.uid, b.query)
|
||||
b.history = o.fetchHistory(ctx, b.sid)
|
||||
o.runConversation(ctx, t.ID, b, plan.System, tr, "agent")
|
||||
return b.answer, refsOf(b), b.fatalErr // 模型失败 → 上抛判 failed
|
||||
}
|
||||
|
||||
// 建邻接与入度(只认两端都存在的边)。保留整条边以便 branch 按 true/false 标签选路。
|
||||
nodeByID := make(map[string]dsl.Node, len(flow.Nodes))
|
||||
outE := make(map[string][]dsl.Edge)
|
||||
indeg := make(map[string]int, len(flow.Nodes))
|
||||
for _, n := range flow.Nodes {
|
||||
nodeByID[n.ID] = n
|
||||
indeg[n.ID] = 0
|
||||
}
|
||||
for _, e := range flow.Edges {
|
||||
if _, ok := nodeByID[e.Source]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := nodeByID[e.Target]; !ok {
|
||||
continue
|
||||
}
|
||||
outE[e.Source] = append(outE[e.Source], e)
|
||||
indeg[e.Target]++
|
||||
}
|
||||
|
||||
// 入口节点(入度 0)置 active;执行时沿连线把下游激活,branch 只激活选中分支。
|
||||
active := make(map[string]bool)
|
||||
for _, n := range flow.Nodes {
|
||||
if indeg[n.ID] == 0 {
|
||||
active[n.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 图里没有 memory 节点 → 沿用旧默认:注入画像+历史(避免回归)。
|
||||
hasMemory := false
|
||||
for _, n := range flow.Nodes {
|
||||
if n.Kind == "memory" {
|
||||
hasMemory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMemory {
|
||||
b.profile = o.fetchMemory(ctx, b.uid, b.query)
|
||||
b.history = o.fetchHistory(ctx, b.sid)
|
||||
}
|
||||
|
||||
for _, n := range flow.Topo() {
|
||||
if !active[n.ID] {
|
||||
continue // 被 branch 剪掉的下游,不执行
|
||||
}
|
||||
// 每个节点一个 span:下游的工具调用 / LLM span 会自动挂到本节点之下,
|
||||
// Jaeger 里即呈现「节点 → 工具 → embedding/milvus」的层级。
|
||||
nctx, nspan := otelx.Tracer().Start(ctx, "node."+n.Kind,
|
||||
trace.WithAttributes(
|
||||
attribute.String("sundynix.node.id", n.ID),
|
||||
attribute.String("sundynix.node.kind", n.Kind),
|
||||
))
|
||||
propagate := targetsOf(outE[n.ID]) // 默认激活全部出边;branch 会改写
|
||||
switch n.Kind {
|
||||
case "input":
|
||||
if txt := cstr(n.Config, "text"); txt != "" {
|
||||
b.query = txt
|
||||
}
|
||||
tr.info("input:"+n.ID, "system", labelOf(n, "输入"), truncate(b.query, 80))
|
||||
case "memory":
|
||||
if cbool(n.Config, "profile") {
|
||||
b.profile = o.fetchMemory(nctx, b.uid, b.query)
|
||||
}
|
||||
if cbool(n.Config, "history") {
|
||||
b.history = o.fetchHistory(nctx, b.sid)
|
||||
}
|
||||
tr.info("memory:"+n.ID, "memory", labelOf(n, "记忆"),
|
||||
fmt.Sprintf("画像 %d 字 · 历史 %d 条", len([]rune(b.profile)), len(b.history)))
|
||||
case "retriever":
|
||||
o.retrieverNode(nctx, n, b, tr)
|
||||
case "tool":
|
||||
o.execToolNode(nctx, t.ID, n, b, tr)
|
||||
case "agent":
|
||||
sys := firstNonEmpty(cstr(n.Config, "system"), plan.System)
|
||||
if cbool(n.Config, "autonomous") { // 开启自主工具 → ReAct(模型自己选工具)
|
||||
o.runReactAgent(nctx, t.ID, b, sys, n, tr, "agent:"+n.ID)
|
||||
} else {
|
||||
o.runConversation(nctx, t.ID, b, sys, tr, "agent:"+n.ID)
|
||||
}
|
||||
case "coordinator": // 多智能体协调:orchestrator 自主把子任务派给专家(agent-as-tool)再综合
|
||||
o.runCoordinator(nctx, t.ID, b, firstNonEmpty(cstr(n.Config, "system"), plan.System), n, tr, "coordinator:"+n.ID)
|
||||
case "aggregate":
|
||||
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 "render":
|
||||
o.renderNode(nctx, t.ID, n, b, tr)
|
||||
case "branch":
|
||||
propagate = o.branchNode(n, b, outE[n.ID], nodeByID, tr)
|
||||
case "approval":
|
||||
propagate = o.approvalNode(nctx, t.ID, n, b, tr, outE[n.ID])
|
||||
case "map":
|
||||
o.mapNode(nctx, t.ID, n, b, tr)
|
||||
case "output":
|
||||
tr.info("output:"+n.ID, "system", labelOf(n, "输出"), "目标:"+firstNonEmpty(cstr(n.Config, "target"), "屏幕"))
|
||||
default:
|
||||
tr.info(n.Kind+":"+n.ID, "system", labelOf(n, n.Kind), "未识别节点,跳过")
|
||||
}
|
||||
nspan.End()
|
||||
if b.rejected || b.fatalErr != nil {
|
||||
break // 审批拒绝/超时 或 模型失败:中止后续节点
|
||||
}
|
||||
for _, tgt := range propagate {
|
||||
active[tgt] = true
|
||||
}
|
||||
}
|
||||
|
||||
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.runConversation(ctx, t.ID, b, plan.System, tr, "agent")
|
||||
}
|
||||
if b.fatalErr != nil {
|
||||
return b.answer, nil, b.fatalErr
|
||||
}
|
||||
return b.answer, refsOf(b), nil // 成功:带回检索来源供忠实度评测
|
||||
}
|
||||
// 注:本文件原有自研图解释器 runGraph 已退役(编排引擎统一为 compose,见 compose_compiler.go
|
||||
// 的 execComposeGraph)。以下是被 compose 各节点 lambda 复用的节点执行器与黑板辅助函数。
|
||||
|
||||
// refsOf 汇总本次执行的检索来源(检索资料 + 工具产出),供忠实度评测。
|
||||
func refsOf(b *board) []string {
|
||||
|
||||
Reference in New Issue
Block a user