ARTICLE DETAIL

资讯详情

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

Java工程师转型AI Agent实战:从LangChain4j到生产级并发与RAG落地

Java工程师转型AI Agent实战:从LangChain4j到生产级并发与RAG落地 1. Java 工程师转型 AI Agent 的底层逻辑与路径选择1.1 为什么 Java 工程师转 AI Agent 有天然优势很多 Java 工程师一提到 AI Agent第一反应是“这是 Python 的天下我是不是得从头学一门语言”。我刚开始接触这个方向时也有同样的焦虑但实际做下来发现Java 工程师转型 AI Agent 不但不是劣势反而在某些维度上有独特的优势。先说最核心的一点AI Agent 的本质是一个工程系统而不是一个算法模型。大模型负责推理和生成但 Agent 要真正“干活”需要任务编排、状态管理、工具调用、异常重试、并发控制、日志追踪、权限校验——这些东西全是后端工程师的看家本领。一个能稳定运行的 Agent 系统80% 的代码量在工程层面只有 20% 在模型交互层面。Java 工程师在 Spring 生态里摸爬滚打多年对依赖注入、AOP、事务管理、线程池、连接池这些概念烂熟于心迁移到 Agent 开发时这些经验可以直接复用。第二个优势是类型系统和工程规范。Python 写 Agent 原型很快但项目一大就容易失控动态类型带来的隐式 bug 在复杂工具链调用中非常致命。Java 的强类型、编译期检查、成熟的构建工具链在构建生产级 Agent 时反而更稳。LangChain4j 和 Spring AI 这两个框架之所以能快速崛起就是因为大量企业级 Java 团队需要把 Agent 能力嵌入到已有的 Spring Boot 系统里而不是另起炉灶搞一套 Python 服务。第三个优势是企业级中间件的熟悉度。Agent 要落地绕不开消息队列、缓存、数据库、网关、监控这些基础设施。Java 工程师对这些组件的使用经验直接决定了 Agent 系统能不能扛住真实流量。热搜词里有个“ai agent 怎么扛并发”这个问题在 Java 世界里其实有标准答案——线程池隔离、异步编排、限流降级只是换了个应用场景而已。1.2 转型路线图从调用 API 到构建自主 Agent我把转型过程拆成四个阶段每个阶段都有明确的产出物避免“学了一堆概念但做不出东西”的尴尬。第一阶段模型调用与 Prompt 工程。目标是能稳定地调用大模型 API理解 temperature、top_p、max_tokens 这些参数的实际影响能写出结构化的 Prompt 并解析返回结果。这个阶段不需要框架用 HttpClient 直接调 REST API 就行目的是把底层交互摸透。第二阶段引入框架做 RAG 和工具调用。这时候开始用 LangChain4j 或 Spring AI把知识库检索、函数调用这些能力接进来。重点是理解 Embedding、向量检索、Function Calling 的机制而不是只会调框架的 API。第三阶段ReAct 模式与多步推理。这是 Agent 真正“智能”的地方——让模型自己决定下一步做什么、调用哪个工具、什么时候停止。ReActReasoning Acting是当前最主流的范式理解它的循环机制是转型的关键节点。第四阶段生产化改造。包括并发控制、可观测性、成本控制、安全护栏、多 Agent 协作。这个阶段才是 Java 工程师真正拉开差距的地方。提示不要一上来就啃 LangChain4j 或 Spring AI 的源码先把一个最小可用的 Agent 跑通再回头理解框架设计效率会高很多。1.3 框架选型LangChain4j 与 Spring AI 怎么选这是被问得最多的问题。我的建议是如果你的项目已经在用 Spring Boot优先选 Spring AI如果是独立 Agent 服务或者需要更灵活的编排能力选 LangChain4j。对比维度LangChain4jSpring AI定位通用 LLM 应用框架Spring 生态的 AI 集成层学习曲线中等概念较多平缓符合 Spring 习惯工具调用注解式 编程式灵活注解式为主规范统一RAG 支持内置多种检索器扩展性强基础 RAG 完善深度定制需自己写多模型支持覆盖广切换成本低主流模型都有国内模型适配好生产特性需自己补齐天然融入 Spring 生态适合场景复杂 Agent、多步推理企业应用嵌入 AI 能力实际项目中我经常两个一起用用 Spring AI 做模型接入和基础对话用 LangChain4j 做复杂的 Agent 编排和 RAG。两者并不冲突因为底层都是 HTTP 调用可以在一个项目里共存。2. 核心原理拆解Agent 到底是怎么“思考”的2.1 从 Chatbot 到 Agent 的本质区别很多人分不清 Chatbot 和 Agent。简单说Chatbot 是“你问我答”Agent 是“你给目标我自己想办法完成”。这个区别决定了架构设计完全不同。Chatbot 的流程是线性的用户输入 → 模型生成 → 返回结果。Agent 的流程是循环的接收目标 → 思考下一步 → 选择工具 → 执行 → 观察结果 → 判断是否完成 → 继续或结束。这个循环就是 ReAct 模式的核心。举个具体例子。用户说“帮我查一下北京明天天气如果下雨就提醒我带伞”。Chatbot 只能回答“我无法查询实时天气”。Agent 会这样做第一步思考“我需要调用天气查询工具”第二步调用工具传入“北京明天”第三步观察返回结果“明天有雨”第四步思考“用户说下雨要提醒带伞我应该生成提醒”第五步输出“明天北京有雨记得带伞”。这个过程中模型不是一次性生成答案而是分多步推理每步都可能调用外部工具。这就是 Agent 的威力也是复杂度所在。2.2 ReAct 模式的循环机制与实现要点ReAct 的全称是 Reasoning and Acting核心思想是让模型在“思考”和“行动”之间交替。一个标准的 ReAct 循环包含四个环节Thought思考模型分析当前状态决定下一步做什么Action行动选择一个工具并生成调用参数Observation观察执行工具把结果返回给模型判断模型根据观察结果决定是继续循环还是输出最终答案在 Java 里实现这个循环关键是要设计好停止条件和最大迭代次数。我见过太多项目因为没设上限模型陷入死循环疯狂调用工具一晚上烧掉几百块 API 费用。public class ReActAgent { private static final int MAX_ITERATIONS 10; public String run(String userGoal) { ListMessage history new ArrayList(); history.add(systemPrompt()); history.add(userMessage(userGoal)); for (int i 0; i MAX_ITERATIONS; i) { String response llmClient.chat(history); AgentStep step parseStep(response); if (step.isFinalAnswer()) { return step.getAnswer(); } String observation toolExecutor.execute( step.getToolName(), step.getToolInput() ); history.add(assistantMessage(response)); history.add(toolResultMessage(observation)); } return 达到最大迭代次数任务未完成; } }这段代码看起来简单但每个环节都有坑。比如parseStep要能容错处理模型返回的非标准格式toolExecutor要做超时和异常隔离history要控制长度避免超出上下文窗口。2.3 Function Calling 与工具注册的工程实践工具调用是 Agent 的手脚。在 Java 里注册工具LangChain4j 和 Spring AI 都支持注解式声明但生产环境我建议用显式注册 参数校验的方式而不是纯靠注解反射。原因很简单注解式虽然优雅但工具的参数校验、权限控制、审计日志很难插入。显式注册可以把这些横切关注点统一处理。Component public class WeatherTool implements AgentTool { Override public String name() { return query_weather; } Override public String description() { return 查询指定城市指定日期的天气参数格式城市|日期(YYYY-MM-DD); } Override public ToolResult execute(String input) { // 参数解析与校验 String[] parts input.split(\\|); if (parts.length ! 2) { return ToolResult.error(参数格式错误应为城市|日期); } // 权限校验、限流、日志 // 实际调用天气 API return ToolResult.success(weatherData); } }工具描述description的写法极其关键。模型是根据描述来决定调用哪个工具的描述写得模糊模型就会乱调。我的经验是描述里要包含工具能做什么、参数格式、返回什么、什么情况下用。这四点写清楚工具调用的准确率能提升一大截。3. 实操落地从零搭建一个可用的 Java Agent3.1 环境准备与依赖配置先明确技术栈Spring Boot 3.2 Spring AI 1.0 LangChain4j 0.35 通义千问通过兼容 OpenAI 协议的接口接入。选这个组合是因为 Spring Boot 提供工程骨架Spring AI 做模型接入LangChain4j 做 Agent 编排通义千问国内访问稳定且成本可控。Maven 依赖核心部分dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-reactor/artifactId version0.35.0/version /dependency配置文件里配置模型接入信息注意 API Key 要走环境变量不要硬编码spring: ai: openai: base-url: ${AI_BASE_URL} api-key: ${AI_API_KEY} chat: options: model: qwen-plus temperature: 0.7 max-tokens: 2000注意temperature 设 0.7 是通用场景的折中值。做工具调用和结构化输出时建议降到 0.1-0.3减少模型“发挥”导致格式错误。3.2 工具层的设计与实现工具层是 Agent 的能力边界。我一般把工具分成三类查询类只读可缓存、操作类有副作用需幂等、计算类纯函数无副作用。查询类工具比如查数据库、调外部 API重点是加缓存和超时控制。操作类工具比如发消息、写数据库重点是幂等设计和权限校验。计算类工具比如日期计算、格式转换重点是纯函数无状态。Component public class ToolRegistry { private final MapString, AgentTool tools new ConcurrentHashMap(); PostConstruct public void init() { register(new WeatherTool()); register(new DatabaseQueryTool()); register(new NotificationTool()); } public void register(AgentTool tool) { tools.put(tool.name(), tool); } public ToolResult execute(String name, String input) { AgentTool tool tools.get(name); if (tool null) { return ToolResult.error(未知工具 name); } try { return CompletableFuture .supplyAsync(() - tool.execute(input), toolExecutor) .get(10, TimeUnit.SECONDS); } catch (TimeoutException e) { return ToolResult.error(工具执行超时); } catch (Exception e) { return ToolResult.error(工具执行异常 e.getMessage()); } } }这里用独立的线程池toolExecutor执行工具和 Web 请求线程隔离。工具执行超时设 10 秒超过就返回错误让模型决定下一步。这个设计在“ai agent 怎么扛并发”的场景下特别重要——工具执行慢不能拖垮整个请求链路。3.3 RAG 知识库的接入与多路召回Agent 要回答专业问题必须接知识库。LangChain4j 的 RAG 支持很完善但直接用默认配置效果往往一般。我的经验是要做多路召回 重排序。多路召回的意思是同时用向量检索、关键词检索、元数据过滤三种方式召回候选文档然后合并去重。向量检索擅长语义相似关键词检索擅长精确匹配元数据过滤擅长范围限定三者互补。public class HybridRetriever { private final EmbeddingStoreTextSegment vectorStore; private final KeywordSearcher keywordSearcher; public ListTextSegment retrieve(String query, int topK) { // 向量召回 ListTextSegment vectorResults vectorStore.search( EmbeddingSearchRequest.builder() .queryEmbedding(embeddingModel.embed(query).content()) .maxResults(topK) .build() ).matches().stream().map(m - m.embedded()).toList(); // 关键词召回 ListTextSegment keywordResults keywordSearcher.search(query, topK); // 合并去重 MapString, TextSegment merged new LinkedHashMap(); vectorResults.forEach(s - merged.put(s.text(), s)); keywordResults.forEach(s - merged.putIfAbsent(s.text(), s)); // 重排序可用模型或规则 return rerank(new ArrayList(merged.values()), query, topK); } }重排序这一步很多人省掉但实测下来对准确率影响很大。简单做法是用一个小的交叉编码模型打分复杂做法是再调一次大模型做相关性判断。预算有限的话用规则重排关键词命中数 向量相似度加权也能有明显提升。3.4 完整 Agent 服务的组装与启动把模型、工具、知识库、ReAct 循环组装起来形成一个完整的 Agent 服务Service public class AgentService { private final ChatLanguageModel model; private final ToolRegistry toolRegistry; private final HybridRetriever retriever; public String chat(String sessionId, String userInput) { // 1. 检索相关知识 ListTextSegment context retriever.retrieve(userInput, 5); // 2. 构建 Prompt String systemPrompt buildSystemPrompt(context); // 3. 执行 ReAct 循环 ReActAgent agent new ReActAgent(model, toolRegistry, systemPrompt); String result agent.run(userInput); // 4. 记录日志与指标 metrics.record(sessionId, userInput, result); return result; } }启动后先用简单问题测试比如“现在几点了”看模型是否正确调用时间工具。再用复杂问题测试多步推理比如“查一下我上个月的订单总额如果超过 1000 就发个提醒”。逐步增加复杂度每步都验证工具调用日志。4. 生产化改造让 Agent 真正扛住流量4.1 并发控制与线程模型设计“ai agent 怎么扛并发”是热搜里的高频问题。Agent 的并发瓶颈不在模型调用本身那是 IO 等待而在上下文管理和工具执行。我的方案是三层隔离Web 请求线程池、Agent 执行线程池、工具执行线程池。Web 层用 Spring 默认的 Tomcat 线程池Agent 执行用独立的ThreadPoolTaskExecutor工具执行再用一个更小的池。这样任何一层慢都不会拖垮其他层。Configuration public class ExecutorConfig { Bean(agentExecutor) public ThreadPoolTaskExecutor agentExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setThreadNamePrefix(agent-); executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } Bean(toolExecutor) public ThreadPoolTaskExecutor toolExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(30); executor.setQueueCapacity(50); executor.setThreadNamePrefix(tool-); executor.initialize(); return executor; } }队列容量和拒绝策略要结合业务定。Agent 请求通常耗时较长几秒到几十秒队列不能太大否则用户等太久。CallerRunsPolicy在队列满时让调用线程自己执行起到背压作用。4.2 可观测性日志、指标与链路追踪Agent 的黑盒特性让排查问题非常痛苦。用户说“它答错了”你根本不知道是检索错了、工具调错了还是模型推理错了。所以全链路追踪是必须的。我在每个 Agent 执行时生成一个 traceId把每步的 Thought、Action、Observation 都记录下来。用 MDC 把 traceId 打进日志配合 ELK 就能完整还原一次执行过程。public String run(String userGoal) { String traceId UUID.randomUUID().toString(); MDC.put(traceId, traceId); try { for (int i 0; i MAX_ITERATIONS; i) { long start System.currentTimeMillis(); String response llmClient.chat(history); log.info(iteration{}, thought{}, cost{}ms, i, response, System.currentTimeMillis() - start); // ... } } finally { MDC.clear(); } }关键指标要采集每次执行的迭代次数、模型调用耗时、工具调用耗时、token 消耗、失败率。这些指标能帮你快速定位性能瓶颈和成本异常。4.3 成本控制与安全护栏Agent 的成本很容易失控因为一次用户请求可能触发多次模型调用。我踩过的坑一个没设上限的 Agent 因为工具返回格式错误模型反复重试单次请求调了 30 多次模型成本是正常请求的 20 倍。控制手段有三个迭代次数上限硬性建议 10 次以内、token 预算累计 token 超阈值就终止、工具调用频率限制同一工具连续调用超过 N 次就熔断。安全护栏方面重点是输入过滤和输出审核。输入侧要防止 Prompt 注入比如用户输入“忽略之前的指令”这类内容。输出侧要过滤敏感信息尤其是工具返回的数据里可能包含隐私字段。public class SafetyGuard { private static final ListString INJECTION_PATTERNS List.of( 忽略之前, ignore previous, system prompt, 你现在是 ); public void checkInput(String input) { for (String pattern : INJECTION_PATTERNS) { if (input.toLowerCase().contains(pattern.toLowerCase())) { throw new SecurityException(检测到潜在注入攻击); } } } }这套规则不可能覆盖所有情况但能挡住大部分低级攻击。生产环境建议再叠一层模型审核用一个小模型判断输入是否可疑。5. 常见问题与排查技巧实录5.1 工具调用失败的排查思路工具调用失败是最常见的问题表现是模型不调用工具、调用错工具、或者调用参数错误。排查要按顺序来。先看工具描述是否清晰。模型是根据 description 选工具的描述模糊必然选错。我一般要求描述里包含“功能 参数格式 使用场景”三要素。再看 Prompt 里是否给了足够的上下文。如果系统提示词里没说“你可以使用工具”模型可能根本不知道有工具可用。最后看模型能力。小模型7B 以下的工具调用能力普遍较弱复杂场景建议用 32B 以上的模型。实测下来通义千问 plus 和 GPT-4 级别的模型在工具调用上表现稳定小模型经常格式错乱。问题现象可能原因排查方法模型不调用工具描述不清 / Prompt 未提示检查工具描述和系统提示词调用错误工具工具描述重叠区分各工具的描述关键词参数格式错误描述未说明格式在描述中明确参数示例工具执行超时外部依赖慢加超时和降级逻辑循环调用同一工具停止条件缺失加迭代上限和重复检测5.2 上下文超长的处理策略Agent 多轮循环后history 会越来越长最终超出模型上下文窗口。处理策略有三种滑动窗口只保留最近 N 轮、摘要压缩把早期对话总结成一段话、关键信息提取只保留工具调用结果丢弃中间推理。我一般用滑动窗口 关键信息提取的组合。保留最近 5 轮完整对话更早的只保留工具调用结果。这样既控制了长度又不丢失关键信息。private ListMessage compressHistory(ListMessage history, int maxTokens) { if (estimateTokens(history) maxTokens) { return history; } ListMessage compressed new ArrayList(); compressed.add(history.get(0)); // system prompt 保留 int start Math.max(1, history.size() - 10); for (int i start; i history.size(); i) { Message msg history.get(i); if (msg.isToolResult() || i history.size() - 5) { compressed.add(msg); } } return compressed; }5.3 模型输出格式不稳定的应对模型偶尔会不按格式输出比如该返回 JSON 却返回了自然语言。应对方法是多重解析 重试。先尝试标准解析失败后用正则提取再失败就重新调用模型并强调格式要求。public AgentStep parseStepWithRetry(String response, int maxRetry) { for (int i 0; i maxRetry; i) { try { return standardParse(response); } catch (ParseException e) { response llmClient.chat( 请严格按照 JSON 格式重新输出只输出 JSON不要有其他内容 response ); } } throw new IllegalStateException(无法解析模型输出); }重试次数不要超过 2 次否则成本会失控。如果重试还失败直接返回错误让用户重试比无限重试更划算。5.4 实操避坑清单最后整理一份我踩过的坑都是文档里不会写的不要在循环里创建新的模型客户端。每次 new 一个客户端会重复建立连接性能极差。客户端要单例复用。工具执行一定要设超时。外部 API 挂起时没有超时的工具会一直阻塞线程最终拖垮整个服务。history 里的工具结果要截断。有些工具返回几万字的 JSON直接塞进 history 会瞬间撑爆上下文。截断到合理长度比如 2000 字符。temperature 在工具调用场景要调低。0.7 的 temperature 会让模型在参数生成时“发挥创意”导致格式错误。工具调用场景建议 0.1。日志里不要打印完整 Prompt。Prompt 里可能包含用户隐私和 API Key日志脱敏是基本要求。多 Agent 协作要设总预算。Agent A 调 Agent BB 又调 C成本会指数级增长。每个 Agent 都要有独立的 token 预算和迭代上限。提示上线前一定要做压测重点测工具超时、模型限流、上下文超长这三种异常场景。正常流程跑通不代表生产可用异常处理才是生产化的关键。这套东西我从零搭到稳定运行大概花了两个月中间踩的坑基本都写在上面的清单里了。Java 工程师转型 AI Agent技术栈的迁移其实不难难的是思维方式的转变——从“确定性流程”转向“概率性推理 工程兜底”。把工程能力用在给模型的不确定性兜底上这才是 Java 工程师在这个方向上的核心竞争力。
返回列表