尧图精选

Android接入DeepSeek API实战:OkHttp+协程+SSE流式

🕒 发布时间:2026/10/2 17:43:58 📁 来源:尧图网络
最近在折腾 Android 端的 AI 功能时我特意把 DeepSeek API 接入从头到尾走了一遍。说实话现在很多大模型接口搞得很重但 DeepSeek API 的方式很直接不需要额外 SDK、不需要复杂鉴权流程只要会发 HTTP 请求就能把对话能力接到自己的 App 里。这篇文章就是写给那些和我一样的 Android 开发者——项目里临时要加一个 AI 助手、聊天机器人或者只是想在真机上验证一下大模型调用效果都能直接照下面的思路抄作业。整个流程如果顺利的话5 分钟内跑通第一个请求完全不是吹牛。我先提前交代几个关键词Android、DeepSeek、API、OkHttp、协程、SSE 流式响应。后面所有代码和思路都围绕这几个点展开。如果你是那种喜欢先看整体结构再动手的人那这篇很适合你如果你想直接复制代码先跑通也可以跳到第 3 部分。我在实际项目里接入 DeepSeek API 时最关心的其实不是“能不能连上”而是“连上之后怎么保证稳定、怎么把 Key 藏好、报错了怎么排”。所以这篇不只是给一段 demo 代码还会把容易踩的坑和排查思路一起整理出来。1. 项目核心思路与整体方案拆解1.1 为什么选择 DeepSeek API 而不是本地模型或其它接口Android 端接入 LLM无非就三种路径自己用 SDK 跑本地模型、接大厂云 API、通过后端中转大模型接口。本地模型的优势是隐私好、不依赖网络但手机算力有限跑大点的模型发热掉帧是常态体验很难做得好。接云 API 是最省事的选择DeepSeek API 在这类方案里属于“上手成本”特别低的那种因为它兼容 OpenAI 格式很多现成的请求代码稍改一下 URL 就能跑。DeepSeek API 另一个让我愿意推荐的点是上下文长度的支持比较充裕在处理 Android 项目里常见的“粘贴报错日志让 AI 分析”这种场景时不太容易出现截断问题。而且价格对个人开发者比较友好测试阶段可以放心大胆调。当然不选本地模型还有一个现实原因Android 端如果要跑一个效果好一点的模型光模型文件就动辄几个 GB打包体积直接爆炸。相比之下API 接入最多只多了几百 KB 的依赖对于大多数工具类 App 来说这才是更合理的工程决策。1.2 三个可行方案官方API直连、封装SDK、后端代理我在接入前对比了三条路直接用官方 API 发 HTTP 请求、找社区 SDK 封装、在自家后端做一层代理。官方 API 直连是最快的方案Android 端只用 OkHttp 就能搞定请求格式和 OpenAI 一致资料也好搜。适合个人项目、Demo、内部工具。社区 SDK 的好处是省掉自己封装数据模型的功夫但问题在于第三方 SDK 的质量参差不齐有的更新不及时模型名一变就跑不通反而增加排障成本。我当时看了一眼现成 SDK 的源码发现很多只是把 HTTP 请求包了一层那还不如自己写清楚。后端代理则适合正式上线产品Key 放服务端客户端只跟自家服务器通信能有效防止 Key 泄露但开发量会多一部分。我这次在项目里的实际选择是先用官方 API 直连把功能跑通等产品形态稳定之后再把请求迁移到后端代理。这样做的好处是前期能快速验证 AI 功能到底适不适合这个产品避免一上来就写一堆服务端逻辑。1.3 整体架构一次性请求还是流式响应调用 DeepSeek API 时response 有两种返回方式非流式一次性返回完整结果和流式SSE 逐字输出。这个选择直接决定了代码结构。非流式实现最简单发一个 POST 请求拿一段 JSON 解析就行适合聊天记录生成、批量文本处理这类不需要即时反馈的场景。缺点是大模型生成时间可能比较长如果用户面对一个空白页等好几秒体验会有点僵硬。流式则是接口返回后不断推送数据App 端像打字机一样把文字逐渐打出来体感快得多也更适合对话场景。我建议即使是刚开始接入也尽量在架构上预留流式处理的位置。因为从非流式改到流式虽然请求体只多一个stream: true但数据解析逻辑完全是另一套写法。先用非流式验证基本链路再在同一个页面里加一个流式开关这个演进路径最舒服。2. Android端基础环境准备与配置2.1 用到的依赖OkHttp、Gson、协程先把你项目的 Android Studio 升级到比较新的版本太低容易遇到 Kotlin 协程依赖兼容问题。我这里用的是 Kotlin okhttp3 4.xJSON 解析用 Gson。选 OkHttp 而不是用 Java 自带HttpURLConnection是因为它在超时控制、连接复用、错误信息展示上都要清晰很多对大模型这种响应时间偏长的接口尤其重要。在模块的 build.gradle.kts里加依赖dependencies { implementation(com.squareup.okhttp3:okhttp:4.12.0) implementation(com.google.code.gson:gson:2.10.1) implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3) }如果你用的是viewModelScope发起请求还需要加 lifecycle 相关依赖或者直接用lifecycleScope。我为了方便先在 Demo 页面里用lifecycleScope处理避免引入太多框架代码。implementation(androidx.lifecycle:lifecycle-runtime-ktx:2.7.0)2.2 AndroidManifest 网络权限与明文流量配置很多第一次接的人都会漏掉这一步没有在 Manifest 里声明网络权限结果运行起来直接UnknownServiceException或者SocketException。在AndroidManifest.xml的manifest标签下加uses-permission android:nameandroid.permission.INTERNET /如果你的接口地址用的不是 HTTPS或者还在内网调试还需要允许明文流量。DeepSeek API 官方地址是 HTTPS所以这一步不是必需的但如果你在代理调试工具里抓包可能需要临时设置android:usesCleartextTraffictrue。不过这只建议在 debug 环境开release 包一定关掉。2.3 API Key 的存放方式千万别硬编码这里必须强调一个原则sk-xxx这种密钥字符串不要直接写在代码里否则一不小心 push 到 Git 仓库就等于把钥匙挂在了大门口。我用的临时方案是把 Key 放到项目根目录的local.properties文件并把该文件加入.gitignore。然后在模块的build.gradle.kts里读取并生成 BuildConfig 字段import java.util.Properties val localProperties Properties().apply { val file rootProject.file(local.properties) if (file.exists()) file.inputStream().use { load(it) } } android { buildFeatures { buildConfig true } defaultConfig { val apiKey localProperties.getProperty(DEEPSEEK_API_KEY) ?: buildConfigField(String, DEEPSEEK_API_KEY, \$apiKey\) } }这样代码里只需要引用BuildConfig.DEEPSEEK_API_KEY。但话说回来这只能避免误提交并不能真正防逆向。只要 APK 里存在明文 Key总有办法被提取出来。正式上线的 App一定要走“客户端只请求自家后端后端再调 DeepSeek API”的代理方案。这也是我在第 5 部分会展开说的重点。3. 核心代码实现5分钟跑通首次调用3.1 网络层请求封装的思路我做了一个很薄的网络封装不引入任何自定义 Manager只写几个数据类和一个挂起函数。因为 DeepSeek API 的请求就是POST /chat/completions请求体主要字段是model、messages、temperature、stream。先定义消息和请求的数据类data class ChatMessage( val role: String, // system / user / assistant val content: String ) data class ChatRequest( val model: String deepseek-chat, val messages: ListChatMessage, val temperature: Double 0.7, val stream: Boolean false )注意model字段。我写这篇文章时官方常用的 Chat 模型是deepseek-chatReasoner 模型是deepseek-reasoner。模型名这类信息更新比较快如果你发现接口报错提示the supported api model names are ...直接按官方文档的最新列表调整即可。再定义响应数据类非流式响应的 JSON 结构大致长这样{ id: chatcmpl-xxx, object: chat.completion, choices: [ { index: 0, message: { role: assistant, content: 你好我是DeepSeek }, finish_reason: stop } ] }对应的 Kotlin 类data class ChatResponse( val id: String?, val choices: ListChoice? ) { data class Choice( val index: Int?, val message: ChatMessage?, val finish_reason: String? ) }3.2 非流式请求的完整代码我的网络层直接用 OkHttp 的execute()配合withContext(Dispatchers.IO)切到子线程避免在主线程做网络操作。注意 OkHttp 4.x 的Response实现了Closeable所以可以用use自动关闭。package com.example.deepseekdemo import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.util.concurrent.TimeUnit object DeepSeekClient { private val client OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() private val gson Gson() suspend fun chat(apiKey: String, messages: ListChatMessage): String withContext(Dispatchers.IO) { val requestBody ChatRequest( model deepseek-chat, messages messages, temperature 0.7, stream false ) val json gson.toJson(requestBody) val request Request.Builder() .url(https://api.deepseek.com/chat/completions) .addHeader(Authorization, Bearer $apiKey) .addHeader(Content-Type, application/json) .post(json.toRequestBody(application/json.toMediaType())) .build() client.newCall(request).execute().use { response - if (!response.isSuccessful) { val errorBody response.body?.string().orEmpty() throw RuntimeException(HTTP ${response.code} - $errorBody) } val responseBody response.body?.string().orEmpty() val chatResponse gson.fromJson(responseBody, ChatResponse::class.java) chatResponse.choices ?.firstOrNull() ?.message ?.content ?: } } }调用时只需要在页面上弹一个协程lifecycleScope.launch { val reply try { DeepSeekClient.chat( apiKey BuildConfig.DEEPSEEK_API_KEY, messages listOf( ChatMessage(system, 你是一名Android开发专家), ChatMessage(user, Kotlin中如何正确使用协程) ) ) } catch (e: Exception) { 请求失败${e.message} } binding.tvReply.text reply }到这里一个能用的非流式调用就跑通了。整个过程确实只需要几分钟重点就是确认 URL、请求头、请求体格式这三件事都写对。3.3 流式响应SSE怎么做流式响应比非流式复杂一些但也不是什么高科技。DeepSeek API 在streamtrue时会像 SSE 一样返回多行数据每行格式类似data: {id:chatcmpl-xxx,choices:[{delta:{content:你好},index:0}]} data: {id:chatcmpl-xxx,choices:[{delta:{content:我是DeepSeek},index:0}]} data: [DONE]用 OkHttp 的execute()拿到 response 后通过body.charStream()逐行读取过滤以data:开头的内容遇到[DONE]结束。伪代码如下suspend fun chatStream( apiKey: String, messages: ListChatMessage, onDelta: (String) - Unit ) withContext(Dispatchers.IO) { val requestBody ChatRequest( model deepseek-chat, messages messages, stream true ) val request Request.Builder() .url(https://api.deepseek.com/chat/completions) .addHeader(Authorization, Bearer $apiKey) .post(Gson().toJson(requestBody).toRequestBody(application/json.toMediaType())) .build() client.newCall(request).execute().use { response - if (!response.isSuccessful) { throw RuntimeException(HTTP ${response.code}) } val reader response.body?.charStream()?.buffered() ?: returnwithContext reader.useLines { lines - lines.forEach { line - if (line.isBlank()) returnforEach val data line.removePrefix(data:) if (data.trim() [DONE]) returnforEach // 解析 delta.content并回调 val json JsonParser.parseString(data).asJsonObject val content json?.getAsJsonArray(choices) ?.firstOrNull()?.asJsonObject ?.getAsJsonObject(delta) ?.get(content) ?.asString if (!content.isNullOrEmpty()) { onDelta(content) } } } } }实测下来流式响应用户体验好非常多尤其是在网络稍慢的时候至少页面不会一直转圈。缺点是 UI 线程更新需要切回主线程建议在onDelta回调里用withContext(Dispatchers.Main)刷新 TextView或者借助MutableStateFlow收集。3.4 超时、重试与请求参数选择大模型接口偶尔会慢所以要给 OkHttp 设置一个偏长的 readTimeout。我一般设成 60 秒因为复杂 prompt 生成时间可能超过 30 秒。connectTimeout 和 writeTimeout 不需要太长30 秒足够。重试策略上不要搞粗暴的无限重试。比较合理的是“最多重试 2 次并且避开 4xx 错误”。4xx 代表请求本身有问题重试也没用5xx 和网络超时可以重试。实现时在catch (e: Exception)里判断当前次数加一个指数退避比如第一次等 500ms第二次等 1s。参数选择上temperature越接近 0 生成结果越保守稳定越接近 1 越有创造性。普通工具类场景建议 0.3~0.5如果做创意文案可以到 0.7 以上。不过 temperature 不是唯一变量DeepSeek API 还有一些其它参数实际使用中我保持最精简只有需要时再加。4. 常见问题与排查技巧实录4.1 一碰就报错HTTP 400 invalid schema 是什么鬼我在热搜里看到很多人遇到类似api error: 400 invalid schema for function artifact的报错。这种错误最典型的场景是你使用了 function calling / tools 功能并且在请求里传了不符合要求的 JSON Schema。OpenAI 兼容接口对tools里的每个 function 参数 schema 有一套严格校验包括类型必须是object、属性格式要标准、不能有奇怪的 pattern 限制等等。如果你在测试一些三方封装工具也可能因为是工具版本太老、schema 生成不规范导致的。解决办法分几步先去掉tools字段看看请求本身能不能通。如果通了问题就出在工具 Schema 上。检查tools[].function.parameters是否符合 JSON Schema 标准比如type必须是objectproperties必须是对象。不要放pattern: ^(?!.*$)。。。”这种复杂正则接口端的校验器可能支持度有限。如果你用的第三方工具尤其是harness之类的社区项目出现这个错先检查工具版本尽量用官方 API 控制台调试。4.2 网络请求失败超时、EOFException、DNS 解析慢Android 模拟器访问宿主机网络时需要把localhost换成10.0.2.2。这个坑我已经见过好几个人问过了如果你用模拟器调试本地代理记住这一点。但 DeepSeek API 是远程地址一般没有这个问题。另一个常见问题是超时时间太短。默认 OkHttp 的 readTimeout 只有 10 秒而大模型生成答案经常超过 10 秒于是你会看到SocketTimeoutException。我建议 readTimeout 至少 30 秒宁可界面等待也不要莫名中断。还有种情况是java.io.EOFException。这通常说明服务端在响应未完成时就关闭了连接可能是网络代理拦截、也可能是 OkHttp 某个版本的连接池问题。可以尝试换网络环境或者在请求头里显式加上Accept: text/event-stream流式时并关闭连接压缩。如果请求目标是 HTTPS也检查一下系统时间是否正确时间偏差太大会导致 TLS 握手失败。4.3 模型名称报错the supported api model names are ...如果你的报错信息里出现the supported api model names are deepseek-flash, deepseek-v4 ...这类内容别怀疑就是model字段传错了。我见过很多把模型名写成deepseek-r1、deepseek-json之类的例子。解决办法不复杂直接去官方文档看当前支持的模型列表用文档里能查到的最新模型名。我写代码时会把model定义成一个常量object DeepSeekModels { const val CHAT deepseek-chat const val REASONER deepseek-reasoner }这样替换起来也方便。如果你发现官方把模型升级了直接改常量一处就行。另外注意第三方中转站可能会起一些自定义模型名不要照搬网上的旧代码最好先GET /models接口确认一下。4.4 常见问题速查表现象最常见原因解决办法HTTP 401API Key 错误或过期检查 Key 是否复制完整重新生成测试HTTP 400请求体格式/model 名称错误校验 JSON确认 model 官方名称HTTP 402余额不足到控制台充值或检查配额HTTP 429请求频率超限增加请求间隔做本地限流SocketTimeoutreadTimeout 太短OkHttp 设置 60 秒以上EOFException网络代理/连接被中断换网络环境关代理抓包返回内容乱码编码解析问题确认请求头Content-Type和响应读取都使用 UTF-8这张表是我在实际联调中根据团队反馈整理出来的不一定覆盖全部情况但覆盖了九成以上新手会遇到的问题。看到报错先不要慌照着表里的方向查通常几分钟就能定位。5. 上线前必看的稳定性与安全优化5.1 API Key 千万别跟 App 一起打包前面提到过用BuildConfig隐藏 Key但这只是防君子不防小人。只要 APK 在用户手里专业的逆向工具就能把字符串提取出来。所以一旦你的项目要发布到应用商店必须做成“客户端 - 自家后端 - DeepSeek API”的三层架构。后端代理的好处有三个一是 Key 不出服务器二是可以在后端加访问控制比如用户登录鉴权三是可以统一做缓存和计费。Android 端只需要请求自家服务器的一个接口把问题和上下文传过去再由后端返回结果。这样做起来确实多一层开发工作但这是对用户和项目都负责的做法。5.2 请求限流、缓存与降级策略AI 接口的响应时间和成本都比普通接口高所以客户端一定要有兜底方案。我在项目里做了三个基础策略本地缓存对相同或相似的问题在一定时间窗口内直接返回缓存的回答避免重复调用。缓存 Key 建议用输入内容的哈希值同时设置过期时间。请求合并如果用户连续发多个问题可以做成队列串行调用 API避免触发限流。也可以在 UI 层做防抖防止用户连点按钮。降级提示接口失败时不要只给一个“网络错误”而是准备一条友好的兜底文案或者建议用户稍后重试。可以将错误类型分为“参数错误”“服务不可用”“余额问题”等分别给出不同文案。这些内容看起来不起眼但上线后对稳定性的影响非常明显。AI 接口不像普通接口那么稳定必须把“失败”当成预期内情况来处理。5.3 后续还能怎么扩展接入 DeepSeek API 只是第一步后续可以做的东西还有很多。最近我在琢磨的两个方向是多轮对话管理和结构化输出。多轮对话要维护好messages数组把用户输入和 AI 输出都存起来再配合系统提示词控制角色。同时要注意上下文长度太长的会话需要做摘要或者裁剪。结构化输出则可以利用 tool/function calling让模型返回 JSON 而不是纯文本比如让 AI 提取一条短信里的时间、地点、事件就能直接对接本地日历、提醒事项。这个玩法在工具类 App 里非常实用。如果你有兴趣可以先用response_format: {type: json_object}这种更简单的方式体验一下然后再进一步研究 function calling。最后的实操心得从我个人的使用体验来说接入 DeepSeek API 最顺的路径就是“先把非流式跑通再改流式最后补安全层”。别一上来就追求完美架构因为 AI 应用的交互方式变化很快先验证产品场景是不是真的需要这个功能比一开始就写一堆抽象代码重要得多。再分享一个小技巧我在调试阶段会专门建一个测试页面放一个按钮和一个纯文本显示区每次请求后把调用耗时和原始 JSON 打印出来。别小看这个笨办法它帮你把“模型返回了什么”和“UI 显示的什么”彻底拆开排障效率极高。等确认接口没问题再慢慢去做气泡 UI、Markdown 渲染、断点续传这些体验优化每一步都会稳很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →