ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Kuikly打造DeepSeek Harness移动端遥控器:鸿蒙与Android跨端实践

用Kuikly打造DeepSeek Harness移动端遥控器:鸿蒙与Android跨端实践 头一回在手机上真正跑通本地模型的完整交互流程说实话有点兴奋。DeepSeek Harness 这套工具我一直当命令行宝贝用连本地模型、切思考模式、挂插件一套工作流下来顺手得很。可一旦离开电脑就啥都干不了。我就琢磨能不能把它塞进手机里试过 WebView 套壳手感稀烂写两套原生维护成本我又扛不住。最后选了腾讯开源的 Kuikly用 Kotlin 写一套逻辑Android 和鸿蒙都能跑。这篇文章记录的就是整个折腾过程给同样想玩本地模型、又对跨端方案心存犹豫的朋友一个参考。适合谁来读你要是正在用 DeepSeek Harness 之类的 CLI 工具跑本地模型想搞一个移动端控制面板或者对鸿蒙 Android 双端复用感兴趣想看看 Kuikly 到底能不能接生产又或者单纯想理解“跨端框架 本地模型服务”这种架构思路。这篇都能给到你想要的答案。1. 整体设计为什么是 Kuikly为什么做“遥控器”而不是“发动机”1.1 方案选型Native、WebView、Compose Multiplatform为什么最后是 Kuikly先说我最初的几个备选方案。第一个是纯原生各写一套Android 用 Kotlin鸿蒙用 ArkTS界面逻辑几乎要写两遍。我做这个东西本身是业余时间自娱自乐后续还要迭代双份工作量直接劝退。第二个是 WebView 里塞一个 SPA前端写起来爽但和本地服务通信、调用系统能力都要走 bridge稍微复杂一点的交互比如流式输出、长连接状态展示体验就会打折扣。第三个是试试 Compose Multiplatform但那时候它对鸿蒙的支持还让我心里没底我在一个内部项目上踩过坑折腾半天调不出原生键盘。Kuikly 吸引我的地方很直接它也是 Kotlin Compose 声明式 UI但腾讯团队明显把鸿蒙当一等公民在做。Kotlin 代码可以直接编译到鸿蒙的 ArkTS 侧的能力层UI 描述又能跨端复用。简单说我写一套 UI 组件和业务逻辑Android 上跑成 Compose View鸿蒙上跑成系统原生组件而不是套个 WebView 壳。性能上接近原生交互也跟手。对个人开发者来说少写一遍代码的吸引力太大了。还有个现实原因DeepSeek Harness 本身是 JVM 生态的工具链用 CLI 跟它交互时我在 Android 和鸿蒙两端还要共用一堆模型配置、会话历史的数据结构。用 Kotlin 定义模型类两边直接复用连 JSON 序列化注解都不用改。这种体验是 WebView 方案给不了的。1.2 架构设计App 是随身遥控器Harness 是家里的发动机我的思路很明确手机不直接跑模型也不直接跑 Harness 的 CLI。手机 App 只负责展示、交互、配置修改真正干活的是跑在电脑或者开发板上的 DeepSeek Harness 服务进程。手机通过局域网 HTTP SSEServer-Sent Events连过去就等于一个贴身遥控器。为什么不把 Harness 整个压进手机DeepSeek Harness 这种推理工作流工具要连接本地模型引擎比如 Ollama、vLLM 这类。手机内存和算力有限跑小模型还能凑合跑大一点的 R1 蒸馏版就吃力了。更重要的是Harness 的插件生态、数据存储、日志分析这些重活放在电脑上永远更稳。手机端做瘦客户端只负责“看”和“点”这是我认为最务实的切分。通信协议上我直接选了 HTTP SSE。DeepSeek Harness 服务端本来就有 HTTP 接口SSE 用来处理模型回复的流式返回每次 token 生成都能实时推到手机界面就像原生聊天软件一样一个字一个字往外蹦。选 SSE 而不是 WebSocket是因为我们只需要服务端往客户端单向推送模型输出客户端请求用普通 POST 就够了没必要引入额外协议状态。整个架构分三层手机端 Kuikly App 负责 UIHarness HTTP 服务负责解析请求、管理会话、调用插件最底层是本地模型引擎通过标准接口暴露给 Harness。这样我手机里只需要保留服务器地址和模型 ID不用关心模型文件放哪、显存够不够这些破事。2. 核心细节解析DeepSeek Harness 到底是什么怎么把它“服务化”2.1 先认识 Harness 的定位命令行不是归宿服务化才是很多朋友一听 DeepSeek Harness以为是另一个“模型下载器”或者“聊天客户端”。其实它的定位更接近一套围绕 DeepSeek 模型的推理工作流管理工具你定义好模型端点、推理参数、指令模板它帮你处理会话上下文、调用插件、记录历史。你可以把它理解成贴身的“模型调度壳”。命令行模式很直接参数敲完就出结果但移动端要复用同一份逻辑就一定要把它变成一个常驻服务。我第一次尝试时走了弯路想直接在 Kuikly App 里用 ProcessBuilder 拉起 Harness CLI一查发现移动平台根本不允许你随便起子进程应用沙箱限制得死死的。后来换了个思路在电脑上跑harness serve --host 0.0.0.0 --port 8765把它变成 HTTP 服务手机端所有操作全走 REST API瞬间清爽。这个转变很关键客户端不需要关心 Harness 的进程生命周期只需要管好“请求”和“响应”。2.2 配置本地模型与思考模式yaml 里藏着关键开关DeepSeek Harness 的核心配置文件是harness.yaml里面配置了模型连接信息、默认推理参数、以及插件清单。拿我自己的配置举例我要连接本机的 Ollama 上跑的deepseek-r1:7bmodel: provider: ollama endpoint: http://127.0.0.1:11434 name: deepseek-r1:7b reasoning: true max_tokens: 4096 temperature: 0.6 top_p: 0.95 plugin: - markdown_render - prompt_optimizer - session_saver server: host: 0.0.0.0 port: 8765这里reasoning: true就是热词里说的“思考模式”。DeepSeek R1 这类推理模型在处理复杂问题时会先输出一段内部思考过程再给出最终回答。开启后 Harness 会给两段输出一段是thinking一段是response。移动端界面我会做成可折叠面板默认收起思考内容只展示最终回答想审视模型思路时点一下就能展开。这对排查模型回答质量太有用了有时候模型答得离谱看一眼思考过程就知道它错在哪个环节。temperature: 0.6和top_p: 0.95是采样参数。0.6 这个值在代码生成和逻辑问答之间比较平衡太低了容易答得机械太高了容易飘。如果只是随便聊天我一般会临时调到 0.8。注意这些参数在 Kuikly App 里都能改我会在设置页做几个滑条修改后直接调 Harness 的/config接口热更新不用重启服务。2.3 插件机制从官方插件到自己写一个Harness 的插件机制帮我省了非常多事。插件本质上是挂在请求/响应链路上的钩子比如prompt_optimizer会在发送前对 prompt 做前置优化session_saver会自动把会话记录按时间归档。移动端想做的事电脑端大部分插件已经覆盖了。我建议第一次玩的时候先装这几个插件markdown_render把模型回复的 Markdown 渲染成结构化内容手机上阅读体验差距很大、session_saver断线重连后找得回历史、prompt_optimizer让模型回答更像人能说的话而不是一堆指令。装插件不难在 Harness 的插件目录放进去然后在配置文件里声明就行。插件安装路径比较容易踩坑。不同版本的 Harness 对插件目录约定不一致有的放在~/.harness/plugins有的放在项目根目录的plugins/下。我的建议是解压插件包之前先看一眼 README 里写的前置要求大部分插件需要特定 Harness 版本版本不匹配会直接加载失败而且日志里还不一定报错更像“警告”级别不仔细观察就错过。我第一次装插件的时候半个多小时才发现是插件目录放错了日志只给了一行黄色警告。这事提醒我玩开源工具先看日志的 warning 级别内容。2.4 把 Harness 做成服务跨域、地址、安全手机要访问电脑上的 Harness 服务必须把服务监听地址从127.0.0.1改成0.0.0.0同时关闭或正确配置防火墙。我在 Windows 上遇到过一次服务明明起来了手机死活连不上最后发现是 Windows 防火墙默认拦截了 8765 端口的入站请求。加一条入站规则就好不需要关闭整个防火墙。还有个容易被忽略的问题是跨域。Harness 作为 HTTP 服务默认只允许本地页面的同源请求手机 App 过来属于跨域必须在服务端配置里加一行server: cors_allowed_origins: *开发阶段先放开后面要做成正式产品建议缩小到固定地址。安全上我也提醒一句这个服务一旦监听0.0.0.0和你同一个局域网的人都能访问可以调用你的模型、看到你的会话。正经用一定要给服务端加 Token 认证让 App 每次请求都带Authorization头。我在 Kuikly 的配置页里专门加了一个 Token 输入框联调时省了不少事。3. 实操过程从零把 Kuikly 项目跑起来再接通 Harness3.1 环境准备JDK、Android Studio、DevEco Studio一个都不能少动手之前先列环境清单。我用的是 JDK 17Android Studio 最新稳定版鸿蒙侧用的是 DevEco Studio 5.0 以上版本。Kuikly 官方推荐用 Gradle 8 以上版本太老容易在依赖解析阶段报莫名奇妙的错。创建项目有两种方式一种是用 Kuikly 提供的脚手架模板直接拉一个 Kotlin Multiplatform 工程另一种是在现有鸿蒙工程里引入 Kuikly 依赖这种适合已经有鸿蒙项目只想加 UI 的场景。我这次是新项目直接用官方模板生成少踩不少配置坑。项目结构大概是这样的composeApp模块放共享 UI 和业务逻辑androidApp负责打包 AndroidharmonyApp负责打包鸿蒙。Kotlin 代码主要集中在composeApp两边共享比例能达到 90% 以上剩下的是各端的系统权限声明和入口文件。创建完先跑一遍默认模板确认 Android 模拟器和鸿蒙模拟器都能起来。这一步要是跑不通后面全白搭。我在鸿蒙模拟器上遇到过hvigor编译慢的问题后来把 Gradle 镜像切到国内源速度快了一倍。3.2 UI 设计单聊、参数面板、思考模式折叠区UI 一共做了三个主要界面会话列表、聊天室、设置页。会话列表是从 Harness 的历史会话接口拉数据展示最近对话标题和更新时间聊天室是核心输入框、发送按钮、消息气泡、思考模式折叠面板都在这里设置页用来配置服务器地址、Token、模型参数。聊天室的核心 UI 用 Kuikly 的 Compose 风格写大致长这样Composable fun ChatScreen(viewModel: ChatViewModel) { val messages by viewModel.messages.collectAsState() val thinkingExpanded by viewModel.thinkingExpanded.collectAsState() Column( modifier Modifier.fillMaxSize().padding(16.dp) ) { MessageList( messages messages, thinkingExpanded thinkingExpanded, onToggleThinking { viewModel.toggleThinking() } ) InputBar( onSend { text - viewModel.send(text) } ) } }Kotlin 写 UI 的好处是状态管理和界面渲染在同一个文件里不用像 Web 前后端那样跨语言传数据。collectAsState是 Compose 的典型用法ViewModel 里的状态一变界面自动刷新。思考模式的设计我琢磨了很久。DeepSeek R1 的思考过程经常很长几百个 token 的推理链路全扔在聊天流里会刷屏。我的方案是模型返回数据里如果thinking字段不为空就把它折叠在一个灰色区块里标题叫“思考过程”旁边一个展开箭头。点开能看到模型的思考链路收起只留精华回答。测试下来这个设计非常实用。3.3 网络层用 Ktor 调 Harness REST APISSE 接收流式回复网络层用的是 Ktor ClientKotlin Multiplatform 项目里最常见的选择。普通请求用POST /chat发送用户消息流式回复用 SSE 订阅/chat/stream。服务端 SSE 的返回格式大致是event: message data: {delta: 你好, thinking: false} event: message data: {delta: , thinking: false} event: done data: {}Ktor 客户端处理 SSE 也简单client.parameter(model, modelId).parameter(stream, true).bodyAsChannel().forEach { line - if (line.startsWith(data:)) { val json line.removePrefix(data:).trim() val delta Json.decodeFromStringDelta(json) viewModel.appendDelta(delta) } }这里有个关键点SSE 连接要保持长时间不关闭但又不能让它一直占着一个网络线程。Ktor 的协程处理天然适合这个场景我把它放进viewModelScope.launch界面退到后台时自动取消。手机息屏之后连接会断重新亮屏后要自动拉一次会话历史这个我是在页面重新可见的事件里做的。网络超时和重试策略也必须考虑到。本地局域网一般很稳但 Wi-Fi 信号差时也会偶尔断流。我给客户端配置了 30 秒连接超时读超时给到了 5 分钟毕竟大模型生成长回复本身就要时间。如果中途断线界面会弹一个“重连”按钮点击后从最后一条已显示的消息继续。3.4 关键代码串讲请求组装、响应解析、缓存会话每次调模型时不是简单地把用户输入原样扔给接口就完事。DeepSeek Harness 的context_management功能会把聊天历史拼接成合适的上下文窗口我只管传当前用户消息和会话 ID剩下交给服务端。suspend fun sendMessage(sessionId: String, content: String) { val payload ChatRequest( session_id sessionId, content content, stream true ) val response client.post($baseUrl/chat/stream) { contentType(ContentType.Application.Json) setBody(payload) header(Authorization, Bearer $token) } // 解析 SSE... }会话缓存我做了两层一层是本地 Room 数据库缓存最近 50 条消息断网时也能看历史另一层是 Harness 服务端的完整会话记录跨设备不丢失。本地缓存只做展示兜底所有写操作最终还是以服务端为准。插件调用链在服务端客户端完全透明。比如prompt_optimizer插件会把“帮我写个快排”这种口语翻译成更严谨的提示词用户无感知但回答质量明显提升。这也印证了“服务端做重活客户端做轻活”这个架构的价值。3.5 鸿蒙打包和 Android 打包签名、权限、真机调试打包这一步坑最多。Android 侧相对简单配好签名文件assembleRelease出 APK鸿蒙侧要走 HAP 打包需要 DevEco Studio 里配证书和调试签名。网络权限别忘了。Android 要在AndroidManifest.xml里加INTERNET权限并且在 targetSdk 28 以上还要配置usesCleartextTraffictrue否则局域网 HTTP 明文请求会被系统拦掉。鸿蒙侧也需要在module.json5里声明ohos.permission.INTERNET。调试时我遇到的经典问题是模拟器里访问不了宿主机地址。Android 模拟器里访问电脑要写10.0.2.2而不是127.0.0.1鸿蒙模拟器又有自己的映射规则。最省事的办法是真机调试手机和电脑连同一个 Wi-Fi直接填电脑的局域网 IP一劳永逸。所以我强烈建议初始化 Kuikly 项目后第一时间连真机跑通默认模板别在模拟器地址映射上浪费时间。真机跑起来后还有个小细节Android 上要关掉“过渡动画”缩放鸿蒙上要关掉“开发人员选项”里的“动画时长缩放”否则 Compose 页面切换动画会显得卡顿其实不是框架问题是系统动画干扰。4. 常见问题与排查技巧实录4.1 高频问题速查表我整理了几个我自己和朋友踩过的高频问题直接做成表格方便对照排查。问题现象可能原因解决办法手机 App 连不上 Harness 服务服务端没有监听 0.0.0.0检查启动参数和配置文件确认 host 不是 127.0.0.1能连上但请求被拒绝防火墙拦截端口Windows增加入站规则放行指定端口发送消息后没有回复模型引擎没启动或模型名不对在 Harness 服务端先跑一次命令行对话测试回复内容出现乱码字符编码不一致确认服务端 UTF-8Ktor 请求 ContentType 带上 charset鸿蒙打包失败hvigor 版本或依赖缓存问题清理~/.hvigor和项目build目录重新同步SSE 流断掉界面卡住网络波动 / 手机息屏增加心跳重连重新可见时拉取会话最近消息思考模式开关无效果reasoning字段没同步到服务端检查配置文件reasoning: true是否生效并确认模型支持这些问题的共同点在于大部分都不是 Kuikly 的锅而是服务端环境和网络配置。所以排查时先从服务端起底再用 curl 模拟一遍请求最后才看 App 端日志效率最高。4.2 排查方法论先分离问题边界我的排查顺序固定三步第一步在电脑上用 curl 直接访问 Harness API比如curl http://127.0.0.1:8765/health确认服务端健康第二步换成局域网 IP 再 curl确认服务监听范围正确、防火墙没拦第三步在 App 端打开调试日志看请求是否发出、响应是否收到。这样能快速把问题定位到网络层、服务端还是 UI 层。有一次我调了很久最后发现是手机连的是 5G 网络Wi-Fi 关了根本和电脑不在同一个局域网。这种低级错误最容易忽略排查网络问题第一件事就是把两端网络拓扑画出来确认“手机可以 ping 通电脑 IP”。一个比较好用的技巧是在 Kuikly 的调试模式里把网络请求和响应头全部打到控制台。Ktor 有个Logging插件开启后每个请求的 URL、Header、Body、响应状态码都一清二楚。几个月后再维护项目你一定会感谢当初留了这个日志开关。4.3 思考模式不生效的典型案例这个案例值得单独写一节。我最初配置好why参数之后测试模型回答时会显示思考过程但过了一段时间又不见了。查了半天发现是 Harness 插件prompt_optimizer对长 prompt 做了重写把“要求模型先思考再回答”这段指令过滤掉了。后来我把 prompt 指令从插件处理链路里排除思考模式才恢复。这件事给我的教训是插件不总是加分项。它就像一个热心但手快的同事帮你整理 prompt 的同时可能把你特意加的关键指令给“优化”没了。排查这类问题建议在配置里临时禁用全部插件看现象是否恢复然后逐个启用二分定位。4.4 本地缓存导致的历史错乱我的 App 本地缓存了最近消息但 Harness 服务端上下文管理同样会保留会话记录。一开始我没做双向同步就出现了一个很怪的现象手机端显示了 10 条消息但模型回复时好像看不到前 5 条。原因是 Harness 有自己的max_context_messages设置默认只保留最近 6 轮。手机端缓存是给展示用的服务端上下文才是模型真正能看到的二者不是一回事。解法是在设置页展示“服务端上下文窗口大小”这个参数并让用户可调同时在 App 端把本地缓存标记为“仅展示用”不直接决定发送给模型的上下文。如果发现模型“失忆”先去服务端确认上下文窗口是否合理再考虑本地逻辑问题。5. 实战心得Kuikly 跨端体验与 Harness 服务化的几点补充5.1 Kuikly 让“一套代码两端跑”真正落地用下来我的整体感受是Kuikly 在常规 UI 场景里已经能扛住生产需求。列表、输入框、弹窗、Tab 切换这些高频组件两边行为基本一致。跟原生代码互相调用也不费劲鸿蒙那边可以通过封装好的接口取系统能力Android 直接走 Activity/Fragment 上下文。最让我意外的是性能。长列表滑动、Markdown 渲染这种曾经在跨端框架里容易掉帧的场景Kuikly 处理得挺流畅。这跟它最终映射成系统原生组件有关没有套一层厚重的 runtime。当然它也有学习成本。核心就是 Kotlin 和 Compose 思想如果你平时写 Android 是 View 体系思维切换需要一点时间。另外Kotlin Multiplatform 的依赖管理更复杂第三方库不是每个都有 KMP 版本选型时要多看两眼。5.2 DeepSeek Harness 服务化部署的扩展空间目前这套方案跑得很顺但我觉得扩展空间还很大。比如手机端可以做语音输入通过系统语音识别后把文字塞进聊天框可以加通知中心模型长任务跑完时推送一条消息还可以做多设备协同手机发起的会话在电脑上无缝继续因为所有状态都在 Harness 服务端。我对未来更感兴趣的一件事是把 Harness 的插件开发环境直接接到手机上。现在插件在服务端改适合深度用户未来可以做一个“插件市场”界面手机上装插件、启停插件甚至配置插件的参数。这会让整个移动端能力再上一个台阶。Kotlin 这套生态还有个隐藏红利手机端拿到的数据模型可以直接复用到脚本工具里。我后来写过一个 Grafana 面板用的是和 App 一模一样的数据结构等于一鱼两吃。5.3 给新手的最后建议如果你也想复刻这个方案我总结几条最关键的提醒。第一先把 Harness 服务端在电脑上跑通再开始写 App服务端不稳客户端再漂亮也是空中楼阁。第二局域网联调一定用真机别在模拟器地址映射上浪费时间。第三配置里关闭调试日志之前先在服务端开启 verbose 日志遇到问题能少走一半弯路。第四版本锁定非常重要Harness、Kuikly、Ktor 的版本组合一旦跑通最好不要轻易升级除非你专门留了时间处理升级带来的兼容问题。我自己踩过一个大坑升级了一下 Ktor 版本结果鸿蒙端的 WebSocket 实现换了接口花了一整个周末去适配。后来学乖了所有核心依赖版本在libs.versions.toml里统一锁定升级前先在分支上完整回归一遍。最后分享一个小经验做这种个人工具项目别一上来就追求界面精美。先用最简单的布局跑通端到端链路确认“手机 - Kuikly - Harness - 模型引擎 - 回传手机”整条链路是通的再回头慢慢磨 UI。这个次序倒过来大概率会陷入“界面改了半天一联调全是问题”的泥潭。这套“移动端跨端框架 模型服务化”的组合我自己用下来相当顺手。周末出门带个手机连着家里的模型等车时也能调调 prompt、看模型思考过程确实有一种“把实验室揣进口袋”的满足感。接下来我准备把语音输入和插件管理两个功能完善一下后面有新进展再回来分享。
返回列表