
1. 为什么要在 Java 生态里造一个 Agent Harness1.1 从一个真实痛点说起去年下半年我所在的团队接到一个内部需求把公司里散落在各个业务系统中的重复性操作——比如自动生成周报、跨系统数据核对、工单自动分派、会议纪要归档——统一收口到一个智能调度层。当时市面上主流的 Agent 框架基本是 Python 生态的LangChain、AutoGen、CrewAI 这些名字大家都不陌生。但问题来了我们整个技术栈是 Java核心业务系统跑在 Spring Boot 上运维体系、监控告警、权限模型、日志规范全是 Java 那一套。如果为了跑 Agent 单独引入一套 Python 服务意味着要额外维护一套运行时、一套依赖管理、一套部署流水线还要处理跨语言调用的序列化和超时问题。这不是技术选型的偏好问题而是工程成本的现实问题。一个中等规模的 Java 团队让运维去维护 Python 虚拟环境、pip 依赖冲突、GIL 并发限制光是这些就够喝一壶的。更别说企业级场景下对事务一致性、线程安全、可观测性的要求Python 生态在这些方面确实不如 Java 成熟。所以我的判断是Agent Harness 这个层不应该绑定在某一种语言生态上而应该是一个可以嵌入现有技术栈的中间层。BizBuddy 就是在这个背景下诞生的——一个纯 Java 实现的企业级 Agent Harness 平台。1.2 Agent Harness 到底是个什么东西先把概念说清楚。很多人把 Agent 和 Agent Harness 混为一谈其实两者是不同层次的东西。Agent是具体的智能体它有自己的角色设定、工具集、记忆机制和决策逻辑。比如一个周报生成 Agent它的职责就是收集本周的代码提交记录、任务完成情况、会议纪要然后按照模板生成一份周报。Agent Harness是承载和管理这些 Agent 的运行框架。它不关心某个 Agent 具体做什么它关心的是Agent 怎么注册、怎么发现、怎么调度、怎么隔离、怎么监控、怎么保证失败后能恢复。用个类比来说Agent 是赛车手Agent Harness 是赛道、维修站、计时系统和赛事规则的总和。BizBuddy 的定位就是后者。它要解决的核心问题是当企业里有几十个甚至上百个 Agent 在跑的时候怎么让它们有序、可靠、可观测地工作。1.3 纯 Java 实现的取舍逻辑选择纯 Java 不是拍脑袋决定的背后有几个硬性约束团队技能栈匹配团队里没有人有生产级 Python 工程经验强行上 Python 意味着学习成本和踩坑成本都要计入项目周期。运维体系复用现有的 CI/CD、日志采集、链路追踪、配置中心全是围绕 Java 构建的新平台直接接入就能用。线程模型可控Java 的虚拟线程Project Loom在 JDK 21 已经正式落地对于 Agent 这种大量 IO 等待的场景虚拟线程的性价比极高。类型安全企业级场景下Agent 之间的消息传递、工具调用的参数校验强类型语言能提前暴露大量问题。当然纯 Java 也有代价。比如 Python 生态里丰富的 LLM 客户端库、向量数据库 SDKJava 版本要么没有要么更新滞后。这部分我们后面会讲怎么补。2. BizBuddy 的整体架构设计2.1 分层架构与核心模块BizBuddy 的整体架构分成四层从下往上依次是基础设施层负责与外部系统交互包括 LLM 调用客户端、工具执行沙箱、持久化存储、消息队列。这一层的设计原则是可替换每个组件都定义接口具体实现可以按需切换。比如 LLM 客户端我们同时支持 HTTP 直连和 SDK 接入两种模式方便在不同网络环境下切换。核心运行时层是 BizBuddy 的心脏包含 Agent 注册中心、调度引擎、上下文管理器、状态机引擎。这一层决定了 Agent 怎么被创建、怎么被调度、怎么在多个步骤之间保持状态。能力扩展层提供 Agent 开发所需的各类能力组件包括工具库、记忆模块、提示词模板引擎、输出解析器。这一层是面向 Agent 开发者的他们在这里组装自己的 Agent。接入层对外暴露 REST API、WebSocket 推送、CLI 命令行三种接入方式同时提供管理控制台用于监控和调试。2.2 为什么选择状态机驱动Agent 的执行过程本质上是一个状态迁移过程接收输入 → 理解意图 → 选择工具 → 执行工具 → 观察结果 → 判断是否继续 → 输出结果。这个过程可能循环多次也可能在中途失败需要回滚。很多框架用循环 条件判断来实现这个流程代码写起来快但一旦 Agent 逻辑复杂起来就会变成一堆嵌套的 if-else调试起来非常痛苦。BizBuddy 选择用显式状态机来驱动 Agent 执行每个状态是一个独立的处理器状态之间的迁移由明确的条件决定。这样做的好处是可观测性强任何时候都能知道 Agent 处于哪个状态执行了多久下一步会去哪里。可恢复性好状态可以持久化服务重启后能从断点继续执行。可测试性高每个状态处理器可以单独测试不需要跑完整的 Agent 流程。代价是开发门槛稍微高一点Agent 开发者需要理解状态机的概念。但考虑到企业级场景对可靠性的要求这个代价是值得的。2.3 调度引擎的设计取舍调度引擎要解决的核心问题是当多个 Agent 同时请求 LLM 或工具资源时怎么分配。我们考虑过三种方案方案优点缺点适用场景单线程串行实现简单无并发问题吞吐量极低原型验证线程池并发吞吐量高Java 原生支持线程管理复杂容易 OOM中等规模虚拟线程吞吐量高资源占用低JDK 21 才支持大规模最终我们选择了虚拟线程 信号量限流的组合。虚拟线程负责处理 IO 等待信号量负责控制对 LLM API 的并发请求数。这样既能保证高吞吐又不会因为请求过多被上游限流。具体参数上我们默认设置 LLM 并发信号量为 10工具执行并发信号量为 50。这两个数字是根据实际压测结果调的LLM 调用延迟高通常 2-10 秒并发太高容易触发上游限流工具执行延迟低通常 100 毫秒以内可以适当放宽。3. 核心模块的实操细节3.1 Agent 注册与发现机制Agent 的注册采用注解 扫描的方式。开发者只需要在 Agent 类上标注BizAgent注解声明 Agent 的名称、版本、描述和所需工具BizBuddy 在启动时会自动扫描并注册。BizAgent( name weekly-report-agent, version 1.0.0, description 自动生成团队周报, tools {git-log-tool, task-query-tool, meeting-notes-tool} ) public class WeeklyReportAgent implements AgentHandler { Override public AgentResult handle(AgentContext context) { // Agent 逻辑 } }这里有个细节值得展开为什么用注解而不是配置文件。配置文件的好处是修改不需要重新编译但缺点是类型不安全写错了要到运行时才发现。注解的方式在编译期就能做校验而且 IDE 的代码补全和跳转支持更好。对于企业级项目我们更倾向于把配置固化在代码里通过版本管理来追踪变更。注册中心内部维护了一个ConcurrentHashMapkey 是 Agent 名称value 是 Agent 的元数据和工厂方法。发现机制支持按名称精确查找和按标签模糊匹配两种模式。标签匹配用于实现路由功能——比如所有带finance标签的 Agent 都走财务专用的 LLM 端点。3.2 上下文管理与记忆机制Agent 在执行过程中需要维护两类上下文会话上下文和长期记忆。会话上下文是单次执行过程中的临时数据包括用户输入、中间结果、工具调用记录等。这部分数据存在内存里执行结束后自动释放。我们用一个AgentContext对象来承载它本质上是一个类型安全的 Map支持按 key 存取任意对象。长期记忆是跨会话持久化的数据比如用户偏好、历史交互摘要、领域知识。这部分数据存在外部存储里我们默认用关系型数据库也支持接入向量数据库做语义检索。这里有个容易踩的坑上下文膨胀。Agent 执行多轮之后上下文里的消息会越来越多如果不加控制很快就会超出 LLM 的上下文窗口。我们的做法是引入一个上下文压缩器当消息数量超过阈值时自动把早期的消息摘要成一段简短描述。摘要本身也是一次 LLM 调用但成本远低于把完整历史塞进去。压缩阈值的设置需要权衡设得太低摘要调用频繁增加延迟和成本设得太高上下文窗口容易爆。我们的经验值是保留最近 10 轮完整对话更早的做摘要。这个数字可以根据具体模型的上下文窗口大小调整。3.3 工具调用的安全沙箱Agent 调用工具是整个系统里风险最高的环节。一个设计不当的工具可能让 Agent 执行任意代码、访问敏感数据、甚至发起恶意请求。BizBuddy 在工具调用上做了三层防护第一层是白名单机制。Agent 只能调用在注解里声明过的工具运行时如果尝试调用未声明的工具直接抛出异常。这防止了 Agent 被提示词注入攻击后调用意外工具。第二层是参数校验。每个工具定义了自己的参数 schema调用前会做类型检查和范围校验。比如一个查询订单工具订单号必须符合特定格式查询时间范围不能超过 30 天。第三层是执行隔离。工具在独立的线程池里执行有超时限制和资源配额。如果一个工具执行超过 30 秒还没返回会被强制中断并记录告警。public class ToolExecutor { private final ExecutorService toolPool Executors.newVirtualThreadPerTaskExecutor(); public ToolResult execute(ToolCall call, Duration timeout) { FutureToolResult future toolPool.submit(() - { // 参数校验 validateParams(call); // 执行工具 return call.getTool().invoke(call.getParams()); }); try { return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); throw new ToolTimeoutException(call.getToolName(), timeout); } } }注意虚拟线程虽然轻量但工具执行如果涉及 CPU 密集型操作仍然会占用载体线程。对于这类工具建议单独配置平台线程池避免拖慢整个虚拟线程调度器。3.4 LLM 调用的重试与降级策略LLM 调用是 Agent 执行中最不可靠的环节。网络抖动、上游限流、模型过载任何一种情况都会导致调用失败。BizBuddy 设计了一套分级重试策略第一级立即重试。针对网络超时这类瞬时故障立即重试一次间隔 100 毫秒。第二级退避重试。针对限流类错误采用指数退避初始间隔 1 秒最大间隔 30 秒最多重试 3 次。第三级降级切换。如果主模型连续失败自动切换到备用模型。备用模型可以是同厂商的不同规格也可以是不同厂商的等价模型。重试策略的配置放在 Agent 级别不同 Agent 可以根据自己的重要性设置不同的重试参数。比如核心业务 Agent 可以配置更激进的重试而实验性 Agent 失败就直接返回错误。这里有个经验重试一定要有上限并且要区分可重试错误和不可重试错误。参数校验失败、权限不足这类错误重试多少次都没用应该直接失败。只有网络类、限流类错误才值得重试。4. 企业级特性的实现细节4.1 可观测性体系搭建企业级平台和玩具项目的最大区别之一就是可观测性。BizBuddy 从三个维度构建可观测性指标Metrics方面我们暴露了以下核心指标指标名称类型说明agent_execution_totalCounterAgent 执行总次数按 Agent 名称和状态分组agent_execution_durationHistogramAgent 执行耗时分布llm_call_totalCounterLLM 调用次数按模型和结果分组llm_token_usageCounterToken 消耗量按模型分组tool_call_totalCounter工具调用次数按工具名和结果分组链路追踪Tracing方面每次 Agent 执行都会生成一个 Trace包含所有 LLM 调用和工具调用的 Span。这样当一次执行变慢时可以快速定位是哪个环节拖了后腿。日志Logging方面我们采用结构化日志每条日志都带 TraceId 和 Agent 名称。日志级别分得很细DEBUG 级别记录完整的提示词和响应INFO 级别记录关键决策点WARN 级别记录可恢复的异常ERROR 级别记录导致执行失败的问题。实操心得DEBUG 级别的日志在生产环境默认关闭但可以通过动态配置临时开启某个 Agent 的 DEBUG 日志。这个功能在排查线上问题时非常有用不用重启服务就能拿到详细日志。4.2 权限模型与多租户隔离企业里不同团队共用一套 Agent 平台时权限隔离是刚需。BizBuddy 的权限模型分三个层次租户隔离是最粗的粒度。每个租户有独立的 Agent 注册空间、独立的工具库、独立的存储。租户之间完全不可见连 Agent 名称都可以重复。角色权限是中间粒度。同一个租户内不同角色对 Agent 有不同的操作权限。比如开发者角色可以注册和修改 Agent运维角色可以查看监控和日志但不能修改 Agent 定义访客角色只能触发执行不能查看内部细节。资源配额是最细的粒度。每个 Agent 有独立的 Token 配额、并发配额、存储配额。配额用完后Agent 会被限流或拒绝执行。权限校验的实现用了 Spring Security 的过滤器链在每个 API 入口做拦截。Agent 执行时的权限校验则通过上下文传递确保 Agent 只能访问自己有权访问的资源。4.3 灰度发布与版本管理Agent 的更新不像普通代码更新那么简单因为 Agent 的行为受提示词、模型版本、工具实现等多个因素影响。BizBuddy 支持 Agent 的多版本共存和灰度发布。每个 Agent 注册时可以指定版本号平台同时保留多个版本。触发执行时可以指定版本也可以不指定让平台按灰度规则路由。灰度规则支持按用户 ID 哈希、按请求比例、按标签匹配等多种模式。版本回滚也很简单把流量切回旧版本即可不需要重新部署。这对于快速试错和紧急止损非常重要。5. 常见问题与排查技巧实录5.1 Agent 执行卡住不动怎么办这是最常见的问题表现是 Agent 执行后长时间没有输出日志也没有新内容。排查思路按以下顺序进行第一步看 LLM 调用是否超时。如果日志停在调用 LLM这一步说明是 LLM 端的问题。检查网络连通性、API Key 是否过期、是否触发了上游限流。第二步看工具执行是否死锁。如果日志停在执行工具这一步检查工具内部是否有死循环、是否在等待一个永远不会到达的资源。虚拟线程虽然轻量但如果工具内部用了synchronized且存在锁竞争仍然会阻塞。第三步看状态机是否进入死循环。如果日志显示状态在反复迁移但就是不结束说明状态机的迁移条件写错了。这种情况需要检查状态迁移图确认是否存在环。第四步看线程池是否耗尽。如果所有 Agent 都卡住可能是共享的线程池被占满了。检查线程池的活跃线程数和队列长度。5.2 Token 消耗异常增长的排查Token 消耗突然增长通常有以下几个原因上下文膨胀检查上下文压缩器是否正常工作压缩阈值是否设置合理。提示词冗余检查系统提示词是否包含了大量不必要的说明工具描述是否过于冗长。重试风暴检查是否有 Agent 在反复重试每次重试都消耗 Token。输出失控检查是否有 Agent 的输出没有长度限制导致模型生成了超长响应。排查时可以先按 Agent 维度统计 Token 消耗找出消耗最大的几个 Agent然后逐个分析。5.3 工具调用失败的常见原因错误类型典型表现排查方向参数校验失败提示参数类型不匹配或超出范围检查 Agent 生成的参数是否符合工具 schema权限不足提示无权访问某资源检查 Agent 的权限配置和资源归属超时工具执行超过设定时限检查工具内部逻辑是否有慢查询或死循环依赖不可用提示连接被拒绝或服务不可达检查工具依赖的外部服务状态序列化失败提示 JSON 解析错误检查工具返回的数据格式是否与声明一致5.4 几个踩过的坑坑一虚拟线程里用 ThreadLocal。虚拟线程数量可能非常多如果每个线程都往 ThreadLocal 里塞大对象内存会迅速膨胀。我们的做法是尽量用方法参数传递上下文必须用 ThreadLocal 的场景改用ScopedValueJDK 21 预览特性。坑二LLM 返回的 JSON 格式不稳定。即使提示词里明确要求返回 JSON模型有时还是会加一些额外的说明文字。我们的做法是在解析前先做一次清洗提取出第一个完整的 JSON 对象忽略前后的多余内容。坑三工具执行结果太大。有些工具返回的数据量很大直接塞进上下文会瞬间撑爆窗口。我们的做法是在工具层面做分页或者让工具返回摘要而不是全量数据。坑四Agent 之间的循环调用。Agent A 调用 Agent BAgent B 又调用 Agent A形成死循环。我们的做法是在上下文里记录调用链检测到循环时直接中断并告警。6. 性能优化与扩展性思考6.1 冷启动优化BizBuddy 启动时需要扫描所有 Agent 注解、初始化工具库、建立数据库连接。在 Agent 数量较多时冷启动可能达到几十秒。优化手段包括并行扫描用并行流替代串行扫描充分利用多核 CPU。懒加载工具库按需初始化不在启动时全部加载。连接池预热数据库连接池和 HTTP 连接池在启动时预热避免首次请求时的建连开销。6.2 水平扩展方案BizBuddy 本身是无状态的可以水平扩展多个实例。但有几个地方需要注意Agent 注册信息需要同步到所有实例我们用了配置中心做推送。执行状态需要持久化到共享存储否则实例重启后无法恢复。限流信号量需要分布式实现单机信号量在多实例下会失效。我们用了 Redis 做分布式信号量。6.3 后续可以扩展的方向这个平台目前已经能满足大部分企业级 Agent 场景的需求但还有几个方向值得继续投入Agent 编排支持多个 Agent 按 DAG 编排实现更复杂的业务流程。A/B 测试同一 Agent 的不同版本自动分流对比效果指标。成本分析按 Agent、按用户、按部门统计 Token 成本支持预算控制。插件市场让不同团队贡献工具和 Agent 模板形成内部生态。我在实际使用中最大的体会是Agent Harness 的价值不在于支持多少种模型、多少种工具而在于它能不能让 Agent 的开发、部署、运维变得像普通微服务一样简单。BizBuddy 在这条路上还有很多可以打磨的地方但至少方向是对的——用团队最熟悉的技术栈解决团队最实际的问题。