1. 为什么 Java 工程师转 AI Agent 有天然优势
1.1 从 CRUD 到智能体:一次能力栈的平移
这两年身边不少做 Java 的朋友都在焦虑同一件事:AI Agent 火了,但好像跟自己没什么关系。打开教程一看,清一色 Python,LangChain、LlamaIndex、AutoGen,全是没碰过的生态。于是很多人得出一个结论——转型要从零开始学 Python。
我的判断恰恰相反。Java 工程师转 AI Agent,不是从零开始,而是一次能力栈的平移。你过去几年积累的东西,至少有七成可以直接复用。
先想清楚 AI Agent 到底是什么。抛开那些玄乎的说法,一个 Agent 的本质就是:让大模型在一个循环里自主决定调用哪些工具、观察结果、再决定下一步,直到任务完成。这个循环在学术上叫 ReAct(Reasoning + Acting),翻译成人话就是"边想边做"。
拆开看,这个循环里需要什么?
- 一个能发起 HTTP 请求、处理 JSON 的客户端——你写过的 RestTemplate、WebClient、Feign 全是干这个的。
- 一套工具注册与调度机制——这不就是 Spring 的依赖注入和策略模式吗?
- 会话状态管理、超时重试、并发控制、日志追踪——这些是 Java 后端工程师的看家本领。
- 把业务逻辑封装成可被调用的服务——你写了多少年的 Service 层,现在只是换个调用方。
真正需要新学的,其实只有两块:提示词工程和大模型 API 的调用范式。前者是软技能,后者一周就能上手。所以别被"AI"两个字吓住,它没有推翻你的技术底座,只是在你熟悉的架构上换了个"大脑"。
1.2 生态已经补齐:LangChain4j 与 Spring AI 的定位
有人会问,Java 生态做 Agent 是不是很落后?放在 2023 年确实如此,但现在已经不是了。目前主流的两条路线是LangChain4j和Spring AI,它们解决的是同一类问题,但气质完全不同。
LangChain4j 的定位更像"功能全集"。它把 Python LangChain 里常用的抽象——ChatModel、EmbeddingModel、ChatMemory、Tool、Retriever、RAG——几乎一比一搬到了 Java。你想做的多路召回、向量检索、工具调用,它都有现成实现。适合想快速验证想法、或者需要复杂 RAG 能力的场景。
Spring AI 的定位则是"Spring 原生"。它把大模型调用抽象成 Spring 里的一等公民,用ChatClient这种流式 API 来组织调用,配置走application.yml,工具用@Tool注解声明,跟@Service、@Bean的思维完全一致。如果你团队本来就是 Spring Boot 技术栈,Spring AI 的上手成本几乎为零。
我的建议很直接:新项目、Spring 技术栈、以工具调用为主的 Agent,优先 Spring AI;需要复杂 RAG、多路召回、或者想参考 Python 生态成熟方案的,用 LangChain4j。两者并不互斥,实际项目里混用也很常见——用 Spring AI 管业务集成,用 LangChain4j 做检索增强。
1.3 一个必须先建立的认知:Agent 不是聊天机器人
转型路上最大的坑,是把 Agent 当成"会聊天的接口"。很多人第一版代码就是:接收用户输入,拼个提示词,调模型,返回文本。这叫 Chatbot,不叫 Agent。
区别在哪?Chatbot 是一问一答,模型只负责生成文字。Agent 是目标驱动,模型要决定"我现在该干什么"——是直接回答,还是调用某个工具查数据,还是先拆解任务再逐步执行。这个"决定"的过程,就是 ReAct 循环的核心。
举个具体例子。用户说"帮我查一下上个月华东区的销售冠军是谁,然后给他发一封祝贺邮件"。
- Chatbot 的反应:直接编一段话,或者告诉你"我无法访问你的数据库"。
- Agent 的反应:先调用
querySalesData工具查出冠军,拿到结果后调用sendEmail工具发邮件,最后汇报"已完成"。
差别就在于 Agent 会主动调用工具并根据结果继续推理。理解了这一点,你后面写的所有代码才有意义。这也是为什么我说 Java 工程师有优势——工具调用的本质就是服务编排,而这正是我们最擅长的事。
2. ReAct 循环的 Java 实现原理拆解
2.1 ReAct 到底在循环什么
ReAct 这个名字来自 2022 年的一篇论文,核心思想是把"推理"和"行动"交织在一起。用人话讲,就是让模型每一步都输出两部分内容:Thought(我在想什么)和Action(我要做什么),然后系统执行 Action,把结果作为 Observation 喂回去,模型再基于新信息产生下一个 Thought。
一个完整的循环长这样:
- 用户提出目标。
- 模型输出 Thought + Action(比如"我需要查销售数据,调用 querySalesData")。
- 系统解析 Action,执行对应工具,得到 Observation。
- 把 Observation 追加到对话历史,再次调用模型。
- 模型判断:任务完成了吗?没完成就继续输出下一个 Action,完成了就输出 Final Answer。
- 循环直到拿到 Final Answer 或达到最大步数。
关键点在于,这个循环是模型驱动的,不是代码写死的。你不能在代码里写"第一步查数据、第二步发邮件",因为真实任务千变万化。你只能提供工具清单和规则,让模型自己决定调用顺序。
这就带来一个工程上的核心问题:如何让模型稳定地输出结构化的 Action。如果模型输出一段自由文本,你的代码根本没法解析。所以实际实现里,我们通常要求模型输出 JSON,或者用模型厂商提供的原生 Function Calling 能力。
2.2 工具调用的三种实现路径
在 Java 里实现工具调用,有三条路,复杂度递增,稳定性也递增。
第一条路:提示词约定 + 文本解析。在系统提示词里告诉模型"你要输出这样的 JSON:{"tool": "xxx", "args": {...}}",然后代码里用正则或 JSON 解析器提取。优点是通用,任何模型都能用;缺点是模型经常不听话,多输出几个字、少个括号,解析就崩了。适合做原型验证。
第二条路:模型原生 Function Calling。主流大模型都支持这个能力——你在请求里传入工具定义(JSON Schema 格式),模型如果决定调用工具,会在响应里返回结构化的tool_calls字段,而不是自由文本。这是目前最稳的方式。Spring AI 和 LangChain4j 都封装了这个能力,你只需要用注解声明工具,框架自动生成 Schema 并解析响应。
第三条路:框架托管。Spring AI 的@Tool注解、LangChain4j 的@Tool注解,本质都是第二条路的封装。你写一个普通 Java 方法,加个注解,框架负责把它转成模型能理解的工具描述,并在模型要求调用时反射执行。这是生产环境的首选。
我实测下来的经验是:能用原生 Function Calling 就别用文本解析。文本解析在 demo 里跑得挺欢,一上生产就各种边界情况,维护成本极高。
2.3 状态管理:Agent 的"记忆"怎么存
Agent 跟普通接口最大的不同,是它需要多轮状态。一次任务可能涉及五六次模型调用,每次都要把之前的对话历史带上,否则模型就"失忆"了。
这里有个容易踩的坑:对话历史不是越长越好。模型的上下文窗口有限,而且历史越长,token 消耗越大、响应越慢、还容易"跑偏"。所以状态管理要解决三个问题:
- 存什么:通常存用户消息、模型回复、工具调用记录、工具返回结果。但工具返回的大段数据(比如查出来 1000 行记录)要截断或摘要,不能原样塞回去。
- 存多久:单次任务内的历史必须保留,跨任务的长期记忆则要另做设计(比如存数据库,按需检索)。
- 怎么裁剪:常见策略是滑动窗口(只保留最近 N 轮)、摘要压缩(把早期历史让模型总结成一段话)、或者关键信息提取。
在 Spring AI 里,ChatMemory接口就是干这个的,默认实现是InMemoryChatMemory,生产环境一般换成基于 Redis 或数据库的实现。LangChain4j 里对应的是ChatMemoryStore。别小看这块,Agent 的稳定性很大程度取决于记忆管理做得好不好。
2.4 并发与超时:Agent 扛并发的真实难点
热搜里有个词叫"ai agent 怎么扛并发",这确实是生产落地的核心痛点。普通接口的并发瓶颈在数据库和 CPU,Agent 的瓶颈在模型 API 的调用——每次调用都是几百毫秒到几秒的网络请求,而且很多模型服务有 QPS 限制。
我踩过的坑总结成几条:
- 单次任务串行,任务之间并行。一个 Agent 任务内部的 ReAct 循环必须串行(因为下一步依赖上一步结果),但不同用户的任务可以并行处理。用线程池或响应式编程都行。
- 给模型调用设超时和重试。模型服务偶尔会抽风,超时设 30 秒左右,重试 2 次,配合指数退避。别用默认的无超时,否则一个卡住的请求会拖垮整个线程池。
- 限制最大循环步数。一定要设
maxIterations,比如 10 步。否则模型可能陷入死循环,一直调用同一个工具,把你的 token 烧光。 - 做限流和降级。模型服务有 QPS 上限,用信号量或令牌桶限流。高峰期可以降级到更小的模型,或者直接返回"当前繁忙"。
这些其实都是 Java 后端的常规操作,只是换了个对象。你过去做过的接口限流、熔断降级,在这里原封不动能用。
3. 用 Spring AI 搭一个能干活的最小 Agent
3.1 环境准备与依赖配置
先说版本。Spring AI 迭代很快,建议用 1.0.0 以上的稳定版,JDK 用 17 或 21。Maven 依赖主要就两个:核心 starter 和具体模型厂商的 starter。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>如果你用的是国内模型服务(比如阿里百炼、通义千问),Spring AI Alibaba 提供了对应的 starter,配置方式类似,只是 base-url 和模型名不同。这里要注意,不同厂商的 API 兼容性有差异,有些支持完整的 Function Calling,有些只支持部分,选型前先确认。
配置文件里至少要配三样:API Key、base-url、模型名。
spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://your-endpoint/v1 chat: options: model: your-model-name temperature: 0.7提示:API Key 千万别硬编码在代码或配置文件里提交到仓库,用环境变量或配置中心。这是老生常谈,但每年都有人栽在这上面。
3.2 用 @Tool 注解声明你的第一个工具
工具就是普通的 Spring Bean 方法,加个@Tool注解。框架会自动读取方法签名和注解描述,生成模型能理解的工具定义。
@Component public class SalesTools { @Tool(description = "根据月份和区域查询销售冠军,参数格式为 yyyy-MM 和区域名") public String querySalesChampion(String month, String region) { // 实际业务查询逻辑 return "张伟,销售额 128 万"; } @Tool(description = "给指定员工发送祝贺邮件,参数为员工姓名和邮件内容") public String sendCongratsEmail(String name, String content) { // 实际发邮件逻辑 return "邮件已发送给 " + name; } }这里有几个细节决定成败:
- description 要写清楚。模型完全靠这段描述判断"什么时候该用这个工具"。写得太笼统,模型就会乱调。最好把参数格式、适用场景都写进去。
- 参数类型要简单。String、int、boolean 这类基础类型最稳。复杂对象虽然也支持,但模型生成参数时容易出错。
- 方法要幂等或可重入。模型可能因为重试而重复调用同一个工具,如果你的工具是"扣款"这种操作,一定要做幂等控制。
3.3 组装 ChatClient 并跑通第一个 ReAct 循环
Spring AI 的ChatClient是流式 API,组装起来很直观:
@RestController public class AgentController { private final ChatClient chatClient; public AgentController(ChatClient.Builder builder, SalesTools salesTools) { this.chatClient = builder .defaultSystem("你是一个销售助理,可以查询数据并发送邮件。请一步步思考,需要数据时调用工具。") .defaultTools(salesTools) .build(); } @GetMapping("/agent") public String run(@RequestParam String task) { return chatClient.prompt() .user(task) .call() .content(); } }就这么几行,一个能调用工具的 Agent 就跑起来了。当你问"查一下 2024-06 华东区的销售冠军并给他发祝贺邮件",框架会自动完成:模型判断需要调querySalesChampion→ 执行 → 把结果喂回模型 → 模型判断需要调sendCongratsEmail→ 执行 → 返回最终答复。
框架帮你隐藏了 ReAct 循环的所有细节,你只需要关心工具本身。这就是 Spring AI 的价值——把复杂的循环编排封装成声明式配置。
3.4 加上记忆和流式输出
生产环境还需要两样东西:记忆和流式。
记忆通过ChatMemory注入:
this.chatClient = builder .defaultSystem("...") .defaultTools(salesTools) .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())) .build();调用时带上会话 ID,框架就会自动维护该会话的历史:
chatClient.prompt() .user(task) .advisors(a -> a.param("chat_memory_conversation_id", sessionId)) .call() .content();流式输出则把.call()换成.stream(),返回Flux<String>,前端用 SSE 接收。对于 Agent 这种响应时间较长的场景,流式能显著改善体验——用户能看到模型"正在思考"和"正在调用工具"的过程,而不是干等十几秒。
注意:流式模式下工具调用的处理会复杂一些,因为工具执行是阻塞的。Spring AI 内部做了处理,但如果你自己实现循环,要小心线程模型。
4. LangChain4j 做 RAG 与多路召回实战
4.1 什么时候该上 RAG
Agent 光有工具还不够。很多场景下,模型需要基于私有知识回答——比如公司内部文档、产品手册、历史工单。这些内容模型训练时没见过,直接问它只会瞎编。这时候就要上 RAG(检索增强生成)。
RAG 的思路很朴素:把私有文档切块、向量化、存进向量库;用户提问时,先检索出最相关的几块,拼进提示词,让模型基于这些内容回答。这样既用上了私有知识,又避免了重新训练模型。
LangChain4j 在这块比 Spring AI 成熟,尤其是多路召回——同时用向量检索、关键词检索等多种方式召回候选,再融合排序。单一向量检索对语义相似但关键词不匹配的查询效果一般,多路召回能明显提升命中率。
4.2 文档切分与向量化的关键参数
RAG 效果好不好,七成取决于文档处理。切分策略是第一个关键点。
- 块大小(chunk size):常见 500 到 1000 字符。太小则上下文不完整,太大则检索精度下降、还浪费 token。中文场景建议按语义切分,别硬按字符数切。
- 重叠(overlap):相邻块之间留 10% 到 20% 的重叠,避免关键信息正好被切断。
- 元数据:每块要带上来源、章节、时间等元信息,检索时可以按元数据过滤,回答时也能标注出处。
向量化用 Embedding 模型,把文本转成向量。这里要注意查询和文档必须用同一个 Embedding 模型,否则向量空间不一致,检索结果全是乱的。这是个新手常犯的错误。
EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(System.getenv("AI_API_KEY")) .build(); EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); DocumentSplitter splitter = DocumentSplitters.recursive(800, 150); List<TextSegment> segments = splitter.split(document); List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); store.addAll(embeddings, segments);4.3 多路召回与重排序的落地
单一向量检索的问题在于:用户问"退款流程",文档里写的是"退货操作指引",语义相近但用词不同,向量检索可能召回不到。多路召回就是同时跑几种检索,取长补短。
典型组合是:向量检索 + 关键词检索(BM25)。向量检索擅长语义匹配,关键词检索擅长精确匹配。两路各召回 Top 20,合并去重后得到候选集,再用重排序模型(Reranker)精排,取 Top 5 喂给大模型。
LangChain4j 里可以用EmbeddingStoreContentRetriever配合自定义的检索器实现。重排序可以用专门的 Reranker 模型,也可以用简单的规则(比如按召回来源加权)。
我实测的经验:多路召回对召回率的提升通常在 10% 到 20%,但会带来延迟增加。如果对延迟敏感,可以只对复杂查询启用多路,简单查询走单路。
4.4 把 RAG 接进 Agent 的两种方式
RAG 和 Agent 结合有两种模式。
第一种:RAG 作为工具。把检索封装成一个@Tool,模型需要知识时主动调用。优点是灵活,模型自己决定要不要查;缺点是模型可能"忘了查",直接凭记忆瞎答。
第二种:RAG 作为前置。每次提问前先检索,把结果拼进系统提示词。优点是保证每次都有知识支撑;缺点是即使不需要检索的简单问题也会触发检索,浪费资源。
我的做法是混合:默认走前置检索,同时把检索也暴露成工具,让模型在需要更深入查询时可以主动再查一次。这样兼顾了稳定性和灵活性。
5. 生产落地的坑与排查清单
5.1 模型不调用工具怎么办
这是最高频的问题。模型明明有工具可用,却直接编了个答案。原因通常有三个:
- 工具描述不清楚。模型不知道这个工具是干嘛的,自然不调。解决方法是把 description 写得更具体,甚至加上"当用户询问 X 时使用此工具"。
- 系统提示词没强调。在 system prompt 里明确要求"涉及数据查询必须调用工具,不要凭记忆回答"。
- 模型能力不足。小模型对 Function Calling 的支持往往不好,换个能力强的模型试试。
排查时可以先打开框架的调试日志,看看实际发给模型的工具定义长什么样,往往一眼就能发现问题。
5.2 工具调用参数错误怎么防
模型生成的参数经常有格式问题——日期格式不对、枚举值拼错、必填参数漏了。防御手段有几层:
- 参数校验:工具方法内部做严格校验,参数不合法就返回明确的错误信息,让模型知道错了并重试。
- 枚举约束:如果参数是有限集合,在 description 里列清楚所有合法值。
- 默认值兜底:非关键参数给默认值,避免因为一个可选参数缺失导致整个调用失败。
提示:工具返回的错误信息要"对模型友好",用人话说明哪里错了、应该怎么改,而不是抛一个 Java 异常堆栈。模型看不懂堆栈,但看得懂"日期格式应为 yyyy-MM-dd"。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型不调用工具 | 描述不清 / 提示词未强调 / 模型能力弱 | 检查工具定义、system prompt、换模型 |
| 工具参数格式错误 | 模型生成不稳定 | 加校验、列枚举、给默认值 |
| 响应特别慢 | 循环步数多 / 模型慢 / 检索慢 | 限制 maxIterations、看各阶段耗时 |
| token 消耗爆炸 | 历史太长 / 工具返回数据太大 | 裁剪历史、截断工具结果 |
| 并发上不去 | 模型 QPS 限制 / 线程池配置 | 限流、加线程池、异步化 |
| 回答胡编乱造 | 没走 RAG / 检索没召回 | 检查检索链路、加多路召回 |
| 会话串味 | 记忆隔离没做好 | 检查 sessionId 传递 |
5.4 我踩过的三个真实坑
第一个坑:工具返回大对象。早期我让工具直接返回一个包含几百条记录的 List,序列化成 JSON 塞回模型,结果一次调用烧掉几万 token,还超了上下文。后来改成工具内部做摘要,只返回关键字段和统计信息。
第二个坑:无限循环。有个工具在特定输入下总是返回"未找到",模型就一直重试同一个工具。加了maxIterations和"同一工具连续调用超过 3 次就强制终止"的逻辑才解决。
第三个坑:并发下的记忆污染。早期用单例的InMemoryChatMemory,多个用户共享,导致 A 的对话历史串到 B 那里。后来改成按 sessionId 隔离,问题消失。这个坑很隐蔽,测试时单用户根本发现不了。
6. 转型路线与学习节奏建议
6.1 分阶段的学习路径
如果你是从零开始,我建议按这个节奏走,别一上来就啃论文。
第一阶段(1 到 2 周):跑通最小闭环。用 Spring AI 或 LangChain4j 写一个能调用一两个工具的 Agent,理解 ReAct 循环。这个阶段的目标是"能跑起来",不追求完美。
第二阶段(2 到 3 周):补齐 RAG。学会文档切分、向量化、检索,做一个基于私有文档的问答。理解 chunk size、overlap、Embedding 这些概念的实际影响。
第三阶段(2 到 4 周):生产化。加上记忆管理、并发控制、限流降级、日志追踪。这部分对 Java 工程师来说是舒适区,主要是把已有经验迁移过来。
第四阶段(持续):深入原理。读 ReAct 论文、了解不同的 Agent 架构(Plan-and-Execute、Reflection 等)、研究提示词工程技巧。这部分是拉开差距的地方。
6.2 哪些 Java 技能可以直接复用
列个清单,你会发现能复用的比想象中多:
- Spring 生态:依赖注入、AOP、配置管理,直接用于工具注册和 Agent 组装。
- 并发编程:线程池、CompletableFuture、响应式编程,用于并发控制和异步调用。
- HTTP 客户端:RestTemplate、WebClient、Feign,用于调用模型 API。
- JSON 处理:Jackson、Gson,用于解析模型响应和工具参数。
- 缓存与存储:Redis、数据库,用于记忆管理和向量存储。
- 监控与日志:Micrometer、Logback,用于 Agent 的可观测性。
真正要新学的,其实只有提示词工程和模型 API 的调用范式。所以别焦虑,你的底子比你以为的厚。
6.3 面试中会被问到的几个点
如果你在准备相关岗位的面试,这几个问题出现频率很高:
- ReAct 循环的原理是什么?答清楚 Thought-Action-Observation 的循环,以及为什么需要它。
- Agent 怎么扛并发?答串行与并行的边界、限流、超时重试、线程池。
- RAG 的完整链路?从文档切分到检索到生成,每个环节的关键参数。
- 工具调用不稳定怎么解决?答原生 Function Calling、参数校验、错误反馈。
- LangChain4j 和 Spring AI 怎么选?答各自的定位和适用场景。
这些问题没有标准答案,面试官想看的是你有没有真正动手做过。所以一定要有能讲出来的项目经验,哪怕是自己练手的小项目。
6.4 一个可以立刻上手的练手项目
如果你不知道从哪练起,我推荐做这个:一个能查数据库并生成报表的 Agent。
需求很简单:用户用自然语言描述想查什么,Agent 自动生成 SQL、执行、把结果整理成表格返回。涉及的能力点很全:工具调用(执行 SQL)、参数校验(防 SQL 注入)、结果处理(格式化)、错误处理(SQL 报错反馈给模型重试)。
这个项目不大,但把 Agent 的核心环节都串了一遍。做完它,你对 ReAct、工具调用、状态管理的理解会扎实很多。而且它足够实用,很多内部工具场景直接能用。
我个人在实际操作中的体会是,转型这件事最怕的不是技术难,而是迟迟不动手。看再多教程,不如自己写一个能跑的小 Agent。从最简单的"查天气"工具开始,跑通了再往上加复杂度。Java 工程师的工程能力是稀缺的——现在市面上会调模型的人不少,但能把 Agent 做成稳定生产系统的人不多。这中间的差距,恰恰是你最擅长的那部分。