ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java工程师转型AI Agent实战:Spring AI与LangChain4j构建ReAct循环

Java工程师转型AI Agent实战:Spring AI与LangChain4j构建ReAct循环 1. 为什么 Java 工程师转 AI Agent 有天然优势1.1 从 CRUD 到智能体一次能力栈的平移这两年身边不少做 Java 的朋友都在焦虑同一件事AI Agent 火了但好像跟自己没什么关系。打开教程一看清一色 PythonLangChain、LlamaIndex、AutoGen全是没碰过的生态。于是很多人得出一个结论——转型要从零开始学 Python。我的判断恰恰相反。Java 工程师转 AI Agent不是从零开始而是一次能力栈的平移。你过去几年积累的东西至少有七成可以直接复用。先想清楚 AI Agent 到底是什么。抛开那些玄乎的说法一个 Agent 的本质就是让大模型在一个循环里自主决定调用哪些工具、观察结果、再决定下一步直到任务完成。这个循环在学术上叫 ReActReasoning 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 怎么扛并发这确实是生产落地的核心痛点。普通接口的并发瓶颈在数据库和 CPUAgent 的瓶颈在模型 API 的调用——每次调用都是几百毫秒到几秒的网络请求而且很多模型服务有 QPS 限制。我踩过的坑总结成几条单次任务串行任务之间并行。一个 Agent 任务内部的 ReAct 循环必须串行因为下一步依赖上一步结果但不同用户的任务可以并行处理。用线程池或响应式编程都行。给模型调用设超时和重试。模型服务偶尔会抽风超时设 30 秒左右重试 2 次配合指数退避。别用默认的无超时否则一个卡住的请求会拖垮整个线程池。限制最大循环步数。一定要设maxIterations比如 10 步。否则模型可能陷入死循环一直调用同一个工具把你的 token 烧光。做限流和降级。模型服务有 QPS 上限用信号量或令牌桶限流。高峰期可以降级到更小的模型或者直接返回当前繁忙。这些其实都是 Java 后端的常规操作只是换了个对象。你过去做过的接口限流、熔断降级在这里原封不动能用。3. 用 Spring AI 搭一个能干活的最小 Agent3.1 环境准备与依赖配置先说版本。Spring AI 迭代很快建议用 1.0.0 以上的稳定版JDK 用 17 或 21。Maven 依赖主要就两个核心 starter 和具体模型厂商的 starter。dependency groupIdorg.springframework.ai/groupId artifactIdspring-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()返回FluxString前端用 SSE 接收。对于 Agent 这种响应时间较长的场景流式能显著改善体验——用户能看到模型正在思考和正在调用工具的过程而不是干等十几秒。注意流式模式下工具调用的处理会复杂一些因为工具执行是阻塞的。Spring AI 内部做了处理但如果你自己实现循环要小心线程模型。4. LangChain4j 做 RAG 与多路召回实战4.1 什么时候该上 RAGAgent 光有工具还不够。很多场景下模型需要基于私有知识回答——比如公司内部文档、产品手册、历史工单。这些内容模型训练时没见过直接问它只会瞎编。这时候就要上 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(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); DocumentSplitter splitter DocumentSplitters.recursive(800, 150); ListTextSegment segments splitter.split(document); ListEmbedding 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 做成稳定生产系统的人不多。这中间的差距恰恰是你最擅长的那部分。
返回列表