【SenseNova U1.5 Lite实战】AI画图全靠抽卡?我给模型配了“质检员+修图师“:一个会自我迭代的生图Agent诞生记

调用一次生图 API 很容易,交付一张能用的信息图很难。信息图同时要求"好看"和"读得懂"——标题不能糊、数字不能错、层次不能乱,而扩散模型每次采样都是一次重新抽奖。本文的做法是给这条流水线装上判别器编辑器:用多模态模型给生成结果打四个维度的分,用 SenseNova U1.5 Lite 的原生图像编辑能力做定向修正,并把「生成—质检—编辑—择优」固化成一条可复现的工程流水线。文中所有结论均来自真实调用记录,包含 6 个只有在跑起来之后才会暴露的坑及其根因分析。


SenseNova U1.5 Lite生图agent操作教程

TL;DR

  • 核心思路:利用"生成难、判别易"的不对称性,让多模态模型充当判别器,把主观质量变成可优化的标量信号;再用 U1.5 Lite 的原生编辑能力做定向修正,而不是每次从头重画
  • 两条优化路径edit(默认,基于上一张图定向修改)与 regenerate(整图重生成),编辑失败自动回退重生成
  • 择优而非取末:迭代过程保留得分最高的那张,而非最后一轮——后者可能越改越差
  • 踩到的 6 个坑:推理模型响应字段不统一、生图尺寸必须 32 倍数、LLM JSON 输出不稳定且兜底不能用原文、超时预算必须按模型切片、st.table 与 numpy 2.x 的 ABI 冲突、st.image 参数改名
  • 实测:单轮端到端 90~220 秒;多模态质检实测 27~35 秒(这是把超时预算从 25 秒提到 90 秒的依据);6 张效果图得分 8.8~9.4 / 10;53 项离线测试全绿

环境速查

