ARTICLE · 1107264
AgentScope Java 源码解析系列第 1 篇 HarnessAgent|"全家桶"不是新引擎,是一摞中间件和一棵会进化的工作区
AgentScope Java 源码解析系列第 1 篇 HarnessAgent|"全家桶"不是新引擎,是一摞中间件和一棵会进化的工作区
本文是 AgentScope Java 源码解析系列 第 1 篇(新支线开篇,模块:HarnessAgent)。
它接续两条线:AgentScope 2.0(Python)源码解析系列 13 篇——那边的结论是「全框架只有 1 个 Agent 类,定制全靠中间件」,本篇验证这件事在 Java 侧以更大规模重演;选型番外第 6/7 篇——那边说 HarnessAgent「把工作区、长期记忆、压缩、子 Agent、沙箱、Plan Mode 打包成一个入口」,本篇拆开看这个包到底怎么打的。
关注追更,Java 支线会沿着 ReActAgent → 事件流 → 分布式状态往下走。
HarnessAgent 不是新的 Agent 引擎。类注释写得很直白:它是「wraps a ReActAgentwith workspace / filesystem / sandbox / subagent / skill / plan-mode / MCP orchestration」——包着一个 core 模块的ReActAgent(getDelegate()可取),自己不实现推理循环。真正的加法发生在Builder.build()(HarnessAgent.java:2358起,到 2988 行结束,一个方法 630 行)。所有"打包能力"都是中间件。build() 按固定顺序装栈:沙箱生命周期 → 调用追踪 → 工作区上下文 → @路径展开 → 会话日志 → 记忆落盘/整理 → 上下文压缩 → 大结果卸载 → 收件箱 → Teams → 子 Agent → 异步工具 → Skill 加载 → Plan Mode。每个能力一个Middleware,插在推理循环的钩子上。子 Agent 的纪律是写死在一段提示词里的( HarnessAgentBuilderSupport.java:87起):「You are NOT the main agent」「NO spawning further subagents — you are a leaf worker」——叶子工人不生叶子,防递归风暴。每个子 Agent 继承父 Agent 约 30 项 builder 配置;子会话 ID 按「声明名@父会话#用户」分桶(:678),跨节点恢复靠它。Skill 有自学习闭环,且默认关着门:草稿仓库 → 用量统计 → 晋升门禁 → 主仓库,门禁默认是 RejectAllGate(HarnessAgent.java:2855)——默认什么都不自动晋升,外加后台 curator 把长期不用的技能老化归档,全程审计日志。Plan Mode = 只读工具白名单: plan_enter/plan_write/plan_exit三个工具(PlanModeTools.java:49-52),PlanModeMiddleware用AgentTool.isReadOnly()判断放行;plan_exit(开始动手)走 ASK 审批——先想清楚、想动了要人点头。配置错误在 build 期就拦: RemoteFilesystemSpec配本地状态存储直接抛IllegalStateException(HarnessAgent.java:2433-2441,错误信息原话「designed for distributed / multi-replica deployments」);沙箱配本地存储降级为警告日志。不等线上炸。
HarnessAgent 的打包方式是「不动引擎——把工程能力做成一摞可插拔的中间件,把 Agent 的定义做成一棵可版本化的文件树(工作区)」。你给自己的系统加"全家桶"能力时,先问哪一层该是中间件、哪一层该是文件,而不是去改核心循环——这套思路搬到任何长任务系统都成立。
适合谁读:读过或想读 AgentScope 源码、关心 2026 年"Agent 打包运行时"怎么实现的 Java/Python 开发者;正在选型 Agent 框架、想知道"HarnessAgent 里到底装了什么"的技术负责人。预计阅读:主线 14 分钟(另有两张按需查阅的字段表)。
一、HarnessAgent 在解决什么问题
先看官方定义。仓库自带的设计博客《如何构建 Agent Harness》第一篇(docs/v2/zh/blogs/how-to-build-agent-harness/01-patterns.md)把 Agent 拆成:
Agent = Model + Harness。Harness 是「模型之外、围绕 Agent Loop 组织上下文、能力、状态、环境与控制机制,并将模型判断转化为可执行、可恢复、可验证任务过程的代码、配置和执行逻辑」。
这段定义接着强调三件事:Harness 不是更长的 System Prompt;不是某个框架的同义词;也不是 Runtime 或 Sandbox。harness 这个词本义是马具——把马(模型)的力量套上缰绳、变成能拉车的那套装备。
没有 Harness 的裸 ReActAgent 跑长任务会遇到什么?一次模型调用没有可靠的跨轮状态;上下文窗口装不下长任务的全部事实;模型可以"声称"任务完成,但没人验证文件真的生成了、测试真的过了。HarnessAgent 要补的就是这些:工作区、记忆、压缩、子 Agent、沙箱、计划模式——番外第 6 篇列过的那张清单。
问题是:怎么把这些能力装进去?这是本篇真正要看的设计题。
二、它在框架里的位置:core 的上面,套了一层
整个仓库是 Maven 多模块:agentscope-core(推理循环、消息、模型、权限、状态)、agentscope-harness(本篇,270 个 Java 文件)、agentscope-extensions(各家模型接入)、agentscope-service(控制面)。依赖关系单向:harness 依赖 core,反过来不行。
harness 模块内部的包结构,按职责分得很清楚:
agent/ | |
agent/middleware/ | 全部 19 个中间件 |
agent/filesystem/ | |
agent/subagent/ | |
agent/skill/ | |
agent/memory/ | |
agent/gateway/agent/channel/ | |
agent/sandbox/ |
注意第一行和第二行的关系:HarnessAgent.java 本体 2,990 行,而"能力"散在 middleware 包的 19 个类里。这个比例本身就是答案的一半。
类注释还给了线程安全契约(HarnessAgent.java 约 166-171 行):HarnessAgent 在调用之间无状态,可以单例并发服务多用户;每次 call() 用 RuntimeContext 的 (userId, sessionId) 隔离状态;同一会话的调用自动排队,不同会话并行。分布式故事(多副本、跨节点恢复)就是从这个契约长出来的。
三、为什么这样设计:包装 + 中间件 + 文件
三个设计决定值得停下来想。
决定一:包装,不继承。 HarnessAgent 实现 Agent 接口,内部持有一个 ReActAgent delegate,call() / stream() / streamEvents() 全部转发。对比另一种做法——写一个继承 ReActAgent 的超级类,把记忆、压缩、子 Agent 全塞进子类——包装的好处是 core 完全不知道 harness 的存在:推理循环不用为任何"全家桶能力"留钩子位,钩子位就是已有的中间件机制本身。Python 版系列第 1 篇的结论(单 Agent 类 + 洋葱中间件)在 Java 侧换了个形式重演:那边是"一个类装天下",这边是"core 一个循环、harness 一摞洋葱"。
决定二:定义落成文件。 工作区(workspace)是磁盘上一棵目录树:AGENTS.md(人格与行为约定)、knowledge/KNOWLEDGE.md(领域知识)、skills/<名字>/SKILL.md(技能包)、subagents/(子 Agent 声明)、tools.json(MCP 服务器 + 工具白名单)。官方文档明确说:工作区文件与 builder API 完全等价——同一个定义,写成文件或写成代码效果相同。那为什么还要文件?文档的原话是「把'定义'表达成文件,正是让一个 agent 天然多租户的关键」:同一套 agent 代码,给不同用户放不同的覆盖目录,就是不同的 Agent,不需要代码分支、不需要多套部署。文件还天然可版本化——设计博客把「代码版本与工作区资产版本一起发布、一起回滚」列为交付纪律。
决定三:两个存储,两套生命周期。 这是最容易踩混的边界。工作区存持久的文件产物:永不压缩的会话日志(agents/)、计划文件(plans/)、任务记录、MEMORY.md 和按天的记忆文件。AgentState 存易失的在途上下文(对话缓冲、滚动摘要、权限/工具/任务/Plan-Mode 子状态,以及指向工作区产物的元数据),序列化进独立的 AgentStateStore,默认落在 ~/.agentscope/state/,从不进工作区树。前者是"Agent 学到了什么",后者是"Agent 说到哪了"——混在一起,恢复和演进就互相打架。
还有一层隐性设计:会话分桶。文件系统按 IsolationScope(默认 USER)切命名空间,子 Agent 的持久会话 ID 按「声明名@父会话#用户」派生(HarnessAgentBuilderSupport.java:678-692),同一套机制对本地、Redis、内存后端统一生效——这是"单机 demo 到分布式集群不换代码"的底座。
四、跟我读源码:三个文件,一个方法
按这个顺序读,效率最高:
docs/v2/zh/docs/harness/workspace.md(先读文档不是偷懒)——工作区三类文件、AgentState 边界、多租户分桶,全在一页。HarnessAgent.java的build()(2358-2988 行)——全篇的心脏。别从头读 2,990 行,直接跳 build(),看它怎么装配。HarnessAgentBuilderSupport.java(928 行)——子 Agent 工厂和 Skill 仓库合成的细节在这里。
build() 的装配顺序(每行都能在源码指认,收藏备用):
@路径 | @文件 引用展开成内容 | ||
.agentscope/bus) | |||
task/task_output 工具;优先动态版(2634-2666) | |||
wait_async_results 工具 | |||
ToolFilter.apply 过白名单(2737-2955) |
装配完,inner.build() 造出 ReActAgent,HarnessAgent 拿着引用和一堆管理器(工作区、计划模式、Skill 晋升器……)完成包装(2964-2988)。
两张按需查阅的配置表(第一次读可跳过):
workspace | ||
sandboxFilesystemSpecremoteFilesystemSpec / localFilesystemSpec | ||
stateStoredistributedStore | JsonFileAgentStateStore | |
compaction(CompactionConfig) | triggerMessages(30).keepMessages(10) | |
memory(MemoryConfig) | ||
enablePlanMode()planFileDirectory / allowShellInPlanMode | ||
skillManageToolEnabledpromotionGate | ||
asLeafSubagent() |
AGENTS.mdknowledge/、skills/、subagents/、tools.json | ||
agents/tasks/、plans/ | ||
MEMORY.mdmemory/YYYY-MM-DD.md |
五、一次调用怎么跑起来
拿设计博客里的最小示例当线索(代码引自 docs/v2/zh/blogs/how-to-build-agent-harness/01-patterns.md:122-133):
一条用户消息进来:call() 带上 (userId, sessionId) 进 HarnessAgent → 转发给内部的 ReActAgent → 推理循环每走一步,中间件栈从外到里过一遍。具体到一轮推理:
工作区上下文中间件把 AGENTS.md、MEMORY.md(长期记忆每轮注入)、KNOWLEDGE.md 拼进 system prompt——这就是"Agent 每轮醒来都记得自己是谁"的机制; 模型决定调工具。若处于 Plan Mode, PlanModeMiddleware先查这个工具的isReadOnly():只读放行,写操作拦下(除非plan_exit已过 ASK 审批);工具执行。文件工具走三层文件系统抽象(本地双层/沙箱/远端);超大输出被大结果卸载写到磁盘,上下文里只留预览和读指针; 消息历史超过阈值,压缩中间件做摘要;对话原文已被会话日志中间件完整落盘(所以敢压); 对话里的事实被记忆落盘抽进 MEMORY.md,后台任务定期整理; 主 Agent 若把活儿派给子 Agent: task工具触发子 Agent 工厂——工厂用继承自父的约 30 项配置(模型、工具箱副本、权限、压缩、状态存储……HarnessAgentBuilderSupport.java:311-429的 captured 系列变量就是这份清单)新造一个 HarnessAgent,系统提示词尾部拼上那段"叶子工人守则";子 Agent 的会话按「声明名@父会话#用户」分桶持久化,副本漂移后能按桶找回;全程事件从 streamEvents()流出(31 种类型化事件),前端和日志靠它知道 Agent 在干什么。
子 Agent 那段守则值得抄一句原文(HarnessAgentBuilderSupport.java:87-115):「You are NOT the main agent」「NO spawning further subagents — you are a leaf worker」「Be ephemeral — You may be terminated after task completion. That's fine.」——纪律写在提示词里,但兜底在机制里:asLeafSubagent() 会在 build 层直接跳过子 Agent 中间件的装配(HarnessAgent.java:2635 的 !leafSubagent 条件),两层防线。
六、调试从哪下手
三个断点,由浅入深:
HarnessAgent.java:2957(log.info("HarnessAgent '{}' built ..."))——build 完成点。看日志里的 workspace、filesystem 实现类、subagents 布尔值,确认装配结果和你想的一致;HarnessAgent.java:2520-2757的中间件装配段——在你在意的那行inner.middleware(...)前后下断点,观察配置如何变成中间件实例(比如压缩阈值、记忆触发条件);HarnessAgentBuilderSupport.java:362(general-purpose 子 Agent 工厂 return 处)——观察一次派生:进来的 builder 快照、出去的子 Agent 配置。
日常排查优先看两棵树:工作区目录(会话日志、plans、MEMORY.md 都是明文)和 ~/.agentscope/state/(AgentState JSON)。
七、想扩展,从哪里下手
✅ 应该改:写自己的中间件。任何一个能力都没锁死——实现 MiddlewareBase,builder.middleware(...)插进栈,和官方 19 个中间件同权。预算控制、自定义审计、业务风控都是这么加的。✅ 应该改:用文件定义子 Agent 和 Skill。 subagents/写声明(名字、描述、工具白名单、独立/共享工作区),.md skills/放技能包——不动代码就能调整 Agent 编制。⚠️ 不应该改:装配顺序和"三选一"约束。中间件先后有语义(会话日志先于压缩、沙箱先于一切),别手工重排;三种文件系统 spec 互斥是刻意的,别绕过校验硬塞两个。 🚫 千万不要改:叶子纪律和晋升门禁。把 SUBAGENT_CONTEXT_SECTION 里的「NO spawning further subagents」删掉,配合放开装配条件,就是子 Agent 生子 Agent 的递归风暴;把 RejectAllGate换成自动晋升,等于让未审计的 Agent 生成代码进入生产技能库——两个都是"看起来无害、出事没有刹车"的改动。
八、最值得学的设计
回到开头的问题:怎么把一堆工程能力"装进"一个 Agent?HarnessAgent 的答案有三层,每层都可迁移。
第一层:能力即中间件,不动引擎。 记忆、压缩、沙箱、子 Agent、Plan Mode,全部是推理循环外圈的洋葱层,core 对它们零感知。收益是正交:关掉任何一个能力 = build 时不装那个中间件,不碰别的。你自己的系统要加横向能力(日志、限流、审计)时,先问"这能不能是一层中间件",多数时候能。
第二层:定义即文件,进化即写回。 Agent 的人格、知识、技能、编制是一棵目录树,与代码解耦、可版本化、天然多租户;而 Agent 运行中学到的东西(记忆、计划、自学习技能)由框架自动写回同一棵树。"一个目录拷走就是一个完整 Agent"——部署单元和版本单元在这里合一。
第三层:纪律三层设防。 危险行为(递归派生、写操作、技能晋升)不是只靠提示词约束,也不是只靠代码拦截,而是提示词(叶子守则)+ 装配条件(leafSubagent)+ 运行时门卫(isReadOnly 白名单、ASK 审批、RejectAllGate)三层叠加。提示词会失效,机制不会——但只上机制会把 Agent 管死,提示词层保住了灵活性。这个配比值得抄。
要说局限:build() 630 行的装配方法、子 Agent 工厂里 30 个 captured 变量,说明"用 builder 组合一切"的代价是组合复杂度本身——这也是为什么配置守卫(三选一、分布式硬检查)要前置:组合空间太大,错误配置必须尽早爆炸。
九、阅读顺序建议
时间紧就三步:workspace.md 文档(30 分钟建立全景)→ HarnessAgent.build()(2358 行起,对着第四节的表读)→ HarnessAgentBuilderSupport.java 的子 Agent 段(87-130 提示词、311-429 工厂、678-733 分桶与工作区)。有余力再看 middleware 包里挑感兴趣的读——PlanModeMiddleware 和 CompactionMiddleware 是两个写得干净的样本。agentscope-service 模块(控制面)不在本篇范围,留给后面的篇目。
十、读完这篇,你应该能回答
为什么 HarnessAgent 不是新引擎(它是 ReActAgent 的包装);它怎么打包能力(一摞中间件 + 一棵文件树);一次调用怎么穿过中间件栈;想改该改哪(自己的中间件、文件定义)、不该改哪(顺序、叶子纪律、晋升门禁)。带走一句话:给系统加"全家桶",先分清哪层是洋葱、哪层是文件,别去改核心循环。
互动两问:
低门槛:你做 Agent 应用时,「上下文越聊越长」和「工具权限不敢放开」哪个更让你头疼?评论区说一个就行。 进阶(可选):Skill 自学习闭环默认用 RejectAllGate 把门关死——你觉得这个默认是太保守还是刚刚好?如果放开自动晋升,你会加什么条件?
追更:系列持续更新,下一篇拆 agentscope-core 的 ReActAgent 推理循环(Java 侧的"想-做-答"到底怎么转)。
裂变:转发到 Java 技术群或朋友圈,帮同样在看 AgentScope / Agent 框架源码的朋友省时间。