
AI Agent 开发是当前后端开发者讨论最多的话题之一。我遇到过不少带着“想转 AI 应用开发”想法来做 Java 面试辅导的同学也接过许多号称要做 Agent 项目但实际上第一步就卡在 Maven 依赖上的咨询。做了大量一对一辅导和代码问题排查后我的判断没有变Agent 开发真正的难点不在提示词而在工程底座。下面以 Java 后端的视角拆解 Agent 开发需要的 Java 知识点给出一个最小可运行的日志分析 Agent 示例并把高概率出现的报错和排查路径列出来。1. 先搞清楚Agent 开发真正考验的是工程底座不是提示词技巧1.1 为什么“AI Agent 辅导”听起来热门却很难直接交付AI Agent 被很多文章描述成一个“只要会和大模型对话就能做出智能应用”的方向。实际接触过 Agent 项目的人会很清楚一个可交付的 Agent 至少要解决这些问题用户输入如何进入系统如何做校验和会话隔离。Agent 如何选择工具调用 Elasticsearch、数据库、外部接口后如何拿到结构化结果。大模型返回的 JSON 如何解析解析失败如何处理。单次调用超时怎么办多个工具并发调用如何控制线程池。上下文怎么保存重启之后会话记忆是否还在。模型调用消耗多少 Token哪一轮调用导致线上故障如何通过日志还原。这些能力没有一样是提示词能替代的。纯模板式“Agent 辅导”只能帮助刚入门的人理解什么是 Agent、怎么跑通一个模型调用 demo。一旦进入企业私有化部署、日志检索、权限控制、稳定性治理问题最终还是落在 Java 后端基本功上。1.2 Agent 开发到底由哪几部分组成把一个 Agent 系统拆开看通常包含五层模型层负责与 LLM 推理服务通信通常是一个 HTTP 接口输入 messages输出 text 或 tool_calls。工具层给 Agent 提供真实能力例如查询日志、执行 SQL、调用订单接口。记忆层保存短期对话上下文和长期业务记忆常见载体是 Redis、MySQL 或向量数据库。规划层决定下一步调用哪个工具可以是一个提示词模板也可以是一段状态机逻辑。执行与观测层真正执行工具调用记录每一步的输入、输出、耗时、错误和 Token 消耗。这些层用 Java 实现时对应技术非常明确。模型层用 HttpClient 或 Feign工具层用 Elasticsearch Java API Client、RedisTemplate、MyBatis记忆层用 Redis 和 MySQL规划层是一段可测试的 Java 逻辑执行层需要线程池、异步任务和结构化日志。只看框架概念不看这些工程组件很难独立完成一个上线的 Agent 服务。1.3 用 Java 做 Agent和后端开发的关系用 Java 做 Agent本质上不是发明一套新框架而是把一个后端系统接入大模型。Spring Boot 的 Bean 管理、Maven 的依赖机制、连接池、事务、异常处理、日志、监控、部署这些知识一个都不能少。很多同学觉得 Java 面试八股文和 Agent 开发没有关系其实关系非常直接面试考线程池参数因为 Agent 的工具调用天然是高并发场景。面试考 HashMap 和 Stream因为要处理模型输出的 JSON 结构。面试考 Redis 缓存和分布式锁因为多实例部署时 Agent 会话状态需要共享。面试考 Elasticsearch 查询因为很多 Agent 的“检索能力”就是通过 ES 实现的。所以不要急着把 Java 基础扔掉。先承认 Java 后端能力是 Agent 开发的地基后面学习 Agent 才会顺。2. 我在 Java 面试辅导中看到的三类 Agent 学习误区2.1 误区一只学 Agent 框架不读源码遇到问题只能重启不少人在学习 Agent 时第一步是安装某个 Agent 编排框架第二步是复制一个示例第三步是跑通 demo。程序能运行时会觉得很顺利一旦报错就会进入“重启、换模型、改提示词”的死循环。问题不在于框架不好而在于没有理解框架内部的调用链。一个 Agent 编排框架至少包含 Tool 注册、模型调用、消息组装、结果解析、异常处理这几层。如果不懂这些组件之间的数据流看到tool call failed这类错误时根本不知道是该检查工具入参还是检查模型返回格式还是检查鉴权配置。正确做法是先找一个足够简单的调用链手写一遍。比如 Java 程序发起 HTTP 请求调用模型服务拿到返回后解析出工具名称和参数再调用本地方法把结果拼接回上下文。这个过程不需要复杂框架但能训练排查问题的能力。2.2 误区二把“大模型调用”当成 Agent 的全部还有一种常见认知是“只要调通了大模型Agent 就做完了”。实际项目中大模型调用只是链路中的一环。模型可能返回空内容可能返回不合法 JSON可能长时间不响应可能被恶意 prompt 诱导产生错误行为。这些都需要在工程层面处理。Java 里有几个非常具体的能力要求使用 Jackson 解析模型输出时要处理未知字段和缺失字段。调用模型接口时要设置连接超时和读取超时。针对模型接口的 5xx 错误要做重试和熔断。对用户输入要做长度限制和内容过滤。对系统提示词和用户输入要拼接成结构化消息而不是简单字符串拼接。这些内容才是 Agent 开发和普通 CRUD 明显不同的地方但它们本质上是后端工程问题。2.3 误区三概念学了一堆Java 工程一个都没写学习路线错位是最可惜的。有人先背了 Agent、Tool、Memory、Plan 一大串术语再学框架最后才发现连 Maven 依赖都不会配置。概念帮助理解方向但真正检验学习效果的是能不能在本地跑起一个 Java 工程。我在辅导中经常看到这样的情况误区表现在咨询中常见的原话正确做法只学框架“我学了某个框架的 demo但改不动了”先读一个工具调用链写单测覆盖只调大模型“模型返回不对我就换提示词”先检查输入上下文、原始日志和解析逻辑只背概念“Agent 记忆我知道有短期和长期”用 Redis 存会话用 ES 存日志动手写一遍技术学习里概念只是索引代码才是正文。每学一个新概念都要用 Java 实现一个最小闭环。3. Agent 开发前先用这份 Java 清单给自己体检3.1 Java 基础自测清单不用等到把所有 Java 知识学完再碰 Agent。但下面这张清单里的内容是 Agent 工程里出现频率最高的 Java 能力项建议逐条自查。模块需要掌握到什么程度为什么 Agent 开发需要集合知道 HashMap 原理会使用 Stream 处理 List组装消息上下文处理工具调用结果并发线程池参数、CompletableFuture、锁Agent 多工具调用、异步归并IO/HTTPHttpClient、文件读写、流处理调用大模型服务读取日志文件JSONJackson 反序列化、泛型处理解析模型输出和 ES 返回结果数据库连接池、事务、慢查询排查持久化会话与记忆SpringIoC、AOP、事务、配置外置工程化组织 Agent 服务网络/认证REST API 概念、鉴权方式工具调用、模型网关接入日志/监控SLF4J、结构化日志、链路追踪Agent 调试和线上观测如果清单里超过三项不熟建议先不要急着做 Agent 项目把对应的 Java 基础补起来。特别是并发和 JSON这两项在 Agent 工程里几乎每天都要用。3.2 并发和资源隔离Agent 后端最容易翻车的地方Agent 调用大模型通常是阻塞式 HTTP 调用单次耗时可能几秒到几十秒。如果使用 Tomcat 默认线程池处理请求一个用户的多轮问答就可能占满线程。更危险的是多个用户同时触发 Agent 工具调用时线程池很可能被打满随后出现接口超时和线程饥饿。常见做法是给 Agent 模型调用单独配置线程池并把工具调用与业务线程隔离。下面是一个最小示例ExecutorService agentPool new ThreadPoolExecutor( 8, // 核心线程数 16, // 最大线程数 60L, // 空闲线程存活时间 TimeUnit.SECONDS, new LinkedBlockingQueue(200), new ThreadPoolExecutor.CallerRunsPolicy() );核心线程数不能开太大因为大模型接口有并发限制和成本限制。队列长度也不宜无限否则内存可能先被打满。CallerRunsPolicy是一种降级策略线程池满时由调用线程执行任务虽然会拖慢调用方但至少不会直接丢弃任务。3.3 环境准备JDK、Maven、Elasticsearch做下面的日志分析 Agent 示例前先确认环境符合要求JDK 17 或更高版本。Maven 3.8 或更高版本。Elasticsearch 8.x本地或远程可访问。可选一个兼容 OpenAI 格式的大模型推理服务。先执行下面三条命令检查环境java -version mvn -version curl http://localhost:9200/_cluster/health最后一条命令如果返回包含status : green或status : yellow的 JSON说明 ES 可以访问。如果连接不上需要先排查 ES 服务是否启动、端口是否正确、是否开启了认证。注意Elasticsearch Java API Client 的服务端版本和客户端版本尽量保持一致至少大版本不能差异过大否则协议不兼容会出现序列化或请求失败的问题。4. 最小可运行案例Java Elasticsearch REST API 实现日志分析 Agent4.1 需求设计这个例子的目标是模拟“日志分析 Agent”用户输入一个关于错误日志的问题Java 程序从 Elasticsearch 查询最近 ERROR 日志把日志内容组装成提示词调用大模型服务返回分析结论。分成三个模块LogQueryTool查询 Elasticsearch返回日志文本。LlmClient调用大模型 HTTP 接口。LogAnalyzerAgent把日志查询和模型调用组合起来。如果本地没有大模型服务可以先让LogAnalyzerAgent只打印 Prompt确认日志查询链路没有问题后再接入模型。4.2 工程结构在本地创建一个 Maven 项目目录结构如下log-analyzer-agent/ ├── pom.xml └── src/main/java/com/beifeng/agent/ ├── LogAnalyzerApp.java ├── LogAnalyzerAgent.java ├── LogQueryTool.java ├── LlmClient.java └── LogEntry.java为了保持示例简单不引入 Spring Boot。实际生产项目可以把这些类注册为 Spring Bean外包 RestClient 和线程池。4.3 依赖配置pom.xml中使用 Java 17并加入 Elasticsearch Java Client 和 Jackson 依赖project modelVersion4.0.0/modelVersion groupIdcom.beifeng/groupId artifactIdlog-analyzer-agent/artifactId version1.0.0/version properties maven.compiler.release17/maven.compiler.release /properties dependencies dependency groupIdco.elastic.clients/groupId artifactIdelasticsearch-java/artifactId version8.11.3/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.16.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.30/version scopeprovided/scope /dependency /dependencies /project版本号可能随官方发布变化落地前要确认你所用的 ES 服务端版本与客户端版本是否兼容。Lombok 依赖可以根据团队习惯取舍这个例子中即便不用 Lombok手写 getter/setter 也能运行。4.4 核心代码查询日志、组装 Prompt、调用模型先定义日志实体LogEntry.javapackage com.beifeng.agent; public class LogEntry { private String level; private String message; private String timestamp; public String getLevel() { return level; } public void setLevel(String level) { this.level level; } public String getMessage() { return message; } public void setMessage(String message) { this.message message; } public String getTimestamp() { return timestamp; } public void setTimestamp(String timestamp) { this.timestamp timestamp; } }定义工具层LogQueryTool.java负责查询 Elasticsearch 中的 ERROR 日志package com.beifeng.agent; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.rest_client.RestClientTransport; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import java.io.IOException; import java.util.stream.Collectors; public class LogQueryTool { private final ElasticsearchClient esClient; private final String index; public LogQueryTool(String host, int port, String index) { RestClient restClient RestClient.builder(new HttpHost(host, port, http)).build(); ElasticsearchTransport transport new RestClientTransport(restClient, new JacksonJsonpMapper()); this.esClient new ElasticsearchClient(transport); this.index index; } public String queryErrorLogs(int size) throws IOException { SearchRequest request SearchRequest.of(s - s .index(index) .query(q - q.match(t - t.field(level).query(ERROR))) .size(size) ); SearchResponseLogEntry response esClient.search(request, LogEntry.class); return response.hits().hits().stream() .map(Hit::source) .filter(source - source ! null) .map(LogEntry::getMessage) .collect(Collectors.joining(\n)); } }这里的关键点是查询条件。示例只匹配了levelERROR生产环境通常还要加时间范围避免一次查出全量历史日志。还应该限制返回条数并做分页防止 ES 返回超大数据导致内存溢出。定义模型调用客户端LlmClient.java使用 Java 自带的HttpClientpackage com.beifeng.agent; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class LlmClient { private final String endpoint; private final String apiKey; private final HttpClient httpClient HttpClient.newHttpClient(); private final ObjectMapper objectMapper new ObjectMapper(); public LlmClient(String endpoint, String apiKey) { this.endpoint endpoint; this.apiKey apiKey; } public String chat(String prompt) throws Exception { String safePrompt prompt.replace(\\, \\\\) .replace(\, \\\) .replace(\n, \\n); String body { model: log-analyzer, messages: [ {role: system, content: 你是日志分析助手回答要简洁、可操作。}, {role: user, content: %s} ] } .formatted(safePrompt); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(endpoint)) .header(Content-Type, application/json) .header(Authorization, Bearer apiKey) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponseString response httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { throw new RuntimeException(LLM 服务返回异常HTTP response.statusCode() 响应 response.body()); } return extractContent(response.body()); } private String extractContent(String responseBody) throws Exception { JsonNode root objectMapper.readTree(responseBody); return root.path(choices).path(0).path(message).path(content).asText(); } }这里默认模型服务返回的是常见的choices[0].message.content结构。不同服务商的字段可能不同实际接入时需要根据 API 文档调整请求体和响应解析。定义编排层LogAnalyzerAgent.javapackage com.beifeng.agent; public class LogAnalyzerAgent { private final LogQueryTool logQueryTool; private final LlmClient llmClient; public LogAnalyzerAgent(LogQueryTool logQueryTool, LlmClient llmClient) { this.logQueryTool logQueryTool; this.llmClient llmClient; } public String analyze(String question) throws Exception { String logs logQueryTool.queryErrorLogs(20); String prompt 你是日志分析助手。请根据给定 ERROR 日志回答用户问题。 要求先写关键错误再写可能原因最后写排查建议。\n\n 用户问题\n question \n\n 最近错误日志\n logs; return llmClient.chat(prompt); } }最后写入口LogAnalyzerApp.javapackage com.beifeng.agent; public class LogAnalyzerApp { public static void main(String[] args) throws Exception { String esHost args.length 0 ? args[0] : localhost; int esPort args.length 1 ? Integer.parseInt(args[1]) : 9200; String index args.length 2 ? args[2] : app-logs; String question args.length 3 ? args[3] : 分析最近的错误日志给出核心原因和排查建议; String llmEndpoint System.getenv(LLM_ENDPOINT); String llmApiKey System.getenv(LLM_API_KEY); if (llmEndpoint null || llmEndpoint.isBlank()) { throw new IllegalArgumentException(请先设置 LLM_ENDPOINT 环境变量); } LogQueryTool logQueryTool new LogQueryTool(esHost, esPort, index); LlmClient llmClient new LlmClient(llmEndpoint, llmApiKey null ? : llmApiKey); LogAnalyzerAgent agent new LogAnalyzerAgent(logQueryTool, llmClient); String answer agent.analyze(question); System.out.println(answer); } }4.5 运行验证先用 Maven 编译mvn clean compile然后设置大模型服务地址export LLM_ENDPOINThttp://localhost:8000/v1/chat/completions export LLM_API_KEYlocal-test-key运行入口类时需要让 Maven 执行 Java 程序。可以在pom.xml中引入exec-maven-plugin然后在命令行执行mvn exec:java -Dexec.mainClasscom.beifeng.agent.LogAnalyzerApp \ -Dexec.argslocalhost 9200 app-logs 分析最近的错误日志在 Elasticsearch 中有 ERROR 日志且大模型服务可用的情况下控制台会输出分析结论。关键错误系统出现大量数据库连接超时。 可能原因连接池满、慢 SQL 占用连接、数据库负载过高。 排查建议先看连接池使用率再查慢查询日志最后检查数据库节点负载。如果不想先接大模型可以临时把LlmClient.chat改成打印 prompt 并返回固定字符串先把 ES 查询链路验证通再接入真实模型。注意运行成功不等于链路可用。还要验证索引名是否正确、查询条件是否命中日志、大模型响应是否被正确解析。建议先用 curl 直接查询 ES确认原始数据存在。5. 从 demo 到生产Agent 项目的分层、配置和可观测性5.1 分层设计上面的示例是一个最小结构。生产环境建议按职责拆成更多模块避免所有逻辑都堆在一个 Agent 类里。层级职责典型实现接入层暴露 HTTP API做参数校验和鉴权Spring Boot Controller编排层管理 Agent 状态、调用工具、组装多轮上下文AgentService、StateMachine工具层封装 ES、数据库、Redis、外部系统LogQueryTool、OrderQueryTool模型层统一封装大模型调用、重试、超时LlmClient、ModelGateway存储层保存会话、记忆、日志MySQL、Redis、Elasticsearch分层的好处是每一层都可以单独测试。比如LogQueryTool不依赖模型可以单独写单元测试LlmClient不依赖业务逻辑可以用 Mock 服务验证。5.2 配置外置和密钥管理不要把 ES 密码、API Key 写在代码里。本地示例可以通过环境变量传入生产环境建议使用配置中心或密钥管理服务。下面是一个环境变量的配置示例export ES_HOST10.0.0.10 export ES_PORT9200 export ES_USERNAMEelastic export ES_PASSWORDyour-password export LLM_ENDPOINThttps://your-llm-gateway.internal/v1/chat/completions export LLM_API_KEYyour-api-keyJava 里使用System.getenv读取而不是在代码中写死。生产环境还应该为模型服务和 ES 都设置网络白名单避免接口暴露到公网。5.3 日志、链路追踪和评估Agent 调试比普通接口调试更麻烦因为一次回答可能涉及多轮模型调用和多次工具调用。建议给每个会话生成一个traceId并在日志中记录用户输入。每一步工具调用的入参和返回摘要。大模型请求的消息数量、Token 消耗、耗时。工具调用是否失败。最终回答的截断摘要。日志结构可以打印成 JSON便于在 Elasticsearch 或 SkyWalking 中聚合分析。例如