Skip to content

开发resendReActAgent 的重新回答功能:在不重新输入问题的情况下,让 Agent 对上一个用户问题重新生成一次回答。#1097

Open
lsh13865950442-droid wants to merge 7 commits intoagentscope-ai:mainfrom
lsh13865950442-droid:jayce-branch-resend
Open

开发resendReActAgent 的重新回答功能:在不重新输入问题的情况下,让 Agent 对上一个用户问题重新生成一次回答。#1097
lsh13865950442-droid wants to merge 7 commits intoagentscope-ai:mainfrom
lsh13865950442-droid:jayce-branch-resend

Conversation

@lsh13865950442-droid
Copy link
Copy Markdown

ReActAgent Resend 功能使用指南

1. 功能简介

resendReActAgent 提供的重新回答功能:在不重新输入问题的情况下,让 Agent 对上一个用户问题重新生成一次回答。

典型使用场景:

  • 用户对 Agent 的回答不满意,希望重新生成
  • 网络抖动或模型超时导致调用失败,需要重试
  • 多轮对话中某一轮结果需要重试而不影响后续状态

2. 核心设计

架构分层

Resend 能力按三层实现:

┌─────────────────────────────────────────────────────────┐
│                     ReActAgent                          │
│  编排层:saveResendAnchor()、loadIfExists(resend=true)  │
│          在每次 call() 前自动触发各组件保存锚点          │
└──────────────────────┬──────────────────────────────────┘
                       │ 调用
┌──────────────────────▼──────────────────────────────────┐
│                   StateModule 接口                       │
│  saveAnchor() / restoreAnchor() / hasAnchor()            │
│  (default 方法,组件自行覆写)                          │
└──────┬──────────────┬──────────────────┬────────────────┘
       │              │                  │
 ┌─────▼──────┐ ┌─────▼──────┐  ┌───────▼──────┐
 │   Memory   │ │PlanNotebook│  │   SkillBox   │
 │  deleteMsg │ │restoreAnchor│  │restoreAnchor │
 │  From(idx) │ │  快照方式  │  │  快照方式    │
 └────────────┘ └────────────┘  └──────────────┘

锚点生命周期

call(userMsg)
  │
  ├── saveResendAnchor(msgs)          ← 每次 call() 前自动执行
  │     ├── anchorInputMsgs = copy(msgs)
  │     ├── memory.saveAnchor()
  │     ├── planNotebook.saveAnchor()
  │     ├── skillBox.saveAnchor()
  │     └── anchorActiveGroups = toolkit.getActiveGroups()
  │
  ├── addToMemory(msgs)
  ├── executeIteration(ReAct 循环)
  └── 返回最终回答 Msg

saveTo(session, sessionKey)          ← 调用方手动执行
  ├── memory.saveTo(...)
  ├── planNotebook.saveTo(...)        包含 _state_anchor
  ├── skillBox.saveTo(...)            包含 skillbox_state_anchor
  ├── session.save("resend_input_msgs", anchorInputMsgs)
  └── session.save("toolkit_activeGroups_anchor", anchorActiveGroups)

loadIfExists(session, sessionKey, resend=true)   ← Resend 入口
  ├── loadFrom(session, sessionKey)   恢复完整状态
  ├── memory.deleteMessagesFrom(cutIndex)  通过消息 ID 定位截断点
  ├── planNotebook.restoreAnchor()
  ├── skillBox.restoreAnchor()
  ├── toolkit.setActiveGroups(anchorActiveGroups)
  └── return anchorInputMsgs          返回原始输入,由调用方重新 call()

3. 使用指南

3.1 正常流程(含持久化)

import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.model.Model;
import io.agentscope.core.session.JsonSession;
import io.agentscope.core.state.SimpleSessionKey;
import io.agentscope.core.session.SessionKey;
import java.nio.file.Path;

public class NormalFlowExample {

    public static void main(String[] args) {
        Model model = buildModel(); // 替换为实际 Model 实现

        ReActAgent agent = ReActAgent.builder()
                .name("assistant")
                .model(model)
                .sysPrompt("你是一个乐于助人的智能助手。")
                .build();

        // 准备 Session 和 SessionKey
        JsonSession session = new JsonSession(Path.of("sessions"));
        SessionKey sessionKey = SimpleSessionKey.of("user-123");

        // 加载历史状态(若存在)
        agent.loadIfExists(session, sessionKey);

        // 构造用户消息
        Msg userMsg = Msg.builder()
                .name("user")
                .role(MsgRole.USER)
                .content(TextBlock.builder().text("请用一句话介绍 Java。").build())
                .build();

        // 调用:内部自动保存锚点到内存
        Msg reply = agent.call(userMsg).block();
        System.out.println(reply.getTextContent());

        // 持久化状态 + 锚点到 Session
        agent.saveTo(session, sessionKey);
    }

