Kotlin/Ktor 实践:利用 MCP 从零打造 AI Agent 服务端指南

前言

最近一直在用NodeJS弄MCP应用,注意到SDK提供了一个Kotlin的版本,尝试后发现居然是基于Ktor的,索性试了下用Kotlin来搭建MCP应用,实践后证明这是一个不错的选择,比如官方Koog也是配了Ktor,Ktor这种小体量web框架就很适合做这种中间层服务来使用。

GitHub的Demo,包含AI-Agent和MCPServer,仅供参考

MCP简介

简单来说就是基于MCP协议对传统的AI-Agent进行赋能,让AI-Agent和工具服务有一个约定的协议进行通讯,通过MCP告诉AI-Agent工具调用端点并提供服务上的工具信息进行注册。在AI对话时让AI优先去选择工具,拿到选择的工具后通过告知的操作端点对MCPServer服务进行调用,然后获取到结果后交给AI一并分析。达到数据不过时,准确性高,不训练AI的前提下定制化AI析数据推理

技术栈

  • AI部署:Ollama部署一个DeepseekR1-1.5B模型(个人电脑有限😅,有条件的建议7B以上)
  • MCPServer: Kotlin,ktor-server,modelcontextprotocol-kotlin-sdk
  • AI-Agent(MCP-Client): Kotlin,ktor-server,ktor-client,modelcontextprotocol-kotlin-sdk,

modelcontextprotocol文档

目标实现

我们本地部署的AI是不具备联网功能所以它无法获取当前时间,我们通过提供时间选择工具达到本地AI也能有当前时间感知的能力

实现MCP Server

MCPServer本身是不具备Web服务能力的,所以我们得依靠Ktor-Server去把他挂载到我们的Web服务上让他成为一个独立可被远端调用的服务

核心依赖

kotlin 复制代码
plugins {
    kotlin("jvm") version "2.2.0"
    id("io.ktor.plugin") version "3.3.3"
}
val mcpVersion = "0.8.1"
implementation("io.modelcontextprotocol:kotlin-sdk:${mcpVersion}")
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-serialization-jackson")
implementation("io.ktor:ktor-server-content-negotiation")
implementation("io.ktor:ktor-server-sse")

1.创建MCPSever的运行示例

kotlin 复制代码
val mcpServer = Server(
    Implementation(
        name = "kotlin mcp server",
        version = "1.0.0"
    ),
    ServerOptions(
        capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
    )
)

2.创建工具

创建运行实例以后我们就可以基于往服务容器中注册工具了

kotlin 复制代码
//写一个扩展函数根据addTool函数的规范创建一个获取时间的工具
fun Server.registerCurrentTimeTool() {
    addTool(
        name = "get_current_time",
        description = """
         获取当前的日期和时间。用户可以使用格式模式(如 'yyyy-MM-dd HH:mm:ss')来指定返回格式。
        """.trimIndent(),
        inputSchema = ToolSchema(
            properties = buildJsonObject {
                putJsonObject("format_pattern") {
                    put("type", "string")
                    put("description", "用于格式化时间的模式,例如 'yyyy-MM-dd HH:mm:ss'。")
                }
            },
        ),

        handler = { request ->
            val userPattern: String? = request.arguments
                ?.get("format_pattern")
                ?.jsonPrimitive
                ?.contentOrNull
            log.info { "请求参数: $userPattern" }
            val formatPattern: String = userPattern ?: run {
                log.warn { " 上游请求时间工具未提供参数,使用默认格式化" }
                "yyyy年MM月dd日 HH:mm:ss (zzz)"
            }
            val formatter = DateTimeFormatter.ofPattern(formatPattern)
                .withZone(ZoneId.systemDefault())
            val currentTime = Instant.now()
            val formattedTime = formatter.format(currentTime)
            return@addTool CallToolResult(
                content = listOf(TextContent(formattedTime))
            )
        }
    )
}

3.创建Transport连接桥梁

