Files
sundynix-agentix/sundynix-dispatcher/internal/eino/compose_graph.go
T
Blizzard 2ee16d1f99 feat(dispatcher): Eino 采纳 Phase C —— 对话主流程跑 compose.Graph + callbacks 归一
按"并存 + 等价回归"策略,对话主流程可跑在 Eino compose.Graph 上:
- compose_graph.go:runConversation 按 EINO_COMPOSE 灰度开关分流;
  runComposeConversation 建图 START→ChatModel→END,Compile→Stream 回流 token;
  模型未就绪/编译失败降级回 runAgent。默认关,graph.go 仍是默认且权威。
- compose_callbacks.go:composeTracer 用 utils/callbacks 把 ChatModel/Tool 的
  start/end/error 桥到 ExecEvent(可观测归一,不再各处手写 emit)。
- LLM 接口 + Pool 增 ChatModel();fakeLLM 加 cm 字段 + stub Eino 模型。
- 测试:compose 图编译运行 / compose 对话流式 / 开关关→走 runAgent。

顺带修真 bug:SubjectTaskStatus 原 sundynix.tasks.status 落在任务流通配
sundynix.tasks.> 内 → 状态事件被当成"幽灵任务"自我放大(实测污染 2300+ 条)。
挪到 sundynix.status.task + dispatcher 加空任务护栏。

验收:make test-go 全绿;live compose 路径 54 字答复 eval 1.00、默认路径
eval 1.00、幽灵任务 0 复发。剩余 branch/map/render 等节点逐步迁移后 graph.go 退役。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:42:50 +08:00

94 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package eino
import (
"context"
"fmt"
"io"
"os"
"time"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/sundynix/sundynix-dispatcher/internal/harness"
)
// composeEnabled 报告是否启用 compose.Graph 编排路径(Phase C 灰度开关,默认关 → 走自研 graph.go)。
// 并存策略:EINO_COMPOSE=1 时对话主流程改走 Eino compose 运行时,行为对齐后再逐步退役 graph.go。
func composeEnabled() bool { return os.Getenv("EINO_COMPOSE") == "1" }
// runConversation 是对话/模型节点的统一入口:按灰度开关选 compose.Graph 或自研 runAgent。
// 二者对外行为一致(据黑板拼消息 → 流式回流 token → 累计成稿),便于等价回归。
func (o *Orchestrator) runConversation(ctx context.Context, taskID string, b *board, system string, tr *execTracer, node string) {
if composeEnabled() {
o.runComposeConversation(ctx, taskID, b, system, tr, node)
return
}
o.runAgent(ctx, taskID, b, system, tr, node)
}
// runComposeConversation 用 Eino compose.Graph 跑对话主流程:
// START → ChatModel 节点 → END,编译为 Runnable 后流式执行;可观测经 callbacks 桥到 ExecEvent。
// 模型未就绪 / 编译失败时降级回自研 runAgent,保证不回归。
func (o *Orchestrator) runComposeConversation(ctx context.Context, taskID string, b *board, system string, tr *execTracer, node string) {
cm := o.pool.ChatModel()
if cm == nil {
o.runAgent(ctx, taskID, b, system, tr, node) // 无模型 → 走自研路径的降级桩
return
}
g := compose.NewGraph[[]*schema.Message, *schema.Message]()
if err := g.AddChatModelNode("model", cm); err != nil {
tr.info(node, "system", "compose 降级", "建图失败,退回自研路径:"+err.Error())
o.runAgent(ctx, taskID, b, system, tr, node)
return
}
_ = g.AddEdge(compose.START, "model")
_ = g.AddEdge("model", compose.END)
r, err := g.Compile(ctx)
if err != nil {
tr.info(node, "system", "compose 降级", "编译失败,退回自研路径:"+err.Error())
o.runAgent(ctx, taskID, b, system, tr, node)
return
}
rc := &RunCtx{
UserID: b.uid, SessionID: b.sid,
System: firstNonEmpty(system, defaultAgentSystem),
Query: b.query,
Profile: b.profile,
History: b.history,
ToolOut: append(append([]string{}, b.toolOut...), b.refs...),
}
msgs, _ := buildMessages(ctx, rc)
t0 := time.Now()
// ChatModel 的 start/end 由 composeTracercallbacks)落轨迹,这里不再手写 emit(归一)。
sr, err := r.Stream(ctx, msgs, compose.WithCallbacks(composeTracer(tr, node)))
if err != nil {
tr.emit(node, "model", "error", "compose 图执行", err.Error(), time.Since(t0).Milliseconds())
return
}
defer sr.Close()
chunks := 0
for {
chunk, rerr := sr.Recv()
if rerr == io.EOF {
break
}
if rerr != nil {
tr.emit(node, "model", "error", "compose 图执行", rerr.Error(), time.Since(t0).Milliseconds())
return
}
if chunk.Content == "" {
continue
}
safe, _ := harness.RedactSecrets(chunk.Content) // 输出护栏:逐片脱敏
_ = o.sink.PublishToken(taskID, []byte(safe))
b.answer += safe
chunks++
}
tr.info(node, "system", "compose 图", fmt.Sprintf("%d 段输出 / %d 字(Eino compose 运行时)", chunks, len([]rune(b.answer))))
}