第 1 课 拍照识物:拍一张照片,让 AI 说出这是什么 | 大模型多模态专题

返回专题

做完你会得到什么

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

拍照识物界面预览

图为执行最终 .aia 里的真实积木生成的布局预览,不是真机截图;AI 的回答与照片、生成的图片都是示意。

难度 用时 前置 用到的模型
★ 30 分钟 会拖按钮、写「当按钮被点击」 DeepSeek(deepseek-flash,文字和识图同一个模型)

下载本课 .aia(VisionSnap.aia)

下载的 .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 的思考模式,让它先推理再讲解,思考过程和讲解分开显示,还能「只讲思路、不给答案」。

文档反馈