尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java开发Agent智能体:从核心循环到生产化实践

Java开发Agent智能体:从核心循环到生产化实践 最近把用Java写的Agent智能体lucky_agent从原型推进到了内部可用的状态终于有底气把完整的思路和经验整理出来。简单说lucky_agent是一个跑在Java技术栈里的Agent运行时目前承担着团队内部的数据查询、告警排障、自动化运维指令执行这类活。选择Java而不是Python不是因为Python做不了而是因为我们核心业务的服务端全是JavaAgent要顺手接工单、接告警、接发布流程就必须长在这个生态里。这篇文章会把选型逻辑、核心循环设计、记忆管理、踩坑记录和生产化改造挨个过一遍适合正在纠结“Java能不能做Agent、怎么做Agent”的开发者也适合准备Java面试时想拿Agent项目当亮点的朋友。1. 为什么用Java写Agent选型背后的现实考量1.1 Python全家桶之外的另一条路很多人一提到Agent默认就是Python加LangChain再套个FastAPI好像不用Python就做不了智能体。我不这么看。如果只是搭个Demo跑通概念Python确实最快生态齐全写起来爽。但lucky_agent要对接的全是Java内部系统工单系统是Java配置中心是Java发布系统是Java数据库访问层用的也是JDBC。Agent要调用这些内部能力最顺手的方案是直接复用现有SDK而不是用Python在外面再包一层HTTP接口。举个例子团队里排障需要拉取服务日志原本就有现成的LogService客户端Java这边一行代码就能调用。如果换成Python就得先给这个能力单独开一个REST接口还要考虑鉴权、网络策略、超时控制。一套流程下来Agent没写多少集成代码倒是堆了一堆。用Java写Agent意味着可以直接用公司的SSO认证、权限模型、RPC客户端来当Agent的手脚省掉的集成工作量非常可观。另外还有一层原因很多Java团队对Python技术栈不熟部署运维能力也偏弱。lucky_agent跑在Spring Boot应用里直接打包成容器镜像走现有的CI/CD流程就能上线监控、日志、告警全复用。我觉得这才是Java做Agent最大的优势不是模型能力而是工程生态的融合成本很低。1.2 lucky_agent的定位服务侧的Agent运行时我给lucky_agent的定位不是聊天机器人而是“能执行任务的智能体”。聊天机器人重在对话体验而Agent重在任务闭环。目前lucky_agent在内部主要干三件事通过一句话查询生成数据查询逻辑执行后返回结果并解释含义比如查订单量、查失败率、查某个用户的历史行为。收到告警后自动拉取关联日志和系统状态初步排查原因给出处置建议。执行运维指令比如重启某个服务、调整配置项、触发版本回滚这类操作必须走审批流。这些场景有个共同点需要频繁和内部系统交互需要对执行过程有强控制需要每一步都可审计。定位清楚之后lucky_agent的架构就围绕“可插拔工具、可控循环、完整审计”来设计。这里插一句现在很多Agent教程都讲得很宏观热词也很多什么“Agent架构”“Agent框架”其实拆开看就是一个循环加一堆工具。别被概念绕晕先搞清楚自己要做的是聊天玩具还是任务执行器再去选框架。2. Agent的核心循环从一次对话到一次任务闭环2.1 Agent运行时需要哪几块积木Agent的本质是一个循环接收任务思考调用工具观察结果再思考直到产出最终答案。lucky_agent的核心循环是一个可以被截断的for循环不是黑盒。整个运行时至少有五块积木缺一块都跑不顺。第一块是LLM客户端Java侧我直接用HttpClient封装了一个OpenAI兼容协议的客户端一套接口同时支持多家模型服务。第二块是工具注册中心负责把Java方法以标准Schema的形式暴露给模型。第三块是上下文组装器把系统提示词、历史消息、工具描述拼成一次请求。第四块是输出解析器从模型返回里提取最终答案或者工具调用指令。第五块是执行器负责真正调用工具方法处理异常和超时。这里顺便把“harness”和“agent”的区别说清楚很多人问过。在我的理解里harness是脚手架和调度层负责上下文构建、循环控制、工具分发的通用部分agent是你定义的工具集合、提示词策略、任务处理逻辑这部分。lucky_agent实际上就是自己搭了一个轻量harness然后往里面塞我们自己的agent能力。所以“用Java开发Agent”本质上是在写harness加业务工具。2.2 ReAct循环的Java实现思路模型的推理模式用的是经典的ReAct思路也就是推理和行动交替进行。Java代码实现起来并不复杂核心就是一个循环。先定义工具接口所有工具都实现这个接口public interface Tool { String name(); String description(); String parametersJsonSchema(); String execute(String argumentsJson); }循环主逻辑简化后长这样public AgentResult run(String userInput) { ListMessage messages new ArrayList(); messages.add(new Message(system, SYSTEM_PROMPT)); messages.add(new Message(user, userInput)); for (int step 0; step maxSteps; step) { ChatResponse resp llmClient.chat(messages); if (resp.hasFinalAnswer()) { return AgentResult.finalAnswer(resp.getContent()); } ToolCall call resp.getToolCall(); Tool tool toolRegistry.get(call.getName()); if (tool null) { messages.add(new Message(tool, 工具不存在请检查可用工具列表后重新选择)); continue; } String result tool.execute(call.getArguments()); messages.add(new Message(assistant, resp.getRawMessage())); messages.add(new Message(tool, result)); } throw new MaxStepsExceededException(maxSteps); }这里有个关键选择为什么用for循环而不是while(true)。核心原因是要给Agent套上限。maxSteps一般设置为5到8一旦超过就终止避免模型陷入反复调用工具的循环把token烧光。这个限制在实验环境好像无所谓但一放到生产环境就是真金白银的成本问题。每个工具返回给模型的结果也需要控制大小。比如查日志的工具日志文件动辄几百上千行全塞给模型还没轮到下一步调用上下文就爆了。我的做法是工具执行后做摘要和截断默认只保留前2000个字符重要的异常堆栈部分单独提取。这一步已经成了lucky_agent工具层的默认行为。2.3 工具注册机制与描述质量的实战经验工具注册我做了两种方式。第一种是注解扫描类似Spring的Component配合自定义注解AgentTool(name query_order, description 查询订单状态、退款进度、物流信息适用于用户询问订单相关问题时调用, parametersSchema {...json schema...}) public class QueryOrderTool implements Tool { // 实现逻辑 }第二种是编码式注册适合工具数量不多、不想动Spring扫描路径的场景toolRegistry.register(new QueryOrderTool()); toolRegistry.register(new QueryLogTool());两种方式各有适用场景。注解扫描适合工具数量多、团队协作起来之后统一管理编码式注册直观可控调试方便。lucky_agent早期用的是编码式后来工具涨到十几个才迁到注解扫描。这里我要强调一个非常容易被忽视的点工具的description写得好不好直接决定Agent的成功率。模型就是靠这短短几句话判断什么时候该调用哪个工具。比如一个查询订单的工具如果描述只写“查询订单”模型在用户问“昨天那笔退款到账了吗”的时候就会犹豫不知道这个工具适不适合。如果改成“查询订单状态、退款进度、物流信息适用于用户询问订单、退款、发货相关问题时调用”模型一眼就能匹配上。我后来给所有工具写描述都遵循一个模板这个工具是什么、能查到什么、适合在什么场景用。光这一项工具调用的准确率就提升了至少两成。顺带提一句Skill可以理解为一组工具的组合配置把完成某个任务需要的工具和规范打包Agent是运行时选择Skill并现场决策的主体。分工清楚代码结构也更好维护。3. 记忆、上下文与状态管理让Agent不“失忆”3.1 短期记忆上下文窗口的截断与摘要Agent跑几轮交互之后消息列表会越来越长。如果不加管理很快就顶到模型的上下文窗口上限请求直接被拒绝。lucky_agent的短期记忆策略分三层系统提示词永远保留中间的历史消息做一次摘要最近N轮原始消息完整保留。具体的实现思路是这样的维护一个消息队列当总Token数超过阈值时把最老的用户和助手消息取出来调用模型生成一段精简摘要把摘要作为一条系统消息放回队列原始消息丢弃。这套逻辑用Java写起来不复杂却非常管用。Token估算方面我用的jtokkit这是一个tiktoken的Java移植版本能比较准地估算上下文长度。这里提醒一下不要用字符串长度来估算Token中英文的Token占用差别很大估算不准会导致请求频繁被拒。有人可能会问摘要会不会丢失关键信息会但这是成本换质量的取舍。我的经验是摘要只针对“中间地带”的对话最近几轮原始消息完整保留这样模型既能看到当前任务的最新上下文又能通过摘要保持对整段会话大方向的把握。实际使用下来任务完成率没有明显下降。3.2 长期记忆向量检索和结构化存储的分工长期记忆这块lucky_agent走了两条路线我自己的体会是路线之间并不冲突反而互为补充。结构化记忆存的是固定格式的结论和记录。比如一次排障的最终结论、一个用户的偏好标签、一个任务的执行结果。这些数据有明确字段直接存MySQL查询走SQL效率高结果精确。Agent在需要的时候可以通过工具直接查出来。向量检索存的是开放域的知识片段和相似历史案例。比如历史故障的处理过程描述、知识库里一段关于某系统架构的说明。这部分先用Embedding接口把文本向量化再写入向量存储。Java侧我用过内存向量库和RedisSearch后者更适合生产。查询时把用户问题向量化做TopK召回召回来的片段拼进上下文。这里必须提醒记忆安全。长期记忆里可能存有机密信息Agent在做生成的时候可能无意间把不该说的内容带出来。业界已经出现类似a-memguard这种专门对Agent记忆做防护的研究核心思路是在写入和读取记忆时都做脱敏和访问控制。我在lucky_agent里的做法是写记忆之前统一过一遍脱敏规则员工号、手机号、内部IP一律替换成占位符查询结果返回到前端之前再做一次反向脱敏确保模型只接触“干净”的数据。3.3 会话恢复与工具幂等Agent跑任务过程中可能出事网络断了、服务重启了、模型超时了。如果每次都是从零开始用户会很崩溃。lucky_agent落了一个会话状态表把Agent每一步的状态都存下来准备调用哪个工具、传了什么参数、工具返回了什么结果、当前在第几步。任务中断之后重启时读取状态从断点继续跑不用重新开始整轮对话。这里有个Java开发里很容易忽略的点工具的幂等。特别是写操作类工具比如“修改配置”“触发发布”如果Agent第一次执行成功但因为网络问题没收到结果它可能重试重试就会产生第二次写操作。我的做法是所有写操作工具都必须接收一个幂等键同一个幂等键只执行一次结果直接返回上次的执行结果。这一步不做Agent生产化就是空谈。4. 实操踩坑Java生态里Agent的典型问题与排查4.1 流式输出与异步编排的取舍我第一次做lucky_agent时所有LLM调用都是同步HTTP体验确实差一个复杂任务要等十几秒页面像卡死一样。后来想改成流式输出走SSE用CompletableFuture做异步编排。结果踩了一脚大的业务线程池配置不合理流量稍微上来线程直接被慢调用占满整个接口都堵死了。排查半天才发现问题出在混合使用Spring的Async线程池和业务自己的线程池。线程饥饿的症状是接口偶尔超时CPU却不高让人非常迷惑。后来我调了一个更务实的方案Agent编排过程大部分用同步阻塞但限制并发数同时用数据流的方式处理流式输出。只有面向用户的输出环节走SSE内部编排不动不动就上异步。异步不是免费的它会把问题复杂化。如果你也准备用Java做Agent我建议一开始只对常驻外部接口用异步内部编排先把正确性跑通再说。4.2 防御式解析LLM输出不稳定的程序化处理模型输出天生不稳定同一个请求多次调用返回的格式可能有差异这是Agent开发里百分之百会遇到的问题。最常见的是模型把工具调用参数包进了Markdown代码块或者在参数JSON里加注释或者字段顺序错乱。Java侧的Jackson直接解析这种输出会抛异常然后整个循环就断了。lucky_agent定了一套防御式解析流程。先从原始输出里提取可能在代码块里的JSON再做一次格式修正最后才是反序列化。提取逻辑大概是private String extractJson(String raw) { String s raw.trim(); if (s.startsWith()) { s s.replaceAll(^(?:json)?\\s*, ) .replaceAll(\\s*$, ); } return s; }除此之外还要处理工具名不存在的情况。经典的错误是模型幻想了一个不存在的工具名Java侧如果直接抛异常循环就断了用户看到的是一段底层报错。我的做法是把错误转成工具结果给模型“工具不存在请从以下列表中选择”让模型自我纠正。这招特别管用好几次模型自己就换对了工具。这种防御式处理也让我意识到Agent开发其实很大程度是在跟不确定性做对抗程序里每多一层容错线上就少一次事故。4.3 超时、令牌预算与线程池保护Agent环节多任何一个环节卡死都会拖垮整个调用链路。lucky_agent对每个环节单独设置超时。模型调用默认30秒只读工具20秒写工具60秒封顶用完即断。超时异常不会直接向外抛而是作为工具结果返回让模型知道这一步超时了可以选择换思路或者向用户说明。令牌预算方面每轮请求之前会先估算当前上下文的Token总数如果超过阈值就触发摘要或者裁剪逻辑。而且要预留一部分Token给模型回复。这就好比开车不能把油跑干再找加油站总得留点余量。Java侧实现这套逻辑不难关键是要有“提前拦截”的意识而不是等API返回超长错误再去处理。线程池保护也提一下。Agent任务往往是长耗时操作一个任务可能在几分钟内占用一个线程。我专门为Agent调用拆分了一个独立的线程池大小按并发数的上限配置并且设置了队列拒绝策略超出能力直接返回“系统繁忙”而不是无限堆积。4.4 agent execution terminated due to error 的排查实录接触Agent比较久的朋友应该对“agent execution terminated due to error”这类报错不陌生。一开始遇到这个问题我有点懵因为它太笼统不带堆栈看不出具体是哪个环节出错。后来我总结出这类错误最常见的四个原因模型返回的工具调用参数无法解析防御式解析没兜住。工具执行时抛了未捕获异常执行器没有统一接住。上下文超过模型限制请求被API直接拒绝。循环达到最大步数仍然没有产出最终答案被循环终止。排查这类问题我最大的心得是让每一步都有迹可循。lucky_agent里每一步都会打印结构化日志包含请求ID、当前步骤、模型返回类型、工具名、工具参数、工具耗时、工具结果摘要。出问题时直接查日志就能看出是模型侧的问题还是工具侧的问题。为了提升排查效率我还做了一个“会话回放”页面。每一轮的模型请求和工具结果都存储在数据库里可以按时间线回放整个过程。有了这个功能复现问题就变成了点开回放看过程再也不用靠文字描述去猜。实践中我发现还有一个非常隐蔽的坑工具异常信息直接拼回了上下文。模型拿到一段Java堆栈之后容易自我归因然后开始道歉“抱歉我遇到了技术错误”之后陷入死循环。所以我规定工具报错必须返回标准化结构只有错误码和可读描述不给模型看原始堆栈这样模型才能做出有效应对。5. Agent安全与生产化从玩具到能用的距离5.1 工具权限不要让Agent为所欲为Agent如果什么工具都能调出事的概率很大。lucky_agent把所有工具按风险等级分成三类只读、低风险写、高风险写。只读工具比如查日志、查订单模型可以直接调用。低风险写比如发送通知、创建草稿可以自动执行但留审计。高风险写比如重启服务、修改配置、触发发布必须走审批流。审批流的实现思路很简单Agent生成“待执行指令”状态置为等待审批人工确认后系统才真正调用工具。这个逻辑相当于在Agent和工具之间加了一个“人类闸门”既保留了Agent的自动化能力又给关键操作上了保险。开发初期我觉得审批流多余拖慢效率后来一次模型幻觉差点触发配置变更才明白这道闸门是保命的。5.2 数据脱敏与输出过滤Agent需要访问的数据往往包含敏感信息。lucky_agent的脱敏在统一入口做不是在各个工具里各自为政。工具返回的数据进入上下文之前先过一遍脱敏过滤器手机号中间四位打码员工号替换成占位符内网IP掩码密钥直接丢弃。这样模型在推理时根本接触不到敏感原文从源头上避免了大模型“记忆泄露”的风险。输出过滤是针对最终结果的。有些数据经过了脱敏但模型可能根据上下文推断出部分敏感内容或者把脱敏内容原样输出。所以最终结果返回用户之前还要再跑一道正则过滤发现疑似敏感内容就替换成提示信息。这一套双保险下来Agent能放心地在生产环境使用。5.3 可观测性日志、指标与轨迹回放Agent应用跟普通接口不一样一次任务要经历多次模型调用和工具调用定位问题必须依靠可观测性。lucky_agent在三个层面做了埋点。日志层面一次任务有一个全局requestId贯穿所有环节结构化输出。指标层面统计模型调用次数、Token消耗、工具成功率、平均步数、平均耗时这些都是判断系统健康度的核心指标。轨迹回放层面所有步骤落库支持按时间轴重现Agent的思考过程和工具调用过程。这里有个比较隐蔽的经验模型复杂度评估要基于“步数分布”。如果平均步数突然从3涨到5大概率是某个工具描述变差了导致模型多绕圈。这个指标比看成功率更敏感。Agent开发里很多问题不是“坏没坏”而是“效率下降了多少”步数就是那个效率温度计。5.4 回归测试怎么知道Agent改坏了Agent是典型的行为不确定系统改了Prompt、调了工具描述、换了模型版本都可能引起行为漂移。lucky_agent准备了约一百条回归用例覆盖各类典型任务每次变更后自动跑一遍再人工抽检。评估指标有四个维度任务完成率看是否得到最终答案工具准确率看是否在正确的时机调用了正确的工具关键信息覆盖率看结论是否遗漏了必需信息流程合规率看是否在需要审批的场景主动挂起等待审批。这四个维度合起来基本能反映Agent的行为质量。我再补充一个点关于Skill和Agent的关系在生产中的具体体现Skill本质上是工具和提示片段的组合包一个Skill对应一类任务的施工图Agent是施工队本身负责现场决策和执行。在lucky_agent的实现里Skill就是一组工具的编排配置Agent启动时根据任务类型动态加载对应的Skill这让工具的组织更清晰也让模型面对的工具列表更精简决策噪音更小。回到Java本身这次实践让我对Java生态的看法又深了一层。做Agent不等于写算法大部分工作是在做工程整合、容错处理和系统设计。Java在这方面的积累一点不浪费类型安全让工具接口不容易写歪Spring生态让配置和部署顺滑可观测性工具链成熟。如果你也在Java团队里想试水Agent建议从一个只调两个工具的窄场景切入先把循环跑通再慢慢加记忆、加工具、加安全策略。最后分享一个压箱底的小技巧给Agent的System Prompt里明确写好“能做什么、不能做什么、优先用什么工具”比反复调模型参数管用得多。很多事故不是模型能力不够而是你忘了告诉它边界在哪里。
返回列表