手写 Mini Agent 框架,彻底搞懂 LLM 底层
看了一堆 Spring AI、LangChain4j 的文章,还是云里雾里?
那就别看了,跟我一起从零写一个,300 行 Java 代码搞定 Agent 的所有核心机制。
写在前面
兄弟们,最近后台问 Agent 的 Java 开发者是真的多。
什么"Spring AI 怎么用"、"LangChain4j 的 Tools 怎么注册"、"为什么我的 Agent 老是死循环"……说实话,这类问题十有八九都是没搞懂底层导致的。
框架封装得太好,反而成了黑盒。Prompt 怎么拼的、Tool Calling 怎么走的、上下文怎么管理的,全都被一行 chatClient.call() 给遮起来了。出了问题不知道从哪查,性能不行不知道往哪优化。
更糟糕的是——AI/Agent 相关的中文教程几乎全是 Python,Java 党想学点底层原理,资料少得可怜。
所以这篇文章我准备反着来——不教你怎么用 Spring AI,而是带你用纯 Java + OkHttp + Jackson 从零撸一个 Mini Agent。代码 300 行左右,但是 Agent 涉及的所有核心概念:
-
LLM 调用链路(HTTP + JSON 全裸写)
-
Tool Calling 机制
-
ReAct 思考-行动循环
-
上下文与记忆管理
-
多轮工具调用与终止判断
全部覆盖,全部裸写,全部能跑。
读完这篇,你再去看 Spring AI 的源码、LangChain4j 的实现、各种 Agent 论文,会有一种"原来就这?"的通透感。
废话不多说,开整。
一、Agent 的本质:一个 while 循环
很多人把 Agent 想得很玄。什么"自主决策"、"具身智能"、"通用人工智能的雏形"……
醒醒,回到工程视角,Agent 的本质就一句话:
一个带工具的 LLM,套在一个 while 循环里,自己决定什么时候停。
就这么简单。Java 伪代码长这样:
public String agent(String userQuery) {
List<Message> messages = new ArrayList<>();
messages.add(Message.user(userQuery));
while (true) {
Response response = llm(messages, availableTools);
if (response.isFinalAnswer()) {
return response.getContent();
}
// 否则就是要调工具
ToolResult result = executeTool(response.getToolCall());
messages.add(response.toMessage());
messages.add(Message.toolResult(result));
}
}
看明白了吗?所谓 Agent,就是把"要不要继续"的决策权交给了 LLM 自己。
-
LLM 觉得信息够了 → 输出最终答案 → 跳出循环
-
LLM 觉得还缺东西 → 调一个工具 → 把结果塞回上下文 → 再问一遍
整个过程的核心,就是这个 while 循环。
下面我们就一步步把这个循环实现得越来越完整。
二、第一步:搞清楚 LLM 的调用链路
在写 Agent 之前,先把最基础的"调一次 LLM"搞明白。这一步不搞透,后面全是糊涂账。
2.1 一次 LLM 调用到底发生了什么?
你以为你调的是这样:
用户输入 → LLM → 输出
实际上,是这样:
用户输入
↓
拼装成 messages 数组(含 system / user / assistant 历史)
↓
序列化成 HTTP 请求体(JSON)
↓
发到模型服务端(比如 api.anthropic.com)
↓
模型推理,流式返回 token
↓
客户端聚合 token,反序列化成结构化对象
↓
返回给你
中间任何一环出问题,你的 Agent 都会"莫名其妙"地翻车。所以我们先把最底层的 HTTP 调用裸写出来。
2.2 项目依赖
pom.xml 加这么几个就够了:
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
我这里用 Claude API 举例,OpenAI / 通义 / DeepSeek 都是一个套路,改个 URL 和 header 字段就行。
2.3 最基础的 LLM 调用封装
public class LLMClient {
privatestaticfinal String API_URL = "https://api.anthropic.com/v1/messages";
privatestaticfinal String API_KEY = System.getenv("ANTHROPIC_API_KEY");
privatestaticfinal OkHttpClient http = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build();
privatestaticfinal ObjectMapper mapper = new ObjectMapper();
public static JsonNode call(List<Map<String, Object>> messages,
String system,
List<Map<String, Object>> tools) throws IOException {
Map<String, Object> body = new LinkedHashMap<>();
body.put("model", "claude-sonnet-4-5");
body.put("max_tokens", 4096);
body.put("messages", messages);
if (system != null && !system.isBlank()) body.put("system", system);
if (tools != null && !tools.isEmpty()) body.put("tools", tools);
Request req = new Request.Builder()
.url(API_URL)
.header("x-api-key", API_KEY)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.post(RequestBody.create(
mapper.writeValueAsString(body),
MediaType.get("application/json")))
.build();
try (Response resp = http.newCall(req).execute()) {
if (!resp.isSuccessful()) {
thrownew IOException("LLM call failed: " + resp.code() + " " + resp.body().string());
}
return mapper.readTree(resp.body().string());
}
}
}
Java 版的好处就在这——你能清楚看到 HTTP 请求体长什么样、响应 JSON 怎么解析。Python 的 SDK 把这些全藏起来了,反而看不见底层。
2.4 messages 数组:Agent 的"记忆载体"
记住一个核心观点:
LLM 是无状态的。所谓"记忆",本质上就是把历史对话全部塞回 messages 数组里。
每一轮对话,messages 长这样(用 Java 的 Map 表示):
List.of(
Map.of("role", "user", "content", "帮我查下北京天气"),
Map.of("role", "assistant", "content", "好的,我调用一下天气工具..."),
Map.of("role", "user", "content", "工具返回结果:晴,25度"),
Map.of("role", "assistant", "content", "北京今天晴,25度,挺舒服的")
);
Agent 的"上下文"、"记忆"、"多轮对话",全部都是在这个列表上做文章。 这个理解很重要,后面会反复用到。
三、第二步:实现工具调用(Tool Calling)
Agent 的"手",就是工具。光会聊天的不叫 Agent,那叫聊天机器人。
3.1 工具的定义:JSON Schema
现代 LLM 的工具调用都是基于 JSON Schema 的。我们定义一个工具,需要告诉模型:
-
工具叫什么(name)
-
干啥的(description)
-
要传什么参数(input_schema)
举个例子,定义一个查天气的工具:
Map<String, Object> getWeatherTool = Map.of(
"name", "get_weather",
"description", "查询指定城市的当前天气",
"input_schema", Map.of(
"type", "object",
"properties", Map.of(
"city", Map.of(
"type", "string",
"description", "城市名,比如:北京、上海"
)
),
"required", List.of("city")
)
);
划重点:description 写得越好,模型用得越准。这是 Agent 调优的重灾区,很多人 Agent 不工作,根因就是工具描述写得跟天书一样。
3.2 工具的执行:函数式接口
工具的"声明"是给模型看的,工具的"实现"是本地方法。Java 没有 Python 那种灵活的字典,我们用 Function<Map, String> 来抽象:
public class ToolRegistry {
@FunctionalInterface
publicinterface Tool {
String execute(Map<String, Object> args) throws Exception;
}
privatestaticfinal Map<String, Tool> REGISTRY = new HashMap<>();
public static void register(String name, Tool tool) {
REGISTRY.put(name, tool);
}
public static String execute(String name, Map<String, Object> args) {
Tool tool = REGISTRY.get(name);
if (tool == null) return"Error: 未知工具 " + name;
try {
return tool.execute(args);
} catch (Exception e) {
// 关键:把异常作为结果返回,不要抛出去
return"Error: 工具执行失败 - " + e.getMessage();
}
}
}
注册一个具体工具:
ToolRegistry.register("get_weather", args -> {
String city = (String) args.get("city");
// 实际项目里这里会调真实的天气 API
return city + " 今天晴,气温 25 度,微风";
});
注意我特意捕获了异常,把错误信息作为工具结果返回给模型。这样模型可以根据错误信息自己纠错重试,这是 Agent 鲁棒性的关键。抛异常 crash 整个 Agent 是新手最容易犯的错。
3.3 整条链路打通
把上面两步串起来,一次完整的工具调用是这样:
用户问题
↓
LLM(带工具声明)
↓
返回 tool_use 结构(说要调用 get_weather,参数 city=北京)
↓
本地执行 get_weather("北京")
↓
拿到结果 "北京 今天晴..."
↓
把结果作为 tool_result 塞回 messages
↓
再次调用 LLM
↓
LLM 输出最终答案
跑一下,你已经手写出一个单步工具调用了。但这还不够——真实场景里,Agent 经常要调好几次工具才能完成任务。比如"帮我对比北京和上海的天气",要调两次。
这就需要下一步——ReAct 循环。
四、第三步:实现 ReAct 循环
ReAct 这个词来自一篇论文,全称 Reasoning + Acting,思考 + 行动。
听起来很学术,翻译成人话就是:让模型自己决定调几次工具、什么时候停。
4.1 核心思路:把单步循环变成多步
把上一节的代码,扔进一个 for 里:
public String reactAgent(String userQuery, int maxIterations) throws IOException {
List<Map<String, Object>> messages = new ArrayList<>();
messages.add(Map.of("role", "user", "content", userQuery));
for (int i = 0; i < maxIterations; i++) {
JsonNode response = LLMClient.call(messages, systemPrompt, toolSchemas);
String stopReason = response.get("stop_reason").asText();
// 终止条件:模型说我说完了
if ("end_turn".equals(stopReason)) {
return extractText(response);
}
// 否则就是要调工具
if ("tool_use".equals(stopReason)) {
// 把模型的响应塞回 messages
messages.add(Map.of(
"role", "assistant",
"content", mapper.convertValue(response.get("content"), List.class)
));
// 一次响应里可能有多个 tool_use(并行调用),全部执行
List<Map<String, Object>> toolResults = new ArrayList<>();
for (JsonNode block : response.get("content")) {
if ("tool_use".equals(block.get("type").asText())) {
String toolName = block.get("name").asText();
Map<String, Object> args = mapper.convertValue(
block.get("input"), Map.class);
String result = ToolRegistry.execute(toolName, args);
toolResults.add(Map.of(
"type", "tool_result",
"tool_use_id", block.get("id").asText(),
"content", result
));
}
}
messages.add(Map.of("role", "user", "content", toolResults));
}
}
return"⚠️ Agent 达到最大迭代次数,强制退出";
}
就这么简单?对,就这么简单。
LangChain4j 那一坨 ReAct Agent 的代码,扒掉所有抽象,核心逻辑就这几十行。
4.2 几个关键设计点
① maxIterations:必须有!
不加这个,模型如果陷入死循环(一直调工具不输出最终答案),你的程序就废了。这种情况实际上非常常见,尤其是工具描述写得不好的时候。我见过一个团队,没加这个保护,被一个死循环刷爆了 API 账单,单日 8000 块。
② 一次响应可能有多个 tool_use
现代模型(Claude、GPT-4)支持并行工具调用——一次返回多个工具调用请求。处理时要全部执行完,再把所有结果一起塞回去。Java 这里还可以用 CompletableFuture 并行执行工具,性能直接起飞,留给你们自己玩。
③ stop_reason 是终止判断的依据
不要去猜模型的输出内容来判断"它是不是说完了"。响应 JSON 里已经给了 stop_reason 字段:
-
"end_turn":模型自然结束,输出最终答案 -
"tool_use":模型要调工具 -
"max_tokens":超出 token 限制(出问题了)
五、第四步:上下文管理与记忆
Agent 跑一会儿你就会发现新问题:messages 列表越来越长。
每一轮 LLM 调用都把全部历史发过去,token 成本爆炸不说,模型还会被无关历史干扰,效果反而下降。
这就是为什么所有正经的 Agent 框架都要做 Context Management(上下文管理)。
5.1 三种常见的策略
策略一:滑动窗口
只保留最近 N 轮对话:
public static List<Map<String, Object>> trimMessages(
List<Map<String, Object>> messages, int maxTurns) {
if (messages.size() <= maxTurns * 2) return messages;
List<Map<String, Object>> trimmed = new ArrayList<>();
trimmed.add(messages.get(0)); // 保留第一条(最初的用户问题)
trimmed.addAll(messages.subList(messages.size() - (maxTurns * 2 - 1), messages.size()));
return trimmed;
}
简单粗暴,适合短对话场景。
策略二:摘要压缩
当 messages 超过某个阈值,调用 LLM 把历史摘要成一段话:
public List<Map<String, Object>> summarizeHistory(List<Map<String, Object>> messages) {
String prompt = "把下面的对话历史压缩成一段简短摘要,保留关键信息:\n" + messages;
JsonNode resp = LLMClient.call(
List.of(Map.of("role", "user", "content", prompt)), "", null);
String summary = resp.get("content").get(0).get("text").asText();
return List.of(Map.of("role", "user", "content", "[历史摘要]:" + summary));
}
适合长对话、复杂任务。注意摘要本身也是要花钱的,不要太频繁。
策略三:结构化记忆(Memory + RAG)
把"历史对话"和"长期记忆"分开。长期记忆存到向量库里(PgVector / Milvus / Redis Stack 都行),按需检索。这块展开能再写一篇。
核心思想:上下文不是越多越好,而是越相关越好。
5.2 工具结果的精简
还有个容易被忽略的点:工具返回的结果也要精简。
比如调一个搜索工具,原始返回可能是几万字的网页内容。直接塞回 messages 是噩梦。最佳实践:
-
工具内部就做好截断(比如只返回前 2000 字符)
-
或者再调一次 LLM,把工具结果先摘要
这是工程经验,框架文档里很少教,但实际项目里 80% 的 token 成本浪费都在这。
六、第五步:组装完整的 Mini Agent
把上面所有概念整合,下面是一个生产级的 Mini Agent 完整代码(约 300 行):
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
publicclass MiniAgent {
// ==================== 1. LLM 客户端 ====================
privatestaticfinal String API_URL = "https://api.anthropic.com/v1/messages";
privatestaticfinal String API_KEY = System.getenv("ANTHROPIC_API_KEY");
privatestaticfinal OkHttpClient http = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build();
privatestaticfinal ObjectMapper mapper = new ObjectMapper();
private static JsonNode callLLM(List<Map<String, Object>> messages,
String system,
List<Map<String, Object>> tools) throws IOException {
Map<String, Object> body = new LinkedHashMap<>();
body.put("model", "claude-sonnet-4-5");
body.put("max_tokens", 4096);
body.put("messages", messages);
if (system != null && !system.isBlank()) body.put("system", system);
if (tools != null && !tools.isEmpty()) body.put("tools", tools);
Request req = new Request.Builder()
.url(API_URL)
.header("x-api-key", API_KEY)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.post(RequestBody.create(
mapper.writeValueAsString(body),
MediaType.get("application/json")))
.build();
try (Response resp = http.newCall(req).execute()) {
String respBody = resp.body().string();
if (!resp.isSuccessful()) {
thrownew IOException("LLM error: " + resp.code() + " " + respBody);
}
return mapper.readTree(respBody);
}
}
// ==================== 2. 工具系统 ====================
@FunctionalInterface
interface Tool {
String execute(Map<String, Object> args) throws Exception;
}
privatefinal Map<String, Tool> registry = new HashMap<>();
privatefinal List<Map<String, Object>> toolSchemas = new ArrayList<>();
public void registerTool(String name, String description,
Map<String, Object> inputSchema, Tool tool) {
registry.put(name, tool);
toolSchemas.add(Map.of(
"name", name,
"description", description,
"input_schema", inputSchema
));
}
private String executeTool(String name, Map<String, Object> args) {
Tool tool = registry.get(name);
if (tool == null) return"Error: 未知工具 " + name;
try {
return tool.execute(args);
} catch (Exception e) {
return"Error: " + e.getMessage();
}
}
// ==================== 3. 上下文管理 ====================
privatestatic List<Map<String, Object>> trimMessages(
List<Map<String, Object>> msgs, int maxTurns) {
if (msgs.size() <= maxTurns * 2) return msgs;
List<Map<String, Object>> trimmed = new ArrayList<>();
trimmed.add(msgs.get(0));
trimmed.addAll(msgs.subList(msgs.size() - (maxTurns * 2 - 1), msgs.size()));
return trimmed;
}
// ==================== 4. Agent 主循环 ====================
privatefinal String systemPrompt;
private List<Map<String, Object>> messages = new ArrayList<>();
public MiniAgent(String systemPrompt) {
this.systemPrompt = systemPrompt;
}
public String run(String userInput, int maxIterations, boolean verbose) throws IOException {
messages.add(Map.of("role", "user", "content", userInput));
for (int i = 0; i < maxIterations; i++) {
if (verbose) System.out.println("\n--- 第 " + (i + 1) + " 轮 ---");
messages = trimMessages(messages, 10);
JsonNode resp = callLLM(messages, systemPrompt, toolSchemas);
String stopReason = resp.get("stop_reason").asText();
// 终止:模型给出最终答案
if ("end_turn".equals(stopReason)) {
StringBuilder finalText = new StringBuilder();
for (JsonNode b : resp.get("content")) {
if ("text".equals(b.get("type").asText())) {
finalText.append(b.get("text").asText());
}
}
messages.add(Map.of(
"role", "assistant",
"content", mapper.convertValue(resp.get("content"), List.class)
));
return finalText.toString();
}
// 工具调用
if ("tool_use".equals(stopReason)) {
messages.add(Map.of(
"role", "assistant",
"content", mapper.convertValue(resp.get("content"), List.class)
));
List<Map<String, Object>> toolResults = new ArrayList<>();
for (JsonNode block : resp.get("content")) {
if ("tool_use".equals(block.get("type").asText())) {
String name = block.get("name").asText();
Map<String, Object> args = mapper.convertValue(
block.get("input"), Map.class);
if (verbose) System.out.println("🔧 调用工具: " + name + "(" + args + ")");
String result = executeTool(name, args);
if (verbose) System.out.println("📦 结果: " +
result.substring(0, Math.min(100, result.length())));
toolResults.add(Map.of(
"type", "tool_result",
"tool_use_id", block.get("id").asText(),
"content", result
));
}
}
messages.add(Map.of("role", "user", "content", toolResults));
}
}
return"⚠️ 达到最大迭代次数";
}
// ==================== 5. 跑起来 ====================
public static void main(String[] args) throws IOException {
MiniAgent agent = new MiniAgent("你是一个聪明的助手,需要时主动调用工具。");
// 注册三个工具
agent.registerTool("get_weather", "查询指定城市的当前天气",
Map.of("type", "object",
"properties", Map.of("city", Map.of("type", "string")),
"required", List.of("city")),
a -> a.get("city") + " 今天晴,气温 25 度,微风");
agent.registerTool("get_current_time", "获取当前时间",
Map.of("type", "object", "properties", Map.of()),
a -> LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
agent.registerTool("calculate", "执行数学表达式计算",
Map.of("type", "object",
"properties", Map.of("expression", Map.of("type", "string")),
"required", List.of("expression")),
a -> {
// 生产环境别用 ScriptEngine 跑用户输入的表达式,这里只是 demo
javax.script.ScriptEngine engine = new javax.script.ScriptEngineManager()
.getEngineByName("JavaScript");
return String.valueOf(engine.eval((String) a.get("expression")));
});
String answer = agent.run(
"现在几点了?顺便帮我算一下 (1024 + 768) * 2,再告诉我北京和上海的天气。",
10, true);
System.out.println("\n✅ 最终答案:" + answer);
}
}
跑起来你会看到类似这样的输出:
--- 第 1 轮 ---
🔧 调用工具: get_current_time({})
📦 结果: 2026-05-09 14:23:11
🔧 调用工具: calculate({expression=(1024 + 768) * 2})
📦 结果: 3584
🔧 调用工具: get_weather({city=北京})
📦 结果: 北京 今天晴,气温 25 度,微风
🔧 调用工具: get_weather({city=上海})
📦 结果: 上海 今天晴,气温 25 度,微风
--- 第 2 轮 ---
✅ 最终答案:现在是 2026-05-09 14:23:11。
(1024 + 768) * 2 = 3584。
北京和上海今天都是晴天,气温 25 度,微风,天气不错~
就这,没了。一个能并行调用工具、能多轮推理、能自主终止的 Agent,完整代码 300 行,无任何 AI SDK 依赖。
七、与 Spring AI / LangChain4j 对比,我们少了什么?
可能有人会问:那 Spring AI、LangChain4j 那一坨代码都是干什么的?
我列一下生产级 Java AI 框架多出来的部分:
|
模块 |
我们的 Mini Agent |
Spring AI / LangChain4j |
|---|---|---|
|
工具调用 |
✅ 有 |
✅ 有,注解驱动( |
|
ReAct 循环 |
✅ 有 |
✅ 有 |
|
上下文裁剪 |
✅ 简单滑窗 |
✅ 多种 ChatMemory 策略 |
|
流式输出(SSE) |
❌ 没做 |
✅ 完整 Reactor 集成 |
|
多模型适配 |
❌ 写死 Claude |
✅ 几十种模型统一抽象 |
|
持久化记忆 |
❌ 没做 |
✅ Redis/PgVector/Milvus |
|
多 Agent 协作 |
❌ 没做 |
✅ 有 |
|
可观测性 |
❌ 简单 println |
✅ Micrometer / OTel |
|
异步与并发 |
❌ 没做 |
✅ Reactive 全家桶 |
|
Spring Boot 集成 |
❌ 纯手搓 |
✅ Starter + 自动装配 |
|
RAG 全链路 |
❌ 没做 |
✅ DocumentLoader/Splitter/向量库 |
所以这些框架不是没意义,它们解决的是"工程化"问题,不是"原理"问题。
但是你只有先理解了原理,才能:
-
看懂 Spring AI / LangChain4j 在做什么
-
Debug 框架抽象不到的问题
-
做出取舍——什么时候该上框架,什么时候自己写更划算
-
把 Prompt、上下文、工具描述这些"非代码"部分调到位
很多人卷不动 Agent,本质上是被框架把"工程问题"和"原理问题"搅成一锅粥。 拆开看,没那么难。
八、几个实战经验,建议收藏
最后分享几个写过 Agent 才知道的坑,全是血泪:
1. 工具描述比代码重要 10 倍
Agent 不工作,先去看 description 写得对不对,参数 description 全不全。这一项调对了,问题能解决一大半。
2. 永远要捕获工具异常并把错误信息返回给模型
模型有自我纠错能力,把异常 message 塞回去比抛出去 crash 整个 Agent 强一万倍。Java 党特别注意:**别 throw new RuntimeException**,全部 return "Error: ..."。
3. maxIterations 不是凑数的
没它你迟早被一个死循环卡爆 API 账单。最好再加个 token 累计上限做双保险。
4. 别一上来就上向量库
80% 的"长期记忆"需求,一个 MySQL 表 + LIKE 查询就能搞定,没必要先把 RAG 那套招呼上。
5. 流式输出要尽早做(SSE)
体验差距巨大。Spring Boot 里用 SseEmitter 或 Flux<String> 都行。一旦用户开始用,再加流式就很麻烦,因为整个调用链路要改。
6. 工具调用并发化
模型一次返回多个 tool_use 是常态。我们 demo 里是顺序执行的,生产代码里务必上 CompletableFuture.allOf() 并行跑,延迟能砍一半以上。
7. 上线前一定加 trace
每一步的 messages、tool_call、tool_result 都得记下来。可以接 Micrometer + Zipkin/Jaeger,或者直接打 ELK。否则线上一出问题,纯瞎猜。
8. 别用 eval 跑用户输入
我 demo 里那个 calculate 工具用了 ScriptEngine,生产环境千万别这么干,标准的 RCE 漏洞。换成 exp4j 之类的安全表达式引擎。
九、总结
整篇文章我们做了一件事:把 Agent 的黑盒子拆开,让你看清里面每个齿轮。
回顾一下核心要点:
-
Agent 的本质:LLM + 工具 + while 循环
-
LLM 的本质:无状态函数,所谓记忆全在 messages 列表里
-
工具的本质:JSON Schema 声明 + 本地方法执行 + 结果回填
-
ReAct 的本质:让模型自己决定何时停,靠 stop_reason 判断
-
上下文管理的本质:在"信息够用"和"成本可控"之间做平衡
把这五点想透,Spring AI、LangChain4j、阿里 Higress AI 网关、各种 Agent 论文,你都能秒懂。
框架可以学完就忘,原理学一次受用很久。
如果这篇对你有帮助,点个赞、收个藏、转发给身边踩坑的朋友。
评论区聊聊:你们写 Java Agent 时遇到过哪些奇葩 bug?
更多推荐



所有评论(0)