feat(core): 添加核心服务模块并实现基本功能

- 新增 llmhub-core-service 模块,实现聊天和嵌入功能
- 添加 LLMService 接口及其实现类 LLMServiceImpl
- 实现了与 LLM 提供商的交互接口 LLMProvider- 新增模型路由管理器 ModelRouteManager 和相关配置
- 添加开发和生产环境配置文件
- 更新项目依赖,引入 Spring Cloud 和 Nacos
This commit is contained in:
gewuyou 2025-04-26 19:00:29 +08:00
parent 096d3f11ab
commit 3c9796524f
23 changed files with 444 additions and 19 deletions

View File

@ -10,18 +10,28 @@ group = "org.jcnc"
version = "1.0-SNAPSHOT"
/**
* 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖
* 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖
*/
configurations.implementation {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}
allprojects {
// 设置全局属性
ext{
set(ProjectFlags.USE_SPRING_BOOT,false)
ext {
set(ProjectFlags.USE_SPRING_BOOT, false)
set(ProjectFlags.USE_LLM_CORE_SPI, false)
set(ProjectFlags.USE_SPRING_CLOUD_BOM, false)
set(ProjectFlags.IS_ROOT_MODULE, false)
}
repositories {
mavenLocal()
val host = System.getenv("GEWUYOU_GITEA_HOST")
host?.let {
maven {
url = uri("http://${host}/api/packages/gewuyou/maven")
isAllowInsecureProtocol = true
}
}
maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
}
@ -44,6 +54,16 @@ allprojects {
google()
gradlePluginPortal()
}
afterEvaluate {
if (project.getPropertyByBoolean(ProjectFlags.IS_ROOT_MODULE)) {
/**
* 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖
*/
configurations.implementation {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}
}
}
}
subprojects {
@ -62,13 +82,25 @@ subprojects {
testRuntimeOnly(libs.junitPlatform.launcher)
}
}
// llm-core-spi
if (project.getPropertyByBoolean(ProjectFlags.USE_LLM_CORE_SPI)) {
dependencies {
implementation(project(Modules.Core.SPI))
}
}
// springCloudBom
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_CLOUD_BOM)) {
dependencies {
implementation(libs.springCloudDependencies.bom)
}
}
}
val libs = rootProject.libs
apply {
plugin(libs.plugins.java.get().pluginId)
plugin(libs.plugins.kotlin.jvm.get().pluginId)
}
println(project.name+":"+project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT))
println(project.name + ":" + project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT))
kotlin {
compilerOptions {

View File

@ -0,0 +1,19 @@
/**
* Modules对象用于统一管理项目中的各个模块路径
* 主要作用是提供一个集中定义和访问模块路径的地方以便在项目中保持一致性和可维护性
*
* @since 2025-04-03 09:07:33
* @author gewuyou
*/
object Modules {
/**
* Core对象定义了核心模块的相关路径
* 主要包含核心模块的SPIService Provider Interface路径
*/
object Core{
// llmhub-core-spi模块的路径用于定义核心功能的SPI
const val SPI = ":llmhub-core:llmhub-core-spi"
}
}

View File

@ -1,3 +1,6 @@
object ProjectFlags {
const val USE_SPRING_BOOT = "useSpringBoot"
const val USE_SPRING_CLOUD_BOM = "useSpringCloudBom"
const val USE_LLM_CORE_SPI = "useLLMCoreSPI"
const val IS_ROOT_MODULE = "isRootModule"
}

View File

@ -1,10 +1,12 @@
[versions]
kotlin-version = "2.0.0"
spring-cloud-version = "2024.0.1"
spring-cloud-starter-version = "4.2.1"
spring-boot-version = "3.4.4"
spring-dependency-management-version = "1.1.7"
aliyun-bailian-version = "2.0.0"
spring-cloud-starter-alibaba-nacos-discovery-version = "2023.0.3.2"
forgeBoot-version = "1.0.0"
[plugins]
# 应用 Java 插件,提供基本的 Java 代码编译和构建能力
java = { id = "java" }
@ -23,10 +25,27 @@ spring-dependency-management = { id = "io.spring.dependency-management", version
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-version" }
[libraries]
# bom
springCloudDependencies-bom = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud-version" }
# kotlinx
# 响应式协程库
kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor"}
# 阿里云百炼
aliyun-bailian = { group = "com.aliyun", name = "bailian20231229", version.ref = "aliyun-bailian-version" }
# SrpingCloud
springCloudStarter-alibaba-nacos-discovery = { group = "com.alibaba.cloud", name = "spring-cloud-starter-alibaba-nacos-discovery", version.ref = "spring-cloud-starter-alibaba-nacos-discovery-version" }
springCloudStarter-loadbalancer = { group = "org.springframework.cloud", name = "spring-cloud-starter-loadbalancer" ,version.ref="spring-cloud-starter-version"}
# SpringBootStarter
springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
springBootStarter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
springBootStarter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" }
junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
# forgeBoot
forgeBoot-webmvc-version-springBootStarter = { group = "com.gewuyou.forgeboot", name = "forgeboot-webmvc-version-spring-boot-starter",version.ref="forgeBoot-version" }
forgeBoot-core-extension = { group = "com.gewuyou.forgeboot", name = "forgeboot-core-extension",version.ref="forgeBoot-version" }
[bundles]

View File

@ -1,5 +1,22 @@
// 开启springboot
extra[ProjectFlags.USE_SPRING_BOOT] = true
extra {
// 开启springboot
setProperty(ProjectFlags.USE_SPRING_BOOT, true)
}
dependencies {
val libs = rootProject.libs
// Nacos 服务发现和配置
implementation(libs.springCloudStarter.alibaba.nacos.discovery)
// WebClient 和 Spring Cloud LoadBalancer
implementation(libs.springBootStarter.webflux)
implementation(libs.springCloudStarter.loadbalancer)
implementation(project(Modules.Core.SPI))
// Kotlin Coroutines
implementation(libs.kotlinx.coruntes.reactor)
implementation(libs.forgeBoot.webmvc.version.springBootStarter)
implementation(libs.forgeBoot.core.extension)
}

View File

@ -0,0 +1,31 @@
package org.jcnc.llmhub.core.service.config
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient
/**
*应用程序配置
*
* @since 2025-04-25 20:36:18
* @author gewuyou
*/
@Configuration
@EnableConfigurationProperties(ModelProperties::class)
class AppConfiguration {
/**
* 创建一个配置了负载均衡的WebClient构建器
*
* 该方法通过Spring框架的WebClient.builder()初始化一个WebClient构建器并为其配置负载均衡功能
* 负载均衡功能使得WebClient能够智能地在多个服务实例间分配请求提高系统的可伸缩性和可靠性
*
* @return WebClient.Builder 配置了负载均衡功能的WebClient构建器
*/
@Bean
@LoadBalanced
fun webClientBuilder(): WebClient.Builder {
return WebClient.builder()
}
}

View File

@ -0,0 +1,19 @@
package org.jcnc.llmhub.core.service.config
import org.springframework.boot.context.properties.ConfigurationProperties
/**
*模型属性
*
* @since 2025-04-26 18:01:48
* @author gewuyou
*/
@ConfigurationProperties(prefix = "llmhub.model-route")
class ModelProperties {
/**
* 模型名前缀 -> 服务名映射
* 该映射表存储了模型名前缀与服务名的对应关系用于快速查找模型对应的服务
* openai -> llmhub-impl-openai
*/
var modelServiceMap: Map<String, String> = emptyMap()
}

View File

@ -0,0 +1,21 @@
package org.jcnc.llmhub.core.service.controller
import com.gewuyou.forgeboot.webmvc.version.annotation.ApiVersion
import org.jcnc.llmhub.core.service.service.impl.LLMServiceImpl
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
/**
*聊天控制器
*
* @since 2025-04-26 16:13:26
* @author gewuyou
*/
@ApiVersion
@RestController
@RequestMapping("/chat")
class ChatController(
private val llmServiceImpl: LLMServiceImpl
) {
}

View File

@ -0,0 +1,39 @@
package org.jcnc.llmhub.core.service.manager
import org.jcnc.llmhub.core.service.config.ModelProperties
import org.springframework.cloud.context.config.annotation.RefreshScope
import org.springframework.stereotype.Component
/**
* 模型路由表管理器
*
* 该类负责管理模型名与服务名之间的映射关系根据模型名前缀来确定对应的后端服务
* 主要作用是动态解析和路由模型请求到正确的服务实例
*
* @since 2025-04-26 17:53:06
* @author gewuyou
*/
@Component
@RefreshScope // 支持Nacos热刷新使得配置更新时无需重启应用即可生效
class ModelRouteManager(
private val modelProperties: ModelProperties
) {
/**
* 根据模型名查找对应服务
*
* 该方法通过遍历模型服务映射表找到与模型名前缀匹配的服务名并返回
* 如果没有找到匹配项则抛出异常表示不支持该模型类型
*
* @param model 模型名用于查找对应服务
* @return 匹配的后端服务名
* @throws IllegalArgumentException 如果模型名不匹配任何已知前缀抛出此异常
*/
fun resolveServiceName(model: String): String {
for ((prefix, serviceName) in modelProperties.modelServiceMap) {
if (model.startsWith(prefix)) {
return serviceName
}
}
throw IllegalArgumentException("Unsupported model type: $model")
}
}

View File

@ -0,0 +1,26 @@
package org.jcnc.llmhub.core.service.service
import kotlinx.coroutines.flow.Flow
import org.jcnc.llmhub.core.spi.entities.request.ChatRequest
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
import org.springframework.web.bind.annotation.PostMapping
/**
*LLM服务
*
* @since 2025-04-26 17:38:18
* @author gewuyou
*/
fun interface LLMService {
/**
* 初始化与聊天服务的连接以处理聊天请求
*
* 此函数接收一个聊天请求对象并返回一个Flow流用于接收聊天响应的部分数据
* 它主要用于建立聊天通信的通道而不是发送具体的消息
*
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
@PostMapping("/chat")
fun chat(request: ChatRequest): Flow<ChatResponsePart>
}

View File

@ -0,0 +1,42 @@
package org.jcnc.llmhub.core.service.service.impl
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.asFlow
import org.jcnc.llmhub.core.service.manager.ModelRouteManager
import org.jcnc.llmhub.core.service.service.LLMService
import org.jcnc.llmhub.core.spi.entities.request.ChatRequest
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
/**
*LLM服务实现
*
* @since 2025-04-25 20:38:13
* @author gewuyou
*/
@Service
class LLMServiceImpl(
private val modelRouteManager: ModelRouteManager,
private val webClientBuilder: WebClient.Builder
) : LLMService {
/**
* 初始化与聊天服务的连接以处理聊天请求
*
* 此函数接收一个聊天请求对象并返回一个Flow流用于接收聊天响应的部分数据
* 它主要用于建立聊天通信的通道而不是发送具体的消息
*
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
override fun chat(request: ChatRequest): Flow<ChatResponsePart> {
val serviceName = modelRouteManager.resolveServiceName(request.model)
val webClient = webClientBuilder.build()
return webClient.post()
.uri("http://$serviceName/provider/chat/stream")
.bodyValue(request)
.retrieve()
.bodyToFlux(ChatResponsePart::class.java)
.asFlow()
}
}

View File

@ -0,0 +1,13 @@
server:
port: 8081
spring:
cloud:
nacos:
discovery:
server-addr: 49.235.96.75:8848 # Nacos 服务地址
llmhub:
model-route:
modelServiceMap:
openai: llmhub-impl-openai
anotherModel: llmhub-impl-another

View File

@ -0,0 +1,7 @@
server:
port: 9002
spring:
cloud:
nacos:
discovery:
server-addr: 49.235.96.75:9001

View File

@ -1,3 +1,5 @@
spring:
application:
name: llmhub-core-service
name: llmhub-core-service
profiles:
active: dev

View File

@ -1,4 +1,10 @@
apply {
plugin(libs.plugins.spring.dependency.management.get().pluginId)
plugin(libs.plugins.spring.boot.get().pluginId)
plugin(libs.plugins.kotlin.plugin.spring.get().pluginId)
}
dependencies {
val libs = rootProject.libs
compileOnly(libs.kotlinx.coruntes.reactor)
compileOnly(libs.springBootStarter.web)
}

View File

@ -0,0 +1,17 @@
package org.jcnc.llmhub.core.spi.entities.request
/**
* ChatRequest数据类用于封装聊天请求的参数
* @since 2025-04-25 17:07:02
* @author gewuyou
* @param prompt 用户的聊天提示或消息是聊天请求的主要输入
* @param model 使用的聊天模型名称决定了解析和响应的方式
* @param options 可选的额外参数集合用于定制聊天请求的行为和输出
* 可以包括如最大回复长度温度随机性
*/
data class ChatRequest(
val prompt: String,
val model: String,
val options: Map<String, Any>? = null
)

View File

@ -0,0 +1,18 @@
package org.jcnc.llmhub.core.spi.entities.request
/**
* 嵌入请求类
*
* 该类用于定义文本嵌入的请求数据结构包含需要进行嵌入处理的输入文本列表
* 和用于处理嵌入的模型标识符
*
* @param input 输入文本列表每个文本作为一个列表元素
* @param model 指定的嵌入模型标识符用于确定使用哪种模型进行嵌入处理
*
* @since 2025-04-25 17:08:13
* @author gewuyou
*/
data class EmbeddingRequest(
val input: List<String>,
val model: String
)

View File

@ -0,0 +1,38 @@
package org.jcnc.llmhub.core.spi.entities.response
/**
* 聊天响应类
*
* 该类用于封装聊天机器人的响应内容包括聊天内容和使用情况
* 主要用途是提供一个结构化的方式来处理和展示聊天机器人的回复信息
*
* @param content 聊天内容字符串表示机器人的回复
* @param other 可选参数表示其他信息例如提示词完成词等
* @param done 可选参数表示聊天是否完成默认为false
* @param usage 可选参数表示聊天的使用情况包括使用的token数量等信息
* @since 2025-04-25 17:07:40
* @author gewuyou
*/
data class ChatResponsePart(
val content: String,
val other: String? = null,
val done: Boolean = false,
val usage: Usage? = null
)
/**
* 使用情况类
*
* 该类用于详细记录聊天过程中token的使用情况包括提示词完成词和总词数
* 主要用途是提供详细的统计信息以便用户了解token的消耗情况
*
* @param promptTokens 提示词使用的token数量
* @param completionTokens 完成词使用的token数量
* @param totalTokens 总共使用的token数量
*/
data class Usage(
val promptTokens: Int,
val completionTokens: Int,
val totalTokens: Int
)

View File

@ -0,0 +1,18 @@
package org.jcnc.llmhub.core.spi.entities.response
/**
* 嵌入响应类
*
* 该类用于表示嵌入向量的响应结果包括向量数据及其维度信息
* 主要用于自然语言处理图像识别等领域where向量表示法被广泛应用
*
* @param vectors 一个包含多个浮点数列表的列表每个浮点数列表代表一个嵌入向量
* @param dimensions 一个整数表示嵌入向量的维度
*
* @since 2025-04-25 17:10:50
* @author gewuyou
*/
data class EmbeddingResponse(
val vectors: List<List<Float>>,
val dimensions: Int
)

View File

@ -0,0 +1,43 @@
package org.jcnc.llmhub.core.spi.provider
import kotlinx.coroutines.flow.Flow
import org.jcnc.llmhub.core.spi.entities.request.ChatRequest
import org.jcnc.llmhub.core.spi.entities.request.EmbeddingRequest
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
import org.jcnc.llmhub.core.spi.entities.response.EmbeddingResponse
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
/**
* LLM 提供商接口
* 该接口定义了与LLMLarge Language Model提供商交互的标准方法
* 主要提供了聊天和嵌入两种功能
*
* @since 2025-04-25 16:13:00
* @author gewuyou
*/
@RequestMapping("/provider/chat")
interface LLMProvider {
/**
* 初始化与聊天服务的连接以处理聊天请求
*
* 此函数接收一个聊天请求对象并返回一个Flow流用于接收聊天响应的部分数据
* 它主要用于建立聊天通信的通道而不是发送具体的消息
*
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
@PostMapping("/chat")
fun chat(request: ChatRequest): Flow<ChatResponsePart>
/**
* 嵌入功能方法
* 该方法允许用户发送嵌入请求以获取LLM生成的嵌入向量
*
* @param request 嵌入请求对象包含需要进行嵌入处理的数据
* @return EmbeddingResponse 嵌入响应对象包含生成的嵌入向量信息
*/
@PostMapping("/embedding")
fun embedding(request: EmbeddingRequest): EmbeddingResponse
}

View File

@ -1,10 +1,5 @@
// 开启springboot
extra[ProjectFlags.IS_ROOT_MODULE] = true
dependencies {
}
/**
* 由于 Kotlin 插件被引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖
*/
configurations.implementation {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}

View File

@ -2,6 +2,6 @@
// 开启springboot
extra[ProjectFlags.USE_SPRING_BOOT] = true
dependencies {
implementation(project(Modules.Core.SPI))
}