ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

纯Java Agent Harness平台设计与实践

纯Java Agent Harness平台设计与实践

1. 项目概述:为什么一个“纯 Java”的 Agent Harness 平台值得重做一遍?

最近在几个技术社区里,看到不少团队在聊“Agent 工程化”这件事——不是单个 LLM 调用 Demo,也不是写个 Prompt 就完事,而是真正在生产环境里跑起来、能灰度、能监控、能回滚、能和现有 ERP/CRM/BI 系统无缝对接的 Agent 流程。这时候,“Harness”这个词就频繁出现:它不是调度器,不是编排引擎,更不是胶水代码集合体;它是一套运行时契约层——定义 Agent 怎么注册、怎么被发现、怎么加载上下文、怎么调用工具、怎么返回结构化结果、怎么被熔断降级。市面上主流方案要么重度依赖 Python 生态(LangChain + FastAPI + Celery),要么绑定特定云厂商(AWS Bedrock Agents、Azure AI Agents),要么干脆用 Node.js 做轻量胶水层。但某金融后台系统、某制造业 MES 中台、某大型政企信创项目,它们的主干系统全是 Java 写的,JVM 进程稳定运行五年不重启是常态,Spring Boot + MyBatis + Dubbo 架构图贴在墙上十年没换过。让这些系统突然引入 Python 解释器或 Node 运行时?不是技术不行,是运维不敢批、安全团队要写三页风险评估报告、信创适配清单里根本没这俩名字。

所以当我在某次内部架构评审会上听到“我们要做一个纯 Java 的 Agent Harness 平台,代号 BizBuddy”时,第一反应不是兴奋,而是皱眉:这不是炫技,是补课。BizBuddy 不追求“支持 100 种模型 API”,它只解决三个刚性问题:第一,已有 Java 业务服务如何零改造暴露为 Agent 可调用的 Tool;第二,多个 Agent 如何共享同一套认证、审计、限流、日志埋点体系;第三,当用户说“帮我查下上季度华东区销售额并生成 PPT”,整个链路从自然语言解析、意图路由、多 Tool 协同、状态持久化到结果渲染,必须全程可追踪、可重放、可审计。它不替代 Spring Cloud Gateway,但要在网关之后再加一层语义网关;它不取代 XXL-JOB,但要把 Agent 的执行生命周期纳入统一任务治理。关键词“纯 Java”在这里不是技术洁癖,而是对稳定性、可观测性、线程模型一致性、以及与现有中间件生态兼容性的硬性承诺。适合谁?不是给想学大模型的大学生练手的,而是给那些已经在线上跑着 200+ 个 Spring Boot 微服务、每天处理千万级订单、连 JVM GC 日志都要存三年的中台团队。如果你的系统里还有@Transactional注解和DataSourceTransactionManager,那 BizBuddy 就是为你写的。

2. 整体架构设计:放弃“通用抽象”,拥抱“领域契约”

很多团队一上来就想设计一个“万能 Agent 框架”,定义IExecutor、IToolProvider、IOrchestrator三层接口,再搞个 SPI 加载机制,最后发现 80% 的代码都在写适配器——把 MyBatis 的Mapper包装成 Tool,把 Dubbo 的GenericService包装成 Tool,把 Quartz 的JobDetail包装成 Tool……BizBuddy 的第一个关键取舍,就是不提供通用 Tool 接口,只提供 BizTool 契约。这个契约只有四个方法:

