多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

纯Java打造企业级Agent Harness:BizBuddy架构设计与实操

纯Java打造企业级Agent Harness:BizBuddy架构设计与实操 1. 为什么我要用纯 Java 造一个 Agent Harness1.1 从一个真实的痛点说起去年下半年我所在的团队接了一个内部效率工具的需求。业务方想要一个能自动处理工单、查询知识库、调用内部审批接口的智能助手。听起来像是典型的 Agent 应用场景对吧我一开始也是这么想的于是花了两周时间调研市面上的各种 Agent 框架。调研结果让我有点意外。大部分主流框架要么是 Python 生态的要么是 TypeScript 生态的Java 这边能拿得出手的企业级方案少得可怜。有几个开源项目看着还行但深入看代码后发现要么是个人项目级别的完成度要么是绑定了特定云厂商的服务想私有化部署还得改一大堆东西。更关键的问题在于我们团队的技术栈是纯 Java。后端服务、中间件、监控体系全是 Java 那一套。如果为了一个 Agent 功能引入 Python 运行时运维成本、部署复杂度、团队学习成本都会陡增。我当时算了一笔账引入 Python 生态意味着要维护两套依赖管理体系、两套日志采集方案、两套监控告警配置长期来看这笔账不划算。所以我做了一个决定自己造一个。这就是 BizBuddy 的起点。1.2 BizBuddy 到底是个什么东西用一句话说BizBuddy 是一个纯 Java 实现的企业级 Agent Harness 平台。这里的关键词是 Harness不是 Framework。Framework 和 Harness 的区别我打个比方你就明白了。Framework 像是给你一堆乐高积木你得自己设计图纸、自己拼装。Harness 像是给你一个已经搭好骨架的模型你只需要往上面挂载具体的功能模块。BizBuddy 提供的是 Agent 运行所需的全套基础设施任务调度、上下文管理、工具注册、记忆存储、可观测性、安全管控。你只需要实现具体的业务逻辑剩下的脏活累活它帮你搞定。它解决的问题很明确让 Java 团队能够用自己熟悉的技术栈快速构建和部署生产级的 Agent 应用。不需要跨语言调用不需要引入外部运行时不需要为了一个功能重构整个技术架构。适合谁来参考我认为有三类人。第一类是 Java 后端工程师想在自己的项目里加入 Agent 能力但不想换技术栈。第二类是架构师正在评估企业级 Agent 平台的选型方案。第三类是对 Agent 底层原理感兴趣的技术人想看看一个 Harness 平台到底需要哪些核心组件。2. 整体架构设计与核心取舍2.1 为什么选择 Harness 而不是 Framework这个决策背后有一个很实际的考量。企业级应用和 Demo 应用最大的区别在于Demo 追求的是快速跑通企业级追求的是稳定可控。我见过太多团队用 Framework 快速搭了一个 Agent Demo演示效果很好但一到生产环境就各种问题。上下文丢失、工具调用超时、并发冲突、日志缺失、权限失控。这些问题在 Demo 阶段不会暴露因为 Demo 的并发量低、场景简单、容错要求低。但企业级场景下每一个问题都可能变成事故。Harness 的设计哲学是把 Agent 运行时的复杂性封装起来暴露给业务开发者的接口尽可能简单。BizBuddy 的核心模块包括Agent 生命周期管理负责 Agent 的创建、初始化、运行、暂停、销毁上下文引擎管理对话历史、工具调用记录、中间状态工具注册中心统一管理所有可被 Agent 调用的工具记忆存储层短期记忆和长期记忆的读写可观测性模块日志、指标、链路追踪安全管控层权限校验、敏感信息过滤、调用频率限制这些模块的设计原则是可替换、可扩展、可降级。比如记忆存储层默认实现是基于内存的但你可以替换成 Redis 或者数据库实现。工具注册中心支持动态注册和热插拔不需要重启服务。2.2 纯 Java 的技术选型考量既然决定用 Java接下来的问题是用哪些 Java 技术栈我评估了几个维度。首先是运行时Java 17 是当前企业级应用的主流选择LTS 版本虚拟线程特性对 Agent 这种 IO 密集型场景很友好。然后是框架Spring Boot 3.x 是自然选择生态成熟团队熟悉。但这里有个取舍Spring Boot 的自动配置和依赖注入虽然方便但会增加启动时间和内存开销。对于 Agent Harness 这种需要快速启动、频繁创建销毁的场景我需要控制 Spring 的使用范围。最终方案是核心运行时用纯 Java 实现不依赖 Spring 容器。只在需要集成外部服务时通过 SPI 机制加载 Spring 相关的适配器。这样既保证了核心的轻量性又保留了集成的灵活性。工具调用协议方面我选择了基于 JSON-RPC 的自定义协议而不是直接暴露 HTTP 接口。原因很简单Agent 调用工具的频率很高HTTP 的每次请求都要建立连接、解析头部开销太大。JSON-RPC 可以复用连接而且协议更简洁。当然如果需要对外暴露 HTTP 接口BizBuddy 也提供了 HTTP 适配器。2.3 核心数据模型设计Agent Harness 的核心数据模型其实不复杂但设计好坏直接影响后续的扩展性。BizBuddy 的核心模型包括AgentDefinitionAgent 的静态定义包括名称、描述、系统提示词、可用工具列表、模型配置。这个对象是不可变的创建后不会修改。AgentInstanceAgent 的运行时实例持有当前会话的上下文、状态、执行历史。每个会话对应一个实例。ExecutionContext执行上下文贯穿一次完整的 Agent 调用链路。包含输入、输出、中间步骤、工具调用记录、耗时统计。ToolDefinition工具的元数据定义包括名称、描述、参数 schema、返回值 schema、权限要求。MemoryEntry记忆条目包含内容、类型短期/长期、时间戳、关联的会话 ID。这些模型的设计遵循一个原则不可变优先。AgentDefinition 和 ToolDefinition 都是不可变的AgentInstance 和 ExecutionContext 是可变但线程安全的。这样做的好处是在多线程环境下不需要额外的同步开销也方便做快照和回放。3. 核心模块的实操细节3.1 Agent 生命周期管理Agent 的生命周期管理是 Harness 的核心。BizBuddy 把 Agent 的生命周期分为五个阶段创建、初始化、运行、暂停、销毁。创建阶段做的事情很简单根据 AgentDefinition 实例化一个 AgentInstance。这个阶段不涉及任何外部调用所以很快。初始化阶段是真正耗时的部分。需要加载工具列表、初始化记忆存储、建立模型连接。这里有个优化点BizBuddy 支持懒初始化。如果某个工具在本次会话中不会被用到就不初始化它。这个优化在实际场景中效果很明显因为一个 Agent 可能注册了 20 个工具但一次会话只用到 3 个。运行阶段是核心。BizBuddy 采用事件驱动的执行模型。Agent 的每一步操作都会产生一个事件事件被放入事件队列由事件处理器异步处理。这样做的好处是可以方便地插入拦截器实现日志记录、性能监控、安全校验等功能。暂停和销毁阶段主要是资源清理。这里有个坑我踩过如果 Agent 正在调用一个耗时工具直接销毁会导致工具调用中断可能留下脏数据。BizBuddy 的做法是销毁前先发送一个取消信号等待当前步骤完成或超时后再真正销毁。public class AgentLifecycleManager { public AgentInstance create(AgentDefinition definition) { return new AgentInstance(definition); } public void initialize(AgentInstance instance) { // 懒初始化只加载必要的工具 SetString requiredTools analyzeRequiredTools(instance); for (String toolName : requiredTools) { ToolDefinition tool toolRegistry.get(toolName); instance.registerTool(tool); } // 初始化记忆存储 instance.setMemoryStore(memoryStoreFactory.create(instance.getId())); } public void run(AgentInstance instance, String input) { ExecutionContext context new ExecutionContext(input); eventBus.publish(new AgentStartEvent(instance, context)); // 事件驱动执行 while (!context.isFinished()) { AgentEvent event eventQueue.poll(); eventHandler.handle(event); } } }3.2 上下文引擎的设计上下文引擎是 Agent Harness 里最容易被低估的模块。很多人觉得上下文就是对话历史存一个 List 就行了。但实际场景下上下文管理要复杂得多。BizBuddy 的上下文引擎需要处理几个问题。第一是上下文窗口限制。大模型的上下文窗口是有限的不能无限追加历史。BizBuddy 的策略是滑动窗口加摘要压缩。当上下文长度超过阈值时把最早的一部分对话压缩成摘要保留关键信息。第二是上下文隔离。不同会话的上下文必须严格隔离不能串。BizBuddy 通过会话 ID 来隔离上下文每个会话有独立的上下文存储。第三是上下文持久化。Agent 运行过程中可能崩溃或重启上下文不能丢。BizBuddy 支持上下文快照定期把上下文序列化到存储层。public class ContextEngine { private static final int MAX_CONTEXT_LENGTH 8000; private static final int SUMMARY_THRESHOLD 6000; public void append(ExecutionContext context, ContextEntry entry) { context.getEntries().add(entry); if (context.getTotalLength() SUMMARY_THRESHOLD) { compressContext(context); } } private void compressContext(ExecutionContext context) { ListContextEntry entries context.getEntries(); int compressCount entries.size() / 2; ListContextEntry toCompress entries.subList(0, compressCount); String summary summarizer.summarize(toCompress); context.getEntries().subList(0, compressCount).clear(); context.getEntries().add(0, new SummaryEntry(summary)); } }这里有个经验摘要压缩的质量直接影响 Agent 的表现。我试过几种摘要策略最后发现用模型自己来生成摘要效果最好但成本也最高。折中方案是用规则引擎做初步压缩只在关键节点调用模型生成摘要。3.3 工具注册与调用机制工具是 Agent 能力的延伸。BizBuddy 的工具注册中心支持三种注册方式注解扫描、配置文件、动态 API。注解扫描是最常用的方式。开发者只需要在方法上加一个AgentTool注解BizBuddy 就会自动扫描并注册。Component public class OrderTools { AgentTool(name queryOrder, description 根据订单号查询订单详情) public OrderInfo queryOrder( ToolParam(name orderId, description 订单号) String orderId) { return orderService.query(orderId); } }配置文件方式适合那些不方便改代码的场景。你可以在 YAML 文件里定义工具的名称、描述、参数 schema然后指定一个实现类。动态 API 方式适合需要运行时注册工具的场景。比如从数据库加载工具定义或者从远程服务同步工具列表。工具调用的核心问题是参数校验和错误处理。BizBuddy 在调用工具前会做严格的参数校验包括类型检查、必填检查、范围检查。如果校验失败会返回一个结构化的错误信息而不是抛异常。这样做的好处是Agent 可以根据错误信息决定下一步怎么做而不是直接崩溃。注意工具的参数 schema 一定要写清楚。我见过很多工具定义只写了参数名没写描述和类型结果模型调用时经常传错参数。好的 schema 应该包含参数名、类型、描述、是否必填、取值范围。3.4 记忆存储的分层设计记忆存储是 Agent 区别于普通对话系统的关键。BizBuddy 把记忆分为三层工作记忆、短期记忆、长期记忆。工作记忆是当前会话的上下文存在内存里读写最快。短期记忆是最近几次会话的摘要存在 Redis 里读写较快。长期记忆是经过沉淀的知识存在数据库或向量库里读写较慢但容量大。记忆的写入策略是工作记忆实时写入短期记忆在会话结束时写入长期记忆由专门的沉淀任务定期写入。读取策略是优先读工作记忆没有则读短期记忆再没有则读长期记忆。public class MemoryManager { public OptionalMemoryEntry recall(String sessionId, String query) { // 优先查工作记忆 OptionalMemoryEntry result workingMemory.get(sessionId, query); if (result.isPresent()) return result; // 查短期记忆 result shortTermMemory.get(sessionId, query); if (result.isPresent()) return result; // 查长期记忆 return longTermMemory.search(query); } }这里有个坑长期记忆的检索如果直接用向量相似度可能会召回不相关的内容。BizBuddy 的做法是加一层过滤先用关键词匹配缩小范围再做向量检索。这样准确率会高很多。4. 可观测性与安全管控4.1 可观测性模块的实现Agent 应用的可观测性比普通应用更重要因为 Agent 的行为具有不确定性。同样的输入可能因为模型的不同、上下文的不同、工具返回的不同产生完全不同的输出。如果没有完善的可观测性出了问题根本没法排查。BizBuddy 的可观测性模块包括三个部分日志、指标、链路追踪。日志方面BizBuddy 记录了 Agent 执行的每一个步骤包括输入、输出、工具调用、模型调用、耗时。日志格式是结构化的 JSON方便后续分析。指标方面BizBuddy 暴露了以下核心指标指标名称类型说明agent_execution_totalCounterAgent 执行总次数agent_execution_durationHistogramAgent 执行耗时分布tool_call_totalCounter工具调用总次数tool_call_durationHistogram工具调用耗时分布model_call_totalCounter模型调用总次数model_token_usageCounterToken 使用量context_lengthGauge当前上下文长度链路追踪方面BizBuddy 集成了 OpenTelemetry每次 Agent 执行都会生成一个 Trace包含多个 Span。这样你可以清楚地看到时间花在了哪里。4.2 安全管控层的设计企业级应用绕不开安全。Agent 的安全风险主要有几类权限越权、敏感信息泄露、恶意调用、资源耗尽。权限越权是指 Agent 调用了它不应该调用的工具。BizBuddy 的解决方案是工具级别的权限控制。每个工具定义里可以指定所需的权限Agent 实例在初始化时会绑定一组权限调用工具时会校验权限。敏感信息泄露是指 Agent 的输出里包含了不该包含的信息。BizBuddy 在输出链路上加了敏感信息过滤器支持正则匹配和自定义规则。恶意调用是指有人通过构造特殊输入来诱导 Agent 执行危险操作。BizBuddy 的做法是输入校验加频率限制。输入校验会检查输入的长度、格式、是否包含可疑模式。频率限制会限制单个会话在单位时间内的调用次数。资源耗尽是指 Agent 陷入死循环或者调用过多工具导致资源耗尽。BizBuddy 设置了最大执行步数和最大执行时间超过阈值会自动终止。public class SecurityGuard { private static final int MAX_STEPS 50; private static final long MAX_DURATION_MS 300_000; public void check(ExecutionContext context) { if (context.getStepCount() MAX_STEPS) { throw new SecurityException(执行步数超过限制); } if (context.getDuration() MAX_DURATION_MS) { throw new SecurityException(执行时间超过限制); } if (!permissionChecker.check(context.getAgent(), context.getCurrentTool())) { throw new SecurityException(权限不足); } } }提示安全管控的阈值不要设得太死。我一开始把最大步数设成 20结果发现有些复杂任务确实需要更多步骤。后来改成 50并且支持按 Agent 配置灵活多了。5. 常见问题与排查技巧5.1 Agent 执行卡住不动怎么办这是最常见的问题。Agent 执行到某一步后没有继续也没有报错。排查思路如下首先看日志确认最后执行到哪一步。如果最后一步是工具调用检查工具是否超时。BizBuddy 默认的工具超时是 30 秒如果工具执行时间超过这个值会被强制中断。如果工具没有超时检查模型调用。模型调用可能因为网络问题或者限流而阻塞。BizBuddy 的模型调用有重试机制但重试次数用完后会抛出异常。如果模型调用也正常检查事件队列。事件队列满了会导致新事件无法入队Agent 就会卡住。这种情况通常是因为事件处理器处理太慢需要检查处理器的性能。5.2 上下文丢失怎么排查上下文丢失的表现是 Agent 突然不记得之前说过的话。排查思路如下先确认上下文存储是否正常。BizBuddy 的上下文默认存在内存里如果服务重启内存中的上下文会丢失。生产环境建议开启上下文持久化。再确认上下文压缩是否触发。如果上下文长度超过阈值会触发压缩。压缩过程中如果出现异常可能导致上下文丢失。检查压缩日志看是否有异常。最后确认会话 ID 是否一致。如果客户端在请求时没有传会话 ID或者传了不同的会话 IDBizBuddy 会认为是不同的会话上下文自然不共享。5.3 工具调用参数错误怎么解决工具调用参数错误通常是因为工具的 schema 定义不清晰。排查思路如下先看工具定义的 schema确认参数名、类型、描述是否完整。如果描述太模糊模型可能理解错。再看模型返回的参数确认是否符合 schema。如果不符合说明模型没有正确理解 schema需要优化 schema 的描述。最后看参数校验逻辑确认是否有误判。有时候参数本身是对的但校验逻辑太严格导致误报。5.4 常见问题速查表问题现象可能原因排查方法解决方案Agent 执行卡住工具超时/模型阻塞/队列满查看日志最后一步调整超时时间/检查网络/优化处理器上下文丢失服务重启/压缩异常/会话ID不一致检查存储/压缩日志/会话ID开启持久化/修复压缩逻辑/统一会话ID工具参数错误schema 不清晰/模型理解错/校验太严检查 schema/模型返回/校验逻辑优化 schema/调整提示词/放宽校验权限校验失败权限未绑定/权限不匹配检查 Agent 权限配置重新绑定权限/调整权限配置执行超时步数过多/单步耗时过长查看执行步数和耗时调整阈值/优化工具性能5.5 几个踩过的坑第一个坑是线程安全问题。BizBuddy 早期版本里AgentInstance 是共享的多个会话共用一个实例。结果发现上下文会串A 会话的上下文跑到 B 会话去了。后来改成每个会话独立实例问题解决。第二个坑是内存泄漏。AgentInstance 销毁时没有清理干净导致内存持续增长。后来加了显式的资源清理逻辑并且在销毁后做一次内存检查。第三个坑是日志太多。Agent 执行的每一步都打日志一天下来日志文件几十个 G。后来改成分级日志正常步骤打 DEBUG异常步骤打 ERROR并且支持采样。6. 一些实操建议6.1 从最小可用版本开始如果你也想造一个类似的 Harness 平台我的建议是从最小可用版本开始。不要一上来就追求大而全先把核心链路跑通Agent 创建、上下文管理、工具调用、模型调用。这四个模块跑通了再逐步加可观测性、安全管控、记忆存储。我当初就是犯了贪多的错误一开始设计了十几个模块结果每个模块都只做了一半整体跑不起来。后来砍掉了一半模块专注核心链路反而进展更快。6.2 工具设计要克制工具不是越多越好。我见过一个 Agent 注册了 50 多个工具结果模型经常选错工具。后来精简到 10 个核心工具准确率反而提升了。工具的设计原则是一个工具只做一件事参数尽量少描述尽量清晰。如果一个工具需要传 10 个参数那它可能应该拆成多个工具。6.3 上下文管理要留余量上下文窗口是稀缺资源。不要等到满了才压缩要留一定的余量。BizBuddy 的阈值是 6000窗口是 8000留了 2000 的余量。这样即使压缩过程中有额外的 token 消耗也不会溢出。另外压缩策略要可配置。不同的模型窗口大小不同不同的场景对上下文的要求也不同。硬编码的阈值迟早会出问题。6.4 可观测性要提前做不要等到出了问题才加日志。Agent 的行为不确定性很高没有可观测性根本没法排查。BizBuddy 从第一版就集成了日志和指标虽然后来改过几次格式但至少一直有。链路追踪建议用 OpenTelemetry标准化程度高集成成本低。如果团队已经有 APM 系统直接对接就行。6.5 安全管控要可配置安全阈值不要硬编码。不同的 Agent、不同的场景对安全的要求不同。BizBuddy 的安全配置支持全局默认值和 Agent 级别覆盖这样既保证了默认安全又保留了灵活性。权限控制建议用 RBAC 模型角色和权限分离。这样新增工具时只需要配置权限不需要改代码。这个项目后续还可以这样扩展支持多模型路由根据任务类型自动选择最合适的模型支持 Agent 编排多个 Agent 协同完成复杂任务支持可视化编排让非技术人员也能配置 Agent 流程。不过这些都是后话了先把核心链路做扎实再说。
返回列表