夜雨聆风学习资料网

ARTICLE · 1107264

AgentScope Java 源码解析系列第 1 篇 HarnessAgent|"全家桶"不是新引擎,是一摞中间件和一棵会进化的工作区

AgentScope Java 源码解析系列第 1 篇 HarnessAgent|"全家桶"不是新引擎,是一摞中间件和一棵会进化的工作区

AgentScope 2.0 源码解析系列 · Java 支线 · 第 1 篇

本文是 AgentScope Java 源码解析系列 第 1 篇(新支线开篇,模块:HarnessAgent)。

它接续两条线:AgentScope 2.0(Python)源码解析系列 13 篇——那边的结论是「全框架只有 1 个 Agent 类,定制全靠中间件」,本篇验证这件事在 Java 侧以更大规模重演;选型番外第 6/7 篇——那边说 HarnessAgent「把工作区、长期记忆、压缩、子 Agent、沙箱、Plan Mode 打包成一个入口」,本篇拆开看这个包到底怎么打的。

关注追更,Java 支线会沿着 ReActAgent → 事件流 → 分布式状态往下走。

📌 TL;DR
  • HarnessAgent 不是新的 Agent 引擎。类注释写得很直白:它是「wraps a ReActAgent with 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/
HarnessAgent 本体 + Builder 装配支持
agent/middleware/全部 19 个中间件
(能力层的真正实现)
agent/filesystem/
文件系统抽象:本地双层叠加 / 沙箱 / 远端存储,及组合路由
agent/subagent/
子 Agent 声明加载、工厂、任务仓库
agent/skill/
四层 Skill 仓库 + 自学习(curator/promoter/审计)
agent/memory/
记忆配置、落盘管理、整理器、上下文压缩
agent/gateway/
、agent/channel/
会话网关、多 Agent 路由、IM 接入
agent/sandbox/
 等
沙箱生命周期、工作区管理、消息总线、周期门

注意第一行和第二行的关系:HarnessAgent.java 本体 2,990 行,而"能力"散在 middleware 包的 19 个类里。这个比例本身就是答案的一半。

类注释还给了线程安全契约(HarnessAgent.java 约 166-171 行):HarnessAgent 在调用之间无状态,可以单例并发服务多用户;每次 call() 用 RuntimeContext 的 (userId, sessionId) 隔离状态;同一会话的调用自动排队,不同会话并行。分布式故事(多副本、跨节点恢复)就是从这个契约长出来的。

     
图 1|HarnessAgent 全景:包装层 + 中间件栈(①-⑨)+ core 推理循环 + 会进化的工作区(源码行号可对照)

三、为什么这样设计:包装 + 中间件 + 文件

三个设计决定值得停下来想。

决定一:包装,不继承。 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/.md(子 Agent 声明)、tools.json(MCP 服务器 + 工具白名单)。官方文档明确说:工作区文件与 builder API 完全等价——同一个定义,写成文件或写成代码效果相同。那为什么还要文件?文档的原话是「把'定义'表达成文件,正是让一个 agent 天然多租户的关键」:同一套 agent 代码,给不同用户放不同的覆盖目录,就是不同的 Agent,不需要代码分支、不需要多套部署。文件还天然可版本化——设计博客把「代码版本与工作区资产版本一起发布、一起回滚」列为交付纪律。

