---
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
提示词优化"]
PO --> AG["AssetGenerator
素材生成"]
AG --> QS["QualitySupervisor
质量检查"]
QS -->|pass| FA["FormatAdapter
格式适配"]
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-任务队列]] — 管线任务的排队机制