如果你最近关注 AI 领域,大概率被「Agent」这个词轰炸过。各种框架、各种概念满天飞:ReAct、Function Calling、Tool Use、Planning、Memory……看起来门槛很高。
庖丁解牛:Agent 的核心就是一个 while 循环。
这篇文章我不想用任何 Agent 框架,而是用纯 Kotlin(AI 相关开发生态其实在 Python 和 TypeScript 会更丰富一些,说来惭愧,我的技能点主要在 JVM 生态上)从零写一个能干活的 Agent,把这个循环亲手跑起来,跑通之后也许你可以更轻松地理解 Agent 的工作。
Agent 到底是什么
回溯历史我们知道,AI 的爆发和普及是从一个聊天工具开始的——ChatGPT,它的背后是 LLM(Large Language Model,大语言模型),但单纯的 LLM 只能“说”,不能执行“做”,它的行为模式是你问一句它答一句,没有其他任何功能。
而 AI Agent 的出现,让 LLM 从“顾问”变成了“执行者”——它能操作软件、调用 API、管理流程,真正融入业务场景。通俗来讲,AI Agent 则可以执行更复杂的任务,自主理解你的意图,规划执行步骤,调用合适的工具,最终完成任务。
LLM 负责“想”,你的代码负责“做”,两者交替进行,直到 LLM 认为任务完成。这个模式有个学名叫 Agent Loop(也常被称为 ReAct 模式:Reason + Act)。
这与传统程序的本质区别在于:传统程序的逻辑是 if-else——开发者预先定义所有可能的输入和对应的输出。而 AI Agent 拥有更强大的能力:
- 自主决策:Agent 根据当前情境和目标,自主决定下一步做什么,而非执行预设指令。
- 工具调用:Agent 可以调用外部工具(搜索、计算、API 等)来扩展能力,而不局限于代码内置的逻辑。
- 持续学习:通过记忆系统,Agent 能够从历史交互中积累经验,优化后续决策。
这意味着,掌握 Agent 开发意味着你不再只是构建被动响应的系统,而是创造能够主动完成任务的智能应用。
Agent 的核心组件
简单来说,AI Agent = 大脑(LLM)+ 手脚(Tools)+ 记忆(Memory)+ 规划(Planning)。它不是一个简单的问答机器,而是一个能够自主决策、执行复杂任务的智能体。
现在,有个新的名词——Harness,上面的等式可以简化成 AI Agent = LLM + Harness。Harness 的意思是马具,放到 AI 时代,它指的是智能体驾驭工程系统。
模型是可替换的内核,真正决定 AI Agent 长什么样的是 Harness,所以我们说的 AI Agent 开发,大多数情况下是开发 Harness,除了少数有条件自训练模型的厂商外,大多数应用都是接入第三方模型。
LLM(大脑)
LLM 是 Agent 的“大脑”,负责理解自然语言输入、生成推理计划、做出决策。当你对 Agent 说“帮我总结今天的会议纪要”时,LLM 需要:
- 理解“总结”和“会议纪要”的含义
- 判断需要调用哪些工具来获取信息
- 将工具返回的原始数据组织成结构化的总结
Tools(工具)
工具是 Agent 的“手脚”,让它能够与外部世界交互。常见的工具类型包括:
- 搜索工具:调用搜索引擎获取实时信息
- 计算工具:执行数学运算或数据分析
- API 工具:调用第三方服务(天气、日历、数据库等)
- 代码执行工具:运行代码片段完成特定计算
Memory(记忆)
记忆系统让 Agent 拥有“上下文感知”能力:
- 短期记忆:当前对话的上下文,让 Agent 理解“它”指的是什么。
- 长期记忆:通过向量数据库存储历史交互,让 Agent 记住用户偏好、项目背景等信息。
Planning(规划)
规划能力让 Agent 能够分解复杂任务、制定执行策略,并在执行过程中反思修正。例如,“帮我写一份技术方案”这个任务,Agent 可能会规划为:
- 分析需求背景
- 搜索相关技术文档
- 设计系统架构
- 撰写方案初稿
- 自我审查并优化
实现一个简单的 Agent
准备工作
本篇文章以接入 DeepSeek 作为示例,因此需要先申请一个 API Key,其他供应商的接入方式基本类似。同时因为我仅抽取 DeepSeek 相关的内容来演示,所以代码中可能会出现一些暂时用不上的封装代码,读者可以根据实际情况理解。
项目依赖方面,没有 LangChain,没有任何 Agent 框架。这是刻意的——我们要看清全部细节。
这里仅仅引入一个 JSON 解析库,用于传输数据。为了方便演示我这里直接使用 org.json,当然你也可以换成其他顺手的,比如 kotlinx.serialization 或者 Gson 等。
dependencies {
...
implementation("org.json:json:20240303")
}
Http 请求我就直接基于 HttpClient 简单封装了一下:
object Http {
private val client: HttpClient = HttpClient.newHttpClient()
fun post(
url: String,
body: JSONObject,
headers: (HttpRequest.Builder) -> HttpRequest.Builder
): JSONObject {
val request = headers(HttpRequest.newBuilder().uri(URI.create(url)).header("content-type", "application/json"))
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build()
val response = client.send(request, BodyHandlers.ofString())
val status = response.statusCode()
if (status != 200) throw RuntimeException("API error [$status] $url: ${response.body()}")
return JSONObject(response.body())
}
fun get(url: String): JSONObject {
val request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(10))
.GET()
.build()
val response = client.send(request, BodyHandlers.ofString())
val status = response.statusCode()
if (status != 200) throw RuntimeException("API error [$status] $url: ${response.body()}")
return JSONObject(response.body())
}
}
定义内部消息格式
sealed class Msg {
class System(val content: String) : Msg()
class User(val content: String) : Msg()
data class Assistant(val blocks: List<AssistantBlock>) : Msg()
data class ToolResults(val results: List<ToolResult>) : Msg()
}
sealed class AssistantBlock {
data class Text(val text: String) : AssistantBlock()
data class ToolCall(
val id: String,
val name: String,
val input: JSONObject
) : AssistantBlock()
}
data class ToolResult(
val callId: String,
val content: String
)
定义了 4 种与厂商无关的消息类型,Agent 只操作这些:
System:系统提示,用于设置 Agent 的行为和风格。User:用户输入,包含用户的问题或指令。Assistant:模型生成的回复,包含文本和工具调用。ToolResults:工具调用的结果,用于更新 Agent 的状态。
定义工具序列化格式
如果你经常接触 AI 相关的工具,你一定知道目前行业已经形成了两套 API 格式,分别是 OpenAI Chat Completions 和 Anthropic Messages,即以两家巨头 LLM 提供商规定的格式为规范,几乎所有的模型都提供了这两种接入格式。
简单来说无非是传输数据的结构差异罢了,我们开发 Agent 时,也可以考虑同时兼容这两种格式。
data class Tool(
val name: String, // 工具名称,LLM 用它来标识要调用哪个
val description: String, // 工具描述,LLM 据此决定何时调用
val properties: Map<String, JSONObject>, // 参数定义(JSON Schema)
val required: List<String>, // 必填参数
val execute: (JSONObject) -> String // 实际执行逻辑
) {
private fun buildSchema(propKey: String): JSONObject = JSONObject()
.put("type", "object")
.put(propKey, JSONObject().apply {
properties.forEach { (k, v) -> put(k, v) }
})
.put("required", JSONArray(required))
fun toAnthropicFormat(): JSONObject = JSONObject()
.put("name", name)
.put("description", description)
.put("input_schema", buildSchema("properties"))
fun toOpenAIFormat(): JSONObject = JSONObject()
.put("type", "function")
.put("function", JSONObject()
.put("name", name)
.put("description", description)
.put("parameters", buildSchema("properties"))
)
}
接入 LLM 供应商
先抽象 LLM 供应商接口,OpenAI Chat Completions 和 Anthropic Messages 两种格式都是基于这个接口实现:
interface LLMProvider {
fun chat(
messages: List<Msg>,
tools: List<Tool>,
systemPrompt: String?
): Msg.Assistant
}
由于本文以 DeepSeek 作为示例,所以任意挑选一种格式即可,这里我选择 OpenAI Chat Completions。
尽管如此,为了方便其他模型的接入,再增加一层抽象:
abstract class OpenAICompatibleProvider(
private val apiKey: String,
private val model: String,
private val baseUrl: String
) : LLMProvider {
override fun chat(
messages: List<Msg>,
tools: List<Tool>,
systemPrompt: String?
): Msg.Assistant {
val body = JSONObject()
.put("model", model)
.put("max_tokens", 1024)
body.put("messages", toMessages(messages, systemPrompt))
if (tools.isNotEmpty()) {
body.put("tools", JSONArray().apply {
tools.forEach { put(it.toOpenAIFormat()) }
})
}
val resp = Http.post("$baseUrl/chat/completions", body) {
it.header("Authorization", "Bearer $apiKey")
}
return parseResponse(resp)
}
private fun toMessages(msgs: List<Msg>, systemPrompt: String?): JSONArray {
val arr = JSONArray()
if (systemPrompt != null) {
arr.put(JSONObject().put("role", "system").put("content", systemPrompt))
}
for (msg in msgs) {
when (msg) {
is Msg.System -> { /* handled by systemPrompt param */ }
is Msg.User -> arr.put(JSONObject().put("role", "user").put("content", msg.content))
is Msg.Assistant -> {
val assistantMsg = JSONObject().put("role", "assistant")
val texts = msg.blocks.filterIsInstance<AssistantBlock.Text>().map { it.text }
val calls = msg.blocks.filterIsInstance<AssistantBlock.ToolCall>()
if (texts.isNotEmpty()) {
assistantMsg.put("content", texts.joinToString("\n"))
}
if (calls.isNotEmpty()) {
val tcArray = JSONArray()
for (tc in calls) {
tcArray.put(
JSONObject()
.put("id", tc.id)
.put("type", "function")
.put("function", JSONObject().put("name", tc.name).put("arguments", tc.input.toString()))
)
}
assistantMsg.put("tool_calls", tcArray)
}
arr.put(assistantMsg)
}
is Msg.ToolResults -> {
for (tr in msg.results) {
arr.put(JSONObject().put("role", "tool").put("tool_call_id", tr.callId).put("content", tr.content))
}
}
}
}
return arr
}
private fun parseResponse(resp: JSONObject): Msg.Assistant {
val message = resp.getJSONArray("choices").getJSONObject(0).getJSONObject("message")
val blocks = mutableListOf<AssistantBlock>()
val content = message.optString("content", "")
if (content.isNotBlank()) {
blocks.add(AssistantBlock.Text(content))
}
val toolCalls = message.optJSONArray("tool_calls")
if (toolCalls != null) {
for (i in 0 until toolCalls.length()) {
val tc = toolCalls.getJSONObject(i)
val func = tc.getJSONObject("function")
val args = JSONObject(func.getString("arguments"))
blocks.add(
AssistantBlock.ToolCall(
id = tc.getString("id"),
name = func.getString("name"),
input = args
)
)
}
}
return Msg.Assistant(blocks)
}
}
这样我们在接入任何模型的时候都可以继承这个抽象类:
class OpenAIProvider(
apiKey: String,
model: String = "gpt-6-luna"
) : OpenAICompatibleProvider(
apiKey,
model,
"https://api.openai.com/v1"
)
class DeepSeekProvider(
apiKey: String,
model: String = "deepseek-flash"
) : OpenAICompatibleProvider(
apiKey,
model,
"https://api.deepseek.com/v1"
)
API Key 我们通过外部传入,不写在默认值里,硬编码密钥是严重的安全反模式。
Agent 运行时
这一步我们把上面剖析的 Agent 本质转换成代码,核心循环是这样的:用户输入 → LLM 思考 → 如需用工具则执行 → 结果送回 LLM → 再思考,直到 LLM 返回不含工具调用的纯文本回复。
万事俱备。现在写那个“while 循环”:
class Agent(private val provider: LLMProvider) {
fun run(
systemPrompt: String,
userMessage: String,
tools: List<Tool>,
history: MutableList<Msg>
): String {
history.add(Msg.User(userMessage))
var iteration = 0
while (iteration++ < 10) {
val assistantMsg = provider.chat(
messages = history,
tools = tools,
systemPrompt = systemPrompt
)
history.add(assistantMsg)
val texts = assistantMsg.blocks.filterIsInstance<AssistantBlock.Text>()
val toolCalls = assistantMsg.blocks.filterIsInstance<AssistantBlock.ToolCall>()
if (toolCalls.isNotEmpty()) {
val results = toolCalls.map { tc ->
println(" 🔧 调用工具: ${tc.name}(${tc.input})")
val tool = tools.find { it.name == tc.name }
val result = tool?.execute?.invoke(tc.input) ?: "错误:未知工具 '${tc.name}'"
println(" 📊 结果: $result")
ToolResult(tc.id, result)
}
history.add(Msg.ToolResults(results))
continue
}
return texts.joinToString("\n") { it.text }
}
return "错误:Agent 迭代次数超过上限"
}
}
值得注意的是,这里我们通过一个列表来维护对话的历史,就相当于实现了对话的记忆功能,用户可以连续输入多个问题,Agent 会根据之前的上下文来回答。
先把 Chatbot 跑起来
虽然还没有定义工具,但是我们已经接好了 API,所以可以先把那个最原始的聊天机器人跑起来。
fun main() {
println("""
请选择 LLM 供应商:
1. Anthropic (Claude)
2. OpenAI
3. DeepSeek
""".trimIndent())
val choice: String
while (true) {
print("输入数字 [1/2/3]: ")
val input = readlnOrNull()?.trim()
if (input == "1" || input == "2" || input == "3") {
choice = input
break
}
println("❌ 无效输入,请重新输入 1、2 或 3")
}
val (envKey, providerName) = when (choice) {
"1" -> "ANTHROPIC_API_KEY" to "Anthropic"
"2" -> "OPENAI_API_KEY" to "OpenAI"
"3" -> "DEEPSEEK_API_KEY" to "DeepSeek"
else -> error("unreachable")
}
val apiKey = System.getenv(envKey)
if (apiKey.isNullOrBlank()) {
println("""
❌ 请设置环境变量 $envKey
export $envKey=<your-api-key>
""".trimIndent())
exitProcess(1)
}
val provider: LLMProvider = when (choice) {
"1" -> AnthropicProvider(apiKey = apiKey)
"2" -> OpenAIProvider(apiKey = apiKey)
"3" -> DeepSeekProvider(apiKey = apiKey)
else -> error("unreachable")
}
val agent = Agent(provider)
val tools = createTools()
val systemPrompt = """
你是一个友好的中文助手。你可以使用提供的工具来帮助用户。
- 用中文回复用户,并且每次回复前都增加敬语“聪明的人类”。
""".trimIndent()
val history = mutableListOf<Msg>()
val divider = "=".repeat(50)
println("""
$divider
🤖 AI Agent 已启动 [供应商: $providerName]
$divider
输入 'exit' 退出
输入 'clear' 重置对话
$divider
""".trimIndent())
while (true) {
print("👤 你: ")
val input = readlnOrNull()?.trim() ?: break
if (input.isBlank()) continue
when (input.lowercase()) {
"exit" -> {
println("👋 再见!")
exitProcess(0)
}
"clear" -> {
history.clear()
println("🗑️ 对话已重置。\n")
continue
}
}
try {
val reply = agent.run(
systemPrompt = systemPrompt,
userMessage = input,
tools = tools,
history = history
)
println("🤖 Agent: $reply")
println()
} catch (e: Exception) {
println("❌ 错误: ${e.message}")
println()
}
}
}
fun createTools(): List<Tool> = listOf()
这就是一个简易的终端聊天工具,用户输入指令,模型返回回复,我在输出时增加了两个 Emoji,用来分辨是用户输入还是模型回复。记得要先配置好 LLM 供应商的 API 密钥。
我添加了 Prompt,引导模型在回复前都增加敬语“聪明的人类”。Tools 还未实现,我只给模型提供了空的工具列表。历史对话在 Agent 类中维护,每次迭代时都会更新历史记录。
运行起来是这样的:
请选择 LLM 供应商:
1. Anthropic (Claude)
2. OpenAI
3. DeepSeek
输入数字 [1/2/3]: 3
==================================================
🤖 AI Agent 已启动 [供应商: DeepSeek]
==================================================
输入 'exit' 退出
输入 'clear' 重置对话
==================================================
👤 你: U there?
🤖 Agent: 聪明的人类,我在的!很高兴为您服务,有什么可以帮您的吗?
👤 你: 今天几号
🤖 Agent: 聪明的人类,很抱歉,我无法实时查看今天的日期。建议您查看手机、电脑或日历小工具,那里有最准确的日期哦!如果还有其他问题,我随时可以帮忙~
👤 你: 广州天气怎么样
🤖 Agent: 聪明的人类,很抱歉,我目前没有实时查询天气的功能。建议您打开手机天气应用或搜索“广州天气”,就能看到最新的温度和降雨情况啦!如果还有其他能帮上忙的事,请随时吩咐~
可以看到,模型根据我提供的 Prompt 来回复用户,但后面两个问题就能看出 LLM 回答的边界,那么下一步,我们就来定义相应的工具,给 LLM 调用。
给 Agent 加上“手脚”
Agent 的能力边界取决于它拥有的工具。一个没有工具的 Agent 能理解需求,却无法采取实际行动。
上面 Tool 抽象规定了所有工具必须遵循的契约:每个工具都有唯一的名字、描述(帮助 LLM 理解用途)和执行方法。我们接下来给 AI 添加上面两个工具,即获取时间和查询天气。
fun createTools(): List<Tool> = listOf(
Tool(
name = "get_current_time",
description = "获取当前日期和时间。不需要参数。",
properties = emptyMap(),
required = emptyList(),
execute = {
LocalDateTime.now().toString()
}
),
Tool(
name = "get_weather",
description = "查询指定城市的当前天气(数据来自 wttr.in)。",
properties = mapOf(
"city" to JSONObject()
.put("type", "string")
.put("description", "城市名称,如:Beijing、Shanghai、Tokyo")
),
required = listOf("city"),
execute = {
val city = URLEncoder.encode(it.getString("city"), Charsets.UTF_8)
try {
val current = Http.get("https://wttr.in/$city?format=j1")
.getJSONArray("current_condition")
.getJSONObject(0)
JSONObject()
.put("城市", it.getString("city"))
.put("天气", current.getJSONArray("weatherDesc").getJSONObject(0).getString("value"))
.put("温度", "${current.getString("temp_C")}°C")
.put("体感温度", "${current.getString("FeelsLikeC")}°C")
.put("湿度", "${current.getString("humidity")}%")
.put("风速", "${current.getString("windspeedKmph")} km/h")
.toString()
} catch (e: Exception) {
"错误:查询天气失败 - ${e.message}"
}
}
)
)
无论我们如何执行逻辑,最后只需将结果以 String 格式返回给 LLM 即可,LLM 会根据结果继续生成回复。
比如获取时间就是调用 LocalDateTime.now(),聪明的 LLM 会自己解析;查询天气也是同样,我使用之前介绍过的 wttr.in 提供的 API 来获取天气数据,直接把 JSON 字符串返回给 LLM,LLM 也会自动解析。
接下来看看效果:
请选择 LLM 供应商:
1. Anthropic (Claude)
2. OpenAI
3. DeepSeek
输入数字 [1/2/3]: 3
==================================================
🤖 AI Agent 已启动 [供应商: DeepSeek]
==================================================
输入 'exit' 退出
输入 'clear' 重置对话
==================================================
👤 你: 今天几号
🔧 调用工具: get_current_time({})
📊 结果: 2026-08-03T16:24:07.183085
🤖 Agent: 聪明的人类,今天是 **2026年8月3日**(星期一),现在是下午4点24分左右。请问还有什么可以帮您的吗?
👤 你: 广州天气怎么样
🔧 调用工具: get_weather({"city":"Guangzhou"})
📊 结果: {"天气":"Light rain shower","温度":"25°C","体感温度":"28°C","风速":"14 km/h","湿度":"91%","城市":"Guangzhou"}
🤖 Agent: 聪明的人类,这是广州今天的天气情况:
| 项目 | 详情 |
|------|------|
| 🌤️ **天气** | 小阵雨 |
| 🌡️ **温度** | 25°C |
| 🥵 **体感温度** | 28°C |
| 💨 **风速** | 14 km/h |
| 💧 **湿度** | 91% |
今天广州有小阵雨,湿度较高,体感会比较闷热。出门记得带把伞哦!还有什么可以帮您的吗?
总结
以上代码展示了一个最简 Agent 的完整结构,用一句伪代码概括 Agent:
while (任务没完成) {
行动 = LLM(目标, 历史, 可用工具)
结果 = 执行(行动)
历史 += 结果
}
关键设计原则:
- LLM 作为决策核心:Agent 不硬编码业务逻辑,所有决策委托给 LLM。
- 记忆分层管理:短期记忆维护当前对话上下文,长期记忆存储跨会话信息。
- 循环执行:Agent 持续交互、逐步完成任务,支持多步推理和多次工具调用。
- 关注点分离:LLM 负责“思考”,工具负责”执行“,Agent 负责“调度”。
当然还有一些可以优化的地方,比如当工具数量不断增长时,直接使用一个 List 来维护是不符合工程规范的,此时应当抽象出一个接口,每个工具实现这个接口来提供自己的功能。
进阶
流式输出
文中的 Agent 回复是阻塞式的——要等模型把整段回复生成完才一次性返回,回复越长用户等得越久。而 LLM 实际是逐 token 产出的,服务端完全可以在生成第一个 token 时就推给客户端,不必等全部生成完。这就是流式输出(Streaming):响应变成一条持续不断的事件流,客户端收到一个 token 就展示一个 token,让用户几乎和模型同步看到文字浮现,体验上的差别是质变,心理等待感大幅降低。
主流 LLM API 的流式输出都基于 SSE(Server-Sent Events):请求时加上 stream: true,响应就变成一条条以 data: 为前缀的事件,每条携带一个增量片段,最后以 [DONE] 结束。HTTP 连接保持打开,服务端持续写入,客户端逐行读取、逐行处理。
需要特别注意的是,工具调用也是流式分片返回的。模型不会一次性给出完整的工具名和参数,而是拆成多个碎片陆续到达——先来工具名,再来一小截参数字符串,拼起来才是完整的 JSON。这些碎片单看任何一片都无法解析,更不能拿去执行工具,必须缓冲、逐片拼接,等流结束凑出完整参数后才能调用。因此 Agent 流式要分两类处理:文字增量即收即显,追求低延迟;工具调用增量先缓冲、后执行,必须等参数攒齐。
记忆系统
记忆系统的核心是对话历史管理——存储、维护和裁剪对话上下文,确保 LLM 获取足够历史信息,同时不超出上下文窗口限制。
记忆的价值体现在三个方面:
- 上下文理解:自然语言大量使用代词指代和省略表达,需要依赖对话历史才能正确理解。
- 任务连续性:复杂任务需要多轮对话完成,记忆让 Agent 能逐步推进。
- 个性化体验:记住用户偏好,避免重复询问。
实际项目中,记忆分为两类:
- 短期记忆:当前对话上下文,对话开始时清空
- 长期记忆:跨会话持久化,用向量数据库存储历史摘要
短期记忆实现要点:使用 List 存储历史,系统提示词固定在最前面。如果历史只增不减,任务一长就会撑爆模型的上下文窗口,token 费用也会失控。生产环境应基于 token 数量裁剪,或实现“记忆压缩”——将早期对话总结为摘要保留。工具结果应摘要化处理,只保留关键信息进入历史。
长期记忆实现思路:使用向量数据库(如 Milvus、Pinecone)存储对话摘要的向量表示。写入策略包括显式保存、自动提取、定期摘要;检索时将当前输入与记忆库做相似度匹配,检索结果作为上下文注入。这种“向量检索 + 上下文注入”的范式称为 RAG(Retrieval Augmented Generation,检索增强生成),是主流的记忆实现方案。
多 Agent 协作模式
复杂任务往往需要多个 Agent 协作完成。常见模式包括:
- Supervisor 模式:一个“主管 Agent”负责分配任务,多个“工人 Agent”各司其职。
- Pipeline 模式:多个 Agent 按流水线方式协作,前一个 Agent 的输出是下一个的输入。
- Debate 模式:多个 Agent 对同一问题提出不同观点,通过辩论得出最优解。
安全边界
Agent 会真实地改动你的系统,必须设防:
- 路径校验:拒绝路径逃逸;
- 危险操作确认:删除、覆盖、网络请求等操作前,暂停循环询问用户;
- 沙箱:让工具在容器或受限目录里执行。
框架推荐
从零开始实现完整的 Agent 系统工作量较大,以下框架可以加速开发:
- LangChain4j:Java/Kotlin 生态的 AI 开发框架,提供 LLM 调用、工具集成、记忆管理等能力。
- Spring AI:Spring 官方的 AI 开发框架,与 Spring Boot 无缝集成,适合企业级应用。
- Koog:JetBrains 官方的 Kotlin Agent 框架。
