feat: compose.NewGraph 全图编译 — 工具节点在 Eino 图里真实执行

dispatcher 按每个任务的 DSL 动态编译 Eino 图:工具/检索节点按拓扑序作为真实图
节点经 NATS 调 MCP,产出注入模型上下文。不再是固定的 recall→prompt→model。

- dsl: 加 Parse(图结构) + (Flow)Topo(Kahn 拓扑序,环退化声明序) + ToolBinding(tool/
  retriever 节点→工具名+参数)
- eino/compile.go: 逐任务 compileFlow —— START→init(身份+记忆召回)→tool_n(真调 MCP,
  失败降级)→prompt(黑板 RunCtx 组装 system+画像+工具产出+历史+输入)→model→END
- eino/orchestrator: 去掉启动期静态图,Handle 内按 DSL 动态编译;删旧 graph.go/state.go
- 工具节点产出作为参考资料注入 system,模型据此作答
- 验证: 全模块 build✓ + e2e PASS; 真实 DeepSeek 双证——回归(input+agent)→'蓝色';
  工具节点(echo 注入事实)→mcp-go 日志证明图里真调 echo→模型据参考资料答'…Milvus…'

注: 分支/并行节点(compose.Branch/fan-out)暂未编译,是更大 TODO。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-10 16:45:33 +08:00
parent aa574a8cb2
commit 71db0e295f
5 changed files with 244 additions and 112 deletions
@@ -10,7 +10,6 @@ import (
"strings"
"time"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/sundynix/sundynix-dispatcher/internal/dsl"
@@ -33,34 +32,36 @@ type ToolCaller interface {
// 工具调用超时;超时即降级(不带工具上下文继续推理)。
const toolCallTimeout = 3 * time.Second
// Orchestrator 把 DSL 任务交给编译好的 Eino 图执行(记忆召回 → 注入 → 流式)。
// Orchestrator 把每个 DSL 任务动态编译为 Eino 图执行(记忆召回 → 工具节点 → 注入 → 流式)。
type Orchestrator struct {
pool *llm.Pool
breaker *harness.CircuitBreaker
sink TokenSink
tools ToolCaller
run compose.Runnable[*contract.Task, *schema.Message]
}
// NewOrchestrator 构建并编译记忆增强图
// NewOrchestrator 持有依赖;图按任务的 DSL 在 Handle 内动态编译
func NewOrchestrator(pool *llm.Pool, breaker *harness.CircuitBreaker, sink TokenSink, tools ToolCaller) (*Orchestrator, error) {
o := &Orchestrator{breaker: breaker, sink: sink, tools: tools}
run, err := buildGraph(context.Background(), pool, o.fetchMemory, o.fetchHistory)
if err != nil {
return nil, err
}
o.run = run
return o, nil
return &Orchestrator{pool: pool, breaker: breaker, sink: sink, tools: tools}, nil
}
// Handle 消费一个任务:执行 Eino 图,把 Token 流回流到 sundynix.streams.<id>。
// Handle 消费一个任务:按 DSL 编译 Eino 图并执行,把 Token 流回流到 sundynix.streams.<id>。
func (o *Orchestrator) Handle(ctx context.Context, t *contract.Task) error {
if !o.breaker.Allow() {
log.Printf("[eino] circuit open, drop task %s", t.ID)
return nil
}
log.Printf("[eino] task %s received (graph=%d bytes), running graph...", t.ID, len(t.Graph))
log.Printf("[eino] task %s received (graph=%d bytes), compiling DSL → Eino graph...", t.ID, len(t.Graph))
stream, err := o.run.Stream(ctx, t)
run, err := o.compileFlow(ctx, t)
if err != nil {
log.Printf("[eino] task %s compile error: %v", t.ID, err)
_ = o.sink.CompleteStream(t.ID)
o.breaker.Report(false)
return err
}
stream, err := run.Stream(ctx, t)
if err != nil {
log.Printf("[eino] task %s graph error: %v", t.ID, err)
_ = o.sink.CompleteStream(t.ID)