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
+81 -2
View File
@@ -39,10 +39,69 @@ type Plan struct {
const defaultSystem = "你是 sundynix-agentix 平台的 AI 助手。"
// Parse 把 DSL 原文解析为图结构。
func Parse(graph json.RawMessage) (*Flow, error) {
var f Flow
if err := json.Unmarshal(graph, &f); err != nil {
return nil, err
}
return &f, nil
}
// Topo 返回节点的拓扑序(Kahn);无边/有环时退化为声明顺序。
func (f *Flow) Topo() []Node {
byID := make(map[string]Node, len(f.Nodes))
indeg := make(map[string]int, len(f.Nodes))
adj := make(map[string][]string)
for _, n := range f.Nodes {
byID[n.ID] = n
indeg[n.ID] = 0
}
for _, e := range f.Edges {
if _, ok := byID[e.Source]; !ok {
continue
}
if _, ok := byID[e.Target]; !ok {
continue
}
adj[e.Source] = append(adj[e.Source], e.Target)
indeg[e.Target]++
}
var queue, order []string
for _, n := range f.Nodes { // 按声明序入队,保证确定性
if indeg[n.ID] == 0 {
queue = append(queue, n.ID)
}
}
for len(queue) > 0 {
id := queue[0]
queue = queue[1:]
order = append(order, id)
for _, t := range adj[id] {
indeg[t]--
if indeg[t] == 0 {
queue = append(queue, t)
}
}
}
out := make([]Node, 0, len(f.Nodes))
seen := make(map[string]bool)
for _, id := range order {
out = append(out, byID[id])
seen[id] = true
}
for _, n := range f.Nodes { // 有环时补齐剩余
if !seen[n.ID] {
out = append(out, n)
}
}
return out
}
// Compile 解析 DSL 图,抽取对话计划。无法解析时退化为把原文当输入(兼容旧行为)。
func Compile(graph json.RawMessage) Plan {
var f Flow
if err := json.Unmarshal(graph, &f); err != nil || len(f.Nodes) == 0 {
f, err := Parse(graph)
if err != nil || len(f.Nodes) == 0 {
return Plan{System: defaultSystem, Query: strings.TrimSpace(string(graph))}
}
@@ -78,6 +137,26 @@ func Compile(graph json.RawMessage) Plan {
return Plan{System: system, Query: query, Tools: tools}
}
// ToolBinding 从 tool/retriever 节点抽取要调用的 MCP 工具名与参数。
// 非工具型节点返回空工具名(由编译器跳过)。
func ToolBinding(n Node) (tool string, args map[string]any) {
args = map[string]any{}
switch n.Kind {
case "tool":
tool = str(n.Config["tool"])
if raw := str(n.Config["args"]); raw != "" {
_ = json.Unmarshal([]byte(raw), &args) // 前端 args 为 JSON 字符串
}
case "retriever":
// 检索节点暂映射到 wiki_searchRAG 接真后改 memory_search / 真实混合检索。
tool = "wiki_search"
if kb := str(n.Config["kb"]); kb != "" {
args["kb"] = kb
}
}
return tool, args
}
func str(v any) string {
if v == nil {
return ""