用Java自研企业级Agent Harness平台:架构设计与实践

发布时间:2026/10/13 5:06:29
用Java自研企业级Agent Harness平台:架构设计与实践
先说结论在对比了市面上主流的 Python Agent 框架后我决定用纯 Java 在内部从零搭建一个企业级 Agent Harness 平台也就是题目里的 BizBuddy。这名字听起来像个智能助手但本质上它是一套面向 Agent 应用的“运行时底座”负责承载、编排、治理和调度企业里越来越多的 Agent 任务。今天把设计过程、技术取舍和踩坑经历整理出来给正在纠结“Java 能不能做 Agent 平台”的朋友一个参考。这个平台解决的核心问题很明确当团队开始成规模地使用大模型能力时单点调用 API 的模式撑不住了。你需要统一接入模型、统一管理工具调用、统一处理审计和限流还要让多个 Agent 能协同完成一个复杂任务。适合的人群也很具体想在企业内部落地 Agent 应用、又不想引入 Python 技术栈的 Java 团队。下面直接进入正题。1. Agent Harness 到底在做什么1.1 框架和 Harness 的边界先说概念。Agent 框架解决的是“怎么写出一个 Agent”比如定义角色、拼接提示词、调用大模型、处理工具返回结果。而 Agent Harness 解决的是“怎么跑好一群 Agent”它们之间怎么编排、状态怎么保存、工具权限怎么控制、流量怎么限流、出现故障怎么恢复、调用过程怎么审计。这个区别看起来不大实际落地时差距非常大。单体 Demo 里你可以让 Agent 自己决定下一步但在企业环境里让每个 Agent 都自由发挥是不现实的。你需要一套确定性的执行骨架哪些步骤可以并行、哪些必须串联、哪些可以在失败后重试、哪些要阻断留给人去审批。BizBuddy 的设计目标就是这套骨架。它不倾向于做一个什么都干的 Agent 框架而是收敛到三个核心职责定义 Agent 的模型与配置、编排多 Agent 的执行流程、提供企业级基础设施鉴权、限流、审计、可观测。框架的活让框架干Harness 的活我们干。1.2 为什么企业需要自建市面上有很多 Agent 编排产品和开源组件为什么还要自建最直接的原因是工具调用链路上有大量的企业私有逻辑无法直接复用。以大模型调用工具为例。模型不会直接执行你的订单查询、库存扣减、状态更新方法它只会返回一个“我想要调用 queryOrder(id123)”的结构化结果。真正执行前你要做权限校验、参数二次校验、数据脱敏、审计留痕执行后你要决定把多少结果塞回上下文塞多了浪费 Token塞少了模型没法继续推理。这些逻辑必须长在自己的系统里而不是依赖某个开源项目的外置插件。还有一个现实原因现有的开源 Agent 框架大多基于 Python团队维护成本高。我们内部存量系统全是 Java基础设施监控、日志体系、配置中心都是围绕 Java 生态建设的。自建一个 Java 版 Harness可以无缝接入现有的运维链路不需要额外维护一套新的运行时。1.3 BizBuddy 的设计原则在设计之初我把需求拆成三条主线模型无关不能绑定任何一家模型供应商。通过统一的 AIProvider 接口接入不同的模型服务上层 Agent 不感知差异。执行可编排支持顺序、并行、条件分支、人工审批节点而不是简单的“循环让模型自己发挥”。企业级治理多租户隔离、操作审计、按调用量计费、限流熔断可以配置化落地。这三条主线贯穿了整个平台的设计。后面每讲到一块具体实现时你会发现所有取舍都是为了守住这几条底线。2. 技术选型与关键取舍2.1 为什么坚持纯 Java最容易被问的问题大模型生态基本都是 Python 的为什么坚持 Java我的答案是Agent 平台的核心不是模型而是工程。大模型对外暴露的就是 HTTP 接口Java 调 HTTP 和 Python 调 HTTP 没有本质区别。真正复杂的部分在于并发编排、状态管理、权限控制、稳定性保障这部分恰好是 Java 的主场。另外企业内部的 Agent 要落地必然要跟现有系统交互查数据库、调内部接口、发消息通知。这些系统大概率是 Java 写的用 Java 做 Harness 可以最小化跨语言桥接成本。比如 Tool 的参数模型可以直接和内部接口的 DTO 对齐不需要在 Python 侧再做一层转换。这里不是说 Python 不好而是团队已经有多年 Java 技术积累生产环境的稳定性实践、故障排查工具、性能调优经验都是现成的。用一个团队不熟悉的语言去重构一套新基建风险远大于收益。2.2 基础框架的取舍确定 Java 之后第一个纠结点是用什么基础框架。我实际对比了三个方向方向优点缺点我们的结论Spring Boot 技术栈生态成熟、人才好找、接入内部组件容易启动较重、函数式编程支持一般最终选择Quarkus 技术栈启动快、内存省、GraalVM 友好团队熟悉度低、生态相对窄暂不考虑Vert.x 技术栈响应式、性能上限高、灵活开发心智负担重、调试复杂局部使用最后选了 Spring Boot 作为主框架。理由很简单一个内部平台系统的瓶颈不会出现在 Web 框架层面而会出现在并发编排、状态设计、工具调用链路上。用最成熟的框架把这些核心逻辑稳住是性价比最高的选择。配合虚拟线程Java 21 的 Project LoomSpring Boot 的并发模型已经足够顺滑。一个 Agent 实例跑在一个虚拟线程里执行到模型调用时阻塞等待响应这个线程几乎不占资源。相比以前用线程池 CompletableFuture 做异步编排代码可读性和排错体验都上了一个台阶。2.3 模型接入层的抽象设计模型接入层是整个平台的“地基”。它的设计思路是定义一个 AIProvider 接口提供聊天补全和流式补全两个方法所有具体的模型实现都实现这个接口。public interface AIProvider { String providerName(); ChatResponse chat(ChatRequest request); StreamChatChunk chatStream(ChatRequest request); boolean supportsToolCalls(); }不要小看这个接口它的价值在于平台内部只依赖这个抽象所有 Agent 的逻辑都写在接口之上。新接入一个模型服务时只需要写一个实现类不用动任何上层代码。实际使用中我发现流式响应的接入特别容易忽略一个问题工具调用通常是在流式输出过程中被模型“喊停”的。模型可能先生成一段自然语言突然输出一个 tool_call 标记然后等你执行完工具再继续生成。所以在 AIProvider 实现里需要把流式分片拼装后识别出完整的工具调用块再汇总给上层。这里面有很多边缘情况要处理建议设计时就把“流式 工具调用”的组合场景纳入接口契约。2.4 序列化与上下文的取舍还有一个重要取舍是关于上下文管理。最开始我们想省事把模型会话的所有历史消息都存下来每次请求全量发给模型。跑了几个真实场景后发现Token 消耗增长快得惊人而且长上下文里模型对早期指令的遵循程度明显下降。于是我们对所有 Agent 会话引入了一套“滚动摘要 关键片段保留”策略超过一定轮次的早期对话先摘要化摘要结果放回上下文的开口处涉及工具执行结果的关键片段按重要性保留。这个策略虽然增加了实现复杂度但 Token 费用和响应质量这两项指标都得到了显著改善。3. 核心域模型与执行引擎3.1 实体模型怎么设计BizBuddy 的核心实体不多但每个都承担着不同职责AgentDefinitionAgent 的静态定义包含名称、性能、支持的模型、绑定的工具列表、超时时间、重试策略。AgentInstance一次会话中实际运行的 Agent 实例持有运行状态、上下文、临时变量。AgentTask提交给 Agent 的具体任务包含原始输入、目标输出、指定的执行图。ToolSpec工具的元信息包括方法描述、参数 Schema、是否需要审批。ExecutionGraph描述多个节点如何编排的有向图。用配置来管理 AgentDefinition 是一个很好的实践。下面是一段简化后的 YAML 配置agents: - name: order-agent model: default-chat tools: [query_order, check_refund_policy, notify_user] history: 20 timeout: 60s retry: 2 memoryStrategy: rolling-summary配置化意味着你可以随时调整一个 Agent 的行为而不需要发版。我们把配置存到了配置中心支持热更新。生产环境曾出现过一次模型供应商临时不可用的情况我们直接改配置把所有 Agent 切换到备用模型整个过程没有重启进程。3.2 编排模型自研轻量 DAG编排是 Agent Harness 最核心的能力。在选型时我们认真考虑过引入现成的 BPMN 工作流引擎比如 Flowable、Camunda 这类。但最终放弃了原因有两点一是 BPMN 的建模成本太高。Agent 编排场景大多数是“顺序执行几个工具、部分节点并行、有条件分支”用 BPMN 要定义流程图 XML还要拖拽建模工具对开发者来说太重了。二是 BPMN 是为人工审批流程和长期状态流程设计的而 Agent 编排更强调快速执行和自动重试。把它们耦合在一起会让轻量场景变得笨重。最终实现了一个轻量 DAG 执行引擎节点类型只有三种任务节点调用一个工具或者触发一个子 Agent。决策节点根据模型输出或代码逻辑判断走哪个分支。并行节点同时触发多个子节点全部成功才算成功支持部分失败策略。下面是一个用 Java 定义执行图的简化示例ExecutionGraph graph ExecutionGraph.newBuilder() .name(ticket_workflow) .addNode(categorize, new AgentNode(category-agent)) .addNode(resolve, new AgentNode(resolver-agent)) .addNode(notify, new ToolNode(notify_user)) .edge(categorize, resolve) .edge(resolve, notify) .build();执行引擎则非常简单直接。Java 21 的虚拟线程让并发代码变得异常简洁public ExecutionResult runPlan(AgentInstance instance, ExecutionGraph graph) { var nodeResults new ConcurrentHashMapString, Object(); try (var executor Executors.newVirtualThreadPerTaskExecutor()) { var futures graph.nodes().stream() .map(node - executor.submit(() - executeNode(instance, node, nodeResults))) .toList(); for (var future : futures) { future.get(futureTimeoutSeconds(), TimeUnit.SECONDS); } } return new ExecutionResult(nodeResults); }这个实现放弃了死锁检测、复杂依赖回溯等高级特性但换来了极低的理解成本。线上运行半年没有出现过编排层面的故障。3.3 状态管理与会话恢复AgentInstance 的生命周期状态我们设计了六个状态含义可迁移到CREATED实例已创建等待调度RUNNINGRUNNING正在执行任务WAITING_TOOL, SUCCEEDED, FAILEDWAITING_TOOL等待工具调用返回RUNNINGSUCCEEDED正常完成终态FAILED不可恢复失败终态TIMEOUT超时被终止终态状态存储使用 Redis 加数据库双写。Redis 保证高频读取的运行状态是瞬时的数据库用来记录完整的会话轨迹。这样设计的好处是恢复场景很顺畅进程重启后读取数据库里的实例快照重新构建上下文可以把一个卡在 WAITING_TOOL 的实例恢复到执行点。这里有一个细节值得注意不要在状态里存模型消息的完整列表。我们曾经把全部历史消息序列化进了实例快照结果大内存对象频繁触发 GC 问题。后来改为只存摘要加最近几轮原始消息状态体积缩小了几乎一个数量级。4. 工具调用链路的实现细节4.1 ToolSpec 与参数校验工具调用是 Agent 和业务系统交互的唯一通道必须设计得足够严谨。BizBuddy 对每个工具都要求定义 ToolSpec里面包含名称、功能说明、版本号、参数 JsonSchema、是否需要人工审批等字段。这里最核心的实践是参数校验必须先于工具执行并且在校验失败时把错误信息回传给模型。大模型偶尔会编造参数比如调用一个查订单工具时给出一个根本不存在格式的订单号。如果不做校验直接执行轻则返回脏数据重则触发业务异常。public record ToolSpec( String name, String description, String version, Schema parameterSchema, boolean requireApproval, SetString requiredRoles ) {}工具本身的实现也很简单每个工具是一个接口的实现。以查询订单为例public class QueryOrderTool implements AgentTool { public ToolSpec spec() { return new ToolSpec(query_order, 根据订单号查询订单状态, Map.of(orderId, new StringSchema().required(true)), false, Set.of(order:read)); } public ToolResult execute(ToolInvocation invocation) { String orderId invocation.argAsString(orderId); OrderVO order orderService.query(orderId); return ToolResult.ok(order, 订单查询成功); } }4.2 注册与动态发现工具的注册和发现使用了 Spring Boot 的自动配置。所有实现了 AgentTool 接口的 Bean 在启动时会被扫描转换成内部的 ToolRegistry并生成一份包含全部工具信息的索引。这个索引用两种方式提供服务一种是在每次请求把全部工具列表塞给模型适合工具数量少的场景另一种是预先建立工具关键词索引库先做一轮粗筛只把可能相关的工具描述发给模型。我们内部工具数量已经超过两百个全量塞入会造成 Token 浪费所以默认走第二种方式。粗筛的实现可以利用向量相似度也可以利用传统的关键词召回。我们当前用的是后者基于工具描述建立倒排索引根据模型的任务描述匹配关联度最高的十几个工具。实测下来准确率已经够用而且部署和运维成本比引入向量库低得多。4.3 工具执行生命周期一个完整的工具调用生命周期包含以下环节模型返回工具调用请求。Harness 验证工具是否存在、版本是否匹配。校验参数 Schema失败则把错误信息返回给模型重新规划。校验权限和审批策略需要人工审批的进入审批队列。脱敏处理对传给工具或从工具返回的数据做字段级脱敏。执行工具并记录耗时、入参、出参摘要。将工具执行结果转化为模型可读的文本片段。在第五步脱敏上我们吃过一次亏。一个查询客户信息的工具把手机号明文返回给了模型模型又把手机号作为自然语言输出给了用户。后来所有工具结果统一经过脱敏过滤器按照字段规则自动打码这个问题才算彻底解决。5. 企业级能力的落地5.1 多租户与权限隔离Agent 资源是共享的但使用方是多个团队隔离必须做好。BizBuddy 的资源模型引入了租户概念。每个租户只能访问自己域内的 Agent 定义、工具和数据。工具的调用权限通过 RBAC 模型控制具体到角色。这里容易忽略的是模型调用本身也需要租户级隔离。不同租户可能配置了不同的模型服务配额甚至不同等级的计费模式。我们把每一次模型调用都打上租户标签在统一网关层做总额度控制和按租户配额控制。5.2 限流、熔断与重试模型服务的外部依赖并不是永远稳定。实际运行中我们遇到过某一路模型服务响应变慢、5xx 错误增多的情况。面对这种情况不能只靠第三方兜底平台自己要有熔断机制。限流层我们使用了令牌桶算法。每个租户每分钟允许的调用次数可配置超过阈值的请求会快速失败并返回明确提示。熔断层使用健康计数和错误率统计连续 N 次失败或错误率超过阈值时暂时切断该路模型服务请求自动切换到备用模型。重试逻辑也要有一定的退避策略。大模型调用超时通常不是因为接口慢而是因为排队或网络抖动。直接立刻重试大概率还是不成功我们采用的策略是 200ms、500ms、1s 三档递增重试最多失败三次。5.3 可观测性与审计日志Agent 平台的排错难度远大于普通接口。一个任务可能经过路由 Agent、业务 Agent、多个工具调用、若干次模型请求任何一环出错问题都可能表现为“用户说结果不对劲”。如果平台不带全链路 Trace排查成本会非常高。BizBuddy 在底层接入了 Micrometer Observation 体系。每个 Agent 任务会生成一个 TraceId各节点的执行耗时、模型调用 Token 数、工具调用结果全部作为观测数据上报。排错时只要拿着用户的会话 ID 搜索就能看到完整链路。审计日志是用户容易忽略但企业内审时非常重视的部分。我们把模型输入的敏感操作和所有工具的实际执行记录到不可篡改的审计日志表里字段包括时间、租户、用户、Agent、工具名、参数摘要、结果摘要。这既满足了合规要求也为我们优化策略提供了宝贵的数据源。5.4 配置热更新与版本灰度Agent 的提示词模板、工具绑定关系、模型参数这类配置如果每次修改都要发版开发和运营的效率都会很低。BizBuddy 把 Agent 定义做成版本化配置由配置中心统一管理。新版发布时不直接覆盖线上旧版本而是先推送到测试环境验证再通过灰度策略放量。比如先让 5% 的流量走到新版本观察错误率和用户反馈稳定后再逐步扩大。如果发现异常一键回滚到上一个版本即可。这个机制表面看起来像常规的配置升级但实际处理时要额外小心 Agent 的状态兼容性——老会话续跑时用的是旧版本定义还是新版本定义我们这里采用的原则是“进行中的会话沿用旧版本新会话用新版本”避免出现上一个工具结果和新的提示词不匹配的问题。6. 踩坑记录与排查心得6.1 上下文无限增长导致的内存压力第一个严重问题是在压力测试阶段暴露的。我们最初把模型消息全部保存在实例内存里长会话跑上百轮之后每个实例的内存占用超过 100MB十几个并发实例直接把堆内存打爆。解决过程也走了弯路。一开始只简单粗暴调大堆内存效果不理想后来把“全量保留”改成“滚动摘要 关键片段保留”内存占用一下降到了原来的 1/8。每个 Agent 定义里增加了历史轮数上限超出部分强制摘要化。6.2 Agent 循环调用风暴在设计多 Agent 协作时我们允许一个 Agent 调用另一个 Agent。这个能力带来了一个隐患A 调用 BB 又调用 A假如没有终止条件调用链会一直递归下去把模型网关打到崩溃。排查时发现循环往往源于同一个任务描述里包含模糊的职责边界。对策有两个一是设置最大调用深度所有嵌套调用超过 5 层直接中止二是引入循环检测每次子任务创建时检查其祖先链里是否出现了相同 Agent 的同名任务一旦发现立即返回错误。6.3 虚拟线程的盲区虚拟线程确实简化了并发编码但它不是万能的尤其是阻塞资源调用的场景。我们在接入一个旧的 JDBC 连接池时发现连接池的内部分配锁会阻塞虚拟线程的调度。用了虚拟线程之后Java 21 官方就已经提示过 piloted 不适用于这种 synchronized 阻塞情况。实测下来确实会带来较大的响应延迟。解决方式分两种一是把阻塞型资源调用放到固定大小的平台线程池中避免虚拟线程被外部同步锁卡死二是优先使用支持虚拟线程友好的客户端比如响应式驱动或者新建的 IO 库。如果你的执行引擎里还要兼容老实现尽量在设计和评审阶段就排查掉这类阻塞点。6.4 工具幻觉引发的连锁错误最后一个问题是模型总会“幻觉”出工具。明明没有这个工具模型还要调用而且参数还编得理所当然。第一次遇到是在一个客户服务 Agent 上模型试图调用一个不存在的“退款审批工具”导致流程中断。应对方案有两层。第一层参数校验后发现工具不存在时把“工具不存在支持的工具列表”一起返回给模型诱导它重新规划第二层是在工具调用前加一个“工具存在性二次确认”避免因为本地工具索引过期导致误判。这两层加下来工具幻觉导致的中断率降低了非常多。做 Agent Harness 最深的体会是真正决定平台生死的不只是模型有多聪明更是工程底座有多稳。用 Java 构建这样一个底座在模型层之上做编排、治理和兜底是一条完全可行的路线。如果你也在团队内部规划类似的平台建议先从执行引擎和工具调用链路做起先跑通一个最小闭环再逐步把可观测性、多租户这些能力加进来。 BizBuddy 到今天已经支撑了好几个真实的业务场景每次迭代依然会觉得很多设计还有优化空间但最初那套“模型无关、执行可编排、企业级治理”的原则始终没有变过。