--- 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-任务队列]] — 管线任务的排队机制