首页 / 资讯中心 / 文章详情

纯Java打造企业级Agent Harness:从失控事故到可控平台

纯Java打造企业级Agent Harness:从失控事故到可控平台 ★ FEATURED ARTICLE
监控大屏上某个内部知识库问答服务的错误率从 0.01% 一路爬升到 12%前后不过十分钟。追到日志才发现罪魁祸首是一个刚上线的 Agent 机器人它在一次会话里对同一个数据库查询工具连发了四十多次请求把平时没有负载的慢查询全部打满整个链路直接拖垮。这不是模型太笨而是我们把 Agent 当成普通 Web 服务来写了——没有人在背后管住它的行为边界。那次事故之后我们团队做了一个决定把散落在各个业务系统里的 Agent 能力收拢起来统一放进一个用纯 Java 实现的内部平台里代号叫 BizBuddy。这个平台不是模型网关也不是 Python 生态里的那些 Agent 编排框架而是一个真正意义上的 Agent Harness——负责 Agent 的完整生命周期、工具调度、记忆管理、权限控制和审计追踪。这篇文章想把 BizBuddy 的设计过程和取舍思路完整讲清楚为什么选纯 Java、核心模块怎么设计、生产环境踩过哪些坑、性能和稳定性怎么保障。如果你所在团队也是 Java 技术栈又要在内部落地 Agent 场景这篇文章应该能帮你少走不少弯路。1. 一切从一次线上事故说起Agent 失控与被逼出来的 Harness1.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/WebSocketJava 生态里的异步客户端和响应式编程模型非常成熟。真正难的是工具的注册、权限控制、事务管理、状态持久化——这些恰恰是 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如果超过单轮会话上限直接强制切换到 FAILEDTOOL_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 MemoryAgent 完成任务过程中的中间状态比如当前正在查询哪张表已经拿到哪些数据。这部分数据直接支撑持续性任务的执行需要精确存取。长期记忆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不要急着把模型的各种花活都接进来。先把行为边界画清楚后面的事情会顺很多。
阅读完成 · 觉得有帮助?
咨询建站