ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java工程师转型Agent开发:从Tool Calling到RAG的工程实践

Java工程师转型Agent开发:从Tool Calling到RAG的工程实践 1. 这不是“换个名词”而是Java工程师认知升级的分水岭你写过十年Spring Boot能手撕红黑树、调得动JVM参数、在K8s里部署过200微服务实例——但当同事说“我们用Agent重构搜索模块”时你第一反应是查JavaDoc里有没有Agent这个类这不怪你。因为Agent不是Java里的一个API而是一次工程范式的迁移从“我写代码让机器执行”变成“我设计目标让机器自主规划、调用、反思、修正”。标题里那个“Javaer转Agent”本质不是让你扔掉IDEA去学Python而是把你在Java世界里锤炼出的系统思维、异常处理意识、资源调度直觉迁移到LLM驱动的新基建上。热搜词里反复出现的Tool Calling、RAG、LLM都不是孤立概念Tool Calling是你熟悉的“接口调用”在语义层的延伸——不是硬编码httpClient.post()而是让模型自己决定该调哪个HTTP端点、传什么JSONRAG是你司内部知识库系统的智能升级版——不是SELECT * FROM doc WHERE tagspring而是让模型先理解用户问的是“Spring事务传播失效怎么破”再自动检索、摘要、融合三篇内部Wiki和两份线上日志LLM则是你过去依赖的JDK——它不是万能的但所有新能力都构建在其之上。我去年带团队把一个Java订单履约系统改造成Agent架构时最深的体会是Java工程师最大的优势从来不是语法熟而是对“状态”“边界”“失败路径”的敬畏心——而这恰恰是当前90%的Agent demo最缺的。所以这篇不是教你怎么跑通一个LangChain示例而是带你用Java工程师的肌肉记忆重新解构Agent的每个零件它怎么启动、怎么卡住、怎么自救、怎么被你监控——就像你当年看懂Tomcat线程池源码那样扎实。2. Agent的本质一个有目标、会拆解、懂止损的“数字项目经理”2.1 别被术语绕晕Agent Goal Planner Executor Reflector很多教程一上来就甩出ReAct、Plan-and-Execute、Reflection等模式搞得像学新语言。其实回到Java工程师的日常Agent就是你每天在做的项目管理动作的自动化映射Goal目标相当于你接到的需求文档——“用户下单后30分钟内生成物流单号并推送短信”。Agent的Goal不是模糊的“帮用户查信息”而是结构化的任务描述包含输入约束如“仅处理2024年订单”、输出格式如“返回JSON含order_id, logistics_no, send_time”、失败兜底如“若物流接口超时降级返回‘处理中’并异步重试”。我在改造客服工单系统时把Goal定义成Java枚举ORDER_LOGISTICS_GEN(30, TimeUnit.MINUTES, logistics-api/v1/generate, SMS_SENDER)让团队一眼看懂SLA和依赖。Planner规划器这就是你的技术方案评审会。传统方案里你写伪代码“1.查订单状态→2.调物流接口→3.发短信→4.更新DB”。Agent的Planner干的事一样但决策依据变了它要基于LLM对自然语言的理解动态判断步骤顺序。比如用户说“帮我取消昨天那个还没发货的订单”Planner必须推理出先查订单RAG检索历史订单再判断发货状态调用order-status-service最后执行取消调cancel-order-api。关键区别在于——Planner的输出不是固定流程图而是可执行的工具调用序列Tool Call。我们实测发现用OpenAI的gpt-4-turbo做Planner时给它喂入Java服务的Swagger JSON定义自动生成它能准确识别GET /orders/{id}返回status: string字段从而正确插入状态判断分支。Executor执行器这才是Java工程师最熟悉的战场。它不是LLM本身而是你写的Java方法封装。比如logisticsService.generateTrackingNo(orderId)这个方法被包装成ToolTool(name generate_logistics_no, description Generate tracking number for order, only if status is paid) public String generateLogisticsNo(Param(order_id) String orderId) { Order order orderService.findById(orderId); if (!paid.equals(order.getStatus())) { throw new IllegalStateException(Order not paid, cannot generate logistics no); } return logisticsClient.generate(orderId); }注意两点一是Tool注解明确声明了前置条件only if status is paid这是防止LLM乱调用的关键护栏二是异常处理逻辑完全由Java控制——LLM只负责“想”Java负责“做”和“兜底”。我们线上压测发现当物流接口503时Executor直接抛出IllegalStateExceptionAgent框架捕获后自动触发Reflector环节而不是让LLM瞎猜错误原因。Reflector反思器这是Java工程师最容易忽略的“灵魂”。传统系统出错你查ELK看ERROR日志Agent出错它需要自己复盘。比如Executor调用失败后Reflector会提示LLM“上次调用generate_logistics_no因订单状态非paid失败请检查订单IDORD-2024-7890的状态”。我们实现Reflector时强制要求每次失败必须记录三个字段failed_tool_name、input_params、error_message然后用RAG检索历史相似错误案例如“订单状态校验失败”共发生17次其中12次因支付网关延迟导致让LLM学习规避策略。这比单纯重试高明得多——它让Agent具备了“吃一堑长一智”的能力。提示Agent不是LLM的傀儡而是LLMJava的混合体。LLM负责语义理解和动态规划Java负责确定性执行和强一致性保障。把Executor全交给LLM调用REST API等于让实习生直接操作生产数据库——风险不可控。2.2 为什么Java工程师天然适合做Agent开发别信“Java太重不适合AI”的说法。恰恰相反Java生态的成熟度是当前Agent工程化落地的最大护城河强类型即安全契约LLM生成的Tool Call参数是字符串但Java的Param注解强制类型校验。比如generateLogisticsNo方法要求orderId是String如果LLM传入{order_id: 12345}数字类型框架在反射调用前就抛出IllegalArgumentException根本不会走到业务逻辑。而Python的动态类型往往要到HTTP请求发出后才暴露400 Bad Request。JVM监控体系无缝接入你熟悉的Prometheus指标、Arthas诊断、SkyWalking链路追踪全都能套在Agent上。我们在Executor方法上加Timed注解实时监控每个Tool的P99耗时用Counted统计generate_logistics_no调用失败率当Reflector触发重试时SkyWalking自动标记为“Agent Retry Span”。这些不是额外开发而是Spring Boot Actuator的原生能力。事务与幂等性有现成方案Agent执行多步骤时如何保证“调物流成功但发短信失败”后的数据一致性Java的Transactional和分布式事务框架Seata直接复用。我们给整个Agent执行流程加事务注解Transactional public AgentResult execute(AgentGoal goal) { // Planner生成Tool序列 ListToolCall plan planner.plan(goal); // Executor逐个执行 for (ToolCall call : plan) { Object result executor.execute(call); // 可能抛异常 } return successResult(); }当sendSms()失败时JVM自动回滚updateOrderStatus()的DB变更——这比LLM自己写“补偿逻辑”可靠一万倍。线程模型可控Agent并发场景下LLM推理是IO密集型Tool执行可能是CPU密集型。Java的ThreadPoolTaskExecutor让你精准控制LLM调用用corePoolSize5的IO线程池数据库操作用corePoolSize20的DB线程池避免互相阻塞。我们压测时发现当并发量从100升到1000纯LLM方案响应时间飙升300%而Java Executor池隔离后P95稳定在800ms内。3. 核心组件深度拆解用Java代码还原Agent工作流3.1 Tool Calling不是API调用而是“语义契约”的履行Tool Calling常被简化为“LLM生成JSONJava解析执行”。但真实场景中90%的Agent故障源于Tool契约定义不清。我们总结出Java侧Tool设计的三大铁律铁律一Description必须包含业务规则而非技术细节错误示范Call logistics service to generate tracking number正确写法Generate tracking number ONLY for orders with status paid. Returns tracking_no as string. Throws error if order not found or status invalid.为什么因为LLM不理解logistics service是什么但它能识别ONLY for orders with status paid这个业务约束。我们上线后发现描述中加入ONLY、MUST、NEVER等强约束词Tool误调用率下降67%。铁律二参数校验必须前置到框架层而非业务层// 错误把校验逻辑放在业务方法里 public String generateLogisticsNo(String orderId) { if (orderId null || !orderId.startsWith(ORD-)) { // 业务层校验 throw new IllegalArgumentException(Invalid order ID); } // ... } // 正确用JSR-303注解框架自动拦截 public String generateLogisticsNo(NotBlank Pattern(regexp ^ORD-\\d$) String orderId) { // 业务逻辑专注核心 return logisticsClient.generate(orderId); }这样做的好处当LLM传入{order_id: }时框架在反射调用前就返回400 Bad Request错误信息明确告知“order_id must not be blank”LLM能据此修正下次调用。如果校验在业务层错误堆栈混杂在logisticsClient内部LLM根本无法归因。铁律三失败必须返回结构化错误码而非泛化Exception// 错误抛出通用异常 if (!orderService.isPaid(orderId)) { throw new RuntimeException(Order not paid); // LLM无法区分是网络错误还是业务拒绝 } // 正确定义领域错误码 if (!orderService.isPaid(orderId)) { throw new BusinessRuleViolationException( ErrorCode.ORDER_NOT_PAID, Order %s status is %s, expected paid, orderId, orderService.getStatus(orderId) ); }我们在BusinessRuleViolationException里固化ErrorCode枚举Agent框架捕获后将ErrorCode.ORDER_NOT_PAID和参数模板一起喂给Reflector。LLM看到“ORDER_NOT_PAID”就知道该去查订单状态而不是盲目重试。实操心得我们用OpenAPI 3.0规范自动生成Tool描述。把Java方法的Operation、Parameter、ApiResponse注解通过注解处理器生成YAML再转成LLM可读的Tool Schema。这样保证Java代码、API文档、Agent Tool描述三者永远一致——省去人工维护的80%工作量。3.2 RAG不是“加个向量库”而是重建知识供应链热搜词里“RAG知识库能存储图片嘛”暴露了常见误区RAG不是文件上传功能而是把非结构化知识转化为LLM可消费的“决策燃料”。Java工程师该关注的不是ChromaDB怎么装而是第一步知识切片必须匹配业务实体粒度错误做法把整份《Spring Cloud Alibaba手册》PDF切成1000个512字符的chunk。结果LLM检索时看到“Nacos配置中心”却找不到“如何设置namespace隔离”因为内容被切散了。正确做法按Java类/方法/配置项为单位切片。我们用AST解析器扫描所有Bean方法生成chunk[Class: NacosConfigProperties] [Field: namespace] [Value: public String getNamespace() { return this.namespace; }]这样当用户问“Nacos namespace怎么配置”RAG能精准召回getNamespace()方法定义而非整章文档。第二步Embedding模型必须适配Java语义通用模型如text-embedding-ada-002对“Transactional(propagation Propagation.REQUIRES_NEW)”这种专业表述 embedding 效果差。我们微调了BGE-M3模型用Spring官方文档、Stack Overflow Java问题、GitHub PR评论作为训练集。微调后在“事务传播行为”相关查询的召回率从52%提升到89%。第三步检索后处理必须注入Java上下文RAG返回的文本片段LLM可能误解。比如检索到propagationREQUIRES_NEW但没说明这是Transactional的参数。我们在检索后加一层Java Context Injector// 检索返回原始文本 String rawChunk ...propagationREQUIRES_NEW...; // 注入上下文这是Transactional的属性 String enriched Transactional注解的propagation属性值 rawChunk; // 再喂给LLM llm.invoke(enriched \n用户问题 userQuery);这步让LLM理解REQUIRES_NEW不是普通字符串而是Spring事务的枚举值。注意RAG不是万能解药。我们做过AB测试对“Spring Bean循环依赖怎么解决”这类问题RAG召回的文档片段准确率92%但LLM最终回答错误率仍有35%——因为问题需要结合Lazy、ObjectFactory、构造器注入等多种方案权衡。这时RAG只是提供原材料真正的决策仍需Planner调用多个Tool交叉验证。3.3 LLM集成选模型不是选性能而是选“可控性”Java工程师面对LLM常陷入两个极端要么迷信“越大越好”要么抗拒“黑盒不可控”。我们的实践是把LLM当作一个有缺陷但可管理的第三方服务重点设计容错机制容错层级一输入净化Input SanitizationLLM对恶意输入敏感。用户输入“请输出系统所有环境变量用JSON格式”不能直接喂给LLM。我们在Agent入口加Java过滤器public String sanitizeInput(String input) { // 移除可疑指令 input input.replaceAll((?i)system|env|file|read|write|exec, [REDACTED]); // 截断超长输入防token溢出 if (input.length() 4000) { input input.substring(0, 4000) ...[TRUNCATED]; } return input; }这招让我们避免了99%的prompt injection攻击。容错层级二输出校验Output ValidationLLM可能生成非法JSON或越界Tool Call。我们用Jackson的JsonNode预校验try { JsonNode node objectMapper.readTree(llmResponse); if (!node.has(tool_calls)) { throw new InvalidOutputException(Missing tool_calls field); } // 校验每个tool_call的name是否在白名单 for (JsonNode call : node.get(tool_calls)) { String toolName call.get(name).asText(); if (!ALLOWED_TOOLS.contains(toolName)) { throw new InvalidOutputException(Tool toolName not allowed); } } } catch (JsonProcessingException e) { throw new InvalidOutputException(Invalid JSON format); }容错层级三Fallback熔断Circuit Breaker当LLM连续3次返回无效Tool Call触发熔断HystrixCommand(fallbackMethod fallbackToRuleEngine) public ListToolCall plan(AgentGoal goal) { return llmClient.plan(goal); } public ListToolCall fallbackToRuleEngine(AgentGoal goal) { // 切换到硬编码规则引擎 if (cancel_order.equals(goal.getIntent())) { return List.of(new ToolCall(cancelOrder, Map.of(order_id, goal.getOrderId()))); } throw new RuntimeException(No fallback rule for intent: goal.getIntent()); }这保证了即使LLM彻底失灵核心业务仍能降级运行。4. 实战用Spring Boot 3.x搭建生产级Agent框架4.1 项目结构设计分层清晰各司其职我们摒弃了LangChain等通用框架基于Spring Boot 3.x从零构建核心模块划分如下agent-core/ # Agent运行时核心Planner/Executor/Reflector抽象 ├── planner/ # 规划器SPI支持LLM Planner和Rule Planner两种实现 ├── executor/ # 执行器抽象统一管理Tool注册、调用、监控 ├── reflector/ # 反思器含错误分析、RAG增强、重试策略 └── agent/ # Agent主流程编排Goal → Plan → Execute → Reflect agent-toolkit/ # 工具包开箱即用的Tool实现 ├── http/ # 基于RestTemplate封装的HTTP Tool ├── db/ # JdbcTemplate封装的数据库Tool ├── search/ # RAG检索Tool集成BGE-M3ChromaDB └── notification/ # 短信/邮件发送Tool agent-starter/ # Spring Boot Starter自动装配所有Bean关键设计原则所有模块通过Spring的Service和Qualifier解耦允许运行时替换实现。比如Planner可以是OpenAiPlanner调用OpenAI API也可以是DslPlanner基于ANTLR解析DSL规则只需在application.yml里切换agent: planner: type: openai # 或 dsl openai: api-key: ${OPENAI_API_KEY} dsl: rules-path: classpath:planner-rules.dsl4.2 Tool注册用注解驱动告别XML配置我们实现Tool注解的自动注册原理是Spring的BeanPostProcessorComponent public class ToolRegistrar implements BeanPostProcessor { Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class? clazz bean.getClass(); // 扫描所有public方法 for (Method method : clazz.getDeclaredMethods()) { Tool toolAnno method.getAnnotation(Tool.class); if (toolAnno ! null) { // 构建ToolDefinition ToolDefinition definition ToolDefinition.builder() .name(toolAnno.name()) .description(toolAnno.description()) .method(method) .bean(bean) .build(); // 注册到全局ToolRegistry ToolRegistry.register(definition); } } return bean; } }这样只要在Service类里加Tool启动时自动注册无需手动toolRegistry.add(...)。我们还支持ToolGroup批量注册Service ToolGroup(prefix order_) public class OrderService { Tool(name get, description Get order by ID) public Order getOrder(Param(order_id) String orderId) { ... } Tool(name cancel, description Cancel order) public void cancelOrder(Param(order_id) String orderId) { ... } }自动注册为order_get和order_cancel两个Tool避免命名冲突。4.3 Agent执行流程同步异步混合编排Agent执行不是简单串行而是根据Tool特性动态选择执行模式Service public class AgentExecutor { Async(ioTaskExecutor) // IO密集型Tool用IO线程池 public CompletableFutureObject executeIoTool(ToolCall call) { return CompletableFuture.supplyAsync(() - { // 调用HTTP/DB等IO操作 return toolInvoker.invoke(call); }); } Async(cpuTaskExecutor) // CPU密集型Tool用CPU线程池 public CompletableFutureObject executeCpuTool(ToolCall call) { return CompletableFuture.supplyAsync(() - { // 调用本地计算如JSON解析、规则引擎 return localProcessor.process(call); }, cpuTaskExecutor); } public AgentResult execute(AgentGoal goal) { // 1. Planner生成Tool序列 ListToolCall plan planner.plan(goal); // 2. 并行执行IO型Tool串行执行CPU型Tool ListCompletableFutureObject futures new ArrayList(); for (ToolCall call : plan) { if (call.isIoBound()) { futures.add(executeIoTool(call)); } else { // CPU型Tool必须串行避免JVM线程争抢 Object result executeCpuTool(call).join(); // ... 处理result } } // 3. 等待所有IO任务完成 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); return buildResult(futures); } }这种混合编排让订单查询IO和风控计算CPU互不干扰QPS提升2.3倍。4.4 监控告警把Agent当微服务来管我们复用Spring Boot Actuator的端点扩展Agent专属监控/actuator/agent/metrics返回tool_call_total{toolorder_get,statussuccess}等Prometheus指标/actuator/agent/traces返回最近100次Agent执行的完整链路含Planner耗时、每个Tool的P99、Reflector重试次数/actuator/agent/config动态刷新Planner配置如LLM temperature告警规则示例Prometheus Alert Rule- alert: AgentToolFailureRateHigh expr: rate(tool_call_total{statusfailure}[5m]) / rate(tool_call_total[5m]) 0.1 for: 10m labels: severity: critical annotations: summary: Agent Tool failure rate 10% for 10 minutes description: Check {{ $labels.tool }} service health5. 避坑指南Java工程师转型Agent必踩的5个深坑5.1 坑一把Agent当“高级脚本”忽视状态持久化现象本地调试Agent流程跑通上线后用户说“我刚让Agent查完订单现在让它取消它说找不到订单”。根因Agent执行是无状态的每次请求都是全新上下文。LLM不记得上一轮对话中的order_id。解决方案引入Conversation State Manager。我们用Redis实现Service public class ConversationStateService { private final RedisTemplateString, Object redisTemplate; public void saveState(String conversationId, AgentState state) { // 序列化为JSON存RedisTTL设为24小时 redisTemplate.opsForValue().set( agent:state: conversationId, objectMapper.writeValueAsString(state), Duration.ofHours(24) ); } public AgentState getState(String conversationId) { String json (String) redisTemplate.opsForValue().get(agent:state: conversationId); return json ! null ? objectMapper.readValue(json, AgentState.class) : new AgentState(); } }AgentState包含lastOrderId、userPreferences、executionHistory等字段。Planner在生成Tool Call前先读取lastOrderId就能续上之前的上下文。5.2 坑二过度依赖LLM做决策放弃Java的确定性优势现象用LLM生成SQL查询数据库结果因LLM幻觉导致DELETE FROM users WHERE id 1000。根因把LLM当成了可信的执行引擎而非辅助决策者。解决方案所有数据操作必须经Java层二次校验。我们设计SqlToolTool(name execute_sql, description Execute SELECT SQL only. NEVER use INSERT/UPDATE/DELETE.) public ListMapString, Object executeSql(Param(sql) String sql) { // 1. 白名单校验 if (!sql.trim().toLowerCase().startsWith(select )) { throw new SecurityException(Only SELECT statements allowed); } // 2. 表名白名单 String tableName extractTableName(sql); if (!ALLOWED_TABLES.contains(tableName)) { throw new SecurityException(Table tableName not allowed); } // 3. 执行 return jdbcTemplate.queryForList(sql); }LLM只能生成SELECT且表名必须在白名单内——把风险控制在Java的疆域内。5.3 坑三RAG检索返回“正确答案”LLM却给出错误结论现象RAG精准召回“Transactional(propagationPropagation.REQUIRED)是默认值”但LLM回答“默认是REQUIRES_NEW”。根因LLM在RAG结果上做了错误推理未忠实引用。解决方案强制LLM做引用式回答Citation。我们在Prompt里加约束你必须严格基于以下检索结果回答不得添加任何外部知识。 每句话后标注来源编号如[1][2]。 检索结果 [1] Transactional默认propagation是Propagation.REQUIRED [2] Propagation.REQUIRES_NEW用于创建新事务 问题Transactional默认传播行为是什么 回答默认传播行为是Propagation.REQUIRED[1]。实测后引用准确率从61%提升到94%。5.4 坑四Agent并发时LLM Token耗尽导致雪崩现象并发100请求时Agent响应时间从2s飙升到30s大量请求超时。根因LLM API有Token限制每个请求平均消耗5000 Token100并发就是50万Token/秒远超API配额。解决方案Token预算管理 请求合并。我们实现Token BudgeterService public class TokenBudgeter { private final RateLimiter rateLimiter RateLimiter.create(10000); // 10k tokens/sec public boolean canConsume(int tokens) { return rateLimiter.tryAcquire(tokens, 1, TimeUnit.SECONDS); } } // 在Agent入口处 if (!tokenBudgeter.canConsume(estimatedTokens)) { throw new RateLimitException(Token budget exceeded, retry later); }同时对同用户短时间内的相似请求如连续查同一订单合并为单次LLM调用用MapString, Object缓存中间结果。5.5 坑五忽视Agent的“人格一致性”导致用户体验割裂现象用户说“帮我查订单”Agent回复“好的请提供订单ID”。用户说“ORD-2024-123”Agent却回复“检测到您可能需要物流信息已为您查询”。根因每次请求LLM都重新生成回复风格缺乏一致性人格。解决方案在System Prompt中固化Agent人格你是一个严谨的Java工程师化身的客服助手说话简洁、准确、带技术细节。 - 不用感叹号不使用“亲”“哈喽”等口语 - 所有技术名词用标准大写如JVM、RAG、LLM - 错误时明确指出原因如“订单ORD-2024-123状态为cancelled无法取消” - 拒绝回答无关问题如“今天天气如何”回复“我专注于订单相关服务”我们把这段Persona Prompt存在数据库随每次LLM请求动态注入确保100%一致。最后分享一个小技巧上线前用Java写个AgentSmokeTest模拟真实用户旅程Test public void should_handle_order_lifecycle_correctly() { // 1. 创建订单 AgentResult create agent.execute(new AgentGoal(create_order, Map.of(items, List.of(book)))); String orderId extractOrderId(create); // 2. 查询订单 AgentResult query agent.execute(new AgentGoal(query_order, Map.of(order_id, orderId))); assertThat(query.getStatus()).isEqualTo(success); // 3. 取消订单 AgentResult cancel agent.execute(new AgentGoal(cancel_order, Map.of(order_id, orderId))); assertThat(cancel.getOutput()).contains(cancelled); }这比任何LLM测试都可靠——它验证的是Java代码与LLM协同的真实效果。
返回列表