1. 从写业务代码到指挥智能体:Javaer 的认知切换
做了七八年 Java 后端的人,第一次听到“Agent”这个词,脑子里蹦出来的大概率是 Java Agent——那种通过premain或者agentmain挂到 JVM 上、配合Instrumentation做字节码增强的东西。我当年排查线上问题的时候,就靠一个自研的 Java Agent 把慢方法的调用链埋点全打出来了。所以当团队里开始讨论“我们要不要搞个 Agent 项目”的时候,我第一反应是:这玩意儿跟 Arthas 有什么区别?
答案很直接:完全不是一回事。Java Agent 是 JVM 层面的技术机制,而当下语境里的 Agent,指的是 AI Agent,也就是让大语言模型具备“感知—决策—行动”闭环能力的一套软件架构。你原来写的是OrderService.createOrder(),现在你要写的是让模型自己判断“用户这句话是想下单还是想退款”,然后决定调用哪个工具、传什么参数、拿到结果之后怎么回复。角色从“实现逻辑的人”变成了“设计决策环境的人”。
这个转变对 Javaer 来说其实有天然优势。为什么?因为 Agent 的本质是一个分布式决策系统——它需要状态管理、需要工具调用的编排、需要错误重试、需要超时控制、需要可观测性。这些东西,Java 后端开发者天天在做。你写过的那些状态机、工作流引擎、RPC 调用链、熔断降级策略,换一层皮就是 Agent 的运行时骨架。真正需要补的课,是对 LLM 能力边界和交互范式的理解,而不是工程能力本身。
这篇内容适合三类人看:第一类是有 Java 后端经验、想转 Agent 方向但不知道从哪下手的开发者;第二类是团队里被安排去调研 Agent 框架、需要快速建立认知地图的技术负责人;第三类是对 Agent 有模糊概念、想搞清楚它和传统程序到底差在哪里的工程师。我会从概念拆解讲到架构分层,再落到 Java 生态里能用的具体工具和踩坑经验,尽量把“Agent 是什么”这件事讲透。
2. Agent 到底是什么:拆开来看就是四件事
2.1 一个公式:Agent = LLM + 工具 + 记忆 + 循环
网上关于 Agent 的定义五花八门,有说“能自主行动的 AI”,有说“具备规划能力的智能体”。这些说法都对,但太抽象。我更愿意用一个工程化的公式来理解:
Agent = LLM(推理核心)+ Tools(行动能力)+ Memory(状态保持)+ Loop(迭代控制)
拆开看,LLM 负责“想”,Tools 负责“做”,Memory 负责“记住”,Loop 负责“反复想和做直到完成”。四个缺一个,都不算完整的 Agent。
举个具体例子。你让一个 Agent 帮你“查一下昨天上海飞北京的航班,选最便宜的那班,然后发邮件通知我”。这个过程里:
- LLM 解析你的意图,拆成子任务:查航班 → 比价 → 选最便宜的 → 发邮件
- Tools 提供航班查询 API、邮件发送 API
- Memory 记住“用户要的是最便宜的”“已经查到了三个航班”“选中的是东航 MU5137”
- Loop 控制流程:查完航班后判断“还需要比价”,比完价后判断“还需要发邮件”,发完邮件后判断“任务完成”
如果你用传统 Java 代码写这个功能,你得硬编码每一步的逻辑。但 Agent 的方式是:你只告诉它“你有这些工具,帮我完成这个目标”,具体怎么调、调几次、参数怎么填,由 LLM 在运行时决定。
2.2 和传统程序最本质的区别:控制反转
我刚开始接触 Agent 的时候,最不适应的就是这一点。写 Java 代码,控制流是我定的:if-else、for 循环、switch case,每一步走哪条路我心里有数。但 Agent 的控制流是 LLM 在运行时决定的,你只能通过 Prompt 和工具描述来“引导”它,不能“命令”它。
这就是所谓的控制反转。传统程序里,代码是控制者,数据是被处理的对象。Agent 里,LLM 是控制者,你的代码变成了它可以调用的“工具”。
这个反转带来的直接后果是:你没法保证 Agent 每次都走同一条路。同一个问题,今天它可能先查航班再比价,明天它可能先比价再查航班。只要最终结果对,路径不同其实无所谓。但如果你习惯了确定性编程,这个思维方式需要刻意练习才能适应。
2.3 Agent 和 Workflow 的边界在哪里
很多人会把 Agent 和 Workflow 搞混。Workflow 是你预先定义好步骤的编排,比如“第一步调 A 接口,第二步调 B 接口,第三步把结果拼起来”。Agent 是让 LLM 自己决定步骤。
那什么时候用 Workflow,什么时候用 Agent?我的经验判断标准很简单:
| 场景特征 | 推荐方案 | 原因 |
|---|---|---|
| 步骤固定、分支有限 | Workflow | 确定性高,调试容易,成本可控 |
| 步骤不固定、需要动态决策 | Agent | 灵活性高,能处理预期外的情况 |
| 对延迟和成本极度敏感 | Workflow | Agent 多轮调用 LLM,延迟和 token 成本都更高 |
| 需要处理自然语言输入 | Agent | LLM 的语义理解能力是 Workflow 不具备的 |
| 任务可以拆成明确的子任务 | 混合方案 | 外层 Workflow 编排,内层 Agent 处理不确定部分 |
实际项目里,纯 Agent 和纯 Workflow 都很少见,大多数是混合架构。比如一个客服系统,意图识别用 Workflow 路由,具体问题处理用 Agent 兜底。
2.4 为什么 Javaer 现在转 Agent 是个好时机
2024 年到 2025 年,Agent 开发还处于“框架混战”阶段。Python 生态有 LangChain、LlamaIndex、CrewAI,Java 生态有 Spring AI、LangChain4j、ADK(Agent Development Kit)。框架多意味着没有标准答案,但也意味着工程经验比框架熟练度更重要。
Javaer 的优势在于:
- 并发和状态管理:Agent 执行过程中需要维护会话状态、工具调用状态、重试状态,这些 Java 生态有成熟方案
- 可观测性:Agent 的调用链比普通 RPC 更复杂,Java 的 Micrometer、OpenTelemetry 集成可以直接复用
- 企业级集成:大部分企业的核心系统是 Java 写的,Agent 要落地必须和这些系统对接
- 类型安全:Java 的强类型系统在定义工具接口时反而比 Python 更清晰
我见过不少团队用 Python 快速搭了 Agent Demo,但一到生产环境就卡在并发、监控、部署上。这时候 Java 生态的工程积累就体现出来了。
3. Java 生态里能用的 Agent 框架:我实际试过的几个
3.1 Spring AI:Spring 开发者的自然延伸
Spring AI 是我第一个认真看的 Java Agent 框架。它的设计哲学很 Spring:约定优于配置,抽象屏蔽细节。你如果写过 Spring Boot 的@RestController,用 Spring AI 定义工具几乎零学习成本。
@Bean @Description("查询指定城市的天气") public Function<WeatherRequest, WeatherResponse> weatherFunction() { return request -> weatherService.query(request.city()); }就这么几行,一个工具就定义好了。Spring AI 会自动把这个 Function 注册成 LLM 可以调用的工具,包括生成 JSON Schema 描述、处理参数反序列化、把结果回传给模型。
但 Spring AI 目前的问题也很明显:抽象层太厚,出问题不好排查。有一次我调试一个工具调用失败的问题,错误信息被包装了三层,最后发现是 LLM 返回的 JSON 格式和 Spring AI 预期的对不上。这种时候你就得把日志级别调到 TRACE,一层层扒。
3.2 LangChain4j:更贴近底层的选择
LangChain4j 是 LangChain 的 Java 移植版,但并不是简单翻译。它的 API 设计更偏向“显式控制”,适合需要精细控制 Agent 行为的场景。
AiServices<Assistant> assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherTool(), new FlightTool()) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build();LangChain4j 的AiServices机制用动态代理把接口方法映射成 LLM 调用,这个设计很巧妙。你定义一个接口,它帮你生成实现,实现里自动处理 Prompt 组装、工具调用、记忆管理。
我比较喜欢 LangChain4j 的一点是它的记忆管理更灵活。Spring AI 的 ChatMemory 抽象比较固定,LangChain4j 可以让你自定义ChatMemoryStore,把对话历史存到 Redis、数据库或者文件里。对于需要持久化会话的 Agent 场景,这个很重要。
3.3 ADK(Agent Development Kit):Kotlin 优先但 JVM 通用
ADK 是 Google 推出的 Agent 开发框架,官方示例以 Kotlin 为主,但完全可以在 Java 项目里用。它的核心概念是Agent、Tool、Session、Runner,结构比 Spring AI 更清晰。
val agent = LlmAgent( name = "flightAssistant", model = GeminiModel("gemini-2.0-flash"), instruction = "你是一个航班助手,帮用户查询和预订航班", tools = listOf(FlightSearchTool(), BookingTool()) )ADK 的Runner负责执行循环,Session管理状态,Tool定义能力。这个分层和我在第 2 节讲的 Agent 四要素几乎一一对应。如果你想要一个“教科书式”的 Agent 框架来学习架构,ADK 的结构最清晰。
3.4 框架选型的实际考量
| 维度 | Spring AI | LangChain4j | ADK |
|---|---|---|---|
| 学习曲线 | 低(Spring 开发者友好) | 中 | 中 |
| 抽象程度 | 高 | 中 | 低 |
| 记忆管理 | 基础 | 灵活 | 完善 |
| 多 Agent 支持 | 弱 | 中 | 强 |
| 可观测性 | Micrometer 集成 | 需自行接入 | 内置 |
| 社区活跃度 | 高 | 高 | 中 |
| 生产落地案例 | 多 | 多 | 较少 |
我的建议是:如果你团队已经在用 Spring Boot,先从 Spring AI 入手,快速跑通 Demo 建立体感。如果发现抽象层限制太多,再考虑 LangChain4j 或 ADK。
4. 一个最小 Agent 的完整拆解:从 Prompt 到工具调用
4.1 工具定义:Agent 的“手”长什么样
工具是 Agent 和外部世界交互的唯一通道。在 Java 里定义一个工具,核心是方法签名 + 描述 + 参数 Schema。
public class OrderTool { @Tool("根据订单号查询订单状态") public OrderStatus queryOrder( @P("订单号,格式为 ORD-XXXXXXXX") String orderId ) { return orderService.getStatus(orderId); } @Tool("取消指定订单,仅在订单状态为待发货时可取消") public CancelResult cancelOrder( @P("订单号") String orderId, @P("取消原因,用于记录日志") String reason ) { return orderService.cancel(orderId, reason); } }这里有几个关键点:
描述比实现更重要。LLM 决定调不调这个工具、怎么填参数,全靠描述。我踩过的坑是:描述写得太简略,模型不知道该在什么场景下调用。比如“查询订单”这个描述,模型可能会在用户问“我的快递到哪了”的时候也调它,但其实应该调物流查询工具。后来我把描述改成“根据订单号查询订单的支付状态和发货状态,不包含物流轨迹信息”,误调率明显下降。
参数描述要包含格式约束。@P("订单号,格式为 ORD-XXXXXXXX")里的格式说明不是给人看的,是给模型看的。模型会根据这个约束来校验用户输入,如果用户给的订单号格式不对,它可能会先追问而不是直接调用。
工具粒度要适中。太粗,模型不知道怎么用;太细,模型要调很多次。我的经验是:一个工具对应一个完整的业务动作。比如“查询订单”是一个动作,“取消订单”是另一个动作,不要把“查询然后判断能不能取消然后取消”塞进一个工具里。
4.2 Prompt 设计:给 Agent 装“大脑”
System Prompt 是 Agent 的行为准则。写得好,Agent 像个靠谱的员工;写得差,Agent 像个刚入职的实习生。
你是一个电商客服助手,负责处理用户的订单相关问题。 你可以使用以下工具: - queryOrder:查询订单状态 - cancelOrder:取消订单 - queryLogistics:查询物流信息 工作原则: 1. 先理解用户意图,再决定调用哪个工具 2. 如果用户意图不明确,先追问澄清 3. 取消订单前必须确认订单状态为“待发货” 4. 如果工具返回错误,向用户解释原因并给出替代方案 5. 不要编造工具没有返回的信息这个 Prompt 里,第 5 条是最重要的。LLM 有“幻觉”倾向,工具没返回的信息它可能会自己编。明确告诉它“不要编造”,能大幅降低胡说八道的概率。
4.3 执行循环:Agent 的“心跳”
Agent 的执行循环用伪代码表示大概是:
while (!taskCompleted && iteration < maxIterations) { // 1. 把当前对话历史和工具定义发给 LLM LlmResponse response = llm.chat(history, toolDefinitions); // 2. 如果 LLM 决定调用工具 if (response.hasToolCall()) { ToolCall call = response.getToolCall(); // 3. 执行工具 ToolResult result = executeTool(call); // 4. 把结果加入历史 history.add(result); } else { // 5. LLM 决定直接回复用户 taskCompleted = true; return response.getContent(); } }这个循环里,maxIterations 是必须设置的。我见过 Agent 陷入死循环的案例:工具一直返回错误,Agent 一直重试,token 烧了几十万。一般设 10 到 15 轮比较合理,超过就强制终止并返回兜底话术。
4.4 记忆管理:Agent 的“记性”
Agent 的记忆分两种:短期记忆和长期记忆。
短期记忆就是当前会话的对话历史。最简单的实现是把所有消息塞进一个 List,每次调用 LLM 时全量传过去。但这样 token 消耗会随对话轮次线性增长。更好的做法是滑动窗口 + 摘要:保留最近 N 轮完整对话,更早的对话用 LLM 压缩成摘要。
public class SlidingWindowMemory { private final int maxMessages; private final Deque<Message> messages = new ArrayDeque<>(); public void add(Message message) { messages.addLast(message); if (messages.size() > maxMessages) { Message old = messages.pollFirst(); // 把旧消息压缩成摘要 summary = summarize(summary, old); } } }长期记忆则是跨会话的。比如用户上次说“我对花生过敏”,这次点餐时 Agent 应该记得。长期记忆通常存在向量数据库里,通过语义检索来召回相关记忆。
5. 踩坑实录:Javaer 转 Agent 最容易翻车的五个地方
5.1 把 Agent 当微服务写:过度工程化的陷阱
我刚转过来的时候,习惯性地把 Agent 拆成 Controller、Service、DAO 三层,工具定义放在 Service 层,Prompt 放在配置文件,执行循环用 Spring 的@Async异步跑。结果发现:调试极其痛苦。
因为 Agent 的执行路径是不确定的,你没法像调试微服务那样在某个方法打断点然后单步跟踪。LLM 的决策过程是个黑盒,你只能通过日志看它“输入了什么、输出了什么、调了什么工具”。
后来我调整了策略:Agent 的核心逻辑保持在一个类里,工具定义用注解,Prompt 用常量,执行循环用同步方式。先跑通,再考虑工程化。过早抽象是 Agent 开发的大忌。
5.2 工具描述写得太“程序员”:模型看不懂
我写过一个工具,描述是“根据 userId 查询用户信息,返回 UserDTO 对象”。测试的时候模型死活不调这个工具。后来我把描述改成“根据用户 ID 查询用户的基本信息,包括姓名、手机号、注册时间”,模型立刻就懂了。
模型不是编译器,它不理解类型系统。UserDTO对模型来说是个无意义的字符串,但“姓名、手机号、注册时间”是它能理解的概念。写工具描述的时候,要想象你在跟一个不懂技术的产品经理解释这个工具是干什么的。
5.3 忽略 Token 成本:月底账单教你做人
Agent 的 token 消耗比普通对话高一个数量级。因为每一轮循环都要把完整的对话历史 + 工具定义 + System Prompt 发给 LLM。如果对话有 20 轮,工具定义有 10 个,每轮消耗的 token 可能是普通对话的 5 到 10 倍。
我做过一个测算:一个中等复杂度的客服 Agent,单次会话平均消耗 8000 到 15000 token。如果按每天 1000 次会话算,一个月的 token 成本相当可观。
控制成本的手段:
- 精简工具定义:只给当前场景需要的工具,不要把所有工具都塞进去
- 压缩历史:用摘要代替全量历史
- 设置 maxIterations:防止死循环
- 缓存 System Prompt:部分模型支持 Prompt Caching,能省不少钱
- 小模型兜底:简单意图识别用小模型,复杂推理才用大模型
5.4 错误处理太粗暴:Agent 直接“摆烂”
传统 Java 代码里,异常处理是try-catch然后返回错误码。但 Agent 里,工具执行失败后,错误信息会作为工具结果返回给 LLM,LLM 会根据错误信息决定下一步。
如果你返回的错误信息是NullPointerException at OrderService.java:42,LLM 完全不知道该怎么办。但如果你返回“订单号 ORD-12345 不存在,请确认订单号是否正确”,LLM 就知道该让用户重新输入订单号。
@Tool("查询订单状态") public String queryOrder(String orderId) { try { Order order = orderService.findByOrderId(orderId); if (order == null) { return "未找到订单号为 " + orderId + " 的订单,请确认订单号是否正确"; } return "订单状态:" + order.getStatus() + ",下单时间:" + order.getCreateTime(); } catch (Exception e) { log.error("查询订单失败", e); return "查询订单时发生系统错误,请稍后重试"; } }工具返回给 LLM 的内容,应该是“人类可读的业务语义”,而不是“机器可读的技术异常”。
5.5 没有可观测性:出了问题两眼一抹黑
Agent 上线后,你最常问的问题是:“为什么这次它调了这个工具而不是那个?”“为什么它回复了这句话?”
如果没有完整的调用链日志,这些问题根本没法回答。我的做法是:每次 LLM 调用、每次工具执行、每次循环迭代都打结构化日志,包含 traceId、sessionId、iteration、input、output、toolName、duration。
log.info("Agent iteration: traceId={}, sessionId={}, iteration={}, " + "llmInput={}, llmOutput={}, toolCall={}, toolResult={}, duration={}ms", traceId, sessionId, iteration, input, output, toolName, result, duration);这些日志不仅能排查问题,还能用来做评测。你可以定期抽样分析 Agent 的决策路径,找出常见的错误模式,然后针对性地优化 Prompt 或工具描述。
6. 从 Javaer 到 Agent 开发者:我的学习路径建议
6.1 第一阶段:建立体感(1 到 2 周)
不要一上来就啃框架源码。先找一个能快速跑通的 Demo,亲手体验一下 Agent 的完整流程。
我的建议是用 Spring AI 或者 LangChain4j 写一个最简单的 Agent:一个工具(比如查询天气),一个 System Prompt,一个执行循环。跑通之后,故意制造一些异常情况——工具返回错误、用户输入模糊、模型调用不存在的工具——观察 Agent 怎么反应。
这个阶段的目标不是写出生产级代码,而是建立对 Agent 行为模式的直觉。你会开始理解为什么 Prompt 里要写“不要编造信息”,为什么工具描述要写得像产品文档。
6.2 第二阶段:深入一个框架(2 到 4 周)
选一个框架深入看。我推荐 LangChain4j,因为它的源码结构比较清晰,核心类不多,适合通读。
重点看这几个部分:
- ChatLanguageModel 的实现:理解不同模型提供商的 API 差异怎么被屏蔽
- Tool 的注册和调用机制:理解 JSON Schema 怎么生成、工具结果怎么回传
- ChatMemory 的实现:理解消息窗口、摘要、持久化的不同策略
- AiServices 的动态代理:理解接口方法怎么映射成 LLM 调用
看源码的时候,带着问题看:如果我要实现一个自定义的 MemoryStore,需要实现哪些接口?如果我要支持一个新的模型提供商,需要改哪些地方?
6.3 第三阶段:做一个真实项目(1 到 2 个月)
找一个你日常工作中重复性高的任务,用 Agent 的方式重新实现。比如:
- 自动整理会议纪要并提取待办事项
- 根据日志自动排查常见线上问题
- 自动生成周报
这个阶段你会遇到所有生产环境才会出现的问题:并发、超时、重试、成本控制、可观测性。解决这些问题的过程,就是从“会用 Agent”到“能交付 Agent”的跨越。
6.4 需要补的课和不需要补的课
需要补的:
- LLM 的基本原理(Transformer、Token、Temperature、Top-P)
- Prompt Engineering 的常用技巧(Few-shot、Chain-of-Thought、ReAct)
- 向量数据库和 Embedding 的基本概念
- 至少一个 Agent 框架的熟练使用
不需要补的:
- 深度学习、模型训练(除非你要做模型微调)
- Python(Java 生态足够用了)
- 前端(Agent 的后端逻辑才是核心)
6.5 一个容易被忽略的能力:评测
Agent 开发和传统开发最大的区别之一是没有单元测试。你没法写assertEquals(expected, agent.run(input)),因为 Agent 的输出是不确定的。
所以你需要建立一套评测体系。我的做法是:
- 收集 50 到 100 个真实用户问题作为测试集
- 对每个问题定义“期望行为”(应该调哪个工具、应该包含哪些信息)
- 定期跑评测,统计工具调用准确率、回复完整率、平均迭代次数
- 每次修改 Prompt 或工具后,重新跑评测对比
这套评测体系比任何 Prompt 技巧都重要。没有评测,你就是在盲调。
7. 关于 Agent 架构,我的一些个人判断
Agent 这个方向现在很热,但热不代表成熟。我个人的判断是:未来两年内,Agent 的开发范式会从“框架驱动”转向“协议驱动”。现在已经能看到苗头了——MCP(Model Context Protocol)在试图标准化工具的定义和调用,A2A(Agent-to-Agent)在试图标准化 Agent 之间的通信。当协议成熟之后,框架的差异会被抹平,开发者只需要关注业务逻辑本身。
对 Javaer 来说,这意味着现在投入时间学习 Agent 的底层原理,比死磕某个框架的 API 更有长期价值。框架会变,但“LLM + 工具 + 记忆 + 循环”这个核心模型不会变。你理解了工具调用的本质是“把方法签名翻译成模型能理解的描述”,理解了记忆管理的本质是“在 token 限制下保留最有用的信息”,理解了执行循环的本质是“在不确定中寻找收敛”,这些认知换到任何框架、任何语言都适用。
最后分享一个我实际工作中的小技巧:在 System Prompt 的最后加一句“如果你不确定,就说不知道”。这句话看起来简单,但能显著降低 Agent 胡编乱造的概率。我测试下来,加了这句话之后,幻觉相关的 bad case 减少了大概四成。原因也不复杂——模型在不确定的时候倾向于“猜一个答案”,但如果你明确给它“说不知道”的许可,它就更愿意承认自己不知道。这个技巧在客服、查询类 Agent 里特别管用。