ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Kuikly的DeepSeek Harness移动端开发实践

2026/9/15 12:49:13 拓冰建站 浏览量
基于Kuikly的DeepSeek Harness移动端开发实践 不用绕弯子我直接把结论放在前面这个项目的本质是用腾讯开源的跨端框架 Kuikly把原本只能在电脑上跑的一套 DeepSeek 工具链——也就是社区里常说的 DeepSeek Harness——搬到了手机里。做出来的东西不是一个简单的“网页套壳”而是真正能离线打开、能处理多轮对话、能管理提示词模板、能随手调用本地配置的移动端应用。先说清楚目标读者。如果你正在用 DeepSeek 的 API 或者自建服务同时又有“在手机上快速调一下接口、整理几段提示词、或者随时翻看历史会话”的需求那这篇文章就是写给你的。如果你只是对“跨端框架”感兴趣想看看 Kuikly 到底能干什么这文里的工程拆解和踩坑记录也能给你一些能直接拿去用的参考。我做的这个应用姑且叫它 DeepSeek Harness Mobile。它不是一个重新发明轮子的项目而是把 Harness 这类工具链里最常用的能力用 Kuikly 在移动端重新实现了一遍。整体开发周期大概两周除去熟悉框架的时间真正写业务逻辑的时间不到一周。这中间的效率差很大程度上要归功于 Kuikly 这套框架的设计思路。1. 为什么是 Kuikly DeepSeek Harness1.1 DeepSeek Harness 到底是什么先把这个概念掰开。DeepSeek Harness 不是 DeepSeek 官方出的某个单一软件而是社区里围绕 DeepSeek 模型生态逐步沉淀出来的一套“开发工具链”的总称。它通常包含几个层面一个用于调用 DeepSeek 模型能力的命令行工具支持对话补全、流式输出、参数配置一套提示词模板管理机制把常用的 system prompt、few-shot 示例、工具调用规则统一收纳可选的桌面端界面或编辑器插件方便在开发环境里调试。简单理解Harness 就是给 DeepSeek 模型套上的一层“缰绳”。它帮你固定模型的角色设定、约束输出格式、管理上下文窗口让你不用每次都在代码里硬编码一堆 prompt 拼接逻辑。但这个生态有一个明显的短板几乎所有的 Harness 实现都是面向桌面开发者的。什么意思呢就是你要用这东西就得开着电脑、开着终端、或者开着编辑器。人在通勤路上、在会议室里、在床上想快速看一下某个 prompt 模板的效果完全没有办法。所以“把它装进口袋”这个需求从一开始就是真实存在的。不是我们为了做一个 App 而找了一个借口而是这个工具链在移动端的缺位本身就是一个明确的痛点。1.2 Kuikly 帮我省掉了什么跨端框架我前后用过 Flutter、React Native也写过纯原生。Kuikly 最让我意外的地方是它对“提升开发效率”这件事的执着程度。先说第一点状态管理。Kuikly 的响应式状态管理和 Vue 3 的响应式体系非常接近。你定义一个变量页面里绑定了它变量一变UI 自动更新。不需要像 React 那样手动依赖 useMemo、useCallback也不需要在状态树的组织上花太多心思。这听起来好像没什么但你在手机上做一个多轮对话界面时消息列表的增量追加、流式输出过程中的逐字刷新、代码块的折叠状态——这些高频、细粒度的 UI 更新在传统原生开发里要写一堆 adapter 和 notifier在 Kuikly 里就是直接改数据源的事。第二点逻辑与 UI 的复用。Kuikly 是 Kotlin 多平台方案UI 层用的是类似 SwiftUI 的声明式语法但业务逻辑层可以直接编写共享的 Kotlin 代码在 Android 和 iOS 上共用。这个项目里我写了一个负责“会话上下文管理”的模块整个逻辑文件只有一个双端共用如果换做原生方案至少得写两份不同语言的实现。第三点和现有生态的衔接。DeepSeek Harness 本身有大量逻辑是基于 Kotlin/Java 的工具链生态写的Kuikly 可以直接复用这些已有的库。我不需要为了一个手机端去重写一套网络层、JSON 解析、数据库缓存Kotlin 生态里成熟的东西直接拿过来就能用。一句话总结Kuikly 帮我省掉的不是“写代码的时间”而是“重复写同一套逻辑的时间”。后者才是跨端开发真正的成本黑洞。2. 工程架构与核心模块拆解2.1 整体分层设计整个项目的架构我分了四层从下往上分别是数据层封装 DeepSeek API 调用负责网络请求、流式解析、错误重试逻辑层负责会话管理、提示词模板解析、上下文窗口裁剪、参数组装状态层用 Kuikly 的 observable 状态模型管理 UI 状态与会话状态展现层基于 Kuikly 声明式 UI 编写页面包括对话页、模板管理页、设置页。这个分层看起来中规中矩但有一个关键设计点值得单独说不要把 API 调用的数据结构直接暴露给 UI 层。我见过很多项目直接把服务端返回的 JSON 映射到 UI 组件上看起来省事但一旦服务端调整字段结构UI 层也要跟着改而且无法做防御性处理。我在数据层和逻辑层之间加了一个仓库层Repository它负责把 API 返回的数据统一转换成 App 内部使用的领域模型。比如服务端返回choices[0].message.content仓库层把它解析成一个AssistantMessage对象UI 层只认这个对象不关心它来自哪个接口、服务端字段叫什么。这个设计在后续迭代里帮了大忙。DeepSeek 的接口调整过几次字段结构我只改了仓库层的两个文件UI 和状态层一行代码都没动。2.2 会话上下文管理这是 Harness 类工具的核心也是最容易写烂的部分。DeepSeek 这类模型本身是无状态的你发过去什么它答什么。要让它支持多轮对话就必须由客户端自己维护历史消息列表并且在每次请求时把相关的历史消息一并发送。但这带来一个工程量问题上下文窗口是有限的。如果你和一个模型聊了一个小时把所有的历史消息都塞进一次请求很快就把 token 窗口撑爆了。Harness 的解决方案是做一个上下文管理器它会统计当前会话中所有消息的 token 占用设置一个丢弃阈值比如总 token 数超过 6000 时开始裁剪裁剪时优先保留 system prompt、最近的对话消息最久远的中间消息逐步浓缩成摘要。这个逻辑在电脑上跑没什么难度但在手机上有额外的约束内存小、CPU 弱不能随便把几万 token 的文本一次性扔进内存做拼接。最终的实现是这么做的每条消息在保存进列表时同时缓存一个 token 估算值上下文管理器在做裁剪时只读取这些估算值做累加不需要重新解析全文。这样即使是一整天的对话历史裁剪计算也能在几毫秒内完成。2.3 提示词模板管理DeepSeek Harness 的上手难点之一是提示词模板的管理。早期版本里模板就是一串硬编码在代码里的字符串改一次 prompt 要重新编译一次。后来社区演进出了基于 Markdown 或 YAML 的模板文件把 system prompt、few-shot 示例、变量占位符都写在一个独立文件里运行时再渲染。我在移动端复刻了这套机制模板文件存放在 App 的文档目录下通过一个模板渲染引擎做插值替换。举个例子一个模板文件长这样name: code_review description: 代码评审助手 system: | 你是一名资深代码评审专家请从代码质量、性能、安全、可维护性四个维度审查代码。 输出格式 - 风险级别高/中/低 - 问题描述 - 优化建议 user: | 请审查以下代码 {{ code }}运行时只需要把{{ code }}替换成实际代码内容就能生成一个完整的请求体。这个机制的好处是prompt 的调整不再依赖代码发布直接在手机文件管理里改一下模板文件重启应用就能生效。3. 从零到一核心功能实操实现3.1 创建工程与依赖配置Kuikly 的工程创建流程很常规通过命令行脚手架初始化一个 Kotlin Multiplatform 项目然后选择需要支持的目标平台。我选择了 Android 和 iOS 双端工程结构大致是这样的myapp/ ├── shared/ # 共享业务代码Kotlin ├── androidApp/ # Android 壳工程 ├── iosApp/ # iOS 壳工程 └── build.gradle.kts依赖方面网络请求用的是 Ktor ClientJSON 解析用 kotlinx.serialization本地存储用 SQLDelight。这三个都是 Kotlin Multiplatform 生态里的标准选择不需要额外引入第三方框架。比较需要注意的一个配置点是网络权限。Android 端需要在AndroidManifest.xml里声明INTERNET权限iOS 端需要在Info.plist里声明 App 传输安全设置否则默认会阻止 HTTP 请求。这个坑我在第一次联调时踩过iOS 端请求一直失败控制台只报一句 “App Transport Security has blocked a cleartext HTTP”排查了半天才发现是网络权限配置的问题。3.2 对话界面的流式输出对话页面是整个应用的核心。如果只是做一个简单的“发送请求等待完整返回然后一次性渲染”的交互那代码很简单但体验很糟糕。大模型接口通常需要几秒才能生成完整回复如果这个时间内界面上一片空白用户会以为应用卡死了。所以我实现了流式输出API 返回的数据按片段到达每到达一个片段就把它追加到当前消息的展示内容里界面实时刷新。这部分实现有几个难点。第一个是数据解析DeepSeek 接口的流式返回格式是 SSE即每行以data:前缀开头的一段 JSON。逐行读取、解析、提取文本片段这部分逻辑不算复杂但要注意处理连接中断和超时。第二个难点是 UI 刷新频率。如果每个 token 到达都触发一次 UI 刷新在低端手机上会导致界面卡顿。我加了一个节流器每 50 毫秒最多刷新一次界面。这样用户看到的文字更新仍然是流畅的但不会因为高频刷新把 UI 线程拖垮。核心代码逻辑大致是// 流式返回的解析与UI更新仅示意核心逻辑 viewModel.messages.add(assistantMessage) flowCollector.onEach { chunk - if (chunk.isNotNull()) { assistantMessage.content chunk.text // 每50ms最多触发一次UI更新 throttleGate.runIfPassed { messageListState.refresh() } } }第三难点是“生成中”状态的展示。流式输出过程中用户可能会发送新消息、切换页面、或者进入后台。这些操作要怎么处理直接影响体验的稳定程度。我的处理方式是生成过程中用户不能发送新消息但可以随时停止生成切出页面不会中断请求回来时内容仍然在正常追加。3.3 会话持久化手机上的应用随时可能被系统杀进程。如果会话只存在内存里用户聊到一半切到别的 App回来发现记录清空了那这个应用基本就废了。会话持久化我用的是 SQLDelight在移动端上跑 SQLite 很合适。表结构设计得很简单session表会话 ID、标题、创建时间、更新时间message表消息 ID、会话 ID、角色、内容、token 估算值、创建时间template表模板名称、内容、更新时间。这里有一个小的设计心得在写入消息时同时计算并保存该消息的 token 估算值放到单独一列。后面做上下文裁剪时直接用这个列的值做累加不用重新读全文计算。这是一个很小的优化但它让裁剪操作的耗时从“毫秒级”稳定保持在“微秒级”即使消息数量很多也毫无压力。另外SQLDelight 是编译期生成代码的SQL 语句写错会在编译阶段就报出来而不是运行到那一步才炸。这一点在维护性上非常加分数据库结构改了编译器会立刻告诉你哪里需要跟着改。3.4 模板渲染与请求参数装配请求的最终组装是把模板渲染结果、上下文管理器的历史消息、以及用户在 UI 层调整的参数temperature、max_tokens、top_p 等合并成一个完整的 API 请求体。一个设计关键点DeepSeek Harness 里system prompt 的角色定位是“整套工具链的规则底座”。在请求体里system prompt 必须排在第一条消息用于设定模型的行为边界而用户输入的历史消息按时间顺序排在其后。我在实现请求参数装配时把 system prompt 放在一个独立字段system里而不是塞进 messages 数组。这样后续如果切换模型或升级 API只需要改一层适配代码不会污染整个历史消息列表。4. 常见问题与排查技巧实录4.1 流式输出卡顿不是主线程的问题刚开始跑流式输出时遇到一个现象中低端 Android 手机上文字滚动时有一点掉帧但主线程看起来又没有明显的阻塞。排查过程比较典型。我先用 Profile 工具抓了主线程负载发现很高但代码里能看到的耗时操作只有文本追加和列表刷新理论上不至于占用这么多 CPU。后来仔细分析才发现问题不在主线程而在渲染层每次刷新时Kuikly 会重新计算整个列表的 layout消息越长这个计算的耗时越大。解决方案其实很简单加一个虚拟列表机制只渲染当前可视范围内的消息项超出范围的用占位容器代替。这样无论会话历史有多长单次刷新最多只核算屏幕上那几条消息的布局。改动之后掉帧问题基本消失甚至比原生 ListView 更顺。4.2 iOS 和 Android 的默认字体渲染差异这是个小问题但对用户观感影响很大。中文文本在 Android 上默认字体是思源黑体在 iOS 上默认字体是苹方。两个字体渲染同等字号的文字视觉大小差异能有 10% 到 15%。如果你的对话界面把气泡高度设置成写死的像素值那么同一个消息在 Android 上显示正常在 iOS 上就有可能出现文字溢出。解决办法有两种一种是每个气泡都做自适应高度完全交给布局引擎去算另一种是给双端单独设置字号和行高参数。我选了第一种因为 Kuikly 的布局引擎计算高度很快且自适应高度天然不会出 bug。4.3 流式请求的中断恢复还有一类问题出现在网络不稳定的场景。流式请求在传输过程中如果断网客户端会自动重试整次请求但这样会导致模型重新生成一遍内容之前的对话上下文没有丢但输出内容重复了。更麻烦的情况是连接断开时服务器其实已经生成了部分内容但客户端还没收到完整的结束标记然后重试后模型会把之前生成的内容再生成一遍。我最终的解决方案是断网时不自动重试只提示用户“网络中断内容输出可能不完整”并提供两个操作按钮“重新生成本回答”和“继续刚才的回答”。后者是把已经收到的部分内容作为历史消息发给模型让它接着往下写。这个机制虽然实现起来麻烦但实际用起来非常救急。4.4 多端状态同步的一个“脏”技巧跨端项目要保证 Android 和 iOS 的行为一致最常见的坑是时间戳格式。同一个事件Android 用System.currentTimeMillis()拿到的是毫秒时间戳iOS 拿到的是 2001 年起的秒级时间戳。如果不做统一会话排序和消息展示顺序在双端会不一样。我的做法是所有时间相关的计算统一使用自建的一个TimeProvider接口在共享代码里注入具体实现保证双端拿到的都是同一个格式的 UTC 毫秒时间戳。听起来是个很基础的细节但我确实见过有项目因为这个同一个账号在两个平台看到的历史记录顺序完全不一样。5. 几个值得分享的实战心得5.1 先做核心链路再补外围功能这个项目里我最庆幸的决定是先把“发送消息 → 流式返回 → 消息存储 → 多轮上下文”这条核心链路打通再来补模板管理、参数调节这些外围功能。原因很简单核心链路决定框架的选型是否正确。如果一开始就花了大把时间做漂亮的设置页和模板编辑界面结果发现核心链路在移动端性能扛不住那整个项目等于白做。我第一版只用了两天时间把最简版本的对话功能跑通了。当时界面丑到不行就是白底黑字没有圆角气泡没有头像但核心逻辑全部验证过流式输出正常、上下文窗口裁剪正常、SQLDelight 持久化正常。第三天才开始做界面美化那时我心里已经很踏实了。5.2 Kuikly 的学习成本并没有想象中高如果你有 SwiftUI 经验Kuikly 的 UI 语法几乎是零成本上手——结构、修饰符、状态绑定的思路非常像。如果你有 Vue 开发经验它的响应式状态管理对你来说也是熟悉的味道。真正需要花时间理解的是它对“共享逻辑”的划分边界。哪些逻辑放在 shared 模块里哪些放在平台特定代码里需要一定的经验。我的经验是尽可能把一切可共享的都放到共享模块包括网络请求、数据库、状态管理只有 UI 控件级别的定制、平台特有的交互手势才放到各自端去实现。5.3 DeepSeek Harness 在移动端的未来这个项目做完以后我对 Harness 这类工具链在移动端的可能性有了更多想法。它不应该只是把桌面端的功能搬运过来更有价值的方向应该是利用手机特有的能力语音输入、相机拍照识别、位置信息、传感器数据——让模型不仅能“读懂文字”还能感知用户所处的真实环境。下一轮迭代我计划加入一个快捷指令面板让用户通过桌面小组件一键唤起常用模板比如“帮我总结这段文字”“给这张照片写个英文描述”。这些场景在桌面端做很别扭在手机上却很自然。如果你想在自己的项目里尝试 Kuikly我的建议很直接不要一上来就写业务逻辑先花两天时间把官方的示例跑一遍把它的状态管理和布局引擎的思维方式理解透再开始动手。框架本身不复杂但理解它的心智模型比只学 API 有用得多。