    private static Model buildModel() {
        throw new UnsupportedOperationException("请替换为实际的 Model 实现");
    }
}

3.2 Resend 流程

import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.session.JsonSession;
import io.agentscope.core.state.SimpleSessionKey;
import io.agentscope.core.session.SessionKey;
import java.nio.file.Path;
import java.util.List;

public class ResendFlowExample {

    public static void main(String[] args) {
        Model model = buildModel();

        ReActAgent agent = ReActAgent.builder()
                .name("assistant")
                .model(model)
                .sysPrompt("你是一个乐于助人的智能助手。")
                .build();

        JsonSession session = new JsonSession(Path.of("sessions"));
        SessionKey sessionKey = SimpleSessionKey.of("user-123");

        // 加载状态并恢复锚点,返回上一次的原始输入消息
        List<Msg> inputMsgs = agent.loadIfExists(session, sessionKey, true);

        // 用原始输入重新执行(内部会再次保存新锚点)
        Msg newReply = agent.call(inputMsgs).block();
        System.out.println("重新生成的回答:" + newReply.getTextContent());

        // 保存新结果到 Session
        agent.saveTo(session, sessionKey);
    }

    private static Model buildModel() {
        throw new UnsupportedOperationException("请替换为实际的 Model 实现");
    }
}

关键区别:resend 流程中,调用方自己拿到 inputMsgs 后调用 agent.call(inputMsgs),整个 call → saveTo 链路与正常流程完全一致,无需单独的 resend() 方法。


4. API 参考

loadIfExists(Session, SessionKey)

public boolean loadIfExists(Session session, SessionKey sessionKey)

从 Session 中加载 Agent 状态(若存在)。返回 true 表示 Session 存在并完成加载,false 表示 Session 不存在(跳过加载)。

参数

  • session:Session 实例(JsonSession、Redis Session 等)
  • sessionKey:Session 键,标识此 Agent 的状态存储位置

loadIfExists(Session, SessionKey, boolean resend)

public List<Msg> loadIfExists(Session session, SessionKey sessionKey, boolean resend)

加载状态(若存在),并在 resend=true 时额外执行锚点恢复,返回原始输入消息列表。

参数

  • session:Session 实例
  • sessionKey:Session 键
  • resendtrue 时恢复所有组件到锚点状态,并返回 anchorInputMsgs

返回值

  • resend=true:返回锚点保存时的原始输入消息列表(非空)
  • resend=false:返回空列表

抛出异常

  • IllegalStateExceptionresend=true 但无锚点(从未调用过 call() 或未持久化)

loadIfExists(Session, String, boolean resend)

public List<Msg> loadIfExists(Session session, String sessionId, boolean resend)

loadIfExists(Session, SessionKey, boolean) 的便利重载,使用字符串 sessionId 替代 SessionKey


saveTo(Session, SessionKey)

public void saveTo(Session session, SessionKey sessionKey)

将 Agent 当前状态持久化到 Session,包括:

  • Agent 元数据(agent_meta
  • Memory 消息列表(memory_messages,若 memoryManaged)
  • Toolkit 已激活工具组(toolkit_activeGroups,若 toolkitManaged)
  • PlanNotebook 状态及锚点(_state / _state_anchor,若 planNotebookManaged)
  • SkillBox 状态及锚点(skillbox_state / skillbox_state_anchor,若 skillBoxManaged)
  • Resend 锚点输入消息(resend_input_msgs,若存在锚点)
  • Toolkit 工具组锚点(toolkit_activeGroups_anchor,若存在锚点)

StatePersistence

控制哪些组件参与状态管理(saveTo / loadFrom / 锚点)。

工厂方法 说明
StatePersistence.all() 管理所有组件(默认)
StatePersistence.none() 不管理任何组件
StatePersistence.memoryOnly() 仅管理 Memory
StatePersistence.builder()...build() 自定义每个组件

Builder 示例

// 不由 Agent 管理 PlanNotebook(由外部逻辑控制)
ReActAgent agent = ReActAgent.builder()
        .name("assistant")
        .model(model)
        .planNotebook(myNotebook)
        .statePersistence(StatePersistence.builder()
                .planNotebookManaged(false)
                .build())
        .build();

5. 回滚范围

执行 resend 时,以下组件参与状态回滚:

组件 是否回滚 回滚方式
Memory(对话历史) ✅ 是 deleteMessagesFrom(cutIndex) 按消息 ID 截断
PlanNotebook(规划笔记本) ✅ 是 restoreAnchor() 恢复内存快照
Toolkit.activeGroups(工具组激活状态) ✅ 是 setActiveGroups(anchorActiveGroups)
SkillBox(技能激活状态) ✅ 是 restoreAnchor() 恢复内存快照
LongTermMemory(外部长期记忆) ❌ 否 外部存储,不参与回滚
工具外部副作用(文件/DB/API) ❌ 否 物理操作无法自动撤销

6. 实现细节

Memory 恢复策略

Memory 的锚点不单独存储一份消息副本,而是通过消息 ID 定位截断点实现恢复:

  1. saveResendAnchor() 记录 anchorInputMsgs(原始输入消息列表,含消息 ID)
  2. resend 时,在当前 Memory 中查找 anchorInputMsgs[0].getId() 对应的位置 cutIndex
  3. 调用 memory.deleteMessagesFrom(cutIndex) 截断该位置之后的所有消息

不同 Memory 实现的截断行为

实现类 deleteMessagesFrom 行为
InMemoryMemory 直接截断内部 List<Msg>
AutoContextMemory 同时截断 workingMemoryStorageoriginalMemoryStorage(按消息 ID 匹配)

持久化文件说明(JsonSession)

使用 JsonSession 时,saveTo 会在 Session 目录下生成以下 key 文件:

Key 内容
agent_meta Agent 元数据(名称、描述、系统提示)
memory_messages Memory 消息列表
toolkit_activeGroups 当前激活的工具组
_state / _state_anchor PlanNotebook 状态及锚点
skillbox_state / skillbox_state_anchor SkillBox 状态及锚点
resend_input_msgs 锚点时的原始输入消息
toolkit_activeGroups_anchor 锚点时的工具组激活状态

7. 文件变更清单

核心模块(agentscope-core)

文件 变更说明
StateModule.java 新增 saveAnchor() / restoreAnchor() / hasAnchor() default 方法
Memory.java 新增 deleteMessagesFrom(int fromIndex) default 方法(按索引截断消息)
InMemoryMemory.java 覆写 deleteMessagesFrom(直接截断内部列表);实现 saveAnchor / restoreAnchor
PlanNotebook.java 实现 saveAnchor / restoreAnchorsaveTo / loadFrom 包含锚点持久化(key: _state_anchor
SkillBox.java 实现 saveAnchor / restoreAnchorsaveTo / loadFrom 包含锚点持久化(key: skillbox_state_anchor
ReActAgent.java 新增 saveResendAnchor();新增 loadIfExists resend 重载;saveTo 额外持久化 resend_input_msgstoolkit_activeGroups_anchor
StatePersistence.java 控制组件管理策略(all() / none() / memoryOnly() / builder()
Session.java 保持纯粹,不包含锚点方法

扩展模块(agentscope-extensions)

文件 变更说明
AutoContextMemory.java 覆写 deleteMessagesFrom,同时截断 workingMemoryStorageoriginalMemoryStorage(按消息 ID 匹配),保证双存储一致性

8. 注意事项

  1. 必须绑定 Session 才能 resend:resend 功能依赖 Session 持久化锚点数据。没有默认的内置 Session,必须通过 loadIfExists(session, key)saveTo(session, key) 显式绑定 Session,否则无法跨请求执行 resend。

  2. 至少执行一次 call() 后才能 resendloadIfExists(..., true) 要求 Session 中存在 resend_input_msgs,即之前至少完成过一次 call() + saveTo(),否则抛出 IllegalStateException

  3. resend 后必须再次 saveTo:resend 执行 call() 后会生成新的锚点(内存中),必须再次调用 saveTo(session, key) 将新锚点持久化,否则下次 resend 仍会读到旧锚点。

  4. 连续 resend 是安全的:每次 call() 开始时都会更新锚点,因此可以连续多次 resend,每次都是从同一问题重新回答。

  5. LongTermMemory 不参与回滚:外部长期记忆(向量数据库等)的写入不会被撤销。

  6. 工具副作用不会回滚:工具执行期间产生的外部副作用(写文件、数据库写入、HTTP 请求等)无法被自动撤销。

  7. StatePersistence 影响回滚范围:若通过 StatePersistence 禁用某组件的管理,该组件将不参与锚点保存与恢复。

@lsh13865950442-droid lsh13865950442-droid requested a review from a team April 1, 2026 08:10
@cla-assistant
Copy link
Copy Markdown

cla-assistant bot commented Apr 1, 2026

CLA assistant check
All committers have signed the CLA.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant