【AgentScope】2. HarnessAgent 架构深度剖析

HarnessAgent 架构深度剖析

一句话概括

HarnessAgent 的架构核心是"薄封装 + Hook 驱动 + 共享对象协作"------它不在推理引擎上重复造轮子,而是用一层薄薄的外壳把身份管理、记忆持久化、工作区注入等工程能力"挂"到现有的推理循环上。

你能学到什么

  • 为什么 HarnessAgent 选择"薄封装"而不是自己写一套推理引擎
  • Hook 机制如何做到"每个能力只做一件事,互不干扰"
  • 三个共享对象如何成为所有 Hook 的"通用语言"
  • 一次 call() 从开始到结束经历了哪些步骤
  • 状态如何在"调用内 -> 跨调用 -> 长期记忆"三个层次之间流转
  • 典型协作场景的内部运作方式

前置知识

详见 README.md。此外需了解 ReAct 模式的基本概念("推理 -> 行动 -> 观察"循环)和事件驱动的基本思想。


核心概念

决策一:薄封装,不替换推理循环 --- "给手机加壳,而不是重新造手机"

生活类比:想象你买了一部手机(ReActAgent),它已经能打电话、发消息、拍照。现在你想给它加一个防摔壳(HarnessAgent),这个壳上还带了一个卡槽可以插银行卡、一个支架可以立在桌上。你不会为了加这些功能去拆开手机重新组装------你只是在手机外面套了一层壳,手机本身完全不受影响。

HarnessAgent 对 ReActAgent 就是这个关系:

  • ReActAgent 已经有完整的"推理 -> 行动 -> 观察"循环能力
  • HarnessAgent 不重写这个循环,只是在它外面套了一层"壳"
  • 这层壳只做两件额外的事:
    1. bindRuntimeContext(ctx):每次调用开始时,把"你是谁"(sessionId、userId)告诉系统,并尝试恢复上次的记忆
    2. forceCompactAndRetry:当模型报告"内容太多装不下"(ContextOverflow)时,强制压缩并重试一次

其他所有能力------工作区注入、记忆管理、会话持久化、子 agent 编排------都是通过 ReActAgent 已有的 Hook 和 Toolkit 扩展点注入的。

为什么这样设计? 因为 ReActAgent 本身是一个经过充分验证的推理引擎,重新造一个既不现实也没必要。"薄封装"意味着 HarnessAgent 的维护者只需要关注工程能力本身,推理引擎的升级和 bug 修复由 ReActAgent 团队负责。

决策二:Hook 驱动,能力正交 --- "流水线上的工位"

生活类比:想象一条汽车装配流水线。每个工位只做一件事:工位 A 装轮胎,工位 B 装方向盘,工位 C 喷漆。工位之间不需要互相认识,只需要按照编号顺序(priority)排好队,各自干各自的活。如果某个工位今天不需要(比如这批车不喷漆),直接跳过就行,不影响其他工位。

HarnessAgent 的 Hook 就是这条流水线上的"工位":

Hook priority(排序号) 做的事
AgentTraceHook 0 纯日志记录,最先执行,不干扰任何人
MemoryFlushHook 5 对话结束后,把新发现的事实写入"日记本"
MemoryMaintenanceHook 6 对话结束后,触发"整理笔记"的请求
CompactionHook 10 推理前检查:历史对话是不是太长了?太长就压缩
SandboxLifecycleHook 50 管理沙箱环境的启停
ToolResultEvictionHook 50 工具返回结果太大?落盘保存,只留一个占位符
SubagentsHook 80 推理前注入"你有这些下属 agent 可以调遣"
WorkspaceContextHook 900 最后把工作区的所有文件内容拼进 system prompt
SessionPersistenceHook 900 最后把当前对话状态保存到磁盘

关键特性

  • 每个 Hook 只做一件事("正交"的意思就是互不重叠)
  • Hook 之间不持有彼此的引用,完全独立
  • 每项能力都能独立开关:比如 CompactionHook 需要显式配置才启用,SessionPersistenceHook 默认开启
  • priority 数字越小越先执行,数字越大越后执行

