【AgentScope】1. HarnessAgent 总览详解

HarnessAgent 总览 详解

一句话概括

HarnessAgent 就是给"只有金鱼记忆的 AI 助手"装上一整套工程化装备------记忆系统、会话管理、上下文控制------让它能像一个靠谱的员工一样长期稳定工作。

你能学到什么

  • 理解"裸 Agent"和"工程化 Agent"的本质区别
  • 掌握 Hook 机制与工作区、双层记忆等核心概念
  • 能跑通 Quickstart 示例并理解每一行代码
  • 理解"三根支柱"设计思想:身份持续、上下文可控、状态可恢复
  • 建立全局认知,知道后续每个文档对应哪个能力模块

前置知识

详见 README.md 前置知识部分。本篇额外需要:

  • ReAct 模式(可选):了解 Agent 的"推理(Reason) -> 行动(Act) -> 观察(Observe)"循环即可

核心概念

裸 Agent vs 工程 Agent ------ 就像实习生 vs 正式员工

想象你招了两个员工:

  • 实习生(裸 Agent / ReActAgent):你问他一个问题,他思考一下、查个资料、给你回答。但下班(对话结束)他就全忘了。第二天来上班,完全不知道昨天干了什么。
  • 正式员工(HarnessAgent):有笔记本记东西(记忆系统),有工牌知道自己的身份(工作区),有档案柜存历史记录(会话持久化),遇到大任务会拆分给同事(子 Agent),工作量太大知道怎么精简(对话压缩)。

裸的 ReActAgent 只有"请求 -> 推理 -> 工具 -> 回复"这一轮循环。它回答不了这些问题:

问题 类比
下一轮怎么办? 明天上班还记得昨天的事吗?
下一天怎么办? 一个月后还记得客户名字吗?
上下文爆了怎么办? 笔记本写满了怎么办?
状态丢了怎么办? 电脑重启后工作还在吗?
任务太重怎么办? 一个人干不完怎么办?

HarnessAgent 不替换推理循环本身,而是在循环的关键时机"插入"各种能力(通过 Hook 机制),把这些问题的默认工程答案打包好。

===

复制代码
裸 Agent(ReActAgent)的工作流:

  用户提问 → Agent 思考 → 调用工具 → 回复用户
       ↑                                     ↓
       └─── 一次结束,什么都不记得 ────────┘


HarnessAgent 的工作流:

  用户提问 → [记忆注入] → Agent 思考 → 调用工具 → [记忆保存] → 回复用户
       ↑                                              ↓
       └── 自动恢复上次状态 ←── 会话持久化 ──────────┘

Hook 机制 ------ 就像流水线上的检查站

生活类比:想象一条汽车生产流水线。汽车(Agent 的推理过程)经过不同的工位(Hook),每个工位负责一件事------有人装方向盘,有人喷漆,有人质检。每个工位有自己的优先级顺序,互不干扰,但都在同一条流水线上工作。

Hook 是在推理循环关键时机自动执行的模块。 每个 Hook 只做一件事,通过 priority(优先级数字)排好执行顺序,可以独立开关,互不影响。完整 Hook 列表和优先级见 02-architecture.md


工作区(Workspace) ------ 就像员工的办公桌

生活类比:想象一个正式员工的办公桌。桌上有工牌(AGENTS.md,定义身份)、笔记本(MEMORY.md,记录长期记忆)、参考书(KNOWLEDGE.md,领域知识)、技能手册(skills/,可复用能力)。每天上班时,员工会把桌上重要的东西看一遍,这就是"工作区上下文注入"。

工作区就是一个目录结构,所有关于 Agent 的信息都以 Markdown 文件的形式存放在这里:

复制代码
workspace/                           ← 默认路径: .agentscope/workspace
├── AGENTS.md                        ← 工牌:人格 / 行为约定
├── MEMORY.md                        ← 笔记本:整理过的长期记忆
├── knowledge/
│   └── KNOWLEDGE.md                 ← 参考书:领域知识
├── memory/
│   └── YYYY-MM-DD.md                ← 日记:每日记忆流水账
├── skills/<skill-name>/SKILL.md     ← 技能手册:自定义技能
└── subagents/<id>.md                ← 下属名单:子 Agent 声明

