基于AI Agent的童话编剧与绘本生成器(四)与后端接入:从独立模块到可服务化系统
一、为什么需要后端接入?
前三篇博客中,我们构建的图像生成模块是“自给自足”的——直接运行 test.py 就能生成一本绘本。但这只是一个脚本,离一个可用的产品还有距离。
真正的绘本生成系统需要:
-
接收用户的创作请求,调用 LLM 生成分页剧本,然后自动配图;
-
多个用户同时使用时,互不干扰(不同故事的参考图、输出目录要隔离);
-
后端能判断图像生成能力是否可用(例如 API Key 有没有配置);
-
即使某页生成失败,也不影响其他页,系统要能优雅降级。
这一篇,我将讲解如何把前几篇的图像生成模块集成到队友的后端服务中,让它成为一个可以被 API 调用的“黑盒”。
二、整体架构:三层各司其职
接入后端的核心目标是:让上层的剧本生成服务(LLM)能够无感知地调用图像生成能力。
我设计了三层架构:
┌─────────────────────────────────────────────────────
│ 上层:故事生成服务(LLM)
│ 输出:story_id + 分页剧本(文本)
└─────────────────────────────────────────────────────
↓
┌─────────────────────────────────────────────────────
│ 中间层:图像生成适配器(story_images.py)
│ 职责:将剧本转换为图像模块的标准输入格式
└─────────────────────────────────────────────────────
↓
┌─────────────────────────────────────────────────────
│ 底层:图像生成执行器(image_generation.py)
│ 职责:动态导入图像模块,管理输出目录,处理并发与错误
└─────────────────────────────────────────────────────
↓
┌─────────────────────────────────────────────────────
│ 图像模块核心(generator.py / Image_generator.py)
│ 职责:调用 Qwen API,一致性评分,重试机制
└─────────────────────────────────────────────────────
每一层都有明确的边界,上层不依赖下层的实现细节。
三、核心一:标准化内部接口
在前一篇博客中,我们将图像模块封装成了 ImageGeneratorService(image_generator文件中),它有一个 generate_page 方法,接收结构化的 payload,返回一个字典。这个字典是图像模块内部与后端执行层之间的契约,而不是最终对前端暴露的 API 响应。
返回格式json示例(成功时):
{
"success": true,
"image_path": "/tmp/generated_images/page_01_try2_abc123.png",
"seed_used": 12047,
"min_score": 85,
"details": { "hero": {"score": 85, "passed": true} }
}
失败时:
{
"success": false,
"error": "Qwen image call failed: 429 Too Many Requests",
"image_path": null
}
其中 min_score 和 details 来自 Qwen-VL-Max 的评分结果。图像模块在生成每一页后,会用 Qwen-VL-Max 对比每个角色的参考图与生成图,给出 0-100 的分数。min_score 是所有角色分数的最小值(短板分数),details 记录每个角色的具体得分和判断理由。
为什么需要这个统一格式?
因为图像模块可能会因为多种原因失败(API 错误、评分不通过等),执行层需要根据 success 和 image_path 决定下一步:将临时文件移动到最终目录,或记录错误日志。这个格式让执行层不需要关心图像模块内部的复杂逻辑。
四、核心二:适配层——将 LLM 文本剧本转成标准 payload
上层 LLM 输出的是分页文本的json,例如:
{
"title": "小红帽的冒险",
"pages": [
{"chapter_title": "出发", "text": "小红帽走进蘑菇森林,开心地笑着。"}
]
}
我们需要将它转换成图像模块能接受的 payload。这部分逻辑在 story_images.py 的 build_page_payloads_from_story 中完成。
几个关键设计点:
4.1 中文提示词优先
前几篇博客使用的是英文提示词,但实际测试发现,Qwen 对中文的理解更准确,生成结果也更贴合儿童绘本风格。因此适配层直接定义了中文风格和负向提示词:
_ILLUSTRATION_STYLE = "儿童绘本插画,柔和水彩质感,色彩温暖明亮,线条干净,造型卡通可爱..."
_NEGATIVE = "模糊,低清晰度,丑陋,变形,肢体畸形,多余手指,照片级写实,恐怖,血腥,水印..."
4.2 稳定的角色 ID 和种子
-
每个故事的主角用一个唯一的 ID:
{story_id}_hero。这样不同故事的参考图不会相互覆盖。 -
种子基准值从
story_id派生:int(hashlib.md5(story_id.encode()).hexdigest()[:8], 16)。相同的故事总能得到相同的种子序列,便于调试和复现。
4..3 自动判断是否为动物主角
如果主角包含“狮子”、“兔子”、“鸟”等关键词,则设置 face_enabled=False。这个字段会传给 Qwen-VL-Max 评分器,告诉它“这是一个动物角色,不需要检测人脸”,评分时会基于整体外观(羽毛颜色、体型、眼睛形状等)进行判断。
4.4 动作描述提取
每页的文本可能较长,我会截取前 400 字符作为 action 字段,足够描述角色的姿态和表情。这个 action 会填入 characters 列表中,作为该页该角色的动作描述。
经过适配层后,每个页面得到一个完整的 payload,后续执行层可以直接使用。
五、核心三:执行层——动态导入、目录隔离与并发控制
image_generation.py 是整个接入的核心执行器。它承担了比较重的职责,我逐一说明。
5.1 动态导入图像模块
图像模块放在 ImageGenerator/Image_generation 目录,与后端主代码物理隔离。为了调用它,我需要把这个目录临时加入 sys.path,然后动态导入:
ig_path = str(Path(__file__).parents[3] / "ImageGenerator" / "Image_generation")
if ig_path not in sys.path:
sys.path.insert(0, ig_path)
import config as ig_config
from generator import QwenImageGenerator
from Image_generator import ImageGeneratorService
这样做的好处是:图像模块可以独立迭代版本,后端代码不用跟着改。
5.2 输出目录隔离
不同故事的图片不能混在一起。我采用以下目录结构:
-
数据根目录(由
settings.data_dir决定):data/ -
故事图片:
data/images/{story_id}/page_{page_num}.png -
角色参考图:
data/character_refs/{story_id}/{character_id}.png
删除故事时只需删除对应的子目录,干净利落。
5.3 临时覆盖图像模块的配置
图像模块的 config.py 中定义了全局输出目录 OUTPUT_DIR 和 CHARACTER_DIR。为了让每个故事写入自己的隔离目录,我需要在调用前临时修改这些配置,调用完再恢复:
prev_out = ig_config.OUTPUT_DIR
prev_char = ig_config.CHARACTER_DIR
try:
ig_config.OUTPUT_DIR = str(story_image_dir)
ig_config.CHARACTER_DIR = str(story_char_dir)
# 调用图像模块...
finally:
ig_config.OUTPUT_DIR = prev_out
ig_config.CHARACTER_DIR = prev_char
5.4 线程锁保护
由于修改了全局配置,且动态导入不是完全线程安全的,我使用 threading.Lock 确保同一时刻只有一个故事在执行图像生成。这牺牲了一定的并发度,但换来了稳定性。
5.5 文件移动与错误降级
图像模块返回的图片路径是临时生成的(如 page_01_try2_abc123.png),我需要把它移动到标准位置 {page_num}.png。移动时要注意:
-
如果目标已存在(例如重试后新文件覆盖旧文件),先删除旧文件;
-
如果移动失败(比如跨设备),则降级为复制。
dest = story_image_dir / f"{page_num}.png"
if dest.exists():
dest.unlink()
shutil.move(src, dest) # 或 shutil.copy2
六、核心四:配置管理
后端使用 pydantic-settings 管理配置,图像相关的配置项有:
-
dashscope_api_key:DashScope 的 API 密钥(二选一) -
image_model_name:图像生成模型,默认qwen-image-2.0-pro -
image_vl_model_name:一致性评分模型,默认qwen-vl-max
这些配置通过 .env 文件或环境变量注入
在执行层中,我从 settings 读取密钥,并设置环境变量 DASHSCOPE_API_KEY,因为图像模块内部是从环境变量读取的。
七、核心五:错误处理与降级策略
图像生成涉及远程 API 调用,容易失败(限流、网络超时、参数错误等)。我设计了多层容错:
-
API 密钥缺失:直接跳过图像生成,日志记录,不抛异常。
-
模块导入失败:打印警告,跳过。
-
单页生成失败:捕获异常,记录日志,继续处理下一页(不中断整个故事)。
-
文件移动失败:降级为复制,仍能保留结果。
-
重试机制:图像模块内部已有重试(Qwen-VL 评分不达标自动重画),执行层不再重复。
这样,即使图像生成全面不可用,后端仍然能返回剧本文字,前端可以显示“图像生成暂时不可用”的占位图。
八、与 LLM 剧本生成模块的衔接
在 story.py(故事生成服务)中,LLM 生成分页剧本后,会调用 try_render_story_images。这个函数会:
-
调用适配层构建
payload列表; -
调用执行层
render_story_images_from_payloads生成图片; -
图片写入
data/images/{story_id}/目录; -
前端通过
/api/v1/assets/images/{story_id}/{page_num}.png访问。
关于异步的思考:图像生成耗时较长(每页约 5-8 秒,整本 40-60 秒)。如果同步执行,API 会超时。生产环境中应当将图像生成作为后台任务(如 Celery)异步执行,用户先看到剧本,稍后刷新看到配图。目前我的实现仍为同步,后续会改进。
九、总结
这一篇,我将独立的图像生成模块成功地集成到了后端服务中。核心设计思路:
-
分层解耦:LLM 输出 → 适配层 → 执行层 → 图像核心,每层只关心自己的契约。
-
内部接口标准化:图像模块返回统一的
{success, image_path, ...}字典,执行层根据这个结果进行后续操作,避免了接口歧义。 -
目录隔离:通过临时覆盖配置,每个故事的图片和参考图都放在独立的目录中。
-
动态导入:使得图像模块可以独立演进。
-
优雅降级:即使图像生成失败,故事文字服务仍然可用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)