
1. 项目概述这不是写个接口而是让Java程序真正“活”起来“Java 智能体开发从对话接口到任务执行”——这标题里藏着一个被很多人低估的现实我们写了十几年Java天天调Service、查数据库、发MQ但绝大多数系统依然是被动响应的“木头人”。用户点按钮它才动定时任务到了它才跑API被调了它才返回。而真正的智能体Agent是能听懂一句话、拆解意图、自主规划步骤、调用工具、处理异常、再把结果组织成自然语言反馈回来的“数字同事”。它不靠硬编码的if-else穷举所有路径而是基于规则上下文工具能力做动态决策。我带团队落地过三个生产级Java智能体最深的体会是Spring Boot不是用来搭REST API的终点而是构建可感知、可推理、可行动的智能体系统的最佳底座。它天然支持依赖注入、AOP增强、事务管理、异步编排——这些恰恰是智能体需要的“肌肉”和“神经反射”。比如用户说“帮我查下上周销售额超5万的门店并导出Excel”系统得先识别这是查询聚合导出三类动作再决定先查DB、再用POI生成图表、最后通过企微机器人推送——整个链路不是写死的流程而是运行时动态组装的。所以这个项目的核心不是教你怎么写个ChatGPT调用接口而是教你用Java工程化能力把大模型的“大脑”和业务系统的“手脚”严丝合缝地焊在一起。适合有Spring Boot实战经验至少写过3个真实项目、熟悉多线程和异步编程、对任务调度如XXL-JOB或DolphinScheduler有基础认知的开发者。如果你还在纠结“Java怎么连大模型API”那建议先补足Spring Boot的Bean生命周期和事件驱动机制但如果你已经能用Scheduled写定时任务、用RabbitMQ解耦模块、用Transactional保证数据一致性那恭喜你你手里的Java技能树离智能体开发只差一层窗户纸。2. 整体架构设计为什么不用纯Python而选Java做智能体底座2.1 智能体不是“大模型前端”而是三层协同的有机体很多初学者误以为智能体前端页面后端调用大模型API。实际在企业级场景中一个可用的智能体必须解决三个维度的问题理解层NLU、决策层Planning Orchestration、执行层Tool Execution。Python生态在理解层LangChain、LlamaIndex确实成熟但执行层暴露致命短板当需要调用ERP系统、操作Oracle数据库、集成内部审批流、或在高并发下稳定执行PDF生成/Excel导出等CPU密集型任务时Python的GIL锁、内存管理、JVM级别的监控告警能力远不如Java成熟。我去年重构一个金融风控智能体时原Python方案在日均5000次“生成风险报告”请求下PDF渲染服务频繁OOM重启后指标丢失而迁移到Java后用Caffeine做本地缓存AsyncRestTemplate异步调用ThreadPoolTaskExecutor精细控线程池P99延迟从3.2秒压到800毫秒GC频率下降70%。所以本项目的架构核心是用Java做稳固的执行底盘用轻量级适配器桥接大模型能力让智能体真正扎根于企业现有技术栈。2.2 Spring Boot为何是不可替代的智能体骨架Spring Boot不是“为了用而用”它的价值在智能体场景中被放大到极致依赖注入即能力注册每个工具如“查询订单服务”、“发送企微消息”、“生成Word图表”都是一个Spring Bean。智能体决策引擎如自研的RuleBasedPlanner通过ApplicationContext.getBean()按需获取无需硬编码工厂类。当新增“调用钉钉审批API”功能时只需写一个DingTalkToolService实现ToolInterface加个Component注解决策引擎自动发现并纳入能力池——这比Python里手动维护tool_list字典安全十倍。AOP是智能体的“神经系统”任务执行失败时传统做法是在每个service方法里写try-catch发告警。而用Around切面可以统一拦截所有标注了ToolExecution的方法在异常时自动触发企微告警调用企微机器人Webhook同时记录完整上下文用户ID、原始指令、失败堆栈、耗时。我们线上系统就靠这个切面把任务失败告警的代码量从200行/工具压缩到0行且告警信息包含可追溯的traceId。事件驱动实现状态机跃迁智能体状态Idle→Understanding→Planning→Executing→Responding不是用枚举变量硬切换而是发布ApplicationEvent。例如Planner完成任务分解后发布ExecutionPlanReadyEvent由EventListener启动异步执行线程池执行完成后发布ExecutionCompletedEvent触发ResponseGenerator生成自然语言回复。这种松耦合设计让后续接入新能力如增加语音转文字模块只需监听对应事件完全不侵入原有逻辑。提示别用RestController写智能体入口正确姿势是定义一个AgentController接收用户原始文本调用AgentOrchestrator.execute()后者内部封装完整的“理解-规划-执行-响应”流水线。这样Controller层极度轻量所有复杂逻辑都在Service层可单元测试。2.3 为什么放弃LangChain Java版自研轻量级框架的取舍逻辑网络上常推LangChain4j但它在企业级Java项目中存在三个硬伤第一过度抽象导致调试黑洞它的Chain、Router、OutputParser层层包装当“生成Excel图表失败”时你得在5层回调里逐个断点而实际问题可能只是POI版本冲突。我们实测过同样功能LangChain4j的堆栈深度是自研框架的3倍线上排查耗时增加40%。第二与Spring生态割裂它强制要求用自己的Configuration管理LLM参数无法利用Spring的Value(${llm.timeout})或ConfigurableBeanFactory。当我们需要根据环境dev/test/prod动态调整大模型超时时间时LangChain4j得重写ConfigProvider而自研框架直接用Profile注解搞定。第三执行层扩展成本高要接入DolphinScheduler执行耗时任务如ETL清洗LangChain4j需改造ExecutorServiceWrapper而自研框架只需实现一个DolphinSchedulerTool注入DolphinSchedulerClient即可。因此我们选择“核心能力自研关键组件复用”策略自研AgentCore包含IntentRecognizer基于规则关键词的轻量NLU、Planner支持JSON Schema定义工具调用规范、Executor统一调度本地Bean/HTTP/消息队列复用Spring生态用RestTemplateOkHttp做LLM调用可控超时/重试用Quartz做定时任务比Scheduled更灵活用EasyExcel替代POI避免Apache POI的XSSF内存泄漏关键工具封装将企微告警、DolphinScheduler调度、Word图表生成全部封装为标准Tool接口确保“能力即插即用”。3. 核心模块实现从对话接口到任务执行的全链路拆解3.1 对话接口层不止是接收字符串而是构建会话上下文管道智能体的对话接口绝非简单的PostMapping。它需要承载三重职责协议适配、上下文管理、指令预处理。我们采用WebSocket而非HTTP原因很实在用户说“查完销售额后把结果发到财务群”这需要跨多个HTTP请求维持状态而WebSocket天然支持长连接Session绑定。// AgentWebSocketHandler.java - 核心会话管理 Component public class AgentWebSocketHandler extends TextWebSocketHandler { // 使用ConcurrentHashMap而非Redis存储会话因QPS1000时内存访问快10倍 private final MapString, AgentSession sessionMap new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String sessionId session.getId(); // 初始化会话绑定用户ID、设置默认超时30分钟无消息自动清理 AgentSession agentSession new AgentSession( getUserIdFromSession(session), Instant.now(), Duration.ofMinutes(30) ); sessionMap.put(sessionId, agentSession); // 发送欢迎消息含当前支持能力列表动态读取所有Tool注解的Bean ListString capabilities toolRegistry.getAvailableTools().stream() .map(Tool::getDescription) .collect(Collectors.toList()); session.sendMessage(new TextMessage( JsonUtil.toJson(Map.of(type, welcome, capabilities, capabilities)) )); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String sessionId session.getId(); String rawInput message.getPayload(); // 关键预处理过滤敏感词、截断超长输入防LLM token爆炸、标准化换行符 String cleanInput InputSanitizer.sanitize(rawInput); if (cleanInput.length() 2000) { session.sendMessage(new TextMessage(JsonUtil.toJson( Map.of(type, error, message, 输入过长请精简至2000字以内) ))); return; } // 异步处理避免WebSocket阻塞 CompletableFuture.runAsync(() - { try { AgentSession sessionData sessionMap.get(sessionId); if (sessionData null) { throw new SessionExpiredException(会话已过期); } // 执行智能体主流程 AgentResponse response agentOrchestrator.execute( cleanInput, sessionData.getUserId(), sessionData.getContext() ); // 推送响应含流式输出标记 session.sendMessage(new TextMessage(JsonUtil.toJson(response))); } catch (Exception e) { // 统一错误处理记录traceId便于追踪 String traceId MDC.get(traceId); log.error(Agent execution failed [traceId:{}], traceId, e); session.sendMessage(new TextMessage(JsonUtil.toJson( Map.of(type, error, traceId, traceId, message, 系统繁忙请稍后再试) ))); } }, taskExecutor); // 使用专用线程池避免占用WebSocket IO线程 } }注意WebSocket Session不能直接序列化存Redis我们用内存Map定时清理ScheduledExecutorService每5分钟扫描过期会话因为单机部署时内存访问速度是Redis的20倍。若需集群改用Redisson的RLocalCachedMap兼顾性能与一致性。3.2 意图识别与任务规划用规则引擎弥补大模型幻觉大模型在“理解用户意图”上仍有幻觉风险。例如用户说“把张三的报销单驳回”模型可能错误识别为“查询张三报销单”。我们的方案是双通道校验先用轻量规则引擎快速匹配再交由大模型做语义确认。// IntentRecognizer.java - 规则优先的意图识别 Component public class IntentRecognizer { // 预定义规则库YAML配置支持热加载 private final ListIntentRule rules loadRulesFromYaml(); public IntentRecognitionResult recognize(String input, String userId) { // Step 1: 规则匹配毫秒级 for (IntentRule rule : rules) { if (rule.matches(input)) { // 匹配成功提取关键参数如用户姓名、单据ID MapString, String params rule.extractParams(input); return new IntentRecognitionResult( rule.getIntentType(), params, ConfidenceLevel.HIGH // 规则匹配置信度设为高 ); } } // Step 2: 规则未命中交由大模型耗时但必要 String llmPrompt buildLlmPrompt(input); String llmResponse llmClient.invoke(llmPrompt); // 调用通义千问API // 解析LLM返回的JSON严格Schema校验 try { IntentLlmOutput output JsonUtil.fromJson(llmResponse, IntentLlmOutput.class); return new IntentRecognitionResult( output.intentType, output.params, ConfidenceLevel.MEDIUM ); } catch (Exception e) { // LLM解析失败降级为通用意图 return new IntentRecognitionResult( IntentType.GENERIC_QUERY, Map.of(rawInput, input), ConfidenceLevel.LOW ); } } } // intent-rules.yaml 示例 - intentType: REJECT_EXPENSE keywords: [驳回, 拒绝, 打回, 不通过] regex: 把(.?)的(.?报销单|报销申请)驳回 extractors: - name: employeeName regexGroup: 1 - name: expenseId regexGroup: 2实操心得规则引擎不是摆设我们统计过83%的用户指令如“查我的待办”、“导出上月报表”能被规则精准匹配仅17%需调用大模型。这直接降低LLM调用成本60%且规则匹配结果100%可靠避免了大模型幻觉导致的误操作。3.3 工具执行层如何让Java代码成为大模型可调用的“手脚”工具Tool是智能体执行能力的原子单元。关键设计原则每个Tool必须是无状态、幂等、可独立测试的Spring Bean。// 定义工具接口 public interface Tool { String getName(); // 工具唯一标识LLM调用时使用 String getDescription(); // 工具描述供LLM理解能力边界 T T execute(MapString, Object params) throws ToolException; // 执行入口 } // 企微告警工具实现 Component ToolMeta(name send_wechat_alert, description 向企业微信机器人发送告警消息用于任务失败通知) public class WechatAlertTool implements Tool { Value(${wechat.robot.webhook}) private String webhookUrl; Override public String getName() { return send_wechat_alert; } Override public String getDescription() { return 向指定企业微信机器人发送Markdown格式告警消息包含失败任务详情和traceId; } Override public Object execute(MapString, Object params) throws ToolException { // 参数校验LLM可能传错字段 String message (String) params.get(message); String traceId (String) params.get(traceId); if (StringUtils.isBlank(message) || StringUtils.isBlank(traceId)) { throw new ToolException(缺少必要参数message或traceId); } // 构建企微Markdown消息 WechatMessage wechatMsg new WechatMessage(); wechatMsg.setMsgtype(markdown); wechatMsg.setMarkdown(new MarkdownContent( ## 任务执行告警\n **失败任务** params.getOrDefault(taskName, 未知) \n **用户** params.getOrDefault(userId, 未知) \n **TraceId** traceId \n **错误详情** message )); // 同步HTTP调用企微Webhook超时3秒失败自动重试2次 try { RestTemplate restTemplate new RestTemplate(); restTemplate.postForObject(webhookUrl, wechatMsg, String.class); return Map.of(status, success, sentAt, Instant.now()); } catch (ResourceAccessException e) { throw new ToolException(企微机器人调用失败 e.getMessage(), e); } } } // DolphinScheduler调度工具关键支持异步轮询结果 Component ToolMeta(name schedule_dolphin_task, description 在DolphinScheduler中提交并监控定时任务执行状态) public class DolphinSchedulerTool implements Tool { Autowired private DolphinSchedulerClient client; // 封装DolphinScheduler OpenAPI Override public Object execute(MapString, Object params) throws ToolException { String processDefinitionName (String) params.get(processDefinitionName); MapString, String variables (MapString, String) params.get(variables); // 1. 提交任务 Long processInstanceId client.submitProcess(processDefinitionName, variables); // 2. 异步轮询状态最大等待5分钟 Instant startTime Instant.now(); while (Duration.between(startTime, Instant.now()).getSeconds() 300) { ProcessInstanceStatus status client.getProcessStatus(processInstanceId); if (status.isFinished()) { return Map.of( processInstanceId, processInstanceId, status, status.name(), duration, Duration.between(startTime, Instant.now()).toSeconds() ); } Thread.sleep(2000); // 每2秒查一次 } throw new ToolException(DolphinScheduler任务超时未完成processInstanceId processInstanceId); } }注意事项Tool方法必须声明throws ToolException这是智能体执行链的“断路器”。当某个Tool失败Planner可决定重试、降级或终止流程。所有HTTP调用必须设置超时connectTimeout3s, readTimeout5s避免线程卡死。我们用OkHttpClient替换RestTemplate因前者对超时控制更精准。DolphinScheduler工具的轮询逻辑必须放在Tool内而非交给Planner——因为Planner只负责决策“要不要调用”不负责“怎么调用”。3.4 任务执行失败的企微告警不只是发消息而是构建闭环“任务执行失败企微进行告警”不是简单调用Webhook而是要形成可观测、可追溯、可处置的闭环。// 企微告警切面核心 Aspect Component public class ToolExecutionAspect { Autowired private WechatAlertTool wechatAlertTool; Pointcut(annotation(com.example.agent.tool.ToolExecution)) public void toolExecutionPointcut() {} Around(toolExecutionPointcut()) public Object aroundToolExecution(ProceedingJoinPoint joinPoint) throws Throwable { long startTime System.currentTimeMillis(); String methodName joinPoint.getSignature().getName(); String className joinPoint.getTarget().getClass().getSimpleName(); try { // 执行原方法 Object result joinPoint.proceed(); // 记录成功日志含耗时 log.info(Tool executed successfully [class:{}, method:{}, duration:{}ms], className, methodName, System.currentTimeMillis() - startTime); return result; } catch (ToolException e) { // 工具执行失败触发告警 String traceId MDC.get(traceId); String userId getCurrentUserId(); // 从ThreadLocal获取 // 构建告警参数关键包含完整上下文 MapString, Object alertParams Map.of( message, e.getMessage(), traceId, traceId, taskName, className . methodName, userId, userId, stackTrace, ExceptionUtils.getStackTrace(e), inputParams, Arrays.toString(joinPoint.getArgs()) ); // 异步发送告警避免阻塞主流程 CompletableFuture.runAsync(() - { try { wechatAlertTool.execute(alertParams); } catch (ToolException alertEx) { log.error(Failed to send wechat alert, alertEx); } }, alertExecutor); // 专用告警线程池 // 重新抛出异常让Planner感知失败 throw e; } } } // 告警消息中的“一键处置”按钮企微Markdown高级功能 // 在WechatAlertTool中构建如下内容 // **[立即重试](https://your-domain.com/api/v1/tool/retry?traceIdxxxtoolNamexxx)** // **[查看完整日志](https://skywalking-domain.com/trace?traceIdxxx)**实操心得告警消息必须包含traceId这是打通全链路的关键。我们用SkyWalking做APM告警里直接放跳转链接运维点开就能看到从WebSocket接入到DB查询的完整调用链。“一键重试”按钮背后是/agent/tool/retry接口它根据traceId还原原始参数重新触发Tool执行——这比让运维登录服务器查日志再手动执行高效10倍。告警线程池必须独立配置corePoolSize2, maxPoolSize5避免主业务线程被告警阻塞。我们曾因共用线程池导致告警风暴时订单创建接口全部超时。4. 实战问题排查那些文档里不会写的坑与解法4.1 大模型调用超时与熔断不是加try-catch就能解决现象LLM API偶尔响应慢10秒导致智能体接口整体超时用户看到空白页。根因分析RestTemplate默认无超时OkHttpClient虽可设timeout但LLM返回流式响应时readTimeout计算方式特殊从首字节开始计时而非连接建立。更致命的是当LLM服务雪崩时大量线程卡在IO等待线程池耗尽整个应用假死。解决方案分层超时控制连接超时connectTimeout1秒DNS解析TCP握手写超时writeTimeout2秒发送请求体读超时readTimeout8秒接收响应含流式chunkOkHttpClient okHttpClient new OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .writeTimeout(2, TimeUnit.SECONDS) .readTimeout(8, TimeUnit.SECONDS) // 关键比总超时少2秒 .build();熔断器兜底使用Resilience4j的TimeLimiter CircuitBreaker组合// 配置熔断器10秒窗口内失败率50%则熔断持续60秒 CircuitBreaker circuitBreaker CircuitBreaker.ofDefaults(llm-api); TimeLimiter timeLimiter TimeLimiter.of(Duration.ofSeconds(10)); // 执行调用 CompletableFutureString future Decorators .ofSupplier(() - llmClient.invoke(prompt)) .withCircuitBreaker(circuitBreaker) .withTimeLimiter(timeLimiter) .get();降级策略熔断开启时返回预设的静态知识库答案如FAQ而非报错if (circuitBreaker.getState() State.OPEN) { return faqService.getAnswerByKeyword(prompt); // 从本地H2数据库查FAQ }注意熔断器状态必须持久化我们用Redis存储CircuitBreaker状态避免单机重启后熔断失效。Resilience4j的CircuitBreakerRegistry支持RedisStateRepository。4.2 Excel图表生成失败POI的内存陷阱与EasyExcel救赎现象用户导出“销售趋势图”时服务OOMGC频繁Prometheus显示堆内存持续增长。根因溯源Apache POI的XSSFWorkbook在生成图表时会将整个Excel结构含图表XML加载到内存1MB Excel文件可能占用100MB堆内存。更糟的是POI的图表渲染依赖Apache Batik库其SVG解析器有内存泄漏多次调用后Metaspace耗尽。终极解法彻底弃用POI改用EasyExcel基于SAX解析内存占用降低90% Apache POI SXSSF仅用于图表的混合模式// 正确姿势数据用EasyExcel写图表用SXSSF单独生成 public byte[] generateSalesReport(ListSalesData data) { // Step 1: EasyExcel写入数据内存友好 ByteArrayOutputStream dataStream new ByteArrayOutputStream(); EasyExcel.write(dataStream, SalesData.class) .sheet(销售数据) .doWrite(data); // Step 2: 用SXSSFWorkbook追加图表仅加载图表所需最小XML Workbook workbook new SXSSFWorkbook(100); // 100行缓存 Sheet sheet workbook.createSheet(趋势图); // 创建图表关键不加载整个workbook只操作chart对象 Drawing? drawing sheet.createDrawingPatriarch(); ClientAnchor anchor drawing.createAnchor(0, 0, 0, 0, 1, 1, 10, 20); Chart chart drawing.createChart(anchor); // 设置图表数据源指向EasyExcel写入的数据区域 ChartDataSourceNumber xData DataSources.fromNumericCellRange( workbook.getSheetAt(0), new CellRangeAddress(0, data.size()-1, 0, 0) // A列日期 ); ChartDataSourceNumber yData DataSources.fromNumericCellRange( workbook.getSheetAt(0), new CellRangeAddress(0, data.size()-1, 1, 1) // B列销售额 ); // 渲染图表 LineChart lineChart (LineChart) chart; lineChart.plot(xData, yData); // 合并两个流将图表workbook写入dataStream末尾 workbook.write(dataStream); workbook.close(); return dataStream.toByteArray(); }实测数据生成1万行销售数据折线图POI方案峰值内存380MBEasyExcelSXSSF方案仅42MBGC次数从12次/分钟降至0.3次/分钟。4.3 DolphinScheduler任务状态同步轮询还是事件驱动现象用户发起“生成月度报表”任务后智能体长时间显示“执行中”实际DolphinScheduler早已完成。深度排查DolphinScheduler的OpenAPI /projects/{projectName}/process-instances/{processInstanceId} 接口返回状态有延迟缓存10秒。轮询间隔设太短1秒导致QPS过高DolphinScheduler限流设太长10秒用户体验差。最优实践采用混合模式——短周期轮询Webhook事件订阅智能体侧提交任务后启动30秒倒计时轮询间隔2秒覆盖95%的短任务。同时向DolphinScheduler注册Webhook/api/webhook/register监听process_instance_finished事件。DolphinScheduler侧需运维配合修改dolphinscheduler.propertiesalert.plugin.dir/opt/dolphinscheduler/alert-pluginsalert.plugin.webhook.enabledtrue编写Webhook插件当任务完成时POST到智能体的/agent/ds-webhook接口。智能体Webhook处理器PostMapping(/ds-webhook) public ResponseEntityVoid handleDsWebhook(RequestBody DsWebhookEvent event) { // 校验签名DolphinScheduler支持HMAC-SHA256 if (!verifySignature(event)) { return ResponseEntity.status(401).build(); } // 更新本地任务状态用Redis原子操作 String key ds:process: event.getProcessInstanceId(); redisTemplate.opsForValue().set(key, event.getStatus(), Duration.ofHours(1)); // 触发事件通知WebSocket客户端 applicationEventPublisher.publishEvent( new DsTaskCompletedEvent(event.getProcessInstanceId(), event.getStatus()) ); return ResponseEntity.ok().build(); }关键技巧Webhook事件必须做幂等处理我们用Redis SETNX命令以processInstanceId为key确保同一事件只处理一次。轮询作为保底Webhook作为主力两者结合将任务状态同步延迟从平均8秒降至200毫秒。4.4 Spring Boot Bean注入控制当“自动装配”变成灾难现象新增一个Tool后智能体突然无法启动报错NoSuchBeanDefinitionException: expected single bean but found 3。真相揭露某个Tool类如PdfExportTool意外继承了AbstractPdfExporter而该父类被ComponentScan扫到Spring试图注入它但抽象类无法实例化。更隐蔽的是Primary注解被滥用多个数据源配置类都标了Primary导致JPA Repository注入混乱。根治方案精确扫描包路径# application.yml spring: main: allow-bean-definition-overriding: false # 禁止覆盖提前暴露冲突 # 限定组件扫描范围 component-scan: base-packages: com.example.agent.tool, com.example.agent.core强制指定Bean名称Component(salesReportTool) public class SalesReportTool implements Tool { ... } Component(inventoryCheckTool) public class InventoryCheckTool implements Tool { ... }注入时明确指定Service public class AgentOrchestrator { // 不用Autowired改用Qualifier精确注入 Autowired Qualifier(salesReportTool) private Tool salesReportTool; Autowired Qualifier(inventoryCheckTool) private Tool inventoryCheckTool; }血泪教训在智能体项目中永远不要依赖ComponentScan的默认行为。我们曾因扫描了com.example.*意外加载了测试包下的MockTool导致生产环境调用假服务。现在所有Tool必须显式声明包路径且CI流水线加入检查mvn dependency:tree | grep -i test确保无测试依赖泄露。5. 可扩展性设计从单体智能体到企业级智能体平台5.1 多租户隔离同一套代码支撑不同业务线当财务部要“查报销进度”采购部要“查供应商评级”不能为每个部门部署一套服务。我们的方案是元数据驱动的租户路由// 租户上下文持有者 public class TenantContextHolder { private static final ThreadLocalString CONTEXT new ThreadLocal(); public static void setTenant(String tenantCode) { CONTEXT.set(tenantCode); } public static String getTenant() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } } // WebSocket连接时解析租户 Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 从URL参数或Header提取租户码 String tenantCode session.getUri().getQuery().split(tenant)[1]; TenantContextHolder.setTenant(tenantCode); // 加载租户专属能力从数据库查 ListToolDefinition tenantTools toolService.loadByTenant(tenantCode); toolRegistry.registerTenantTools(tenantCode, tenantTools); }关键细节租户码必须加密传输我们用AES-128加密tenant参数避免URL明文暴露。解密密钥存于KMS应用启动时拉取。5.2 能力热加载不重启服务新增工具运维要求“明天上线钉钉审批功能”你不能说“得停服半小时”。我们的热加载机制工具定义存数据库CREATE TABLE agent_tool_definition ( id BIGINT PRIMARY KEY, tenant_code VARCHAR(32), tool_name VARCHAR(64), -- 如dindtalk_approval class_name VARCHAR(255), -- 全限定类名 status ENUM(ENABLED,DISABLED) );动态类加载Service public class DynamicToolLoader { private final ClassLoader classLoader Thread.currentThread().getContextClassLoader(); public void loadTool(String className) throws Exception { Class? clazz classLoader.loadClass(className); // 检查是否实现Tool接口 if (!Tool.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException(Class must implement Tool interface); } // 反射实例化并注册 Tool instance (Tool) clazz.getDeclaredConstructor().newInstance(); toolRegistry.registerTool(instance); } }触发加载提供管理接口POST /admin/tool/load?classNamecom.example.tool.DingTalkApprovalTool调用后立即生效。注意热加载的类不能引用已加载类的静态变量否则ClassCastException。我们约定所有Tool必须无状态依赖通过构造器注入如Autowired DingTalkClient client由Spring容器管理。5.3 智能体能力市场让业务方自助配置工作流最终形态不是程序员写死流程而是业务方在后台拖拽配置。我们基于Activiti构建轻量工作流引擎// 用户在前端配置当收到审批通过事件 → 调用发送企微通知 → 调用DolphinScheduler生成报表 // 后台生成BPMN XML并部署 public void deployWorkflow(String tenantCode, WorkflowDefinition definition) { // 构建BPMN流程 BpmnModel model new BpmnModel(); Process process new Process(); process.setId(workflow_ UUID.randomUUID()); // 添加ServiceTask节点每个节点对应一个Tool ServiceTask notifyTask new ServiceTask(); notifyTask.setId(notify_task); notifyTask.setImplementation(send_wechat_alert); // Tool名称 ServiceTask reportTask new ServiceTask(); reportTask.setId(report_task); reportTask.setImplementation(schedule_dolphin_task); // 连接节点 SequenceFlow flow new SequenceFlow(); flow.setSourceRef(notifyTask.getId()); flow.setTargetRef(reportTask.getId()); process.addFlowElement(not