
上一篇把事件流收了口EventCodec 把拆散的框架细事件攒齐SSE 主路径从 HTTP 请求一路通到六种对外事件。但收口的只是消息怎么流还有三件事悬在半空——会话状态活在内存里进程重启即丢多租户请求共用同一段上下文错误与耗时只有零散的日志可看。最小内核解决了Agent 能不能跑这三件事决定Agent 能不能长期跑在一个被多个用户同时访问的进程里。本篇进入 AgentScope 的 harness 层。harness 直译是挽具——套在马身上、让力气有处使的那套装备框架语境里它就是把 ReAct 内核套进工程边界的这一层。主角从 ReActAgent 换成 HarnessAgent围绕它展开三个共享对象、状态会话与多租户、Redis 分布式存储与 Compaction 记忆压缩、Middleware 横切层最后用 dream-scope 的 ScopeChatAgent 装配实录收口回答主路径为什么是 HarnessAgent。前置知识已读系列 01理解 ReAct 循环、Msg、Model 与六模块边界Java 21 与 Spring Boot 基础。阅读目标理解 HarnessAgent 与 ReActAgent 的分工掌握 Workspace 目录约定、基于 RuntimeContext 的多租户状态会话能独立完成 Redis 分布式存储与 Compaction 配置并说清 Middleware 的钩子模型。关于代码文中 Java 片段均为从仓库摘出的关键片段完整代码见各处标注的仓库路径版本以 dream-scope 0.1.0-SNAPSHOT 与 AgentScope 2.0.3 源码为准。一、HarnessAgent 与 ReActAgent薄包装厚工程先把分工说清。ReActAgent 是 core 层的直接实现职责单一驱动模型推理—工具行动—结果回填的循环直到最终答复或迭代上限。它不知道 HTTP、不知道持久化、也不知道一个请求背后是哪个用户。HarnessAgent 是 ReActAgent 的一层薄包装。薄指它不改写 ReAct 循环本身——推理还是那个推理工具执行还是那个执行厚指它把长期运行所需的工程能力以 builder 挂点的形式叠加在循环外围toolkit / generateOptions / fallbackModel工具包、采样参数、备用模型compaction / memory记忆压缩与会话记忆配置subagents子 Agent 声明细节留给后续篇目middlewares横切中间件第五节展开distributedStore / stateStore分布式状态存储第三、四节展开workspace / skillRepository工作区目录与技能仓库enablePlanMode计划模式开关。这组挂点对应一条设计哲学core 算法不动Harness 只往里加东西。升级框架时ReAct 循环的行为可预期工程能力的增减只发生在 builder 链上不渗透进业务代码。更关键的架构认知是Harness 的各项能力互不依赖。compaction 不关心 workspacemiddleware 不关心 stateStore。它们只通过三个共享对象通信RuntimeContext承载本次调用的身份Workspace约定文件读写AgentStateStore / DistributedStore负责跨调用的状态恢复。三者的展开是第二节的主角下文逐节拆开。这里先记住结论理解了这三个对象就理解了 HarnessAgent 装配的全部衔接面。装配链路如下图builder 挂点收进 HarnessAgentHarnessAgent 内部持有 ReActAgent能力之间靠共享对象衔接在 01 的 boot 对照路径上官方 starter 装配的是 ReActAgent内存 Memory、空 Toolkitdream-scope 的 web 主路径装配的是 HarnessAgent。两条路径的差异不是谁更强而是工程边界的有无——01 第四节三个缺口中的进程内状态分水岭正在这条装配线上。二、三个共享对象身份、目录、状态2.1 RuntimeContext本次调用的身份RuntimeContext 承载一次调用的运行时维度最常用的是 userId 与 sessionId。两个要点**第一它不持久化。**每次请求创建一个新实例随调用结束而结束。持久化的状态交给第三节的 AgentStateStoreRuntimeContext 只负责这一次是谁在调。职责分离由此建立同一个 Agent 实例被所有请求共享用户维度全部由调用参数携带Agent 内部不存在任何请求态字段。**第二它在整条链路上透传。**从 streamHandle 构建到框架事件、中间件钩子第五节签名里的 ctx 参数、状态存储寻址用的都是同一个对象。01 第四节的 EventCodec 按请求 new 一个实例streamHandle 里构建的 RuntimeContext 同理Msg inbound MessageCodec.toUserMessage(request.input(), request.imageUrls()); RuntimeContext context RuntimeContext.builder() .userId(blankToNull(request.userId())) .sessionId(blankToNull(request.sessionId())) .build(); context.put(AgentSpawnTool.CTX_FORCE_SYNC, Boolean.TRUE);片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.javablankToNull 是私有工具方法null 或空白字符串统一转成 null。这半行代码是会话语义的开关第三节解释为什么必须它。context.put 的那行则演示了 RuntimeContext 的另一个角色调用侧附加参数的载体。子 Agent 强制同步执行这类只对本次调用生效的开关不走全局配置放进 context 随调用传递。2.2 Workspace把约定落成目录Workspace 是 Harness 的工作区目录把Agent 需要哪些静态资源变成文件系统约定。dream-scope 的目录结构如下路径用途AGENTS.md工作区说明供模型读取的行为约定MEMORY.md长期记忆文件knowledge/检索资料目录skills/*/SKILL.md技能定义Harness 自动扫描subagents/*.md子 Agent 的 Markdown 声明Harness 自动扫描tools.json工具描述目录约定替代了口头约定想要新增一个技能放一个skills/name/SKILL.md即可不需要改装配代码想加一个文件声明的子 Agent往subagents/放 Markdown。Harness 在构建时扫描这些目录与 builder 上编程式注册的内容合并生效。物理位置由配置决定。web 模块application.yml里dream-scope.workspace-dir默认.agentscope/workspace启动时把 classpath 内置的workspace/目录拷贝过去同名覆盖。这样做的动机是部署友好仓库里带着一套内置的工作区作为默认值生产上用数据卷挂载真实路径即可替换Agent 的代码与资源解耦。需要注意边界workspace 管的是约定文件的读写会话状态不走它——那是 AgentStateStore 的事。两套机制如果混用会把每用户一份的状态写进全局一份的目录里。2.3 AgentStateStore 与 DistributedStore跨调用的状态前两个对象解决这次调用是谁和文件放哪里第三个共享对象解决上一轮聊到哪了。Memory对话记忆、Plan 状态这些组件会被序列化为 State 对象通过 AgentStateStore 接口落盘寻址键是(userId, sessionId)sessionId 必填且非空userId 可选null 表示匿名或单租户。DistributedStore 是更上层的聚合接口把所有需要分布式持久化的组件收进一处组件职责Redis 实现AgentStateStoreAgent 状态持久化RedisAgentStateStoreBaseStore工作区文件系统的 KV 抽象RedisStoreSandboxSnapshotSpec沙箱快照RedisSnapshotSpecSandboxExecutionGuard沙箱并发锁RedisSandboxExecutionGuard为什么偏偏是这四个它们各自对应一类必须跨进程共享的资源AgentStateStore解决会话状态放哪——对话记忆、计划状态的读写第三节的主角BaseStore解决工作区文件放哪——多副本部署时workspace 里的知识库与技能文件不能只躺在单机磁盘上SandboxSnapshotSpec解决沙箱环境如何复刻——快照定义共享后任意节点都能还原同一个沙箱环境SandboxExecutionGuard解决沙箱能否并发执行——用分布式锁防止多个节点同时抢占同一个沙箱。一行配置即可把四个组件全部切到同一个分布式后端。dream-scope 选择了这条路径但不是无脑全上——先看第三节的状态会话再回来看它值不值。三、状态会话与多租户从内存裸奔到 Redis 槽位3.1 五个实现一条优先级规则AgentScope 为 AgentStateStore 提供五种实现按部署形态选择实现适用场景InMemoryAgentStateStore单元测试JsonFileAgentStateStore单节点开发HarnessAgent 默认RedisAgentStateStore多副本生产默认MysqlAgentStateStore已有关系型数据库基础设施OssAgentStateStore阿里云生态从单机到多副本的过渡不需要换代码只需要换实现。框架还定义了一条装配优先级规则值得单独记住显式 builder 方法如 .stateStore(...) distributedStore 自动注入 本地默认。意思是builder 上显式指定的实现优先只配了 distributedStore 时其携带的各组件实现被自动注入什么都没配落回本地默认JsonFile。优先级从左到右逐级兜底这也意味着 distributedStore 提供一站式默认之后仍可对单个组件做精细覆盖。一次会话请求的存储寻址决策如下图3.2 ScopeChatAgent 的二选一dream-scope 在 ScopeChatAgent 构造器里把这条优先级规则用成了显式的二选一if (distributedStore ! null) { builder.distributedStore(distributedStore); } else if (stateStore ! null) { builder.stateStore(stateStore); }片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.java生产路径走 distributedStoreRedis单测路径注入 stateStore文件会话两者都空时框架用内存态进程重启会话丢失——类注释把这三行语义写得很直白。这比什么都配了再看谁生效更可控条件分支只出现在装配点一处不散落在运行时。3.3 会话槽位的准入规则有了存储还剩什么样的请求算会话。ScopeChatAgent 的规则一句话userId 与 sessionId 成对非空才写入 Redis 槽位只传一个等同于无会话。这与框架层默认不同2.3 节的规则是 sessionId 必填且非空、userId 可选null 表示匿名或单租户——dream-scope 作为应用层选择收严这道闸。这就是 2.1 节 blankToNull 存在的原因。前端只传 sessionId 不传 userId 时如果空白字符串原样进入 context框架可能拿一个空串键值写出一半槽位统一转成 null 后单边传值与没传语义合并会话干脆不建。这个决策换来了清晰的租户边界每个(userId, sessionId)对应一个独立状态槽位多租户隔离是存储层的天然结果不需要业务代码维护任何 Map。01 第五节提到续聊时在请求体中带上成对的 sessionId 与 userId会话即写入 Redis 同一槽位背后的机制就是本节内容。顺手澄清一个容易混淆的点多租户不等于多 Agent 实例。chat Agent 是单例所有租户共享同一个 HarnessAgent隔离发生在状态层而不是进程里堆 N 个 Agent。这也是 RuntimeContext 不持久化的原因——它只是寻址凭证不是存储本身。四、Redis 分布式存储与 Compaction会话活下来上下文瘦下去4.1 Redis从 ping 开始的 fail fast生产路径的 Redis 由 ChatRedis 打开static JedisPooled open(String uri) { JedisPooled jedis new JedisPooled(requireUri(uri)); try { jedis.ping(); log.info(redis ping ok {}, safeHost(uri)); } catch (RuntimeException ex) { jedis.close(); log.warn(redis ping failed {}, safeHost(uri)); throw new IllegalStateException(redis required but unreachable: uri, ex); } return jedis; }片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ChatRedis.javaping 失败直接抛异常进程起不来。这是 01 提过的 fail fast快速失败带着失效连接进入服务只会把错误推迟到第一个请求状态层是会话的命脉起一个注定写不进会话的服务没有意义。连接到手之后DistributedStore 的获取只有一行return RedisDistributedStore.fromJedis(jedis, prefix);片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ChatRedis.javafromJedis 一个静态工厂返回完整 DistributedStorestate、baseStore、snapshot、executionGuard 四组件全部指向同一个 Rediskey 前缀默认dream-scope:可通过配置调整。四个组件单独各配一套存储当然也可行但运维上是四份部署心智聚到一个后端出问题时只需要盯一个数据源。这是一站式默认 单点排障对自由组合的取舍多副本部署场景下前者更划算。4.2 Compaction按条数压缩的记忆多轮对话的另一面是上下文膨胀消息只增不减早晚触及模型窗口。AgentScope 的解法是 Compaction上下文压缩对话过长时把早期消息压缩成摘要腾出窗口空间通过 CompactionConfig 配置static CompactionConfig compactionConfig(int triggerMessages, int keepMessages) { int trigger triggerMessages 0 ? triggerMessages : ChatHarnessOptions.DEFAULT_TRIGGER_MESSAGES; int keep keepMessages 0 ? keepMessages : ChatHarnessOptions.DEFAULT_KEEP_MESSAGES; return CompactionConfig.builder() .triggerMessages(trigger) .keepMessages(keep) .keepTokens(0) .build(); }片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.java三个参数的语义消息条数达到 triggerMessages 时触发压缩压缩后保留最近 keepMessages 条原文早期内容转为摘要。dream-scope 默认 30 / 10ChatHarnessOptions.DEFAULT_TRIGGER_MESSAGES / DEFAULT_KEEP_MESSAGES配置项dream-scope.compaction.*配错为 0 或负数时回落默认值不静默接受非法配置。keepTokens(0) 是一个值得停一下的决策CompactionConfig 同时支持按 token 数与按消息条数两种触发阈值把 token 阈值置 0 即显式关掉只留条数阈值。两个阈值同时生效时触发时机的归属会变得难以解释——到底是条数到了还是 token 到了只留一个行为可预期。参数宁少勿杂调试时同样适用。压缩与第四节的 Redis 是配套关系压缩发生在推理循环内压缩结果随 State 一起持久化。进程重启后恢复的会话是压缩过的会话而不是丢失的会话。五、Middleware推理链路上的横切层5.1 接口而非抽象类2.0.3 的一个变化MiddlewareBase中间件基类框架在推理链路上预留的横切挂点概念类似 Web 开发里的 AOP——面向切面编程把日志、鉴权这类与业务无关的逻辑从业务代码里拆出来统一挂载在 2.0.3 中是接口不是抽象类。这是从 1.x 迁移时容易踩的坑旧版本是抽象类升级后extends会直接编译失败。接口形态带来的是 default 方法的自由一个中间件只实现它关心的钩子其余钩子默认直通。dream-scope 自带的 LoggingMiddleware 就是典型只实现 5 个钩子中的 3 个。5.2 五个钩子两种类型五个钩子按行为分两类。四个 Onion洋葱类型钩子共用统一签名FluxAgentEvent onXxx(Agent agent, RuntimeContext ctx, XxxInput input, FunctionXxxInput, FluxAgentEvent next)钩子签名形状示意agent 为当前 Agent 实例ctx 为本次调用的 RuntimeContext四个钩子的输入类型各不相同dream-scope 的 LoggingMiddleware 即按此形状实现 onAgent见 5.3 节实际代码。onAgent包裹一整轮回复流程覆盖所有 ReAct 轮次、工具执行与最终输出onReasoning包裹一次推理步骤输入组装 → 模型调用 → 流式解码onActing包裹单次工具调用执行onModelCall包裹一次原始模型 API 调用最贴近模型。一个 Transformer变换器类型钩子onSystemPrompt系统提示词组装时触发签名不同——输入String current返回MonoString前一输出即下一输入多个中间件按序串联。嵌套关系onAgent 在最外层内部每轮推理进入 onReasoningonReasoning 内先 onSystemPrompt 再 onModelCall工具调用时进入 onActing。一次带工具调用的请求中钩子像洋葱一样层层包裹洋葱模型的核心是next参数调用next.apply(input)才会继续执行链上的下一环。不调用 next 就是短路——这是中间件拥有拦截能力的原因。设想一个权限中间件在 onActing 里检查工具名不满足条件直接返回拒绝事件的 Flux工具执行根本不会发生。5.3 LoggingMiddleware观察者的写法LoggingMiddleware 在三个钩子上打点。以 onAgent 为例洋葱模型的标准写法Override public FluxAgentEvent onAgent( Agent agent, RuntimeContext ctx, AgentInput input, FunctionAgentInput, FluxAgentEvent next) { String agentId agentId(agent); String sessionId sessionId(ctx); long start System.nanoTime(); log.info(chat middleware onAgent start agentId{} sessionId{}, agentId, sessionId); return next.apply(input) .doOnComplete(() - log.info( chat middleware onAgent complete agentId{} sessionId{} elapsedMs{}, agentId, sessionId, elapsedMs(start))) .doOnError(error - log.warn( chat middleware onAgent error agentId{} sessionId{} elapsedMs{} message{}, agentId, sessionId, elapsedMs(start), error null ? : error.getMessage())); }片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/middleware/LoggingMiddleware.java几个细节各有讲究doOnComplete / doOnError 是旁路操作挂回调、不改事件流中间件因此是透明的日志、埋点类中间件都该遵守这条边界。错误日志走 warn错误分类由上层 mapStreamError 收口成 AgentTimeoutException / AgentProviderException 两种领域异常中间件只记录现象不再用 error 级别与上层的错误处理抢话语权避免同一个错误在日志系统里出现两份记录。onActing 只记工具名不记参数与输出工具参数可能包含 API Key、用户隐私无差别写日志是审计需求越界变成泄露风险。字段取舍在写中间件时就要想清楚。三个钩子的日志字段各有一个增量onAgent 记 agentId 与 sessionIdonModelCall 多记 model区分主模型与 fallbackonActing 多记 tools逗号拼接的工具名。当前未实现 onReasoning 与 onSystemPrompt 两个钩子——意味着无法区分模型调用耗时与推理步骤总耗时这是后续可以补的观察点。5.4 ChatOtel全局 Tracer 的双检锁懒初始化可观测性的另一条路是 OpenTelemetry开源的可观测性标准产出结构化 trace 数据供 Jaeger 等 APM 后端消费。框架内置的 OtelTracingMiddleware 读取全局 Tracer 产生 span链路中的一个节点如一次模型调用而全局 Tracer 的注册由 dream-scope 自己的 ChatOtel 负责public static void install() { if (installed || !endpointConfigured()) { return; } synchronized (LOCK) { if (installed || !endpointConfigured()) { return; } if (blank(System.getProperty(otel.service.name)) blank(System.getenv(OTEL_SERVICE_NAME))) { System.setProperty(otel.service.name, dream-scope); } AutoConfiguredOpenTelemetrySdk.initialize(); installed true; log.info(OpenTelemetry SDK registered (OTLP {}), otlpEndpoint()); } }片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/middleware/ChatOtel.java三层防线双检锁锁外先查一次锁内再查一次多线程并发 install 时 SDK 只初始化一次。installed 是普通 boolean——锁内写、锁内读自洽但锁外快速路径读非 volatile 变量严格按 Java 内存模型存在可见性风险最坏情况只是错过快速路径、进锁再查一次工程上可接受。要更稳妥可声明为 volatile 或 AtomicBoolean。懒初始化只有配置了OTEL_EXPORTER_OTLP_ENDPOINT环境变量或同名系统属性才真正初始化否则保持 no-op空操作。本地开发不配 endpoint零开销上生产配一个环境变量链路数据自动导出。幂等installed 标志保证重复调用无害——生产装配与单测路径都可能触发 install第二次调用直接返回。service name 也有兜底OTEL_SERVICE_NAME 与 otel.service.name 都没配时写入 dream-scope避免链路在 APM 后端里以 unknown service 出现。5.5 权限取舍的真实决策把 01 的安全设计与本节的中间件能力放在一起能看到 dream-scope 在权限上的两层取舍装配期消灭disableFilesystemTools()与disableShellTool()在 builder 链上直接砍掉工作区文件工具与 shellHTTP 进程从能力清单上就不存在这些危险项运行期拦截onActing 短路是框架预留的通用拦截路径适合按调用方身份动态判断的场景。dream-scope 主路径选择了前者。理由chat 面向公网 HTTP文件与 shell 的危险性与调用方无关任何身份都不该碰——静态装配一刀切最简单也不依赖运行时判断的正确性。动态拦截留给真正需要按身份区分权限的场景多租户差异化授权。两层不是替代关系装配期收紧攻击面运行期拦截做细粒度控制纵深防御里各占一层。六、收口主路径为什么是 HarnessAgent——ScopeChatAgent 装配实录6.1 16 个参数与两个入口前面每节都引用了 ScopeChatAgent 的局部现在看全貌。它的私有构造器有 16 个参数private ScopeChatAgent( Model model, Duration callTimeout, AgentStateStore stateStore, Path workspace, int compactionTriggerMessages, int compactionKeepMessages, GenerateOptions generateOptions, Model fallbackModel, DistributedStore distributedStore, AutoCloseable redisClient, boolean planModeEnabled, String planDirectory, RetrievePort retrievePort, ListMcpClientWrapper mcpClients, String sysPromptOverride, ChatNacosClient nacos) {片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.java16 个参数不是平铺的按职责分五组分组参数职责模型/超时model, callTimeout主模型与调用超时存储stateStore, distributedStore会话状态的两条持久化路径第三节工作区/压缩workspace, compactionTriggerMessages, compactionKeepMessages, planModeEnabled, planDirectory工作区目录、压缩阈值、计划模式落盘可选扩展generateOptions, fallbackModel, retrievePort, mcpClients, sysPromptOverride采样覆盖、备用模型、RAG、MCP、部署方提示词增量外部客户端redisClient, nacosRedis 连接与 Nacos 客户端由 create 工厂注入第一观感是该用 Builder 了。实际的取舍是两个入口 一处汇聚公开构造器最多 6 个参数model、callTimeout、stateStore、workspace 与压缩两项服务单测——临时 workspace、注入文件 store、不连 Redis生产 create 工厂组装全部 16 个。所有依赖集中在一个私有构造器里builder 模式反而要多出两百行转发代码。类是无 Spring 的普通类web 的 PortsConfig 通过 create 注入一切——01 说的框架类型的集中点指的就是这里。6.2 装配顺序的三条约束私有构造器的开头几十行藏着三条顺序约束约束一ChatOtel.install() 必须最先。ChatOtel.install(); this.callTimeout normalizeTimeout(callTimeout);片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.java全局 Tracer 注册晚于任何 span 产生那段链路就永久丢失。install 放在构造器第一行后续装配过程中产生的任何 span 都有处可去。约束二提示词三层拼接顺序固定。String base SYS_PROMPT; if (sysPromptOverride ! null !sysPromptOverride.isBlank()) { base SYS_PROMPT \n sysPromptOverride.trim(); } String prompt planModeEnabled ? base PLAN_PROMPT : base;片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.java拼接顺序基础系统提示 → 配置覆盖 → Plan Mode 附加。三层各有分工SYS_PROMPT仓库内置的基线包含工具路由与子 Agent 调度规则sysPromptOverride部署方注入的增量PLAN_PROMPT只在计划模式开启时追加。注意 Nacos 的动态提示词不走这条拼接——它作为 onSystemPrompt 中间件在每次推理时动态生效与 build 时固化的 sysPrompt 是两条路径。动态提示词中间件的形态如下Override public MonoString onSystemPrompt(Agent agent, RuntimeContext ctx, String current) { String base current null ? : current; String loaded nacos.sysPrompt(null); if (loaded null || loaded.isBlank()) { return Mono.just(base); } return Mono.just(base \n loaded); }片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/nacos/ChatNacosPromptMiddleware.java节选完整实现含防重与空值分支启动时静态拼接 运行时 Transformer 追加两种提示词更新路径各自独立互不干扰。约束三Nacos 技能仓库在 builder 链之后条件设置。HarnessAgent.Builder builder HarnessAgent.builder() .name(AgentIds.CHAT) .sysPrompt(prompt) .model(Objects.requireNonNull(model, model)) .toolkit(chatToolkit(retrievePort, this.mcpClients)) .compaction(compactionConfig(compactionTriggerMessages, compactionKeepMessages)) .memory(MemoryConfig.defaults()) .subagents(chatSubagents()) .middlewares(chatMiddlewares(extras)) .disableFilesystemTools() .disableShellTool(); if (nacosSkills ! null) { builder.skillRepository(nacosSkills); } if (generateOptions ! null) { builder.generateOptions(generateOptions); } if (fallbackModel ! null) { builder.fallbackModel(fallbackModel); } if (distributedStore ! null) { builder.distributedStore(distributedStore); } else if (stateStore ! null) { builder.stateStore(stateStore); } boolean hasWorkspace workspace ! null !workspace.toString().isBlank(); if (hasWorkspace) { builder.workspace(workspace); } if (planModeEnabled hasWorkspace) { builder.enablePlanMode() .planFileDirectory(planDirectory null || planDirectory.isBlank() ? plans : planDirectory); } this.agent builder.build();片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.java主链只放必选项条件项全部收在链后用 if 守卫Nacos 技能仓库、采样参数、备用模型、存储、工作区、计划模式每个挂点一个条件一行对应一个工程决策。这个写法让 builder 链本身成为一张必装清单而条件分支成为选装清单装配审查时一眼可分。Plan Mode 依赖 workspace 落盘计划文件写进plans/目录没有工作区就不开这是挂点之间的隐式依赖被显式写出的例子。6.3 双入口的双路径ScopeChatAgent 对外实现 domain 的 StreamingAgentHandlerstreamHandle 是唯一推理入口按请求是否要求结构化输出分叉普通路径订阅agent.streamEvents(...)细事件经 EventCodec 转六种对外事件结构化路径走agent.call(messages, schema, context)因为 2.0.3 的 streamEvents 没有 schema 重载完整输出后编成一条 Done 事件。01 第四节的 EventCodec 细节在此不重复两路径最终汇入同一个 AgentStreamHandler 回调。handle同步入口不是另一条裸 call而是订阅 streamHandle、拼齐 Done 后返回等待上限是 callTimeout 加 5 秒收尾余量。同步与流式共用一条推理链这是 01 的结论也是装配层的直接结果。6.4 生命周期申请与释放互为镜像create 工厂里资源申请顺序被严格定义ChatRedis.openping 校验→ ChatMcp.openAllMCP模型连接外部工具与数据的开放协议建连→ resolveModel → new ScopeChatAgent触发 builder 装配。任一步失败已申请的资源按逆序关闭} catch (RuntimeException ex) { ChatMcp.closeQuietly(mcpClients); jedis.close(); throw ex; }片段出处dream-scope-adapter/src/main/java/com/zhu/scope/adapter/ScopeChatAgent.javaclose 方法镜像了申请顺序先 agent.close()再 MCP 连接最后 Jedis。build 失败不留半开连接进程退出不留悬挂资源。6.5 为什么主路径是 HarnessAgent现在可以完整回答标题问题。三个理由能力与循环解耦ReActAgent 提供循环HarnessAgent 提供边界。业务对循环的依赖被限制在调用这个动作上框架升级时推理行为可预期共享对象即协议RuntimeContext、Workspace、AgentStateStore 三个对象是 Harness 各能力的全部衔接面。会话、多租户、记忆压缩不需要业务代码发明任何协议交给框架的约定即可装配点集中第六节的全部分支都发生在 ScopeChatAgent 一个类的构造器里两入口对外装配细节对内。web 与 domain 只看到 AgentInvokeRequest 进、AgentEvent 出框架类型不出 adapter——六边形边界的承诺在装配层兑现。如果主路径直接用 ReActAgent上面三条都要业务代码自己补自己设计会话结构、自己定义租户隔离、自己实现压缩触发。HarnessAgent 的价值不在多几个功能而在这些工程命题已经有了框架级的形状。七、本篇学到了什么与 03 预告框架侧本篇完成了 harness 层的四个认知HarnessAgent 是 ReActAgent 的薄包装工程能力全部以 builder 挂点按需开启core 循环不改写RuntimeContext、Workspace、AgentStateStore 三个共享对象构成能力的全部衔接面状态会话以(userId, sessionId)寻址五种存储实现与显式 builder distributedStore 自动注入 本地默认的优先级规则覆盖从单测到多副本的部署谱系MiddlewareBase 以接口形态提供四个洋葱钩子与一个提示词变换钩子next 短路即拦截ChatOtel 的双检锁懒初始化让可观测能力零配置缺席、一配置生效。工程侧ScopeChatAgent 把这些概念装配成一个真实可运行的主路径16 参数构造器汇聚两个入口ChatOtel.install 最先、提示词三层拼接、条件挂点守卫三条顺序约束资源申请与释放互为镜像。权限取舍落在装配期消灭危险工具、运行期拦截留给动态授权的两层设计上。主路径选择 HarnessAgent本质是选择让框架的工程约定替代业务代码里的自造协议。下一篇进入知识与工具层Tool 注解驱动的工具系统——工具如何被扫描注册、签名约定如何影响模型调用以及 RAG 检索链路——从 retrieve 工具到向量检索、查询改写与重排序的完整路径。工具是 Agent 的手检索是 Agent 的书架两者在本文 SYS_PROMPT 的工具路由里已经露过面届时拆开看内部。项目信息dream-scope 开源地址GitHub - logosssss/dream-scope: 基于 AgentScope Java 2.0 的模块化单体 Agent 运行时 · GitHub 觉得有帮助欢迎 starDream-SaaS 项目地址https://dream-saas.com有问题评论区见欢迎交流~