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

📅 发布时间:2026/10/12 5:42:23
纯Java打造企业级Agent Harness:BizBuddy架构设计与工程实践
1. 为什么我要用纯 Java 造一个 Agent Harness1.1 从一个真实的痛点说起去年下半年我所在的团队接了一个内部效率工具的需求。背景很简单公司内部有好几个业务系统客服、运营、数据分析的同事每天要在这些系统之间来回切换重复执行一些固定流程比如查订单、拉报表、生成周报、批量发通知。大家一开始想的是接一个大模型接口写个脚本把流程串起来就完事了。真动手才发现事情远没有这么简单。一个能跑通的 Demo 和一个能上生产的 Agent 平台中间隔着的不是模型能力而是工程化。模型调用只是最外面那一层底下要处理的东西包括会话状态怎么存、工具怎么注册和发现、多轮对话的上下文怎么裁剪、失败重试怎么做、权限怎么隔离、审计日志怎么落、并发上来之后怎么限流、不同业务方怎么接入自己的工具。这些东西才是真正决定一个 Agent 平台能不能在企业里活下来的关键。我当时给这个平台起了个名字叫BizBuddy定位很明确一个面向企业内部业务场景的 Agent Harness智能体承载框架。所谓 Harness你可以理解成马具——模型是那匹马能力很强但方向不定Harness 就是套在它身上、让它能被人驾驭、能拉着车稳定跑起来的那套东西。它不负责训练模型也不负责做模型本身它负责的是把模型、工具、记忆、权限、观测这些零件组装成一个可运维、可扩展、可审计的系统。1.2 为什么坚持纯 Java这是整个项目里被问得最多的一个问题。现在做 Agent 相关的东西Python 生态几乎是默认选项LangChain、LlamaIndex 这些框架成熟度摆在那里为什么还要用 Java 从头造我的理由有三条都是被现实逼出来的。第一企业存量系统绝大多数是 Java。我们要对接的订单系统、权限中心、消息网关、报表服务全是 Spring Boot 写的。如果 Agent 平台用 Python 写那每一次工具调用都要跨语言、跨进程、跨网络中间多一层序列化和网络开销不说运维上还要维护两套技术栈、两套发布流程、两套监控。对于一个人力有限的小团队这是实打实的负担。第二Java 的并发和稳定性模型更适合长驻服务。Agent 平台本质上是一个长时间运行的服务要处理大量并发的会话、要管理连接池、要做优雅停机、要做线程隔离。JVM 在这方面的成熟度、可观测性工具链、以及团队已有的排障经验都是现成的资产。Python 的 GIL 在高并发 IO 场景下虽然影响没那么大但真到了要精细控制线程池、要做背压的时候Java 的这套东西用起来更顺手。第三类型系统带来的可维护性。Agent 平台里到处都是结构化数据工具的参数 schema、模型的返回结构、会话的状态机。用强类型语言写编译期就能挡掉一大批低级错误IDE 的补全和重构也好用得多。项目做到后期代码量上来了这一点带来的收益非常明显。当然纯 Java 也有代价。最大的代价是生态。Python 那边现成的 prompt 模板库、向量库客户端、各种模型的 SDKJava 这边要么没有要么质量参差不齐。所以 BizBuddy 里相当一部分工作是在补这些基础设施。这也是为什么这个项目值得写一篇总结——踩过的坑足够多。1.3 这个平台到底解决了什么问题说人话BizBuddy 让业务方可以这样用写一个 Java 类加上一个注解声明这个类是一个工具说明它的名字、描述、参数然后在配置里注册这个工具属于哪个 Agent业务同事在聊天界面里用自然语言描述需求Agent 自己决定调哪个工具、传什么参数、拿到结果之后怎么组织语言回复。举个具体例子。运营同事想查上周华东区退货率最高的三个品类。以前的做法是找数据同学写 SQL等半天拿到结果再自己整理。有了 BizBuddy 之后运营直接在对话框里输入这句话Agent 会识别出这需要调用报表查询工具自动把自然语言转成结构化查询参数调用后端接口拿到数据后再用自然语言总结成一段话返回。整个过程几秒钟。这个例子里模型负责的是理解意图和组织语言真正的数据查询还是走原来的后端接口权限、数据口径、审计全都没变。这就是 Harness 的价值它不替代企业已有的系统它是在已有系统之上加了一层自然语言的交互入口。适合谁来参考这篇内容如果你正在做企业内部工具、正在评估要不要自建 Agent 平台、或者单纯好奇一个生产级 Agent 系统到底要考虑哪些东西那这篇应该对你有用。我会尽量把设计取舍讲透而不是只贴代码。2. 整体架构设计与关键取舍2.1 分层设计把会变的和不变的分开BizBuddy 的整体架构我分了四层从下往上依次是模型接入层、核心运行时层、工具与能力层、接入层。这个分层不是拍脑袋定的而是遵循一个原则把容易变化的部分隔离出去让核心逻辑保持稳定。模型接入层负责屏蔽不同模型提供方的差异。今天用这家明天可能换那家接口协议、返回格式、流式方式都不一样。这一层定义一个统一的ModelClient接口上层只认这个接口具体实现可以是任何一家。这样换模型的时候改动被限制在这一层里。核心运行时层是整个平台的心脏包含会话管理、Agent 调度、工具调用编排、上下文管理、记忆存储。这一层是纯业务逻辑不依赖任何具体的模型或工具是测试覆盖的重点。工具与能力层是业务方接入的地方。每个工具就是一个普通的 Java 类通过注解声明元信息运行时通过反射扫描注册。业务方不需要懂 Agent 的内部机制只要按约定写好工具就行。接入层负责对外暴露能力可以是 HTTP 接口、WebSocket、消息队列消费者甚至是一个定时任务。这一层很薄主要是协议转换和鉴权。提示分层的关键不是层数多而是依赖方向单一。上层可以依赖下层下层绝不能反向依赖上层。我在代码评审时会把这条当作硬性红线一旦发现下层 import 了上层的类直接打回。2.2 为什么不用现成的 Agent 框架这个问题我在立项时认真评估过。当时看了几个主流的 Java Agent 框架也考虑过用 Python 框架加一层 Java 网关的方案。最后决定自研核心原因是控制力。现成框架的问题在于它们为了通用性做了大量抽象这些抽象在你需求简单的时候是助力需求一复杂就变成阻力。比如上下文管理框架给你一个默认策略但企业场景里不同 Agent 的上下文策略可能完全不同客服 Agent 需要保留完整对话历史报表 Agent 只需要保留最近一轮代码助手 Agent 需要保留文件内容。要改这些就得深入框架内部改着改着就变成了框架的二次开发还不如自己写。另一个原因是可观测性。企业里跑的东西出问题必须能查。现成框架的日志和埋点往往不够细或者格式不符合公司已有的监控体系。自研的话从第一天就可以把 traceId、会话 ID、工具调用耗时这些关键信息按公司规范打出来接入现有监控几乎零成本。当然自研不是没有代价。最大的代价是时间。一个能用的框架别人可能一周就搭起来了我们花了将近两个月才把核心跑通。但这两个月里每一行代码我们都清楚它在干什么后面加功能、排故障的时候这个清楚省下的时间远超当初多花的。2.3 核心数据模型会话、消息、工具调用整个平台的数据模型其实就三个核心实体理解了它们整个系统就理解了一大半。会话Session是一次完整交互的容器。它有一个唯一 ID有创建时间、最后活跃时间、所属用户、所属 Agent 类型。会话是有生命周期的长时间不活跃会被归档或清理。会话里存的是消息列表和状态。消息Message是会话里的基本单位分三种角色用户消息、助手消息、工具消息。用户消息是输入助手消息是模型的输出可能包含工具调用请求工具消息是工具执行的结果。这三者按时间顺序排列构成对话历史。工具调用ToolCall是助手消息里的一种特殊内容。当模型决定调用工具时它输出的不是纯文本而是一个结构化的调用请求包含工具名和参数。平台解析这个请求执行对应工具把结果作为工具消息追加到历史里然后再把更新后的历史发给模型让它继续。这三个实体的关系可以用一句话概括一个会话包含多条消息助手消息里可能包含多个工具调用工具调用的结果又变成新的消息。整个 Agent 的运行就是这个循环不断转下去直到模型输出一条不含工具调用的纯文本消息为止。2.4 状态管理内存、Redis 还是数据库会话状态存哪里是个必须早做决定的问题。我评估了三种方案。纯内存最简单一个ConcurrentHashMap就搞定读写快没有外部依赖。问题是服务重启状态就没了多实例部署也没法共享。适合单机 Demo不适合生产。纯数据库最稳状态持久化重启不丢多实例共享。问题是每次读写都要走数据库高频对话场景下数据库压力大而且对话历史这种数据读写模式是写多读也多关系型数据库不一定是最优解。最后我选的是内存加 Redis 的混合方案。活跃会话的状态放在内存里读写走本地快同时异步写一份到 Redis用于故障恢复和多实例共享。会话结束或超时后完整历史归档到数据库用于审计和后续分析。这个方案的关键在于一致性。内存和 Redis 之间是异步同步理论上存在短暂不一致。我的处理是以内存为准Redis 只作为恢复源。如果服务崩溃重启从 Redis 加载最近的状态可能丢失最后几条消息但不会导致状态错乱。对于企业内部工具这个一致性级别是够的如果换成金融交易场景那就得换成同步写加事务。注意状态管理这块千万不要一上来就追求完美一致。先想清楚业务能容忍多大的不一致再选方案。很多团队在这里过度设计最后系统复杂得没人敢改。3. 核心模块的实操细节3.1 模型接入层如何优雅地屏蔽差异模型接入层的核心是一个接口和一组实现。接口定义得很简单public interface ModelClient { ModelResponse chat(ModelRequest request); void chatStream(ModelRequest request, StreamCallback callback); }ModelRequest里封装了消息列表、工具定义、温度、最大 token 这些参数。ModelResponse里封装了模型返回的文本、工具调用请求、token 用量。所有具体模型的差异都被这两个类吃掉了。实现层里我踩过最大的坑是流式响应和工具调用的兼容。有些模型在流式模式下工具调用的参数是分片返回的你需要自己拼接。比如一个工具调用参数是{city: 上海}流式返回可能是{ci、ty: 上、海}这样分三次来的。如果不处理解析就会失败。我的做法是在流式回调里维护一个缓冲区按工具调用的 index 分组每个 index 维护一个参数片段列表等流结束时再统一拼接解析。这个逻辑不复杂但如果不注意线上就会出现偶尔工具调用失败的诡异问题而且很难复现。另一个坑是超时和重试。模型接口偶尔会慢偶尔会返回 5xx。我的策略是连接超时设短一点比如 5 秒读超时设长一点比如 60 秒因为模型生成确实慢重试只对幂等的请求做而且最多重试两次重试之间加指数退避。这里要特别注意带工具调用的请求不要轻易重试因为模型可能已经决定要调工具了重试可能导致重复调用。3.2 工具注册注解加反射的取舍工具注册我用的是注解加反射的方案。业务方这样写AgentTool(name queryOrder, description 根据订单号查询订单详情) public class QueryOrderTool { ToolParam(name orderId, description 订单号, required true) private String orderId; public ToolResult execute() { // 调用后端订单服务 return ToolResult.success(orderService.query(orderId)); } }启动时平台扫描所有带AgentTool的类解析出工具名、描述、参数 schema注册到一个全局的ToolRegistry里。运行时模型返回工具调用请求平台从 registry 里找到对应的工具类用反射创建实例、注入参数、执行方法。这个方案的好处是接入成本极低业务方不用继承任何基类不用实现任何接口加个注解就行。坏处是反射有性能开销而且编译期检查弱。参数名写错了编译不报错运行时才炸。为了弥补这个缺点我加了一个启动期校验扫描到工具类之后检查参数 schema 是否合法、必填参数是否有默认值、工具名是否重复。有问题直接在启动时抛异常不让服务带病上线。这个校验帮我挡掉过好几次低级错误。实操心得反射方案一定要配启动期校验。宁可启动失败也不要运行时才发现问题。生产环境的启动失败是可控的运行时的偶发失败是灾难。3.3 上下文管理裁剪策略决定成本和质量上下文管理是 Agent 平台里最容易被低估、又最影响效果和成本的模块。模型有上下文窗口限制对话轮次多了历史消息必须裁剪。怎么裁直接决定了 Agent 的记性和每次调用的 token 成本。我实现了三种策略可以按 Agent 配置。滑动窗口最简单只保留最近 N 轮对话。优点是实现简单、成本可控缺点是会丢失早期的重要信息。适合那种每轮独立的场景比如查询类 Agent。摘要压缩复杂一些当历史超过阈值时调用模型把早期对话压缩成一段摘要摘要加最近几轮一起发给模型。优点是保留了长期信息缺点是压缩本身要花一次模型调用有额外成本和延迟。适合长对话场景比如客服。关键信息提取最复杂从历史里提取出结构化的事实比如用户 ID、订单号、偏好单独存储每次请求时把这些事实作为系统提示的一部分注入。优点是信息密度高、token 省缺点是需要额外的提取逻辑而且提取可能出错。适合信息密集的场景比如多轮填表。实际用下来我的建议是默认用滑动窗口特殊场景再上摘要。关键信息提取虽然理论上最优但工程复杂度高除非场景真的需要否则不划算。3.4 工具调用的编排串行、并行还是混合一轮对话里模型可能一次返回多个工具调用。这些调用怎么执行是个需要仔细设计的问题。最简单的是串行一个一个执行前一个的结果作为后一个的输入。适合有依赖关系的调用比如先查用户 ID再根据 ID 查订单。缺点是慢N 个调用就是 N 倍时间。并行快多个调用同时执行。适合无依赖的调用比如同时查三个不同城市的天气。但并行有风险如果工具之间有隐式依赖或者共享资源并行可能出问题。我的方案是默认串行允许工具声明自己可以并行。工具类上加一个AgentTool(parallelizable true)的标记平台看到这个标记就把多个可并行的调用放到线程池里跑。这样既保证了默认安全又给了优化空间。这里有个细节要注意并行执行的结果顺序。模型返回的工具调用是有顺序的并行执行完结果必须按原顺序组装回消息里否则模型会困惑。我的做法是给每个调用分配一个 index结果按 index 排序后再组装。3.5 权限与审计企业场景的硬要求企业内部工具权限和审计是绕不过去的。BizBuddy 在这块的设计原则是权限校验在工具执行前审计日志在工具执行后。权限校验分两层。第一层是用户级这个用户有没有权限使用这个 Agent。第二层是工具级这个 Agent 有没有权限调用这个工具以及这个用户通过这个 Agent 调用这个工具时数据范围是什么。第二层是关键因为同一个工具不同用户能查的数据范围可能不同。我的实现是给工具加一个ToolPermission注解声明需要的权限码。执行前平台从当前会话里取出用户身份调用公司的权限中心校验。校验不通过直接返回错误不执行工具。审计日志记录每一次工具调用谁、什么时候、通过哪个 Agent、调用了哪个工具、参数是什么、结果状态如何、耗时多少。这些日志异步写到审计系统不阻塞主流程。参数里如果有敏感信息比如身份证号、手机号要在写日志前脱敏。注意审计日志的脱敏一定要在写入前做不能指望下游系统脱敏。下游系统可能有很多个你控制不了每一个。敏感信息一旦落盘就是合规风险。4. 常见问题与排查实录4.1 模型幻觉调用不存在的工具这是上线初期最常见的问题。模型有时候会编一个工具名出来比如实际工具叫queryOrder它返回getOrder。平台找不到这个工具就会报错。排查思路先看模型返回的原始内容确认工具名到底是什么。如果确实是模型编的说明工具描述不够清晰或者工具太多导致模型混淆。解决方法有三个层次。第一优化工具描述让名字和描述更明确减少歧义。第二减少单次暴露的工具数量如果一个 Agent 挂了 20 个工具模型很容易选错可以按场景拆分 Agent。第三加一层容错工具找不到时不直接报错而是返回一条工具不存在可用工具列表如下的消息给模型让它重新选择。第三层是兜底前两层才是根本。4.2 工具调用参数解析失败模型返回的参数是 JSON 字符串有时候格式不合法比如多了个逗号、少了引号、或者类型不对该是数字的给了字符串。解析失败工具就没法执行。我的处理是宽松解析加类型转换。JSON 解析用宽松模式允许一些常见的不规范写法。解析出来之后按工具声明的参数类型做转换字符串转数字、数字转字符串都支持。转换失败才报错。另外参数校验要给出明确的错误信息。不要只说参数错误要说参数 orderId 期望是字符串实际收到的是数组。明确的错误信息模型看到之后往往能自己纠正重新发起调用。4.3 会话状态丢失或错乱这个问题比较隐蔽通常出现在多实例部署或者服务重启之后。表现是用户发现 Agent失忆了或者回复的内容对不上之前的对话。排查思路先确认会话状态存在哪里内存还是 Redis。如果是内存多实例部署时请求被负载均衡打到不同实例状态自然对不上。如果是 Redis检查 key 的过期时间设置以及序列化反序列化是否正常。我的经验是会话状态一定要有版本号。每次更新状态版本号加一。读取时如果发现版本号比预期低说明状态是旧的要么拒绝使用要么触发重新加载。这个机制能挡掉大部分状态错乱问题。4.4 高并发下的性能瓶颈压测的时候发现并发一上来响应时间飙升。用工具分析瓶颈通常在两个地方模型调用和工具执行。模型调用是外部依赖你控制不了它的速度能做的是连接池复用和合理的超时设置。连接池要够大但也不能无限大否则会把模型服务打挂。超时要设但不能太短否则正常的长回复会被误杀。工具执行如果是 IO 密集的用线程池隔离避免一个慢工具拖垮整个平台。线程池的大小要按工具类型分别设置查询类工具可以大一点写操作类工具要小一点避免并发写导致数据问题。4.5 常见问题速查表问题现象可能原因排查方向解决思路模型调用不存在的工具工具描述不清或数量过多查看模型原始返回优化描述、拆分 Agent、加容错参数解析失败JSON 格式不合法或类型不符打印原始参数字符串宽松解析、类型转换、明确报错会话状态丢失多实例未共享或序列化问题检查存储位置和 key加版本号、统一存储、校验序列化高并发响应慢模型调用或工具执行瓶颈用 APM 工具定位连接池、线程池隔离、超时设置工具执行超时后端服务慢或死锁查看工具内部日志加超时、熔断、降级审计日志缺失异步写入失败或队列满检查日志队列状态加监控、队列满时降级为同步5. 一些踩坑之后的经验总结5.1 关于纯 Java这件事的再思考项目做完回头看纯 Java 这个选择是对的但代价确实比预想的大。最大的代价不是写代码本身而是生态缺失带来的隐性成本。比如做一个向量检索Python 那边几行代码的事Java 这边要自己封装客户端、处理连接、做序列化工作量翻了好几倍。所以我的建议是如果你的团队 Java 底子厚、存量系统多纯 Java 值得如果团队更熟 Python、存量系统少没必要为了统一技术栈硬上 Java。技术选型要看团队的实际能力不要被纯这个字绑架。5.2 关于 Agent 平台的边界做这个项目最大的收获是搞清楚了 Agent 平台不该做什么。它不该替代业务系统不该承担核心交易逻辑不该做模型训练。它的定位就是一层自然语言交互层把已有的能力用更自然的方式暴露给用户。想清楚这个边界之后很多设计决策就顺了。比如工具执行失败平台不该自己重试业务逻辑而应该把失败信息返回给模型让模型决定怎么办。再比如数据权限平台不该自己维护一套权限体系而应该复用公司已有的权限中心。5.3 关于可观测性上线之后可观测性的重要性远超我的预期。Agent 系统的不确定性比传统系统高得多同样的输入模型可能给出不同的输出工具可能因为各种原因失败。没有足够的日志和埋点出了问题根本无从下手。我的做法是每个环节都打点模型调用打了、工具调用打了、上下文裁剪打了、状态读写打了。日志里带上 traceId一次请求的所有日志能串起来。这些埋点在开发阶段看起来是负担上线之后是救命的。5.4 最后分享一个小技巧如果你也在做类似的平台我强烈建议先做一个最小可观测闭环一个最简单的 Agent一个最简单的工具一套完整的日志。先让这个闭环跑通再往上加功能。不要一上来就设计大而全的架构那样很容易陷入设计三个月写代码三天改架构三个月的循环。BizBuddy 的第一版核心代码不到两千行但日志、埋点、错误处理一样不少。正是这个小但完整的底子让后面加功能变得很顺。这个经验我觉得比任何架构图都值钱。