做完你会得到什么
一个「照片绘本」App:选一张家里小猫的照片,半分钟后得到一页绘本——题目、两三段小故事,配一张温暖绘本风的插画。不满意可以「换个结局」,或者换成「中国水墨」风格「重画插画」。

图为执行最终 .aia 里的真实积木生成的布局预览,不是真机截图;AI 的回答与照片、生成的图片都是示意。
| 难度 | 用时 | 前置 | 用到的模型 |
|---|---|---|---|
| ★★★ | 60 分钟 | 第 3 课(JSON 解析)、第 4 课(文生图) | DeepSeek(看图写故事)+ 豆包 Seedream(画插画) |
接力是怎么跑的
照片 ──压缩──▶ DeepSeek.ChatWithImage ──GotText──▶ 解析 JSON
├─ title → 标签_故事题目
├─ story → 标签_故事
└─ scene ──+风格──▶ Doubao.GenerateImage ──GotImage──▶ 图像_插画
第一棒让 DeepSeek 做它擅长的「看懂照片、写故事」,并顺手写一句画面描述(scene);第二棒豆包不用看照片,只按这句描述画。这样插画和故事内容一定对得上。
为什么不直接把照片交给豆包图生图?因为图生图会照着照片的构图画,画出来是「照片变画风」(那是第 5 课);这里要的是故事里的场景——小猫坐在月亮船上,照片里可没有月亮船。
准备
| 准备什么 | 说明 |
|---|---|
| DeepSeek API Key | 同第 1 课(标签名 deepseekKey) |
| 火山方舟 API Key | 同第 4 课(标签名 doubaoKey),并已开通 Seedream |
两个 Key 用和前几课一样的标签名存在微数据库里。同一台手机上做过前几课的话,打开本课 App 两个 Key 已经在了——前提是用同一个 App(伴侣调试时都是 AI 伴侣这一个 App,所以共用)。
界面组件
| 组件 | 命名 | 关键属性 / 作用 |
|---|---|---|
| 密码输入框 ×2 | 密码输入框_密钥、密码输入框_密钥2 | 各配一个保存按钮 |
| 下拉框 | 下拉框_风格 | 元素字串:温暖绘本,中国水墨,吉卜力动画,儿童蜡笔 |
| 图像 | 图像_照片、图像_插画 | |
| 标签 | 标签_故事题目、标签_故事 | 题目标签也当卡片标题用 |
| 按钮 | 按钮_换结局、按钮_重画 | 默认不启用 |
| DeepSeek(拓展) | DeepSeek1 | 设计器里把 StreamMode 设为假(要完整 JSON) |
| Doubao(拓展) | Doubao1 | 默认属性 |
| Web 客户端 | Web客户端1 | 只用来解码 JSON |
分步搭
第 1 步:两个 Key,一个状态行
两个 Key 各存各的,状态行把「哪个已填、哪个未填」说清楚。
when Screen1.Initialize() {
组件美化1.SetAsCard(垂直布局_密钥)
组件美化1.SetAsCard(垂直布局_照片)
组件美化1.SetAsCard(垂直布局_故事)
// 要拿完整 JSON 再解析,写故事这一步不用流式(设计器里已关掉 StreamMode)
密码输入框_密钥.Text = 微数据库1.GetValue("deepseekKey", "")
密码输入框_密钥2.Text = 微数据库1.GetValue("doubaoKey", "")
DeepSeek1.APIKey = 密码输入框_密钥.Text
Doubao1.APIKey = 密码输入框_密钥2.Text
显示密钥状态()
}
when 按钮_保存密钥.Click() {
var 密钥 = trim(密码输入框_密钥.Text)
if 密钥 == "" {
底部提示条1.Show("先粘贴 DeepSeek 的 API Key")
} else {
密码输入框_密钥.Text = 密钥
微数据库1.StoreValue("deepseekKey", 密钥)
DeepSeek1.APIKey = 密钥
显示密钥状态()
底部提示条1.Show("DeepSeek API Key 已保存")
}
}
when 按钮_保存密钥2.Click() {
var 密钥 = trim(密码输入框_密钥2.Text)
if 密钥 == "" {
底部提示条1.Show("先粘贴火山方舟的 API Key")
} else {
密码输入框_密钥2.Text = 密钥
微数据库1.StoreValue("doubaoKey", 密钥)
Doubao1.APIKey = 密钥
显示密钥状态()
底部提示条1.Show("火山方舟 API Key 已保存")
}
}
procedure 显示密钥状态() {
if DeepSeek1.APIKey == "" or Doubao1.APIKey == "" {
标签_密钥状态.Text = join("还没填完:DeepSeek ", choose(DeepSeek1.APIKey == "", "未填", "已填"), ",豆包 ", choose(Doubao1.APIKey == "", "未填", "已填"), "。写故事用 DeepSeek,画插画用豆包,两个都要。")
} else {
标签_密钥状态.Text = "两个都已保存,只存在这台手机上。"
}
}
第 2 步:第一棒——看图写故事,约定三个字段
提示词同时约定内容(给 6 到 10 岁孩子、温暖、200 字左右)和格式(只回 JSON,三个字段)。scene 的要求最讲究:要写清主角、动作、场景,不要出现文字和对话——生图模型画字常常画成乱码。
global 正在忙 = false
global 画面 = ""
when 按钮_拍照.Click() {
照相机1.TakePicture()
}
when 照相机1.AfterPicture(image) {
写故事(image)
}
when 图像选择器_相册.AfterPicking() {
写故事(图像选择器_相册.Selection)
}
procedure 写故事(图片) {
if 正在忙 {
底部提示条1.Show("还在写上一个故事,稍等一下")
} else if DeepSeek1.APIKey == "" or Doubao1.APIKey == "" {
底部提示条1.Show("先在上面把两个 API Key 都填好")
} else {
var 小图 = 图片处理1.Compress(图片, 1024, 80)
if 小图 == "" {
底部提示条1.Show("这张图片处理不了,换一张试试")
} else {
图像_照片.Picture = 小图
图像_插画.Picture = ""
标签_故事题目.Text = "③ 故事"
标签_故事.Text = ""
开始忙("正在看图写故事……")
DeepSeek1.StartNewChat()
DeepSeek1.ChatWithImage("看这张照片,为 6 到 10 岁的孩子写一个温暖的小故事。只输出一个 JSON 对象,不要任何解释,不要代码块标记。对象有三个字段:title 是故事题目(不超过 12 个字);story 是故事正文(200 字左右,分 2 到 3 段);scene 是一句插画画面描述(不超过 60 字,写清主角、动作和场景,不要出现文字和对话)。", 小图)
}
}
}
两个 Key 都填好才开始:第一棒跑完、第二棒才发现豆包没 Key,白白花了一次写故事的钱。
第 3 步:解析,然后交棒
解析与第 3 课相同:从第一个 { 截到最后一个 } 再解码。拿到之后马上调用「画插画」——这就是接力的那一下。
给缺失字段兜底:dictget(结果, "scene", dictget(结果, "title", …)) 的意思是「没有 scene 就用 title,再没有就用一句默认的」,保证第二棒手里一定有东西可画。
when DeepSeek1.GotText(text) {
// 与第 3 课相同:从第一个 { 截到最后一个 } 再解码
var 开头 = startsat(text, "{")
var 结尾 = 0
var 结果 = ""
for i = 1 to textlength(text) {
if segment(text, i, 1) == "}" {
结尾 = i
}
}
if 开头 > 0 and 结尾 > 开头 {
结果 = Web客户端1.JsonTextDecodeWithDictionaries(segment(text, 开头, 结尾 - 开头 + 1))
}
if isdict(结果) and dictget(结果, "story", "") != "" {
标签_故事题目.Text = join("③ ", dictget(结果, "title", "无题"))
标签_故事.Text = dictget(结果, "story", "")
// 画面描述缺失时退回用题目,保证第二棒有东西可画
画面 = dictget(结果, "scene", dictget(结果, "title", "一个温暖的童话场景"))
画插画()
} else {
结束忙("故事没按约定的格式返回,再选一次照片试试。")
}
}
模型在 JSON 字符串里写的 \n,解码后就是真正的换行,标签里会分段显示。
第 4 步:第二棒——画插画
画面描述 + 风格 + 固定要求(色彩柔和、不要文字)拼成生图提示词。「重画插画」只重跑这一棒,不重写故事、不再花 DeepSeek 的钱。
procedure 画插画() {
开始忙("故事写好了,正在画插画……")
Doubao1.GenerateImage(join(画面, "。", 下拉框_风格.Selection, "风格的儿童绘本插画,色彩柔和,画面里不要出现任何文字。"))
}
when 按钮_重画.Click() {
if not 正在忙 and 画面 != "" {
画插画()
}
}
when Doubao1.GotImage(imageUrl) {
图像_插画.Picture = imageUrl
结束忙("完成!可以换个结局,或换一种风格重画插画。")
}
中途验证:选一张照片。状态行先「正在看图写故事……」,几秒后题目和故事出现、状态变成「正在画插画……」,再过十几秒插画出来。
第 5 步:换个结局(多轮对话)
「换个结局」用普通 Chat:对话里已经有照片和上一版故事,只需说「换一个不同的结局,格式不变」。新故事回来走的还是第 3 步的 GotText,于是自动重画对应的插画。
when 按钮_换结局.Click() {
if not 正在忙 {
开始忙("正在换一个结局……")
DeepSeek1.Chat("换一个完全不同的结局,其余要求不变,同样只输出包含 title、story、scene 三个字段的 JSON 对象。")
}
}
procedure 开始忙(状态) {
正在忙 = true
标签_状态.Text = 状态
按钮_拍照.Enabled = false
图像选择器_相册.Enabled = false
按钮_换结局.Enabled = false
按钮_重画.Enabled = false
}
procedure 结束忙(状态) {
正在忙 = false
标签_状态.Text = 状态
按钮_拍照.Enabled = true
图像选择器_相册.Enabled = true
按钮_换结局.Enabled = 标签_故事.Text != ""
按钮_重画.Enabled = 画面 != ""
}
追问里要再提一次格式(「同样只输出包含 title、story、scene 的 JSON」),否则模型第二轮很容易放飞成一段普通文字。
第 6 步:分清是哪一棒出的错
两个模型的错误都进同一个「出现错误时」。用 functionName 区分:豆包生图是 GenerateImage,DeepSeek 是 ChatWithImage 或 Chat。JSON 解码失败(JsonTextDecodeWithDictionaries)会紧接着在 GotText 里按「格式不对」处理,这里跳过,免得提示两遍。
when Screen1.ErrorOccurred(component, functionName, errorNumber, message) {
// 解码失败会紧接着在 GotText 里按「格式不对」处理,这里不重复提示
if functionName != "JsonTextDecodeWithDictionaries" {
结束忙(join(choose(functionName == "GenerateImage", "画插画", "写故事"), "失败:", 错误说明(message)))
}
}
function 错误说明(message) {
return choose(containstext(message, "401"), "API Key 不对,检查是否抄全。", choose(containstext(message, "402"), "DeepSeek 账户余额不足。", choose(containstext(message, "ModelNotOpen"), "生图模型还没开通,到火山方舟「开通管理」开通 Seedream。", choose(containstext(message, "Sensitive"), "内容被判定为不适合生成,换一张照片试试。", choose(containstext(message, "429"), "请求太频繁了,等几秒再试。", message)))))
}
第二棒失败时故事已经在屏幕上了,「重画插画」仍然可点,直接重试第二棒即可。
这一课的要点
- 让第一个模型为第二个模型写输入:
scene字段就是写给豆包的提示词。这是多模型 App 最常用的模式。 - 各司其职:看图写作给对话模型,画画给生图模型;能只重跑一棒就不全部重来。
- 结构化输出 + 多轮对话:追问时重申格式,
GotText一处解析,所有入口共用。 - 出错要说清是哪一步:
functionName区分来源,失败后保留已经得到的结果。
常见问题
| 现象 | 原因与处理 |
|---|---|
| 提示「先在上面把两个 API Key 都填好」 | 两个都要。状态行会写哪个未填 |
| 「写故事失败:API Key 不对」 | DeepSeek 的 Key 有问题;「画插画失败:API Key 不对」则是火山方舟的 |
| 故事出来了,插画一直没出 | 第二棒在画,十几秒到几十秒;失败时状态行会写「画插画失败:…」,点「重画插画」重试 |
| 「故事没按约定的格式返回」 | 偶发。DeepSeek 的 StreamMode 要是假(设计器里关掉),流式时 GotText 不会触发 |
| 插画和故事对不上 | 检查 画面 有没有取到 scene;也可以在提示词里要求 scene「必须描写故事高潮的场景」 |
| 插画里有乱七八糟的字 | 生图模型画字不可靠。scene 和生图提示词里都写「不要出现文字」 |
| 换结局后故事没变 | 追问里没重申 JSON 格式,模型回了普通文字,被当成「格式不对」。照第 5 步的写法 |
平台说明
- 安卓、鸿蒙都能运行;两个拓展组件在鸿蒙上都有同名实现。
- 业务检查见验证记录:两个 Key 的状态与保存、少一个 Key 不开始、三个字段解析与换行保留、风格拼进生图提示词、只重画不重写、换结局带着照片与上一版、缺 scene 兜底、格式错误与截断、两棒分别出错的提示都逐条执行过。
学完以后
7 课做完,你已经掌握了把大模型接进 App 的几乎所有套路:识图、思考、结构化输出、生图、图生图、视频、多模型接力。回到专题首页看看还能怎么组合。
扫码添加客服咨询