☰
纯Java打造企业级Agent Harness:从失控事故到可控平台
2026/10/11 6:37:57 网站建设 项目流程

监控大屏上,某个内部知识库问答服务的错误率从 0.01% 一路爬升到 12%,前后不过十分钟。追到日志才发现,罪魁祸首是一个刚上线的 Agent 机器人:它在一次会话里对同一个数据库查询工具连发了四十多次请求,把平时没有负载的慢查询全部打满,整个链路直接拖垮。这不是模型太笨,而是我们把 Agent 当成普通 Web 服务来写了——没有人在背后管住它的行为边界。

那次事故之后,我们团队做了一个决定:把散落在各个业务系统里的 Agent 能力收拢起来,统一放进一个用纯 Java 实现的内部平台里,代号叫 BizBuddy。这个平台不是模型网关,也不是 Python 生态里的那些 Agent 编排框架,而是一个真正意义上的 Agent Harness——负责 Agent 的完整生命周期、工具调度、记忆管理、权限控制和审计追踪。这篇文章想把 BizBuddy 的设计过程和取舍思路完整讲清楚:为什么选纯 Java、核心模块怎么设计、生产环境踩过哪些坑、性能和稳定性怎么保障。如果你所在团队也是 Java 技术栈,又要在内部落地 Agent 场景,这篇文章应该能帮你少走不少弯路。

1. 一切从一次线上事故说起:Agent 失控与被逼出来的 Harness

1.1 事故回放:循环调用工具压垮下游

事故发生的背景很简单。当时某个业务方找过来,说想做一个"能自己查数据、自己总结结果"的问答机器人,接入内部知识库和几个经营分析系统。开发同学上手也很快,直接用主流的大模型 SDK 写了一个循环:把用户问题发给模型,模型返回工具调用指令,代码执行工具,再把结果塞回给模型,如此往复直到模型给出最终答案。

上线第一天就出了问题。监控显示,数据库连接池使用率持续 100%,一堆平时执行只需要几十毫秒的查询突然变成了慢查询。我们把 trace 串起来看,发现机器人陷入了"查订单表 → 结果字段带出更多订单 ID → 模型又调同样的查询工具 → 再查一遍"的循环。最要命的是,工具执行代码里没做任何失败限制,模型每一次返回的指令都会被无条件执行。四十多次重复查询打过去,任何一个库都扛不住。

这个锅不能全让模型背。大模型的推理能力确实存在不确定性,尤其是工具调用这类指令生成,它可能因为上下文太长、指令理解偏差,反复生成同一条调用。真正的问题在于我们没有任何一层机制去拦截这种失控行为。普通 Web 服务没有这个问题,因为请求-响应是一次性的,入口有鉴权、有参数校验、有超时;但 Agent 是一个会自主发起动作的循环,循环本身没有任何节流和校验机制。

1.2 为什么普通的 Web 服务思路撑不住 Agent 场景

后来复盘时我们总结了一句话:Agent 和普通接口的本质区别,在于它需要被当作一个"具有自主行为的进程"来管理,而不是一个"被动的请求处理器"。

普通接口的请求是有清晰边界的:进来一个请求,做一步操作,返回一个结果。但 Agent 的一个会话可能包含几十轮模型推理、几十次工具调用,每次工具调用都可能产生副作用——写数据库、发消息、改配置、触发外部流程。这些副作用一旦出错或者失控,影响范围比单次接口失败大得多。

其次,普通 Web 服务有成熟的治理体系:网关、熔断、限流、监控告警都是现成的。但 Agent 的治理要增加很多新维度:单次会话内的工具调用次数限制、工具权限的最小化授权、模型输出格式的校验、会话上下文的持久化和版本兼容、以及整个推理过程的审计追踪。这些东西,靠业务代码自己写完全没有统一标准,每个团队各写一套,最后一定是灾难。

BizBuddy 就是带着这些诉求立项的。它不是一个研究型项目,而是一个面向生产的内部平台。我们的目标很明确:让业务方写 Agent 时,只需要关注"这个 Agent 要完成什么任务"和"它允许用哪些工具",至于循环怎么控制、状态怎么保存、日志怎么记录、权限怎么校验,全部交给 Harness 处理。

2. 为什么坚持纯 Java:存量系统、统一运维与边界自律

2.1 Python 生态的诱惑与 Java 的现实优势

立项时第一个大问题就是技术栈。市面上主流的 Agent 框架几乎都出自 Python 生态,开箱即用的组件非常多,社区案例也丰富。团队里也有人提出:要不要单独起一个 Python 服务放 Agent 逻辑,Java 业务通过 HTTP 调用?

这个方案我们认真考虑过,讨论了两轮,最后还是否掉了。核心原因不是 Python 不好,而是我们评估下来,混合架构带来的成本比收益大。

第一,我们公司内部的核心业务系统几乎全是 Java 技术栈。Agent 要驱动的工具、要访问的权限体系、要对接的审批流全部在 Java 这边。如果要让 Python 服务接入这些能力,要么引入一套跨语言的 RPC 框架,要么把工具调用全部包成 HTTP 接口——这两种做法都会大幅增加链路的复杂度和延迟。

第二,运维体系要求统一。公司现有的监控、日志、配置中心、服务发现全部围绕 JVM 生态建设。加一个 Python 服务进来意味着要重新搭一套日志采集、链路追踪和部署流水线,还要养一个能维护 Python 生产环境的团队。为了一个 Agent 服务做到这些,性价比太低。

第三,也是很多人容易忽略的一点,Java 做 Agent 平台并不吃亏。模型调用本质是 HTTP/WebSocket,Java 生态里的异步客户端和响应式编程模型非常成熟。真正难的是工具的注册、权限控制、事务管理、状态持久化——这些恰恰是 Java 企业级开发积累最深厚的领域。

2.2 什么是"纯 Java":自研轻量内核的取舍

"纯 Java"在 BizBuddy 里有两个含义。第一层是不引入 Python 代理、不搞跨语言脚本,整个平台从运行时到管理端全部跑在 JVM 上。第二层是内核核心逻辑自己写,不直接依赖那套重量级的编排引擎。

有人会问:Java 生态里也有不少 Agent 相关框架,为什么还要自研?我们调研过一些已有的 Java 版 LLM 编排框架,发现它们解决的是"能把一个 agent 跑起来"的问题,但距离企业级还有不少距离。比如工具调用的循环次数控制、会话级状态机、细粒度的权限模型、对内部 RPC 协议的原生支持,这些方面要么没有,要么就做得很薄。

再者,Agent 场景本身的演进速度太快,今天的设计可能几个月后就过时了。如果依赖一个外部框架,框架升级会绑架我们的迭代节奏。自研轻量内核的好处是,团队可以随时根据业务反馈调整核心抽象,坏处是初期成本高。但考虑到我们要承载的是一个持续演进的企业级能力底座,这个成本我觉得值。

自研不代表闭门造车。模型调用我们直接使用标准 HTTP 客户端,向量检索走内部已有的向量检索服务,缓存、消息队列、定时任务这些基础能力全部复用公司中间件。BizBuddy 真正自研的部分只集中在"Agent 专属逻辑"上:推理循环的状态机、工具注册与鉴权中心、会话记忆的分段管理、以及事件化的审计模块。

2.3 适合用"纯 Java"写 Agent 平台的场景边界

我也想把"纯 Java"的适用边界讲清楚,免得有人看了这篇文章直接套用。如果你的场景是快速验证一个 Agent 原型,或者你所在的团队本来就以 Python 为主,那完全没必要学我们。纯 Java 的核心优势在于"融入存量体系",如果公司没有 Java 存量系统,这个优势就不存在了。

但如果你的情况也满足下面几条,那纯 Java 方案会很合适:

  • 公司核心业务系统是 Java 技术栈,Agent 要深度调用现有服务;
  • 有严格的安全合规要求,需要统一的鉴权、审计和权限管控;
  • 运维体系已经标准化,不希望为单个项目引入新的技术栈;
  • Agent 需要和现有事务、工作流、消息系统做深度集成。

简单说,BizBuddy 的选择不是"Java 比 Python 好",而是"在企业内部落地,Java 的治理优势比 Python 的生态优势更值钱"。

3. BizBuddy 的核心架构:围绕"可控性"展开的四个关键模块

整个平台的设计原则,如果浓缩成一句话,就是"给 Agent 的自由度,必须在平台的边界内"。基于这个原则,我们把架构拆成了四个核心模块:Agent Runtime、ToolRegistry、Memory Manager 和 Observability。下面逐个讲。

3.1 Agent Runtime:推理循环的显式状态机实现

Runtime 是整个平台的中枢,负责驱动 Agent 的推理循环。我们做的第一件事,就是把隐式的 while 循环改造成显式的状态机。每一条 Agent 会话从创建到结束,都会经历一系列明确的状态,比如等待输入、调用工具、等待模型响应、生成回复、异常终止等。

这里贴一个简化版的状态定义,方便理解:

public enum AgentState { IDLE, RECEIVING_INPUT, PREPARING_CONTEXT, THINKING, TOOL_CALLING, WAITING_TOOL_RESULT, REFLECTING, GENERATING_RESPONSE, COMPLETED, FAILED, CANCELLED }

每个状态下平台都会强制执行约束。比如 THINKING 状态下必须计算本轮已消耗的模型 Token,如果超过单轮会话上限,直接强制切换到 FAILED;TOOL_CALLING 状态下必须检查工具调用次数、工具权限、单工具超时,任何一项不满足都会中断循环。

状态机的另外一个好处是可恢复性。如果 JVM 重启导致某个 Agent 会话中断,我们可以根据持久化的状态恢复执行,而不是从头开始。这在企业级场景里很重要,因为很多 Agent 任务需要长时间运行,比如"每天定时生成数据报表并对异常指标给出建议",这类任务跑一半断了,用户可不想重新开始。

状态转移的时机全部通过事件驱动,每个状态切换都会生成一条 AuditEvent 写入审计日志。这样不仅方便排查问题,也为后续做行为分析积累了原始数据。

3.2 ToolRegistry:工具调用不做直连,只走注册与鉴权

工具调用是 Agent 最容易失控的地方,所以我们把工具调用单独抽成了一个中心化模块:ToolRegistry。业务方的工具不能直接被 Agent 调用,必须先注册到平台,声明工具名称、参数 Schema、调用方式、超时时间、限流阈值、权限等级。

ToolRegistry 的核心逻辑有以下几条:

  • 工具注册时要做参数 Schema 校验,确保模型生成的参数能被正确解析;
  • 每次调用前做权限校验,判断当前会话的 Agent 是否有权限调用该工具;
  • 每次调用前做频率校验和并发校验,超过阈值的调用直接拒绝;
  • 所有调用都会记录时间、参数、返回值摘要和耗时。

实际运行中,ToolRegistry 最重要的作用是防止工具被"绕过"。模型输出的工具名必须和注册表里的完全一致,一旦出现未注册的工具名,Runtime 不会把它当作普通错误直接传给模型继续尝试,而是直接中断循环并提示使用者"工具调用超出授权范围"。这比让模型自己处理错误要安全得多,因为我们不想让模型"临场发挥"决定调用一个没被批准的工具。

3.3 Memory 分段与序列化版本策略

Agent 的记忆管理比想象中复杂。早期我们把整个会话上下文塞到一个 JSON 里存 Redis,后来发现两个问题:一是上下文太大导致每次构建请求都超时,二是模型容易被无关的历史信息干扰。于是我们按生命周期把记忆分成了三段:

会话记忆(Session Memory):当前对话内的短期上下文,包括用户每一次输入和 Agent 每一条回复。这部分默认保留最近几轮,超出部分做摘要压缩。

工作记忆(Working Memory):Agent 完成任务过程中的中间状态,比如"当前正在查询哪张表""已经拿到哪些数据"。这部分数据直接支撑持续性任务的执行,需要精确存取。

长期记忆(Long-term Memory):跨会话的偏好、历史结论、用户属性等。长期记忆统一存到独立的存储服务,通过向量检索按需召回。

三段记忆在实现上是隔离的,序列化时用不同的类定义。这里要特别提醒一件事:序列化协议一定要带版本号。我们曾因为把 Java 对象直接存进 Redis,之后类结构一变,反序列化直接炸掉,所有存量会话全部失效。后来我们统一改成带版本号的持久化结构,每次变更必须兼容旧版本,否则无法上线。

3.4 可观测性:把每一次思考过程变成可检索事件

Agent 是一个多步骤系统,一旦行为异常,如果没有可观测性,排查会极其痛苦。BizBuddy 里,我们把 Agent 的每一步动作都定义为事件,包括模型请求发出、模型响应返回、工具调用开始、工具调用结束、状态机切换、会话创建、会话结束等。

每个事件都带有会话 ID、追踪 ID、Agent 名称、工具名称和耗时信息。这些事件统一发送到内部的消息队列,一份走实时监控告警,一份落盘做离线分析。我们在监控大盘上能看到每个 Agent 的调用频率、工具分布、成功率、平均延迟。

后来我们还在事件基础上加了"行为审计"能力:当某个 Agent 在单次会话内调用工具超过预设阈值时,平台自动标记异常并在告警里附上完整的事件链路。这比事后翻日志要高效得多。可以说,可观测性不是平台的附属功能,而是平台真正能"管住"Agent 的底气。

4. 生产环境踩坑实录:四类典型故障的完整排查链路

自平台上线至今,我们踩过的坑不少。这一节不去背解决方案手册,而是把四个有代表性的故障的完整排查链路写出来,大家以后遇到类似问题可以参考排查思路。

4.1 故障一:工具失败后的无脑重试导致下游雪崩

现象:某个 Agent 上线三天后,下游一个核心系统的错误率突然飙升到 20%,但平台上 Agent 本身的错误率却不高。

排查:我们先用 trace 关联,发现所有错误都来自同一个工具调用,而且调用方全部是同一个 Agent。接着查看工具调用的明细日志,发现同一个参数的调用在十分钟内被重复了二十多次。进一步追踪后确认,模型生成的工具调用参数是不变的——因为它每次拿到的错误结果都一样,就认为"再试一次可能成功",而我们的工具执行层没有任何重试限制。

根因:两处设计缺陷。第一,工具执行层在捕获到调用失败后直接把异常信息返回给模型,没有对失败次数做统计。第二,Agent 循环本身没有"同一工具连续失败 N 次必须终止"的熔断规则。

修复:在 ToolRegistry 里增加两颗"保险丝"。一是同级熔断,同一会话内同一个工具连续失败 3 次,后续调用直接短路,不再发给模型;二是全局限流,同一工具在单位时间内的调用次数超过阈值后,拒绝服务并告警。这个故障给我们的教训是:Agent 平台必须假设模型永远会"不撞南墙不回头",风险控制只能由平台兜底。

4.2 故障二:Redis 里的会话上下文类升级后集体反序列化失败

现象:一次例行升级后,所有存量会话全部打不开。用户反馈说"我的历史对话都不见了"。

排查:看日志发现反序列化异常,错误信息指向某个 Java 类的字段变更。原因是团队某工程师在优化上下文结构时,删除了一个旧字段,但 Redis 里的历史数据仍然使用旧结构,反序列化器版本不兼容。因为我们的 Redis 序列化用的还是默认的 JDK 序列化,没有版本兼容机制。

根因:做了"Do Not Break Version"这条铁律,但没在技术上强制落地。这类问题最大的隐蔽性在于:测试环境数据量小,几乎没有历史数据,问题根本触发不出来。

修复:做了三件事。第一,自定义序列化方案,每条上下文数据带 schemaVersion 字段,反序列化时按版本号走不同的映射逻辑。第二,不允许直接删除字段,只能标记废弃并保持默认值兜底。第三,上线前增加一个"历史数据兼容测试",用生产环境的脱敏数据跑一遍反序列化。这件事之后,我们再也不敢对持久化结构做"手起刀落"式的改动。

4.3 故障三:流式输出阻塞把 Executor 池拖死

现象:某段时间,Agent 服务的响应时间大面积上涨,但 CPU 和内存都很正常,线程池监控显示活跃线程数远小于最大线程数。

排查:这部分很容易被 CT 误导。线程池没达到上限不代表没有瓶颈,我们把线程池的任务队列深挖出来,发现队列里堆了几万个待执行任务。再往细节看,这些任务全部卡在"等待模型流式响应"上。

根因:Agent 调用模型采用流式输出时,我们用了阻塞队列来接收消息。消费端处理一个 Token 需要做格式化、事件写入等多个步骤,处理速度跟不上模型输出速度时,阻塞队列积压,积压又导致上游的发送逻辑迟迟不释放,最终整个线程池的可用线程被占满。

修复:流式响应改用响应式消费模型,用独立线程池处理 Token 流,并实时丢弃来不及处理的中间 Token;同时对整条流式链路做了背压控制,消费阻塞时主动向模型端发送暂停信号。另外,我们把"模型流式输出超时"独立出来告警,现在能在问题发生的第一时间发现。

4.4 故障四:乐观超时让状态机卡在"等待模型"的死路

现象:一个长耗时任务 Agent 偶尔会"卡死",状态一直停留在"等待模型响应",没有任何报错。

排查:我们查看状态机日志,发现卡死的会话全部有一个共同点:调用模型时设置的超时时间为 10 秒,但模型实际返回最慢时需要 30 秒。超时发生后,代码捕获了超时异常,却没有正确地把状态机推进到"需要重试或终止"的状态,而是留在了"等待模型响应"。更隐蔽的是,这个会话在 Redis 里的状态数据没有过期时间,于是它永久滞留在运行队列,占用一个并发额度。

根因:状态机的状态转移设计不考虑超时场景。任何等待状态都必须配置"超时后转移到哪里",这条规则在初期没有严格落实。

修复:把所有等待类状态统一加上超时监听器,超时后自动根据当前会话的剩余重试次数,决定进入"待重试"还是"异常终止"。同时给所有运行中的会话加上心跳和最大运行时长,超过两小时没有完成的任务强制终止并审计。现在平台里不会再出现"僵尸 Agent 会话"。

5. 性能、资源与降级:企业级 Harness 必须做的三件事

5.1 虚拟线程带来的并发模型简化

BizBuddy 开发初期,Agent 的每一次工具调用都通过 CompletableFuture 串起来,代码写得很痛苦。每个环节都要考虑异步回调,一旦编排逻辑复杂起来,可读性和可维护性都很差。后来我们升级到 Java 21,全面改用虚拟线程,这才从异步泥潭里解放出来。

虚拟线程让"同步写法 + 高并发承载"变成了现实。Agent 的推理循环天然适合虚拟线程:每个会话一个虚拟线程,阻塞等待模型响应时释放底层平台线程,并发会话数量能轻松支撑数千个而不需要复杂的异步编排。从实际观测来看,单个节点跑几百个并发 Agent 会话,线程资源消耗完全可忽略。

如果你还在用 Java 17 及以下版本,想写 Agent 平台的话,建议评估一下升级到 Java 21 的成本。虚拟线程对 Agent 这类 IO 密集型、同步编程优先的场景,带来的简化是革命性的。

5.2 限流、熔断、背压:Agent 入口的"防洪堤"

Agent 平台面向企业内部时会面临两类流量:一类是用户主动触发的低并发请求,另一类是定时任务和批量任务触发的并发洪峰。后者一旦发起,如果没有限流,瞬间就能把下游压垮。

我们在平台入口设计了三级限流:

  • Agent 级限流:单个 Agent 每分钟最多启动 N 个会话;
  • 工具级限流:单个工具每分钟最多调用 M 次;
  • 模型级限流:单个模型 Key 每分钟最多请求 K 次。

三个数字都做成可在配置中心动态调整的参数,业务方申请 Agent 时必须填写预估调用量,平台根据配置自动下发限流规则。每个会话内还有更细的限制:最大工具调用次数 20 次、最大思考轮数 5 轮、单次会话最大 Token 消耗 2 万。这些限制不是拍脑袋定的,是根据线上实际分布反复调整后的结果。