public interface BizTool { // 工具唯一标识,如 "sales-report-query-v2" String toolId(); // 工具描述,用于 LLM 的 function calling 描述 String description(); // 输入参数 Schema,JSON Schema 格式字符串 String inputSchema(); // 执行逻辑,入参是已反序列化的 Map<String, Object>,出参是 Map<String, Object> Map<String, Object> execute(Map<String, Object> input) throws BizToolException; }

注意:execute()方法的入参不是自定义 POJO,而是Map<String, Object>;出参同理。这不是偷懒,而是刻意为之。原因有三:第一,LLM 的 function calling 返回的是 JSON Object,强转 POJO 需要反射+泛型擦除+类型推导,线上出错时堆栈信息极其晦涩;第二,业务系统里大量存在“动态字段”场景——比如 CRM 客户表的扩展属性是存在ext_attrsJSON 字段里的,用 Map 天然兼容;第三,也是最关键的,所有 Tool 的输入输出都走同一套 Jackson ObjectMapper 实例,且禁用FAIL_ON_UNKNOWN_PROPERTIES。这意味着前端传来的{ "region": "华东", "quarter": "2024-Q2", "include_ppt": true },后端 Tool 里可以直接input.get("region"),不用管它是不是SalesQueryRequest类型。我们牺牲了 IDE 的自动补全,换来了部署时零编译、零 classpath 冲突、零序列化版本兼容问题。

第二个取舍是放弃“智能编排”,专注“确定性路由”。BizBuddy 不内置 Plan-and-Execute 或 ReAct 等复杂推理循环。它只做一件事:根据用户原始 query,通过一个轻量级规则引擎(基于 AviatorScript)匹配预定义的BizIntent,然后路由到对应BizAgent。BizIntent是一个 YAML 文件:

intentId: sales-summary-report triggerPatterns: - "查.*销售额" - "看.*业绩汇总" - "给我.*销售报表" requiredTools: - sales-report-query-v2 - ppt-generator-v1 outputTemplate: | {{#if result.pptUrl}} 报告已生成,点击下载:{{result.pptUrl}} {{else}} 数据获取失败,请稍后重试。 {{/if}}

这个设计背后是血泪教训:某次上线后,LLM 把“查上季度华东销售额”错误识别为“客户满意度分析”,触发了完全无关的 Tool 链,导致下游数据库被高频扫描。而规则引擎匹配是确定性的,triggerPatterns支持正则和模糊匹配(用 Jieba 分词预处理),匹配失败就 fallback 到兜底 Agent,不会胡乱执行。BizAgent 本身就是一个 Spring Bean,它的execute()方法接收BizIntentContext(含原始 query、匹配到的 intent、用户身份、会话 ID),然后按requiredTools顺序调用,每个 Tool 执行后,结果自动注入下一个 Tool 的 input。没有异步回调,没有状态机,没有 Saga 模式——所有状态都存在 Redis 的 Hash 结构里,Key 是biz:session:{sessionId}:state,Field 是 Tool 名,Value 是 JSON 字符串。这样做的好处是:排查问题时,直接HGETALL biz:session:abc123:state就能看到每一步的输入输出,比翻几百行日志快十倍。

第三个取舍是拒绝“模型即服务”,坚持“模型即配置”。BizBuddy 不托管模型权重,不提供训练接口,不封装 HuggingFace Pipeline。它只管理模型的 HTTP Endpoint、Token 认证方式、超时时间、重试策略。配置长这样:

models: - id: qwen2-7b-chat endpoint: https://llm-gateway.internal/api/v1/chat/completions authType: bearer-token token: ${LLM_API_TOKEN} timeoutMs: 30000 maxRetries: 2 # 模型能力声明,用于 Intent 匹配时的兜底判断 capabilities: - function-calling - json-mode - streaming

为什么?因为真实企业里,模型服务从来不是孤立存在的。它可能跑在 GPU 集群上,由另一个团队维护;可能调用的是私有化部署的千问,也可能调用的是合规审查过的 Azure OpenAI;甚至同一个 Agent,在测试环境用本地 Qwen,生产环境必须切到合规模型。如果框架把模型能力写死在代码里,每次切换都要改源码、重新打包、走发布流程。而配置驱动,只需要改 YAML、发个 ConfigMap、触发一次@RefreshScope,5 秒内生效。BizBuddy 的模型客户端底层用的是 OkHttp + Retrofit,所有请求头、SSL 证书、代理设置都走 Spring Boot 的RestTemplateCustomizer,和现有微服务调用习惯完全一致。

3. 核心模块实现:从 Tool 注册到会话状态持久化

3.1 BizTool 的零侵入注册机制

业务系统接入 BizBuddy,最怕改原有代码。BizBuddy 提供两种注册方式:注解式和 API 式。注解式适用于新开发的 Tool:

@Component @BizToolMeta( toolId = "inventory-check-v3", description = "查询指定仓库指定商品的实时库存数量,支持批次号过滤", inputSchema = """ { "type": "object", "properties": { "warehouseCode": { "type": "string" }, "skuId": { "type": "string" }, "batchNo": { "type": "string", "nullable": true } }, "required": ["warehouseCode", "skuId"] } """ ) public class InventoryCheckTool implements BizTool { @Autowired private InventoryService inventoryService; @Override public String toolId() { return "inventory-check-v3"; } @Override public String description() { return "..."; } @Override public String inputSchema() { return "..."; } @Override public Map<String, Object> execute(Map<String, Object> input) { String warehouseCode = (String) input.get("warehouseCode"); String skuId = (String) input.get("skuId"); String batchNo = (String) input.get("batchNo"); // 直接调用现有业务 Service,无任何包装 Integer stock = inventoryService.getStock(warehouseCode, skuId, batchNo); return Map.of("stock", stock, "warehouseCode", warehouseCode, "skuId", skuId); } }

关键点在于@BizToolMeta注解。BizBuddy 启动时,通过 Spring 的BeanPostProcessor扫描所有@BizToolMetaBean,提取元数据存入内存注册中心(ConcurrentHashMap),同时校验inputSchema是否为合法 JSON Schema(用 everit-org/json-schema)。这个过程不依赖任何 AOP 或字节码增强,纯粹是 Spring 生命周期管理,启动耗时 < 50ms。

对于老系统,无法加注解怎么办?BizBuddy 提供BizToolRegistryAPI:

// 在 legacy-system 的 ApplicationRunner 中调用 bizToolRegistry.register(new BizTool() { @Override public String toolId() { return "legacy-order-status-v1"; } @Override public String description() { return "查询老订单系统中的订单状态(HTTP 调用)"; } @Override public String inputSchema() { return "{\"type\":\"object\",\"properties\":{\"orderId\":{\"type\":\"string\"}},\"required\":[\"orderId\"]}"; } @Override public Map<String, Object> execute(Map<String, Object> input) { String orderId = (String) input.get("orderId"); // 调用老系统的 SOAP 接口 return legacyOrderClient.queryStatus(orderId); } });

这里有个隐藏技巧:BizToolRegistry.register()是线程安全的,且支持热注册。某次灰度时,发现某个 Tool 的inputSchema写错了,导致 LLM 传参格式不对。运维直接在 Grafana 里点个按钮,触发一个 Actuator Endpoint,动态 reload 了这个 Tool 的 Schema,5 秒后新请求就恢复正常,完全不用重启服务。

3.2 BizIntent 的动态加载与热更新

BizIntent文件默认放在src/main/resources/intents/下,启动时加载。但生产环境需要热更新。BizBuddy 采用“双存储”策略:本地文件系统作为基准,Redis 作为运行时覆盖层。结构如下:

  • Redis Key:biz:intent:config:version→ 存储当前生效的配置版本号(如v20240520-001)
  • Redis Key:biz:intent:config:v20240520-001→ Hash 结构,Field 是 intentId,Value 是 YAML 字符串

启动时,BizBuddy 先读本地文件生成v0版本,再检查 Redis 中是否有更高版本。如果有,则用 Redis 数据覆盖内存中的 Intent Registry。热更新流程是原子的:运维先HSET biz:intent:config:v20240520-002 xxx yyy,再SET biz:intent:config:version v20240520-002。BizBuddy 的IntentRefresher定时任务(默认 30 秒)检测到 version 变更,就全量 reload。reload 过程是双缓冲的:新 Intent 加载到临时 registry,校验通过后,原子替换主 registry 的引用。整个过程业务请求无感知,旧请求继续用老 Intent,新请求立即用新 Intent。

为什么不用 ZooKeeper 或 Nacos?因为某次压测发现,当 Intent 配置超过 200 个时,Nacos 的长连接推送在高并发下有 3~5 秒延迟,导致部分请求匹配到过期 Intent。而 Redis 的SET命令是微秒级的,且 BizBuddy 自己控制刷新节奏,比依赖外部配置中心更可控。

3.3 会话状态的分层持久化设计

Agent 执行不是原子操作,一个“生成销售报告”可能跨 3 秒、调 2 个 Tool、产生 4KB 中间数据。状态必须持久化,但不能全扔数据库——太重;也不能全放内存——节点宕机就丢。BizBuddy 采用三级缓存策略:

层级存储介质TTL用途容量占比
L1ThreadLocal请求生命周期当前请求的临时变量、中间结果缓存< 5%
L2Redis Hash (biz:session:{id}:state)24 小时所有 Tool 的输入输出、执行时间戳、错误堆栈~70%
L3MySQL 表biz_session_log永久关键字段:session_id、user_id、query、intent_id、status、cost_ms、created_at~25%

L2 和 L3 的写入是异步的。BizBuddy 用@Async+ 自定义线程池(核心 4,最大 16,队列 1000)将状态变更事件发到内存队列,再批量刷入 Redis 和 MySQL。这样即使 Redis 瞬间不可用,状态也不会丢失,只是查询慢一点;MySQL 挂了,最多丢 1 秒内的日志,不影响执行。

MySQL 表结构经过精心设计:

CREATE TABLE `biz_session_log` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `session_id` VARCHAR(64) NOT NULL COMMENT '会话ID,全局唯一', `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID', `query` TEXT NOT NULL COMMENT '原始用户query', `intent_id` VARCHAR(64) DEFAULT NULL COMMENT '匹配到的intentId', `tool_trace` JSON DEFAULT NULL COMMENT 'Tool执行链路,[{toolId:, input:, output:, costMs:, error:}]', `status` ENUM('success','failed','timeout','rejected') NOT NULL DEFAULT 'success', `cost_ms` INT NOT NULL DEFAULT 0 COMMENT '总耗时毫秒', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), INDEX `idx_user_created` (`user_id`, `created_at`), INDEX `idx_session` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BizBuddy 会话执行日志';

重点在tool_trace字段。它不是存完整 JSON,而是只存关键字段:toolId、input的摘要(SHA256 前 8 位)、output的摘要、costMs、error的简短消息(如TimeoutException: connect timed out)。完整 input/output 存在 Redis 里,MySQL 只存索引。这样单条日志控制在 2KB 内,百万级日志表查询依然流畅。某次审计要求查“张三昨天所有失败会话”,SELECT * FROM biz_session_log WHERE user_id='zhangsan' AND status='failed' AND created_at > '2024-05-19 00:00:00',100 万数据 0.3 秒返回,DBA 点头说:“这设计可以”。

3.4 安全与可观测性嵌入式设计

企业级平台,安全不是附加功能,是基因。BizBuddy 的安全控制全部下沉到BizAgent.execute()的前置拦截器里:

public class BizAgentSecurityInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 1. JWT Token 解析,提取 user_id, tenant_id, roles String token = resolveToken(request); JwtClaims claims = jwtParser.parse(token); // 2. 权限校验:每个 BizIntent 关联一个权限码,如 "sales:report:read" String intentId = getRequestedIntentId(request); String requiredPermission = intentPermissionMap.get(intentId); if (!claims.getRoles().contains(requiredPermission)) { throw new BizSecurityException("Access denied for intent: " + intentId); } // 3. 敏感词过滤:对原始 query 做 DFA 过滤,命中则记录审计日志并拒绝 String query = request.getParameter("query"); if (sensitiveWordFilter.contains(query)) { auditLogger.warn("Sensitive word detected: {} by user {}", sensitiveWordFilter.match(query), claims.getUserId()); throw new BizSecurityException("Query contains prohibited content"); } return true; } }

可观测性更是无处不在。BizBuddy 默认集成 Micrometer + Prometheus:

  • 每个 Tool 执行打点:biz_tool_execution_seconds_count{toolId="sales-report-query-v2",status="success"}
  • 每个 Intent 匹配打点:biz_intent_match_count{intentId="sales-summary-report",matched="true"}
  • 会话耗时直方图:biz_session_cost_seconds_bucket{le="1.0","le="2.0",...}

但最关键的,是OpenTelemetry 全链路追踪。BizBuddy 的 Trace Context 从 Controller 层就开始注入:

@PostMapping("/v1/agent/execute") public ResponseEntity<BizResponse> execute(@RequestBody BizRequest request) { // 创建 Span,注入 traceId Span span = tracer.spanBuilder("biz-agent-execute") .setAttribute("biz.query", request.getQuery()) .setAttribute("biz.user_id", request.getUserId()) .startSpan(); try (Scope scope = span.makeCurrent()) { BizResponse response = bizAgent.execute(request); span.setAttribute("biz.status", "success"); return ResponseEntity.ok(response); } catch (Exception e) { span.setAttribute("biz.status", "error"); span.recordException(e); throw e; } finally { span.end(); } }

Tool 内部也能延续 Trace:

@Override public Map<String, Object> execute(Map<String, Object> input) { Span current = Span.current(); Span toolSpan = tracer.spanBuilder("tool-inventory-check-v3") .setParent(Context.current().with(current)) .setAttribute("tool.input.warehouseCode", (String) input.get("warehouseCode")) .startSpan(); try (Scope scope = toolSpan.makeCurrent()) { // 执行业务逻辑 return inventoryService.getStock(...); } finally { toolSpan.end(); } }

这样在 Jaeger 里,一个会话的 Trace 是树状的:biz-agent-execute→biz-intent-match→tool-sales-report-query-v2→tool-ppt-generator-v1,每个节点都有耗时、标签、错误。某次发现 PPT 生成慢,直接点开tool-ppt-generator-v1Span,看到http.url标签是https://ppt-service.internal/generate,http.status_code是 503,立刻定位到下游 PPT 服务雪崩,而不是在 BizBuddy 日志里大海捞针。

4. 实操避坑指南:那些文档里不会写的血泪经验

4.1 JSON Schema 校验的陷阱与绕过方案

BizTool 的inputSchema必须是合法 JSON Schema,这是为了保证 LLM 能正确生成参数。但实际落地时,业务同学写的 Schema 经常踩坑。最常见的三个问题:

  1. $ref引用外部文件:有人写"properties": { "address": { "$ref": "https://schema.example.com/address.json" } }。BizBuddy 启动时会尝试 HTTP GET,超时卡住。解决方案:启动时禁用$ref解析,只支持内联 Schema;提供SchemaValidator工具类,让开发在 IDE 里提前校验。

  2. oneOf/anyOf导致 LLM 生成歧义 JSON:比如{"oneOf": [{"type":"string"},{"type":"number"}]},LLM 可能生成"123"(字符串)或123(数字),后端Map<String, Object>接收时类型不一致。解决方案:强制要求oneOf场景必须用discriminator,且 BizBuddy 的JsonSchemaParser会自动添加type字段校验。

  3. 日期格式不统一:有人写"type": "string", "format": "date-time",但业务系统期望yyyy-MM-dd。LLM 生成的可能是2024-05-20T00:00:00Z。解决方案:BizBuddy 的JacksonObjectMapper配置了DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY和SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,并提供@JsonFormat(pattern="yyyy-MM-dd")注解支持,但仅限于 Tool 内部转换,不污染契约。

提示:我们最终在 CI 流程里加了一条检查:mvn compile exec:java -Dexec.mainClass="com.bizbuddy.SchemaChecker",扫描所有@BizToolMeta.inputSchema,用json-schema-validator库做静态校验,不通过直接 fail build。上线前的 Schema 质量提升了 90%。

4.2 Redis 状态存储的并发冲突实战解法

多个 Tool 并行执行时,会同时写 Redis 的同一个 Hash Key(biz:session:{id}:state)。虽然HSET是原子的,但“读-改-写”模式会丢数据。比如 Tool A 读到{"step1":"done"},准备写{"step1":"done","step2":"running"};Tool B 同时读到{"step1":"done"},写{"step1":"done","step3":"running"}。最终结果是{"step1":"done","step3":"running"},Tool A 的step2丢了。

标准解法是 Lua 脚本:

-- redis-script-set-tool-state.lua local sessionId = KEYS[1] local toolId = ARGV[1] local stateJson = ARGV[2] local timestamp = ARGV[3] return redis.call("HSET", "biz:session:"..sessionId..":state", toolId, stateJson..":"..timestamp)

但实测发现,当并发 > 500 QPS 时,Lua 脚本执行时间波动大,平均 2ms,P99 达到 15ms,拖慢整体响应。最终我们采用Redis Stream + Consumer Group方案:

  • 每个会话一个 Stream:XADD biz:session:abc123:stream * toolId inventory-check-v3 input {"warehouseCode":"WH001"}
  • 单独一个 Consumer Groupbiz-session-state-writer,只有一个消费者,顺序消费所有 Stream 事件,合并写入 Hash。
  • 这样写入是严格有序的,且 Consumer 可以 ACK,失败重试。压测显示,1000 QPS 下,P99 写入延迟稳定在 3ms 内。

注意:Stream 方案增加了架构复杂度,但换来的是确定性。我们把它做成可选模块,默认用 HSET,高并发场景手动开启 Stream 模式。不要为了“先进”而增加不必要的复杂度。

4.3 意图匹配的误判率优化实战

规则引擎匹配不是万能的。某次上线后,triggerPatterns里写了- "查.*销售额",结果用户问“查一下销售额有没有异常”,被匹配到,但实际该 Intent 只支持正常查询,不支持异常检测。误判率一度达 12%。

优化分三步:

  1. 前置 NLP 过滤:接入轻量级中文分词(HanLP 的 mini 版),对 query 做关键词提取。BizIntent配置里增加requiredKeywords: ["销售额", "查询"],forbiddenKeywords: ["异常", "预警", "监控"]。匹配时先过关键词关,再进正则。

  2. LLM 辅助校验:对高价值 Intent(如涉及资金操作),启用“双校验”模式。规则引擎匹配后,再调一次小模型(Qwen1.5-0.5B),Prompt 是:“请判断以下用户问题是否属于【销售报表查询】意图?问题:{query}。回答:是/否。理由:...”。只取“是/否”二字,耗时 < 200ms,准确率提升到 99.2%。

  3. 人工反馈闭环:在前端加一个“反馈此结果不准”按钮,点击后把session_id+query+matched_intent_id发到 Kafka。后台 Flink 作业实时统计误判 Top 10,每天邮件推送给运营同学,他们直接在管理后台修改triggerPatterns,2 小时内生效。

实操心得:不要迷信单一技术。规则引擎快而准,LLM 灵活但贵,人工反馈慢但可靠。BizBuddy 的设计哲学是“用最简单的方法解决 80% 的问题,对剩下的 20% 分层治理”。

4.4 生产环境 JVM 参数调优实录

BizBuddy 是 CPU 密集型应用:JSON 解析、Schema 校验、Aviator 表达式计算、Trace Span 创建。上线初期,某次大促流量突增,JVM Full GC 频繁,-XX:+PrintGCDetails显示老年代每 5 分钟 GC 一次,STW 时间 1.2 秒。

根因分析发现两个问题:

  • Jackson ObjectMapper 复用不足:每个 Tool 执行都 new 一个ObjectMapper,创建成本高,且内部JsonFactory缓存未复用。解决方案:全局单例ObjectMapper,配置configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)等常用选项,启动时预热readValue("{}")。

  • AviatorScript 编译缓存缺失:triggerPatterns的正则表达式每次匹配都重新编译。解决方案:用ExpressionCache缓存编译后的Expression对象,Key 是正则字符串,最大容量 1000。

JVM 参数最终定为:

-Xms4g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+UnlockExperimentalVMOptions \ -XX:G1NewSizePercent=30 \ -XX:G1MaxNewSizePercent=60 \ -XX:G1HeapRegionSize=2M \ -XX:G1ReservePercent=15 \ -XX:G1HeapWastePercent=5 \ -XX:G1MixedGCCountTarget=4 \ -XX:InitiatingOccupancyFraction=45 \ -XX:+AlwaysPreTouch \ -XX:+UseStringDeduplication \ -Dfile.encoding=UTF-8

关键点:-XX:+UseStringDeduplication减少重复 JSON 字符串内存占用;-XX:+AlwaysPreTouch避免运行时内存页分配抖动;G1HeapRegionSize=2M适配 4G 堆内存。调优后,Full GC 消失,Young GC P99 < 50ms,CPU 使用率从 95% 降到 65%。

最后分享一个小技巧:BizBuddy 的 Actuator Endpoint/actuator/bizbuddy/status返回实时指标:当前在线 Tool 数、Intent 数、Redis 连接数、最近 1 分钟成功率。运维同学不用登录服务器,直接 curl 这个地址就能看健康状态。真正的生产友好,不是写在 PPT 上的,是刻在代码里的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进