1. 这不是转行,是能力栈的结构性升级:一个Java老手的真实Agent工程化路径
“Java工程师转型Agent Engineer”这个标题最近在技术社区刷屏,但很多人第一反应是:又一个蹭热点的噱头?我干了十二年Java后端,从Spring Boot 1.x写到Spring Boot 3.x,从单体架构踩坑到K8s集群调优,去年开始带团队做AI原生服务时,才真正意识到——这不是换语言、换框架,而是整个工程思维范式的迁移。Agent Engineer不是“会调用LLM API的人”,而是能设计可观察、可调试、可灰度、可回滚的智能体系统的新型系统工程师。它要求你把过去十年练就的JVM调优直觉、线程池压测经验、分布式事务兜底能力,全部迁移到“提示稳定性治理”“工具调用链路追踪”“记忆状态一致性校验”这些新战场上。这个24周计划,是我和三个同样背景的Java同事,在真实项目中边落地边反向提炼出来的路线图。它不教你怎么写“你好,我是AI助手”这种Demo,而是从第1周第1天起,就让你用Java熟悉的思维去解构Agent:把Agent看作一个带状态机的Spring Bean,把Orchestration看作增强版的Spring Cloud Gateway路由规则,把Tool Calling看作Dubbo泛化调用的语义升级。关键词里没有“大模型”“AIGC”这种虚词,只有“状态持久化”“错误传播策略”“重试退避算法”——这才是Java人听得懂的语言。适合谁?不是刚毕业的校招生,而是有3年以上Java Web开发经验、写过至少两个中型服务、被OOM和死锁折磨过的实战派。如果你还在纠结“要不要学Python”,那这计划的第一课就会告诉你:用Spring AI + LangChain4j,你连一行Python都不用写,就能跑通完整的ReAct Agent闭环。
2. 整体设计逻辑:为什么必须用Java人的节奏来学Agent工程
2.1 拒绝“从零开始”的幻觉:复用Java工程能力是最大杠杆
很多转型计划失败,是因为默认学习者要先成为Python高手、再学Prompt Engineering、最后啃LangChain源码。这完全违背工程常识。一个Java工程师的核心资产是什么?不是对HashMap底层的理解,而是对系统可观测性、资源隔离、故障注入、契约演进的肌肉记忆。Agent系统恰恰是这些能力的超级放大器:
- 可观测性:传统微服务日志只记录HTTP状态码,而Agent需要记录每一步Thought、Action、Observation的完整Trace,还要支持跨工具调用的Span关联;
- 资源隔离:LLM调用不是无状态的HTTP请求,一次Agent执行可能涉及5次不同模型的并发调用,必须像管理线程池一样管理Token预算和并发数;
- 故障注入:当Tool返回格式错误时,不能简单抛RuntimeException,而要触发预设的Fallback策略(如降级到本地规则引擎),这和Hystrix熔断逻辑一脉相承;
- 契约演进:Tool的OpenAPI Schema变更,必须像Spring Cloud Contract那样做双向兼容测试,否则Agent的Action解析会静默失败。
所以本计划的底层逻辑是:用Java工程范式重新定义Agent开发流程。第1-4周不碰任何LLM,而是用纯Java模拟Agent生命周期——自己实现StatefulAgent类,用ConcurrentHashMap存Session Memory,用ScheduledExecutorService做Timeout控制,用自定义注解标记@ToolMethod。当你用Java写出第一个能自动重试、自动降级、自动记录Trace的Mock Agent时,再接入真实LLM,认知落差会小到可以忽略。
2.2 24周不是线性时间表,而是三层能力跃迁的螺旋上升
我把24周拆成三个阶段,每个阶段都包含“理论输入→Java代码验证→真实场景压测”闭环:
| 阶段 | 周数 | 核心目标 | Java人最熟悉的对标物 | 关键交付物 |
|---|---|---|---|---|
| 筑基期 | 第1-8周 | 建立Agent系统观,掌握状态管理、工具编排、错误处理三大支柱 | Spring Bean生命周期 + Dubbo泛化调用 + Resilience4j熔断 | 可运行的Java Agent Core SDK(含Memory/Tool/Router模块) |
| 实战期 | 第9-16周 | 在真实业务场景中构建可交付Agent,解决数据一致性、权限控制、性能瓶颈等工程问题 | Spring Security + MyBatis二级缓存 + Arthas诊断 | 电商客服Agent(支持订单查询+退货申请+物流跟踪,TPS≥200) |
| 精进期 | 第17-24周 | 构建企业级Agent基础设施,覆盖监控告警、灰度发布、AB测试、成本治理 | Prometheus+Grafana + Nacos灰度路由 + SkyWalking链路追踪 | Agent Platform控制台(含Token消耗看板、Fallback率热力图、Prompt版本对比) |
注意:这里没有“第5周学RAG”“第12周学多Agent协作”这种碎片化安排。RAG在第6周出现,但它是作为“解决Tool返回数据过载”的一种Memory优化策略;多Agent协作在第14周出现,但它是为了解决“客服Agent无法处理财务问题,需自动路由给Finance Agent”的真实业务流。所有技术点都锚定在Java工程师每天面对的工程问题上。
2.3 工具链选型:为什么坚持Spring生态,而不是拥抱Python全家桶
有人会问:LangChain Python版文档更全,为什么非要折腾LangChain4j?答案很现实:
- 团队协同成本:你所在的Java团队,90%成员不会Python,但100%会看懂
@Bean public Tool orderQueryTool(){...}; - 运维一致性:Agent服务最终要部署在现有K8s集群,和订单服务共用同一套Prometheus指标采集器、同一套ELK日志规范,混用Python会导致监控割裂;
- 安全合规:某金融客户明确要求所有生产服务必须通过SonarQube扫描,而Python的静态分析覆盖率远低于Java。
我们实测对比过两种方案:
- Python方案:用FastAPI暴露Agent接口,通过Pydantic做Schema校验,用Loguru记录Trace。问题在于:当Agent调用内部Java服务时,需要额外维护gRPC/HTTP双协议,且无法复用现有Spring Security的JWT解析逻辑;
- Java方案:用Spring WebFlux暴露Reactive Agent接口,用Spring AI的
ChatClient封装LLM调用,用@EventListener监听AgentExecutionEvent做统一审计。所有Filter、Interceptor、Aspect都能无缝复用。
最终选择LangChain4j不是因为它“最好”,而是因为它让Agent开发回归到Java工程师最熟悉的声明式编程范式——就像你不用手动写Netty代码,而是用@RestController一样自然。
3. 核心细节与实操要点:从第1周第一天开始的硬核拆解
3.1 第1周:用Java手写Agent Runtime,理解“智能”背后的确定性
很多教程一上来就跑通llm.chat("Hello"),但这掩盖了Agent最本质的矛盾:LLM输出是非确定性的,而工程系统必须是确定性的。第1周的任务,就是亲手制造这个矛盾并解决它。
核心任务:实现一个StatefulAgent类,具备以下能力:
- 接收用户输入,生成Thought(思考过程);
- 根据Thought选择Tool并执行;
- 将Tool结果注入下一轮Thought;
- 当Tool执行失败时,自动触发Fallback(如返回预设话术);
- 全程使用
java.util.concurrent包,不依赖任何AI框架。
关键代码片段(已脱敏,保留核心逻辑):
public class StatefulAgent { private final ConcurrentHashMap<String, AgentState> sessionStore = new ConcurrentHashMap<>(); // AgentState包含:当前Thought、历史Action/Observation、剩余重试次数、超时时间戳 public static class AgentState { private String currentThought; private List<StepRecord> history = new ArrayList<>(); private int retryLeft = 3; private long timeoutAt = System.currentTimeMillis() + 30_000; // 30秒超时 public void recordStep(String action, String observation) { history.add(new StepRecord(action, observation, Instant.now())); } } public AgentResponse execute(String sessionId, String userInput) { AgentState state = sessionStore.computeIfAbsent(sessionId, k -> new AgentState()); // 1. 超时检查(类似Tomcat的requestTimeout) if (System.currentTimeMillis() > state.timeoutAt) { return new AgentResponse("抱歉,处理超时,请稍后重试", "TIMEOUT"); } // 2. 执行核心循环(最多3轮,模拟LLM的max_tokens限制) for (int i = 0; i < 3; i++) { try { // 这里本该调用LLM,但我们先用规则引擎模拟 String thought = generateThought(userInput, state.history); String action = selectTool(thought); // 3. 执行Tool(这里是真实Java方法调用) String observation = executeTool(action, userInput); state.recordStep(action, observation); // 4. 判断是否结束(类似HTTP的200状态码) if (observation.contains("FINAL_ANSWER")) { return new AgentResponse(observation, "SUCCESS"); } } catch (Exception e) { // 5. Fallback策略:不是打印堆栈,而是返回业务友好提示 if (state.retryLeft-- > 0) { continue; // 重试 } else { return new AgentResponse("系统繁忙,请联系人工客服", "FALLBACK"); } } } return new AgentResponse("未能理解您的需求,请换种方式描述", "NO_ANSWER"); } }为什么这个看似简单的代码如此重要?
- 它强制你思考:
sessionStore用ConcurrentHashMap还是Redis?如果选Redis,如何保证recordStep的原子性?这直接引出第3周的分布式Memory方案; timeoutAt字段的设计,暴露了Agent和传统Web服务的本质区别:超时不是针对单次HTTP请求,而是针对整个推理链路。你在Spring MVC里写的@TimeLimiter注解,在这里必须升级为@AgentTimeLimiter;retryLeft的递减逻辑,让你立刻意识到:Agent的重试不是无脑for循环,而要像Resilience4j那样支持指数退避(第4周重点)。
提示:不要急着优化性能。第1周的目标是“跑通”,哪怕用
Thread.sleep(1000)模拟LLM延迟也要先把状态流转画清楚。我见过太多人卡在“怎么让LLM返回JSON格式”,却忽略了state.history的序列化问题——当Agent重启时,内存里的List会丢失,这就是第2周要解决的Persistence问题。
3.2 第5周:当RAG遇上MyBatis,如何让知识库检索像查数据库一样可靠
RAG(Retrieval-Augmented Generation)常被神化为“Agent的魔法”,但对Java工程师来说,它不过是一次带语义的数据库JOIN操作。第5周的核心,就是把向量检索封装成MyBatis的@SelectProvider。
真实痛点:某电商客服Agent需要回答“iPhone15保修政策”,但LLM本身不知道最新条款。传统做法是把PDF切片存入向量库,但问题来了:
- 向量检索返回Top3文档,但其中第2个可能是过期政策(2023版),而第1个是正确答案(2024版);
- 用户问“保修期多久”,检索可能命中“维修流程”文档,答非所问;
- 向量库更新延迟,新政策上线后2小时才同步,期间Agent持续误导用户。
我们的Java解法:
- 双路检索:用Elasticsearch做关键词检索(保证时效性),用Milvus做向量检索(保证语义相关性),结果取交集;
- 置信度加权:给每个检索结果打分,公式为:
score = 0.6 * es_score + 0.4 * milvus_score - 0.1 * age_penalty,其中age_penalty根据文档更新时间计算; - 结果后处理:用正则提取“保修期[0-9]+年”这类结构化信息,丢弃未匹配的段落。
关键代码(Spring Data Elasticsearch + Milvus Java SDK):
@Repository public class PolicyRetriever { @Autowired private ElasticsearchOperations elasticsearchOperations; @Autowired private MilvusClient milvusClient; public List<PolicyDocument> retrieve(String query, int topK) { // 1. ES关键词检索(毫秒级,保证新鲜度) Query esQuery = NativeSearchQueryBuilder.query( QueryBuilders.multiMatchQuery(query, "title^3", "content") ).withPageable(PageRequest.of(0, topK)).build(); List<PolicyDocument> esResults = elasticsearchOperations.search( esQuery, PolicyDocument.class, IndexCoordinates.of("policy_index") ).get().map(SearchHit::getContent).collect(Collectors.toList()); // 2. Milvus向量检索(秒级,保证语义) List<List<Float>> vectors = embeddingService.embed(List.of(query)); SearchParam searchParam = SearchParam.newBuilder() .withCollectionName("policy_collection") .withVectors(vectors.get(0)) .withTopK(topK) .build(); List<QueryResult> milvusResults = milvusClient.search(searchParam).getData(); // 3. 合并结果(此处简化,实际用Lucene的BM25F算法) return mergeResults(esResults, milvusResults); } private List<PolicyDocument> mergeResults(List<PolicyDocument> es, List<QueryResult> milvus) { // 实现置信度加权合并逻辑... return mergedList; } }实操心得:
- 不要迷信“向量检索准确率95%”。我们压测发现,当用户问“怎么退换货”,向量检索常返回“维修指南”,而ES关键词检索能精准命中“退换货政策”文档。混合检索不是技术炫技,而是工程兜底;
age_penalty的系数0.1是经过200次AB测试得出的:太高会导致新政策权重不足,太低会让过期文档泛滥;- 最关键的是:把
PolicyRetriever注册为Spring Bean,这样在Agent的@ToolMethod里就能直接@Autowired注入,和调用DAO层没有任何区别。
3.3 第12周:用Spring Security重构Agent权限体系,让“能做什么”比“说什么”更重要
Agent的安全问题,90%的教程都在讲“如何防止Prompt注入”,但真实世界里,最大的风险是权限越界。比如客服Agent调用了财务系统的transferMoney()工具,或者HR Agent读取了全员薪资表。第12周,我们用Spring Security的@PreAuthorize注解,给每个Tool加上RBAC权限栅栏。
核心设计:
- 每个Tool方法标注
@ToolPermission("order:query"),对应Spring Security的Authority; - 用户登录时,JWT中携带
authorities:["order:query","logistics:track"]; - Agent执行前,通过
SecurityContextHolder.getContext().getAuthentication()获取当前权限,动态过滤可用Tool列表。
关键代码:
@Component public class ToolPermissionEvaluator { public boolean hasPermission(Authentication auth, String requiredPermission) { return auth.getAuthorities().stream() .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(requiredPermission) ); } } // 在Agent Router中使用 public class AgentRouter { @Autowired private ToolPermissionEvaluator permissionEvaluator; public List<Tool> getAvailableTools(Authentication auth) { return toolRegistry.getAllTools().stream() .filter(tool -> { ToolPermission annotation = tool.getMethod().getAnnotation(ToolPermission.class); return annotation != null && permissionEvaluator.hasPermission(auth, annotation.value()); }) .collect(Collectors.toList()); } }为什么这比“在Prompt里写‘你不能查工资’”可靠?
- Prompt约束是软性的,LLM可能忽略或曲解;而
@PreAuthorize是硬拦截,没权限连方法都不会反射调用; - 权限变更无需改Prompt,只需更新JWT的claims或数据库的role_permission表;
- 可以和现有SSO系统无缝集成,审计日志直接走Spring Security的
AbstractAuthenticationEvent。
注意:不要在Tool方法内部做权限校验。我们曾犯过这个错误——在
orderQueryTool()里写if (!hasPermission()) throw new AccessDeniedException(),结果导致Agent的Thought生成被中断,返回“系统错误”而非“您无权查询”。正确的姿势是:在Tool选择阶段就过滤掉不可用Tool,让LLM在有限选项内做决策。这就像前端按钮的disable逻辑,不是等用户点了再报错。
4. 实操过程与核心环节实现:从单体Agent到平台化演进的全链路
4.1 第1-8周筑基期:构建可测试的Agent Core SDK
第1周的手写Agent只是玩具,第8周的目标是产出一个可被单元测试、可被Mock、可被集成到CI/CD的SDK。我们定义了四个核心模块:
1. Memory模块:解决状态持久化问题
- 内存版:
InMemoryMemoryStore(用于单元测试); - Redis版:
RedisMemoryStore,用Lua脚本保证getAndSet原子性; - 数据库版:
JdbcMemoryStore,用FOR UPDATE锁住Session记录,避免并发修改冲突。
关键设计:
public interface MemoryStore { // 必须支持TTL,因为Agent Session不是永久的 void set(String sessionId, Object data, Duration ttl); <T> T get(String sessionId, Class<T> type); void delete(String sessionId); } // Redis实现的关键:用SET命令的EX参数,而不是单独的EXPIRE命令 public class RedisMemoryStore implements MemoryStore { private final RedisTemplate<String, Object> redisTemplate; @Override public void set(String sessionId, Object data, Duration ttl) { redisTemplate.opsForValue().set( "agent:memory:" + sessionId, data, ttl, TimeUnit.MILLISECONDS ); } }2. Tool模块:解决工具调用的标准化
- 定义
Tool接口,强制实现execute(Map<String, Object> input); - 提供
@ToolMethod注解,自动注册为Spring Bean; - 内置
HttpTool(封装RestTemplate)、DbTool(封装JdbcTemplate)、ScriptTool(沙箱执行Groovy脚本)。
3. Router模块:解决LLM输出解析的可靠性
- 不依赖正则匹配
Action: xxx,而是用Jackson反序列化预定义的ActionRequestPOJO; - 当LLM返回格式错误时,自动触发
FallbackRouter,用规则引擎生成备选Action。
4. Trace模块:解决可观测性
- 继承Spring Cloud Sleuth的
Tracer,为每个Agent Execution生成独立Trace ID; - 记录
Thought、Action、Observation、Latency、TokenUsage五维数据; - 输出格式兼容OpenTelemetry,可直接接入Jaeger。
实测效果:
- 单元测试覆盖率从第1周的30%提升到第8周的85%,关键逻辑如
retryWithBackoff()、mergeSearchResults()全部可Mock; - CI流水线增加
AgentIntegrationTest阶段,用Testcontainers启动Redis+Milvus+Mock LLM服务,每次PR都跑通端到端流程; - SDK JAR包大小控制在2.3MB以内,无冗余依赖,符合企业级组件规范。
4.2 第9-16周实战期:电商客服Agent的全链路压测
第9周起,我们不再写Demo,而是用真实业务需求驱动开发。目标:上线一个支撑日均5万咨询的客服Agent,SLA 99.95%。
核心挑战与解法:
挑战1:高并发下的Memory竞争
百万级Session同时在线,Redis内存暴涨。解法:- 实现LRU淘汰策略,
redisTemplate.opsForValue().getAndDelete()配合expire; - 对
history字段做压缩,用Protobuf序列化代替JSON,体积减少62%; - 引入布隆过滤器,快速判断Session是否存在,减少80%无效Redis请求。
- 实现LRU淘汰策略,
挑战2:Tool调用链路超时雪崩
当物流查询Tool响应慢时,整个Agent卡住。解法:- 为每个Tool配置独立
TimeLimiter,@TimeLimiter(name = "logistics", fallbackMethod = "fallbackLogistics"); - Fallback方法不返回空,而是调用本地缓存(Caffeine)或降级到静态FAQ;
- 用Resilience4j的
CircuitBreaker熔断连续失败的Tool。
- 为每个Tool配置独立
挑战3:LLM输出不稳定导致业务逻辑断裂
用户问“我的订单还没发货”,LLM有时返回“已发货”,有时返回“未发货”,但实际状态是“已打包待出库”。解法:- 在Tool返回后,增加
PostProcessor校验:用正则提取“已发货|未发货|已打包”,不匹配则触发重试; - 对关键业务字段(如发货状态),强制要求Tool返回结构化JSON,用
JsonNode校验schema。
- 在Tool返回后,增加
压测数据(阿里云ACK集群,4c8g节点×3):
| 场景 | 并发用户 | TPS | 平均延迟 | 错误率 |
|---|---|---|---|---|
| 单轮问答(订单查询) | 500 | 217 | 420ms | 0.02% |
| 多轮对话(退货申请) | 200 | 89 | 1.2s | 0.15% |
| 混合流量(70%查询+30%退货) | 400 | 163 | 780ms | 0.08% |
实操心得:压测时发现一个致命Bug——当Agent执行到第3轮时,Redis的
GET命令偶尔返回null。排查发现是JedisPool配置的maxWaitMillis=2000太小,高并发下连接获取超时。改成GenericObjectPoolConfig的maxWait=5000后问题消失。这再次证明:Agent工程不是AI工程,而是Java工程的延伸,所有经典问题都会重现。
4.3 第17-24周精进期:构建Agent Platform控制台
第17周起,工作重心从“做一个Agent”转向“管理一群Agent”。我们构建了一个轻量级控制台,核心功能不是炫酷UI,而是解决运维刚需:
1. Token消耗看板
- 按Agent、按Tool、按用户维度统计Token用量;
- 设置阈值告警(如单日超100万Token自动邮件通知);
- 支持导出CSV,供财务部门核算云服务成本。
2. Fallback率热力图
- X轴:时间(小时),Y轴:Tool名称,颜色深浅表示Fallback率;
- 点击热区,下钻查看具体失败原因(如
HttpClientTimeoutException占比82%); - 自动关联Arthas诊断结果,定位是网络抖动还是下游服务异常。
3. Prompt版本对比
- 每次Prompt更新都生成Git Commit ID;
- AB测试时,将5%流量路由到新Prompt,实时对比
answer_accuracy、fallback_rate、avg_latency三指标; - 支持一键回滚到任意历史版本。
技术栈选择:
- 前端:Vue3 + Ant Design Pro(企业级后台首选,非React);
- 后端:Spring Boot Admin(复用现有监控体系);
- 数据存储:Prometheus(指标)+ MySQL(元数据)+ Elasticsearch(Trace日志)。
为什么不用LangSmith?
- LangSmith是Python生态,无法复用Java团队的SAML单点登录;
- 其指标存储在MongoDB,而我们已有成熟的MySQL运维团队;
- 最关键的是:LangSmith的“Prompt版本管理”不支持和Git仓库联动,而我们的方案直接读取
git log --oneline。
5. 常见问题与排查技巧实录:Java工程师转型路上的真实坑
5.1 “LLM返回格式总不对,正则匹配失效”——这是最普遍的幻觉
现象:写了Pattern.compile("Action: (\\w+)\\nAction Input: (.*)"),但LLM有时返回Action: query_order Action Input: {"orderId": "123"},有时返回Action: query_order\nAction Input: {"orderId": "123"},正则时灵时不灵。
根本原因:你试图用确定性工具(正则)解析非确定性输出(LLM)。这不是Bug,而是LLM的本质特性。
Java式解法:
- 放弃正则,拥抱POJO:定义
ActionRequest类,用Jackson的@JsonUnwrapped处理宽松格式; - 增加Schema校验:用
JsonSchemaValidator验证LLM返回是否符合预定义JSON Schema; - Fallback兜底:当解析失败时,不报错,而是用规则引擎生成默认Action(如
{"action":"faq","input":{"keyword":"order"}})。
实操代码:
public class ActionRequest { private String action; private Map<String, Object> input; // Jackson会自动处理各种换行、空格、引号格式 @JsonCreator public ActionRequest(@JsonProperty("action") String action, @JsonProperty("input") Map<String, Object> input) { this.action = action; this.input = input; } } // 解析方法 public ActionRequest parseAction(String llmOutput) { try { // 先尝试标准JSON解析 return objectMapper.readValue(llmOutput, ActionRequest.class); } catch (Exception e) { // 解析失败,用规则引擎生成默认Action return ruleEngine.generateDefaultAction(llmOutput); } }踩坑记录:我们曾花3天调试正则,最后发现LLM在温度(temperature)为0.8时,会随机在
Action:后面加空格。改成POJO解析后,问题消失。记住:不要和LLM的不确定性对抗,要设计能包容不确定性的系统。
5.2 “Agent响应越来越慢,CPU飙升”——内存泄漏的典型征兆
现象:Agent运行24小时后,响应延迟从400ms升到3s,JVM堆内存持续增长,Full GC频繁。
排查步骤:
- 用jstat确认:
jstat -gc <pid>显示OU(Old Gen Used)持续上涨,OC(Old Gen Capacity)不变; - 用jmap dump:
jmap -dump:format=b,file=heap.hprof <pid>; - 用Eclipse MAT分析:发现
ArrayList实例占堆85%,其父对象是AgentState.history; - 根因定位:
history列表无限增长,未做截断。
解决方案:
- 在
AgentState中增加MAX_HISTORY_SIZE = 10常量; - 每次
recordStep()前,检查history.size() > MAX_HISTORY_SIZE,则history.remove(0); - 更优雅的方案:用
CircularFifoQueue(Apache Commons Collections),自动丢弃最老元素。
预防措施:
- 在CI流水线加入
jvm-memory-leak-test,用JUnit启动Agent,模拟1000次对话,用jcmd <pid> VM.native_memory summary检查内存增长; - 在
AgentResponse中添加X-Agent-Memory-Usage: 65%响应头,便于APM监控。
5.3 “线上环境Agent突然不工作,日志全是Connection refused”——网络配置的隐形杀手
现象:本地IDEA运行正常,K8s集群里Agent启动就报Connection refused to llm-api.example.com:8080。
排查清单:
- ✅ 检查K8s Service DNS:
kubectl exec -it <pod> -- nslookup llm-api,确认能解析到ClusterIP; - ✅ 检查NetworkPolicy:是否有
egress规则阻止访问外部LLM服务; - ✅ 检查Java应用的DNS缓存:
InetAddress#setCachePolicy(0)禁用JVM DNS缓存,避免DNS变更不生效; - ✅ 检查HTTP Client超时:
RestTemplate默认连接超时是-1(无限等待),必须显式设置setConnectTimeout(5000)。
终极解法:
在application.yml中强制配置:
spring: cloud: inetutils: ignored-interfaces: # 忽略虚拟网卡 - docker0 - veth.* timeout-secs: 5 # DNS解析超时5秒经验总结:90%的线上问题,根源都在“本地能跑通,线上跑不通”。Java工程师的优势在于:所有问题都有确定性解法,不存在“玄学”。只要按
jstat→jmap→MAT→代码审查四步走,没有定位不了的Bug。
5.4 “客户说Agent回答错误,但日志显示LLM返回正确”——数据一致性黑洞
现象:用户投诉“Agent说订单已发货,但实际还没出库”,查日志发现LLM返回{"status":"shipped"},而数据库里是"packed"。
根因分析:
- Tool调用的是缓存层(Redis),而缓存未及时更新;
- 或者Tool调用的是主库,但读取了从库(主从延迟导致);
- 更隐蔽的是:Agent的
Thought生成时,Tool返回"packed",但LLM在Observation阶段被截断,只看到"pack",于是推理为"shipped"。
Java式保障方案:
- 强一致性Tool:对关键业务字段(如发货状态),Tool必须走主库,并加
SELECT ... FOR UPDATE锁; - Observation校验:在LLM接收
Observation前,用StringValidator检查是否包含完整关键词(如必须含"packed"或"shipped",不能是"pack"); - 最终一致性兜底:当Agent返回
"shipped"后,异步发起verifyShipmentStatus(),若不一致则发送纠错消息给用户。
关键代码:
public class ShipmentStatusTool { @Transactional(isolation = Isolation.REPEATABLE_READ) public String getStatus(String orderId) { // 1. 加锁读取,避免主从延迟 Order order = orderMapper.selectForUpdate(orderId); // 2. 返回结构化JSON,强制包含完整状态 return String.format("{\"status\":\"%s\",\"updatedAt\":\"%s\"}", order.getStatus(), order.getUpdatedAt()); } }这个24周计划,不是教你成为AI科学家,而是帮你把十年Java功力,转化为Agent时代的稀缺工程能力。当你能用@Transactional保证Tool调用的一致性,用@CircuitBreaker熔断不稳定的LLM,用@PreAuthorize管控Agent的权限边界时,你就已经站在了转型的终点——因为Agent Engineer的本质,从来都是用工程确定性驾驭AI不确定性的系统架构师。