MCPServer提供不同的连接使用方式,比如SSE,内部IO调用,所以我们得选用一个连接器来进行连接。由于我们是利用Ktor-server来挂载提供远端调用,所以我们使用SSE的Transport,并且SDK内置的SSETransport是基于Ktor实现的,我们可以直接把Ktor的Call交给SSETransport处理,非常的方便。

创建对应AI-Agent的Transport管理

kotlin 复制代码
//AI-Agent的Transport会话管理
//考虑到可能会有多个AI-Agent进行SSE连接,把他们对应的Transport工具管理归属起来
object SessionManager {
    private val sessions: ConcurrentHashMap<String, SseServerTransport> = ConcurrentHashMap()

    fun addSession(transport: SseServerTransport) {
        sessions[transport.sessionId] = transport
        transport.onClose {
            sessions.remove(transport.sessionId)
        }
    }

    fun getTransport(sessionId: String): SseServerTransport? {
        return sessions[sessionId]
    }
    //可自行扩展会话删除等完善会话管理,示例仅供流程运行参考
}

1. 创建MCPServer的SSE连接点

kotlin 复制代码
//利用Ktor-Server提供AI-Agent连接的SSE请求
val postEndpoint = "post"
sse("mcp/sse") {
    //创建与MCPServer的连接处理器,并且提供SSE请求用户调用端点,和sse的会话提供连接器使用响应
    //这里的调用端点AI-Agent客户端的SDK会自动匹配到mcp/${postEndpoint}下
    val transport = SseServerTransport(postEndpoint, this)
    mcpServer.createSession(transport) //这玩意虽然是挂起的,但是不会一直挂起!!!还是会往下执行
    SessionManager.addSession(transport)
    while (isActive) { //避免sse{}执行完毕后导致的会话结束Transport无法使用会话响应
        delay(100)
    }
}

2. 创建MCPServer的操作端点

kotlin 复制代码
val postEndpoint = "post"
//创建调用端点请求(是POST)
post("sse/${postEndpoint}") {
    //查询AI-Agent的会话Id
    val sessionId = call.queryParameters["sessionId"] ?: run {
        call.respondText("没有sessionId参数,不知道你是哪一个SSE会话", status = HttpStatusCode.BadRequest)
        return@post
    }
     //查询对应的Transport工具
    val transport = SessionManager.getTransport(sessionId)
        ?: run {
            call.respondText("无效会话Id,对应的会话已经关闭了", status = HttpStatusCode.Unauthorized)
            return@post
        }
    try {
        //把会话上下问交给 transport.handlePostMessage进行处理
        //这里面会自动根据上游发来MCP内容约定去进行相关操作然后响应
        //注意: 这里并不会进行响应需要的数据,他只会响应一个接受状态,实际上数据的响应是走的SSE
        transport.handlePostMessage(call)
    } catch (e: Exception) {
        if (!call.response.isCommitted) {
            call.respondText("MCP服务处理出错: ${e.message}", status = HttpStatusCode.InternalServerError)
        }
    }
}

完整的MCPServer路由

kotlin 复制代码
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import io.ktor.server.sse.sse
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import java.util.concurrent.ConcurrentHashMap

/**
 *
 * @author setruth
 * @date 2025/12/13
 * @time 12:21
 */

object SessionManager {
    private val sessions: ConcurrentHashMap<String, SseServerTransport> = ConcurrentHashMap()

    fun addSession(transport: SseServerTransport) {
        sessions[transport.sessionId] = transport
        transport.onClose {
            sessions.remove(transport.sessionId)
        }
    }

    fun getTransport(sessionId: String): SseServerTransport? {
        return sessions[sessionId]
    }
}


