ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Javaer 转 Agent 开发:从概念到框架选型与踩坑实录

Javaer 转 Agent 开发:从概念到框架选型与踩坑实录

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灵活性高,能处理预期外的情况
对延迟和成本极度敏感WorkflowAgent 多轮调用 LLM,延迟和 token 成本都更高
需要处理自然语言输入AgentLLM 的语义理解能力是 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 AILangChain4jADK
学习曲线低(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 的输出是不确定的。

所以你需要建立一套评测体系。我的做法是:

  1. 收集 50 到 100 个真实用户问题作为测试集
  2. 对每个问题定义“期望行为”(应该调哪个工具、应该包含哪些信息)
  3. 定期跑评测,统计工具调用准确率、回复完整率、平均迭代次数
  4. 每次修改 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 里特别管用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进