背压也要做。当工具返回结果速度变慢,Agent 循环应该能感知并降低请求频率,而不是继续向模型请求下一步动作。我们在工具调用层增加了基于令牌桶的流量整形,工具服务端响应变慢时,令牌桶填充速率自动降低,从源头减少了无效调用。

5.3 模型失效时的预案设计

企业级平台必须面对一个现实:外部模型服务不可能永远稳定。网络抖动、模型超时、限流、内容审核拒绝,任何一次异常都不应该让整个 Agent 服务不可用。

我们的策略是建立多级降级链。首选模型 Primary 挂了,自动切换到备用模型;备用模型也异常,则降级到本地规则引擎,执行预设好的确定性逻辑;如果连规则引擎都不满足应用场景,则直接把会话转入人工处理队列。

降级切换要快,因此我们做了模型健康度探测:每 30 秒对当前主模型发一次轻量请求,连续失败 3 次就把模型标记不健康,新会话自动路由到备用模型。已进入运行的会话不会强制切换,避免中途切换导致上下文错乱。

有一个经验:降级切到备用模型时,提示词也要跟着切换。不同模型的指令遵循能力、工具调用格式支持程度都不一样,直接复用同一套提示词,很可能出现格式解析失败。BizBuddy 里每个 Agent 可以同时配置多套提示词模板,按模型类型自动选择。

6. 测试与灰度:如何证明一个会自己行动的系统是安全的

6.1 离线回归:用"黄金样本集"锁定行为边界

Agent 平台最难测试的地方在于行为不确定。传统单元测试只能验证单个组件,无法验证"整个 Agent 在给定输入下是否会做不该做的事"。我们建立了一套离线回归体系:从线上收集大量真实会话数据,清洗后做成"黄金样本集"。

每个黄金样本包括:输入问题、当前会话历史、可用工具列表、期望的工具调用序列、期望的最终回复。回归测试时,我们用录制好的模型响应回放来驱动 Agent,而不是真实调用模型。这样可以完全复现线上场景,每分钟能跑几百个样本,几十秒就能完成一轮回归。

这套回归体系最大的作用是防止"回归性失控"。比如某次我们调整了上下文压缩策略,离线回归立刻发现一个 Agent 在压缩后丢失了关键约束指令,导致它开始调用超出授权范围的工具。如果没有离线回归,这个问题大概率会直接漏到生产环境。

6.2 灰度放量与行为审计:上线不是终点

即使离线测试全绿,Agent 上线后仍然可能遇到新场景,所以我们坚持灰度发布。流程是:先在内部员工群小范围放量,跑 3 天看行为审计;没问题再扩大到一个部门;最后才全量开放。

灰度期间重点关注三件事:工具调用分布是否合理、单会话成本是否超预期、是否有未注册工具调用或越权调用。如果发现某个 Agent 频繁触发某类高风险工具,即使它完成了任务,也会先把权限收紧再继续灰度。

行为审计要保留足够长的时间。我们默认保留 180 天的完整事件日志,这不仅是合规需要,也是后续优化 Agent 提示词和工具设计的重要依据。很多问题不是立刻暴露的,可能某个 Agent 上线两周后,随着用户问法变多变杂,行为开始偏离预期。这时候翻审计日志,能很快定位是提示词问题还是工具边界定义问题。

最后再分享一点个人体会。做 Agent Harness 平台这一年多,我的一个核心感受是:Agent 的技术难点其实不在模型调用,而在如何管理一个"会自己行动"的程序。模型的能力会越来越强,工具会越来越多,用户的问题会越来越开放,但平台要做的始终是同一件事——在赋予 Agent 自主性的同时,牢牢守住安全边界和行为边界。纯 Java 的选择,自研内核的决定,以及那些看似琐碎的状态机、限流、审计模块,本质上都是在为"可控"服务。如果你的团队也在规划类似的平台,我建议从最小闭环开始做一个能管控整个循环的 Harness,不要急着把模型的各种花活都接进来。先把行为边界画清楚,后面的事情会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询