345 lines
11 KiB
Markdown
345 lines
11 KiB
Markdown
---
|
||
tags: [pipeline, eino, graph-pattern, quality-check, fallback, state-machine, go]
|
||
create time: 2026-06-03 10:20
|
||
---
|
||
|
||
# 05. 生成管线 (Generation Pipeline)
|
||
|
||
## 概述
|
||
|
||
基于 CloudWeGo Eino 的 4 阶段生成管线,带质量回退和降级,确保任务不阻塞。
|
||
|
||
---
|
||
|
||
## 正文
|
||
|
||
## 管线拓扑
|
||
|
||
```mermaid
|
||
graph TB
|
||
START(["START"]) --> PO["PromptOptimizer<br/>提示词优化"]
|
||
PO --> AG["AssetGenerator<br/>素材生成"]
|
||
AG --> QS["QualitySupervisor<br/>质量检查"]
|
||
|
||
QS -->|pass| FA["FormatAdapter<br/>格式适配"]
|
||
QS -->|fail + retry < 3| PO
|
||
QS -->|fail + retry >= 3| FA
|
||
|
||
FA --> END(["END"])
|
||
|
||
style QS fill:#fff3cd,stroke:#ffc107
|
||
style PO fill:#d1ecf1,stroke:#17a2b8
|
||
style AG fill:#d4edda,stroke:#28a745
|
||
style FA fill:#d4edda,stroke:#28a745
|
||
```
|
||
|
||
**四阶段详解**:
|
||
|
||
| 阶段 | 节点名 | 职责 | 耗时特征 |
|
||
|------|--------|------|----------|
|
||
| 1 | `PromptOptimizer` | 合并风格、注入重试原因、追加技术参数 | < 1ms(纯 CPU) |
|
||
| 2 | `AssetGenerator` | 调用 AI 推理 API 出图 | 10-120s(IO 密集) |
|
||
| 3 | `QualitySupervisor` | 质检,决定路由目标 | 1-5s(可含 LLM 调用) |
|
||
| 4 | `FormatAdapter` | 精灵表切割 + GIF 预览 | 1-3s(CPU 密集) |
|
||
|
||
## PipelineState 全局状态
|
||
|
||
Eino `compose.Graph` 通过 `WithGenLocalState` 注入全局状态,各节点通过 `StatePreHandler` / `StatePostHandler` 读写:
|
||
|
||
```go
|
||
type PipelineState struct {
|
||
Input PipelineInput // 原始输入(首次运行时保存)
|
||
FinalPrompt string // PromptOptimizer 输出
|
||
RawImages []GeneratedImage // AssetGenerator 输出
|
||
PassQuality bool // QualitySupervisor 质检结果
|
||
RejectReason string // 质检不通过原因
|
||
RetryCount int // 重试次数(最多 3 次)
|
||
NextNode string // QualitySupervisor 设置的路由目标
|
||
}
|
||
```
|
||
|
||
**状态流转**:
|
||
|
||
```
|
||
RetryCount=0, Input=原始输入
|
||
│
|
||
▼ PromptOptimizer
|
||
FinalPrompt = "风格约束:...。用户描述:...。技术参数:..."
|
||
│
|
||
▼ AssetGenerator
|
||
RawImages = [GeneratedImage, ...]
|
||
│
|
||
▼ QualitySupervisor
|
||
├── pass=true → NextNode="format_adapter"
|
||
├── pass=false, RetryCount<3 → RetryCount++, NextNode="prompt_optimizer"
|
||
└── pass=false, RetryCount>=3 → NextNode="format_adapter"(降级)
|
||
```
|
||
|
||
## 节点详解
|
||
|
||
### 1. PromptOptimizer — 提示词优化
|
||
|
||
**职责**:合并多源提示词,输出最终提示词(纯字符串拼接,无 LLM 调用)。
|
||
|
||
```
|
||
输入:PipelineInput
|
||
输出:string(最终提示词)
|
||
|
||
拼接顺序:
|
||
1. 工程全局风格提示词 (GlobalStylePrompt)
|
||
2. 用户原始提示词 (Prompt)
|
||
3. 风格描述 (ProjectStyle + TaskStyle → "风格约束:色调:暖色;线条:粗线条")
|
||
4. 重试拒绝原因 (RejectReason → "注意修正以下问题:风格不一致")
|
||
5. 技术参数段 ("素材类型: sprite;分辨率: 64;输出格式: spritesheet")
|
||
```
|
||
|
||
**StatePreHandler**:
|
||
|
||
| 场景 | 行为 |
|
||
|------|------|
|
||
| 首次运行 (`RetryCount=0`) | 保存原始输入到 `state.Input` |
|
||
| 重试运行 | 注入 `RejectReason` 到输入 |
|
||
|
||
**StatePostHandler**:将最终提示词写入 `state.FinalPrompt`。
|
||
|
||
### 2. AssetGenerator — 素材生成
|
||
|
||
**职责**:调用 AI 推理 API 生成图片,支持三级图片来源回退。
|
||
|
||
```
|
||
输入:string(提示词)
|
||
输出:[]GeneratedImage(原始图片列表)
|
||
|
||
图片来源优先级:
|
||
1. 任务级参考图 (ReferenceImageData) → 图生图 (/images/edits)
|
||
2. 工程级参考图 (ProjectReferenceImage URL) → 下载后图生图
|
||
3. 无参考图 → 纯文生图 (/images/generations)
|
||
```
|
||
|
||
**三级回退逻辑**:
|
||
|
||
```go
|
||
if len(refData) > 0 {
|
||
// 1. 任务级参考图:直接使用 base64 解码后的数据
|
||
return GenerateImagesFromRef(ctx, prompt, refData, params)
|
||
}
|
||
if projectRefURL != "" {
|
||
// 2. 工程级参考图:下载后使用
|
||
refData, err = downloadImage(ctx, projectRefURL)
|
||
if err == nil {
|
||
return GenerateImagesFromRef(ctx, prompt, refData, params)
|
||
}
|
||
// 下载失败,回退到纯文生图
|
||
}
|
||
// 3. 纯文生图
|
||
return GenerateImages(ctx, prompt, params)
|
||
```
|
||
|
||
**外部 API 弹性**:
|
||
|
||
| HTTP 状态 | 行为 | 说明 |
|
||
|-----------|------|------|
|
||
| `200 OK` | 解析响应 | 成功 |
|
||
| `5xx` | 自动重试(默认 2 次,间隔 5s) | 服务端临时故障 |
|
||
| `4xx` | 立即失败,不重试 | 客户端错误(参数错误等) |
|
||
| 超时 | 10 分钟(由 WorkerPool context 控制) | 长任务保护 |
|
||
|
||
**Mock 降级**:
|
||
|
||
当 `APIKey` 为空时,生成彩色占位图(纯色 PNG),便于开发和演示:
|
||
|
||
```go
|
||
if imgCfg.APIKey == "" {
|
||
l.Warn("image API key not configured, using mock")
|
||
return generateMockImages(size, count)
|
||
}
|
||
```
|
||
|
||
**StatePostHandler**:将原始图片写入 `state.RawImages`。
|
||
|
||
### 3. QualitySupervisor — 质量检查
|
||
|
||
**职责**:检查生成图片的质量和风格一致性,决定路由目标。
|
||
|
||
```
|
||
输入:[]GeneratedImage
|
||
输出:PipelineInput(用于路由分支读取)
|
||
|
||
决策逻辑:
|
||
1. 调用 CheckQuality(ctx, images, style)
|
||
2. pass=true → NextNode = "format_adapter"
|
||
3. pass=false + RetryCount < 3 → RetryCount++, NextNode = "prompt_optimizer"
|
||
4. pass=false + RetryCount >= 3 → NextNode = "format_adapter"(降级)
|
||
```
|
||
|
||
> [!tip] 重试策略思考
|
||
>
|
||
> **为什么是 3 次?**
|
||
> - 第 1 次失败:LLM API 波动或偶发噪声,重试大概率通过
|
||
> - 第 2 次失败:提示词可能不够精确,重新优化后改善
|
||
> - 第 3 次仍失败:当前参数组合确实无法生成合格图片,继续重试只会浪费资源
|
||
>
|
||
> 超过 3 次后 **降级到 FormatAdapter**——即使结果不完美,也比永远阻塞管线要好。这就是「宁可降级,不可阻塞」的原则。
|
||
|
||
**路由分支**(Eino `AddBranch`):
|
||
|
||
```go
|
||
g.AddBranch(nodeQualitySupervisor, compose.NewGraphBranch(
|
||
func(ctx context.Context, _ PipelineInput) (string, error) {
|
||
var next string
|
||
compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||
next = state.NextNode
|
||
return nil
|
||
})
|
||
return next, nil
|
||
},
|
||
map[string]bool{nodePromptOptimizer: true, nodeFormatAdapter: true},
|
||
))
|
||
```
|
||
|
||
**质量检查器**:
|
||
|
||
| 检查器 | 行为 | 用途 |
|
||
|--------|------|------|
|
||
| `defaultCheckQuality` | 始终返回 `true` | 生产环境(当前默认) |
|
||
| `NewCountedQualityChecker(n)` | 第 n 次调用后通过 | 测试重试逻辑 |
|
||
| `AlwaysFailQualityChecker` | 始终返回 `false` | 测试降级路径 |
|
||
|
||
> [!tip] 可扩展性:`QualityChecker` 是一个可替换的函数变量,未来可接入 LLM 视觉模型进行真正的质量评估。
|
||
|
||
### 4. FormatAdapter — 格式适配
|
||
|
||
**职责**:将原始图片转换为游戏引擎友好的格式。
|
||
|
||
```
|
||
输入:PipelineInput
|
||
输出:PipelineOutput
|
||
|
||
处理模式:
|
||
├── 精灵表模式 (Format="spritesheet", 单张图)
|
||
│ ├── PNG 解码
|
||
│ ├── splitsprite.Process() 切割为独立帧
|
||
│ ├── gifmaker.Encode() 生成 GIF 预览
|
||
│ └── 输出:原始精灵表 + 帧列表 + GIF 预览
|
||
│
|
||
└── 普通模式(多图或非精灵表)
|
||
└── 原样透传
|
||
```
|
||
|
||
**精灵表处理流程**:
|
||
|
||
```
|
||
单张精灵表 PNG
|
||
│
|
||
▼ splitsprite.Process()
|
||
独立帧列表 [frame_0, frame_1, ..., frame_N]
|
||
│
|
||
├── 保留原始精灵表
|
||
├── 编码每帧为独立 PNG
|
||
└── gifmaker.Encode() → 预览 GIF
|
||
│
|
||
▼ PipelineOutput{
|
||
Assets: [spritesheet.png, frame_000.png, ..., preview.gif]
|
||
Metadata: {FrameWidth, FrameHeight, FrameCount, Directions, GIFURL}
|
||
}
|
||
```
|
||
|
||
**降级策略**:
|
||
|
||
| 失败点 | 降级行为 |
|
||
|--------|----------|
|
||
| `splitsprite.Process()` 失败 | 整张图作为单帧输出 |
|
||
| `gifmaker.Encode()` 失败 | 跳过 GIF 预览,不阻塞管线 |
|
||
| 单帧编码失败 | 返回错误(无法降级) |
|
||
|
||
## 进度上报
|
||
|
||
管线通过 context 注入 `ProgressReporter` 回调,实时上报进度:
|
||
|
||
```go
|
||
type ProgressReporter func(stage string, progress int)
|
||
```
|
||
|
||
**进度点**:
|
||
|
||
| 阶段 | 进度值 | 触发时机 |
|
||
|------|--------|----------|
|
||
| `prompt_builder` | 10 | 首次进入 PromptOptimizer |
|
||
| `prompt_builder` | 30 + retry*10 | 重试进入 PromptOptimizer |
|
||
| `asset_generator` | 35 | PromptOptimizer 完成 |
|
||
| `quality_supervisor` | 60 | AssetGenerator 完成 |
|
||
| `quality_supervisor` | 50 + retry*10 | 质检不通过(重试) |
|
||
| `format_adapter` | 85 | 质检通过或降级 |
|
||
|
||
进度通过 `EventBus.Publish()` 推送给 SSE 客户端。
|
||
|
||
## 阶段耗时监控
|
||
|
||
`WithStageTimer` 注入阶段计时器到 context,`reportProgress` 在每次进度上报时计算上一阶段的耗时并上报 Prometheus:
|
||
|
||
```go
|
||
metrics.PipelineStageDuration.WithLabelValues(prevStage).Observe(elapsed.Seconds())
|
||
```
|
||
|
||
**监控指标**:
|
||
|
||
| Prometheus 指标 | 类型 | Label | 说明 |
|
||
|-----------------|------|-------|------|
|
||
| `gen2d_pipeline_total` | Counter | `status` | 管线执行总量 (success/fail/timeout) |
|
||
| `gen2d_pipeline_duration_seconds` | Histogram | `status` | 端到端耗时 |
|
||
| `gen2d_pipeline_stage_duration_seconds` | Histogram | `stage` | 各阶段耗时 |
|
||
| `gen2d_pipeline_retries_total` | Counter | `stage` | 重试次数 |
|
||
| `gen2d_pipeline_tasks_active` | Gauge | — | 当前活跃管线数 |
|
||
|
||
## Eino Graph 编译与执行
|
||
|
||
```go
|
||
func RunPipeline(ctx context.Context, in PipelineInput) (*PipelineOutput, error) {
|
||
// 1. 创建 Graph
|
||
g, err := NewGenerateGraph()
|
||
|
||
// 2. 编译(maxRunSteps=20 防止无限循环)
|
||
r, err := g.Compile(ctx, compose.WithMaxRunSteps(20))
|
||
|
||
// 3. 执行
|
||
output, err := r.Invoke(WithStageTimer(ctx), in)
|
||
|
||
return &output, nil
|
||
}
|
||
```
|
||
|
||
**安全机制**:
|
||
|
||
| 机制 | 说明 |
|
||
|------|------|
|
||
| `WithMaxRunSteps(20)` | 最大执行步数,防止无限循环(3 次重试 * 4 节点 + 余量) |
|
||
| `context.WithTimeout(10min)` | WorkerPool 层面的任务超时 |
|
||
| `context.Canceled` | 优雅关闭时取消正在执行的管线 |
|
||
|
||
## PipelineInput 完整结构
|
||
|
||
```go
|
||
type PipelineInput struct {
|
||
ProjectID string // 工程 ID
|
||
TaskID string // 任务 ID
|
||
Prompt string // 用户原始文本
|
||
AssetType string // sprite / background / ui / animation
|
||
ProjectStyle map[string]string // 工程风格键值对
|
||
TaskStyle map[string]string // 任务风格覆盖
|
||
Params AssetParams // 技术参数
|
||
RejectReason string // 重试时由 state 注入
|
||
Tags []string // 用户选择的标签
|
||
UserNote string // 用户额外描述
|
||
ReferenceImageData []byte // 任务级参考图(base64 解码后)
|
||
ProjectReferenceImage string // 工程级参考图 CDN URL
|
||
GlobalStylePrompt string // 工程全局风格提示词
|
||
}
|
||
```
|
||
|
||
## 关联文档
|
||
|
||
- [[00-索引]] — 文档导航与架构总览图
|
||
- [[01-系统总览]] — 分层架构与 Service 层定位
|
||
- [[02-协程池]] — 管线执行的并发控制
|
||
- [[03-任务队列]] — 管线任务的排队机制
|