ARTICLE DETAIL

资讯详情

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

KMP 全栈开发实战:用 Compose Multiplatform 从 Android 到 AI Agent 的配置骨架

KMP 全栈开发实战:用 Compose Multiplatform 从 Android 到 AI Agent 的配置骨架 1. 从 Android 单端到 KMP 全栈AI Agent 接入的真实痛点如果你现在手上有一个 Compose Multiplatform 项目Android 端 UI 已经跑起来了想再往前一步接入 AI Agent 能力大概率会卡在同一个地方Key 放哪、请求从哪个模块发、Android 和 Desktop 要不要各写一套网络层。我见过太多项目在androidMain里直接OkHttpClient硬编码一个 API Key等到要加 iOS 或 Desktop 目标时整段逻辑只能复制粘贴改一处漏三处。Kotlin Multiplatform 的价值在这里才真正体现出来。Compose Multiplatform 负责共享 UI而 AI Agent 的调用链路——Prompt 组装、消息历史、流式解析、错误重试——全部是纯 Kotlin 逻辑天然属于commonMain。你只需要在共享模块里维护一套 Agent 客户端Android、Desktop、iOS 各自只负责把结果渲染出来。这篇要交付的是一套可以直接复制的配置骨架settings.json和config.toml两个文件怎么放、统一 Key 通道怎么设计、Gradle 依赖怎么声明最后用一个端到端的连通性验证动作确认整条链路是通的。目标不是讲概念而是让你从零搭出一个能跑起来的跨端智能应用雏形。适合已经写过 Compose、但对 KMP 模块划分和 AI 服务接入还不太熟的同学。2. TaoToken 前置统一 Key 通道与项目结构在动手写代码之前先把 Key 通道这件事定下来。跨端项目最忌讳的就是每个平台各自读一份配置Android 读BuildConfig、Desktop 读环境变量、iOS 读 plist最后没人说得清线上到底用的哪个 Key。我的做法是所有平台统一走一个AiConfig数据类由共享模块在启动时注入Key 本身从本地配置文件或环境变量读取绝不进版本库。TaoToken 在这里扮演的是统一模型接入层的角色。它提供 OpenAI 兼容的接口形态意味着你在commonMain里写的请求代码不需要为不同模型厂商改结构换模型只改baseUrl和model字段。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为baseUrl使用。推荐的 KMP 项目结构如下重点是shared模块内部的分层MyKmpAgent/ ├── shared/ │ ├── src/ │ │ ├── commonMain/kotlin/ │ │ │ ├── ai/ │ │ │ │ ├── agent/AgentClient.kt │ │ │ │ ├── llm/LlmProvider.kt │ │ │ │ └── prompt/PromptBuilder.kt │ │ │ ├── network/HttpEngineFactory.kt │ │ │ ├── config/AiConfig.kt │ │ │ └── domain/Message.kt │ │ ├── androidMain/kotlin/ │ │ ├── desktopMain/kotlin/ │ │ └── iosMain/kotlin/ │ └── build.gradle.kts ├── androidApp/ ├── desktopApp/ └── settings.gradle.ktsai包只放纯逻辑不依赖任何平台 APInetwork包负责创建 HTTP 引擎这里用 Ktor 的HttpClient引擎实现按平台 expect/actual 分发config包持有AiConfig。Android 端和 Desktop 端各自在入口处构造AiConfig并传给共享的AgentClient。Key 的读取策略建议这样本地开发时放在项目根目录的local.propertiesAndroid 侧或环境变量Desktop 侧共享模块只接收字符串不关心来源。这样既避免了 Key 硬编码也让 CI 环境可以统一注入。3. 可复制配置settings.json 与 config.toml 骨架这一节给出两个配置文件的完整骨架。settings.json用于声明模型与 Agent 行为参数config.toml用于声明构建与运行期的基础设施参数。两者都放在项目根目录由共享模块的配置加载器读取。先看settings.json{ ai: { provider: taotoken, baseUrl: https://taotoken.net/api, model: claude-3-5-sonnet, temperature: 0.7, maxTokens: 2048, timeoutSeconds: 60 }, agent: { systemPrompt: 你是一个跨端智能助手回答简洁准确。, maxHistoryRounds: 10, enableStream: true }, keyChannel: { source: env, envName: TAOTOKEN_API_KEY, fallbackFile: local.properties } }字段说明baseUrl固定为 TaoToken 的 API 基址不带路径后缀model按你实际开通的模型填写keyChannel.source支持env和file两种env优先读环境变量读不到再回退到fallbackFile。enableStream控制是否走流式返回Compose 端做打字机效果时需要打开。再看config.toml这个文件主要给 Gradle 和运行期读取[build] kotlinVersion 2.0.21 composeVersion 1.7.0 ktorVersion 3.0.0 [targets] android true desktop true ios false [network] connectTimeoutMs 15000 requestTimeoutMs 60000 retryCount 2 [logging] level info logRequestBody falselogRequestBody默认关闭避免把用户输入打到日志里。retryCount设为 2配合 Ktor 的HttpRequestRetry插件使用。接下来是共享模块的 Gradle 依赖声明shared/build.gradle.kts关键片段kotlin { androidTarget() jvm(desktop) // iosX64(); iosArm64(); iosSimulatorArm64() sourceSets { val commonMain by getting { dependencies { implementation(compose.runtime) implementation(compose.foundation) implementation(io.ktor:ktor-client-core:3.0.0) implementation(io.ktor:ktor-client-content-negotiation:3.0.0) implementation(io.ktor:ktor-serialization-kotlinx-json:3.0.0) implementation(org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3) implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0) } } val androidMain by getting { dependencies { implementation(io.ktor:ktor-client-okhttp:3.0.0) } } val desktopMain by getting { dependencies { implementation(io.ktor:ktor-client-cio:3.0.0) } } } }注意commonMain里只声明ktor-client-core具体引擎在平台源集里补。这样 Android 用 OkHttp、Desktop 用 CIO共享代码完全无感。配置加载器写在commonMain用kotlinx.serialization解析settings.jsonSerializable data class AiSettings( val ai: AiSection, val agent: AgentSection, val keyChannel: KeyChannelSection ) Serializable data class AiSection( val provider: String, val baseUrl: String, val model: String, val temperature: Double 0.7, val maxTokens: Int 2048, val timeoutSeconds: Int 60 )AiConfig的构造逻辑先读环境变量TAOTOKEN_API_KEY为空则读local.properties里的同名键再为空就抛异常并在 UI 层提示用户配置。这一步是整个 Key 通道的核心务必只在一处实现。4. 端到端连通性验证一次请求跑通全链路配置就位后写一个最小的AgentClient来验证链路。先定义统一接口方便后续换模型interface LlmProvider { suspend fun chat(messages: ListMessage): String suspend fun chatStream(messages: ListMessage): FlowString }Message是共享的领域模型Serializable data class Message( val role: String, val content: String )AgentClient的实现走 OpenAI 兼容的/v1/chat/completions路径class AgentClient( private val config: AiConfig, private val httpClient: HttpClient ) : LlmProvider { override suspend fun chat(messages: ListMessage): String { val response httpClient.post(${config.baseUrl}/v1/chat/completions) { header(HttpHeaders.Authorization, Bearer ${config.apiKey}) header(HttpHeaders.ContentType, ContentType.Application.Json) setBody( ChatRequest( model config.model, messages messages, temperature config.temperature, maxTokens config.maxTokens ) ) } val body response.bodyChatResponse() return body.choices.first().message.content } }请求体与响应体的序列化类Serializable data class ChatRequest( val model: String, val messages: ListMessage, val temperature: Double, SerialName(max_tokens) val maxTokens: Int ) Serializable data class ChatResponse( val choices: ListChoice ) Serializable data class Choice( val message: Message )HTTP 引擎的 expect/actual 分发// commonMain expect fun createHttpEngine(): HttpClientEngineFactory* // androidMain actual fun createHttpEngine() OkHttp // desktopMain actual fun createHttpEngine() CIO在commonMain里组装客户端fun buildAgentClient(config: AiConfig): AgentClient { val client HttpClient(createHttpEngine()) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys true }) } install(HttpTimeout) { requestTimeoutMillis config.timeoutSeconds * 1000L } } return AgentClient(config, client) }验证动作在 Android 的MainActivity或 Desktop 的main里调用一次chat传入一条用户消息打印返回内容。如果控制台输出模型回复说明 Key 通道、网络层、序列化、共享模块全部打通。这一步跑通之前不要急着写 UI否则出问题很难定位是配置还是渲染。Compose 端调用示例Composable fun AgentScreen(client: AgentClient) { var reply by remember { mutableStateOf() } LaunchedEffect(Unit) { reply client.chat(listOf(Message(user, 你好做个自我介绍))) } Text(text reply) }Android 和 Desktop 共用这个AgentScreen差异只在client的构造来源。5. 本篇常见错排查401 或 403 返回九成是 Key 没读到。先确认环境变量名和settings.json里的envName完全一致大小写敏感。Android 侧如果用了local.properties注意该文件默认在.gitignore里CI 上需要单独注入。另外检查Authorization头是不是Bearer加空格再加 Key少空格会直接 401。序列化报错Unknown key模型返回的 JSON 字段比你的ChatResponse多。在Json配置里加ignoreUnknownKeys true上面代码已经带了如果你自己写的没加补上即可。Desktop 端连不上但 Android 正常多半是引擎选错。Desktop 用 CIOAndroid 用 OkHttp如果createHttpEngine的 actual 实现写反了会出现平台特有的连接异常。检查desktopMain和androidMain的 actual 函数是否对应。流式返回卡住不输出enableStream打开后Ktor 需要用preparePost加bodyAsChannel逐行读不能直接用bodyChatResponse()。流式解析要按data:前缀切分遇到[DONE]结束。这块建议单独封装一个chatStream实现不要和同步chat混在一起。Gradle 同步失败提示找不到 compose 插件settings.gradle.kts里的pluginManagement仓库需要包含google()和mavenCentral()Compose Multiplatform 的插件坐标是org.jetbrains.compose版本和config.toml里的composeVersion保持一致。Key 泄露风险任何时候不要把 Key 写进settings.json提交到仓库。settings.json只放envName和fallbackFile真实 Key 走环境变量或本地文件。如果已经提交过立刻在控制台轮换 Key。6. 下一步把骨架跑成真正的 Agent到这里你已经有了一个能跨 Android 和 Desktop 发请求的共享 Agent 客户端。接下来要做的不是继续堆 UI而是把 Agent 的几块核心能力补进commonMainPrompt 模板管理、多轮历史裁剪、工具调用协议解析。这些逻辑和平台无关放在共享模块里维护成本最低。如果你要长期在这个项目上做编码和 Agent 工作流建议把 Key 管理和额度规划放到 Coding Plan 里统一处理入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 先把 API Key 建好再对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 确认请求格式和模型名。想先验证模型返回效果可以直接在模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里试几条 Prompt确认没问题再写进代码。骨架跑通只是起点。真正让 KMP 项目在 AI 时代站住脚的是共享模块里那套与平台无关的 Agent 引擎——它今天服务 Android 和 Desktop明天加 iOS 和 Web 时你只需要补一个 UI 层。
返回列表