Files
cs-note/hzh/Gen2D/05-生成管线.md
T

345 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-任务队列]] — 管线任务的排队机制