用Kuikly把DeepSeek Harness装进口袋:跨端AI应用实战 📅 发布时间:2026/9/14 22:51:28 👁 浏览次数: 说实话我一开始也没想到能把这事跑通。事情是这样的我平时在电脑上用得最多的就是 DeepSeek Harness它把模型调用、提示词模板、上下文管理、工具函数这些东西全封装成了一条顺畅的工作流。每次在终端里敲两下就能唤起整个 AI 管线体验确实很爽。但问题也明显——我总不能天天背着笔记本。地铁上、咖啡馆里、出差路上很多时候我需要的只是随时掏出来就能问一嘴的能力。于是我就想能不能把这套 Harness 直接搬进手机里试过一些现成方案都不太合手。要么是套了个网页壳交互别扭要么是重新写一套移动端逻辑等于推翻重来。后来我看到了腾讯开源的 Kuikly一个用 Kotlin 做跨端开发的框架细琢磨了一下方案最后真把 DeepSeek Harness 装进了口袋。这篇文章就是完整记录从架构选型到代码实现再到真机调试踩坑你照着走一遍也能做出来。1. 为什么要把 DeepSeek Harness 装进口袋1.1 Harness 到底解决什么问题先把这个概念对齐一下很多人听到 Harness 会以为是某个具体的模型或者新的推理引擎其实不是。Harness 在 AI 工程里一般指的是包裹在模型外面的一整套工作流控制层。DeepSeek Harness 做的事情可以简单理解为三块第一块是模型调用的统一入口你不需要每次重复写 API 请求、鉴权、错误重试这些低层逻辑第二块是提示词和上下文的组织它会根据你的输入自动拼装系统提示词、历史消息、工具调用结果第三块是工具链的编排比如搜索、代码执行、结构化输出这些能力都可以以函数的方式注入到对话流里。所以真正有价值的不是它帮你省了几行代码而是它把和人对话变成了和工具对话并且让这个过程中的状态管理、流式输出、异常恢复都变成了可复用的标准件。问题来了这么一套东西本质上是跑在 Node.js / Python 这类服务端环境里的。手机上没有 Node 运行时也不方便直接跑 Python 子进程那要怎么装进口袋这就是我这个项目要解决的核心矛盾。1.2 为什么选 Kuikly 而不是 Flutter / RN先交代一下选型过程。我最早考虑过 Flutter 和 React Native毕竟这两个生态成熟资料也多。但深入一想有个点绕不过去DeepSeek Harness 的调用链、数据结构、配置模型我希望能尽可能地和现有代码共用逻辑而不是在移动端另起炉灶重写一遍。Kuikly 的独特之处在于它基于 Kotlin Multiplatform业务逻辑用 Kotlin 写一遍Android 和 iOS 都能跑。这对我来说非常关键因为我的 Harness 侧完全可以用 Kotlin 重新实现核心调度逻辑然后 UI 层用 Kuikly 的声明式语法直接搭建。另一个理由是性能。Kuikly 在 UI 渲染上走的是自绘引擎路线和 Flutter 类似但因为它和 Kotlin 协程、Flow 这类异步原生的配合更顺畅所以在做流式输出这种高频更新场景时代码写起来非常顺手。用 Flow 接收 token 流再通过 Kuikly 的状态机制驱动界面刷新整个链路干净利落。当然 Flutter 也不是不行但对我来说Kotlin 一套代码两端复用加上和现有 Harness 逻辑的亲近感这个优势太明显了。2. 整体设计Harness 移动化的四种路线2.1 先拆开 Harness 的壳动手之前我先把 Harness 内部的结构拆了一遍搞清楚哪些是必须保留的核心哪些是可以砍掉的重型依赖。一个典型的 DeepSeek Harness 工作流大概长这样入口接收用户消息 → 加载配置模型名、温度、top_p 等→ 组装上下文系统提示词 历史消息 工具定义→ 调用模型 API → 解析流式返回 → 判断是否需要触发工具调用 → 如果有函数调用就执行并把结果回填 → 继续生成下一段内容。在这个链路里真正不可替代的是上下文组装 工具调用循环 状态管理这三件事。至于具体的运行环境其实只是一个执行载体。2.2 路线对比远端代理、本地服务和纯重写我梳理了三条技术路线逐一做了评估。第一条是远端代理方案。把 Harness 部署在一台服务器上对外暴露 HTTP 接口手机端只做展示层。这个方案实现成本最低但有两个我忍不了的缺点一是每次请求都要经过网络中转延迟明显二是断网场景下完全不可用这就违背了口袋工具的初衷。第二条是本地服务方案。在手机里塞一个轻量级运行时比如通过 Termux 跑 Python然后 Harness 跑在本地Kuikly 通过 localhost 调用。这条路理论上可行但工程复杂度很高。Termux 的 Python 环境在 Android 后台容易被杀iOS 上又没有类似的运行时等于只解决了一半问题。第三条是纯重写方案。用 Kotlin 把 Harness 的核心调度逻辑重新实现一遍直接跑在 App 进程里。UI 层用 Kuikly 写所有状态管理、上下文组装、工具调用都在端上完成。代价是工作量最大但收益也最直接没有网络中转、离线可用、启动快、交互完全原生。我最后选了第三条路线加上一条变通——配置层面保留对远端 Harness 接口的兼容。2.3 我最终选定的架构最终的架构可以分成三层。最底层是 Harness Core也就是用 Kotlin 重新实现的调度内核。这里包含模型客户端负责和 DeepSeek API 通信、上下文管理器维护会话历史、token 预算控制、工具注册表维护可调用的函数列表及其执行逻辑。中间层是 Kuikly 的业务逻辑层负责把 Harness Core 暴露出来的状态转换成 UI 可以消费的数据。比如用一个 Flow 来收集流式输出的 token然后通过状态持有对象驱动界面变化。最上层就是 Kuikly 的 UI 层完全用声明式写法。左边是会话列表右边是对话窗口底部是输入框。整个界面看起来和常见的聊天软件差不多但背后跑的是完整的 Harness 调度逻辑。这里有个关键决策不引入任何服务端组件API Key 直接存在端上加密存储里所有请求由 App 直连模型接口。这样既保证了架构简单也把隐私风险控制在一个端上。3. Kuikly 工程搭建与核心链路实现3.1 环境准备与第一个 Kuikly 页面先搭环境。Kuikly 的工程结构和标准 Kotlin Multiplatform 项目类似你需要准备 JDK 17、Android SDK、Xcode如果你要编译 iOS 的话然后在项目里引入 Kuikly 的 Gradle 插件。我建了一个新工程模块结构大概是这样的com.example.harness ├── core # Harness Core 逻辑 │ ├── model # 请求/响应数据模型 │ ├── client # 模型 API 客户端 │ ├── context # 上下文管理器 │ └── tools # 工具注册与执行 ├── ui # Kuikly 声明式 UI │ ├── chat # 对话页 │ ├── session # 会话列表页 │ └── settings # 设置页 └── platform # expect/actual 平台适配第一个页面不用复杂先用 Kuikly 写一个简单的文本展示验证整条编译链路通不通。在 Kuikly 里页面是一个 Composable 函数我用Text组件渲染了一行字然后分别跑 Android 和 iOS 的构建任务。这一步看起来简单但特别重要。Kuikly 的编译链涉及 Kotlin/Native第一次跑 iOS 构建会下载一堆依赖耗时比较长。建议先跑 Android 构建确认基础环境没问题再跑 iOS排查起来更容易。3.2 把 Harness 的调用链路搬到 Kotlin接下来是核心工程。我在 Kotlin 里实现了自己的HarnessClient先定义好数据模型Serializable data class ChatMessage( val role: String, val content: String ) Serializable data class ChatRequest( val model: String, val messages: ListChatMessage, val temperature: Double 0.7, val topP: Double 0.9, val maxTokens: Int 2048, val stream: Boolean true ) Serializable data class ToolCall( val name: String, val arguments: String )调用部分用 Ktor Client它天然支持 Kotlin 多平台Android 底层走 OkHttpiOS 底层走 Darwin 引擎一套代码两边跑。class HarnessClient(private val apiKey: String) { private val client HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys true }) } } fun streamChat(request: ChatRequest): FlowString flow { val response client.post(https://api.deepseek.com/v1/chat/completions) { contentType(ContentType.Application.Json) header(Authorization, Bearer $apiKey) setBody(request) }.bodyAsText() // 解析 SSE 流 val lines response.lineSequence() for (line in lines) { if (line.startsWith(data:)) { val data line.removePrefix(data:).trim() if (data ! [DONE]) { val json Json.parseToJsonElement(data) val text json.jsonObject[choices] ?.jsonArray?.firstOrNull() ?.jsonObject?.get(delta) ?.jsonObject?.get(content)?.jsonPrimitive?.contentOrNull if (text ! null) emit(text) } } } } }这段代码的关键点是使用了Flow来逐段发出 token。因为对话生成要求流式展示用户输入之后应该一个一个字地看到回复出现在屏幕上而不是等全部生成完了再一次性刷出来。用Flow的好处就是天然支持这样的异步序列。API 地址和鉴权方式我直接写成了可配置项这样后续如果 Harness 版本升级或者你换了别的模型端点只需要改配置不需要动代码。3.3 上下文管理器与工具调用循环流式请求只解决了单次问答的问题但 Harness 的灵魂在于它会自动维护上下文并且能在对话过程中调用工具。我实现了一个ContextManager负责每轮对话结束后把新的消息追加到历史列表里同时做一个简单的 token 预算控制class ContextManager(private val maxContextTokens: Int 8192) { private val history mutableListOfChatMessage() fun addUserMessage(content: String) { history.add(ChatMessage(user, content)) trimIfNeeded() } fun addAssistantMessage(content: String) { history.add(ChatMessage(assistant, content)) trimIfNeeded() } fun buildMessages(systemPrompt: String): ListChatMessage { return listOf(ChatMessage(system, systemPrompt)) history } private fun trimIfNeeded() { var totalTokens history.sumOf { estimateTokens(it.content) } while (totalTokens maxContextTokens history.isNotEmpty()) { totalTokens - estimateTokens(history.removeAt(0).content) } } private fun estimateTokens(text: String): Int { return text.length / 2 // 中文场景粗略估算 } }这里 token 估算用的是字符数除以 2这个近似公式。中文场景下大概两个字符对应一个 token做一个粗略的预算控制足够用不需要为了这个引入额外的分词库。工具调用循环是另一件大事。我实现了一个工具注册表每个工具就是一个函数定义好名称、描述和参数格式后在模型返回tool_calls的时候自动触发执行然后把结果回传。fun interface HarnessTool { suspend fun execute(arguments: String): String } val tools mutableMapOfString, HarnessTool() fun registerTool(name: String, tool: HarnessTool) { tools[name] tool }真实场景中我自己注册了两个工具一个是get_current_time用来做时间相关问答一个是search_notes用来在本地笔记库里做关键词检索。这两件事都不需要联网跑在端上就能执行响应速度非常快也给用户一种这个助手是真的会干活的感觉。3.4 UI 层设计让手机上的交互不别扭底层链路打通之后UI 层就是重头戏了。Kuikly 的 UI 写法和 Compose 非常像声明式、状态驱动。我设计了一个简单的双页面结构主页面是聊天窗口侧面抽屉是会话列表。聊天窗口的关键点在于列表的自动滚动和流式刷新。我定义了一个ChatStateclass ChatState { var messages by mutableStateOf(listOfUiMessage()) var isGenerating by mutableStateOf(false) }在收到流式 token 时不是每次emit都去更新整个列表那样性能会很差。我的做法是在工具侧做一个缓冲维护一个StringBuilder每次收到 token 先追加进去然后隔 30 到 50 毫秒刷一次 UI。这样既保证了视觉上的流畅又避免了每一帧都触发重组。页面代码的骨架大概是这样的Composable fun ChatScreen(state: ChatState, onSendMessage: (String) - Unit) { Column { MessageList(messages state.messages) InputBar(onSendMessage onSendMessage) } }UI 这块我多花了一些心思在输入框的交互上。手机上打字不方便所以我在输入框上方加了一排快捷指令按钮比如总结当前话题翻译上一条回复列出关键点。这些快捷指令其实就是在发送前把预设的提示词模板插进去本质上还是在调用 Harness 的提示词管理能力但用户体验提升了一大截。4. 真机调试与打包那些不跑一遍根本发现不了的坑4.1 编译链路的坑Kuikly 的跨端编译机制和 Flutter 不太一样它是基于 Kotlin Multiplatform 的所以 Android 走的是 JVM 编译iOS 走的是 Kotlin/Native 编译。这就导致一个问题很多在 Android 上正常的代码在 iOS 上可能编译不过去尤其是涉及反射、序列化这类涉及运行时特性的东西。我实际遇到的一个具体问题是 kotlinx.serialization 在 iOS 上的 JSON 解析策略和 Android 有细微差别。同样是解析null字段Android 端会忽略并保留默认值iOS 端在某些配置下会直接抛异常。最后我的解决方法是给所有数据模型都加上默认值并且在整个工程里统一开启ignoreUnknownKeys true。这类问题排查起来很费时间因为两个平台的日志风格不一样。Android 上直接看 Logcat 就行iOS 上得用 Xcode 的设备控制台。建议在开发初期就养成一个习惯任何数据类都显式声明默认值任何 JSON 字段都允许未知字段这个小习惯能帮你省掉无数个抓狂的夜晚。4.2 网络与数据格式的坑真机调试遇到的第二个大头是网络。Android 模拟器默认可以通过10.0.2.2访问宿主机但真机没有这个概念。如果你像我一样在本地跑了一个 Harness 服务用于联调注意不要把localhost写死在代码里。我的做法是在设置页增加了一个可配置的 API 地址默认指向线上模型接口但允许在调试时改成局域网内服务器的 IP。另外一个坑是 API Key 的安全。我一开始图省事把 Key 直接写在了代码里结果被同事提醒这样太危险。后来改成了端上加密存储Android 用 EncryptedSharedPreferencesiOS 用 Keychain通过 expect/actual 封装成一个统一的接口。这个改动不复杂但属于不做会出事的级别。还有流式返回的解析我发现 DeepSeek 的 SSE 格式在某些情况下会返回多条data在同一行如果只是简单按行解析会漏数据。我的解决方法是先把整个响应按data:切分然后再逐段解析这样兼容性更好。4.3 端上性能优化实测第一个性能瓶颈是列表刷新。最开始我在每个 token 到达时都触发一次messages的更新结果列表在长对话时卡顿非常明显。后来优化成批量刷新 最后一条消息原地修改滑动流畅度立刻上了一个台阶。第二个瓶颈是记忆体占用。长对话的 token 数会持续增长如果不加控制几百轮之后上下文早就爆炸了。我做的处理是分两级第一级用 ContextManager 做 token 预算裁剪超出部分从最早的对话开始丢弃第二级在 UI 层只保留最近 50 条消息的完整内容更早的折叠成摘要。这样既保证了对话质量也控制了内存。真机实测下来一个包含 100 轮对话的会话内存占用大概在 80 到 120 MB 之间滑动帧率基本维持在 58 到 60 帧。这个数据在可接受范围内日常使用完全感觉不到卡顿。5. 常见问题速查与实操心得5.1 问题与排查一览现象可能原因解决办法iOS 编译报 JSON 解析异常kotlinx.serialization 对 null 字段处理不一致所有数据字段设置默认值开启 ignoreUnknownKeys流式输出时 UI 卡顿每次 token 都触发列表重组缓冲 30-50ms 批量刷新末尾消息原地更新真机请求局域网服务失败写死了 localhostAPI 地址做成可配置项支持局域网 IP长对话后上下文混乱token 超预算未裁剪ContextManager 按 token 预算丢弃最老消息API Key 泄漏明文写在代码中端上加密存储避免提交到版本库偶发漏消息SSE 多条 data 出现在同一行按data:前缀切分而不是按行切分5.2 几点实操心得这套方案做下来我最深的体会有三条。第一条是跨端框架的价值不在 UI 而在逻辑复用。Kuikly 对我来说最大的意义不是省掉了写两套界面的工作量而是让 Harness Core 这一整层模型调度逻辑可以用 Kotlin 写一遍、两端共享。UI 重写是不可避的但核心业务逻辑的复用让整个项目的维护成本低了一大截。第二条是移动端的大模型应用核心体验在于交互节奏。流式输出、列表滚动、键盘弹出这些细节每一样都会直接影响用户对这个工具跟不跟手的判断。我在性能优化上花的时间比写业务逻辑多得多但回头看是值得的。第三条是别过度设计。我最初想过在端上引入数据库、做复杂的权限系统、甚至打算给工具调用加一个可视化编排界面后来被现实教育了。首个版本能做扎实对话 上下文 几个实用工具这三件事比堆一堆花哨功能重要得多。如果你也想把自己常用的 AI 工具链搬到手机上我的建议很简单先去把你现有的 Harness 拆成核心逻辑和执行环境两部分然后找一个能复用你主力语言的跨端框架一次只做一件事先把最小闭环跑通。项目在路上慢慢迭代工具顺手不顺手只有每天摸手机的时候才知道值不值。