fun Route.mcpServer(
    path: String,
    toolConfiguration: Server.() -> Unit,
) {
    val mcpServer = Server(
        Implementation(
            name = "kotlin mcp server",
            version = "1.0.0"
        ),
        ServerOptions(
            capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
        )
    )

    mcpServer.toolConfiguration()

    val postEndpoint = "post"
    sse("$path/sse") {
        val transport = SseServerTransport(postEndpoint, this)
        mcpServer.createSession(transport)
        SessionManager.addSession(transport)
        while (isActive) {
            delay(100)
        }
    }
    post("$path/${postEndpoint}") {
        val sessionId = call.queryParameters["sessionId"] ?: run {
            call.respondText("没有sessionId参数,不知道你是哪一个SSE会话", status = HttpStatusCode.BadRequest)
            return@post
        }
        val transport = SessionManager.getTransport(sessionId)
            ?: run {
                call.respondText("无效会话Id,对应的会话已经关闭了", status = HttpStatusCode.Unauthorized)
                return@post
            }
        try {
            transport.handlePostMessage(call)
        } catch (e: Exception) {
            if (!call.response.isCommitted) {
                call.respondText("MCP服务处理出错: ${e.message}", status = HttpStatusCode.InternalServerError)
            }
        }
    }
}

运行MCPServer

kotlin 复制代码
const val MCP_PORT = 3000

fun main() {
    embeddedServer(Netty, port = MCP_PORT, host = "127.0.0.1", module = Application::module)
        .start(wait = true)
}

fun Application.module() {
    install(ContentNegotiation) {
        jackson()
    }
    install(SSE)
    routing {
        mcpServer("/mcp") { // 配置路由
            registerCurrentTimeTool() //注册获取时间工具工具
        }
    }
}

MCPServer的连接测试

访问127.0.0.1:3000/mcp/sse后我们会获取到McpServer响应过来的endpoint事件,当AI-Agent(MCPClient)连接后就会拿到调用端点的信息也就是我们上面写的post,并且提供了会话Id信息。上游后面POST请求/mcp/post路径的时候就会带上会话Id的参数,我们就能拿到去提供对应的Transport工具去处理调用了。

实现AI-Agent(MCPClient)

与MCPServer交互的是MCPClient,但是一般来说MCPClient都是和AI-Agent处于一个服务中所以这里就直接整合起来了。然后Koog也有Ktor的SDK,利用Ktor服务结合Koog去代理AI就非常流畅了再加上MCPClient来使用。

核心依赖

kotlin 复制代码
plugins {
    kotlin("jvm") version "2.2.0"
    id("io.ktor.plugin") version "3.3.3"
}
val mcpVersion = "0.8.1"
implementation("io.modelcontextprotocol:kotlin-sdk:${mcpVersion}")
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-serialization-jackson")
implementation("io.ktor:ktor-server-content-negotiation")
implementation("io.ktor:ktor-server-sse")
implementation("io.ktor:ktor-server-cors")
implementation("io.ktor:ktor-server-config-yaml")
implementation("io.ktor:ktor-client-core")
implementation("io.ktor:ktor-client-cio")
implementation("ai.koog:koog-ktor:0.5.4")

1. 创建MCPClient

1.1 创建MCPClient

kotlin 复制代码
private val mcpClient: Client = Client(clientInfo = Implementation(name = "kotlin mcp client", version = "1.0.0"))

1.2 创建连接器

我们是使用的SDK去创建MCPServer的Transport连接器,所以Client也是有对应的Transport去进行连接,避免通讯方式的不统一

kotlin 复制代码
//利用Ktor-client去作为发送MCP请求的客户端
val mcpSeverReqClient = HttpClient(CIO) {
    install(SSE)
    install(Logging)
}
//由于MCPServer我们创建的是SSE的连接器,所以这边也用SSE的
val transport = SseClientTransport(
    mcpSeverReqClient,
    serverScriptPath, //MCPSerber的SSE请求地址路径
    5.seconds,
)

1.3 获取连接MCPServer并获取工具信息存储

创建好MCPClient和Transport后我们就可以使用Client去连接Transport,让Transport去代理Client的所有操作去和MCPSevrer进行交互了

kotlin 复制代码
mcpClient.connect(transport) // 挂起-> 获取与MCPServer的SSE连接
val toolsResult = mcpClient.listTools() // 挂起-> 获取工具列表 
tools = toolsResult.tools.map { //处理一下过滤调一些无用的工具元数据避免干扰AI判断
    LLMTool(
        name = it.name,
        description = it.description ?: "",
        parameters = extractSimplifiedProperties(it), //自写的函数不是内部的
    )
}
toolsFormatStr = """ //提供工具列表Prompt提示
    ## IV. 可用工具列表:${mapper.writeValueAsString(tools)}
""".trimIndent() 