决定三:两个存储,两套生命周期。 这是最容易踩混的边界。工作区存持久的文件产物:永不压缩的会话日志(agents//sessions/)、计划文件(plans/)、任务记录、MEMORY.md 和按天的记忆文件。AgentState 存易失的在途上下文(对话缓冲、滚动摘要、权限/工具/任务/Plan-Mode 子状态,以及指向工作区产物的元数据),序列化进独立的 AgentStateStore,默认落在 ~/.agentscope/state//,从不进工作区树。前者是"Agent 学到了什么",后者是"Agent 说到哪了"——混在一起,恢复和演进就互相打架。

还有一层隐性设计:会话分桶。文件系统按 IsolationScope(默认 USER)切命名空间,子 Agent 的持久会话 ID 按「声明名@父会话#用户」派生(HarnessAgentBuilderSupport.java:678-692),同一套机制对本地、Redis、内存后端统一生效——这是"单机 demo 到分布式集群不换代码"的底座。

四、跟我读源码:三个文件,一个方法

按这个顺序读,效率最高:

  1. docs/v2/zh/docs/harness/workspace.md(先读文档不是偷懒)——工作区三类文件、AgentState 边界、多租户分桶,全在一页。
  2. HarnessAgent.java 的 build()(2358-2988 行)——全篇的心脏。别从头读 2,990 行,直接跳 build(),看它怎么装配。
  3. HarnessAgentBuilderSupport.java(928 行)——子 Agent 工厂和 Skill 仓库合成的细节在这里。

build() 的装配顺序(每行都能在源码指认,收藏备用):

序号
装配步骤
启用条件
干什么
1
校验
总是
三种文件系统 spec 至多配一个,配多了抛异常(2364-2377)
2
DistributedStore 自动接线
配了 distributedStore
状态存储、快照、执行守卫、消息总线缺什么补什么(2383-2409)
3
沙箱生命周期中间件
配了 SandboxFilesystemSpec
沙箱的创建/恢复/回收(2448-2483)
4
调用追踪中间件
agentTracingLogEnabled
每次 Agent 调用记一条
5
工作区上下文中间件
默认开
把 AGENTS.md/MEMORY.md/KNOWLEDGE.md 注入 system prompt(2529-2541)
6
@路径
展开
默认开
提示词里的 @文件 引用展开成内容
7
会话日志中间件
默认开(注释原话「independent of memory hooks — always persist」)
完整对话永不压缩落盘(2546-2563)
8
记忆落盘 + 记忆整理
配了 model 且没禁记忆钩子
对话事实抽进 MEMORY.md;后台整理压缩(2564-2601)
9
上下文压缩
配了 CompactionConfig
消息超阈值时摘要压缩
10
大结果卸载
配了 ToolResultEvictionConfig
超大工具输出写磁盘,上下文留 head/tail + 读指针
11
收件箱
消息总线存在(默认工作区实现,.agentscope/bus)
跨 Agent 消息、异步工具回执
12
子 Agent
非叶子、未禁用、有模型
注册 task/task_output 工具;优先动态版(2634-2666)
13
异步工具
消息总线存在
等异步结果的 wait_async_results 工具
14
工具注册
各自条件
记忆搜索/读取/保存、会话搜索、文件系统、产物交付、shell(仅沙箱)、Web(2689-2735)
15
Plan Mode + Skill + 白名单
各自条件
三个 plan 工具 + 只读门卫;四层 Skill 仓库 + 自学习;最后 ToolFilter.apply 过白名单(2737-2955)

装配完,inner.build() 造出 ReActAgent,HarnessAgent 拿着引用和一堆管理器(工作区、计划模式、Skill 晋升器……)完成包装(2964-2988)。

两张按需查阅的配置表(第一次读可跳过):

Builder 能力开关(按需查阅)——挑影响行为的
字段
默认
作用
workspace
解析到默认目录
工作区根;AGENTS.md/skills/subagents 都从这里找
sandboxFilesystemSpec
 / remoteFilesystemSpec / localFilesystemSpec
都不配走本地双层
三选一,互斥;本地双层 = 项目目录垫底、工作区盖上面(Claude-Code 同款模型)
stateStore
 / distributedStore
JsonFileAgentStateStore
(按 agentId 分目录)
AgentState 存哪;分布式部署必须换
compaction(CompactionConfig)
不开
官方示例:triggerMessages(30).keepMessages(10)
memory(MemoryConfig)
model 继承主模型
flushPrompt / consolidationPrompt 可换,含保留天数
enablePlanMode()
 / planFileDirectory / allowShellInPlanMode
关
Plan Mode 三件套
skillManageToolEnabled
 + promotionGate
关
Skill 自学习闭环;门禁默认 RejectAllGate
asLeafSubagent()
否
被当子 Agent 造时标记叶子,禁再派生
工作区文件三类(按需查阅)
类型
谁写
例子
静态资产
你 / 团队
AGENTS.md
、knowledge/、skills/、subagents/、tools.json
运行时文件
框架每次 call 写回
agents//sessions/
、tasks/、plans/
长期记忆
Agent + 后台任务
MEMORY.md
、memory/YYYY-MM-DD.md

五、一次调用怎么跑起来

拿设计博客里的最小示例当线索(代码引自 docs/v2/zh/blogs/how-to-build-agent-harness/01-patterns.md:122-133):

HarnessAgent agent = HarnessAgent.builder()    .name("remediation-agent")    .model(model)    .workspace(Paths.get(".agentscope/workspace"))    .build();agent.call(message, RuntimeContext.builder()    .userId("u-1842")    .sessionId("remediation-2026-0917")    .build()).block();

一条用户消息进来:call() 带上 (userId, sessionId) 进 HarnessAgent → 转发给内部的 ReActAgent → 推理循环每走一步,中间件栈从外到里过一遍。具体到一轮推理:

  1. 工作区上下文中间件把 AGENTS.md、MEMORY.md(长期记忆每轮注入)、KNOWLEDGE.md 拼进 system prompt——这就是"Agent 每轮醒来都记得自己是谁"的机制;
  2. 模型决定调工具。若处于 Plan Mode,PlanModeMiddleware 先查这个工具的 isReadOnly():只读放行,写操作拦下(除非 plan_exit 已过 ASK 审批);
  3. 工具执行。文件工具走三层文件系统抽象(本地双层/沙箱/远端);超大输出被大结果卸载写到磁盘,上下文里只留预览和读指针;
  4. 消息历史超过阈值,压缩中间件做摘要;对话原文已被会话日志中间件完整落盘(所以敢压);
  5. 对话里的事实被记忆落盘抽进 MEMORY.md,后台任务定期整理;
  6. 主 Agent 若把活儿派给子 Agent:task 工具触发子 Agent 工厂——工厂用继承自父的约 30 项配置(模型、工具箱副本、权限、压缩、状态存储……HarnessAgentBuilderSupport.java:311-429 的 captured 系列变量就是这份清单)新造一个 HarnessAgent,系统提示词尾部拼上那段"叶子工人守则";子 Agent 的会话按「声明名@父会话#用户」分桶持久化,副本漂移后能按桶找回;
  7. 全程事件从 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 条件),两层防线。

六、调试从哪下手

三个断点,由浅入深:

  1. HarnessAgent.java:2957(log.info("HarnessAgent '{}' built ..."))——build 完成点。看日志里的 workspace、filesystem 实现类、subagents 布尔值,确认装配结果和你想的一致;
  2. HarnessAgent.java:2520-2757 的中间件装配段——在你在意的那行 inner.middleware(...) 前后下断点,观察配置如何变成中间件实例(比如压缩阈值、记忆触发条件);
  3. 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 #Agent #智能体

追更:系列持续更新,下一篇拆 agentscope-core 的 ReActAgent 推理循环(Java 侧的"想-做-答"到底怎么转)。

裂变:转发到 Java 技术群或朋友圈,帮同样在看 AgentScope / Agent 框架源码的朋友省时间。

AgentScope 2.0 源码解析系列 · Java 支线 · 源码 18

相关学习资料