做完你会得到什么
一个「拍照识物」App:拍一张照片(或从相册选一张),几秒后 AI 一段一段地说出这是什么、有什么特点;接着问「它多少钱?」「怎么养?」,AI 记得刚才那张图。

图为执行最终 .aia 里的真实积木生成的布局预览,不是真机截图;AI 的回答与照片、生成的图片都是示意。
| 难度 | 用时 | 前置 | 用到的模型 |
|---|---|---|---|
| ★ | 30 分钟 | 会拖按钮、写「当按钮被点击」 | DeepSeek(deepseek-flash,文字和识图同一个模型) |
下载的 .aia 已经带好 LLMAI2Ext 拓展,导入就能用;只需要在 App 里粘贴你自己的 API Key。
准备
| 准备什么 | 说明 |
|---|---|
| DeepSeek API Key | 到 DeepSeek 开放平台 注册,「API keys」里点「创建」,复制 sk- 开头的那串。新账号要先充值才能调用;按 token 计费,一张压缩后的图只折算几百个 token。 |
| 一台安卓或鸿蒙手机 | 装好 AI 伴侣,能连上本平台调试即可(连接方法) |
| LLMAI2Ext 拓展 | 从零自己搭时才需要:到 LLMAI2Ext 文档 下载 .aix,在设计器「拓展」里导入。导入后调色板里会出现 DeepSeek、Kimi、Qwen、Doubao 四个组件 |
API Key 等于你的钱包钥匙:只填在自己手机的 App 里,不要写死在积木里再把 .aia 发给别人。本课把它存在手机上的微数据库里,App 分享出去也不会带上。
界面组件
| 组件 | 命名 | 关键属性 / 作用 |
|---|---|---|
| 密码输入框 | 密码输入框_密钥 | 粘贴 API Key,输入时显示成圆点 |
| 按钮 | 按钮_保存密钥、按钮_拍照、按钮_追问 | |
| 图像选择器 | 图像选择器_相册 | 外观和按钮一样,点了打开相册 |
| 文本输入框 | 文本输入框_问题 | 默认文字:这是什么?先说名称,再用两三句话介绍。 |
| 图像 | 图像_预览 | 高 220,缩放图片以适应 |
| 标签 | 标签_状态、标签_回答 | 回答放在垂直滚动条布局里,长了能上下滑 |
| 文本输入框 | 文本输入框_追问 | 提示:接着问,比如:它多少钱? |
| DeepSeek(拓展) | DeepSeek1 | 属性全用默认:ModelName 是 deepseek-flash,StreamMode 为真 |
| 照相机 | 照相机1 | |
| 图片处理 | 图片处理1 | 压缩照片 |
| 微数据库 | 微数据库1 | 存 API Key |
| 底部提示条 | 底部提示条1 | 弹一行提示 |
| 组件美化 | 组件美化1 | 把三个垂直布局变成卡片 |
分步搭
第 1 步:保存 API Key
API Key 存进微数据库,每次打开 App 时读出来设给 DeepSeek1。存之前用 trim 去掉首尾空格——从网页复制时常常带着空格或换行,带着它请求会直接 401。
when Screen1.Initialize() {
组件美化1.SetAsCard(垂直布局_密钥)
组件美化1.SetAsCard(垂直布局_拍照)
组件美化1.SetAsCard(垂直布局_回答)
密码输入框_密钥.Text = 微数据库1.GetValue("deepseekKey", "")
使用密钥(密码输入框_密钥.Text)
}
when 按钮_保存密钥.Click() {
var 密钥 = trim(密码输入框_密钥.Text)
if 密钥 == "" {
底部提示条1.Show("先粘贴 API Key 再点保存")
} else {
密码输入框_密钥.Text = 密钥
微数据库1.StoreValue("deepseekKey", 密钥)
使用密钥(密钥)
底部提示条1.Show("API Key 已保存")
}
}
procedure 使用密钥(密钥) {
DeepSeek1.APIKey = 密钥
if 密钥 == "" {
标签_密钥状态.Text = "还没填:到 platform.deepseek.com 创建 API Key,粘贴到这里点保存。"
} else {
标签_密钥状态.Text = "已保存,只存在这台手机上。"
}
}
中途验证:连上 AI 伴侣,粘贴 Key 点「保存」,状态行变成「已保存,只存在这台手机上」;在伴侣里重新加载项目,Key 还在输入框里。
第 2 步:拍照或选图,压缩后交给 DeepSeek
拍照和选图走同一个过程「识别」。关键是先压缩再发:手机原图动辄三四千像素、好几 MB,原样发出去要转成 Base64 文本,体积再大三分之一,上传慢、token 也多。图片处理.压缩把最长边缩到 1024、质量 80,一般只剩一两百 KB,识物完全够用。
每换一张图先调用 StartNewChat 开新对话。拓展默认是多轮对话,发过的图会留在对话历史里,后面每次提问都会重新发一遍;不开新对话,换了五张图,第五次提问就带着五张图,又慢又贵。
global 正在回答 = false
global 有图 = false
when 按钮_拍照.Click() {
照相机1.TakePicture()
}
when 照相机1.AfterPicture(image) {
识别(image)
}
when 图像选择器_相册.AfterPicking() {
识别(图像选择器_相册.Selection)
}
procedure 识别(图片) {
if 正在回答 {
底部提示条1.Show("上一张还没回答完,稍等一下")
} else if DeepSeek1.APIKey == "" {
底部提示条1.Show("先在上面填好 API Key 并点保存")
} else {
// 手机照片动辄好几 MB:最长边缩到 1024、质量 80,只剩一两百 KB,上传快、也省 token
var 小图 = 图片处理1.Compress(图片, 1024, 80)
if 小图 == "" {
底部提示条1.Show("这张图片处理不了,换一张试试")
} else {
var 问题 = trim(文本输入框_问题.Text)
if 问题 == "" {
问题 = "这是什么?先说名称,再用两三句话介绍。"
}
图像_预览.Picture = 小图
有图 = true
// 换一张图就开新对话:旧图不再跟着后面每一轮重复发送
DeepSeek1.StartNewChat()
开始回答("正在看图……")
DeepSeek1.ChatWithImage(问题, 小图)
}
}
}
这一段用到了下一步才定义的「开始回答」,先接着往下做,第 3 步做完再一起运行。
第 3 步:流式显示回答,回答期间按钮变灰
StreamMode 默认为真,回答会拆成很多小段,每到一段触发一次 GotStream。要把新的一段接在已有文字后面(join(标签_回答.Text, part)),写成 标签_回答.Text = part 就只剩最后一小段——这是最常见的错误。最后一次触发时 part 是空的、finished 为真,在这里把按钮放开。
procedure 开始回答(状态) {
正在回答 = true
标签_回答.Text = ""
标签_状态.Text = 状态
按钮_拍照.Enabled = false
图像选择器_相册.Enabled = false
按钮_追问.Enabled = false
}
procedure 结束回答() {
正在回答 = false
按钮_拍照.Enabled = true
图像选择器_相册.Enabled = true
按钮_追问.Enabled = true
}
when DeepSeek1.GotStream(part, finished) {
标签_回答.Text = join(标签_回答.Text, part)
if finished {
结束回答()
标签_状态.Text = "回答完了。可以在下面接着追问,模型记得这张图。"
}
}
中途验证:对着桌上的杯子拍一张。预览里出现照片,三个按钮变灰,状态行「正在看图……」;两三秒后回答一段段冒出来,说完按钮恢复。
第 4 步:接着追问
追问用普通的 Chat。因为上一步没有开新对话,请求里自动带着刚才的图和回答,问「它多少钱?」模型知道「它」是什么。
when 按钮_追问.Click() {
var 追问 = trim(文本输入框_追问.Text)
if not 有图 {
底部提示条1.Show("先拍一张或选一张图")
} else if 正在回答 {
底部提示条1.Show("还在回答,稍等一下")
} else if 追问 == "" {
底部提示条1.Show("先写下想追问的话")
} else {
文本输入框_追问.Text = ""
开始回答(join("追问:", 追问))
DeepSeek1.Chat(追问)
}
}
中途验证:识完杯子后追问「适合装热水吗?」,回答应该是针对这只杯子的,而不是泛泛地讲杯子。
第 5 步:出错时说人话
Key 不对、余额不足、请求太频繁,都会触发屏幕的出现错误时。拓展报错时错误编号固定是 3300,真正的原因在消息里,格式是 Error 401 in extension LLMAI2Ext: HTTP Error 401: …,所以按消息里的状态码判断。最重要的是先 结束回答(),不然按钮会一直是灰的。
when Screen1.ErrorOccurred(component, functionName, errorNumber, message) {
结束回答()
标签_状态.Text = 错误说明(message)
}
function 错误说明(message) {
return choose(containstext(message, "401"), "API Key 不对:检查是否抄全,或到平台重新创建一个。", choose(containstext(message, "402"), "账户余额不足:到 DeepSeek 开放平台充值后再试。", choose(containstext(message, "429"), "请求太频繁了,等几秒再试。", join("出错了:", message))))
}
中途验证:把 Key 故意删掉最后一位再保存,拍一张:状态行显示「API Key 不对」,按钮恢复可点。
这一课的要点
- 识图就是多一个参数的对话:
ChatWithImage(提示词, 本地图片路径)。照相机的「拍摄完成」、图像选择器的「选中项」、图片处理的返回值,都可以直接当路径用。 - 先压缩再发。识物、识植物用 1024 就够;要看清小字(作业题、说明书)用 1600。
- 一张图一个对话:换图前
StartNewChat,否则旧图会一直跟着重发。 - 流式要拼接:
join(旧文字, part),finished为真时收尾。 - 提示词决定回答的样子。把「想问它什么」改成「用一句话告诉我这是什么,不要多说」,回答就变短;改成「用英语回答并给出音标」,就成了拍照学单词。
常见问题
| 现象 | 原因与处理 |
|---|---|
| 拍完照没反应,底部提示「先在上面填好 API Key」 | Key 没保存,或保存的是空格。粘贴后一定要点「保存」 |
| 状态行「API Key 不对」 | 复制时少了字符,或用了别家平台的 Key。DeepSeek 的 Key 以 sk- 开头,到开放平台重新创建一个 |
| 状态行「账户余额不足」 | DeepSeek 开放平台是预付费的,新账号余额为 0,充值后再试 |
| 回答只剩最后几个字 | GotStream 里写成了 标签_回答.Text = part,要用 join 接在后面 |
| 按钮一直是灰的,再也点不了 | 出错时没有恢复按钮:在「出现错误时」里先调用 结束回答() |
| 回答很慢,追问越来越慢 | 照片没压缩,或者换图时没 StartNewChat,历史里攒了好几张图 |
把 ModelName 改成别的模型后报错「可能不支持图片」 |
换成了纯文字模型。DeepSeek 官方识图就用 deepseek-flash(默认值);接第三方服务时,要换成对方的视觉模型名 |
想识别项目素材里的图片(比如 cat.png),直接把素材名传给 ChatWithImage 报「图片处理失败」 |
打包后的素材不是一个能直接读的文件。先 图片处理1.Compress("cat.png", 1024, 80),它认得素材名,返回的是真实文件路径 |
平台说明
- 安卓:AI 伴侣调试与编译 APK 都可以。拍照需要相机权限,第一次会弹窗询问。
- 鸿蒙:鸿蒙版运行时内置了同名的 DeepSeek 组件,同一个工程可以直接编译鸿蒙应用(HAP);图片处理、照相机都有鸿蒙实现。
- 本课 .aia 的积木都经过逐条执行检查,大模型的回答用替身模拟,见验证记录。
下一课
第 2 课 拍题讲解:同样是拍照识图,换成看作业题。打开 DeepSeek 的思考模式,让它先推理再讲解,思考过程和讲解分开显示,还能「只讲思路、不给答案」。
扫码添加客服咨询