ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

纯Java构建企业级Agent Harness平台:BizBuddy架构设计与工程实践

2026/10/7 9:23:59 拓冰建站 浏览量
纯Java构建企业级Agent Harness平台:BizBuddy架构设计与工程实践 1. 为什么我要用纯 Java 造一个 Agent Harness 平台先说清楚一件事Agent Harness 这个词最近被聊得很多但很多人把它和 Agent 本身混为一谈。我刚开始接触的时候也绕过弯路以为写个能调大模型的循环就算 Harness 了后来踩了几个坑才明白Agent 是“干活的”Harness 是“管干活的”。Agent 负责推理、决策、调用工具Harness 负责给 Agent 提供运行环境、生命周期管理、上下文注入、权限控制、可观测性、失败重试这一整套基础设施。打个比方Agent 是赛车手Harness 是赛道、维修站、无线电和裁判系统的总和。BizBuddy 这个项目就是我在这个认知下用纯 Java 从零搭起来的一套企业级 Agent Harness 平台。为什么强调“纯 Java”因为市面上大部分 Agent 框架要么是 Python 生态LangChain、AutoGen 那一挂要么是 Node/TypeScript 生态企业里那些跑了十几年的 Java 单体、Spring Boot 微服务、RuoYi-Vue-Plus 这类后台管理系统想接 Agent 能力的时候非常别扭——要么起一个 Python 服务做胶水层要么把业务逻辑重写一遍。这两种方案我都试过第一种运维复杂度直接翻倍第二种纯属给自己找罪受。BizBuddy 想解决的问题很具体让一个已有的 Java 企业系统在不引入第二套技术栈的前提下把 Agent 能力当成一个普通的基础设施模块接进来。它适合谁参考我认为有三类人一是手里有 Java 后台系统、想加 AI 能力但不想换栈的工程师二是正在做 Agent 平台选型、纠结自研还是套壳的架构师三是对 AgentScope 这类框架感兴趣、但想看看“如果我自己实现一遍会怎么做”的开发者。整篇文章我会把设计取舍、核心实现、踩过的坑都摊开讲代码和配置能给的我尽量给全你可以直接抄作业。2. 整体架构设计与技术选型取舍2.1 为什么不用现成的 Agent 框架做底座我一开始的路线是“站在巨人肩膀上”用 AgentScope 或者类似的框架做核心外面套一层 Java 的 REST 接口。做了两周就放弃了原因有三个都是实打实的痛点。第一是进程边界带来的上下文损耗。Agent 执行过程中需要频繁读写会话状态、工具调用记录、中间推理结果如果核心在 Python 进程、业务在 Java 进程每次交互都要序列化过网络延迟和调试成本都上去了。我实测过一个中等复杂度的多轮工具调用场景跨进程方案比同进程方案平均多出 40% 到 60% 的耗时而且日志分散在两个地方排查问题像拼图。第二是企业级能力对不上。企业系统里最看重的东西——行级权限、审计日志、事务一致性、灰度发布——这些在通用 Agent 框架里要么没有要么是弱实现。比如行级权限业务系统里一个用户只能看自己部门的数据这个约束必须能下沉到 Agent 的工具调用层而不是靠 Prompt 里写一句“请不要访问其他部门数据”来约束。Prompt 约束在工程上是不可靠的。第三是依赖治理。引入一个 Python 运行时意味着 CI/CD 流水线要维护两套环境镜像体积、安全扫描、版本升级全都要做双份。对于已经在 RuoYi-Vue-Plus 这类成熟脚手架上跑了好几年的团队来说这个成本不划算。所以最终决定核心用纯 Java 实现只把大模型调用当成一个外部 HTTP 依赖。这样整个平台就是一个标准的 Spring Boot 应用能无缝塞进现有的 Java 技术体系里。2.2 BizBuddy 的分层结构整个平台我分成了五层从下往上说。最底层是模型接入层负责对接各家大模型的 HTTP API做统一的请求封装、流式响应解析、Token 计数、失败重试。这一层的关键设计是把模型当成可替换的驱动用接口抽象出来换模型只改配置不改代码。往上一层是工具执行层这是 Harness 的核心。所有 Agent 能调用的能力——查数据库、调内部 API、读文件、发消息——都注册成工具Tool每个工具有明确的入参 schema、权限声明、超时设置和幂等标记。工具执行层负责参数校验、权限拦截、超时控制、结果序列化。再往上是会话与上下文层管理多轮对话的状态、上下文窗口的裁剪策略、长期记忆的存取。这一层最容易被低估但实际做下来它决定了 Agent 能不能在长任务里保持稳定。然后是编排层负责 Agent 的生命周期创建、执行、暂停、恢复、终止以及多 Agent 之间的协作调度。这一层我用的是状态机模型每个 Agent 实例有明确的状态流转避免出现“Agent 卡死但没人知道”的情况。最上面是接入层提供 REST API、SSE 流式推送、WebSocket以及和 RuoYi-Vue-Plus 权限体系的对接。业务系统通过这一层来创建 Agent 任务、查询执行状态、接收流式输出。2.3 关键取舍同步还是异步状态放哪有两个设计决策我想单独拎出来讲因为它们直接决定了平台的性格。第一个是执行模型选同步还是异步。我最终选了异步为主、同步为辅。原因是 Agent 任务天然是长任务一次多轮工具调用可能跑几十秒甚至几分钟如果用同步 HTTP 请求连接超时、网关限制、用户体验全是问题。所以核心执行走异步任务队列客户端拿到一个 taskId然后通过 SSE 或者轮询拿结果。但我也保留了一个同步接口用于那些确定很快返回的简单场景比如单轮问答避免过度设计。第二个是会话状态放哪。内存、Redis、数据库三个选项我都考虑过。纯内存最简单但重启就丢不适合企业场景纯数据库最稳但高频读写扛不住最后选的是Redis 做热状态、数据库做冷归档的组合。正在执行的会话状态放 Redis带 TTL任务结束后把完整轨迹落库用于审计和回放。这个组合在实测里既能扛住并发又满足了企业审计的硬要求。3. 核心模块的细节拆解与实操要点3.1 模型接入层把大模型当成可替换驱动模型接入层看起来简单其实坑不少。我抽象了一个ModelProvider接口核心方法就两个chat和chatStream。所有具体模型实现这个接口通过 Spring 的ConditionalOnProperty按配置加载。public interface ModelProvider { ChatResponse chat(ChatRequest request); void chatStream(ChatRequest request, StreamCallback callback); String providerName(); }这里有个关键细节流式响应的解析必须做增量拼接和异常兜底。大模型的 SSE 流经常出现半截 JSON、心跳空行、意外断流如果解析逻辑写得糙线上就会偶发解析异常。我的做法是维护一个缓冲区按行切分遇到不完整的行就留着等下一批数据同时给整个流设置一个空闲超时超过 30 秒没有新数据就主动断开并标记为超时失败。Token 计数这块我要多说一句。企业场景下成本是要算清楚的所以每次调用都要记录输入输出 Token 数。但不同模型的计数方式不一样有的返回在响应体里有的要自己估算。我的策略是优先用响应体里的官方计数拿不到就用字符数除以一个经验系数估算并且把这个估算标记出来避免和真实账单对不上时抓瞎。注意模型接入层一定要做熔断和降级。我踩过的坑是某次上游模型服务抖动导致所有 Agent 任务全部卡在等待响应上线程池被占满整个平台雪崩。后来加了 Resilience4j 做熔断连续失败达到阈值就快速失败同时支持配置一个备用模型做降级。3.2 工具执行层权限、超时、幂等一个都不能少工具执行层是 Harness 区别于普通 Agent 框架的核心。我定义了一个Tool抽象public interface Tool { String name(); JsonSchema inputSchema(); ToolResult execute(ToolContext context, JsonNode input); default Duration timeout() { return Duration.ofSeconds(30); } default boolean idempotent() { return false; } }权限拦截是重点。每个工具可以声明自己需要的权限标识执行前 Harness 会拿当前会话的用户身份去校验。这里我直接复用了 RuoYi-Vue-Plus 的权限模型工具权限和菜单权限走同一套PreAuthorize逻辑这样运维只需要维护一套权限数据。行级权限则通过ToolContext里携带的用户上下文在工具实现内部做数据过滤比如查数据库时自动拼上部门 ID 条件。超时控制我用的是独立的线程池加Future.get(timeout)。这里有个坑如果工具内部是阻塞 IO超时后线程并不会真正中断只是调用方不再等待。所以对于可能长时间阻塞的工具我会额外要求实现方支持中断或者在工具内部自己检查中断标志。幂等标记是为了重试安全。Agent 执行过程中如果某一步失败需要重试非幂等的工具比如“发送通知”重试就会造成重复副作用。所以我在编排层做重试时只对标记为幂等的工具自动重试非幂等的工具失败后交给上层决策。工具属性作用配置建议name唯一标识Agent 调用时使用用动词加名词如 queryOrderinputSchema参数校验和给模型看的描述描述要写清楚模型靠它理解怎么传参timeout单次执行超时查询类 10s写入类 30s外部调用 60sidempotent是否可安全重试只读操作 true写操作默认 falserequiredPermission所需权限标识与业务系统权限体系对齐3.3 会话与上下文层窗口裁剪是门手艺上下文窗口管理是我花时间最多的地方。大模型的上下文长度是有限的而 Agent 任务可能产生大量中间结果如果不做裁剪很快就会超限。我的裁剪策略是分层保留系统提示词永远保留最近 N 轮对话完整保留更早的历史做摘要压缩工具调用的原始结果如果很长只保留摘要加一个引用 ID需要时再按 ID 取回。这个策略的核心思想是把“必须精确”和“可以模糊”的内容分开处理。摘要压缩我用的是模型自己来做把一段历史对话喂给模型让它输出一段结构化摘要。这里要注意摘要本身也会消耗 Token所以要设置一个触发阈值比如历史超过窗口的 60% 才开始压缩避免频繁压缩浪费成本。长期记忆我用的是向量检索加关键词检索的混合方案。纯向量检索在精确匹配场景下会翻车比如用户问“订单号 12345 的状态”向量检索可能召回一堆语义相似但订单号不对的记录。所以我的做法是先做关键词精确匹配命中就直接用没命中再走向量检索。3.4 编排层用状态机管住 Agent 的生命周期编排层我用状态机来建模Agent 实例的状态包括CREATED、RUNNING、WAITING_TOOL、WAITING_USER、COMPLETED、FAILED、CANCELLED。每次状态流转都要落库这样任何时刻都能查到 Agent 在干什么。为什么要这么较真因为我遇到过 Agent “假死”的情况模型返回了一个工具调用但工具执行卡住了整个任务既没完成也没报错用户那边一直转圈。有了明确的状态机我就能设置状态超时——比如WAITING_TOOL超过工具超时时间还没流转就自动标记为失败并触发告警。多 Agent 协作我用的是主从模式一个协调者 Agent 负责拆解任务把子任务分发给工作者 Agent收集结果后汇总。这里的关键是子任务的隔离每个工作者 Agent 有独立的上下文和工具权限避免互相污染。协调者和工作者之间通过消息队列通信而不是直接方法调用这样单个工作者失败不会拖垮整个编排。4. 完整实操流程与关键环节实现4.1 环境准备与项目骨架搭建先说环境。JDK 我用的是 17Spring Boot 3.2.x构建工具 Maven。数据库 MySQL 8缓存 Redis 7。这些版本不是随便选的Spring Boot 3.x 要求 JDK 17 起步而 JDK 17 的虚拟线程虽然是 21 才正式但 17 已经有预览对 Agent 这种高并发 IO 场景很友好。如果你还在 JDK 8建议至少升到 17否则后面很多并发工具用起来会别扭。项目骨架我参考了 RuoYi-Vue-Plus 的分层习惯但做了精简。核心模块划分如下bizbuddy/ ├── bizbuddy-common # 通用工具、常量、异常 ├── bizbuddy-model # 模型接入层 ├── bizbuddy-tool # 工具执行层 ├── bizbuddy-session # 会话与上下文层 ├── bizbuddy-orchestrator # 编排层 ├── bizbuddy-api # 接入层REST/SSE └── bizbuddy-admin # 管理后台复用 RuoYi 前端依赖上除了 Spring Boot 全家桶我引入了几个关键库resilience4j做熔断限流mybatis-plus做数据访问和 RuoYi 保持一致redisson做 Redis 客户端比 Lettuce 在分布式锁上更省心jackson做 JSON 处理。没有引入任何 Python 相关的东西整个构建产物就是一个可执行 jar。4.2 定义一个工具并注册到平台我拿一个最典型的场景举例查询订单。先定义工具类Component public class QueryOrderTool implements Tool { Autowired private OrderMapper orderMapper; Override public String name() { return queryOrder; } Override public JsonSchema inputSchema() { return JsonSchema.builder() .addProperty(orderNo, string, 订单号必填) .addRequired(orderNo) .build(); } Override public ToolResult execute(ToolContext context, JsonNode input) { String orderNo input.get(orderNo).asText(); Long deptId context.getUserContext().getDeptId(); // 行级权限只能查本部门订单 Order order orderMapper.selectByOrderNoAndDept(orderNo, deptId); if (order null) { return ToolResult.notFound(订单不存在或无权访问); } return ToolResult.success(order); } Override public Duration timeout() { return Duration.ofSeconds(10); } Override public boolean idempotent() { return true; } Override public String requiredPermission() { return biz:order:query; } }工具注册我用的是 Spring 的自动扫描加一个注册中心。启动时把所有Tool实现收集起来按 name 建索引同时把 inputSchema 导出成模型能理解的工具描述。这里有个细节工具描述的质量直接决定模型调用准确率。我一开始把描述写得很简略结果模型经常传错参数。后来我把每个参数的说明写清楚包括格式示例调用准确率明显提升。4.3 一次完整的 Agent 执行流程我把一次典型的 Agent 执行拆成下面这些步骤你可以对照着看整个链路。接收请求接入层收到POST /agent/task参数包括用户问题、会话 ID、可用的工具集。校验用户身份和权限。创建任务编排层生成 taskId初始化 Agent 状态为CREATED把任务丢进异步执行队列。加载上下文会话层根据会话 ID 从 Redis 加载历史做窗口裁剪拼装成完整的消息列表。调用模型模型接入层发起流式请求边接收边通过 SSE 推送给客户端。解析工具调用如果模型返回工具调用意图编排层把状态切到WAITING_TOOL调用工具执行层。执行工具工具执行层做权限校验、参数校验、超时控制执行后把结果追加到上下文。循环把工具结果回传给模型继续下一轮直到模型不再请求工具、直接给出最终回答。收尾状态切到COMPLETED完整轨迹落库清理 Redis 热状态推送结束事件。这个流程里第 5 到第 7 步的循环是核心也是最容易出问题的地方。我设置了最大循环次数默认 10 轮防止模型陷入无限工具调用。同时每一轮都记录耗时和 Token 消耗方便事后分析。4.4 流式输出的实现细节流式输出我用的是 Spring 的SseEmitter。这里有几个实操要点。第一SSE 连接要设置合理的超时。默认超时可能太短长任务会被切断。我设置的是 5 分钟同时客户端要能处理重连。第二事件类型要区分。我定义了message模型输出片段、tool_call工具调用通知、tool_result工具结果、done结束、error错误几种事件类型前端根据类型做不同渲染。这样用户能看到 Agent 在“思考”和“干活”的过程体验比干等好很多。第三背压处理。如果模型输出很快但客户端消费慢SSE 缓冲区会堆积。我的做法是给 emitter 设置一个发送队列上限超过就丢弃中间的心跳事件保证关键事件不丢。GetMapping(/agent/stream/{taskId}) public SseEmitter stream(PathVariable String taskId) { SseEmitter emitter new SseEmitter(300_000L); emitter.onTimeout(() - log.warn(SSE timeout, taskId{}, taskId)); emitter.onError(e - log.error(SSE error, taskId{}, taskId, e)); streamManager.register(taskId, emitter); return emitter; }5. 常见问题排查与避坑经验实录5.1 模型返回的工具调用参数格式错误这是最高频的问题。模型有时候会把参数包成字符串有时候会漏字段有时候类型不对。我的处理分三层第一层是 JSON Schema 校验不通过直接返回错误给模型让它重试第二层是类型容错比如字符串 123 能自动转成数字 123第三层是重试次数限制同一个工具连续失败 3 次就放弃并告知用户。实操心得在系统提示词里明确写出工具调用的格式要求并且给一两个正确示例能显著降低格式错误率。我试过加示例和不加示例错误率差了将近一半。5.2 Agent 任务卡死无响应前面提过状态机的作用这里说具体排查。我遇到过几种卡死场景模型服务无响应、工具执行阻塞、Redis 连接池耗尽。排查思路是先看状态机当前状态再看该状态的超时设置最后看依赖服务的健康度。为此我在管理后台做了一个任务监控页实时展示每个任务的状态和停留时长超过阈值标红。卡死场景现象排查方法解决手段模型无响应状态停在 RUNNING看模型调用日志和熔断器状态熔断降级到备用模型工具阻塞状态停在 WAITING_TOOL看工具执行线程栈超时中断标记工具失败Redis 耗尽大量任务同时卡住看连接池指标扩容连接池加限流死循环调用循环次数飙升看循环计数达到上限强制终止5.3 上下文超限导致调用失败上下文超限的表现是模型返回 400 错误提示 token 超限。根因通常是工具返回的结果太长比如查了一个大列表直接塞进上下文。我的解决方法是在工具层就做结果截断超过一定长度的结果只返回摘要和前 N 条完整结果存起来给一个引用 ID。这样从源头控制上下文增长比事后裁剪更有效。5.4 并发场景下的会话串扰这个坑我踩得比较深。早期版本里同一个用户开两个会话偶尔会出现 A 会话的回答跑到 B 会话里。根因是会话状态用了共享的 ThreadLocal 或者缓存 key 设计有误。修复方法是所有会话状态必须以 sessionId 为隔离维度任何缓存 key、线程上下文都要带上 sessionId并且在代码审查时把这条当成硬性规范。5.5 权限绕过风险Agent 场景下的权限绕过是个隐蔽的安全问题。比如用户没有某个工具的权限但通过精心构造的 Prompt 让模型去调用如果 Harness 只在入口校验一次权限就可能被绕过。我的做法是每次工具调用都独立校验权限不依赖入口的一次性校验。同时所有工具调用都记审计日志包括调用者、工具名、参数、结果状态方便事后追溯。6. 和 RuoYi-Vue-Plus 生态的整合实践6.1 权限体系复用RuoYi-Vue-Plus 的权限模型是基于角色和菜单的我把 Agent 工具权限映射成一种特殊的菜单权限。这样在角色管理界面里管理员可以直接勾选某个角色能用哪些 Agent 工具不需要额外维护一套权限数据。实现上工具的requiredPermission返回的标识和 RuoYi 的权限标识格式保持一致校验时直接调用 RuoYi 的权限服务。6.2 管理后台的对接管理后台我直接复用了 RuoYi 的前端框架新增了几个页面Agent 任务列表、工具管理、模型配置、执行轨迹回放。执行轨迹回放这个功能很实用它把一次任务的完整消息流、工具调用、耗时、Token 消耗都展示出来排查问题时一目了然。数据来源就是前面说的落库的完整轨迹。6.3 部署与运维部署上 BizBuddy 就是一个标准的 Spring Boot 应用打成 jar 后可以独立部署也可以作为模块嵌入现有系统。我推荐独立部署通过内网 API 和业务系统通信这样升级 Agent 平台不影响业务系统。配置方面模型密钥、超时参数、循环上限这些都放在配置中心支持热更新不用重启。注意模型密钥一定要加密存储不要明文写在配置文件里。我用的是 Jasypt 做配置加密密钥通过环境变量注入这样即使配置文件泄露密钥也是安全的。7. 我对这套方案的一些真实体会做 BizBuddy 这段时间最大的感受是Agent Harness 的难点不在 AI在工程。模型能力是外部给定的你能控制的是怎么把它稳定、安全、可观测地集成进现有系统。我见过太多项目把精力全花在 Prompt 调优上结果上线后因为权限、超时、并发这些工程问题翻车。另一个体会是不要过度设计。我一开始想做一个支持任意复杂编排的通用引擎后来发现 80% 的场景就是“单 Agent 加几个工具”把这条路径做扎实比什么都强。复杂的多 Agent 协作我保留了接口但默认不启用等真有需求再打开。最后分享一个我一直在用的小技巧给每个工具写一个“自测用例”。就是一段固定的输入和期望输出每次改完工具代码跑一遍。这看起来笨但能挡住很多低级错误尤其是参数校验和权限逻辑这种容易改坏的地方。工具多了以后这套自测用例就是你的回归测试网比事后救火省心得多。