15 KiB
多模型并行调用功能实现计划
Goal: 支持同时配置3个LLM模型,主页面同时显示3个结果框,发送时并行调用所有已配置的模型
Architecture: 配置页面同时显示3组模型配置;主页结果显示区改为水平布局3个卡片;发送时并行调用所有模型
Tech Stack: Kotlin, Android, OkHttp, Coroutine, SharedPreferences
任务1: 修改 SecondActivity.kt 数据结构
Files:
-
Modify:
flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt -
Step 1: 修改 LLMConfig 类,添加 enabled 字段
在第650行 data class LLMConfig 中添加:
data class LLMConfig(
val name: String,
val baseUrl: String,
val apiKey: String,
val model: String,
val enabled: Boolean = true // 新增:是否启用
)
- Step 2: 修改 SettingsData 类
在第663行 data class SettingsData 中,将 llmConfigs 类型改为固定3个元素的列表:
data class SettingsData(
val llmConfigs: List<LLMConfig>,
val selectedLlmIndex: Int?,
val headerConfigs: List<HeaderConfig>?,
val promptConfigs: List<PromptConfig>?,
val buttonConfigs: List<ButtonConfig>?,
val noteApiConfig: NoteApiConfig?,
val llmConfig: LLMConfig? = null
)
- Step 3: 提交
git add flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt
git commit -m "feat: add enabled field to LLMConfig"
任务2: 修改 activity_second.xml 布局
Files:
-
Modify:
flomo-ai/app/src/main/res/layout/activity_second.xml -
Step 1: 将现有卡片内容包装为可复用的 LinearLayout
保留第一组配置,作为模板。新增2组相同的结构。
每组配置需要唯一ID:
- 第一组: etModelName1, etBaseUrl1, etApiKey1, btnToggleApiKey1, etModel1, btnTestConnection1, tvTestStatus1
- 第二组: etModelName2, etBaseUrl2, etApiKey2, btnToggleApiKey2, etModel2, btnTestConnection2, tvTestStatus2
- 第三组: etModelName3, etBaseUrl3, etApiKey3, btnToggleApiKey3, etModel3, btnTestConnection3, tvTestStatus3
布局结构:
<!-- 第一组模型配置 -->
<androidx.cardview.widget.CardView>
<LinearLayout>
<TextView android:text="模型1"/>
<!-- 配置名称、Base URL、API Key、Model、测试按钮 -->
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 第二组模型配置 -->
<androidx.cardview.widget.CardView>
<!-- 同上结构,ID带2 -->
</androidx.cardview.widget.CardView>
<!-- 第三组模型配置 -->
<androidx.cardview.widget.CardView>
<!-- 同上结构,ID带3 -->
</androidx.cardview.widget.CardView>
- Step 2: 提交
git add flomo-ai/app/src/main/res/layout/activity_second.xml
git commit -m "feat: add 3 model config cards layout"
任务3: 修改 SecondActivity.kt 逻辑
Files:
-
Modify:
flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt:152-700 -
Step 1: 添加3组视图引用
在第46行后添加3组EditText和Button引用:
private lateinit var etBaseUrl1: EditText
private lateinit var etApiKey1: EditText
private lateinit var etModel1: EditText
private lateinit var etModelName1: EditText
private lateinit var btnTestConnection1: Button
private lateinit var tvTestStatus1: TextView
private lateinit var etBaseUrl2: EditText
private lateinit var etApiKey2: EditText
private lateinit var etModel2: EditText
private lateinit var etModelName2: EditText
private lateinit var btnTestConnection2: Button
private lateinit var tvTestStatus2: TextView
private lateinit var etBaseUrl3: EditText
private lateinit var etApiKey3: EditText
private lateinit var etModel3: EditText
private lateinit var etModelName3: EditText
private lateinit var btnTestConnection3: Button
private lateinit var tvTestStatus3: TextView
- Step 2: 修改 initViews() 方法,初始化所有视图
etBaseUrl1 = findViewById(R.id.etBaseUrl1)
// ... 其他第一组视图
etBaseUrl2 = findViewById(R.id.etBaseUrl2)
// ... 其他第二组视图
etBaseUrl3 = findViewById(R.id.etBaseUrl3)
// ... 其他第三组视图
- Step 3: 修改 loadConfigurations() 方法,加载3个配置
从 SharedPreferences 加载3个配置,填充到对应视图。如果配置不足3个,补齐空配置。
- Step 4: 修改 saveConfigurations() 方法,保存3个配置
从3个视图组读取数据,保存为包含3个元素的列表。
- Step 5: 添加3个独立的测试方法
private fun testConnection1() { testConnection(etBaseUrl1, etApiKey1, etModel1, tvTestStatus1, btnTestConnection1) }
private fun testConnection2() { testConnection(etBaseUrl2, etApiKey2, etModel2, tvTestStatus2, btnTestConnection2) }
private fun testConnection3() { testConnection(etBaseUrl3, etApiKey3, etModel3, tvTestStatus3, btnTestConnection3) }
- Step 6: 设置3个测试按钮的点击事件
btnTestConnection1.setOnClickListener { testConnection1() }
btnTestConnection2.setOnClickListener { testConnection2() }
btnTestConnection3.setOnClickListener { testConnection3() }
- Step 7: 提交
git add flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt
git commit -m "feat: support 3 model configs with individual test"
任务4: 修改 activity_main.xml 布局
Files:
-
Modify:
flomo-ai/app/src/main/res/layout/activity_main.xml -
Step 1: 修改结果显示区为水平布局
将原来的单个结果卡片改为水平 ScrollView + LinearLayout,包含3个结果卡片。
<!-- 结果显示区标题 -->
<TextView android:text="大模型返回结果"/>
<!-- 水平滚动容器 -->
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 结果卡片1: 300dp宽 -->
<include layout="@layout/result_card" android:id="@+id/resultCard1"/>
<!-- 结果卡片2: 300dp宽 -->
<include layout="@layout/result_card" android:id="@+id/resultCard2"/>
<!-- 结果卡片3: 300dp宽 -->
<include layout="@layout/result_card" android:id="@+id/resultCard3"/>
</LinearLayout>
</HorizontalScrollView>
- Step 2: 创建 result_card.xml 布局
创建 flomo-ai/app/src/main/res/layout/result_card.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/result_card_bg"
android:padding="16dp"
android:layout_marginEnd="12dp">
<TextView
android:id="@+id/tvModelName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"/>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/primary"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/outputStatusLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/primary"/>
<EditText
android:id="@+id/outputTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:textSize="14sp"
android:editable="true"
android:gravity="top"
android:background="@android:drawable/edit_text"/>
<Button
android:id="@+id/btnCopyResult"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginTop="8dp"
android:text="复制结果"/>
</LinearLayout>
- Step 3: 提交
git add flomo-ai/app/src/main/res/layout/activity_main.xml flomo-ai/app/src/main/res/layout/result_card.xml
git commit -m "feat: add horizontal result cards layout"
任务5: 修改 MainActivity.kt 逻辑
Files:
-
Modify:
flomo-ai/app/src/main/java/com/example/flomo_ai/MainActivity.kt -
Step 1: 添加3组视图引用
private lateinit var outputTextView1: EditText
private lateinit var outputTextView2: EditText
private lateinit var outputTextView3: EditText
private lateinit var outputStatusLabel1: TextView
private lateinit var outputStatusLabel2: TextView
private lateinit var outputStatusLabel3: TextView
private lateinit var tvModelName1: TextView
private lateinit var tvModelName2: TextView
private lateinit var tvModelName3: TextView
private lateinit var btnCopyResult1: Button
private lateinit var btnCopyResult2: Button
private lateinit var btnCopyResult3: Button
- Step 2: 在 onCreate() 中初始化视图
outputTextView1 = findViewById(R.id.outputTextView1)
outputTextView2 = findViewById(R.id.outputTextView2)
outputTextView3 = findViewById(R.id.outputTextView3)
// ... 其他视图
- Step 3: 修改 sendButton.setOnClickListener,并行发送
sendButton.setOnClickListener {
val inputText = inputEditText.text.toString()
if (inputText.isEmpty()) {
Toast.makeText(this, "请输入内容", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val promptContent = promptContentText.text.toString()
val fullContent = if (promptContent.isNotEmpty() && promptContent != "无特殊指令") {
"$promptContent$inputText"
} else {
inputText
}
// 获取所有已启用的模型配置
val llmConfigs = loadLlmConfigs()
// 初始化所有状态
llmConfigs.forEachIndexed { index, config ->
when(index) {
0 -> { outputTextView1.setText(""); outputStatusLabel1.text = "等待中..." }
1 -> { outputTextView2.setText(""); outputStatusLabel2.text = "等待中..." }
2 -> { outputTextView3.setText(""); outputStatusLabel3.text = "等待中..." }
}
}
// 并行发送请求
llmConfigs.forEachIndexed { index, config ->
if (config.apiKey.isBlank() || config.baseUrl.isBlank()) {
// 跳过未配置的模型
when(index) {
0 -> outputStatusLabel1.text = "未配置"
1 -> outputStatusLabel2.text = "未配置"
2 -> outputStatusLabel3.text = "未配置"
}
return@forEachIndexed
}
CoroutineScope(Dispatchers.IO).launch {
try {
val result = callLlm(config, fullContent)
withContext(Dispatchers.Main) {
when(index) {
0 -> {
outputTextView1.setText(result)
outputStatusLabel1.text = if (result.startsWith("错误:")) "失败" else "完成"
}
1 -> {
outputTextView2.setText(result)
outputStatusLabel2.text = if (result.startsWith("错误:")) "失败" else "完成"
}
2 -> {
outputTextView3.setText(result)
outputStatusLabel3.text = if (result.startsWith("错误:")) "失败" else "完成"
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
when(index) {
0 -> { outputTextView1.setText("错误: ${e.message}"); outputStatusLabel1.text = "失败" }
1 -> { outputTextView2.setText("错误: ${e.message}"); outputStatusLabel2.text = "失败" }
2 -> { outputTextView3.setText("错误: ${e.message}"); outputStatusLabel3.text = "失败" }
}
}
}
}
}
}
- Step 4: 添加 loadLlmConfigs() 和 callLlm() 方法
private fun loadLlmConfigs(): List<LLMConfig> {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null) ?: return emptyList()
return try {
val settings = Gson().fromJson(json, SettingsData::class.java)
settings.llmConfigs ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}
private suspend fun callLlm(config: LLMConfig, content: String): String {
return withContext(Dispatchers.IO) {
val messagesJson = JSONArray().apply {
put(JSONObject().apply {
put("role", "user")
put("content", content)
})
}
val requestBody = JSONObject().apply {
put("model", config.model.ifEmpty { "gpt-4o" })
put("messages", messagesJson)
put("max_tokens", 65536)
}
val client = OkHttpClient()
val requestBuilder = Request.Builder()
.url("${config.baseUrl}/chat/completions")
.addHeader("Content-Type", "application/json")
if (config.apiKey.isNotBlank()) {
requestBuilder.addHeader("Authorization", "Bearer ${config.apiKey}")
}
val request = requestBuilder
.post(requestBody.toString().toRequestBody("application/json".toMediaType()))
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
val responseBody = response.body?.string()
val responseJson = JSONObject(responseBody ?: "")
val choices = responseJson.getJSONArray("choices")
if (choices.length() > 0) {
choices.getJSONObject(0).getJSONObject("message").getString("content")
} else {
"错误: API返回空结果"
}
} else {
"错误: ${response.code} ${response.message}"
}
}
}
- Step 5: 添加3个复制按钮的点击事件
btnCopyResult1.setOnClickListener { copyToClipboard(outputTextView1.text.toString()) }
btnCopyResult2.setOnClickListener { copyToClipboard(outputTextView2.text.toString()) }
btnCopyResult3.setOnClickListener { copyToClipboard(outputTextView3.text.toString()) }
- Step 6: 提交
git add flomo-ai/app/src/main/java/com/example/flomo_ai/MainActivity.kt
git commit -m "feat: parallel LLM calls with 3 result cards"
自检清单
- 所有3个模型配置都能独立测试
- 主页面显示3个水平排列的结果卡片
- 发送时并行调用所有已配置的模型
- 每个结果卡片独立显示各自的结果
- 每个结果卡片有独立的复制按钮
- 配置保存后重启应用能正确加载