refactor(llmx): 重构项目配置和网络请求处理

- 更新 Nacos 配置中的 IP地址
- 修改 Spring Boot项目配置,使用更具体的 USE_SPRING_BOOT_WEB 标志
- 重构 DashScopeAdapter 中的网络请求和响应处理逻辑,提高可读性和维护性
- 在 ChatController 和 LLMProvider 中添加 NDJSON 媒体类型支持
This commit is contained in:
gewuyou 2025-05-07 22:59:39 +08:00
parent 73eeaa19c1
commit 6bf046d6e0
9 changed files with 83 additions and 50 deletions

View File

@ -19,7 +19,7 @@ configurations.implementation {
allprojects {
// 设置全局属性
ext {
set(ProjectFlags.USE_SPRING_BOOT, false)
set(ProjectFlags.USE_SPRING_BOOT_WEB, false)
set(ProjectFlags.USE_LLM_CORE_SPI, false)
set(ProjectFlags.USE_SPRING_CLOUD_BOM, false)
set(ProjectFlags.IS_ROOT_MODULE, false)
@ -67,7 +67,8 @@ allprojects {
subprojects {
afterEvaluate {
// springbootWeb
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT)) {
// springbootWeb
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT_WEB)) {
apply {
plugin(libs.plugins.spring.dependency.management.get().pluginId)
plugin(libs.plugins.spring.boot.get().pluginId)

View File

@ -1,5 +1,5 @@
object ProjectFlags {
const val USE_SPRING_BOOT = "useSpringBoot"
const val USE_SPRING_BOOT_WEB = "useSpringBootWeb"
const val USE_SPRING_CLOUD_BOM = "useSpringCloudBom"
const val USE_LLM_CORE_SPI = "useLLMCoreSPI"
const val IS_ROOT_MODULE = "isRootModule"

View File

@ -1,6 +1,6 @@
extra {
// 开启springboot
setProperty(ProjectFlags.USE_SPRING_BOOT, true)
setProperty(ProjectFlags.USE_SPRING_BOOT_WEB, true)
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
}
dependencies {

View File

@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.Flow
import org.jcnc.llmx.core.service.service.impl.LLMServiceImpl
import org.jcnc.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
@ -33,7 +34,7 @@ class ChatController(
* @param request 聊天请求对象包含了发起聊天所需的各种参数和用户信息
* @return 返回一个Flow流流中依次提供了聊天响应的部分数据允许异步处理和逐步消费响应内容
*/
@PostMapping("/stream")
@PostMapping("/stream", produces = [MediaType.APPLICATION_NDJSON_VALUE])
fun chat(@RequestBody request: ChatRequest): Flow<ChatResponsePart> {
return llmServiceImpl.chat(request)
}

View File

@ -4,7 +4,7 @@ spring:
username: nacos
password: L4s6f9y3
server-addr: 49.235.96.75:8848
ip: 192.168.1.6
ip: 192.168.1.100
discovery:
server-addr: ${spring.cloud.nacos.server-addr}
username: ${spring.cloud.nacos.username}

View File

@ -5,6 +5,7 @@ import org.jcnc.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.core.spi.entities.request.EmbeddingRequest
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.jcnc.llmx.core.spi.entities.response.EmbeddingResponse
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
@ -29,7 +30,7 @@ interface LLMProvider {
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
@PostMapping("/chat")
@PostMapping("/chat", produces = [MediaType.APPLICATION_NDJSON_VALUE])
fun chat(request: ChatRequest): Flow<ChatResponsePart>
/**

View File

@ -1,6 +1,6 @@
// 开启springboot
extra[ProjectFlags.USE_SPRING_BOOT] = true
setProperty(ProjectFlags.USE_SPRING_BOOT_WEB, true)
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
dependencies {
// Nacos 服务发现和配置

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.gewuyou.forgeboot.core.extension.log
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
import okhttp3.Headers.Companion.toHeaders
import okhttp3.MediaType.Companion.toMediaType
@ -54,52 +55,81 @@ class DashScopeAdapter(
val requestJson = objectMapper.writeValueAsString(requestBody)
log.info("📤 请求参数: {}", requestJson)
val request = Request.Builder()
.url(url)
.headers(headers.toHeaders())
.post(requestJson.toRequestBody("application/json".toMediaType()))
.build()
val call = okHttpClient.newCall(request)
val response = call.execute()
val request = buildRequest(url, headers, requestJson)
okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw RuntimeException("❌ DashScope 请求失败: HTTP ${response.code}")
}
val responseBody = response.body ?: throw RuntimeException("❌ DashScope 响应体为空")
val bufferedReader: BufferedReader = responseBody.charStream().buffered()
responseBody.charStream().buffered().use { reader ->
val allContent = StringBuilder()
try {
processResponse(dispatcher, reader, extractContent, allContent)
log.info("📦 完整内容: {}", allContent)
} catch (e: Exception) {
log.error("🚨 读取 DashScope 响应流失败: {}", e.message, e)
throw e
}
}
}
}
/**
* 处理响应流
*
* 本函数读取HTTP响应中的数据行解析并提取内容
* 它在循环中读取每一行并使用提供的函数提取内容
*
* @param dispatcher 协程调度器
* @param reader 响应体的BufferedReader
* @param extractContent 一个函数用于从JSON响应中提取内容
* @param allContent 保存所有提取内容的StringBuilder
*/
private suspend fun FlowCollector<ChatResponsePart>.processResponse(
dispatcher: CoroutineDispatcher,
reader: BufferedReader,
extractContent: (String) -> ChatResponsePart,
allContent: StringBuilder
) {
while (currentCoroutineContext().isActive) {
val line = withContext(dispatcher) {
bufferedReader.readLine()
reader.readLine()
} ?: break
log.info("📥 接收到行: {}", line)
log.debug("📥 接收到行: {}", line)
if (line.startsWith("data:")) {
val jsonPart = line.removePrefix("data:").trim()
try {
val part = extractContent(jsonPart)
allContent.append(part.content)
log.info("✅ 提取内容: {}", part)
log.debug("✅ 提取内容: {}", part)
emit(part)
} catch (e: Exception) {
log.warn("⚠️ 无法解析 JSON: {}", jsonPart, e)
}
}
}
log.info("📦 完整内容: {}", allContent)
} catch (e: Exception) {
log.error("🚨 读取 DashScope 响应流失败: {}", e.message, e)
throw e
} finally {
withContext(dispatcher) {
bufferedReader.close()
}
response.close()
}
/**
* 构建请求
*
* 本函数构建一个OkHttp请求对象用于发送聊天请求
*
* @param url 请求的URL
* @param headers 请求的头部信息
* @param json 请求的主体内容的JSON字符串
* @return 返回构建好的Request对象
*/
private fun buildRequest(url: String, headers: Map<String, String>, json: String): Request {
return Request.Builder()
.url(url)
.headers(headers.toHeaders())
.post(json.toRequestBody("application/json".toMediaType()))
.build()
}
}

View File

@ -4,7 +4,7 @@ spring:
username: nacos
password: L4s6f9y3
server-addr: 49.235.96.75:8848
ip: 192.168.1.6
ip: 192.168.1.100
discovery:
server-addr: ${spring.cloud.nacos.server-addr}
username: ${spring.cloud.nacos.username}