看了一堆 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 的。我们定义一个工具,需要告诉模型:

  1. 工具叫什么(name)

  2. 干啥的(description)

  3. 要传什么参数(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

工具调用

✅ 有

✅ 有,注解驱动(@Tool

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 的黑盒子拆开,让你看清里面每个齿轮

回顾一下核心要点:

  1. Agent 的本质:LLM + 工具 + while 循环

  2. LLM 的本质:无状态函数,所谓记忆全在 messages 列表里

  3. 工具的本质:JSON Schema 声明 + 本地方法执行 + 结果回填

  4. ReAct 的本质:让模型自己决定何时停,靠 stop_reason 判断

  5. 上下文管理的本质:在"信息够用"和"成本可控"之间做平衡

把这五点想透,Spring AI、LangChain4j、阿里 Higress AI 网关、各种 Agent 论文,你都能秒懂。

框架可以学完就忘,原理学一次受用很久。


如果这篇对你有帮助,点个赞、收个藏、转发给身边踩坑的朋友。
评论区聊聊:你们写 Java Agent 时遇到过哪些奇葩 bug?

更多推荐