从Demo到生产:BizBuddy纯Java企业级Agent Harness运行治理设计
发布时间:2026/10/11 4:30:30 锦皓数字建站

先把结论放在前面BizBuddy 不是又一个“把 Prompt 包装一下”的 Agent 框架而是一个用纯 Java 实现的企业级 Agent Harness 平台。Harness 这个词在测试领域很常见意思是“夹具、运行容器”它负责把被测对象装进去、控制它的执行、记录它的行为和结果。换到 Agent 场景BizBuddy 做的事情同样如此模型负责做决策平台负责把决策变成一次可追踪、可恢复、可审计、可终止的企业级运行过程。如果你之前只写过“调模型接口、拼上下文、循环几轮”的 Agent 原型你可能会觉得 Harness 多此一举。但一旦把 Agent 放进生产环境问题立刻变成另一套进程崩了怎么办工具调用重复执行了怎么办模型陷入死循环怎么强制停止谁在什么时间调用了哪个敏感工具这些问题不是模型能回答的而是运行平台必须回答的。这篇文章就把 BizBuddy 在设计上的核心取舍拆开讲清楚适合那些准备把 Agent 从一个 Demo 变成内部生产系统的团队参考。1. 为什么是 Harness 而不是另一个 Agent 框架BizBuddy 想解决的真实问题先还原一个很典型的场景。某团队最初做内部自动化助手时用的是一套很轻量的脚本调大模型接口把返回的内容当作下一步指令继续调工具再继续调模型。Demo 跑得很顺但是上线第一周就出了问题某个工具返回了异常格式模型不理解于是开始反复调用同一个写操作更麻烦的是进程重启后对话上下文丢了用户完全不知道这个任务到底执行到哪一步。那套脚本没有状态、没有生命周期、没有审计出了问题只能靠人翻日志拼时间线。BizBuddy 的核心出发点就是把“Agent 的运行权”从模型手里收回到平台手里。模型只负责输出决策比如“下一步调用 queryOrder 工具参数是 orderId1001”至于这个决策是否被允许、调用结果存到哪里、失败后是重试还是终止、整个过程如何被事后追踪全部由 Harness 负责。这个边界定清楚之后Agent 就不再是一条脆弱的循环而是一个可以被治理的长时运行服务。1.1 Agent 平台的难点不在“对话”而在“运行权”很多框架把精力花在怎么写更长的提示词、怎么编排 Chain但忽略了一个事实Agent 的每一次工具调用都可能是外部系统的副作用。发邮件、改数据库、调用支付接口、执行命令这些动作一旦发生就不是模型输出一个文本那么简单。真正难的是保证这些动作被正确控制执行前有没有权限检查执行中有没有记录执行后如果任务中断下一次恢复能不能不重复执行。所以 BizBuddy 从一开始就把状态、生命周期、副作用标识作为第一公民。每个 Agent 任务不是简单的“一问一答”而是一条拥有明确状态的对象等待中、运行中、等待工具、等待人工审批、完成、失败、被终止。这个设计很像我们以前写工作流引擎先定义清楚状态和流转再谈业务逻辑。1.2 为什么坚持纯 Java 实现有人问过为什么不用 Python 生态里那些现成的 Agent 库答案很实际BizBuddy 要服务的企业运行环境大部分已经围绕 Java 建设了。安全认证、配置中心、监控系统、发布流程全部是 Java 技术栈引入一个 Python 运行时意味着多一套部署、多一种依赖链、多一份安全审计负担。纯 Java 让整个平台可以打成单 JAR 包丢到任何一台有 Java 运行时的机器上就能跑。另一个原因是类型安全感。Agent 和工具调用之间的数据交换是大模型生成的 JSON非常容易缺字段、多字段、给错类型。在 Python 里你很难在编译期发现这些问题但在 Java 里把工具参数定义为强类型 record用 JSON 绑定库去解析再配合启动期生成的 JSON Schema就可以把很多错误暴露在代码编译阶段或者启动阶段而不是让它在凌晨两点的生产环境里炸出来。当然纯 Java 的代价也很明显AI 相关的现成组件少很多能力要自己封装。比如模型调用、流式消息解析、向量检索这类东西Java 生态不像 Python 那么丰富。但在 BizBuddy 里这反而是可控的因为 Harness 本来就不需要依赖于某家模型厂商的 SDK它的核心价值在于流程治理而不是模型封装。2. 总体架构四个模块和一个稳定内核BizBuddy 的架构是在写第一行业务代码之前定的。原因很简单Agent 平台最容易翻车的地方就是模块边界不清晰最后模型调用、工具调用、权限逻辑、状态存储全都纠缠在一起改成任何一方都要动全身。2.1 模块划分与依赖方向整个平台分成五个模块core 负责定义领域模型和执行状态机runtime 负责真正跑 Agent 循环connector 负责对接不同模型服务的外部 HTTP 接口tools 负责工具注册、参数校验和执行console 负责管理面接口包括任务提交、审批处理、日志查询、审计导出。依赖方向是单向的console 和 runtime 都依赖 coreconnector 也依赖 core但 core 不依赖任何具体模型服务。这句话听起来像正确的废话但实际执行起来非常关键。因为只要你允许 core 包里去 import 某一个模型厂商的 SDK这个平台就会慢慢被那个厂商的接口模型绑架后续接一个新的模型服务就要改核心代码。BizBuddy 的做法是在 core 里定义模型无关的接口所有厂商差异都由 connector 模块去适配。2.2 虚拟线程为什么我放弃响应式编程第一版 BizBuddy 曾经考虑过用响应式编程模型因为 Agent 在执行过程中要等待模型响应、等待工具返回中间有很多 IO 阻塞。在传统线程模型下一个 Agent 任务占一个线程几百个并发就很容易把线程池打满。但响应式编程的代码写起来非常绕一旦出现复杂的条件分支和循环回调链会让人崩溃而且排查问题时堆栈已经完全看不出业务调用关系。后来 Java 21 提供了虚拟线程这个问题就迎刃而解。BizBuddy 让每个 Agent 任务跑在一个独立的虚拟线程里代码可以用最简单的顺序逻辑来写调用模型拿到结果保存状态判断下一步。虚拟线程的调度由运行时接管阻塞的时候自动让出底层载体线程完全不需要改造代码风格。实测下来普通机器上同时跑几百个后台 Agent 任务内存和线程开销都在可接受范围。2.3 内核不绑死任何模型SPI 接口是第一个取舍模型接入是 Agent 平台里变化最快的部分。今天这个服务支持工具调用明天那个服务支持 JSON 模式后天另一个服务又推出新参数如果内核直接绑定某一家的协议升级成本会非常高。BizBuddy 的做法是定义一组极小的能力接口比如ModelProvider、ChatSession、ChatChunk、ToolCallRequest。每个 provider 只暴露它支持的能力比如是否支持流式、是否支持工具调用、单次最大输出 token 数。上层调度逻辑根据能力描述决定怎么调用而不是假设所有模型都长一个样。这样设计的取舍是放弃了直接使用厂商 SDK 的便利性换来了接口稳定性和替换灵活性。BizBuddy 里接一个新模型服务大多数时候只是新增一个 connector 类填写若干个适配方法不需要动 runtime 和 core。这个代价我认为非常值得因为在企业环境里模型服务被替换是常态而不变量是流程本身。3. 一次 Agent 任务的完整生命周期状态机与持久化恢复如果只把 Agent 当成一个“多轮对话循环”那 watch out 的点不多。但 BizBuddy 把 Agent 任务当成一条有状态的工作流来处理就需要一套严格的生命周期。这套状态机不是摆设它是崩溃恢复、人工审批、并发控制的基础。核心状态包括PENDING 表示任务已提交还在排队RUNNING 表示正在执行WAITING_TOOL 表示已经发出工具调用等待结果WAITING_APPROVAL 表示调到了敏感工具需要人工审批COMPLETED 表示正常结束FAILED 表示出错终止CANCELLED 表示人工取消。还有一个容易被忽略的 TERMINATED_WITH_LIMIT表示被平台强制终止通常因为超过循环上限或预算上限。enum RunState { PENDING, RUNNING, WAITING_TOOL, WAITING_APPROVAL, COMPLETED, FAILED, CANCELLED, TERMINATED_WITH_LIMIT }状态流转的规则也很简单一切状态变更都通过事件驱动状态落库之后才允许执行下一步。这个顺序保证了在并发或者重启场景下不会出现“内存里以为执行了数据库里却没有记录”的情况。3.1 状态机设计先画状态再造循环BizBuddy 的 Agent 循环不是简单地在代码里写一个while(true)而是由一个状态机驱动。每次循环开始前runtime 从数据库读取当前状态和上下文然后决定下一步是调用模型、调用工具、等待审批还是终止。这样做的好处是循环可以被随时暂停比如遇到审批需求任务会进入 WAITING_APPROVAL 状态并持久化这时候即使整个平台重启任务也不会丢。执行模型推理时runtime 会先持久化一条“模型调用发起”事件并在同一个数据库事务里把状态更新为 RUNNING。模型返回后再记录“模型调用完成”事件把新的消息追加到会话存储里。这些事件构成了一条完整的时间线之后无论是排障、复现还是审计都能精确知道每一步发生了什么。3.2 检查点与崩溃恢复用数据换确定性企业级平台最怕的就是进程突然被杀掉。BizBuddy 的处理方式很简单也很有用每执行完一个关键步骤就写检查点。检查点包含完整会话消息序列、当前状态、已完成的工具调用列表、尚未执行的计划以及用于幂等的操作键。这里有一个细节值得展开。工具调用不是发起请求就算成功比如调用一个内部订单接口超时了无法确定请求是否已经被对端处理。BizBuddy 在发起工具调用之前会先给这次调用分配一个idempotencyKey落库之后再真正执行。如果恢复后发现某次调用处于“已发出但结果未知”的状态runtime 会把同样的幂等键重发给工具端让工具端自行去重。对于实在不支持幂等的外部系统平台不会盲目重放而是把任务标记为 FAILED 并转人工处理。宁可停下来也不能造成重复扣款或重复发单。3.3 循环上限与终止条件模型不喊停平台来喊停大模型很擅长把对话延续下去但它并不擅长主动说“我不需要再行动了”。很多 Agent 事故的根源就是模型认为下一步总该再做点什么于是一直调用工具。BizBuddy 将终止条件做成平台级硬配置每个任务有最大迭代轮数、最大消耗 token 数、最长执行时长任何一个阈值被触发runtime 就会强制终止循环状态置为 TERMINATED_WITH_LIMIT同时保留已经生成的中间结果方便事后分析。我建议默认迭代上限不要设太大常规任务 15 轮以内就够了。如果你的 Agent 经常需要超过 20 轮才能完成说明工具划分或者规划逻辑有问题而不是“模型更聪明”的表现。终止之后平台会生成一份终止原因说明用户可以在控制台上看到是哪一项阈值触发的这个信息对排查 Agent 行为非常重要。4. 工具注册、调用与权限纯 Java 最舒服的战场工具调用是 Agent 平台和外部世界交互的桥梁。BizBuddy 在这个模块花了很多心思因为工具系统的体验直接决定了平台能不能被业务团队真正用起来。4.1 注解声明工具启动期生成 JSON Schema我不想让业务方为了接入一个工具而去手写大段的 JSON Schema 或者解析代码。BizBuddy 的做法是用注解直接声明工具方法和参数。开发者只需要写一个普通 Java 方法加上AgentTool注解工具名和描述写在注解里参数用Param标注说明和约束。启动的时候平台会扫描所有标注了注解的类自动构建工具清单并根据参数类型生成对应的 JSON Schema。AgentTool(name query_order, description 查询订单状态orderId 是唯一订单号) public OrderInfo queryOrder( Param(description 订单号, required true) String orderId) { return orderService.findByOrderId(orderId); }为什么非要在启动期扫描而不是每次调用时反射解析因为启动期扫描可以把很多问题提前暴露出来比如两个工具重名、参数类型不支持、多个工具生成了冲突的 Schema这些在启动阶段直接失败而不是等到 Agent 运行到一半才报错。对于企业平台来说启动失败比运行到一半失败好处理得多。4.2 大模型输出到 Java 参数的解析严格校验还是宽容修复模型生成的参数 JSON 经常“不干净”字符串带引号、字段名多了空格、数组写成字符串。BizBuddy 的做法是先用宽松模式尝试解析比如把额外空白去掉、把兼容的字符串数字转成数值但如果宽松解析之后仍然无法满足工具参数定义的约束就严格拒绝并返回错误信息给模型让模型基于错误信息重试。参数修复一般允许重试两次超过两次就把该工具调用标记为失败。这里要特别提一个容易被忽略的点工具的参数模式里应当注入系统级字段但模型不应该能看到或者修改这些字段。例如当前用户、租户 ID、权限范围这些字段应该由 runtime 在执行时从上下文注入而不是让模型自己传进来。BizBuddy 的AgentExecutionContext保存了这些安全属性工具方法可以直接拿到但模型看到的 Schema 里只有业务参数。这是防提示注入和防越权的基础设计。4.3 工具权限与审批链路给 Agent 的手上锁不是所有工具都允许 Agent 自主调用。BizBuddy 把工具分成三类允许自动调用、需要权限校验、需要人工审批。权限校验对应 RBAC 模型比如当前用户没有“发送消息”权限调用这类工具时直接拒绝并把拒绝原因返回给模型。需要人工审批的工具则更复杂一点模型生成工具调用请求后runtime 不会立刻执行而是把请求持久化为一个审批单状态变成 WAITING_APPROVAL。审批单会展示给管理员谁在哪个任务里、因为什么原因、想调用哪个工具、参数是什么。管理员可以在控制台批准或者拒绝批准后 runtime 继续执行拒绝后任务回到 RUNNING 状态并把拒绝信息作为新的上下文发给模型。这个链路保证了“让人在关键环节兜底”也是企业级 Agent 和玩具 Agent 最明显的分水岭。5. 模型接入层统一接口背后的“方言翻译”模型接入层是 BizBuddy 里最容易陷入“过度设计”的地方。有些平台为了支持多家模型服务抽象出一个非常庞大的接口里面塞了几十个方法结果每家模型只实现其中一半大量方法抛出“不支持”。BizBuddy 换了一个思路接口尽量小能力用描述对象表达。5.1 Provider 之间的能力差异不是几个开关能拉平的不同模型服务在接口协议上的差异远不止 URL 和密钥不同。有的支持流式返回有的不支持有的工具调用参数叫functions有的叫tools有的系统提示词有特殊限制有的对 temperature 的取值范围有要求有的输出上限是 4K tokens有的是 8K tokens。企图用一个统一的参数对象把这些差异全部表达出来结果就是接口越来越臃肿。BizBuddy 的做法是定义一个Capability集合每个 provider 在初始化时声明自己支持哪些能力。runtime 在调用时先查能力集合再决定策略支持流式就走流式通道不支持就等完整返回支持工具调用就传工具清单不支持就只能让模型输出特定格式再解析。这样核心逻辑始终保持在一个合理的复杂度范围内。5.2 流式与非流式的双轨设计流式输出对用户交互体验很重要但对后台 Agent 任务的稳定性却是个干扰项。BizBuddy 把两种场景拆开面向实时用户的任务优先使用流式通道把增量消息推送给前端面向后台批处理的任务则使用非流式通道直接获取最终结果。这两个通道在内部都会转换成统一的ChatChunk事件最终落库时不留差异化痕迹。这样做有一个好处如果某个模型服务当天流式接口不稳定平台可以只对复杂后台任务自动切换到非流式而实时会话仍然保持流式体验。Provider 适配层仅仅依赖统一的模型无关消息类型不在 core 里暴露供应商特有的数据格式。5.3 重试、熔断与降级模型不可用时平台仍可用企业级平台必须假设模型服务会出问题而且可能是几分钟甚至更长时间的问题。BizBuddy 对模型调用做了完整的重试和熔断策略网络超时、5xx 错误走指数退避重试重试次数默认三次退避时间加上随机抖动避免集群同时重试造成雪崩。如果重试仍然失败runtime 会触发熔断开关短时间内不再把新任务路由到该模型服务。此时可以启用降级策略优先切换备用模型备用模型也没有可用时任务进入挂起状态而不是直接失败等模型服务恢复后继续执行。这个设计保证了 Agent 平台不会因为模型服务抖动就全线崩盘。指数退避公式delay min(base * 2^(attempt-1), maxDelay) random(0, jitter)6. 可观测性与审计Agent 每一步都要有交代Agent 平台的排障和普通接口排障完全是两码事。普通接口出了问题看一个请求日志就够了Agent 任务出了问题可能涉及一次会话、几十轮推理、多次工具调用、一次人工审批信息分散在多个地方。BizBuddy 在早期就把可观测性当成一等公民来做而不是上线以后补。6.1 一次会话一条 trace从 HTTP 请求到模型调用每个 Agent 任务从入口开始就分配一个全局唯一runId同时在日志系统里注入一个traceId。平台内部所有环节包括模型调用、工具执行、状态变更、审批事件都会携带这两个 ID。用户在控制台上输入一个 runId就能看到整条时间线哪次推理产生了哪条消息哪个工具被调用参数是什么结果是什么中间有没有出错有没有等待审批。这里有个实际经验日志关联不能只靠程序员自觉。BizBuddy 在抽象层强制要求所有日志方法都必须先从上下文取出 relate ID如果检测到当前线程上下文为空会立刻抛出异常。这个规则让漏传上下文的 bug 在开发阶段就能被发现。6.2 审计日志不是日志是证据普通业务日志是可以被清理的审计日志则不应该被随意删除。BizBuddy 把审计日志单独存储在独立表里只提供追加写入不提供普通业务代码的更新和删除接口。记录内容包括操作主体也就是哪个用户或哪个系统账户触发操作对象即哪个 Agent 任务、哪个工具操作内容包括完整的出入参操作结果时间线审批记录。为什么强调审计日志是证据因为企业里一旦发生越权调用或者敏感工具误操作你需要向安全团队和业务方解释到底发生了什么。没有审计日志Agent 就变成了一个“黑盒工具人”出了问题只能靠猜。BizBuddy 的审计事件还支持导出方便合规检查时做数据分析。6.3 数据脱敏与沙箱模型看到的是被加工过的世界模型在推理过程中会接收大量上下文但企业数据不能原样交给外部模型服务。BizBuddy 在构建提示词前会做一层脱敏处理手机号、身份证号、银行卡号、企业内部工号通过规则识别后替换成脱敏占位符。脱敏后的上下文才进入模型请求。工具调用层面则是相反的逻辑工具执行时如果确实需要真实数据平台只把最小必要字段传给工具方法并且只有在工具本身的 ACL 允许时才能拿到真实值。也就是说模型看到的是“员工张三工号 1024”而工具拿到的可能是经过权限校验后的完整对象两者之间的桥梁由 runtime 控制。对于允许执行任意代码的工具BizBuddy 坚持放到独立子进程里运行并关闭网络访问权限只开放约定的输入输出通道。这是防止提示注入影响扩大的关键措施。模型生成的代码本身也应当被视为不可信输入而不是当成“自己人”。7. 部署、测试与往后走的经验到了这一节BizBuddy 已经不是一个原型项目而是一个可以交付给内部业务团队使用的平台。最后一公里通常最磨人但纯 Java 在这里反而带来了不少红利。7.1 单 JAR 交付企业环境里最省心的物理形态BizBuddy 的部署产物就是一个可执行 JAR内部打包了控制台页面、REST API、执行引擎、工具扫描器和配置解析逻辑。业务团队拿到这个 JAR只要把数据库连接信息和模型服务信息通过环境变量或配置文件填好一条命令就能启动。相比那些要部署 Python 解释器、Node 运行时、多个微服务的方案单 JAR 实在太省心了。纯 Java 做容器化也同样方便基础镜像很小启动速度稳定没有奇奇怪怪的动态链接库依赖。对测试环境和生产环境做一致性验证时只要保证同一个 JAR 版本就不会出现“本地能跑、测试环境挂了”的经典问题。7.2 可回放的测试让 Agent 行为变成可回归的单元Agent 测试常让人头疼因为模型输出有随机性。BizBuddy 的解法是做一个录制回放测试框架开发环境接入真实模型服务时把输入会话序列、模型返回结果、工具调用记录全部录制下来测试环境使用一个模拟 Provider按录制文件返回事先存储的响应从而让测试行为完全确定。回归测试时就跑录制的会话断言工具调用顺序、参数、最终状态和预期一致。这个思路可能最简单但价值最大。录制回放让 Agent 的端到端测试从“碰运气”变成“可预期”也让后续改动工具的 Schema 或修改提示词时能快速判断哪些已有场景被无意破坏。配合工具 Schema 契约测试任何参数约束变化都会在 CI 阶段暴露而不是上线以后让 Agent 开始胡调。7.3 如果重来一次我会在第一天就盯住三件事BizBuddy 开发过程中踩过不少坑如果让我重做一遍有三件事会放在更高优先级。第一是把状态机和持久化模型先定下来不要一上来就接模型状态不稳后面所有功能都是空中楼阁。第二是工具权限模型从第一天就开始设计哪怕最初只有两三个工具也要把 ACL、审批、审计链路跑通因为事后补权限就像装修完再改水电。第三是把日志和全链路追踪做成强制规范不能靠自觉因为在 Agent 场景里任何一个环节丢了上下文排障成本都会变成灾难。纯 Java 做 Agent Harness 平台技术挑战从来不在“能不能跑通模型”而在“能不能让 Agent 在真实业务规则下稳定、可控、可解释地运行”。BizBuddy 把智能决策交给模型把运行治理握在平台手里这个边界我认为是企业级 Agent 平台最关键的取舍。如果你也在做类似的平台建议先把控制权握在自己手里再谈智能。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。