为什么这样设计? 如果把所有能力都写在一个大函数里,改一个功能可能影响另一个,测试也困难。Hook 机制让每个能力像一个"插件",插上就生效,拔掉就关闭,互不干扰。

决策三:共享对象是唯一耦合点 --- "公司的公共公告板"

生活类比:想象一个公司里有很多部门(Hook),它们之间不直接沟通。但是公司大厅里有一块公共公告板,上面贴着三张表:

  1. 当次任务身份卡(RuntimeContext):写着"这次任务是谁的、用什么 session"
  2. 办公桌文件索引(WorkspaceManager):写着"办公桌抽屉里有什么文件、怎么读、怎么写"
  3. 文件柜位置(AbstractFilesystem):写着"文件柜在哪------是本地磁盘、远程服务器、还是沙箱"

每个部门(Hook)只看公告板上的信息来工作,不需要知道其他部门在干什么。公告板就是所有部门唯一的"共同语言"。

三个共享对象的详细职责:

对象 职责 生命周期
RuntimeContext 当次调用的身份信息:sessionId、userId、session 引用、额外数据 每次调用 call() 时重新注入,不持久化
WorkspaceManager 工作区的无状态读写器:读文件时先查 filesystem,没命中再查本地磁盘 构建时创建,跨调用复用
AbstractFilesystem 存储后端抽象:可以是本地磁盘、沙箱、或远程 KV 存储 构建时创建,跨调用复用

为什么这样设计? 如果 Hook 之间直接互相调用,就会出现"蜘蛛网"一样的依赖关系,改一个牵动一片。共享对象让所有 Hook 通过"公告板"间接协作,依赖关系变成星形的------每个 Hook 只依赖公告板,不依赖其他 Hook。


关键代码解读

构建阶段:Builder.build()

构建阶段是"一次性"的------构建完成后,运行期间不再改变 Hook 链或工具集。这就像建房子:先打好地基、装好管道电线,然后才开始住人。

复制代码
HarnessAgent.Builder.build()
  │
  ├── 创建三个共享对象
  │   ├── WorkspaceManager(办公桌管理员)
  │   ├── AbstractFilesystem(文件柜)
  │   └── RuntimeContext 的引用(身份卡槽,还没插卡)
  │
  ├── 按 priority 排好 Hook 链
  │   ├── [0]  AgentTraceHook       ← 必装
  │   ├── [5]  MemoryFlushHook      ← 必装
  │   ├── [6]  MemoryMaintenanceHook ← 必装
  │   ├── [10] CompactionHook       ← 可选,需显式配置
  │   ├── [50] SandboxLifecycleHook ← 可选,需配置沙箱
  │   ├── [50] ToolResultEvictionHook ← 可选,需显式配置
  │   ├── [80] SubagentsHook        ← 必装
  │   ├── [900] WorkspaceContextHook ← 必装
  │   └── [900] SessionPersistenceHook ← 必装
  │
  ├── 追加内置工具到 Toolkit
  │   └── 用户自定义工具 + 内置工具(文件读写、记忆搜索等)
  │
  ├── 从 workspace/skills/ 装配技能包
  │   └── SkillBox(自动加载或手动配置)
  │
  ├── 交给 ReActAgent.builder() 构建最终 delegate
  │
  └── 启动后台守护线程
      └── MemoryMaintenanceScheduler(6 小时周期运行)

Hook x 事件矩阵:完整触发表

ReActAgent 在推理循环的各个关键节点触发事件,Hook 在对应事件上按 priority 升序执行。这张表是理解整个系统的"地图":