1.4 提供工具调用函数

kotlin 复制代码
//当给AIPrompt进行规范后后只需要获取AI的回答去解析工具name和对应参数即可,利用MCPClient就能去调用对应的Tool
//如果需要扩展可以和AI约定更多信息比如带上不同的MCPServer的标签 就能实现调用不同MCPClient去获取对应的工具信息
suspend fun executeTool(name: String, input: Map<String, Any>): String {
    val toolResult = mcpClient.callTool(name, input)
    //这里只是演示Text的数据流响应时的处理,对应我们的get_current_time工具响应类型
    val textContent = toolResult.content.find { it.type == ContentTypes.TEXT }
    if (textContent == null) {
        return "无内容"
    }
    return (textContent as TextContent).text
}

完整MCPClient

kotlin 复制代码
private val log = KotlinLogging.logger { }
// 工具参数
data class LLMToolProperty(
    val name: String,
    val type: String,
    val description: String,
)
// 工具定义
data class LLMTool(
    val name: String,
    val description: String,
    val parameters: List<LLMToolProperty>,
)

/**
 *
 * @author setruth
 * @date 2025/12/13
 * @time 13:31
 */
class MCPClient() : AutoCloseable {
    private val mcpClient: Client = Client(clientInfo = Implementation(name = "kotlin mcp client", version = "1.0.0"))
    private val mcpSeverReqClient = HttpClient(CIO) {
        install(SSE)
        install(Logging)
    }
    private var tools: List<LLMTool> = listOf()
    var toolsFormatStr = ""
    suspend fun connectToServer(serverScriptPath: String) {
        try {
            val transport = SseClientTransport(
                mcpSeverReqClient,
                serverScriptPath,
                5.seconds,
            )
            mcpClient.connect(transport)
            val toolsResult = mcpClient.listTools()
            tools = toolsResult.tools.map {
                val tool= LLMTool(
                    name = it.name,
                    description = it.description ?: "",
                    parameters = extractSimplifiedProperties(it),
                )
                log.info { "工具: $tool" }
                tool
            }
            toolsFormatStr = """
                ## IV. 可用工具列表:${mapper.writeValueAsString(tools)}
            """.trimIndent()
        } catch (e: Exception) {
            log.error(e) { "MCP服务连接失败: ${e.message}" }
        }
    }

    suspend fun executeTool(name: String, input: Map<String, Any>): String {
        val toolResult = mcpClient.callTool(name, input)
        val textContent = toolResult.content.find { it.type == ContentTypes.TEXT }
        if (textContent == null) {
            return "无内容"
        }
        return (textContent as TextContent).text
    }

    override fun close() {
        runBlocking {
            mcpClient.close()
        }
    }

    private fun extractSimplifiedProperties(sdkTool: Tool): List<LLMToolProperty> {
        val toolNode: JsonNode = try {
            mapper.valueToTree(sdkTool)
        } catch (e: Exception) {
            return emptyList()
        }

        val propertiesNode = toolNode
            .get("inputSchema")
            ?.get("properties")

        if (propertiesNode == null || !propertiesNode.isObject) {
            return emptyList()
        }

        val paramsList = mutableListOf<LLMToolProperty>()
        propertiesNode.properties().forEach { (paramName, detailsNode) ->
            val typeContentNode = detailsNode.get("type")
            val type = typeContentNode?.get("content")?.asText() ?: typeContentNode?.asText() ?: "string"

            val descriptionContentNode = detailsNode.get("description")
            val description = descriptionContentNode?.get("content")?.asText() ?: descriptionContentNode?.asText()
            ?: "无描述"

            paramsList.add(
                LLMToolProperty(
                    name = paramName,
                    type = type.trim(),
                    description = description.trim()
                )
            )
        }

        return paramsList
    }
}

2. 实现AI-Agent

2.1 系统Prompt定义

AI是否能选择和选择是否准确,在我们不训练AI的前提下就只能靠堆和调整Prompt提示达到准确效果

kotlin 复制代码
const val SystemPromptBase = """你是一个强大的 AI 助手。你的回复必须严格遵循以下规则,回复内容必须是**纯净**的工具调用 JSON **或** **纯净**的自然语言文本,绝不允许混合。

## I. 工具调用模式:决策与输出 ( STRICT JSON MODE )

1. **触发条件:** 仅当用户的请求内容与**可用工具列表中的任一工具描述**直接相关时,才决定使用工具。
2. **可用性限制:** 你只允许使用当前上下文中提供的工具。**严禁**自行编造、猜测或调用任何不存在的工具或函数。
3. **最终格式(必须):** 如果你决定调用工具,你的回复**必须且只能**是一个完整的、**未经任何包裹**的 JSON 字符串。格式必须严格为:
   {"name": "工具名", "input": {参数名1: 值1, 参数名2: 值2}}
   ⚠️ **参数注意:** `input` 的值必须是一个有效的 **JSON 对象(Map)**,包含工具所需的所有必填参数。
4. **纯净输出(核心):** 工具调用 JSON **前后**及**内部**,**绝不允许**包含以下任何内容:
   * Markdown 标记(包括 ```、**、#)
   * 任何解释、推理、思考过程(包括 <think> 标签)
   * 额外的空行、换行符或多余的空格。
   * 任何除 JSON 结构本身以外的文本。

## II. 纯文本模式:直接回复 ( PLAIN TEXT MODE )

5. **回复条件与意图:** 如果你不需要调用工具,或者用户的请求与任何可用工具均不相关,你**必须作为 AI 助手直接、完整地回答用户的问题或请求**。
6. **格式排除:** 在纯文本回复中,**严禁**包含任何 JSON 格式标记(花括号 {}、方括号 []、双引号 ")或任何可能被误识别为结构化数据的符号。回复必须是自然、有帮助的语言文本。
## III. 最终校验

7. **唯一性:** 最终的输出**只能**是 I 模式下的**纯净 JSON 字符串**,**或**是 II 模式下的**纯净文本**。不允许同时出现或混合。
"""

2.2 创建对话路由

kotlin 复制代码
fun Route.appRoute() {
    runBlocking {
        log.info { "尝试连接 MCP 服务器..." }
        mcpClient.connectToServer("http://127.0.0.1:3000/mcp/sse")
    }
    post("chat") {
        val userInput = call.receive<UserQuery>()
        call.respond(HttpStatusCode.OK, ResData(mcpProcess(userInput.query)))
    }
}

2.3 创建对话流程(核心)

基于MCP使用工具的对话流程
  1. 初始请求与决策:

    • 输入: 接收用户询问系统约束工具描述
    • 动作: 第一次询问 LLM 进行工具选择
  2. 工具选择解析:

    • 结果: 解析 LLM 的回复,判断是直接回复,还是工具调用请求 。
  3. 工具执行

    • 调用: 若 LLM 选择工具,使用 MCPClient 进行远端工具调用。
    • 获取: 阻塞等待并接收工具结果 。
  4. 二次询问与结束

    • 输入: 将工具结果作为对话历史的一部分。
    • 动作: 第二次询问 LLM 进行最终推理生成回复响应给客户端

kotlin 复制代码
suspend fun RoutingContext.mcpProcess(input: String): String {
    // 创建顶层第一次会话,带上工具描述和系统约束规则
    val topPrompt = SystemPromptBase.trimIndent() + "\n\n" + mcpClient.toolsFormatStr
    val aiSelectPrompt = prompt(UUID.randomUUID().toString()) {
        system(topPrompt)
        user(input)
    }
    // 获取工具选择结果
    val messages = askAI(aiSelectPrompt).toText(true, "ai选择工具询问")
    return when (val result = selectTool(messages)) {
        is SelectResult.NoSelect -> { // AI没选择工具直接返回AI回复
            log.info { "AI未选择工具,直接返回: $messages" }
            result.message
        }

        is SelectResult.Select -> { // AI选择工具后进行工具调用
            log.info { "AI已选择工具: ${result.name},参数: ${result.input}" }
            //使用mcpClient进行远端工具调用
            val toolRes = mcpClient.executeTool(result.name, result.input)
            log.info { "工具返回: $toolRes" }
            // 带工具配合历史对话让AI一并分析回复
            val resultPrompt = prompt(aiSelectPrompt) {
                system("使用的工具${result.name}已返回的数据时:${toolRes},基于这个工具给的数据去思考再次回复之前的询问,并且不要带上工具的任何信息")
            }
            askAI(resultPrompt).toText(true, "ai带上工具结果去思考")
        }
    }
}

完整AI-Agent

kotlin 复制代码
data class ResData(val data: String)
data class UserQuery(val query: String)
sealed class SelectResult {
    data class NoSelect(val message: String) : SelectResult()
    data class Select(val name: String, val input: Map<String, Any>) : SelectResult()
}

private val log = KotlinLogging.logger { }


fun Route.appRoute() {
    runBlocking {
        log.info { "尝试连接 MCP 服务器..." }
        mcpClient.connectToServer("http://127.0.0.1:3000/mcp/sse")
    }
    post("chat") {
        val userInput = call.receive<UserQuery>()
        call.respond(HttpStatusCode.OK, ResData(mcpProcess(userInput.query)))
    }
}

suspend fun RoutingContext.mcpProcess(input: String): String {
    // 创建顶层第一次会话,带上工具描述和系统约束规则
    val topPrompt = SystemPromptBase.trimIndent() + "\n\n" + mcpClient.toolsFormatStr
    val aiSelectPrompt = prompt(UUID.randomUUID().toString()) {
        system(topPrompt)
        user(input)
    }
    // 获取工具选择结果
    val messages = askAI(aiSelectPrompt).toText(true, "ai选择工具询问")
    return when (val result = selectTool(messages)) {
        is SelectResult.NoSelect -> { // AI没选择工具直接返回AI回复
            log.info { "AI未选择工具,直接返回: $messages" }
            result.message
        }

        is SelectResult.Select -> { // AI选择工具后进行工具调用
            log.info { "AI已选择工具: ${result.name},参数: ${result.input}" }
            //使用mcpClient进行远端工具调用
            val toolRes = mcpClient.executeTool(result.name, result.input)
            log.info { "工具返回: $toolRes" }
            // 带工具配合历史对话让AI一并分析回复
            val resultPrompt = prompt(aiSelectPrompt) {
                system("使用的工具${result.name}已返回的数据时:${toolRes},基于这个工具给的数据去思考再次回复之前的询问,并且不要带上工具的任何信息")
            }
            askAI(resultPrompt).toText(true, "ai带上工具结果去思考")
        }
    }
}

/**
 * 基于和AI约定的`{"name": "工具名", "input": {参数名1: 值1, 参数名2: 值2}}`规则去解析AI是否选择了工具
 * 
 * @param messages AI响应文本
 */
fun selectTool(messages: String): SelectResult {
    if (!messages.startsWith("{") || !messages.endsWith("}")) {
        return SelectResult.NoSelect(messages)
    }
    try {
        val toolCall = mapper.readValue<SelectResult.Select>(messages)
        if (toolCall.name.isBlank()) {
            return SelectResult.NoSelect(messages)
        }
        return toolCall
    } catch (e: Exception) {
        log.error(e) { "解析工具调用失败,原始信息:${messages}" }
        return SelectResult.NoSelect(messages)
    }
}

/**
 * 将消息响应列表转换为压缩后的文本内容
 *
 * @param showRawThink 是否显示AI原始思考内容,默认为false
 * @param step 步骤标识符,用于日志输出,默认为空字符串
 * @return 清理并压缩后的文本内容
 */
private fun List<Message.Response>.toText(showRawThink: Boolean = false, step: String = ""): String {
    // 合并所有响应内容
    val mergeText = joinToString(separator = "") { it.content }
    if (showRawThink) {
        log.info { "$step AI原始思考:${mergeText}" }
    }

    // 移除<think>标签及其包含的内容
    val thinkRegex = Regex("""<think>[\s\S]*?</think>""")
    val cleanedContent = mergeText.replace(thinkRegex, "")
    // 压缩连续的空白字符为单个空格
    val whitespaceRegex = Regex("""\s+""")
    val compressedContent = cleanedContent.replace(whitespaceRegex, " ")
    return compressedContent.trim()
}

/**
 * 向AI模型发送询问并获取响应结果
 *
 * @param prompt 要发送给AI模型的提示信息
 * @return AI模型返回的消息响应列表
 */
private suspend fun RoutingContext.askAI(prompt: Prompt): List<Message.Response> {
    try {
        //Koog的模型收录并部署很完整,有一些得自己去定义 特别是Ollama中的
        return llm().execute(prompt, DEEPSEEK_R1_1_5B)
    } catch (e: Exception) {
        log.error(e) { "LLM执行失败" }
        throw e
    }
}

创建AI-Agent服务

yaml配置

选择KOOG的KtorSDK以后只需要配置代理方和地址即可,如果有密钥需要写入apikey

Ktor - Koog

yaml 复制代码
koog:
  ollama:
    enable: true
    baseUrl: http://localhost:11434
ktor:
  application:
        modules:
            - com.setruth.mcp.client.MainKt.module
  deployment:
    port: 3001

主程序

kotlin 复制代码
val mapper = jacksonObjectMapper()
val mcpClient = MCPClient()
/**
 *
 * @author setruth
 * @date 2025/12/13
 * @time 13:27
 */
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)


fun Application.module() {
    monitor.subscribe(ApplicationStopping) { application ->
        mcpClient.close()
    }
    install(ContentNegotiation) {
        jackson()
    }
    install(Koog)
    install(SSE)
    install(CORS){
        anyMethod()
        anyHost()
        allowCredentials = true
        allowNonSimpleContentTypes = true
    }
    routing {
        staticResources("/", "static")
        appRoute()
    }
}

AI-Agent运行测试

运行后我们看到获取到了工具,接下来连接我们的localhost:3000/chat去通过AI-Agent和AI对话看是否能正确选择工具

实践结果

当询问AI名字的时候AI没有选择工具直接回复我们了✔️

当我们问时间的时候AI选择了我们的获取时间工具拿到了时间去进行分析回复✔️

自此我们利用MCP服务对AI进行了能力增强,并且是基于我们给的数据来进行回复回答,保证了准确性也避免了幻觉问题

注意事项

AI推理选择工具很大一定程度上得看AI本身,小模型本身推理能力是非常差劲的,很难按照你的规范来完整响应给你,而且对上下文的分析也有限,我拿1.5B当时都调了好一会Prompt,拿7B虽然推理能力好点,但是对于这种长Prompt的约束AI很多时候也比较死脑筋,当我换在线API比如Deepseek后,很完美的达到了预计效果。所以当调Prompt死活效果不对的情况下建议试试换个大点的模型,推荐先拿在线AI接入来进行Prompt调整,再根据小点的模型逐步精细化,并且大模型也能理解更多的工具 而不只是简单的时间获取。

相关推荐
喜熊的Btm8 小时前
探索 Kotlin 的不可变集合库
kotlin·android jetpack
天天扭码8 小时前
深入MCP本质——编写自定义MCP Server并通过Cursor调用
前端·mcp
天天扭码10 小时前
解放双手!使用Cursor+Figma MCP 高效还原响应式设计稿
前端·mcp
小小工匠10 小时前
LLM - MCP Powered Agent_从工具失配到架构重构的实战指南
agent·mcp·千具之痛
Thomas_Cai12 小时前
MCP服务创建指南
人工智能·大模型·agent·智能体·mcp
栗子叶13 小时前
Spring AI MCP Server接入百炼问题排查
spring ai·mcp·百炼·获取工具异常
刘立军14 小时前
程序员应该熟悉的概念(4)MCP能做什么
人工智能·mcp