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:
2026-06-20 13:24:53 +08:00
parent 93d5a90495
commit 3dc2015a91
9 changed files with 348 additions and 150 deletions

View File

@@ -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 流式输出逐 tokenTTS 需要完整句子才能合成。解法:**句子切分器 + 管道并行**
AI 编排层基于 [CloudWeGo Eino](https://github.com/cloudwego/eino) 框架的 `compose.Graph` 实现,替代了原有的手写 goroutine 管道。Eino Graph 是一个声明式的有向无环图DAG编排器支持类型安全的流式数据传递和 Callback AOP 机制
**核心矛盾**LLM 流式输出逐 tokenTTS 需要完整句子才能合成。解法:**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 contextGraph 内所有流中断 | 前端清空播放队列 |
---
@@ -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