docs: 更新技术文档,同步 Eino 重构和默认 provider 变更
- 架构设计:更新为 Eino Graph 声明式编排,增加三级存储架构说明 - 接口文档:AI 编排器章节重写为 Eino Graph,更新 LLM 服务接口 - 技术选型:新增 Eino 框架选型章节,修正 STT/LLM/TTS 默认方案 - 语音交互:Pipeline 描述改为 Eino Graph - 成本控制:模型引用修正为 qwen3-vl-plus - 技术名词解释:新增 Eino 框架相关术语 - README:增加 10/11/12 Eino 文档索引 - 10-Eino重构方案:状态更新为已实施 - CLAUDE.md:同步所有变更
This commit is contained in:
220
docs/02-接口文档.md
220
docs/02-接口文档.md
@@ -643,44 +643,49 @@ type Options struct {
|
||||
|
||||
| Provider | 连接方式 | 说明 |
|
||||
|----------|---------|------|
|
||||
| Deepgram(默认) | WebSocket `wss://api.deepgram.com/v1/listen` | 流式识别,延迟极低,模型 nova-2 |
|
||||
| MiMo ASR | HTTP POST OpenAI 兼容 `/chat/completions` | 国产替代,PCM 自动转 WAV,支持 zh/en/auto |
|
||||
| MiMo ASR(默认) | HTTP POST OpenAI 兼容 `/chat/completions` | 国产替代,PCM 自动转 WAV,支持 zh/en/auto |
|
||||
| Deepgram | WebSocket `wss://api.deepgram.com/v1/listen` | 流式识别,延迟极低,模型 nova-2 |
|
||||
|
||||
### LLM 服务接口
|
||||
|
||||
多模态推理:接收图像 + 文本 + 对话历史,流式返回回复。
|
||||
多模态推理通过 Eino 框架的 `eino-ext/components/model/openai` ChatModel 组件实现,替代了原有的手动 `llm.Service` 接口。
|
||||
|
||||
**Eino ChatModel 配置**:
|
||||
|
||||
```go
|
||||
// Service 多模态大模型服务契约。
|
||||
type Service interface {
|
||||
// ChatStream 流式推理,返回增量文本的 channel。
|
||||
ChatStream(ctx context.Context, req Request) (<-chan Chunk, error)
|
||||
}
|
||||
chatModel, _ := openaiImpl.NewChatModel(ctx, &openaiImpl.ChatModelConfig{
|
||||
APIKey: cfg.AI.LLM.APIKey,
|
||||
Model: cfg.AI.LLM.Model, // 默认 "qwen3-vl-plus"
|
||||
BaseURL: cfg.AI.LLM.Endpoint, // 默认 DashScope OpenAI 兼容接口
|
||||
Timeout: time.Duration(cfg.AI.LLM.Timeout) * time.Second,
|
||||
})
|
||||
```
|
||||
|
||||
// Request 推理请求。
|
||||
type Request struct {
|
||||
Image []byte // JPEG 图片(已从 Base64 解码)
|
||||
Text string // 用户语音识别后的文本
|
||||
History []models.Message // 最近 N 轮对话历史
|
||||
Language string // "zh-CN"
|
||||
SystemPrompt string // 系统提示词(含场景 prompt)
|
||||
}
|
||||
**ChatModel 接口**(Eino 组件标准接口):
|
||||
|
||||
// Chunk 流式推理的一个增量片段。
|
||||
type Chunk struct {
|
||||
Delta string
|
||||
Done bool
|
||||
TokensUsed *TokenUsage // 仅 Done=true 时有值
|
||||
Model string // 实际使用的模型名
|
||||
```go
|
||||
type BaseChatModel interface {
|
||||
Generate(ctx, []*schema.Message, ...Option) (*schema.Message, error)
|
||||
Stream(ctx, []*schema.Message, ...Option) (*schema.StreamReader[*schema.Message], error)
|
||||
}
|
||||
```
|
||||
|
||||
CamTalk 使用 `Stream()` 模式,通过 Eino Graph 的 Stream 调用触发,token 级流式输出通过 Callback `OnEndWithStreamOutput` 推送到客户端。
|
||||
|
||||
**接入约定**:
|
||||
- 端点:`POST {endpoint}/chat/completions`,通过配置切换
|
||||
- 图片传入:`image_url` 字段使用 `data:image/jpeg;base64,...` 格式
|
||||
- 流式响应:`stream: true`,通过 SSE 逐 chunk 返回
|
||||
- 超时:10 秒,超时返回 `LLM_TIMEOUT` 错误
|
||||
- 系统提示词:根据语言和场景(scenario)动态构建
|
||||
- 端点:通过 `ai.llm.endpoint` 配置,支持任何 OpenAI 兼容接口
|
||||
- 默认模型:`qwen3-vl-plus`(DashScope),通过 `ai.llm.model` 配置切换
|
||||
- 图片传入:History 节点构建 `schema.Message.UserInputMultiContent`,使用 `Base64Data` + `MIMEType` 格式
|
||||
- 流式响应:Eino 框架原生 `StreamReader` 支持
|
||||
- 超时:通过 `ChatModelConfig.Timeout` 控制
|
||||
- 系统提示词:History 节点根据语言和场景(scenario)动态构建
|
||||
|
||||
**保留的类型定义**(`ai/llm/llm.go`):
|
||||
|
||||
```go
|
||||
// Request / Chunk / TokenUsage 类型定义仍保留在 ai/llm 包中,
|
||||
// 供 prompt.go 和 scenarios.go 使用。LLM 推理本身通过 eino-ext ChatModel 执行。
|
||||
```
|
||||
|
||||
### TTS 服务接口
|
||||
|
||||
@@ -708,16 +713,18 @@ type Options struct {
|
||||
|
||||
| Provider | 端点 | 说明 |
|
||||
|----------|------|------|
|
||||
| OpenAI TTS(默认) | `POST /audio/speech` | 逐句合成,返回 MP3 流 |
|
||||
| MiMo TTS | `POST /chat/completions` | 国产替代,base64 音频响应 |
|
||||
| MiMo TTS(默认) | `POST /chat/completions` | 国产替代,base64 音频响应 |
|
||||
| OpenAI TTS | `POST /audio/speech` | 逐句合成,返回 MP3 流 |
|
||||
|
||||
---
|
||||
|
||||
## 四、AI 编排器(Orchestrator)
|
||||
|
||||
### 编排策略:句子级流式并行
|
||||
### 编排架构:Eino Graph 声明式编排
|
||||
|
||||
核心矛盾:LLM 流式输出逐 token,TTS 需要完整句子才能合成。解法:**句子切分器 + 管道并行**。
|
||||
AI 编排层基于 [CloudWeGo Eino](https://github.com/cloudwego/eino) 框架的 `compose.Graph` 实现,替代了原有的手写 goroutine 管道。Eino Graph 是一个声明式的有向无环图(DAG)编排器,支持类型安全的流式数据传递和 Callback AOP 机制。
|
||||
|
||||
**核心矛盾**:LLM 流式输出逐 token,TTS 需要完整句子才能合成。解法:**Eino TransformableLambda 句子切分 + Stream 模式管道**。
|
||||
|
||||
```
|
||||
LLM 流式输出: "这" "是一" "朵红色" "的花。" "它看起" "来很美" "丽。"
|
||||
@@ -732,9 +739,29 @@ LLM 流式输出: "这" "是一" "朵红色" "的花。" "它看起" "来很美
|
||||
```
|
||||
|
||||
**时序保证**:
|
||||
- `llm_chunk` 消息一定先于对应句子的 `tts_audio` 到达客户端
|
||||
- `llm_chunk` 通过 Callback `OnEndWithStreamOutput` 实时推送,一定先于对应句子的 `tts_audio` 到达客户端
|
||||
- 用户先看到文字,紧接着听到语音(感知延迟 < 0.5 秒)
|
||||
|
||||
### Graph 拓扑
|
||||
|
||||
```
|
||||
START → STT → History → ChatModel → Msg2Str → Splitter → TTS → Done → END
|
||||
```
|
||||
|
||||
| 节点 | Lambda 类型 | 输入 → 输出 | 职责 |
|
||||
|------|------------|------------|------|
|
||||
| STT | InvokableLambda | `PipelineInput → STTOutput` | 语音识别(文本模式跳过),发送 `stt_result`,写入 State |
|
||||
| History | InvokableLambda | `STTOutput → []*schema.Message` | 组装系统提示词 + 对话历史 + 多模态图像消息 |
|
||||
| ChatModel | ChatModel(原生) | `[]*schema.Message → StreamReader[*Message]` | Eino 原生 LLM 流式推理 |
|
||||
| Msg2Str | TransformableLambda | `StreamReader[*Message] → StreamReader[string]` | 提取 LLM 输出文本 |
|
||||
| Splitter | TransformableLambda | `StreamReader[string] → StreamReader[string]` | 按句子分隔符切分,逐句输出 |
|
||||
| TTS | TransformableLambda | `StreamReader[string] → StreamReader[struct{}]` | 逐句调用 TTS 服务,推送 `tts_audio` |
|
||||
| Done | InvokableLambda | `struct{} → PipelineOutput` | 发送 `llm_done`,返回最终输出 |
|
||||
|
||||
**依赖版本**:
|
||||
- `github.com/cloudwego/eino v0.9.9`
|
||||
- `github.com/cloudwego/eino-ext/components/model/openai v0.1.13`
|
||||
|
||||
### Orchestrator 接口
|
||||
|
||||
```go
|
||||
@@ -752,30 +779,76 @@ type Sender interface {
|
||||
}
|
||||
```
|
||||
|
||||
**Pipeline 实现流程**:
|
||||
1. Base64 解码音频/图片
|
||||
2. 调用 `stt.Recognize()` → 发送 `stt_result`
|
||||
3. 调用 `llm.ChatStream()` 获取流式输出,goroutine 消费 token → 发送 `llm_chunk` + 句子切分
|
||||
4. 另一 goroutine 从句子 channel 读取 → 调用 `tts.SynthesizeStream()` → 发送 `tts_audio`
|
||||
5. 流结束 → 发送 `llm_done`
|
||||
6. TTS 失败静默跳过,STT/LLM 失败发送对应 error 消息
|
||||
WS Handler 通过 `Orchestrator` 接口与编排层交互,不感知 Eino 实现细节。
|
||||
|
||||
### EinoOrchestrator 执行流程
|
||||
|
||||
`EinoOrchestrator` 实现 `Orchestrator` 接口,包装 Eino Graph:
|
||||
|
||||
1. 设置活跃请求,获取会话配置
|
||||
2. Base64 解码音频/图片
|
||||
3. 构建 `PipelineInput`
|
||||
4. 注入 context 值(Sender、RequestID、SessionID、PipelineState、StartTime)
|
||||
5. 追加用户消息到历史
|
||||
6. 调用 `graph.Runnable.Stream(ctx, input, callbacks)` — Stream 模式触发整条链路惰性执行
|
||||
7. 消费 `StreamReader[PipelineOutput]` 直到 EOF
|
||||
8. 追加助手消息到历史
|
||||
|
||||
### Callback 机制
|
||||
|
||||
LLM token 推送通过 Eino Callback 实现,而非在 Lambda 节点中硬编码:
|
||||
|
||||
```go
|
||||
// 构建 typed callback handler
|
||||
handler := callbacks.NewHandlerHelper().ChatModel(&modelCallbackHandler{}).Handler()
|
||||
|
||||
// 运行时传入(不在 Compile 时注册)
|
||||
streamReader, err := runnable.Stream(ctx, input, compose.WithCallbacks(handler))
|
||||
```
|
||||
|
||||
**OnEndWithStreamOutput** 回调:
|
||||
- 接收 ChatModel 的 `StreamReader[*schema.Message]`
|
||||
- 逐 chunk 推送 `llm_chunk` 到客户端
|
||||
- 累积完整回复到 `PipelineState`
|
||||
- 记录 token 用量
|
||||
|
||||
### State 机制
|
||||
|
||||
`PipelineState` 是 Graph 级别的线程安全状态,通过 `compose.WithGenLocalState` 注册:
|
||||
|
||||
```go
|
||||
type PipelineState struct {
|
||||
FullResponse strings.Builder // LLM 完整回复(Callback 累积)
|
||||
TranscribedText string // STT 识别文本
|
||||
TokenUsage *TokenUsage // Token 用量
|
||||
SessionID string
|
||||
RequestID string
|
||||
ImageData []byte
|
||||
Scenario string
|
||||
Language string
|
||||
TTSEnabled bool
|
||||
}
|
||||
```
|
||||
|
||||
各节点通过 `stateFromCtx(ctx)` 读写 State,实现跨节点数据共享。
|
||||
|
||||
### 并发控制
|
||||
|
||||
- 每个 `ProcessQuery` 调用在独立 goroutine 中运行
|
||||
- `context.WithTimeout` 确保 10 秒总超时
|
||||
- `interrupt` 消息触发 `cancel()`,LLM/TTS 流式全部中断
|
||||
- `context.WithTimeout` 确保总超时
|
||||
- `interrupt` 消息触发 `cancel()`,Eino Graph 内部所有流式节点中断
|
||||
- 同一 session 内同时只允许一个活跃请求,新请求自动取消上一个
|
||||
- `PipelineState` 使用 `sync.Mutex` 保护并发写入
|
||||
|
||||
### 错误处理与降级
|
||||
|
||||
| 故障点 | 处理策略 | 客户端表现 |
|
||||
|--------|---------|-----------|
|
||||
| STT 失败 | 发送 `STT_ERROR`,终止本次请求 | 回退到纯文本模式 |
|
||||
| LLM 超时(>10s) | 发送 `LLM_TIMEOUT`,取消 TTS | 提示用户重试 |
|
||||
| STT 失败 | 发送 `STT_ERROR`,Graph 终止 | 回退到纯文本模式 |
|
||||
| LLM 超时 | 发送 `LLM_TIMEOUT`,取消下游 | 提示用户重试 |
|
||||
| LLM 部分输出后失败 | 已推送的 `llm_chunk` 保留,发送 `error` 通知中断 | 显示已收到的部分文字 |
|
||||
| TTS 失败 | 静默跳过,`llm_done` 正常发送 | 只有文字回复,无语音 |
|
||||
| interrupt 打断 | cancel context,清空所有流 | 前端清空播放队列 |
|
||||
| interrupt 打断 | cancel context,Graph 内所有流中断 | 前端清空播放队列 |
|
||||
|
||||
---
|
||||
|
||||
@@ -942,19 +1015,32 @@ type SessionRepository interface {
|
||||
### 依赖注入
|
||||
|
||||
```go
|
||||
if cfg.Storage.Driver == "postgres" {
|
||||
pool, _ := store.NewPostgresPool(ctx, cfg.Storage.DSN)
|
||||
// 存储层初始化
|
||||
if cfg.Storage.Persistence.Enabled {
|
||||
pool, _ := store.NewPostgresPool(ctx, cfg.Storage.Persistence.DSN)
|
||||
userRepo = store.NewPgUserRepository(pool)
|
||||
msgRepo = store.NewPgMessageRepository(pool)
|
||||
sessRepo = store.NewPgSessionRepository(pool)
|
||||
}
|
||||
|
||||
// Session Manager 初始化(支持三级存储自动降级)
|
||||
if cfg.Storage.Redis.Enabled {
|
||||
sessionMgr = session.NewTieredManager(30*time.Minute, 20, redisClient,
|
||||
session.WithMessageRepository(msgRepo),
|
||||
session.WithSessionRepository(sessRepo),
|
||||
)
|
||||
} else if cfg.Storage.Persistence.Enabled {
|
||||
sessionMgr = session.NewMemoryManager(30*time.Minute, 20,
|
||||
session.WithMessageRepository(msgRepo),
|
||||
session.WithSessionRepository(sessRepo),
|
||||
)
|
||||
} else {
|
||||
userRepo = store.NewMemUserRepository()
|
||||
sessionMgr = session.NewMemoryManager(30*time.Minute, 20)
|
||||
}
|
||||
|
||||
// Eino Graph 初始化
|
||||
pipelineGraph, _ := eino.NewPipelineGraph(ctx, cfg, sttService, ttsService, sessionMgr)
|
||||
orchestrator := eino.NewEinoOrchestrator(pipelineGraph, sessionMgr, cfg.AI.LLM.Model)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1021,27 +1107,27 @@ type AIConfig struct {
|
||||
}
|
||||
|
||||
type STTConfig struct {
|
||||
Provider string `mapstructure:"provider"` // "deepgram" | "mimo" | "xiaomi"
|
||||
Provider string `mapstructure:"provider"` // "mimo" | "deepgram"
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
Model string `mapstructure:"model"` // 默认 "nova-2"
|
||||
Model string `mapstructure:"model"` // 默认 "mimo-v2.5-asr"
|
||||
Endpoint string `mapstructure:"endpoint"`
|
||||
Timeout int `mapstructure:"timeout"` // 秒,默认 5
|
||||
HTTPClientTimeout int `mapstructure:"http_client_timeout"` // 秒,默认 30
|
||||
}
|
||||
|
||||
type LLMConfig struct {
|
||||
Provider string `mapstructure:"provider"` // "openai"
|
||||
Provider string `mapstructure:"provider"` // "dashscope" / "openai"
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
Model string `mapstructure:"model"` // 默认 "gpt-4o"
|
||||
Model string `mapstructure:"model"` // 默认 "qwen3-vl-plus"
|
||||
Endpoint string `mapstructure:"endpoint"`
|
||||
Timeout int `mapstructure:"timeout"` // 秒,默认 10
|
||||
Timeout int `mapstructure:"timeout"` // 秒,默认 30
|
||||
HTTPClientTimeout int `mapstructure:"http_client_timeout"` // 秒,默认 60
|
||||
}
|
||||
|
||||
type TTSConfig struct {
|
||||
Provider string `mapstructure:"provider"` // "openai" | "mimo" | "xiaomi"
|
||||
Provider string `mapstructure:"provider"` // "mimo" | "openai"
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
Model string `mapstructure:"model"` // 默认 "tts-1"
|
||||
Model string `mapstructure:"model"` // 默认 "mimo-v2.5-tts"
|
||||
Voice string `mapstructure:"voice"` // 默认 "mimo_default"
|
||||
Speed float64 `mapstructure:"speed"` // 默认 1.0
|
||||
Endpoint string `mapstructure:"endpoint"`
|
||||
@@ -1094,30 +1180,34 @@ redis:
|
||||
|
||||
ai:
|
||||
stt:
|
||||
provider: deepgram
|
||||
model: nova-2
|
||||
endpoint: "wss://api.deepgram.com/v1/listen"
|
||||
provider: mimo
|
||||
model: mimo-v2.5-asr
|
||||
endpoint: "https://api.xiaomimimo.com/v1"
|
||||
timeout: 5
|
||||
http_client_timeout: 30
|
||||
llm:
|
||||
provider: openai
|
||||
model: gpt-4o
|
||||
endpoint: "https://api.openai.com/v1"
|
||||
timeout: 10
|
||||
provider: dashscope
|
||||
model: qwen3-vl-plus
|
||||
endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
timeout: 30
|
||||
http_client_timeout: 60
|
||||
tts:
|
||||
provider: openai
|
||||
model: tts-1
|
||||
provider: mimo
|
||||
model: mimo-v2.5-tts
|
||||
voice: mimo_default
|
||||
speed: 1.0
|
||||
endpoint: "https://api.openai.com/v1"
|
||||
endpoint: "https://token-plan-cn.xiaomimimo.com/v1"
|
||||
timeout: 5
|
||||
http_client_timeout: 30
|
||||
output_format: mp3
|
||||
sample_rate: 24000
|
||||
|
||||
storage:
|
||||
driver: memory
|
||||
redis:
|
||||
enabled: true
|
||||
persistence:
|
||||
enabled: true
|
||||
driver: postgres
|
||||
|
||||
auth:
|
||||
access_ttl: 15
|
||||
|
||||
Reference in New Issue
Block a user