- graph.go: 构建 Graph 拓扑 START→STT→History→ChatModel→Splitter→TTS→Done→END - 创建 eino-ext ChatModel 对接 DashScope OpenAI 兼容接口 - 统一使用值类型(PipelineInput/PipelineOutput) - Callback 在运行时通过 Stream option 传入 - adapter.go: EinoOrchestrator 实现 orchestrator.Orchestrator 接口 - 解码 base64 音频/图片,注入 context 值 - 调用 Graph.Stream() 触发惰性执行并消费输出 - 追加用户/助手消息到历史 - main.go: 移除旧 llmService + orchestrator.New() 替换为 eino.NewPipelineGraph() + eino.NewEinoOrchestrator() - 各节点统一使用值类型,State 传递请求元数据 Co-Authored-By: Claude <noreply@anthropic.com>
114 lines
3.3 KiB
Go
114 lines
3.3 KiB
Go
package eino
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
openaiImpl "github.com/cloudwego/eino-ext/components/model/openai"
|
||
"github.com/cloudwego/eino/compose"
|
||
|
||
"github.com/hhs/camtalk/internal/ai/stt"
|
||
"github.com/hhs/camtalk/internal/ai/tts"
|
||
"github.com/hhs/camtalk/internal/config"
|
||
"github.com/hhs/camtalk/internal/logger"
|
||
"github.com/hhs/camtalk/internal/models"
|
||
"github.com/hhs/camtalk/internal/session"
|
||
)
|
||
|
||
const (
|
||
nodeSTT = "stt"
|
||
nodeHistory = "history"
|
||
nodeLLM = "llm"
|
||
nodeSplitter = "splitter"
|
||
nodeTTS = "tts"
|
||
nodeDone = "done"
|
||
)
|
||
|
||
// PipelineGraph 封装编译后的 Eino Graph。
|
||
type PipelineGraph struct {
|
||
Runnable compose.Runnable[PipelineInput, PipelineOutput]
|
||
}
|
||
|
||
// NewPipelineGraph 构建 CamTalk AI 编排 Graph。
|
||
//
|
||
// 拓扑:START → STT → History → ChatModel → Splitter → TTS → Done → END
|
||
//
|
||
// Graph 使用 Stream 模式调用,ChatModel 实现真正的 token 级流式输出。
|
||
// LLM token 通过 Callback 的 OnEndWithStreamOutput 实时推送到客户端。
|
||
func NewPipelineGraph(
|
||
ctx context.Context,
|
||
cfg *config.Config,
|
||
sttService stt.Service,
|
||
ttsService tts.Service,
|
||
sessionMgr session.Manager,
|
||
) (*PipelineGraph, error) {
|
||
log := logger.Log
|
||
|
||
// 1. 创建 eino-ext ChatModel(对接 DashScope OpenAI 兼容接口)
|
||
chatModel, err := openaiImpl.NewChatModel(ctx, &openaiImpl.ChatModelConfig{
|
||
APIKey: cfg.AI.LLM.APIKey,
|
||
Model: cfg.AI.LLM.Model,
|
||
BaseURL: cfg.AI.LLM.Endpoint,
|
||
Timeout: time.Duration(cfg.AI.LLM.Timeout) * time.Second,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
log.Infow("Eino ChatModel 初始化成功",
|
||
"model", cfg.AI.LLM.Model,
|
||
"endpoint", cfg.AI.LLM.Endpoint)
|
||
|
||
// 2. 构建 Graph(值类型,非指针)
|
||
g := compose.NewGraph[PipelineInput, PipelineOutput](
|
||
compose.WithGenLocalState(genLocalState),
|
||
)
|
||
|
||
// 3. 添加节点
|
||
maxHistory := cfg.Session.MaxHistory
|
||
|
||
_ = g.AddLambdaNode(nodeSTT, NewSTTLambda(sttService))
|
||
_ = g.AddLambdaNode(nodeHistory, NewHistoryLambda(sessionMgr.GetHistory, maxHistory))
|
||
_ = g.AddChatModelNode(nodeLLM, chatModel)
|
||
_ = g.AddLambdaNode(nodeSplitter, NewSplitterLambda())
|
||
_ = g.AddLambdaNode(nodeTTS, NewTTSLambda(
|
||
ttsService,
|
||
cfg.AI.TTS.Voice,
|
||
cfg.AI.TTS.Speed,
|
||
cfg.AI.TTS.OutputFormat,
|
||
cfg.AI.TTS.SampleRate,
|
||
))
|
||
_ = g.AddLambdaNode(nodeDone, NewDoneLambda(cfg.AI.LLM.Model))
|
||
|
||
// 4. 连接边
|
||
_ = g.AddEdge(compose.START, nodeSTT)
|
||
_ = g.AddEdge(nodeSTT, nodeHistory)
|
||
_ = g.AddEdge(nodeHistory, nodeLLM)
|
||
_ = g.AddEdge(nodeLLM, nodeSplitter)
|
||
_ = g.AddEdge(nodeSplitter, nodeTTS)
|
||
_ = g.AddEdge(nodeTTS, nodeDone)
|
||
_ = g.AddEdge(nodeDone, compose.END)
|
||
|
||
// 5. 编译(回调在运行时通过 Stream option 传入)
|
||
runnable, err := g.Compile(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
log.Infow("Eino Graph 编译成功", "nodes", 6)
|
||
return &PipelineGraph{Runnable: runnable}, nil
|
||
}
|
||
|
||
// buildPipelineInput 从 WebSocket 请求和会话配置构建 Graph 输入。
|
||
func buildPipelineInput(req models.WsQuery, sessionID string, sess *models.Session, audioData, imageData []byte) PipelineInput {
|
||
return PipelineInput{
|
||
AudioData: audioData,
|
||
ImageData: imageData,
|
||
Text: req.Text,
|
||
SessionID: sessionID,
|
||
RequestID: req.RequestID,
|
||
Language: sess.Config.Language,
|
||
Scenario: sess.Config.Scenario,
|
||
TTSEnabled: sess.Config.TTSEnabled,
|
||
}
|
||
}
|