项目
Python3.12.3
OSWindows 11
API Base URLhttps://token.sensenova.cn/v1
SDKopenai-python 2.24.0(OpenAI 兼容协议)
生图 / 编辑sensenova-u1.5-lite
Prompt 工程deepseek-v4-flash(回退 glm-5.2 / sensenova-6.8-flash-lite / sensenova-6.7-flash-lite
多模态质检sensenova-6.8-flash-lite(回退 sensenova-6.7-flash-lite
前端 / 后端Streamlit 1.37.1 / FastAPI + Uvicorn

一、问题定义:为什么信息图不能"一锤定音"

把信息图生成当成普通文生图来做,会稳定地得到"每次都差一点"的结果。原因是两者的成功标准不同:

  • 普通文生图:只要视觉上成立即可,观感是连续量,容错空间大
  • 信息图:存在一组离散的硬约束——标题文字必须清晰可辨、数据标签必须与图表对应、分栏不能重叠、层次必须可读

扩散模型对这些硬约束没有直接的表达能力。它不理解"这个数字必须能被读出来",它只是在拟合"信息图大概长这样"的分布。于是每次采样都是一次独立抽样:构图可能很好但文字糊了,重来一次文字清楚了但布局塌了。

1.1 生成与判别的不对称

这里有个可以借力的结构性事实:构造一个解很难,但验证一个解很容易。这与机器学习中"判别通常比生成容易"的经典结论一致,具体到本任务:

  • 让模型"画出一张布局合理、文字清晰的信息图"——需要逐个像素地做对,搜索空间巨大
  • 让模型"看看这张图哪里糊了、哪里重叠了"——只需在已给定的结果上做局部判断,难度低一个量级

这意味着:同一个量级的模型,作为判别器的可靠性显著高于作为生成器的可靠性。所以与其反复调 Prompt 去"求"一个好结果,不如:

  1. 让生成器自由采样
  2. 用判别器给结果打分并定位问题
  3. 把问题反馈回去,做有针对性的修改

第三步是本文的重点。常见做法是"根据反馈重写 Prompt 再生成一遍",但这会丢弃已经画对的部分——构图、配色、整体排版都得重新抽一次奖。SenseNova U1.5 Lite 提供了另一条路:原生图像编辑,在保留原图的前提下按指令修改局部。这正是本文选择它作为核心的原因之一。

1.2 失败模式必须可观测

闭环能成立还有一个前提:失败必须是可观测的。如果判别器只能说"不好",生成器无法据此改进。

所以质检 Prompt 的设计重点不是打分,而是产出可执行的定位信息——issuessuggestions 必须是针对具体位置的短句。实测中文案模型给出的反馈是有效的:

issues: ['底部三个小图标下方的说明文字字号偏小,远距离阅读可能稍显费力',
         '副标题「全球每年减少1000万吨塑料污染」的表述逻辑略显模糊']

这样的反馈可以直接拼成编辑指令喂回生图模型,闭环才真正闭合。


二、系统总览

输入主题
   │
   ▼
┌─────────────────────────────────────────────────────────┐
│ ① Prompt 工程    deepseek-v4-flash                       │
│    自然语言 → 结构化 JSON(布局/风格/配色/文案/英文 Prompt)│
│    JSON 解析失败 → 确定性模板(绝不回传原始 JSON)         │
└─────────────────────────────────────────────────────────┘
   │ enhanced_prompt
   ▼
┌─────────────────────────────────────────────────────────┐
│ ② 尺寸校验 + 图像生成   sensenova-u1.5-lite              │
│    validate_size() 本地拦截非法尺寸                       │
│    auto 检测 ComfyUI:可用→本地,不可用→云端              │
│    限流 / 敏感词 → 自动重试                               │
└─────────────────────────────────────────────────────────┘
   │ image_path
   ▼
┌─────────────────────────────────────────────────────────┐
│ ③ 多模态质检     sensenova-6.8-flash-lite                │
│    四维度 0-10 分 + issues/suggestions                    │
│    超时/限流 → 回退同级多模态模型 → 仍失败则降级通过       │
└─────────────────────────────────────────────────────────┘
   │
   ├─ 达标 ──────────────┐
   │                     │
   └─ 未达标 ─┐          │
              ▼          │
   ④ 优化:edit(定向修改)/ regenerate(整图重生成)
              │
              └──── 回到 ②(编辑失败则自动退回重生成)
                           │
                           ▼
              ⑤ 输出全程得分最高的那张(Best-of-N)

模块与职责:

模块职责
src/engines/llm_engine.pyPrompt 工程、模型回退、时间预算
src/engines/image_generator.py云端生成、原生编辑、ComfyUI 双后端、尺寸校验
src/engines/quality_review_agent.py多模态质检、同级回退、共享时间预算
src/pipeline.py编排循环、编辑/重生成策略、Best-of-N 择优
src/utils/json_utils.pyLLM 输出 JSON 稳健提取
src/utils/deadline.py模型链的时间预算切片

三、效果展示

在这里插入图片描述

图 1:主题「2026年中国AI大模型产业发展趋势」,布局 data_dashboard,风格 tech,质检 9.4/10。

其余实测产物(全部由本项目直接生成,存放于 docs/showcase/):

效果图效果图
9:16 综合信息图3:4 对比布局
9:16 · 1536×2720 · 教育风格 · 质检 9.13:4 · 1760×2368 · 对比布局 · 质检 9.4
U1.5 原生编辑产物run_demo 示例 2
原生编辑产物 · 2720×15369:16 · 1536×2720 · 质检 9.0

在这里插入图片描述

图 2:Streamlit 前端。侧边栏可切换后端、迭代轮数、优化策略,并直接填写 ComfyUI 地址。

在这里插入图片描述

图 3:FastAPI 自动生成的 Swagger 文档,含 /api/generate/api/status/api/image/{request_id}

在这里插入图片描述

图 4:GET /api/status 返回服务状态、已配置模型、输出目录与 ComfyUI 可用性。

在这里插入图片描述

图 5:53 项离线单元测试全部通过,覆盖 JSON 容错、时间切片、流水线择优、ComfyUI 路径、前端加载等。

在这里插入图片描述

图 6:python run_test.py 实测输出,验证 LLM Prompt 工程正常。


四、模型选型:把合适的模型放在合适的位置

GET /v1/models 实测返回 6 个模型:

模型 ID输入输出在本项目中的角色
deepseek-v4-flashtexttextPrompt 工程(首选)
glm-5.2texttextPrompt 工程回退
sensenova-6.8-flash-litetext + imagetext多模态质检(首选)
sensenova-6.7-flash-litetext + imagetext多模态质检回退
sensenova-u1.5-litetextimage信息图生成 / 原生编辑
sensenova-u1-fasttextimage未使用(U1 系,非 U1.5)

4.1 一个容易被忽略的约束:质检回退不能跨模态

设计回退链时有个陷阱:质检的回退目标必须仍然支持图片输入。若回退到纯文本模型,它收到带 image_url 的消息会直接报错,回退反而制造了新的失败。

所以本项目把回退链按能力分组,而不是用一条通用链:

# 质检链只包含多模态模型
MULTIMODAL_MODEL_CHAIN = [
    "sensenova-6.8-flash-lite",
    "sensenova-6.7-flash-lite",
]

# Prompt 工程链只包含文本模型
MODEL_CHAIN = [
    "deepseek-v4-flash",
    "glm-5.2",
    "sensenova-6.8-flash-lite",
    "sensenova-6.7-flash-lite",
]

4.2 生图与编辑是同一个模型的不同端点

sensenova-u1.5-lite 同时提供两个端点,这一点值得强调——它不是"生成模型 + 一个外挂的编辑工具",而是同一个统一多模态架构下的两种能力:

  • 生成:POST /v1/images/generations
  • 编辑:POST /v1/images/edits

这也是"原生图像编辑"这个词的含义。好处是编辑结果与原图在风格、字体、配色上天然一致,不会出现"拼贴感"。


五、Prompt 工程:把自然语言编译成结构化生图指令

5.1 输出契约

第一步要让 LLM 把用户的自然语言主题编译成一份结构化规格:

{
    "enhanced_prompt": "详细的英文生图 Prompt",
    "layout": "布局类型",
    "style": "视觉风格",
    "color_scheme": ["主色", "辅色", "强调色"],
    "key_elements": ["核心元素"],
    "text_content": {"title": "...", "subtitle": "...", "sections": [], "data_points": [], "footer": "..."}
}

一个关键设计:生图 Prompt 用英文,画面文字用中文。生图模型对英文指令的遵循度更好,而成品信息图面向中文读者。这个"指令语言 ≠ 内容语言"的分离,是信息图场景下很实用的一条经验。

5.2 坑 1:LLM 的 JSON 输出并不稳定

实测中 deepseek-v4-flash 的返回存在多种形态:干净 JSON、被 ```json 围栏包裹、前后带导言/结语、含尾随逗号,偶尔还会被 max_tokens 截断。

朴素的解析方式(只处理围栏 + 取首 { 到末 })有两个问题:

  1. 遇到"导言 + 围栏"就失效——startswith("```") 不成立
  2. 取首 { 到末 }字符串内部含花括号时会截错

第二点尤其隐蔽。考虑:

{"a": "值 {x} 结束", "b": 2}

rfind("}") 切会得到 {"a": "值 {x} 结束", "b": 2} ——恰好对。但如果 JSON 后面还跟着说明文字,就会把说明也吞进去。正确做法是从第一个 { 开始做括号平衡扫描,并正确跳过字符串字面量与转义:

# src/utils/json_utils.py
def _scan_balanced_json(text: str) -> str | None:
    start = text.find("{")
    if start == -1:
        return None
    depth = 0
    in_string = False
    escaped = False
    for i in range(start, len(text)):
        ch = text[i]
        if in_string:
            if escaped:
                escaped = False
            elif ch == "\\":
                escaped = True
            elif ch == '"':
                in_string = False
            continue
        if ch == '"':
            in_string = True
        elif ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                return text[start:i + 1]
    return None

配套还需要兼容尾随逗号(LLM 偶发):

repaired = re.sub(r",(\s*[}\]])", r"\1", candidate)

5.3 坑 1 的加强版:兜底绝不能回传原文

这一条是本文最想强调的工程教训。

早期代码的兜底逻辑是"解析失败就把原文当 enhanced_prompt 用":

except json.JSONDecodeError:
    data = {"enhanced_prompt": raw, ...}   # ❌ 危险

这会造成静默的质量事故raw 是一段带围栏的 JSON 文本,生图模型收到后不会报错,而是真的去"画"这串文本,产出的图完全不可用。更糟的是整条流水线不报任何错,日志里只有一行 warning,用户看到的是"生成成功"。

正确做法是回退到确定性模板——它不聪明,但保证产出一条语法正确、语义完整的生图指令:

# src/engines/llm_engine.py
data = extract_json_object(raw)
if not isinstance(data, dict):
    # 关键:解析失败时绝不能把原文当 Prompt。
    # 原文是带围栏的 JSON 文本,直接喂给生图模型会产出废图且不报错。
    logger.warning("LLM 输出无法解析为 JSON,回退到确定性模板")
    data = json.loads(
        self._default_prompt(request, layout_desc, style_desc, lang_hint)
    )

实测中这条兜底真的被触发过:run_demo.py 的一次运行里整条 LLM 链都超时(日志显示 模型 sensenova-6.7-flash-lite 调用超时(>22s)),系统用默认模板继续生成,最终成品仍拿到 8.8/10。

可迁移的经验:降级路径的产物必须满足同样的输出契约。回传原文虽然"保留了信息量",但契约已经破坏——下游期待的是一条自然语言指令,收到的却是一段结构化数据。


六、图像生成:接口约束与安全过滤

6.1 坑 2:尺寸必须是 32 的倍数

生图接口约束:宽高均为 32 的整数倍、单边 ∈ [512, 4096]、宽高比 ≤ 3:1。项目早期的 4:3 / 3:4 用了 2400x18001800x2400,其中 1800 = 32 × 56.25,直接被拒:

在这里插入图片描述

图 7:2400x1800 返回 HTTP 400 并给出完整约束说明;2368x1760 在 30.3 秒内正常返回图片。

修正后的尺寸表:

宽高比修正前修正后合法性
16:92752×15362720×1536
9:161536×27521536×2720
1:12048×20482048×2048
4:32400×18002368×1760
3:41800×24001760×2368

更重要的是在本地提前拦截。一次非法请求要花掉一次完整的远端往返,在免费额度下这是实打实的浪费:

def validate_size(size: str) -> None:
    w, h = (int(x) for x in size.lower().split("x"))
    if w % 32 or h % 32:
        raise ValueError(f"尺寸 {size} 非法:宽高必须都是 32 的整数倍")
    if not (512 <= w <= 4096 and 512 <= h <= 4096):
        raise ValueError(f"尺寸 {size} 非法:单边需在 [512, 4096] 之间")
    if max(w, h) / min(w, h) > 3.0:
        raise ValueError(f"尺寸 {size} 非法:宽高比超过 3:1")

4:3 恰好是个容易踩坑的比例:直觉上的 2400x18002048x1536 都不满足 32 倍数或范围约束,必须换算到 2368x1760 这类"看起来不整"的值。

6.2 坑 3:自定义参数要走 extra_body

watermark 是 SenseNova 的自定义参数,OpenAI SDK 不认这个关键字,直接传会抛:

TypeError: Images.generate() got an unexpected keyword argument 'watermark'

必须通过 SDK 提供的扩展通道传递:

resp = self.client.images.generate(
    model=self.image_model,
    prompt=prompt,
    size=size,
    n=1,
    extra_body={"watermark": SENSENOVA_CONFIG["image_watermark"]},
)

这是 OpenAI 兼容生态的通用经验:凡是平台自定义字段,一律走 extra_body / extra_query,而不是指望 SDK 为每个厂商都开一个具名参数。

6.3 生图返回的是 b64_json

响应默认返回 base64 而非 URL(response_format 可切换,但 url有效期 24 小时的临时链接)。生产环境下落盘用 b64_json 更稳妥,代码需同时兼容两种:

b64 = getattr(image_item, "b64_json", None)
image_url = getattr(image_item, "url", None)
if not b64 and not image_url:
    raise RuntimeError("图片生成接口未返回图片数据")

6.4 安全过滤是随机的,所以重试是有效策略

生图接口内置安全过滤,偶发返回 code 18 sensitive image。这个判定带随机性——同样的 Prompt 多数时候能过。实测 run_demo.py 中确实触发了一次并重试成功:

图片生成第 1 次失败(Error code: 400 - {'error': {'message': 'sensitive image', ...}}),自动重试...

由于接口不暴露随机种子,每次重试都是一次全新采样,重试是这里唯一可行的手段。


七、多模态质检:把主观质量变成可优化的标量

7.1 评分契约

质检模型需要输出一份结构化评分,其中 issues / suggestions 是闭环能否成立的关键:

{
    "score": 8.5,
    "text_accuracy": 8.0,
    "layout_quality": 9.0,
    "visual_appeal": 8.5,
    "readability": 8.0,
    "issues": ["问题1"],
    "suggestions": ["建议1"],
    "passed": true
}

7.2 输入体积必须控制

2K PNG 约 5MB,base64 后约 7MB,直接作为请求体不现实。质检任务判断的是"布局/文字/层次",对分辨率不敏感,因此缩放到最大边 896px 并转 JPEG:

def _encode_image(self, image_path: str, max_side: int = 896) -> str:
    with Image.open(image_path) as img:
        img = img.convert("RGB")
        w, h = img.size
        if max(w, h) > max_side:
            scale = max_side / max(w, h)
            img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=82)
    return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"

体积从约 5MB 降到约 100KB,请求体可控。

7.3 坑 4:超时预算必须匹配真实耗时

这是本项目影响最大的一个坑,因为它是静默失效的。

早期把质检超时设为 25 秒,理由是"不希望质检拖慢流程"。但实测多模态质检的真实耗时是:

在这里插入图片描述

图 8:同一次调用中,两张图的质检分别耗时 34.9 秒和 27.3 秒,均超过旧的 25 秒预算。

也就是说 25 秒几乎必然触发超时。后果不是"报错",而是走降级分支返回 score=6.5, passed=True——流程看起来一切正常,但自动优化迭代从未被触发过。整个闭环被架空,而这从日志外完全看不出来。

可迁移的经验:为降级路径设阈值时,必须先测出该操作的真实耗时分布。凭直觉设的超时值如果落在正常耗时区间内,降级就成了默认路径,而不是兜底路径。

修正后预算设为 90 秒,并保留降级作为最后手段。

7.4 坑 5:预算要按模型切片,否则回退形同虚设

把预算提到 90 秒后还有第二层问题。原实现让每个模型都拿到全部剩余时间

# ❌ 有问题的实现
deadline = time.monotonic() + budget
for model in self._chain:
    remaining = deadline - time.monotonic()
    if remaining <= 0:
        break
    result = call_with_deadline(model, remaining)   # 第一个模型就吃掉全部预算

如果第一个模型卡住,它会耗尽 90 秒,循环因 remaining <= 0 直接 break,备选模型一次都不会被尝试。回退机制写成了一行死代码。

正确做法是按剩余模型数把预算切片,保证每个备选模型都能分到一段时间:

# src/utils/deadline.py
def model_time_slices(total: int, deadline: float, min_slice: float = 20.0):
    for i in range(total):
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            return
        left = total - i
        slice_ = remaining / left
        if slice_ < min_slice:
            slice_ = min(min_slice, remaining)
        yield slice_

修复效果在实测中非常直观:

修复前修复后
主模型 6.8 超时后直接降级 score=6.5备选模型接手,评出 9.2/10

同样是超时,修复前丢掉了一次真实的质检信号,修复后拿到了完整评分。min_slice 的作用是防止切得太碎——在预算快耗尽时,与其给每个模型 5 秒(都来不及返回),不如集中给一个模型一个最小可用窗口。

7.5 推理模型的响应字段不统一

还有一处兼容性问题:不同模型把正文放在不同字段。

  • deepseek-v4-flash:正文在 content,思维链在 reasoning_content
  • sensenova-6.8-flash-litecontent 可能为空,正文在 reasoning 尾部
  • 部分模型的非标准字段会被 Pydantic 收进 model_extra

统一收敛:

def extract_message_text(message) -> str:
    content = getattr(message, "content", None)
    if content:
        return content
    for field in ("reasoning_content", "reasoning"):
        value = getattr(message, field, None)
        if value:
            return value
    extra = getattr(message, "model_extra", None) or {}
    for field in ("reasoning_content", "reasoning"):
        value = extra.get(field)
        if value:
            return value
    return ""

对推理模型,再从其思维链尾部按 Final Output Generation: / Final Output: 等标记提取最终答案。


八、从「重生成」到「定向编辑」:U1.5 原生图像编辑

这是本项目最能体现 U1.5 Lite 差异化能力的一节。

8.1 为什么编辑优于重生成

假设首轮生成已经不错,只是底部文字偏小。两种策略的后果:

重生成(regenerate)定向编辑(edit)
构图/配色重新采样,可能变好也可能变差保留
已画对的部分全部丢弃保留
收敛性无记忆,可能在两个状态间震荡单调改进
成本一次完整生图(30~80s)一次编辑(实测 82s,但保留成果)

关键在于重生成没有记忆:它不知道上一轮哪里画对了,每次都是无条件重新采样。而编辑是在已有结果上做条件生成,天然具备"保留正确部分"的性质。

8.2 接口差异:编辑用 images 数组

/v1/images/edits 与生成端点参数结构不同:它用 images 数组接收输入图(OpenAI SDK 的 images.edit() 用的是单文件 image,不贴合),所以直接发 HTTP:

def edit(self, image_path: str, instruction: str, aspect_ratio: str = "16:9", ...):
    data_uri = self.encode_image_for_edit(image_path)
    payload = {
        "model": self.image_model,
        "images": [{"image_url": data_uri}],
        "prompt": instruction.strip(),
        "n": 1,
        "size": size,
        "response_format": "b64_json",
        "watermark": SENSENOVA_CONFIG["image_watermark"],
    }
    with httpx.Client(timeout=300, verify=False) as client:
        r = client.post(f"{base_url}/images/edits", headers=headers, json=payload)
        r.raise_for_status()
        b64 = r.json()["data"][0]["b64_json"]

两个细节:

  1. 图片必须是带前缀的 data URIdata:image/png;base64,...),裸 base64 会被拒绝
  2. 请求体可能达数 MB,超时要放宽(这里用 300 秒),并在超过阈值时降级为缩放后的 JPEG

8.3 编辑指令的构造:为什么不再调一次 LLM

质检已经给出了 issuessuggestions,直觉上可以用 LLM 把它们润色成更好的指令。本项目没有这么做,理由是成本与收益不成正比:

  • 编辑本身就是一次耗时调用(实测 82 秒),再叠一次 LLM 会让单轮迭代成本近乎翻倍
  • issues / suggestions 已经是针对具体问题的短句,拼接后语义足够明确
  • 拼接是确定性的,不引入新的不确定性;而 LLM 润色可能引入新的幻觉

实现:

@staticmethod
def build_edit_instruction(review) -> str:
    parts = []
    if review.issues:
        parts.append("请修正以下问题:" + ";".join(review.issues[:5]))
    if review.suggestions:
        parts.append("优化方向:" + ";".join(review.suggestions[:5]))
    parts.append("保持原有整体构图、配色和主要文字不变,只针对上述问题做最小化修改。")
    return "\n".join(parts)

最后一句"最小化修改"很关键——它显式约束了编辑的改动范围,避免模型借机重画整张图。

8.4 实测

在这里插入图片描述

图 9:调用 /v1/images/edits 成功,耗时 82.1 秒,输出 2720×1536。输入图 5.3MB,编码为 data URI 后约 6.87MB。

编辑失败时流水线会自动退回整图重生成,不会让整轮迭代作废。


九、迭代控制:Best-of-N 与重试预算

9.1 为什么输出"最高分"而不是"最后一轮"

这是多轮迭代最容易做错的地方。既然质检给了分数,自然会想"一直迭代到达标为止"。但实测观察到一个反直觉的现象:后续轮次的得分并不保证更高

原因不难理解:每轮的编辑/重生成都带有随机性,本轮修好了文字,可能顺带破坏了布局。如果无条件采用最后一轮,就可能出现"越改越差"却仍然输出最差结果的情况。

所以流水线的策略是 Best-of-N:记录每一轮的结果,最终返回整条链中得分最高的那张。

best = None
for iteration in range(1, max_iterations + 1):
    image_url, image_path = ...            # 生成或编辑
    review = self.reviewer.review(image_path, prompt_result)
    if best is None or review.score > best[0]:
        best = (review.score, image_url, image_path, review)
    if review.passed:
        break
    # 准备下一轮的编辑/重生成输入

同时把每一轮的得分、四维度、耗时、issues 记录在 result.history 中,既是可观测性来源,也是做质量分析的数据基础。

9.2 首轮失败不判死

早期实现里,首轮生图失败就直接返回失败。但生图失败常是瞬时问题(限流、敏感词拦截),而 max_iterations 本身就是一份重试预算。改成把剩余轮次当作预算继续尝试后,run_demo.py 中那次 sensitive image 拦截才得以自动恢复。

实测日志里这条路径确实被走到:

Retrying ... as it raised RateLimitError: Error code: 429 -
{'error': {'message': 'Allocated quota exceeded, please increase your quota limit.', ...}}

十、ComfyUI:可选的本地后端

10.1 双格式工作流

项目提供两种格式,覆盖两种使用方式:

  • infographic_default.jsonAPI 格式,直接 POST /prompt 提交
  • infographic_default_ui.jsonUI 格式,可直接拖入 ComfyUI 编辑器查看

只提供 API 格式是个常见的可复现性缺口——读者拿到 JSON 却无法在编辑器里看到它长什么样,也就无从修改。

10.2 已修复的两个路径 bug

ComfyUI 路径无法在本机实测(无 GPU),因此更依赖代码审查。审查中发现并修复了两个问题:

① 产物目录没传下去。 客户端把图片落到默认的 ./output,而不是本次请求的 run_dir,导致 GET /api/image/{request_id} 按 request_id 查不到图。修复后 output_dir 从 pipeline 一路传到 extract_output_images()

image_url 用错了文件名。 早期用本地保存后的文件名(带 comfyui_<ts>_ 前缀)去拼 /view 链接,但服务端上根本没有这个名字的文件。修复后改用服务端返回的真实 filename

def build_view_url(self, filename: str, subfolder: str = "", img_type: str = "output") -> str:
    params = {"filename": filename, "subfolder": subfolder, "type": img_type}
    return f"{self.base_url}/view?{urllib.parse.urlencode(params)}"

这两个 bug 的共同特征是:只在非默认路径下暴露,单看代码逻辑很容易忽略。

10.3 诊断信息要可执行

早期 UI 只显示"未检测到",读者无法判断是没装、没启动还是端口不对。现在 diagnose() 区分失败原因并给出下一步:

在这里插入图片描述

图 10:点击「检测 ComfyUI 状态」后的提示。ComfyUI 是可选后端,未部署时明确告知将使用云端 API,属于预期行为而非错误。

except httpx.ConnectError:
    return False, (
        f"连不上 {self.base_url}(连接被拒绝)。\n\n"
        "ComfyUI 未启动,或监听地址/端口与这里不一致。请确认:\n"
        "1. ComfyUI 已启动:`python main.py --listen`\n"
        "2. 端口一致(默认 8188;若改过端口请同步修改上方地址)"
    )

十一、工程坑汇总

#现象根因处置
1contentNone推理模型把正文放 reasoning,DeepSeek 放 reasoning_contentextract_message_text() 统一收敛 4 种字段来源
2生图 400 invalid size尺寸非 32 倍数(旧 4:3 用 2400×1800)改用 32 倍数尺寸 + 本地 validate_size() 预检
3unexpected keyword argument 'watermark'平台自定义参数不在 SDK 具名参数里extra_body
4LLM JSON 解析失败后产出废图兜底把原始 JSON 当 Prompt,输出契约被破坏回退确定性模板;稳健解析(平衡括号 + 尾随逗号)
5质检几乎总是降级超时 25s 落在正常耗时(27~35s)区间内预算提到 90s;降级仅作兜底
6模型回退从未生效每个模型都拿全部剩余预算,首个卡住即耗尽model_time_slices() 按剩余模型数切片
7结果页报错 numpy.core.multiarray failed to importStreamlit 自带 pyarrow 针对 numpy 1.x 编译,环境是 numpy 2.4表格改用 Markdown,绕开 pyarrow
8st.image() got an unexpected keyword argument1.50 起 use_column_width 改名为 use_container_width按运行时签名选择参数名

后两个属于前端与依赖的版本兼容问题,它们的共同特点是代码逻辑没错,但与运行环境的版本组合冲突,且都表现为"生成成功但页面报错",极易误判成生图有问题。

顺带一提,浏览器控制台的 net::ERR_HTTP2_PROTOCOL_ERROR 就是这类脚本异常的下游症状——渲染中断后前端资源加载失败,看起来像网络问题,实际根源在 Python 侧。排查时应先看 Streamlit 服务端日志


十二、实测数据与对比

12.1 端到端生成

测试主题布局 / 风格尺寸质检耗时
16:9 数据仪表盘2026 中国 AI 大模型产业data_dashboard / tech2720×15369.490.9s
9:16 综合信息图地球日环保小知识infographic / educational1536×27209.190.0s
3:4 对比线上 vs 线下教学comparison / educational1760×23689.4114.4s
run_demo.py 示例 1AI 大模型产业趋势data_dashboard / tech2720×15368.8221.2s
run_demo.py 示例 2地球日环保小知识infographic / educational1536×27209.0167.5s

在这里插入图片描述

图 11:端到端生成日志,含 Prompt 工程、生图、质检三个阶段的 HTTP 200 与最终得分。

在这里插入图片描述

图 12:python run_demo.py 第 1 个示例的成品图,质检 8.8/10。这一轮 LLM Prompt 工程整条链超时,走了确定性模板兜底,成品仍然可用。

耗时区间较宽(90~220 秒)的原因是后两次运行触发了限流重试与模型回退。顺利时单轮约 90~115 秒。

12.2 耗时构成

阶段实测说明
Prompt 工程10 ~ 30 s触发模型回退时更久
信息图生成(2K)30 ~ 80 s与分辨率、Prompt 长度相关
多模态质检27 ~ 35 s超时回退时更久
原生图像编辑82 s单次实测

12.3 消融对比

变量取值结果
超时预算25 s质检几乎必然降级(真实耗时 27~35s),闭环失效
超时预算90 s + 按模型切片主模型超时后备选接手,评出 9.2/10
生图尺寸2400×1800HTTP 400
生图尺寸2368×1760HTTP 200
JSON 兜底回传原文产出废图且不报错(静默失败)
JSON 兜底确定性模板产出可用信息图(实测 8.8/10)
迭代输出最后一轮可能越改越差
迭代输出Best-of-N始终给出全程最高分

12.4 离线测试

在这里插入图片描述

图 13:53 项离线测试通过。流水线、ComfyUI、质检相关用例使用打桩引擎/打桩 HTTP 客户端,无需联网、GPU 或 ComfyUI 即可验证编排逻辑。

其中两条前端用例值得一提——它们用 Streamlit 官方 AppTest 无头加载真实 app.py 并断言零异常,正是这类测试在改代码时拦下了 st.image 参数名问题:

def test_streamlit_app_loads_without_exception():
    from streamlit.testing.v1 import AppTest
    app = AppTest.from_file(os.path.join(ROOT, "app.py"), default_timeout=120).run()
    assert not app.exception, [e.value for e in app.exception]

在这里插入图片描述

图 14:修复后 app.py 无异常执行完成,成功渲染信息图与迭代明细表格。


十三、设计取舍:为什么不用 X

不用 LangChain / Dify:闭环每一步(重试、回退、降级、超时切片、择优)都需要精细控制,框架的抽象反而增加调试难度。且本项目只依赖 openai、httpx、pydantic、streamlit、fastapi 等基础库,部署简单。若未来要接入搜索、代码执行等复杂工具,框架的工具链生态会更有优势。

编辑指令不用 LLM 润色:见 8.3,成本翻倍而收益不确定,且会引入新的不确定性来源。

质检回退不跨模态:见 4.1,回退到纯文本模型会直接报错。

不把 max_iterations 当成"一直迭代到达标":见 9.1,配合 Best-of-N 使用,迭代次数应该是"采样预算"而非"达标承诺"。


十四、局限性与可改进方向

诚实说明本项目的边界:

  1. ComfyUI 本地后端未在 GPU 环境实测。代码路径已实现、加了双格式工作流与单元测试,但 8B 模型需要 36GB+ 显存,作者当前只有云端 API 的验证条件。这条路径仍属"未经真机验证"。
  2. 质检本质是主观打分。多模态模型给出的是它自己的判断,可作为优化信号,但不能替代人工终审。引入 OCR 做文字客观校验是自然的下一步。
  3. 编辑的精细控制有边界。实测能执行"改配色、加粗标题"这类宏观指令,但"只改左下角第三个字"级别的局部控制仍有挑战。
  4. 免费额度会限制压测规模。实测中已遇到 429 insufficient_quota,这也是耗时数据波动大的原因之一。
  5. sensenova-u1-fast 未纳入对比。它是 U1 系的专用信息图模型,本次未做与 U1.5 Lite 的横向质量/耗时对比,是后续值得补的实验。

可改进方向:接入 OCR 做客观文字校验;持久化质检历史以分析不同布局/风格的得分分布;为财报、科普等常见主题沉淀 Prompt 模板库;在 GPU 环境补全 ComfyUI 与 8 步 LoRA 的实测数据。


十五、总结

回到开头的问题:如何让信息图生成从"抽奖"变成"交付"。本文给出的答案不是更好的 Prompt,而是一套结构

  1. 用判别器换可靠性:利用生成与判别的不对称性,让多模态模型打分并定位问题,把主观质量变成可优化的标量
  2. 用编辑器换收敛性:借助 U1.5 Lite 的原生图像编辑,在保留已有成果的前提下定向修正,避免重生成的"无记忆重新采样"
  3. 用 Best-of-N 换稳定性:承认每轮都有随机性,输出全程最高分而不是最后一轮
  4. 用确定性兜底换可用性:降级路径必须满足相同的输出契约,绝不能为了让流程"不中断"而回传破坏契约的中间产物
  5. 用可观测换可调试:超时、降级、回退、择优全部记录到 history,让静默失效无处藏身

第 4 点和第 5 点其实是同一件事的两面:本文踩过的几个最隐蔽的坑(回传原始 JSON、超时阈值落在正常耗时区间、回退写成死代码)都不是崩溃,而是静默地给出错误结果。工程上真正危险的从来不是报错,而是"看起来成功了"。

一句话总结:给生图模型配一个会打分的判别器和一支会修改的编辑器,再让流水线只交付最高分的那张——轻量多模态模型从"展厅"走向"车间",靠的正是这种把随机性关进工程结构里的能力。


附:快速复现

# 1. 安装依赖
pip install -r requirements.txt

# 2. 配置密钥
cp .env.example .env
# 编辑 .env 填入 SENSENOVA_API_KEY

# 3. 离线测试(不联网、不需要 GPU)
python -m pytest tests/ -v

# 4. 冒烟测试(验证 API Key 与 LLM)
python run_test.py

# 5. 启动 Web UI
streamlit run app.py

# 或启动 API 服务,访问 http://localhost:8000/docs
python run_api.py

# 或命令行批量演示
python run_demo.py

参考资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青霄客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值