资讯详情

资讯详情

iOS视觉精确AI辅导技术实现:从屏幕采集到坐标渲染

最近在 Hacker News 上看到一个很有意思的项目方向Show HN: Visually Precise AI Tutoring on iOS。它核心不是再做一款“拍照搜题”App而是试图把 AI 辅导从“一段文字答案”升级成“能在屏幕上精确指向问题位置”的视觉级交互。这个方向其实击中了当前 AI 教育应用一个很明显的痛点大多数辅导反馈停留在文本语义层模型能告诉你“哪里错了”但没法在真实屏幕上给你画出来。本文不评价具体产品而是把它拆成一条 iOS 工程师可以直接上手的技术链路屏幕内容采集 → 视觉结构化 → 多模态大模型推理 → 视觉结果渲染。无论你是想复刻类似应用还是想给自己的教育类 App 增加 AI 辅导能力都可以按这条链路一步步落地。全文包含可运行的 Swift 代码示例、权限处理、坐标系转换、常见坑点和工程建议适合已经掌握 Swift 基础、想深入 AI iOS 方向的同学阅读和复用。1. “视觉精确的 AI 辅导”是什么1.1 从文本问答到“看见屏幕”常规的 AI 辅导产品交互流程一般是用户拍一道题或截一张图上传给大模型模型返回解析步骤和最终答案。这种方式对“静态题目”效果不错但对“动态操作类学习场景”就有明显短板用户在 App 里点错了按钮、配置错了参数、触发了一个报错弹窗此时单纯把截图发给模型模型只能看到一张孤立图片无法知道你刚才做了什么操作也无法在屏幕上精确标记“这个红色区域就是你出错的地方”。“视觉精确”的 AI 辅导核心变化是把屏幕本身作为上下文。应用持续或按需采集屏幕内容经过结构化处理之后连同用户问题一起交给多模态大模型。模型不仅返回文字步骤还能返回屏幕上的目标区域坐标。iOS 端拿到坐标后在屏幕上渲染高亮框、箭头、序号标注甚至引导用户点击下一个位置。这样一来AI 的回答就从“该怎么做”扩展成了“看这里这就是问题所在然后点这个按钮”。1.2 视觉精确的三个层次理解了方向之后可以把“视觉精确”拆成三个可量化的层次后续架构设计都围绕它们展开。第一层是空间精确。AI 必须能定位到屏幕上的具体元素比如“第二行公式中的 x 符号”“当前页面右上角的保存按钮”“报错弹窗中的关闭图标”。这些信息最终要落到一个矩形区域或坐标点而不是一句含糊的“红色字体部分”。第二层是语义精确。模型需要理解当前屏幕上下文。同样是“保存失败”四个字出现在表单页和出现在代码编辑器里原因完全不同。视觉精确辅导要求模型把文字识别结果、按钮状态、输入框内容、页面结构组合成完整语义而不能只看单独的 OCR 文本。第三层是时序精确。优秀辅导不是单次问答而是多轮交互。学生点击了某个按钮后屏幕状态发生变化AI 需要感知这次变化并基于“前后状态差异”继续指导。例如学生第一次选错了选项界面出现错误提示AI 下一次回答就要能引用这个错误提示区域而不是重复之前的内容。1.3 适用场景与读者定位这类技术适合三类场景一是数学、物理等理科题目辅导模型可以精确指出公式推导中从第几行开始出错二是软件操作类教学例如教用户配置证书、处理 Xcode 打包报错、操作复杂后台系统AI 可以直接高亮界面按钮三是编程入门辅导用户在 iOS 模拟器或在线编辑器里运行代码AI 定位控制台报错并高亮对应代码行。本文面向的读者是有一定 Swift 和 Xcode 使用经验、想进入 AI 应用开发方向、或者正在设计教育类产品交互的开发者。不需要你提前掌握机器学习和 Vision 框架细节但建议你对 SwiftUI 或 UIKit 的 UI 层级、异步网络请求、JSON 解析有基本概念。读完本文后你能搭建出一条最小可用链路并知道每一步的常见坑在哪里。2. iOS 端视觉 AI 应用的整体架构2.1 核心链路采集 → 识别 → 推理 → 渲染整套系统可以抽象为四个模块串成如下链路屏幕采集截图 / ReplayKit ↓ 视觉结构化Vision OCR、元素检测 ↓ 大模型推理多模态大模型 / 文本模型 ↓ 结果渲染覆盖层高亮、坐标标注、步骤展示四个模块职责非常清晰屏幕采集层负责拿到当前屏幕的 UIImage 或视频帧。这里需要区分应用内截图和系统级屏幕录制两种方式权限模型完全不同。视觉结构化层把像素级图片转成 AI 可读的文本与坐标信息。例如 OCR 识别出屏幕上所有文字及其位置这一步是实现“空间精确”的关键。大模型推理层把“用户问题 视觉结构化结果 历史对话”一起发送给大模型让模型返回带坐标引用的结构化答案。结果渲染层把模型返回的归一化坐标转换回屏幕坐标在 UI 上绘制高亮框、文字气泡、操作引导等。这四个模块可以分别开发、分别测试最后再串联。这也是我在实际项目中比较推荐的做法不要一开始就追求完整的 Broadcast Extension 实时链路先做“截图 本地识别 在线推理 静态标注”跑通之后再升级。2.2 技术选型建议围绕四个模块iOS 生态内有比较成熟的选型UI 框架SwiftUI 为主UIKit 兜底。渲染覆盖层用 SwiftUI 的 Canvas 或 overlay 很直观成本低。屏幕采集最简单的是应用内窗口截图使用UIGraphicsImageRenderer即可如果要采集其他 App 的屏幕则必须用 ReplayKit Broadcast Upload Extension。视觉识别优先使用 Vision 框架。它能做文字识别OCR、人脸检测、矩形检测、图片分类。对于通用元素检测甚至可以用 Vision 的VNRecognizeTextRequest先提取文本坐标再用VNDetectRectanglesRequest获取区域。大模型接入通过 URLSession 调用国内或海外主流大模型的多模态接口。注意不同模型的接口格式、图片编码方式、JSON 输出能力差异较大建议在服务端做一层封装客户端只面向统一的协议。结果渲染归一化坐标 SwiftUI Shape 绘制。不要直接使用模型返回的像素坐标而是约定一个归一化坐标体系适配不同屏幕尺寸。2.3 环境准备与隐私前提本文示例基于 Xcode 15 和 iOS 17 环境Swift 版本为 5.x。虽然代码用到了 iOS 17 才完善的一些 API但核心思路在 iOS 15 上也能实现。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。动手开发前有两个隐私前提必须想清楚第一屏幕内容极其敏感。无论是应用内截图还是系统级屏幕录制都必须在用户知情的前提下进行。应用内截图只影响自己 App 范围相对简单系统级录制会弹出系统级“正在共享屏幕”提示产品上要设计清晰的说明文案。第二教育类产品如果面向未成年人还要额外考虑数据最小化原则。能只上传局部截图就尽量不要整屏上传能本地完成 OCR 就不要把原始截图发给服务器。建议在客户端完成视觉结构化只把文本和坐标发送给大模型从源头减少隐私风险。3. 屏幕内容采集截图还是屏幕录制3.1 两种主流方案对比在 iOS 上获取屏幕内容没有真正意义上“万能静默截屏”的公共 API。系统对用户隐私的保护很强开发者只能在系统允许的框架内操作因此不同业务场景方案不同。第一种方案是应用内截图。这种方案最稳定也最容易实现。它只捕获我们自己的 App 窗口内容适合“AI 辅导我们自己的学习页面”比如题库、讲义阅读器、代码练习器、表单填写向导。优点是权限简单无需额外配置描述文件只要 App 在前台即可随时截图。缺点是无法捕获其他 App 的内容不能做跨应用辅导。第二种方案是 ReplayKit Broadcast Upload Extension。这是 iOS 官方提供的屏幕共享录制能力。用户在控制中心或者 App 内点击“开始直播/共享屏幕”后系统会弹出提示然后系统把屏幕视频帧通过扩展传递给开发者。优点是能捕获整个设备屏幕或指定 App适合“AI 辅导用户使用其他软件”。缺点是交互链路复杂、权限提示明显、性能开销大而且扩展进程与主 App 是独立进程数据通信需要额外设计。3.2 方案一应用内截图代码实现应用内截图最直接的方式是拿到当前UIWindow利用drawHierarchy绘制到图形上下文。这里给出一个可以在 SwiftUI 工程中复用的函数import UIKit func captureAppScreen() - UIImage? { // 获取当前活跃的 WindowScene 和 keyWindow guard let windowScene UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .first, let keyWindow windowScene.windows.first(where: { $0.isKeyWindow }) else { return nil } let format UIGraphicsImageRendererFormat() format.scale UIScreen.main.scale format.opaque false let renderer UIGraphicsImageRenderer( bounds: keyWindow.bounds, format: format ) return renderer.image { _ in keyWindow.drawHierarchy(in: keyWindow.bounds, afterScreenUpdates: true) } }这段代码需要注意三点必须在主线程调用否则drawHierarchy可能绘制出空白内容。afterScreenUpdates: true表示等屏幕内容更新完成后再绘制适合捕获最新 UI 状态但如果调用非常频繁会有一定性能损耗。如果界面包含SKScene、MTKView、AVPlayerLayer等独立渲染层drawHierarchy不一定能捕获到内容需要额外处理或换成UIView快照。在 SwiftUI 中可以把它包装成一个Buttonaction 或一个Timer驱动的采集器。截取到的 UIImage 后续会传给视觉结构化模块。3.3 方案二ReplayKit Broadcast Upload Extension如果你确实需要捕获其他 App 的屏幕只能选择 ReplayKit 方案。整体流程如下首先在主 App 中为工程新增一个 Broadcast Upload Extension Target。这个扩展本身并不负责展示任何 UI它只是接收系统传入的屏幕视频帧。Xcode 会自动生成SampleHandler.swift文件核心方法如下import ReplayKit class SampleHandler: RPBroadcastSampleHandler { override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { // 用户点击开始共享后触发这里可以通知主 App 开始接收 } override func broadcastPaused() { // 用户暂停共享时触发 } override func broadcastResumed() { // 用户恢复共享时触发 } override func broadcastFinished() { // 用户结束共享时触发 // 记得清理共享容器中的临时文件 } override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with type: RPSampleBufferType) { // 系统不断把屏幕/音频/App 音频样本传到这里 // 判断 type .video 时从 sampleBuffer 中取出像素缓冲区 guard type .video else { return } guard let pixelBuffer CMSampleBufferGetImageBuffer(sampleBuffer) else { return } // 将该帧转成 JPEG 或写入共享文件再通知主 App 读取 // 注意扩展进程内存受限不要无限制缓存帧 } }扩展与主 App 是独立进程不能直接调用主 App 的单例或内存变量。通常需要用 App Group 共享容器传递数据扩展把视频帧压缩成 JPEG Data写入UserDefaults(suiteName:)或临时文件主 App 通过监听通知或轮询读取。考虑到扩展内存很小建议只保留最近 12 帧或者按需向扩展发送“需要帧”的信号。使用这套方案时要明确告知用户系统会显示屏幕共享状态。产品设计上不能把这种录制伪装成无感知后台截屏这是苹果审核的红线也是用户隐私的基本要求。3.4 权限状态检查无论哪种方案启动采集前都应该检查权限状态。ReplayKit 方案相对特殊因为系统没有提供独立的“屏幕录制权限”检查 API需要在调用相关 API 时捕获错误并通过RPBroadcastActivityViewController引导用户完成授权。对于应用内截图则不需要额外的系统权限。实际开发中更常见的是麦克风权限和相册权限这里不展开。需要强调的是无论使用哪种采集方案应用的《隐私政策》里都应当如实说明屏幕内容的用途、存储方式、上传策略和删除机制尤其是教育类应用涉及未成年人场景时必须格外谨慎。4. 视觉结构化让 AI 看懂屏幕坐标4.1 Vision 框架 OCR 与元素检测拿到 UIImage 之后下一步是把图片转成“文字 位置”的结构化数据。iOS 原生自带 Vision 框架可以离线完成 OCR延迟低且不产生网络流量。下面是一个最小可用的 OCR 函数import Vision func recognizeText(in image: UIImage) - [VNRecognizedTextObservation] { guard let cgImage image.cgImage else { return [] } var observations: [VNRecognizedTextObservation] [] let request VNRecognizeTextRequest { request, error in guard error nil else { return } observations request.results as? [VNRecognizedTextObservation] ?? [] } request.recognitionLevel .accurate request.recognitionLanguages [zh-Hans, en-US] request.usesLanguageCorrection true let handler VNImageRequestHandler(cgImage: cgImage, options: [:]) try? handler.perform([request]) return observations }说明几个细节recognitionLevel .accurate识别精度高但速度稍慢实时预览场景可以改用.fast。recognitionLanguages根据目标用户调整。中文教育场景建议把zh-Hans放在第一位否则默认模型对中文支持不够稳定。usesLanguageCorrection对英文单词纠错有帮助但对中文识别有时候会画蛇添足需要实际测试后决定开关。遍历结果时每个VNRecognizedTextObservation包含两部分关键信息topCandidates(1)能拿到识别文本boundingBox能拿到归一化坐标。下面代码演示如何提取for observation in observations { guard let candidate observation.topCandidates(1).first else { continue } let text candidate.string let box observation.boundingBox print(文本\(text)归一化坐标\(box)) }4.2 归一化坐标与屏幕坐标互转Vision 的boundingBox有一个非常经典的坑坐标系原点在左下角而 UIKit/SwiftUI 的原点在左上角。如果不做转换画出来的高亮框会上下颠倒。转换公式如下// visionBox 是 CGRect取值范围 0~1 // imageWidth / imageHeight 是原始图片像素尺寸 // uiRect 是 UIKit 左上角坐标系下的矩形 let uiX visionBox.minX * imageWidth let uiY (1 - visionBox.minY - visionBox.height) * imageHeight let uiWidth visionBox.width * imageWidth let uiHeight visionBox.height * imageHeight let uiRect CGRect(x: uiX, y: uiY, width: uiWidth, height: uiHeight)这里的核心是先把 Vision 的归一化坐标转换为像素坐标再做纵向翻转。注意翻转时不仅要翻minY还要减去矩形自身高度否则元素会向下偏移一个矩形高度。当你把坐标发给大模型时建议统一使用“归一化坐标 像素坐标”双份描述。给模型看的 JSON 里带上归一化坐标[0.1, 0.2, 0.3, 0.15]便于模型理解相对位置渲染时再转成像素坐标避免因屏幕尺寸不同导致偏移。4.3 构造 AI 可读的视觉状态描述大模型并不能直接理解一个矩形框和一段 OCR 文本之间的语义关系。为了让模型“看懂屏幕”我们需要把 OCR 结果整理成结构化 JSON。建议字段如下{ screen_size: { width: 1170, height: 2532 }, elements: [ { id: 0, type: text, content: 2x 3 7, box: [0.08, 0.31, 0.36, 0.08] }, { id: 1, type: input, content: , placeholder: 请输入答案, box: [0.12, 0.42, 0.28, 0.06] }, { id: 2, type: button, content: 提交, box: [0.42, 0.51, 0.16, 0.06] } ] }关于type字段如果要做得简单可以先用text统一标注如果想更精确可以结合VNDetectRectanglesRequest检测图片中的按钮、卡片、输入框等矩形区域再通过坐标重叠匹配判断类型。不过这一步在最小可行性版本里可以省略先把文本元素做好就够了。生成这段 JSON 的 Swift 代码如下struct ScreenState: Codable { let screen_size: CGSizeProxy let elements: [ElementProxy] } struct ElementProxy: Codable { let id: Int let type: String let content: String let box: [CGFloat] } func buildScreenState(from observations: [VNRecognizedTextObservation], imageSize: CGSize) - ScreenState { var elements: [ElementProxy] [] for (index, obs) in observations.enumerated() { guard let text obs.topCandidates(1).first?.string else { continue } let box obs.boundingBox elements.append(ElementProxy( id: index, type: text, content: text, box: [box.minX, box.minY, box.width, box.height] )) } return ScreenState( screen_size: CGSizeProxy(width: imageSize.width, height: imageSize.height), elements: elements ) }注意示例中的CGSizeProxy和ElementProxy是为了方便 Codable 序列化而定义的简单结构体实际项目中可以按你的 JSON 规范调整。这一阶段做完你手里的素材已经足够让大模型做“基于坐标的精确回答”了。5. 接入多模态大模型把“问题 屏幕状态”交给 AI5.1 提示词设计接入大模型时最常见的错误是直接把截图 Base64 塞给模型然后期待它输出精确坐标。实际效果往往不理想模型对像素坐标的感知不稳定很容易出现“指着空白区域说话”的情况。更稳妥的做法是让模型基于结构化 JSON 做推演再结合局部图片提升理解。以“数学题目辅导”为例系统提示词可以这样设计你是屏幕辅助学习助手。用户会提供当前屏幕的结构化元素列表 每个元素包含 id、type、content 和归一化坐标 box。 你的任务是 1. 根据用户问题分析屏幕中与问题相关的内容。 2. 如果发现了错误或需要强调的位置必须引用对应元素的 id。 3. 禁止编造屏幕上不存在的元素。 4. 输出必须是 JSON字段为 - answer完整辅导文本 - highlights需要高亮的元素 id 数组或归一化框数组 - next_actions建议用户执行的操作列表这段提示词有三个关键设计一是“禁止编造元素”。AI 很容易顺着用户的话编造“右上角那个按钮”即使屏幕上根本没有。明确禁止之后模型会在找不到匹配元素时如实说“当前屏幕中没有找到相关内容”而不是忽悠用户。二是强制 JSON 输出。后续代码解析会方便非常多也容易做字段校验。三是把“错误位置”和“下一步操作”分开。既满足用户“哪里错了”的需求也满足“接下来怎么操作”的指导性需求。5.2 调用接口与代码示例以 OpenAI 兼容的 Chat Completions 接口为例下面给出 Swift 网络请求的核心片段。这个示例目的是演示通用调用方式具体 URL、模型名、鉴权方式需要按你实际使用的服务调整import Foundation struct LLMRequest: Codable { let model: String let messages: [Message] let response_format: ResponseFormat? struct Message: Codable { let role: String let content: String } struct ResponseFormat: Codable { let type: String } } struct LLMResponse: Codable { let choices: [Choice] struct Choice: Codable { let message: Message } } func callLLM(screenStateJSON: String, userQuestion: String, apiKey: String, completion: escaping (ResultString, Error) - Void) { let url URL(string: https://api.example.com/v1/chat/completions)! var request URLRequest(url: url) request.httpMethod POST request.setValue(application/json, forHTTPHeaderField: Content-Type) request.setValue(Bearer \(apiKey), forHTTPHeaderField: Authorization) let systemPrompt 你是屏幕辅助学习助手。用户会提供当前屏幕的结构化元素列表... let userContent 当前屏幕状态 \(screenStateJSON) 用户问题 \(userQuestion) let payload LLMRequest( model: your-model-name, messages: [ LLMRequest.Message(role: system, content: systemPrompt), LLMRequest.Message(role: user, content: userContent) ], response_format: LLMRequest.ResponseFormat(type: json_object) ) request.httpBody try? JSONEncoder().encode(payload) URLSession.shared.dataTask(with: request) { data, _, error in guard let data data else { completion(.failure(error ?? NSError(domain: LLMError, code: -1))) return } do { let response try JSONDecoder().decode(LLMResponse.self, from: data) let content response.choices.first?.message.content ?? completion(.success(content)) } catch { completion(.failure(error)) } }.resume() }这里有几个需要注意的地方response_format字段不是所有模型都支持支持 JSON Output 的模型才能稳定输出合法 JSON。如果不支持就需要在提示词里强调“只输出 JSON不要多余解释”并在解析时做容错处理。生产环境不要把 API Key 写在客户端。正确做法是客户端请求自己的服务端由服务端保存密钥并转发大模型请求避免密钥泄露。如果模型允许视觉输入可以把重要区域的截图裁剪后作为图片一并发送如果只发送结构化 JSON则在真实图表、复杂公式场景下理解能力会受限。最优选择是“结构化 JSON 局部裁剪图”一起送既控制 token 又提升准确率。5.3 响应解析与容错模型输出的 JSON 不一定严谨常见问题包括多了一个尾逗号、用单引号代替双引号、在 JSON 前后混入解释性文字。解析时要做好容错。下面是一个解析示例使用JSONSerialization先做一次宽松解析失败后再尝试提取 JSON 片段func parseLLMResponse(_ content: String) - [String: Any]? { // 先直接尝试解析 if let data content.data(using: .utf8), let json try? JSONSerialization.jsonObject(with: data) as? [String: Any] { return json } // 失败后尝试提取 {} 之间的内容 if let start content.firstIndex(of: {), let end content.lastIndex(of: }), start end { let jsonString String(content[start...end]) if let data jsonString.data(using: .utf8), let json try? JSONSerialization.jsonObject(with: data) as? [String: Any] { return json } } return nil }解析完之后还需要对坐标做合法性校验。模型可能返回负坐标、超过 1 的归一化坐标、或者与屏幕尺寸不匹配的像素坐标。统一处理方式是只要坐标值不在0...1范围内就丢弃对应高亮或者回退到“该元素关联的 OCR 框”。6. 实现“视觉精确”的结果渲染6.1 在截图上绘制边界框把模型返回的高亮元素 ID 映射回ScreenState.elements后可以拿到归一化坐标。最简单可靠的渲染方式是直接在截图上绘制边界框然后展示给用户。SwiftUI 中可以用Canvas完成struct HighlightOverlay: View { let image: UIImage let boxes: [CGRect] // 这里放转换后的像素坐标或者保存归一化坐标动态转换 var body: some View { ZStack { Image(uiImage: image) .resizable() .scaledToFit() Canvas { context, size in for box in boxes { // 需要把像素坐标按当前视图尺寸等比缩放 let scaleX size.width / image.size.width let scaleY size.height / image.size.height let rect CGRect( x: box.minX * scaleX, y: box.minY * scaleY, width: box.width * scaleX, height: box.height * scaleY ) let path Path(roundedRect: rect, cornerRadius: 12) context.stroke(path, with: .color(.orange), lineWidth: 4) context.fill(path, with: .color(.orange.opacity(0.15))) } } .allowsHitTesting(false) } } }建议用scaledToFit配合动态等比缩放而不是直接写死尺寸这样在不同 iPhone 上都能显示正常。allowsHitTesting(false)保证覆盖层不阻挡用户的点击操作。6.2 在实时覆盖层中做高亮标注如果产品形态是“实时辅导”比分说用户一边操作屏幕一边接收指导那么可以创建一个独立的透明 UIWindow 覆盖在内容层之上。这个 window 的windowLevel设置高于普通内容但不遮挡系统状态栏let overlayWindow UIWindow(windowScene: windowScene) overlayWindow.windowLevel .alert 1 overlayWindow.backgroundColor .clear overlayWindow.rootViewController UIHostingController( rootView: HighlightOverlay(image: currentFrame, boxes: boxes) ) overlayWindow.isHidden false使用覆盖层时要注意两点一是覆盖层不该完全拦截触摸事件。如果需要在高亮区域显示可点击的按钮比如“点击此处查看详细解析”那么只让按钮区域响应触摸其他区域设置allowsHitTesting(false)。二是根据业务场景不要一直占满全屏。长时间遮挡屏幕会影响用户操作。合理的交互是AI 给出高亮后用户点击“完成”即自动消失或者高亮只保留 35 秒。6.3 点击坐标回传与交互闭环“视觉精确”最有价值的地方是可以把 AI 的建议变成可点击的入口。例如模型说“请点击右上角的提交按钮”客户端如果能将归一化坐标映射到按钮区域就可以在覆盖层上画一个“点击”按钮用户点击后触发回调App 内部执行相应跳转或操作。但这里有一个 iOS 平台边界如果你的 App 要模拟点击另一个 App 的界面iOS 官方没有为普通 App 开放任意模拟触摸的公共 API。所以一个更现实的方案是在自己的 App 内部根据坐标执行内部跳转在辅导其他 App 的场景只做“高亮引导 用户手动点击”避免触碰系统限制。换句话说视觉精确的最终落点不一定是“替你操作”而是“精确告诉你操作哪里”。从产品角度来看这种交互反而更容易获得用户信任。7. 常见问题与排查思路开发过程中最大的时间消耗往往来自权限、坐标系和扩展进程通信。下面整理一张高频问题表并逐一展开说明。问题现象常见原因解决思路drawHierarchy 截图为空白非主线程调用、图片尺寸为 0确保主线程执行检查 window.boundsVision OCR 识别中文不准确未设置中文识别语言设置recognitionLanguages [zh-Hans, en-US]高亮框上下颠倒或偏移Vision 坐标系与 UIKit 不一致按公式翻转 Y 轴并减去高度模型返回坐标越界模型幻觉或归一化理解错误解析后校验 0...1 范围非法值丢弃Broadcast Extension 无法启动签名配置或 App Group 配置错误检查 entitlement确保主 App 与扩展共享 Group扩展与主 App 数据不同步进程间通信时序问题使用 Darwin Notification App Group 文件传递覆盖层无法显示windowLevel 设置过低使用.alert 1层级下面挑几个最常见的展开讲。7.1 Broadcast Extension 无法拉起表现在主 App 中跳转RPBroadcastActivityViewController后用户选择“开始直播”但SampleHandler.broadcastStarted一直没有被调用。排查步骤建议按顺序执行确认 Broadcast Upload Extension 的 Bundle Identifier 是否正确并且 Extension 所属 Target 与主 App 在同一个 App Group 中。检查 Extension 的 Deployment Target确保不低于主 App 的最低版本。在 Extension 的broadcastFinished里加日志确认是否有被动结束。用真机测试。模拟器对 ReplayKit 支持有限很多场景跑不通。常见根因是 Extension 没有正确签名或者主 App 的NSExtension配置缺少RPBroadcastProcessMode字段。7.2 高亮框位置总是偏上或偏下表现模型返回的坐标看起来对但画出来的高亮框老是对不准文字。首先检查 Vision 坐标转换是否遗漏了高度。很多初学者只做了一次翻转y 1 - minY忘了减去矩形高度。正确的转换公式是let y (1 - visionBox.minY - visionBox.height) * imageHeight其次检查图片裁剪逻辑。如果截图时取了屏幕一部分区域但 OCR 用的是整张图片坐标比例就会被拉伸。规范做法是截图、OCR、渲染三者的参考图片尺寸保持一致。7.3 模型总在“编造屏幕元素”表现屏幕上明明没有“重置按钮”模型却一本正经地分析按钮位置。这类问题本质上是提示词对模型的约束不够。解决办法可以从三个方向同时入手在系统提示词中明确添加“只能引用给定 elements 中存在的 id禁止描述不存在的元素”。在用户消息中追加一句判断规则“如果没有找到相关元素请输出空的高亮数组并解释原因。”在后端增加校验服务当模型返回的元素 id 不在ScreenState.elements中时自动丢弃该高亮并追加一条修复请求。7.4 屏幕录制内存持续增长ReplayKit 扩展在较老的机型上容易出现内存吃紧因为系统不断把视频帧传给扩展。解决思路是不要保存所有帧只保留最新一帧或者设定一个时间间隔例如每 2 秒采集一帧或只在用户点击“暂停”时采集。另外帧转 JPEG 时注意使用UIImage的压缩参数控制体积if let data image.jpegData(compressionQuality: 0.6) { // 写入共享容器 }一般 0.50.7 的压缩质量已经足够 OCR 使用没必要用 1.0 无损压缩。8. 最佳实践与工程建议8.1 隐私合规是一条硬边界做这类 AI 教育应用隐私不是可选项而是第一优先级。屏幕截图可能包含账号信息、个人信息、聊天记录甚至未成年人面部信息。工程上我建议至少做到以下几点默认不上传原始截图。优先在端侧完成 OCR 和元素检测只把文本、坐标、屏幕尺寸发给模型。必须上传截图时对图片做脱敏处理。比如先裁剪问题区域再使用系统隐私遮罩或手动打码。提供“单次授权”机制。用户每次发起辅导时再触发采集而不是进入 App 就自动采集。服务端保存的日志不要包含完整截图只保留结构化 JSON 和模型结果并给用户提供一键清除学习记录的能力。8.2 降低大模型成本与延迟视觉结构化之后发给模型的文本已经比原始截图小很多但多轮对话中历史上下文仍会越来越大。建议采用以下策略只保留最近 35 轮对话摘要不保存完整历史。每次发送屏幕状态时只发送用户问题关联区域附近的元素。例如问题提到“公式”就只保留 OCR 文本框中包含数学符号的元素过滤掉状态栏、底部 Tab 栏等无关内容。图片传给模型前先裁剪按元素框外扩一定像素而不是发整张截图。8.3 不要让模型直接暴露给客户端真实项目中客户端不应该直接持有大模型 API Key。正确架构是iOS 客户端 → 自己的后端服务 → 大模型服务。后端可以承担几项关键职责统一封装不同模型提供商的接口切换模型时客户端无需改动。对模型输入做脱敏和内容安全检测。对模型输出做 JSON Schema 校验拦截非法坐标。记录每次辅导的输入输出用于评估模型质量和后续微调数据集建设。8.4 模型输出的坐标必须做“合法范围校验”模型输出的highlights数组可能在理论上完全合法但在现实中指向空白区域。建议在服务端增加一层校验函数func validateHighlight(_ box: [CGFloat], elements: [ElementProxy]) - Bool { guard box.count 4 else { return false } for value in box { guard value 0, value 1 else { return false } } // 可选检查是否与任一 OCR 元素框重叠 let highlightRect CGRect(x: box[0], y: box[1], width: box[2], height: box[3]) for element in elements { let elementRect CGRect(x: element.box[0], y: element.box[1], width: element.box[2], height: element.box[3]) if highlightRect.intersects(elementRect) { return true } } return false }如果没有任何重叠就直接把该高亮判断为无效避免用户看到 AI 指向空白区域。8.5 建立离线回归数据集视觉精确类功能最怕“调一次坏一次”。改了一版提示词可能某类题目更准了但另一类屏幕的误报率上升了。建议从第一天起就建立离线回归数据集收集真实用户授权的屏幕截图和问题记录。每一条样本标注正确高亮元素 id、正确回答文本、正确操作步骤。每次修改提示词或模型后先跑一遍离线数据集对比高亮命中和文本准确率再发布到线上。这一步听起来重但对教育类产品非常值得。没有回归数据集的 AI 功能后期维护会非常痛苦。9. 总结与下一步学习路线回到最开始提到的 Show HN 项目Visually Precise AI Tutoring on iOS。这类产品的技术骨架本质上就是本文这条链路屏幕采集、视觉结构化、大模型推理、坐标渲染。它不是单一技术点而是多个系统能力的组合。真正决定体验上限的不是某一个模型有多强而是你能不能把“屏幕状态”准确转成模型可消费、又能映射回屏幕坐标的结构化数据。如果你想从零开始尝试我的建议是先放弃实时屏幕录制做一个最小闭环在你自己 App 内截屏 → Vision OCR → 生成 JSON → 调用大模型 → 在截图上画框。这条链路两天左右就能跑通能让你快速感受到“视觉精确反馈”和“纯文本回答”的差异。跑通之后再逐步加入 Broadcast Extension、多轮对话、局部图片传输和线上回归评测每一步都有明确的可验证标准。过程中遇到问题优先从两个角度排查一是权限链路有没有完整走通二是坐标系到底有没有翻转正确。这两类问题占据了我个人在同类项目中超过一半的调试时间。如果你正在 iOS 上做 AI 辅导或智能操作引导欢迎把本文收藏起来等真正动手时对照着配置和排错能少走不少弯路。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →