Agent在移动端的尝试-SmartFloat ,让你在做题时解放双手

Agent在移动端的尝试-SmartFloat ,让你在做题时解放双手 Agent在移动端的尝试-SmartFloat 让你在做题时解放双手基本信息本项目主要是通过LLMandroid的无障碍服务通过OCR或者截图获取屏幕信息发送给大模型获取到点击坐标的一个APP。github仓库SmartFloatgitee仓库SmartFloat功能特点语音控制通过语音输入命令无需手动操作AI驱动集成大语言模型智能理解用户意图⚡高效执行结合OCR技术快速识别屏幕内容并执行操作精准定位支持多次点击校准提高操作准确性任务管理支持任务打断和状态管理技术栈语言Kotlin框架Android Jetpack (ViewModel, Room)网络RetrofitOCRMLKit语音识别腾讯云ASRUI悬浮窗服务屏幕信息获取方式的选择1 直接截图发送给AI一开始想直接截图发送给AI告诉图片的长宽和坐标原点位置等大模型返回点击坐标就行结果第一次就花了50多秒钟才收到响应而且点击的坐标也有和大偏差。后来就在图片上用Canvas画上坐标的4个点还有一些坐标线让大模型能够根据坐标线来定位到真正想点击的位置。虽然有用但是还是有一部分点击偏差比较大。在prompt里面添加了一个history字段用来记录AI的操作和点击坐标再反过来发回给大模型并且在图片上上一次的点击的位置绘制一个浅红色的小点让大模型知道自己上一次操作的细节以此来调整下一次点击最终成果可以顶着超高的响应时长完成打开QQ点击头像打开相册打开原神进入游戏点击进入人物界面点击武器详情页的功能大概每个点击之间都要消耗2000Token和30秒的等待时间对于游戏界面这样复杂的环境点击坐标的偏差会更大往往需要点击3次才能正确定位坐标而且是不考虑错误点击影响的情况。2 使用无障碍获取屏幕节点尝试使用无障碍服务来获取屏幕内能点击的节点和节点的坐标然后通过文本的形式发送给大模型代替图片。但是现实没有想象的那么美好我遍历出可点击的节点后发现有很多组件都无法获取到真实显示的内容而且对于webView和gameView就只能是获取到外围的一个大组件。内部的内容无法读取这个方案的实用性就很差。最终被放弃了3 OCR识别文本和坐标既然直接读取图片太慢那我就在本地使用OCR先把截图里面的文本和文本坐标扫描出来通过文本的形式发送给大模型。我使用的是MLKit。终于对于文本环境这个方案非常实用完全可以做到自动完成选择题或者是打开一些软件之类的任务。这个方案在测试时可以完成形式与政策课程的线上考试都是选择题在使用deepseek-v4-flash的情况下每轮循环消耗Token600-800耗时1-2秒且缓存命中率有40%-50%是目前测试中表现最好的方案Token减少方法1 解除深度思考模式首先在发送请求时把thinking的type设置为disabledrequestLlmRequest(modelmodel,messageslistOf(LlmMessage(roleuser,contentlistOf(LlmContent(typetext,textgson.toJson(llmBody))))),thinkingLLmThinkingType(disabled))这个可以让模型不适应深度思考模式因为这个系统并不注重模型输出了什么所以不需要。在设置以后能显著减少输出消耗的Token大概能从1000减少到100以内稳定在70左右可以说减少了90%的输出Token消耗2 增加token的缓存命中率然后对于大模型计算 token 缓存命中最核心的逻辑就是前缀匹配我们要在发送的消息中尽量把不变的信息放在前面变化的信息放在后面比如提示词和页面坐标就是不变的。在构建请求体的时候我使用的是Gson将内容直接转换成JSON的字符串。dataclassLLmBody(vala_prompt:String,valb_question:String,valc_maxX:Int,valc_maxY:Int,vald_history:ListProcessHistory,varz_content:String?)由于Gson在转换字符串时是使用元素的变量名首字母进行排序我就在原来的基础上给每个变量名都强行添加了一个首字母让提示词出现在前面。或者不使用Gson,我们可以自己拼接将prompt放在前面也是可以的。如此操作下来成功将缓存命中率从0%提升到40%左右响应速度也快了不少。返回格式的设置dataclassLLmResponse(valtapPoints:ListTapPoints,//用来记录点击细节可以一次执行多次点击valcommand:String?,//用户最开始的命令保证不在任务中途出现太多幻觉或者忘记一开始的目标valisEnd:Boolean,//用于表示任务是否结束未结束就要在点击后重新获取屏幕信息再次发起下一次请求valremark:String//表名在这一步操作中大模型做了什么后续会记录在history里面让大模型参考)dataclassTapPoints(valtapX:Double,valtapY:Double,valdelay:Int,//在返回后点击执行的延时)任务打断机制一开始想要设置一个Bool值来控制任务的开始与停止但是如果在等待响应的途中按下了停止然后在上一次还没有响应之前就又开始下一次任务结果就会混杂两次任务的结果导致混乱现在使用的方案是给每个任务都设置一个单独的ID停顿了就把ID增加1在收到返回时会先校验任务ID如果不相同就直接丢弃以此来做到打断任务。悬浮按钮的状态介绍悬浮按钮主要分为未录音 编辑命令 执行中 三个状态未录音状态显示一个麦克风手指不移动长按后就会触发录音变成红色松手就会结束录音并进入编辑命令状态。编辑命令状态会在悬浮窗按钮从左到右显示上传按钮命令内容编辑按钮和删除按钮点击删除按钮会丢弃识别结果回到未录音状态点击上传按钮会读取命令并开始执行任务进入执行中状态。编辑按钮可修改语音识别结果支持手动输入命令。执行中状态显示暂停按钮在原地点击后就会暂停任务。细节在未录音状态时手指放到按钮上会读秒会有一个500毫秒的检查当超过时就会开始录音在录音或者执行中状态会在手指送开按钮时做判断如果是录音中就会停止录音如果松开时的坐标与按下坐标的x和y差值小于10不是移动按钮行为就会打断任务状态转换流程未录音 →(长按录音)→ 录音中 →(松手)→ 编辑命令 ↓(点击删除) 未录音 ↓(点击上传) 执行中 →(完成/打断)→ 未录音模型的分享与接收通过读取和编辑剪切板实现了模型的分析与接收。fungetClipboardContent(context:Context):String{//读取剪切板valclipboardcontext.getSystemService(Context.CLIPBOARD_SERVICE)asClipboardManagervalclipDataclipboard.primaryClipif(clipData!nullclipData.itemCount0){valitemclipData.getItemAt(0)Log.d(MainActivity getClipboardContent,item.toString())returnitem.text?.toString()?:}else{Log.d(MainActivity getClipboardContent,null)}return}funcopyAddMode(context:Context,addModel:AddModel){//添加到剪切板valclipboardcontext.getSystemService(Context.CLIPBOARD_SERVICE)asClipboardManagervaloldClipclipboard.primaryClipvalgsonGson()valaddModeTextgson.toJson(addModel)valnewClipClipData.newPlainText(add_model,addModeText)if(oldClip!null){for(iin0until oldClip.itemCount){newClip.addItem(oldClip.getItemAt(i))}}Toast.makeText(context,复制成功,Toast.LENGTH_SHORT).show()clipboard.setPrimaryClip(newClip)}dataclassAddModel(varmodelName:String,varapiKey:String,varbaseUrl:String)在添加模型弹窗有一个剪切板按钮点击后会读取剪切板里面第一条信息如果是{apiKey:xxxxxx,baseUrl:https://xxx/xxx,modelName:xxx-v1.0-xxx}就会转换成AddMode对象然后直接填充到添加弹窗的输入框里面。在编辑模型弹窗放了个复制按钮点击后就把模型转换成AddMode对象然后变成Json放到剪切板第一条。用户能直接复制分享给朋友。不过这个方法在传递apiKey这种敏感信息时可能会泄露后续考虑增添加密算法和一键分享。现有问题项目结构smartfloat/ ├── app/ │ ├── src/main/java/com/zyy/smartfloat/ │ │ ├── database/ # 数据库相关 │ │ ├── network/ # 网络请求 │ │ ├── prompt/ # 提示词管理 │ │ ├── service/ # 服务类悬浮窗、无障碍 │ │ ├── utils/ # 工具类 │ │ └── viewmodel/ # ViewModel层 │ └── src/main/res/ │ ├── raw/ # 提示词资源 │ └── xml/ # 配置文件 └── README.md使用说明安装应用并授予录音、悬浮窗和无障碍权限、配置模型长按悬浮按钮开始录音语音输入命令后松手可编辑识别结果后点击上传执行等待AI执行完成end