Android适配器模式配 TaoToken:settings.json 骨架与验证动作
1. 从 RecyclerView 里那个“写死”的模型请求说起如果你做过 Android 上的 AI 对话类功能大概率写过这样的代码列表项里点一下“发送”然后某个 Presenter 或 ViewModel 里直接OkHttp拼一个 URL把 Key 塞进 Header请求某个固定厂商的接口。第一版能跑第二版产品说“再加一个模型”第三版说“不同模型走不同通道”代码就开始失控了。问题不在于网络请求本身而在于列表项和数据源之间的那层“翻译”被写死了。RecyclerView 只认ViewHolder和Adapter它不关心你背后是 OpenAI 风格还是 Anthropic 风格也不关心 Key 从哪来。这正好是适配器模式Adapter Pattern的主场把一个类的接口变成客户端期待的另一种接口让原本不匹配、无法一起工作的东西能协作。放到 Android 开发里这个“客户端”就是你的RecyclerView.Adapter它期待的是统一的ChatMessage和统一的发送回调而“不匹配的类”是各家模型 API 的请求格式、鉴权方式、返回结构。我们要做的就是写一个模型适配器层把多模型 API 统一成 Adapter 能消费的接口。这篇就聚焦一件事用适配器模式统一接入多模型 API以 TaoToken 作为统一 Key 和 API 通道交付一份可复制的settings.json配置骨架再走一遍连通性验证让你在 RecyclerView 列表项里切换模型服务时不用改 Adapter 的核心逻辑。适合谁看已经会写 RecyclerView、写过 Retrofit/OkHttp 请求但被多模型接入搞得很烦的 Android 开发者。读完你能拿到一份能直接落地的配置骨架和验证脚本。2. 为什么用 TaoToken 做统一通道以及前置准备多模型接入最烦的三件事Key 管理、请求格式差异、通道切换。如果每个模型都单独申请 Key、单独写一套请求封装Adapter 里就会堆满if (model xxx)的分支这跟适配器模式的初衷完全相反。TaoToken 在这里扮演的是“统一入口”的角色一个 Key、一个 API 地址背后对接多个模型服务。对 Android 端来说你只需要面向一个 BaseUrl 和一个鉴权头写代码模型差异交给适配器层去翻译。这样 Adapter 拿到的永远是统一的ChatRequest和ChatResponse切换模型只是换一个配置字段。前置准备分三步都不复杂第一步拿到 API Key。访问https://taotoken.net/api-keys登录后在控制台创建 Key。建议给 Android 项目单独建一个 Key方便后续按项目排查用量。第二步确认 API 基地址。TaoToken 的 API 入口是https://taotoken.net/api注意这个地址不带任何查询参数直接作为 Retrofit 的baseUrl使用。如果你在浏览器里手动测记得路径要拼完整。第三步想清楚你的适配器边界。我的做法是定义一个ModelAdapter接口里面只有两个方法buildRequest(ChatMessage): RequestBody和parseResponse(String): ChatMessage。所有模型差异都收敛在这两个方法里RecyclerView 的 Adapter 只跟ModelAdapter打交道。提示不要把 Key 硬编码进settings.json后提交到 Git。下面骨架里我会用占位符实际项目建议走local.properties或 BuildConfig 注入。3. 可复制的 settings.json 配置骨架Android 项目里用settings.json做多模型配置好处是结构清晰、方便热更新、也方便在调试时手动改。下面这份骨架你可以直接复制改掉apiKey占位符就能用。{ version: 1.0, defaultProvider: taotoken, providers: { taotoken: { baseUrl: https://taotoken.net/api, apiKey: sk-REPLACE_WITH_YOUR_KEY, authHeader: Authorization, authPrefix: Bearer , timeoutSeconds: 30, models: [ { id: general-chat, displayName: 通用对话, endpoint: /v1/chat/completions, requestStyle: openai, maxTokens: 2048 }, { id: claude-code, displayName: Claude 编码, endpoint: /v1/messages, requestStyle: anthropic, maxTokens: 4096 } ] } }, adapterMapping: { openai: OpenAiStyleAdapter, anthropic: AnthropicStyleAdapter } }这份骨架里有几个关键设计点值得展开说。providers.taotoken.baseUrl固定为https://taotoken.net/api所有模型共用。authHeader和authPrefix抽出来是因为不同风格的接口鉴权头写法可能不同抽出来之后适配器只需要读配置不用写死字符串。models数组是核心。每个模型有id、endpoint、requestStyle三个关键字段。requestStyle决定了用哪个适配器实现去翻译请求体。adapterMapping把requestStyle映射到具体的适配器类名这样新增模型风格时只需要加一个适配器类改一行映射。在 Android 里解析这份配置可以用kotlinx.serialization或 Gson。下面是一个 Kotlin 数据类骨架Serializable data class AppSettings( val version: String, val defaultProvider: String, val providers: MapString, ProviderConfig, val adapterMapping: MapString, String ) Serializable data class ProviderConfig( val baseUrl: String, val apiKey: String, val authHeader: String, val authPrefix: String, val timeoutSeconds: Long, val models: ListModelConfig ) Serializable data class ModelConfig( val id: String, val displayName: String, val endpoint: String, val requestStyle: String, val maxTokens: Int )解析完之后ModelAdapterFactory根据requestStyle从adapterMapping里找到适配器类实例化后交给 RecyclerView 的 Adapter 使用。这样列表项点击切换模型时只是换了一个ModelAdapter实例Adapter 本身的onBindViewHolder逻辑完全不用动。4. 适配器层与 RecyclerView 的对接代码配置有了接下来是把适配器模式真正落到代码里。先定义统一接口interface ModelAdapter { fun buildRequestBody(message: ChatMessage, config: ModelConfig): RequestBody fun parseResponse(raw: String): ChatMessage fun endpoint(config: ModelConfig): String }然后是两个实现。OpenAI 风格的适配器class OpenAiStyleAdapter : ModelAdapter { override fun buildRequestBody(message: ChatMessage, config: ModelConfig): RequestBody { val json buildJsonObject { put(model, config.id) put(max_tokens, config.maxTokens) putJsonArray(messages) { addJsonObject { put(role, user) put(content, message.content) } } } return json.toString().toRequestBody(application/json.toMediaType()) } override fun parseResponse(raw: String): ChatMessage { val obj Json.parseToJsonElement(raw).jsonObject val content obj[choices]?.jsonArray?.firstOrNull() ?.jsonObject?.get(message) ?.jsonObject?.get(content) ?.jsonPrimitive?.content ?: return ChatMessage(role assistant, content content) } override fun endpoint(config: ModelConfig) config.endpoint }Anthropic 风格的适配器结构类似区别在请求体字段名和响应解析路径。这里不展开全部代码重点是所有差异都被关在适配器内部。RecyclerView 的 Adapter 只依赖ModelAdapter接口class ChatListAdapter( private val messages: ListChatMessage, private val modelAdapter: ModelAdapter, private val modelConfig: ModelConfig, private val client: OkHttpClient ) : RecyclerView.AdapterChatViewHolder() { override fun onBindViewHolder(holder: ChatViewHolder, position: Int) { val msg messages[position] holder.bind(msg) holder.sendButton.setOnClickListener { val body modelAdapter.buildRequestBody(msg, modelConfig) val request Request.Builder() .url(https://taotoken.net/api modelAdapter.endpoint(modelConfig)) .header(Authorization, Bearer ${BuildConfig.TAOTOKEN_KEY}) .post(body) .build() client.newCall(request).enqueue(/* 回调里调 parseResponse */) } } }切换模型时只需要在 Activity 或 Fragment 里换掉modelAdapter和modelConfig两个参数然后notifyDataSetChanged()。列表项本身、ViewHolder、布局文件都不用改。这就是适配器模式带来的解耦收益。5. 连通性验证从命令行到 Android 端配置和代码写完了别急着跑 App先用命令行验证通道是通的。这一步能帮你快速区分“是配置问题”还是“是 Android 代码问题”。先验证 Key 和 BaseUrl 是否可用。用 curl 发一个最小请求curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的KEY \ -H Content-Type: application/json \ -d { model: general-chat, max_tokens: 64, messages: [{role: user, content: ping}] }如果返回里有choices字段和一段文本说明 Key、BaseUrl、请求格式都对。如果返回 401检查 Key 是否复制完整如果返回 404检查endpoint路径是否拼错注意baseUrl末尾不要多加斜杠。命令行通了之后在 Android 端加一个最小的验证入口。我习惯在MainActivity里放一个隐藏按钮点击后发一个ping请求把原始响应打到 Logcatfun verifyConnectivity() { val client OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() val body {model:general-chat,max_tokens:16,messages:[{role:user,content:ping}]} .toRequestBody(application/json.toMediaType()) val request Request.Builder() .url(https://taotoken.net/api/v1/chat/completions) .header(Authorization, Bearer ${BuildConfig.TAOTOKEN_KEY}) .post(body) .build() client.newCall(request).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { Log.d(TaoTokenVerify, code${response.code} body${response.body?.string()}) } override fun onFailure(call: Call, e: IOException) { Log.e(TaoTokenVerify, failed, e) } }) }实测下来Logcat 里看到code200并且 body 里有内容就说明 Android 端的网络层、鉴权头、请求体格式全部正确。这时候再回到 RecyclerView 的列表项里点发送成功率会高很多。注意Android 9 以上默认禁止明文 HTTP但 TaoToken 的 API 是 HTTPS不需要额外配置usesCleartextTraffic。如果你在模拟器里遇到网络问题先检查是否开了飞行模式或 DNS 异常。6. 本篇常见错排查错误一settings.json解析失败App 启动崩溃。最常见原因是 JSON 里多了尾逗号或者apiKey占位符没替换导致反序列化类型不匹配。排查方法把settings.json丢进任意 JSON 校验工具跑一遍确认结构合法。Kotlin 侧建议给apiKey字段加默认值避免空值崩溃。错误二请求返回 401 Unauthorized。九成是authPrefix拼错。TaoToken 用的是Bearer加空格如果你在配置里写成Bearer不带空格拼出来的 Header 就是Bearer sk-xxx变成Bearersk-xxx服务端认不出来。检查authHeader和authPrefix两个字段的拼接结果。错误三切换模型后请求路径不对。比如从general-chat切到claude-codeendpoint从/v1/chat/completions变成/v1/messages但代码里如果写死了路径就会 404。确认endpoint是从ModelConfig里读的而不是硬编码。错误四RecyclerView 列表项点击后没有反应。检查onBindViewHolder里是否给按钮设置了setOnClickListener以及modelAdapter是否在切换模型后重新赋值。如果用了notifyDataSetChanged()但列表没刷新确认messages是可变列表且数据源确实变了。错误五响应解析出来是空字符串。不同风格的响应结构不同OpenAI 风格在choices[0].message.contentAnthropic 风格在content[0].text。如果你用 OpenAI 适配器去解析 Anthropic 的响应自然拿不到内容。确认requestStyle和实际请求的模型匹配。排障时如果拿不准是 Key 问题还是代码问题最快的办法是回到第 5 节的 curl 命令用同一个 Key 和路径测一遍。命令行通了问题一定在 Android 代码侧。7. 下一步把适配器模式用顺走到这里你已经有了一个能跑通的多模型接入骨架settings.json管配置ModelAdapter管翻译RecyclerView 管展示TaoToken 管通道。新增一个模型时你只需要在models数组里加一项如果请求风格是已有的连适配器类都不用写。如果你在验证过程中遇到接入层面的报错建议先去https://taotoken.net/api-keys确认 Key 状态再对照https://taotoken.net/doc里的接口说明核对路径和字段。想先直观感受一下模型返回效果可以直接用https://taotoken.net/model-chat在网页里发一条消息确认通道本身没问题。对于需要长期在 Android 项目里做编码辅助、或者要接 Agent 类能力的场景可以了解一下https://taotoken.net/coding-plan它在用量和通道稳定性上更适合持续开发。配置骨架和适配器代码都可以直接复用切换的只是底层通道的接入方式。最后留一个我踩过的坑settings.json里的timeoutSeconds别设太小。移动网络下首包延迟偶尔会超过 10 秒设成 30 秒比较稳。如果你在列表项里做流式输出记得把 OkHttp 的readTimeout单独调大否则长回复会被截断。