如果你最近关注 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 拥有更强大的能力:

  1. 自主决策:Agent 根据当前情境和目标,自主决定下一步做什么,而非执行预设指令。
  2. 工具调用:Agent 可以调用外部工具(搜索、计算、API 等)来扩展能力,而不局限于代码内置的逻辑。
  3. 持续学习:通过记忆系统,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 可能会规划为:

  1. 分析需求背景
  2. 搜索相关技术文档
  3. 设计系统架构
  4. 撰写方案初稿
  5. 自我审查并优化

实现一个简单的 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 框架。