Agent 到底是什么?一个 while 循环的本质
如果你在社交媒体上搜索 "AI Agent",你会看到无数花哨的定义:自主智能体、具身认知系统、目标驱动的决策引擎……
忘掉它们。
今天我们从最朴素的角度出发,用 一段 Java 代码 告诉你 Agent 的全部秘密。
一句话定义
Agent = while 循环 + 工具调用
就这么简单。
一个 LLM 本身只能生成文本。它不能读文件、不能跑测试、不能看报错。如果没有循环,每次工具调用都需要你手动把结果粘贴回去——你自己就成了那个循环。
Agent 做的事情,就是把你从这个循环里解放出来。
最小 Agent 的架构
整个控制流只有 一个退出条件:模型的 finish_reason 不再是 "tool_calls"。
这意味着:
- 模型决定该调用什么工具(推理)
- 系统执行工具并返回结果(执行)
- 模型根据结果决定下一步(再推理)
- 直到模型认为任务完成,输出最终回答(终止)
用 Java 实现一个完整的 Agent
下面我们从零开始,用 Java 实现一个真正可运行的 Agent。没有框架魔法,没有 Spring,就是最裸的 HTTP 调用——这样你能看清每一个细节。
我们使用 OpenAI 的 Chat Completions API,通过 function calling 机制让模型调用工具。
第一步:准备工作
先引入两个 Java 世界的基础库:OkHttp 负责 HTTP 请求,Gson 负责 JSON 序列化。
Maven 依赖:
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
第二步:定义工具
Agent 需要"手脚"。我们只给它一个工具——执行 bash 命令。一个工具就够了,因为 bash 几乎能做任何事。
OpenAI 的 function calling 格式如下:
static final List<Map<String, Object>> TOOLS = List.of(
Map.of(
"type", "function",
"function", Map.of(
"name", "bash",
"description", "Execute a bash shell command and return the output.",
"parameters", Map.of(
"type", "object",
"properties", Map.of(
"command", Map.of(
"type", "string",
"description", "The bash command to execute"
)
),
"required", List.of("command")
)
)
)
);
工具的执行函数同样简单——用 ProcessBuilder 跑一个 shell 命令,拿到输出:
static String executeBash(String command) {
try {
Process process = new ProcessBuilder("sh", "-c", command)
.redirectErrorStream(true) // 合并 stdout 和 stderr
.start();
String output = new String(process.getInputStream().readAllBytes());
boolean finished = process.waitFor(120, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
return "Error: Timeout (120s)";
}
return output.isBlank() ? "(no output)" : output.trim();
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
第三步:核心——Agent 循环
这是整篇文章最重要的部分。所有 Agent 的本质,都藏在下面这个方法里:
static void agentLoop(List<Map<String, Object>> messages, ModelClient modelClient)
throws IOException {
while (true) {
// 1. 调用模型:把完整的消息历史发给 LLM
Map<String, Object> choice = modelClient.call(messages);
String finishReason = (String) choice.get("finish_reason");
Map<String, Object> assistantMsg =
(Map<String, Object>) choice.get("message");
// 2. 把模型的回复追加到消息历史
messages.add(assistantMsg);
// 3. 检查退出条件:模型不再调用工具 → 任务完成
if (!"tool_calls".equals(finishReason)) {
String content = (String) assistantMsg.get("content");
if (content != null) {
System.out.println("\nAssistant: " + content);
}
return;
}
// 4. 执行每一个工具调用,收集结果
List<Map<String, Object>> toolCalls =
(List<Map<String, Object>>) assistantMsg.get("tool_calls");
for (Map<String, Object> toolCall : toolCalls) {
String callId = (String) toolCall.get("id");
Map<String, Object> function =
(Map<String, Object>) toolCall.get("function");
String arguments = (String) function.get("arguments");
Map<String, String> args = gson.fromJson(arguments, Map.class);
String command = args.get("command");
System.out.println("\n> bash: " + command);
String output = executeBash(command);
System.out.println(output);
// 5. 把工具结果追加到消息历史(OpenAI 格式:role=tool)
messages.add(Map.of(
"role", "tool",
"tool_call_id", callId,
"content", output
));
}
// 回到第 1 步
}
}
就这么多,这就是一个完整的 Agent。
逐步拆解循环
让我们慢下来,看看每一步到底在干什么:
| 步骤 | 做什么 | 为什么 |
|---|---|---|
| 调用模型 | 把完整消息历史发给 LLM | 模型需要看到所有上下文才能做出正确决策 |
| 追加回复 | 把 assistant 消息加入历史 | 保持对话连贯性,模型下一轮要看到自己上一轮说了什么 |
| 检查退出 | finish_reason 不是 tool_calls |
模型认为不需要再调工具 = 任务完成 |
| 执行工具 | 运行 bash 命令 | 让模型的"想法"变成"行动" |
| 追加结果 | 把 tool 消息加入历史 |
模型需要看到执行结果才能推理下一步 |
这个循环有一个精妙之处:模型自己决定什么时候停下来。你不需要写任何条件判断逻辑,不需要状态机,不需要流程图。模型说"我还需要调工具",循环就继续;模型说"我搞定了",循环就结束。
第四步:HTTP 调用
为了完整性,这里是调用 OpenAI Chat Completions API 的方法。这部分是纯粹的胶水代码,不是重点:
private static final String API_URL = "https://api.openai.com/v1/chat/completions";
private static final String MODEL = "gpt-4o-mini";
private static final OkHttpClient httpClient = new OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS).build();
private static final Gson gson = new Gson();
static ModelClient createOpenAIClient(String apiKey) {
return messages -> {
Map<String, Object> body = new LinkedHashMap<>();
body.put("model", MODEL);
body.put("messages", messages);
body.put("tools", TOOLS);
Request request = new Request.Builder()
.url(API_URL)
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.post(RequestBody.create(
gson.toJson(body),
MediaType.get("application/json")))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("API error " + response.code()
+ ": " + response.body().string());
}
Map<String, Object> responseBody = gson.fromJson(
response.body().string(), Map.class);
List<Map<String, Object>> choices =
(List<Map<String, Object>>) responseBody.get("choices");
return choices.get(0);
}
};
}
第五步:启动入口
public static void main(String[] args) throws IOException {
String apiKey = System.getenv("OPENAI_API_KEY");
if (apiKey == null) {
System.err.println("Please set OPENAI_API_KEY");
return;
}
ModelClient client = createOpenAIClient(apiKey);
String userInput = args.length > 0 ? args[0] : "List current directory files";
System.out.println("User: " + userInput);
List<Map<String, Object>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", SYSTEM_PROMPT));
messages.add(Map.of("role", "user", "content", userInput));
agentLoop(messages, client);
}
消息数组:Agent 的短期记忆
注意到了吗?整个 Agent 最重要的数据结构不是模型,不是工具,而是那个贯穿始终的 messages 列表。
messages = [
{ role: "system", content: "You are a coding agent..." } // 系统提示
{ role: "user", content: "帮我创建一个 hello.py" } // 用户输入
{ role: "assistant", tool_calls: [bash("touch hello.py")] } // 模型决定调 bash
{ role: "tool", content: "(no output)" } // bash 执行结果
{ role: "assistant", tool_calls: [bash("cat hello.py")] } // 模型决定再调
{ role: "tool", content: "print('hello')" } // 第二次执行结果
{ role: "assistant", content: "hello.py 已创建..." } // 最终回答
]
每一轮循环都在往这个列表里追加:模型的回复、工具的结果,如此往复。模型看到完整的历史,才能做出连贯的决策。
这里有一个值得思考的问题:消息只会越来越多,直到塞满上下文窗口。当你的 Agent 执行了 50 次工具调用,messages 数组里可能已经有几十万 token。怎么办?这个问题我们后面会专门讲。
动手试试
# 克隆项目
git clone https://github.com/geyuxu/yuxu-java-agent.git
cd yuxu-java-agent
# 设置 API Key
export OPENAI_API_KEY="your-key-here"
# 编译运行
mvn compile exec:java -Dexec.args="'创建一个 hello.py,写入 Hello World 并运行它'"
试试这些提示词,观察 Agent 是如何一步步调用 bash、获取结果、再决定下一步的:
"列出当前目录下所有 Java 文件""创建一个 test_output 目录,在里面写 3 个文件""查看系统内存使用情况,找出占用最多的前 5 个进程"
真正的 Agent 多出来的是什么
我们写了不到 100 行核心逻辑。一个生产级的 Agent 可能有几千行。那些多出来的代码在处理什么?
+-- 流式响应(逐 token 展示,而非等全部生成完)
+-- 权限检查(执行危险命令前需要用户确认)
+-- 错误恢复(API 超时、限流、网络抖动的重试)
while (true) ------+-- 上下文压缩(token 超限时自动摘要历史消息)
+-- 中止处理(用户随时可以按 Ctrl+C 中断)
+-- 最大轮次限制(防止模型陷入无限循环)
+-- 多工具并发(同时执行多个互不依赖的工具调用)
每一项都是 真实世界的需求 在向那个简洁的 while 循环施加压力。但无论代码怎么膨胀,内核从未改变:
调用模型 -> 检查是否需要工具 -> 执行工具 -> 把结果喂回去 -> 重复
你可能注意到了,在我们的 Agent 循环里,有一个看似不起眼但意义深远的设计:模型自己决定什么时候停下来。
这不是一个工程上的偷懒。这是 Agent 和传统自动化脚本的根本区别。
一个 Shell 脚本是你写好每一步:先做 A,再做 B,最后做 C。步骤是固定的,遇到意外就挂了。
而 Agent 的循环里,你只定义了"能做什么"(工具),不定义"该做什么"(流程)。模型在每一轮循环中根据当前的完整上下文——用户的原始请求、之前所有工具调用的结果、中间遇到的错误——自主决定下一步。它可能会调 3 次工具,也可能调 30 次。它可能会遇到错误后换一种方式重试,也可能发现任务比预期简单而提前结束。
这就是为什么一个 while 循环 + 工具调用,就构成了一个 Agent。循环提供了持续行动的能力,工具提供了作用于外部世界的能力,而模型提供了在每个节点做出判断的能力。
三者缺一不可。
总结
Agent 的本质是一个 while 循环。模型负责"想",工具负责"做",循环负责把它们串起来。