资讯动态

纯Java自研企业级Agent Harness:BizBuddy架构设计与实践

发布时间:2026/10/27 3:04:58 来源:尧图企业网站定制
1. 为什么我要用纯 Java 造一个 Agent Harness1.1 从一个真实的痛点说起去年下半年我所在的团队接了一个企业内部智能助手的项目。业务方的诉求很朴素让员工能用自然语言查询内部系统的数据、触发审批流、生成周报。听起来就是个典型的 Agent 应用市面上框架一抓一大把Python 生态里 AgentScope、LangChain 之类的工具已经相当成熟按理说直接拿来用就行。但真正落地的时候问题一个接一个冒出来。我们的核心业务系统全是 Java 写的Spring Boot MyBatis-Plus 那一套几十个微服务权限体系用的是 RuoYi-Vue-Plus 那套行级权限模型。如果 Agent 层用 Python 写就意味着要额外维护一套服务间的鉴权、日志、链路追踪运维团队直接表示不接受——他们不想为了一个智能功能再养一套 Python 的部署流水线。更麻烦的是可控性。业务方明确要求Agent 的每一步操作都要能审计、能回放、能中断。Python 生态里很多框架为了追求开发效率把执行流程封装得很深你想在中间插一个人工确认的钩子得翻半天源码。而 Java 这边Spring 的 AOP、事件机制、线程池管理都是现成的做这种可插拔的拦截反而更顺手。于是就有了 BizBuddy 这个项目。它的定位很明确一个纯 Java 实现的企业级 Agent Harness 平台。注意这里的关键词是 Harness不是 Agent 本身。这两者的区别是我在项目初期花了最多口舌去解释的事情后面会专门展开。1.2 Harness 和 Agent 到底差在哪很多人第一次听到 Agent Harness 这个词会懵。我打个比方Agent 就像是一个司机Harness 就是那辆车加上整套仪表盘、刹车、安全带和行车记录仪。Agent 负责思考和决策——它调用大模型解析意图规划下一步动作。而 Harness 负责承载和约束——它管理 Agent 的生命周期提供工具调用的通道记录每一步的输入输出在危险操作前踩刹车在超时的时候强制中断。这个区分非常重要因为它决定了架构的边界。BizBuddy 本身不实现任何具体的 Agent 逻辑它提供的是一套运行时容器你把自己的 Agent 注册进来Harness 负责给它分配资源、注入工具、记录轨迹、处理异常。这样做的好处是业务团队可以专注于写自己的 Agent 提示词和工具函数而不用关心并发、重试、审计这些脏活。我见过太多项目把 Agent 和 Harness 揉在一起结果就是每加一个新 Agent 就要复制一遍基础设施代码最后变成一坨谁也不敢动的泥球。BizBuddy 从第一天就把这条线划清楚后面扩展起来才不至于失控。1.3 技术选型的几个关键取舍选纯 Java 不是情怀是算过账的。我们对比过三条路线路线优势劣势我们的判断纯 Python 框架生态成熟开发快与 Java 主系统集成成本高运维复杂放弃Java 调 Python 服务兼顾生态和集成多一层网络开销调试链路长备选纯 Java 自研 Harness与现有体系无缝可控性最强需要自己造轮子最终选择最终选纯 Java核心原因是集成成本。我们的鉴权、审计、配置中心、监控全是 Java 生态的Agent 平台如果游离在外光是打通这些就要花掉大半预算。而 Java 这边其实并不缺造 Harness 的料Spring Boot 做容器MyBatis-Plus 做持久化虚拟线程JDK 21解决并发这些组合起来完全够用。至于 AgentScope 这类框架我的态度是借鉴思路不照搬实现。它把 Agent 的编排抽象得很优雅但那是 Python 的优雅。在 Java 里我用更符合 Spring 习惯的方式重新表达了一遍比如用Tool注解替代装饰器用事件发布替代回调链。2. BizBuddy 的整体架构与核心模块拆解2.1 一张图看懂分层设计BizBuddy 的分层不复杂从上到下四层每一层职责单一接入层对外暴露 REST 和 SSE 接口处理会话建立、消息推送、流式返回。这一层用 Spring WebFlux 做响应式因为 Agent 的思考过程是流式的用阻塞式 Servlet 会很别扭。编排层这是 Harness 的核心负责 Agent 的注册、调度、状态机管理。一个会话从创建到结束中间经历接收输入→规划→工具调用→生成回复→归档几个状态编排层就是管这个状态流转的。能力层工具Tool、记忆Memory、模型客户端Model Client都在这一层。工具是可插拔的业务方实现接口后注册进来即可记忆分短期会话内和长期跨会话短期用内存长期落库。基础设施层持久化、缓存、配置、日志、监控。这一层尽量复用公司现有的中间件不重复造。分层的原则是依赖单向上层依赖下层下层不知道上层的存在。这样编排层可以独立测试能力层可以独立替换。我特别强调这一点是因为见过太多项目让工具直接回调编排层的对象最后形成循环依赖改一处崩一片。2.2 核心对象模型Agent、Session、Turn、StepBizBuddy 里有四个核心概念理解它们就理解了整个运行时Agent是静态定义描述这个智能体能做什么。它包含名称、系统提示词、可用工具列表、模型配置。Agent 本身无状态可以被多个会话共享。Session是一次完整的对话上下文有唯一 ID绑定一个 Agent。Session 里维护着消息历史、变量槽位、以及当前状态。Turn是 Session 里的一轮交互从用户发一条消息开始到 Agent 给出最终回复结束。一轮 Turn 里可能包含多次模型调用和工具调用。Step是 Turn 里的最小执行单元一次模型调用是一个 Step一次工具调用也是一个 Step。Step 是审计的最小粒度每一步的输入、输出、耗时、token 消耗都记录在案。为什么要拆这么细因为企业级场景对可观测性的要求极高。业务方要能回答这次回答为什么这么慢这个数据是从哪个工具查出来的这一步花了多少钱。如果只有 Session 和 Turn 两个粒度这些问题是答不上来的。Step 级别的记录让每一次模型调用和工具调用都变得可追溯。2.3 状态机设计让 Agent 的执行可中断可恢复Agent 的执行天然是异步的、可能长时间的。用户发一条消息Agent 可能要调用三次模型、五次工具耗时几十秒。这期间用户可能想取消系统可能超时工具可能失败。用一根线性的代码写下来异常处理会非常难看。BizBuddy 用状态机来管这个流程。一个 Turn 的状态流转大致是PENDING → PLANNING → EXECUTING → RESPONDING → COMPLETED ↓ ↓ ↓ FAILED WAITING_HUMAN CANCELLEDWAITING_HUMAN这个状态是企业级场景的关键。当 Agent 要执行一个敏感操作比如提交审批、发送邮件Harness 会暂停执行把决策权交回给人。人确认后状态从WAITING_HUMAN回到EXECUTING继续。这个暂停-恢复能力是纯 Java 状态机 持久化实现的Python 框架里做同样的事要绕不少弯。状态机的另一个好处是幂等恢复。如果服务重启正在执行的 Turn 可以从数据库里恢复状态从最后一个成功的 Step 继续而不是从头再来。这在长流程场景下能省下大量 token 成本。3. 核心模块的实操实现细节3.1 工具注册用注解把普通方法变成 Agent 能力工具是 Agent 和外部世界交互的唯一通道。BizBuddy 的工具注册机制设计得很Spring——你写一个普通方法加个注解它就变成了 Agent 可调用的工具。Component public class OrderTools { Tool(name queryOrderStatus, description 根据订单号查询订单当前状态返回状态码和描述) public OrderStatus queryOrderStatus( ToolParam(description 订单号格式为 ORD 开头的字符串) String orderNo) { return orderService.getStatus(orderNo); } }启动时Harness 扫描所有带Tool的方法解析参数类型和描述生成一份 JSON Schema 注册到工具仓库。当模型决定调用某个工具时Harness 根据工具名找到对应方法把模型返回的 JSON 参数反序列化成方法入参反射调用再把结果序列化回给模型。这里有几个坑我踩过值得展开说第一参数描述的粒度。模型能不能正确调用工具八成取决于参数描述写得好不好。我一开始写订单号模型经常传进来一个带空格的字符串。后来改成订单号格式为 ORD 开头的字符串例如 ORD20240101001调用成功率明显上升。描述里带上格式和示例比单纯说订单号有用得多。第二返回值的体积控制。工具返回的数据会全部塞进模型的上下文如果返回一个几百行的列表token 瞬间爆炸。我的做法是在工具层做截断和摘要比如查询列表默认只返回前 20 条并附上共 N 条已展示前 20 条的提示。这个逻辑放在工具里而不是指望模型自己处理。第三异常的处理。工具抛异常时不能直接把堆栈丢给模型那会污染上下文。Harness 会捕获异常转成一句人类可读的说明比如查询订单失败订单号不存在。模型看到这句话会尝试换个方式或者告知用户而不是被一堆 Java 堆栈搞懵。3.2 模型客户端多模型适配与降级策略企业场景很少只用一家模型。我们内部就同时接了三个来源公有云的大模型、私有化部署的开源模型、以及一个专门做嵌入的小模型。BizBuddy 的模型客户端层做了统一抽象上层只认ModelClient接口具体用哪个由配置决定。public interface ModelClient { ChatResponse chat(ChatRequest request); FluxChatChunk stream(ChatRequest request); }流式接口返回Flux配合 WebFlux 直接推给前端用户能看到文字一个个蹦出来体验比等半天再一次性返回好太多。多模型带来的一个现实问题是降级。公有云模型偶尔会限流或超时这时候不能直接报错给用户。我的策略是配置一个主模型和一个备用模型主模型连续失败 N 次后自动切到备用同时打点告警。切换逻辑封装在客户端层上层无感知。还有一个细节是token 计费统计。每次模型调用返回的 usage 信息都要落库按会话、按 Agent、按天维度聚合。财务那边要按部门分摊成本没有这个统计根本没法交代。这件事必须在客户端层做因为只有这一层拿得到原始的 usage 数据。3.3 记忆管理短期上下文与长期知识的分离记忆这块我走过弯路。一开始想做一个万能记忆把对话历史、用户偏好、业务知识全塞在一起结果就是上下文越来越长模型越来越慢还经常记串。后来拆成两层问题迎刃而解短期记忆是当前会话的消息列表有窗口限制。我的做法是保留最近 N 轮完整对话更早的做摘要压缩。摘要不是简单截断而是让模型把前面的对话浓缩成几句话保留关键信息用户说了什么诉求、已经确认了什么事实。这样既控制了长度又不丢上下文。长期记忆是跨会话的知识比如这个用户偏好用表格展示数据这个部门的审批流程是三级。长期记忆存在数据库里按用户或按组织维度检索在会话开始时注入到系统提示词里。两层记忆的检索时机不同短期记忆每轮都带长期记忆只在会话初始化时查一次。这个区分让上下文长度可控也避免了长期记忆污染当前对话。3.4 审计与回放每一步都要留痕企业级平台和玩具项目的最大区别就是审计。BizBuddy 的每一个 Step 都会落库记录内容包括Step 类型、输入、输出、耗时、token 消耗、状态、错误信息。这些数据支撑了两个关键能力回放给定一个 Session ID能把整个执行过程按时间线还原出来。产品经理看用户反馈时直接调出回放一眼就能看出是哪一步出了问题。成本分析按 Agent、按部门、按时间段统计 token 消耗和调用次数。这个报表每周发给业务方让他们知道钱花在哪了。审计数据的写入我用的是异步 批量。如果每个 Step 都同步写库高并发下数据库扛不住。做法是先写本地队列后台线程批量刷库牺牲一点点实时性换取吞吐。但要注意关键状态变更比如 WAITING_HUMAN必须同步写否则服务重启后状态就丢了。4. 踩过的坑与排查实录4.1 虚拟线程用不对反而更慢JDK 21 的虚拟线程是个好东西但我在项目初期用错了地方。当时想当然地把所有工具调用都丢到虚拟线程里执行觉得这样并发能力拉满。结果压测时发现涉及数据库查询的工具反而变慢了。排查后发现虚拟线程适合的是IO 密集且阻塞时间短的场景。而我们的数据库连接池是固定大小的虚拟线程一多大量线程卡在等连接上反而增加了调度开销。后来改成模型调用网络 IO耗时长用虚拟线程数据库操作走原有的线程池。这样各取所长整体吞吐上去了。提示虚拟线程不是银弹用之前先想清楚瓶颈在哪。连接池、锁竞争这些传统瓶颈虚拟线程救不了。4.2 流式返回时的连接泄漏SSE 流式返回有个隐蔽的坑客户端中途断开连接时服务端的 Flux 如果不做处理会一直跑下去直到模型返回完毕。压测时这个问题导致大量连接堆积最后 OOM。解决办法是在 Flux 上加doOnCancel钩子客户端断开时主动取消上游的模型调用释放资源。同时给每个流设置一个最大存活时间超时强制关闭。这个坑很隐蔽因为功能测试时根本发现不了只有压测或者真实用户频繁刷新页面时才会暴露。4.3 工具调用的死循环模型有时候会陷入调用工具→结果不满意→再调用同一个工具的死循环。我遇到过一次一个查询工具因为参数格式问题一直返回空模型就一直重试一个 Turn 里调了三十多次token 烧了一大截。Harness 层必须做调用次数熔断。我的配置是同一个工具在同一个 Turn 里最多调用 5 次超过就强制中断并返回提示。同时如果连续两次调用同一个工具且参数完全相同直接判定为死循环提前终止。这个保护机制上线后再没出现过 token 被烧穿的情况。4.4 常见问题速查表现象可能原因排查方向解决方式模型不调用工具工具描述不清检查 description 和参数说明补充格式和示例工具调用参数错误参数类型不匹配看审计日志里的原始参数调整 Schema 或加校验响应特别慢上下文过长统计每轮 token 数启用摘要压缩流式返回中断连接被取消看 doOnCancel 日志加取消钩子和超时状态恢复失败关键状态未同步落库检查落库策略关键状态改同步写token 消耗异常死循环调用看 Step 调用次数加熔断和去重5. 和现有 Java 生态的集成经验5.1 复用 RuoYi-Vue-Plus 的权限体系BizBuddy 没有自己造权限而是直接复用了公司现有的 RuoYi-Vue-Plus 那套。Agent 的调用权限、工具的可见范围全部走原有的行级权限模型。具体做法是在工具执行前Harness 会拿当前用户的身份去查权限判断这个用户能不能调用这个工具、能不能访问这个数据。这个校验逻辑用 AOP 切面实现业务方写工具时完全不用关心权限切面自动兜底。这样做的好处是权限体系统一不会出现Agent 能查到用户本来看不到的数据这种越权问题。企业场景下越权是红线必须在架构层面堵死。5.2 用 MyBatis-Plus 管理元数据Agent 定义、工具注册信息、会话记录、审计日志这些元数据全部用 MyBatis-Plus 管理。选它是因为团队熟而且它的代码生成和分页插件确实省事。一个实践细节审计日志表增长极快我按天做了分表用 MyBatis-Plus 的动态表名插件自动路由。查询时按时间范围定位到具体表避免全表扫描。这个优化让审计查询从秒级降到了毫秒级。5.3 配置中心与灰度发布Agent 的提示词、模型参数、工具开关这些都不应该硬编码。BizBuddy 把它们放在配置中心支持热更新和灰度。灰度这块我做了个简单但实用的设计每个 Agent 可以配置多个版本按用户 ID 哈希分流。新版本先给 10% 的用户用观察指标没问题再全量。这个机制让我们改提示词时心里有底出问题能快速回滚。6. 关于要不要自研 Harness的一些个人判断写到这里我想聊聊一个更宏观的问题到底什么情况下值得自研一个 Agent Harness什么情况下直接用现成的框架就好。我的判断标准有三条。第一看集成成本。如果你的主系统是 Java 生态且对权限、审计、运维有硬性要求那自研 Harness 的收益是明显的因为省下的集成成本远超造轮子的投入。反过来如果是个独立的创新项目没有历史包袱那用成熟框架快速验证更划算。第二看可控性要求。如果业务方要求每一步都能审计、能中断、能回放那自研几乎是唯一选择。现成框架为了通用性往往把这些能力做得很浅你要深度定制反而更费劲。第三看团队基因。团队如果全是 Java 背景硬上 Python 框架学习成本和维护成本都会很高。用自己熟悉的语言造轮子虽然前期慢一点但后期维护和排障会顺畅得多。BizBuddy 上线到现在跑了小半年接了七八个业务 Agent日均调用量稳定在几万次。回头看最大的收获不是技术本身而是把Agent 和 Harness 分离这个理念落到了实处。业务团队现在写 Agent 只需要关注提示词和工具逻辑基础设施的事全交给 Harness协作效率比一开始预想的高不少。最后分享一个小心得Harness 的接口设计一定要窄。我一开始把太多能力暴露给上层结果每个业务 Agent 都用不同的方式调用最后很难统一管理。后来收敛成几个核心接口——注册、执行、查询状态——反而让整个系统清爽了很多。接口越窄约束越强系统越稳。这个道理做过平台的人应该都懂。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