每次 Agent 开始推理前,WorkspaceContextHook 会自动读取这些文件,把它们"喂"给模型。这样 Agent 就知道自己是谁、知道什么、能做什么。


双层记忆 ------ 就像"日记本 + 整理后的笔记"

生活类比:你每天写日记(第一层),把当天发生的事流水账式地记下来。周末你花时间把日记整理成要点笔记(第二层),扔掉重复的、合并相似的。日常工作用日记查细节,用要点笔记快速回忆大局。

HarnessAgent 的记忆也分两层:

  • 第一层:日流水账(memory/YYYY-MM-DD.md------每次对话结束后,LLM 自动从对话中提炼关键事实,追加到当天的流水账文件里。高频写入,质量一般。

  • 第二层:长期记忆(MEMORY.md ------后台有个"整理员"(MemoryConsolidator),定期把流水账合并、去重、重写成高质量的长期记忆。低频更新,质量高。

    对话内容 → LLM 提炼事实 → 追加到当日流水账 → 后台整理 → 更新 MEMORY.md
    (6小时一轮)


对话压缩 ------ 就像"给塞满的抽屉瘦身"

生活类比:你的抽屉里塞满了收据(对话历史)。有一天抽屉关不上了(上下文溢出)。你需要把旧的收据整理成一张摘要表,只保留最近几张重要的,这样抽屉就又有空间了。

当对话消息积累到一定数量(默认 30 条),CompactionHook 会自动触发压缩:

  1. 先把对话中的关键事实提取到日流水账(不丢失信息)
  2. 用 LLM 把旧对话生成一份摘要
  3. 用摘要替换掉旧消息,只保留最近几条

极端情况下,如果模型真的报了"context overflow"错误,HarnessAgent 会捕获这个错误,强制压缩一次,然后自动重试------这就是"溢出恢复"。


会话持久化 ------ 就像"员工下班打卡,明天打卡上班接着干"

生活类比:你每天下班时,把没做完的工作整理成一份交接单(状态快照)。第二天上班时,读一下交接单,就能接着昨天的进度继续。如果公司搬家了(进程重启),只要交接单还在,工作就不会断。

在 HarnessAgent 里,每次 call() 结束时:

  1. SessionPersistenceHook 把 Agent 的当前状态(Memory、上下文等)保存到文件
  2. 下次用同一个 sessionId 调用时,bindRuntimeContext 自动从文件恢复状态

关键是:只要 sessionId 不变,即使你重启了 Java 进程,Agent 也能记得之前聊过什么。


大工具结果卸载 ------ 就像"快递太大放不进信箱,先放驿站"

生活类比:你收到一个巨大的快递包裹(工具返回的超大结果),但你的信箱(上下文窗口)装不下。快递员把包裹放在驿站(文件系统),在你信箱里留一张取件通知(占位符 + 预览)。你需要的时候再去驿站取。

ToolResultEvictionHook 在工具返回结果超过 80K 字符时自动触发:把完整结果保存到文件,上下文里只留一个简短的占位符。Agent 需要详细信息时,可以用 read_file 工具按需读取。


子 Agent 编排 ------ 就像"经理分配任务给下属"

生活类比:你是部门经理(主 Agent),手下有几个专员(子 Agent),各有所长。遇到复杂任务时,你把子任务分配给合适的专员去做。你可以等专员做完汇报(同步),也可以说"你先做着,我稍后来问"(后台异步)。

SubagentsHook 为主 Agent 注册 tasktask_output 两个工具。主 Agent 可以:

  • 同步委派:等子 Agent 做完,直接拿到结果
  • 后台委派 :让子 Agent 在后台执行,后续通过 task_output 查询进度和结果

RuntimeContext ------ 就像"当次通话的来电显示"

生活类比:每次客户打电话来,客服系统会显示来电号码(sessionId)和客户姓名(userId)。客服看到这些信息就知道该恢复哪份档案、以什么身份和客户沟通。电话挂了,这个"来电显示"信息就消失了------它不会被永久记录。

RuntimeContext 就是每次 call() 的身份载体:

  • sessionId:决定状态存放在哪里,日志归档到哪里
  • userId:决定文件系统的命名空间(天然的多租户隔离)
  • extra:可以携带额外的自定义数据

不会被持久化,只在当次调用的 Hook 和工具之间共享。


关键代码解读

下面逐行解读 Quickstart 示例代码。这是你接触 HarnessAgent 的第一个程序,每个部分都很重要。

1. 引入依赖

xml 复制代码
<dependency>
    <groupId>io.agentscope</groupId>
    <artifactId>agentscope-harness</artifactId>
    <!-- 使用项目定义的版本变量 -->
    <version>${agentscope.version}</version>
</dependency>

这一步只是告诉 Maven:"我要用 agentscope-harness 这个库"。类似于你在手机上安装一个 App。

2. 准备工作区

java 复制代码
// 1. 准备工作区:第一次运行生成 AGENTS.md,后续运行复用
Path workspace = Paths.get(".agentscope/workspace");
initWorkspaceIfAbsent(workspace);
  • Paths.get(".agentscope/workspace"):指定工作区的路径。相对于当前运行目录。
  • initWorkspaceIfAbsent(workspace):如果工作区目录不存在就创建,如果 AGENTS.md 不存在就写入默认内容。
java 复制代码
private static void initWorkspaceIfAbsent(Path workspace) throws Exception {
    // 创建工作区目录(如果已存在则不报错)
    Files.createDirectories(workspace);
    // 定位 AGENTS.md 文件
    Path agentsMd = workspace.resolve("AGENTS.md");
    // 如果文件已存在,直接返回------不覆盖用户的自定义内容
    if (Files.exists(agentsMd)) return;
    // 首次运行:写入默认的人格定义
    Files.writeString(agentsMd, """
            # 笔记助手

            你是一个帮助用户整理笔记和知识的助手。

            ## 行为约定
            - 主动记录用户提到的关键事实(姓名、计划、偏好等)
            - 回答用简洁中文,必要时给出要点列表
            - 对不确定的内容要主动说明,不要臆造
            """);
}

这段代码的要点:只在首次运行时创建 。之后你再运行,它会检测到 AGENTS.md 已经存在,直接跳过。这意味着你可以手动修改 AGENTS.md 来定制 Agent 的人格,不用担心被覆盖。

3. 构建模型

java 复制代码
// 2. 构建模型
Model model = DashScopeChatModel.builder()
        // 从环境变量读取 API Key------永远不要把密钥硬编码在代码里
        .apiKey(System.getenv("DASHSCOPE_API_KEY"))
        // 指定使用哪个模型
        .modelName("qwen-max")
        // 启用流式输出(边生成边返回,不用等全部生成完)
        .stream(true)
        .build();
  • DashScopeChatModel:阿里云的通义千问模型客户端。你也可以换成其他模型。
  • System.getenv("DASHSCOPE_API_KEY"):从环境变量读取密钥,安全做法。
  • stream(true):流式模式,模型一边思考一边返回结果,用户体验更好。

4. 构建 HarnessAgent(核心!)

java 复制代码
// 3. 构建 HarnessAgent
HarnessAgent agent = HarnessAgent.builder()
        // 给 Agent 起个名字,方便在日志里识别
        .name("quickstart-agent")
        // 系统提示词:告诉 Agent 它的基本职责
        .sysPrompt("你是一个帮助用户做笔记的助手。")
        // 绑定上面创建的模型
        .model(model)
        // 指定工作区路径------Agent 的"办公桌"在哪里
        .workspace(workspace)
        // 配置对话压缩:何时压缩、保留多少、压缩前先提取事实
        .compaction(CompactionConfig.builder()
                // 消息达到 30 条时触发压缩
                .triggerMessages(30)
                // 压缩后保留最近 10 条消息
                .keepMessages(10)
                // 压缩前先把事实提取到日流水账,避免信息丢失
                .flushBeforeCompact(true)
                .build())
        .build();

这段代码体现了 HarnessAgent 的"组装"思想:你通过 Builder 模式,把各个能力"装配"到 Agent 上。不配置 compaction 就没有压缩能力;配置了就自动拥有。

5. 创建 RuntimeContext

java 复制代码
// 4. 同一个 RuntimeContext 发起两轮对话
//    sessionId 相同 → 第二轮自动从 Session 恢复第一轮的状态
RuntimeContext ctx = RuntimeContext.builder()
        // 会话 ID:决定了状态存储位置。同一个 ID = 同一个会话
        .sessionId("demo-session")
        // 用户 ID:决定了文件命名空间,天然隔离不同用户的数据
        .userId("alice")
        .build();

RuntimeContext 是"当次调用的身份证"。sessionId 是最关键的字段------它决定了 Agent 从哪里恢复状态、把状态保存到哪里。

6. 第一轮对话

java 复制代码
// 第一轮:告诉 Agent 一些信息
Msg turn1 = agent.call(
        // 构建用户消息
        Msg.builder().role(MsgRole.USER)
                .textContent("我叫天宇,今天准备一个关于 ReAct 的技术分享。")
                .build(),
        // 传入 RuntimeContext
        ctx).block();  // .block() 同步等待结果;流式模式见 10-streaming.md
// 打印 Agent 的回复
System.out.println("[turn1] " + turn1.getTextContent());
  • Msg.builder().role(MsgRole.USER).textContent(...):构建一条用户角色的消息。
  • agent.call(msg, ctx):发起调用,返回一个响应式对象。
  • .block():同步阻塞,等待 Agent 完成推理后返回结果。

7. 第二轮对话(验证记忆恢复)

java 复制代码
// 第二轮:问 Agent 之前告诉它的信息
Msg turn2 = agent.call(
        Msg.builder().role(MsgRole.USER)
                .textContent("我叫什么?我今天要干什么?")
                .build(),
        // 注意:用的是同一个 ctx,sessionId 相同
        ctx).block();
System.out.println("[turn2] " + turn2.getTextContent());

关键是:同一个 sessionId 。因为用了同一个 ctx,第二轮 call() 会在开头自动从 Session 恢复第一轮的对话状态,所以 Agent 能回答"你叫天宇,今天要准备 ReAct 的技术分享"。


整体流程图

一次 agent.call(msg, ctx) 只需理解三个阶段:

  1. 绑定身份 --- bindRuntimeContext(ctx):把 sessionId、userId 分发给所有 Hook,如果是已有会话则从文件恢复状态
  2. ReAct 循环 --- 委托给内部 ReActAgent 的推理循环,各 Hook 在推理前/后/工具执行时自动介入
  3. 收尾持久化 --- 提取对话事实到记忆、保存会话状态到文件,返回最终回复

如果 LLM 报 ContextOverflow 错误,HarnessAgent 会强制压缩一次并自动重试。

完整时序(含每个 Hook 的触发时机和详细步骤)见 02-architecture.md


三根支柱:能力如何协同工作

HarnessAgent 的七大核心能力不是孤立的,它们分别支撑着"持续稳定运行"的三根支柱:

支柱 组成 效果
身份持续 工作区上下文注入 + 双层持久记忆 + 技能自动加载 每轮推理前,Agent 的人格和知识被重新"喂"给模型;对话事实沉淀回工作区。人格和知识不断累积,不随单次调用消失
上下文可控 对话压缩 + 工具结果卸载 + 溢出恢复 对话太长自动摘要,工具结果太大卸载到文件,溢出时强制压缩并重试。任意长度会话都不会压垮模型
状态可恢复 会话持久化 + RuntimeContext + 可插拔文件系统 每次调用结束保存状态到文件,下次调用自动恢复。进程重启、机器宕机,Agent 都能从断点继续

这三根支柱靠三个共享对象串起来:

共享对象 类比 职责
WorkspaceManager 办公桌管理员 谁来读写工作区的文件
AbstractFilesystem 文件柜/网盘 工作区的文件实际存在哪里
RuntimeContext 来电显示 当次调用是谁在说话

每个 Hook 只做自己的事,通过这三个对象和其他 Hook 协作------这就是 HarnessAgent 把一组独立能力合成一个"持续稳定 Agent"的方式。


与其他模块的关系

本文是总览,建立了全局认知。以下是后续各文档与本文概念的对应关系:

下一篇文档 对应的核心能力 关键问题
02-architecture.md 全部 Hook 怎么驱动一切?一次 call() 经历了什么?
03-workspace.md 工作区上下文注入 AGENTS.md / MEMORY.md 怎么注入到 prompt?
04-session.md 会话持久化 状态怎么跨进程保留?
05-memory.md 双层持久记忆 + 对话压缩 日流水账和 MEMORY.md 怎么协作?
06-filesystem.md 可插拔文件系统 文件存在本地还是远端还是沙箱?
07-tool.md 内置工具 Agent 自带哪些工具?
08-skill.md 技能加载 怎么给 Agent 装"插件"?
09-subagent.md 子 Agent 编排 主 Agent 怎么分配任务给子 Agent?
10-streaming.md 流式输出 怎么实时看到 Agent 的"思考过程"?
11-sandbox.md 沙箱 怎么隔离执行环境?

建议阅读顺序:按编号顺序读,每篇约 15-25 分钟,总计约 3.5 小时。


运行验证

如果你想亲手跑一下 Quickstart 示例,可以按以下步骤操作:

bash 复制代码
# 1. 设置 API Key(替换成你自己的)
export DASHSCOPE_API_KEY=your_key_here

# 2. 首次运行需要安装依赖到本地 Maven 仓库
mvn -pl agentscope-examples/agents/harness-examples/harness-quickstart -am install \
    -DskipTests -Dspotless.check.skip=true -Dmaven.javadoc.skip=true -q

# 3. 执行示例程序
mvn -pl agentscope-examples/agents/harness-examples/harness-quickstart exec:java \
    -Dexec.mainClass=io.agentscope.harness.example.QuickstartExample \
    -Dspotless.check.skip=true -q

运行后你应该观察到:

  1. .agentscope/workspace/AGENTS.md 被自动创建
  2. 第二轮提问"我叫什么"能正确回答"天宇"
  3. 多聊几轮后(消息数 >= 30),workspace/memory/ 目录下会出现日流水账文件
  4. 重启进程、用同一个 sessionId 再调用,Agent 依然记得之前的内容

学习要点

  • 能说清楚"裸 Agent"和"HarnessAgent"的区别------前者只有一轮循环,后者有七大工程化能力
  • 理解 Hook 机制:每个 Hook 只做一件事,通过 priority 排序,可以独立开关
  • 理解工作区 + 双层记忆:Agent 的"办公桌"与"日记本 + 笔记"的记忆模式
  • 能跑通 Quickstart 示例,理解每一行代码的作用
  • 理解三根支柱设计思想:身份持续、上下文可控、状态可恢复
相关推荐
Maiko Star1 天前
理解 RAG 的“为什么”与 Spring AI 实战初体验
人工智能·rag·springai
Maiko Star2 天前
SpringAI 模型 API 调用中的错误处理、重试与熔断降级实战
错误处理·springai
小码农叔叔4 天前
【AI智能体】AgentScope Java 整合SpringBoot 实战操作详解
agentscope·agentscope java·agentscope详解·agentscope使用·agentscope使用详解·agentscope总结
装不满的克莱因瓶5 天前
SpringAI Alibaba Tool工具调用机制实战-注解注册与函数调用全流程
人工智能·ai·tools·智能体·springai·tool
小当家.1056 天前
Spring AI vs LangChain4j:Java 后端接大模型,两条路线怎么选
java·人工智能·spring·langchain·springai
装不满的克莱因瓶8 天前
新版AI开发框架SpringAIAlibaba vs AgentScope 选型指南
java·开发语言·人工智能·ai·agent·alibaba·springai
@SmartSi10 天前
AgentScope Java 入门:如何使用 DashScopeChatModel 集成百练模型
java·agentscope
@SmartSi10 天前
AgentScope Java 入门:如何使用 OpenAIChatModel 集成兼容 OpenAI 协议模型
java·agentscope
linmoo198612 天前
Agent应用实践之四 - 基础:AgentScope-SpringBoot集成源码解析
人工智能·spring boot·agent·agentscope·openclaw