事件 触发时机 触发的 Hook(按执行顺序) 做了什么
PreCallEvent 推理循环启动前 Trace(0) 记录"推理开始了"
PreReasoningEvent 每次调用模型前 Trace(0) -> Compact(10) -> Subagents(80) -> WorkspaceCtx(900) 日志 -> 检查是否需要压缩 -> 注入子 agent 信息 -> 注入工作区文件内容
PostReasoningEvent 每次模型返回后 Trace(0) 记录"模型回答了"
PreActingEvent 每个工具调用前 Trace(0) 记录"要调用工具了"
PostActingEvent 每个工具调用后 Trace(0) -> Eviction(50) 日志 -> 如果工具结果太大就落盘
PostCallEvent 最终回复产出后 Trace(0) -> MemFlush(5) -> MemMaint(6) -> Session(900) 日志 -> 写日记 -> 请求整理笔记 -> 保存会话
ErrorEvent 推理出现异常时 Trace(0) -> Session(900) 日志 -> 保存当前状态(防止丢失)

priority 数字的设计意图

  • 0:纯日志,最先跑,不碰任何数据
  • 5/6/10:记忆相关,在推理循环外围管理上下文生命周期
  • 50:沙箱和工具结果处理,在行动阶段就地解决
  • 80:子 agent 注入,必须在工作区注入之前------因为子 agent 信息需要出现在 system prompt 里
  • 900:最后写 system prompt 和持久化------保证叠加在所有前置处理之上,且"记忆先 flush 再 snapshot"

call() 生命周期的完整时序

当用户调用 agent.call(msg, ctx) 时,系统内部的完整流程:

复制代码
用户调用 agent.call(msg, ctx)
  │
  ▼
① HarnessAgent.bindRuntimeContext(ctx)
  │  把 ctx(sessionId、userId)分发给所有 Hook
  │  尝试从 session 恢复上次记忆(如果有的话)
  │
  ▼
② 委托给 ReActAgent: delegate.call(msg)
  │
  ├── PreCallEvent
  │   └── Trace(0): 记录"推理开始"
  │
  ▼
  ┌─── ReAct 推理循环(反复执行,直到模型不再调用工具)───┐
  │                                                        │
  │  PreReasoningEvent                                     │
  │    ├── Trace(0): 日志                                   │
  │    ├── Compact(10): 历史太长?压缩!                      │
  │    ├── Subagents(80): 注入"你有这些下属"                  │
  │    └── WorkspaceCtx(900): 注入工作区文件到 system prompt  │
  │                                                        │
  │  调用模型 stream(messages)                               │
  │    └── 模型返回 ChatResponse                            │
  │                                                        │
  │  PostReasoningEvent                                    │
  │    └── Trace(0): 日志                                   │
  │                                                        │
  │  如果模型返回了 tool_calls:                              │
  │    ┌── 逐个执行工具 ──┐                                 │
  │    │ PreActingEvent   │                                 │
  │    │   └── Trace(0)   │                                 │
  │    │ 执行工具 invoke() │                                 │
  │    │ PostActingEvent  │                                 │
  │    │   ├── Trace(0)   │                                 │
  │    │   └── Eviction(50): 结果太大就落盘+占位符            │
  │    └──────────────────┘                                 │
  │                                                        │
  │  回到循环顶部,把工具结果加入消息,再次推理                 │
  └────────────────────────────────────────────────────────┘
  │
  ├── PostCallEvent
  │   ├── Trace(0): 日志
  │   ├── MemFlush(5): 把新事实写入日记本,完整对话落盘到 JSONL
  │   ├── MemMaint(6): 请求"整理笔记"
  │   └── Session(900): 保存当前会话状态到磁盘
  │
  ▼
③ 返回最终消息给用户

  异常路径:
  ├── ErrorEvent -> Trace(0) + Session(900) 保存状态
  └── ContextOverflow -> forceCompactAndRetry -> 重新调用 delegate.call()

整体流程图

三层架构总览

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        调用方                                    │
│                  agent.call(msg, ctx)                            │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│              第一层:薄包装层 (HarnessAgent)                      │
│                                                                  │
│   ① bindRuntimeContext(ctx)        ③ forceCompactAndRetry       │
│      分发身份信息、恢复记忆            ContextOverflow 时的兜底    │
│                                                                  │
│   ┌───────────────────────────────────────────────────────────┐  │
│   │         第二层:推理内核 (ReActAgent)                       │  │
│   │                                                           │  │
│   │   ┌─────────────────────────────────────────────────┐     │  │
│   │   │  Hook 链(按 priority 排序,拦截生命周期事件)     │     │  │
│   │   │  Trace -> MemFlush -> Compact -> Subagents ->     │     │  │
│   │   │  WorkspaceCtx -> Eviction -> Session               │     │  │
│   │   └─────────────────────────────────────────────────┘     │  │
│   │                          ↕ 事件驱动                        │  │
│   │   ┌─────────────────────────────────────────────────┐     │  │
│   │   │  ReAct 循环: reason -> act -> observe            │     │  │
│   │   └─────────────────────────────────────────────────┘     │  │
│   │                          ↕ 工具调用                        │  │
│   │   ┌─────────────────────────────────────────────────┐     │  │
│   │   │  Toolkit: FilesystemTool / MemorySearch /         │     │  │
│   │   │          AgentSpawnTool / TaskTool / ...          │     │  │
│   │   └─────────────────────────────────────────────────┘     │  │
│   │                          ↕ 读写上下文                      │  │
│   │   ┌─────────────────────────────────────────────────┐     │  │
│   │   │  Memory (InMemoryMemory)                         │     │  │
│   │   └─────────────────────────────────────────────────┘     │  │
│   └───────────────────────────────────────────────────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
                           │
                           │ 读写
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│              第三层:共享对象(Hook 协作的通用语言)                │
│                                                                  │
│   ┌──────────────┐  ┌──────────────────┐  ┌──────────────────┐  │
│   │RuntimeContext│  │WorkspaceManager  │  │AbstractFilesystem│  │
│   │              │  │                  │  │                  │  │
│   │ sessionId    │  │ 读: filesystem   │  │ 本地磁盘         │  │
│   │ userId       │  │     -> 本地兜底   │  │ 沙箱             │  │
│   │ session 引用  │  │ 写: filesystem   │  │ 远程 KV 存储     │  │
│   │ extra 数据   │  │                  │  │                  │  │
│   └──────────────┘  └──────────────────┘  └──────────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

状态流转图

状态在系统中有三个层次,从"最短命"到"最持久":

复制代码
┌─────────────────────────────────────────────────────────────┐
│  调用内 (in-call)                                            │
│  随 call() 开始而创建,随 call() 结束而消散                    │
│                                                              │
│  ┌──────────────────────┐  ┌──────────────────────┐         │
│  │ Memory               │  │ RuntimeContext       │         │
│  │ (InMemoryMemory)     │  │ sessionId, userId    │         │
│  │ 当次对话的消息序列     │  │ 当次调用的身份信息    │         │
│  └──────────┬───────────┘  └──────────────────────┘         │
│             │                                                │
└─────────────┼────────────────────────────────────────────────┘
              │
              │ PostCallEvent 时持久化
              │
    ┌─────────┼──────────────────────────────────┐
    │         │                                  │
    ▼         ▼                                  ▼
┌──────────────────────┐  ┌──────────────────────────────────┐
│ 跨调用 (cross-call)   │  │ 长期记忆 (long-term)              │
│ 同 sessionId 下持久    │  │ 跨 session 积累                   │
│                       │  │                                   │
│ WorkspaceSession     │  │ memory/YYYY-MM-DD.md              │
│ agents/<id>/         │  │ 每日事实流水账(追加写入)           │
│   context/<sess>/    │  │                                   │
│   *.json             │  │ MEMORY.md                         │
│ Memory 快照 + 状态    │  │ 整理后的长期记忆(整体重写)         │
│                       │  │                                   │
│ sessions/<sess>.log  │  │ memory_index.db                   │
│   .jsonl             │  │ SQLite FTS5 全文索引               │
│ 完整对话日志(追加)   │  │                                   │
│                       │  │                                   │
└───────────┬──────────┘  └──────────────────────────────────┘
            │                        │
            │                        │
            ▼                        ▼
    下次 call() 开头                每次 PreReasoningEvent
    bindRuntimeContext              WorkspaceContextHook
    + loadIfExists                  读取 MEMORY.md 注入
    恢复到 Memory                      system prompt

核心规律(用三句话记住):

  1. Memory 是"工作内存" :随 call() 结束时,通过两条路(写日记 + 保存快照)持久化
  2. WorkspaceSession 保证"下次还认识你":同一个 sessionId 再来,能恢复到上次的状态
  3. MEMORY.md + FTS 索引保证"长期不忘":重要事实不随 session 丢失,下次任何 session 都能查到

与其他模块的关系

本文是架构总览,下方的每个子模块文档对应架构中的一个具体部分。


⬅️ 上一篇:01-overview | 📖 回到目录 | ➡️ 下一篇:03-workspace


学习要点

1. 三个核心设计决策必须牢记

决策 一句话 为什么
薄封装 不替换推理循环,只在外层加壳 复用已有引擎,降低维护成本
Hook 驱动 每个能力是独立插件,按编号排队执行 能力正交,可独立开关和测试
共享对象 三个对象是所有 Hook 的"公共公告板" 解耦 Hook 之间的直接依赖

2. Priority 数字有明确含义

  • 0:日志,最先,不碰数据
  • 5-10:记忆与压缩,管上下文生命周期
  • 50:沙箱与工具结果,在行动阶段就地处理
  • 80:子 agent 注入,必须在 900 之前
  • 900:最后写 system prompt 和持久化

3. 状态有三个生命周期

  • 调用内:Memory 和 RuntimeContext,用完就散
  • 跨调用:WorkspaceSession 和 JSONL 日志,同 sessionId 可恢复
  • 长期MEMORY.md 和 FTS 索引,跨 session 永久保存

4. 构建 vs 运行的清晰分界

  • 构建阶段(Builder.build()):一次性装好 Hook 链、工具集、共享对象
  • 运行阶段(call()):不再改变结构,只按预设的管道执行

5. Hook x Event 矩阵是理解系统的"钥匙"

读懂了这张矩阵表,就等于拿到了系统内部运作的地图。每个 Hook 在什么时候执行、做什么事、为什么按这个顺序------全在这张表里。

下一篇03-workspace.md 将深入工作区的目录结构,看看 WorkspaceManager 是如何实现"filesystem 优先 -> 本地兜底"的两层读取策略。

相关推荐
blue_dou1 小时前
架构与能力边界解析:七款CRM产品四大核心维度对比测评
大数据·架构·逻辑回归·流程图
未来之窗软件服务2 小时前
自适应开发3分钟重构软件·阿雪心学·无相无界(13)—东方仙盟
重构·架构·仙盟创梦ide·东方仙盟·东方仙盟无相无界
该昵称用户已存在2 小时前
双碳目标下的能源中台自建之路:MyEMS 百万测点场景的架构自主权与数据库选型为题
数据库·架构·能源
是大强2 小时前
嵌入式五层架构分层(应用→模块→系统→驱动→平台)
架构
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章11:Kerberos安全认证
数据仓库·hadoop·学习·架构·高炉炼铁·工业智能体·高炉炼铁智能化
装不满的克莱因瓶2 小时前
DDD 设计与 Maven 多模块拆分:从单体项目到领域驱动架构实践
java·架构·maven·ddd
@insist1232 小时前
系统架构设计师-系统可靠性模型计算全解析
架构·系统架构·软考·系统架构设计师·软件水平考试
装不满的克莱因瓶2 小时前
JSON 处理与内嵌 Tomcat 部署:Spring Boot 如何实现前后端数据交互与一键启动?
java·spring boot·spring·架构·tomcat·json
团象科技2 小时前
出海企业技术架构优化实地观察 拆解AWS Lambda无服务器的落地细节
架构·serverless·aws