Files
liteFlow/docs/liteflow-react-agent-guide.md
everywhere.z d72d04475b docs(agent): clarify lifecycle and security caveats in react agent guide
Add notes on framework integration prerequisites, event listener
threading, build-time capability methods, session reuse semantics,
built-in tool risks, and two new troubleshooting entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:13:31 +08:00

48 KiB
Raw Blame History

LiteFlow ReAct Agent 使用指南

本文介绍如何在 LiteFlow 中使用 liteflow-react-agent,把 agentscope-java 的 ReActAgent 当作普通 LiteFlow 节点编排到 EL 链路中。

读完本文后,你应该能够:

  • 引入对应模型平台模块,并完成 liteflow.agent.* 配置;
  • 编写一个继承 ReActAgentComponent 的 Agent 组件;
  • 在 EL 中组合 Agent、普通节点、条件路由和并行节点
  • 通过 ExecuteOption.eventListener(...) 接收 Agent 执行中的流式事件;
  • 正确理解 conversation、agentKey、memory、workspace、内置文件工具、Shell 工具和 Skills 的边界。

当前仓库根版本:2.16.0

当前源码中 liteflow-react-agent 聚合模块的 maven.compiler.source / target17,根 compile-17+ profile 会在 JDK 17 及以上激活该模块。实际运行时还需要满足 agentscope-java 及具体模型 SDK 的运行要求。


1. 模块说明

liteflow-react-agent 是一个聚合模块,核心能力在 liteflow-react-agent-core,不同模型供应商由独立子模块提供便捷入口。

模块 作用
liteflow-react-agent-core ReActAgentComponentModelSpec 基础设施、conversation / agentKey 会话管理、memory 持久化、流式事件桥接、workspace 文件工具、受管 Shell 工具
liteflow-react-agent-openai OpenAI 官方 API + OpenAI 兼容协议,内置 DeepSeek、Kimi、GLM、Minimax 便捷入口
liteflow-react-agent-anthropic Anthropic Claude 模型入口
liteflow-react-agent-gemini Google Gemini 模型入口
liteflow-react-agent-dashscope 阿里云 DashScope / Qwen 模型入口

业务项目通常只需要引入一个平台模块。平台模块会传递依赖 liteflow-react-agent-core


2. 快速开始

2.1 引入依赖

以 DeepSeek 这类 OpenAI 兼容平台为例:

<dependency>
    <groupId>com.yomahub</groupId>
    <artifactId>liteflow-react-agent-openai</artifactId>
    <version>${liteflow.version}</version>
</dependency>

业务应用仍需引入 LiteFlow 对应的运行集成,例如 liteflow-spring-boot-starterliteflow-spring-boot4-starter 或其他已有 LiteFlow starter模型平台模块只提供 ReAct Agent 能力和平台 SDK 入口。如果一个应用里要同时使用多个模型平台,可以同时引入多个平台模块。

2.2 配置 LiteFlow 与 Agent

最小配置需要包含规则文件、workspace 根目录和模型凭据。生产环境建议先关闭 Shell 工具,再按需开启。

liteflow.rule-source=agent/flow.el.xml

liteflow.agent.workspace.root=/var/lib/liteflow/agent-workspaces
liteflow.agent.shell.mode=disabled

liteflow.agent.openai-compatible.deepseek.api-key=${DEEPSEEK_API_KEY}
# DeepSeek.of(...) 已内置默认 baseUrl如需覆盖再配置下一行。
liteflow.agent.openai-compatible.deepseek.base-url=https://api.deepseek.com/v1

liteflow.agent.workspace.root 是必填项。没有配置时,首次执行 Agent 组件会抛出:

AgentConfigException: liteflow.agent.workspace.root is required

2.3 编写 Agent 组件

Agent 组件继承 ReActAgentComponent,至少实现三个无参方法:

  • model():返回一个 ModelSpec<?>,声明使用哪个平台、哪个模型及可选高级参数;
  • systemPrompt():创建 Agent 时使用的系统提示词;
  • userPrompt():每次调用时发送给 Agent 的用户消息。

当前源码已把执行上下文改为通过 ctx() 动态获取,而不是把 ReActAgentContext 作为参数传入各个 hook。ctx() 只能在 process() 生命周期内调用,包括 systemPrompt()userPrompt()、自定义工具回调、Hook 回调和 handleReply()

package demo.agent;

import com.yomahub.liteflow.agent.component.ReActAgentComponent;
import com.yomahub.liteflow.agent.model.ModelSpec;
import com.yomahub.liteflow.agent.openai.DeepSeek;
import org.springframework.stereotype.Component;

@Component("deepseekAgent")
public class DeepSeekAgentCmp extends ReActAgentComponent {

    @Override
    protected ModelSpec<?> model() {
        return DeepSeek.of("deepseek-chat");
    }

    @Override
    protected String systemPrompt() {
        return "你是一名简洁的中文助理,回答严格控制在两句话以内。";
    }

    @Override
    protected String userPrompt() {
        Object req = getSlot().getChainReqData(getSlot().getChainId());
        return req == null ? "" : req.toString();
    }

    @Override
    protected boolean enableShellTool() { return false; }

    @Override
    protected boolean enableWorkspaceFileTools() { return false; }
}

如果自定义工具、Hook 或 Model 会被缓存并跨多次 invocation 复用,不要在这些对象中保存某次调用的 ReActAgentContext 引用;应保存组件实例,并在运行时通过组件内部类或组件暴露的公开方法间接调用受保护的 ctx(),获取当次上下文。

2.4 在 EL 中编排

Agent 节点和普通 NodeComponent 一样使用:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
<flow>
    <chain name="deepseekChain">
        THEN(prepare, deepseekAgent, recordReply);
    </chain>
</flow>

调用方式也和普通 LiteFlow 链路一致:

LiteflowResponse response = flowExecutor.execute2Resp("deepseekChain", "用一句话介绍 LiteFlow");
if (response.isSuccess()) {
    Object reply = response.getSlot().getResponseData();
}

默认情况下Agent 回复会通过 reply.getTextContent() 写入 slot.responseData。如果后续节点希望从指定位置读取回复,可以覆写 handleReply(reply),或者像测试用例中的 RecordReplyCmp 一样把 responseData 转存到节点输出。

2.5 下游节点如何拿到 Agent 的执行结果

ReAct Agent 节点执行完后,结果传递给下一个节点有两种方式。

方式 1默认走 slot.responseData(最简单)

ReActAgentComponent#handleReply() 默认实现:

protected void handleReply(Msg reply) {
    ctx().getSlot().setResponseData(reply == null ? null : reply.getTextContent());
}

下一个普通 NodeComponent 直接读取即可:

@LiteflowComponent("recordReply")
public class RecordReplyCmp extends NodeComponent {
    @Override
    public void process() {
        String reply = (String) this.getSlot().getResponseData();
        // 进行后续处理 ...
    }
}

链路结束后,外部调用方也可以通过 response.getSlot().getResponseData() 拿到。

方式 2覆写 handleReply 写入自定义位置

需要做结构化处理、写到 ContextBean或者一条链路里有多个 Agent 节点时,建议覆写 handleReply,否则后一个 Agent 的 responseData 会覆盖前一个:

@Override
protected void handleReply(Msg reply) {
    String text = reply == null ? null : reply.getTextContent();
    // 选择 1写入自定义 ContextBean
    ctx().getSlot().getContextBean(MyAgentCtx.class).setReply(getNodeId(), text);
    // 选择 2以 nodeId 为 key 存到 slot 输出,避免相互覆盖
    ctx().getSlot().setOutput(getNodeId(), text);
}

下游节点对应使用 slot.getContextBean(MyAgentCtx.class)slot.getOutput(nodeId) 读取。

多 Agent 节点共存的注意事项:默认 responseData 是 slot 级别的单一字段,后写覆盖先写。链路中存在多个 ReAct Agent 时,请覆写 handleReplysetOutput(nodeId, ...) 或自定义 ContextBean 区分各 Agent 的输出。

2.6 流式输出

LiteFlow 的 execute2Resp(...) 仍然保持原有语义:整条 chain 执行完成后返回 LiteflowResponse。如果希望在 Agent 执行过程中实时拿到模型输出、工具结果或最终结果,可以通过 ExecuteOption.eventListener(...) 注册事件监听器。

import com.yomahub.liteflow.core.ExecuteOption;
import com.yomahub.liteflow.flow.FlowEvent;
import com.yomahub.liteflow.flow.LiteflowResponse;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

List<FlowEvent> events = new CopyOnWriteArrayList<>();

LiteflowResponse response = flowExecutor.execute2Resp("deepseekChain", "用一句话介绍 LiteFlow",
        ExecuteOption.of()
                .conversationId("chat-user-1-conv-1")
                .eventListener(event -> {
                    if ("agent.reasoning".equals(event.getType()) && event.getText() != null) {
                        // 可在这里转发到 SSE、WebSocket 或命令行输出。
                        System.out.print(event.getText());
                    }
                    events.add(event);
                }));

if (response.isSuccess()) {
    Object finalReply = response.getSlot().getResponseData();
}

当前 ReAct Agent 会把 AgentScope 的流事件转换成 LiteFlow 通用 FlowEvent

FlowEvent#getType() 含义 典型内容
agent.reasoning 模型推理 / 回复过程 增量文本、最终 assistant 消息、工具调用请求
agent.tool_result 工具执行结果 工具输出或工具执行中的增量片段
agent.summary 达到最大迭代次数后的总结 summary 生成过程或最终 summary
agent.result 本轮 Agent 最终结果 与最终 handleReply(reply) 使用的消息一致

FlowEvent 会携带 chainIdnodeIdrequestIdconversationIdtextlasttimestamp 和原始 data。其中 nodeId 对多 Agent 链路很重要:WHEN(agentA, agentB) 并发执行时,多个 Agent 的流式事件可能交错到达,调用方应按 nodeIdconversationId 或业务自定义字段分组展示。

没有注册 eventListener 时,ReActAgentComponent 会继续走原来的阻塞调用路径,不会产生额外事件开销;注册 listener 后,组件会使用 AgentScope 的 agent.stream(...) 执行,并在流结束后照常调用 handleReply(reply)、保存 memory、返回 LiteflowResponse

eventListener 在 chain 执行线程中同步回调。生产环境转发到 SSE、WebSocket 或消息队列时,建议在 listener 中只做轻量入队或缓冲,不要执行耗时 I/Olistener 抛出的异常会向上传播,并可能导致本次链路失败。


3. ReActAgentComponent 扩展点

ReActAgentComponent#process()final。框架在其中统一完成配置读取、conversation 解析、agentKey 解析、Session 获取、加锁、Agent 懒构建、调用、回复处理和 memory 保存。业务侧通过覆写受保护方法定制行为。

方法 是否必须 默认行为 说明
model() 返回 ModelSpec<?>,由框架从 AgentConfig 解析凭据并构造 agentscope Model
systemPrompt() 返回系统提示词,同一 (conversationId, agentKey) 首次构建 Agent 时调用
userPrompt() 返回本轮用户消息,每次 process() 都调用
tools() 空列表 注册自定义 @Tool 对象
skills() 空列表 返回本 Agent 可使用的技能名白名单;空列表表示允许使用配置目录中的全部技能
enableSkills() 读取 liteflow.agent.skills.enabled(默认 false 是否为本组件启用 AgentScope skills全局关闭时可保持既有 Agent 行为不变
usedSkills() 本轮已成功加载技能名列表 只能在 process() 触发的生命周期内读取,例如 userPrompt()、工具回调、Hook 回调和 handleReply()
resolveConversationId() 先复用 slot.conversationId,再读 chainReqData Map 中的 conversationId,最后调用 ConversationIdGenerator.generate() 决定本次调用所属业务会话;同一条 chain 内首个 Agent 写回 slot 后,后续 Agent 默认复用
agentKey() 当前 nodeId,为空时为 default 在同一 conversation 中区分不同 Agent 实例和记忆;默认不同节点互相隔离
maxIterations() -1 返回正数时覆盖全局 defaults.max-iterations
enableShellTool() true 是否注册内置受管 Shell 工具;配置为 DISABLED 时即使返回 true 也不会注册
enableWorkspaceFileTools() true 是否注册内置 workspace 文件工具
hooks() 空列表 注册 agentscope Hook
enableReActLogging() liteflow.agent.logging.react-enabled(默认 true 是否注册内置 ReActLoggingHook,将 reason / act / error 事件写到日志
handleReply(reply) 写入 slot.responseData 自定义回复处理逻辑
buildModel() 委派 model().resolve(agentConfig()) 逃生舱:完全自行构造 agentscope Model

ReActAgentContext 可通过组件的 ctx() 方法取得,提供四项执行上下文:

方法 说明
getSlot() 当前 LiteFlow Slot
getConversationId() 安全化后的 conversation ID决定 workspace 子目录
getAgentKey() 安全化后的 Agent key默认来自 nodeId
getWorkspaceDir() 当前 conversation 对应的 workspace 目录

注意:systemPrompt() 只在同一 (conversationId, agentKey) 下首次构建 Agent 时调用;后续调用会复用同一个 Agent 实例和 memory。动态输入应放在 userPrompt() 中,并通过 ctx()getSlot() 读取当次 invocation 的数据。

同理,tools()hooks()skills()enableSkills()enableReActLogging()buildModel() 都属于 Agent 构建期能力声明,通常只在同一 (conversationId, agentKey) 首次构建缓存 Agent 时生效。不要让这些方法依赖单次请求数据;如果确实需要按请求隔离模型、工具或 hook请把请求维度体现在 agentKey()conversationId 中,让框架构建新的 AgentSession。


4. ModelSpec 与模型入口

4.1 核心设计

ModelSpec<SELF> 是所有平台模型描述符的基类。子类按“哪个平台 + 哪个模型 + 可选高级参数”三段式给出,框架负责从 AgentConfig 解析凭据并构造 agentscope Model

基类提供的共性参数(所有平台共享):

方法 类型 说明
temperature(double) Double 采样温度
topP(double) Double nucleus sampling
topK(int) Integer top-k sampling
maxTokens(int) Integer 最大输出 token
seed(long) Long 随机种子
stream(boolean) Boolean 是否让底层模型请求使用流式模式
cacheControl(boolean) Boolean 缓存控制;当前 OpenAI 与 DashScope 内置解析会下发该参数

所有参数均为可选,未设置时不写入 GenerateOptions 或模型 Builderagentscope 使用服务端或 SDK 默认值。

注意:ModelSpec.stream(true) 只控制底层模型请求是否使用流式传输;调用方是否能在 LiteFlow 执行期间收到事件,取决于本次调用是否通过 ExecuteOption.eventListener(...) 注册了监听器。没有 listener 时,最终仍然只通过 LiteflowResponse 读取结果。

4.2 平台入口一览

每个平台模块提供一个不可变入口类,通过静态 of(modelName)custom(configKey, modelName) 方法返回平台对应的 Spec 子类。Spec 子类在基类共性参数之上暴露平台个性参数。

模块 入口类 Spec 子类 个性参数
liteflow-react-agent-openai OpenAI OpenAISpec reasoningEffort, frequencyPenalty, presencePenalty
liteflow-react-agent-openai DeepSeek OpenAICompatibleSpec 继承 OpenAISpec 全部参数,内置默认 baseUrl
liteflow-react-agent-openai Kimi OpenAICompatibleSpec 同上
liteflow-react-agent-openai GLM OpenAICompatibleSpec 同上
liteflow-react-agent-openai Minimax OpenAICompatibleSpec 同上
liteflow-react-agent-openai OpenAICompatible OpenAICompatibleSpec 自定义 configKey,用于任意 OpenAI 兼容厂商;无默认 baseUrl
liteflow-react-agent-anthropic Anthropic AnthropicSpec thinking(t -> t.budget(...).enabled(...));当前内置解析下发 budget
liteflow-react-agent-anthropic AnthropicCompatible AnthropicSpec 自定义 configKey,用于 Anthropic 兼容网关
liteflow-react-agent-gemini Gemini GeminiSpec thinking(t -> t.level(...).budget(...))
liteflow-react-agent-dashscope DashScope DashScopeSpec thinking(t -> t.budget(...)),设置 budget 时会启用 thinking

4.3 使用示例

OpenAI

@Override
protected ModelSpec<?> model() {
    return OpenAI.of("gpt-4o")
            .temperature(0.7)
            .maxTokens(1000)
            .stream(true);
}

凭据来源:liteflow.agent.openai.api-key

DeepSeekOpenAI 兼容):

@Override
protected ModelSpec<?> model() {
    return DeepSeek.of("deepseek-chat")
            .temperature(0.5);
}

凭据来源:liteflow.agent.openai-compatible.deepseek.api-keybase-url 可选;未配置时使用 DeepSeek 入口内置的默认地址。

自定义 OpenAI 兼容厂商:

@Override
protected ModelSpec<?> model() {
    return OpenAICompatible.custom("myvendor", "my-model")
            .temperature(0.7);
}

凭据来源:liteflow.agent.openai-compatible.myvendor.api-key / base-url。自定义厂商没有内置默认地址,通常需要配置 base-url

Anthropic Claude

@Override
protected ModelSpec<?> model() {
    return Anthropic.of("claude-sonnet-4-5")
            .temperature(0.5)
            .thinking(t -> t.budget(2000).enabled(true));
}

凭据来源:liteflow.agent.anthropic.api-key

Anthropic 兼容网关:

@Override
protected ModelSpec<?> model() {
    return AnthropicCompatible.custom("gateway", "claude-haiku");
}

凭据来源:liteflow.agent.anthropic-compatible.gateway.api-key / base-url

Google Gemini

@Override
protected ModelSpec<?> model() {
    return Gemini.of("gemini-2.5-flash")
            .thinking(t -> t.level("high").budget(1024));
}

凭据来源:liteflow.agent.gemini.api-key

阿里云 DashScope

@Override
protected ModelSpec<?> model() {
    return DashScope.of("qwen-plus")
            .thinking(t -> t.budget(2048));
}

凭据来源:liteflow.agent.dashscope.api-key

4.4 凭据配置结构

所有平台凭据都使用 PlatformCredential

字段 说明
apiKey API Key
baseUrl 可选,自定义网关或兼容端点
extra 可选,业务自定义键值;当前内置 ProviderSpec 尚未读取

YAML 示例:

liteflow:
  agent:
    workspace:
      root: /var/lib/liteflow/agent-workspaces
    shell:
      mode: disabled
    openai:
      api-key: ${OPENAI_API_KEY}
    anthropic:
      api-key: ${ANTHROPIC_API_KEY}
    gemini:
      api-key: ${GEMINI_API_KEY}
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}
    openai-compatible:
      deepseek:
        api-key: ${DEEPSEEK_API_KEY}
        base-url: https://api.deepseek.com/v1
      kimi:
        api-key: ${KIMI_API_KEY}
        base-url: https://api.moonshot.cn/v1
    anthropic-compatible:
      gateway:
        api-key: ${ANTHROPIC_GATEWAY_API_KEY}
        base-url: https://anthropic-gateway.example.com

4.5 凭据解析规则

框架内建两种凭据解析策略,通过 CredentialResolver 实现:

  • 头等平台OpenAI / Anthropic / Gemini / DashScope):从 AgentConfig 的单一 PlatformCredential 字段读取(如 cfg.getOpenai())。缺失时抛出 AgentConfigException,提示 liteflow.agent.<platform>.api-key
  • 兼容平台DeepSeek / Kimi / GLM / Minimax / OpenAICompatible.custom / AnthropicCompatible.custom):从 AgentConfigMap<String, PlatformCredential> 中按 configKey 读取。缺失时抛出提示 liteflow.agent.<type>.<configKey>.api-key

4.6 逃生舱buildModel()

如果 ModelSpec 无法满足需求(例如需要传入 agentscope 原生的高级参数),可以直接覆写 buildModel(),完全自行构造 agentscope Model

@Override
protected Model buildModel() {
    return OpenAIChatModel.builder()
            .apiKey(agentConfig().getOpenai().getApiKey())
            .modelName("gpt-4o")
            .generateOptions(GenerateOptions.builder()
                    .temperature(0.7)
                    .build())
            .stream(true)
            .build();
}

覆写 buildModel() 后,默认实现中的 model().resolve(agentConfig()) 不会被调用;但因为 model() 仍是抽象方法,子类仍需实现它。


5. Conversation、agentKey 与 memory

5.1 两层标识分别负责什么

当前源码不是单一 sessionId 模型,而是把会话拆成两层:

  • conversationId:业务 / 对话维度,整条 chain 内一致,决定 workspace 子目录;
  • agentKey:组件维度,默认是 nodeId,用于在同一段 conversation 中区分不同 Agent 的 ReActAgent 实例和记忆。

每次执行 Agent 组件时,框架会通过 AgentSessionManager.acquire(conversationId, agentKey) 获取一个 AgentSession。同一个 (conversationId, agentKey) 会复用:

  • 同一个 ReActAgent 实例;
  • 同一个 Agent memory
  • 同一把 ReentrantLock
  • 同一个持久化 key格式为安全化后的 conversationId + "__" + agentKey

同一个 conversationId 下的不同 agentKey 默认拥有独立 Agent 和独立 memory但共享同一个 workspace 子目录,便于多个 Agent 通过文件协作。

因此,同一个 (conversationId, agentKey) 下的调用会串行执行,避免多线程同时修改同一份 memory。不同 agentKey 可以并行执行,但如果共享 workspace需要由业务自行避免写同名文件造成冲突。

5.2 conversationId 从哪里来

默认实现是:

protected String resolveConversationId() {
    Slot slot = getSlot();
    String existing = slot.getConversationId();
    if (existing != null && !existing.isEmpty()) {
        return existing;
    }
    Object req = slot.getChainReqData(slot.getChainId());
    if (req instanceof Map<?, ?> map) {
        Object v = map.get(CONVERSATION_ID_REQUEST_KEY);
        if (v != null) {
            String s = v.toString();
            if (!s.isEmpty()) {
                return s;
            }
        }
    }
    return ConversationIdGenerator.generate();
}

ConversationIdGenerator.generate() 返回 YYYYMMDD_<12 位 NanoId> 形式的字符串(例如 20260430_3F7K9PQRSTUV)。首个 Agent 解析出 conversationId 后会写回 slot.setConversationId(cid),同一条 chain 中后续 Agent 会默认复用它。

也可以在调用链路时显式传入 conversationId

LiteflowResponse response = flowExecutor.execute2Resp(
        "deepseekChain",
        request,
        ExecuteOption.of().conversationId("chat-user-1-conv-1"));

或者让框架先生成一个 conversationId再从响应中取回供下一轮复用

LiteflowResponse first = flowExecutor.execute2Resp(
        "deepseekChain",
        request,
        ExecuteOption.of().autoConversationId());
String cid = first.getConversationId();

如果要按业务请求对象实现多轮对话,也可以覆写:

@Override
protected String resolveConversationId() {
    ChatRequest req = getSlot().getChainReqData(getSlot().getChainId());
    return "chat-" + req.getUserId() + "-" + req.getConversationId();
}

conversationIdagentKey 只允许直接使用 [a-zA-Z0-9_-]+。其他字符会被 URL 编码,并把 % 替换为 _,从而安全地用作目录名或缓存 key。空值会变成 _

5.3 agentKey 从哪里来

默认实现是当前节点的 nodeId

protected String agentKey() {
    String nodeId = getNodeId();
    return (nodeId == null || nodeId.isEmpty()) ? "default" : nodeId;
}

这意味着同一段 conversation 中,mathAgentsummaryAgent 默认有各自独立的 Agent 记忆;但它们共享 workspace.root/<conversationId>/ 目录。

如果确实希望多个节点共享同一个 Agent 记忆,可以让它们返回同一个 agentKey();如果希望同一个节点的不同执行完全隔离,可以把 requestId 或业务标识拼入 agentKey()

5.4 JVM 热 Session 配置

liteflow.agent.session.* 控制当前 JVM 中热 Session 的缓存时间和数量:

配置项 默认值 说明
idle-timeout 30m Session 空闲多久后可被清理
cleanup-interval 1m 后台清理线程检查间隔,最低 20ms
max-sessions 10000 当前 JVM 中最多保留多少个热 Session(conversationId, agentKey) 计数

清理线程名为 liteflow-agent-session-cleaner。它只会清理已经超过 idle-timeout 且当前没有被加锁的 Session。

当 Session 数量超过 max-sessions 时,会按 lastActive 淘汰最旧的 JVM 内缓存。这个淘汰只移除热 Agent 实例,不删除 workspace也不删除 Redis、MySQL 或文件中的持久化记忆。

注意:AgentSessionManager 在首次执行 Agent 组件时按当前 liteflow.agent.* 配置懒创建,已有热 Session 中的 Agent 也会被复用。运行中修改 memory、skills、模型、工具或 hook 配置后,已有 (conversationId, agentKey) 不一定立即体现新配置;通常需要使用新的 conversation / agentKey或重启应用。

5.5 memory 持久化模式

liteflow.agent.session.memory.* 控制 Agent memory 保存在哪里。它和热 Session 缓存是两件事:热缓存决定当前 JVM 里保留多久memory 持久化决定重启或重新加载后能否恢复对话历史。

模式 含义 适用场景
JVM 默认值,使用 AgentScope 的 JVM 内 Session进程退出后丢失 单进程内多轮对话
NONE 不加载也不保存持久化 Session热缓存中的同一 (conversationId, agentKey) 仍会复用到过期或淘汰 不需要持久化;如需严格无状态,配合每次唯一的 conversationId 或 agentKey 使用
LOCAL_FILE 保存到 workspace.root/.agent-session/ 下,由 AgentScope 文件 Session 管理 本地文件持久化、开发测试、小规模部署
REDIS 使用用户提供的 Redis 客户端 Bean 多实例共享短期会话
MYSQL 使用用户提供的 DataSource Bean 需要数据库持久化和运维治理

通用开关:

配置项 默认值 说明
load-on-first-use true 当前 JVM 首次构建某个 Session 的 Agent 时,尝试恢复历史
save-after-call true 调用成功后保存记忆
save-on-error true 调用抛错时也尝试保存记忆

LOCAL_FILE 示例:

liteflow.agent.session.memory.mode=LOCAL_FILE
liteflow.agent.session.memory.load-on-first-use=true
liteflow.agent.session.memory.save-after-call=true
liteflow.agent.session.memory.save-on-error=true

REDIS 示例:

liteflow.agent.session.memory.mode=REDIS
liteflow.agent.session.memory.redis.bean-name=redissonClient
liteflow.agent.session.memory.redis.client-type=REDISSON
liteflow.agent.session.memory.redis.key-prefix=liteflow:agent:session

client-type 可选 REDISSONJEDISLETTUCE。LiteFlow 不创建 Redis 连接,必须由业务框架提供对应 Bean。

MYSQL 示例:

liteflow.agent.session.memory.mode=MYSQL
liteflow.agent.session.memory.mysql.data-source-bean-name=agentDataSource
liteflow.agent.session.memory.mysql.database-name=agentscope
liteflow.agent.session.memory.mysql.table-name=agentscope_sessions
liteflow.agent.session.memory.mysql.create-if-not-exist=false

LiteFlow 不创建 JDBC 连接池,DataSource 也需要由业务应用提供。


6. Workspace 与内置工具

6.1 Workspace 目录结构

每个 conversation 都会在 liteflow.agent.workspace.root 下获得一个独立子目录:

/var/lib/liteflow/agent-workspaces/
├── chat-user-1-conv-1/
├── chat-user-1-conv-2/
└── .agent-session/

<conversationId>/ 是同一段 conversation 中多个 Agent 共享的工具读写目录;.agent-session/ 只在 session.memory.mode=LOCAL_FILE 时创建,用于保存 AgentScope 的记忆文件。

6.2 Workspace 配置

配置项 默认值 说明
root 必填workspace 根目录
auto-create true 根目录不存在时是否自动创建
cleanup-on-session-expire true 空闲 Session 过期清理时,若该 conversation 下没有其他存活 AgentSession是否删除对应 workspace 子目录
cleanup-on-jvm-shutdown false AgentSessionManager.close() 时是否删除当前存活 Session 对应的 workspace
max-file-bytes 10485760 read_file 单次最多读取的字节数;超出时截断读取
max-list-size 1000 list_files 单次最多返回的条目数

如果 Agent 生成的文件需要长期保留,例如报告、草稿、审计材料,应把 cleanup-on-session-expire 设为 false,并由业务侧做归档。

6.3 Workspace 文件工具

enableWorkspaceFileTools() 默认为 true。开启后,框架会注册 WorkspaceFileTools

工具名 行为
read_file 读取当前 conversation workspace 内的文本文件,超过 max-file-bytes 时截断
write_file 覆盖写入文本文件,并自动创建父目录
list_files 列出目录内容,最多返回 max-list-size
delete_file 删除文件

所有路径都必须是相对路径。绝对路径或 .. 越界路径会被拒绝。

当前文件工具面向文本内容:read_filemax-file-bytes 截断读取,write_file 是覆盖写入并自动创建父目录。业务如果允许 Agent 写入大文件或二进制文件,建议关闭内置文件工具,改用自定义工具做大小、类型和审计控制。

关闭方式:

@Override
protected boolean enableWorkspaceFileTools() {
    return false;
}

6.4 受管 Shell 工具

enableShellTool() 默认为 true。如果配置中的 liteflow.agent.shell.mode 不是 DISABLED,框架会注册 ManagedShellCommandTool,工具名为 execute_shell_command

Shell 工具在当前 conversation workspace 下执行命令。当前实现会按空白符切分命令字符串,用 ProcessBuilder 直接执行 token 列表,并通过首 token 做白名单或黑名单判断;不会通过系统 shell 解释管道、重定向、变量展开等语法。配置项如下:

配置项 默认值
mode WHITELIST
whitelist ls, find, tree, stat, file, basename, dirname, pwd, which, cat, head, tail, grep, sed, awk, wc, sort, uniq, cut, tr, diff, echo, printf, expr, date, whoami, hostname, uname, env, df, du, ps, md5sum, sha256sum, jq, curl, wget, python3, node
blacklist rm, sudo, shutdown, mkfs, dd
timeout 30s
max-output-bytes 1048576

生产环境建议默认关闭:

liteflow.agent.shell.mode=disabled

确实需要 Shell 时,优先使用 WHITELIST,并把白名单收窄到业务需要的命令。

默认白名单包含 curlwgetpython3node 等能力较强的命令。即使当前实现不经过系统 shell、不支持管道和重定向这些命令仍可能带来网络访问或脚本执行风险生产环境不要直接沿用默认白名单。

6.5 ReAct 事件日志

框架内置 ReActLoggingHook,把 agent 的 reason / act / error 事件写入 SLF4J便于在终端直接观察 ReAct 推理过程。日志中的方括号 ID 为 conversationId:agentKey

事件 日志格式
PreReasoningEvent [agent:reason][conversationId:agentKey] >>> model=... messages=N
PostReasoningEvent [agent:reason][conversationId:agentKey] <<< text=... toolCalls=[...]
PreActingEvent [agent:act][conversationId:agentKey] >>> tool=... input=...
PostActingEvent [agent:act][conversationId:agentKey] <<< tool=... result=...
ErrorEvent [agent:error][conversationId:agentKey] ...
  • 全局开关:liteflow.agent.logging.react-enabled(默认 true)。
  • 单组件开关:覆写 enableReActLogging() 强制返回 true / false
  • 输出 logger 名:com.yomahub.liteflow.agent.hook.ReActLoggingHook(可在 logback / log4j2 中独立调级)。
  • 文本字段超过 500 字会被截断为 ...(truncated)

如果业务侧已经通过自定义 Hook 处理事件流,建议关掉内置日志钩子以避免重复输出。


7. Skills 支持

liteflow-react-agent 支持加载 AgentScope 的 filesystem skills。Skill 适合承载“什么时候使用”“如何执行”的长指令,也可以把某些 Java @Tool 只绑定到指定 skill 上,避免所有工具都全局暴露给 Agent。

7.1 开启 Skills

Skills 默认关闭。开启后,框架会从 liteflow.agent.skills.path 指向的目录扫描技能:

liteflow.agent.skills.enabled=true
liteflow.agent.skills.path=./skills
liteflow.agent.skills.strict=true
配置项 默认值 说明
enabled false 是否启用配置驱动的 skills 支持;关闭时不会注册 load_skill_through_path 工具
path ./skills skills 根目录,目录下每个子目录表示一个 skill
strict true 严格模式。目录缺失、声明的 skill 不存在、tools 类加载失败等问题会快速失败;设为 false 时记录 warn 并尽量继续

配置只是把可选技能放入 Agent 的 SkillBox。某个技能是否在本轮真正被使用,取决于 AgentScope ReAct 运行过程中是否调用 load_skill_through_path 加载该技能。

7.2 目录结构与 SKILL.md

推荐目录结构:

skills/
├── demo/
│   └── SKILL.md
└── tool-skill/
    └── SKILL.md

最小 SKILL.md 示例:

---
name: demo
description: Demo skill for LiteFlow ReAct agent
---

# Demo Skill

Use this skill when the request is about a simple demonstration.

name 是组件 skills() 过滤时使用的技能名,也会出现在 usedSkills() 返回值中。建议让目录名与 name 保持一致,便于排查。

7.3 组件级技能过滤

如果某个 Agent 只能在指定技能内选择,覆写 skills() 即可:

@Override
protected List<String> skills() {
    return List.of("demo", "tool-skill");
}

语义如下:

  • skills() 返回空列表:允许使用 skills.path 下的全部技能;
  • skills() 返回非空列表:只把这些技能放入本 Agent 的 SkillBox
  • 严格模式下,白名单中有不存在的技能会抛出 AgentConfigException;非严格模式下会记录 warn并加载能找到的技能
  • 不需要额外的 dependentSkills()skills() 本身就是本 Agent 的技能选择范围。

也可以让某个组件在全局开启时仍然禁用 skills

@Override
protected boolean enableSkills() {
    return false;
}

注意:skills()enableSkills() 只在某个 (conversationId, agentKey) 首次构建并缓存 ReActAgent 时求值。它们表示组件能力声明,不应依赖单次请求数据;同一 Session 后续复用缓存 Agent 时不会重新读取这些声明。

7.4 Skill 专属 Java 工具

如果某个 Java 工具只应随指定 skill 可用,可以在 SKILL.md frontmatter 中声明 tools

---
name: tool-skill
description: Skill that binds a Java tool
tools: com.example.agent.tool.SkillEchoTool
---

# Tool Skill

Use this skill when a Java tool should be available after loading the skill.

多个工具类可以用逗号分隔:

tools: com.example.agent.tool.SkillEchoTool, com.example.agent.tool.OrderTool

工具类要求:

  • 必须在应用 classpath 上;
  • 必须有无参构造器;
  • 工具方法仍按 agentscope-java 的 @Tool / @ToolParam 方式声明;
  • LiteFlow 会通过 SkillBox.registration().skill(skill).tool(tool).apply() 把工具绑定到该 skill不会把它作为全局 tools() 工具注册。

SkillToolManifest 可以识别 YAML list 形式的 tools,但为了兼容 AgentScope 当前 filesystem skill repository 的简单 YAML 解析,文档推荐使用上面的标量或逗号分隔写法。

7.5 记录本轮使用的技能

usedSkills() 返回当前 invocation 中已经成功加载过的技能名列表。典型用法是在 handleReply() 中把回复和技能使用情况一起写到下游可读的位置:

@Override
protected void handleReply(Msg reply) {
    ctx().getSlot().setOutput(getNodeId(), Map.of(
            "reply", reply == null ? "" : reply.getTextContent(),
            "skillsUsed", usedSkills()
    ));
}

使用边界:

  • 每次 process() 开始前会清空上一轮记录;
  • 只有 load_skill_through_path 成功加载的技能会被记录;
  • 可在 process() 触发的调用链内读取,例如 userPrompt()、工具回调、Hook 回调和 handleReply()
  • process() 最终清理后,后续 LiteFlow 生命周期回调不应再依赖 usedSkills()

7.6 当前边界

当前实现只接入 AgentScope 的 skill repository、SkillBox 和 skill-loading 工具,不会启用 AgentScope 的 skillBox.codeExecution()。文件读写和命令执行仍使用 LiteFlow 自己的 WorkspaceFileToolsManagedShellCommandTool,并继续受 liteflow.agent.workspace.*liteflow.agent.shell.* 配置控制。


8. 常见编排方式

8.1 注册自定义工具

自定义工具是普通对象,方法上使用 agentscope 的 @Tool@ToolParam

package demo.agent.tool;

import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;

public class OrderTool {

    @Tool(name = "query_order_status", description = "Query order status by order number")
    public String query(@ToolParam(name = "orderNo") String orderNo) {
        return "订单 " + orderNo + " 正在处理中";
    }
}

在 Agent 组件里注册:

@Override
protected List<Object> tools() {
    return List.of(new OrderTool());
}

如果工具需要访问当前 Slot、workspace 或 conversation 信息,不要在构造工具时捕获 ctx() 的返回值;可以把工具写成组件内部类,或由组件提供一个公开代理方法,在工具方法执行时再间接调用受保护的 ctx()

8.2 用 IF 做路由

<chain name="routerChain">
    THEN(
        prepare,
        IF(isMath, mathAgent, deepseekAgent),
        recordReply
    );
</chain>

isMath 可以是普通的 NodeBooleanComponentmathAgentdeepseekAgent 都是 ReActAgentComponent 子类。

8.3 用 WHEN 并行调用多个模型

<chain name="parallelChain">
    THEN(
        prepare,
        WHEN(deepseekAgent, dashscopeAgent).maxWaitSeconds(60),
        recordReply
    );
</chain>

默认情况下,同一条 chain 内的多个 Agent 会共享同一个 conversationId,但因为默认 agentKey() 是各自的 nodeId,所以 deepseekAgentdashscopeAgent 会使用不同的 AgentSession、不同的锁和不同的 memory可以并行执行它们共享同一个 workspace 子目录。

如果多个 Agent 覆写为相同的 agentKey(),它们会因为同一把 Session 锁而串行执行。要真正隔离并行,请确保 (conversationId, agentKey) 组合不同;如需文件层面也隔离,则同时让它们使用不同的 conversationId 或在共享 workspace 内写入不同子目录。

@Override
protected String agentKey() {
    return getNodeId() + "-" + getSlot().getRequestId();
}

8.4 多轮对话

多轮对话的关键是让多次调用使用同一个 conversationId,同时需要让同一个 Agent 节点保持同一个 agentKey(默认 nodeId 已满足):

LiteflowResponse first = flowExecutor.execute2Resp(
        "deepseekChain",
        firstRequest,
        ExecuteOption.of().conversationId("chat-user-1-conv-1"));

LiteflowResponse second = flowExecutor.execute2Resp(
        "deepseekChain",
        secondRequest,
        ExecuteOption.of().conversationId("chat-user-1-conv-1"));

或者在组件中按业务对象覆写:

@Override
protected String resolveConversationId() {
    ChatRequest req = getSlot().getChainReqData(getSlot().getChainId());
    return "chat-" + req.getUserId() + "-" + req.getConversationId();
}

只要多次执行解析出的 (conversationId, agentKey) 相同Agent 就会复用同一份 memory。是否能跨 JVM 重启恢复,取决于 liteflow.agent.session.memory.mode


9. 完整配置速查

# LiteFlow 规则
liteflow.rule-source=agent/flow.el.xml

# workspace
liteflow.agent.workspace.root=/var/lib/liteflow/agent-workspaces
liteflow.agent.workspace.auto-create=true
liteflow.agent.workspace.cleanup-on-session-expire=true
liteflow.agent.workspace.cleanup-on-jvm-shutdown=false
liteflow.agent.workspace.max-file-bytes=10485760
liteflow.agent.workspace.max-list-size=1000

# 当前 JVM 中的热 Session
liteflow.agent.session.idle-timeout=30m
liteflow.agent.session.cleanup-interval=1m
liteflow.agent.session.max-sessions=10000

# Agent memory 持久化
liteflow.agent.session.memory.mode=JVM
liteflow.agent.session.memory.load-on-first-use=true
liteflow.agent.session.memory.save-after-call=true
liteflow.agent.session.memory.save-on-error=true

# Redis memory仅 mode=REDIS 时需要
liteflow.agent.session.memory.redis.bean-name=redissonClient
liteflow.agent.session.memory.redis.client-type=REDISSON
liteflow.agent.session.memory.redis.key-prefix=liteflow:agent:session

# MySQL memory仅 mode=MYSQL 时需要
liteflow.agent.session.memory.mysql.data-source-bean-name=agentDataSource
liteflow.agent.session.memory.mysql.database-name=agentscope
liteflow.agent.session.memory.mysql.table-name=agentscope_sessions
liteflow.agent.session.memory.mysql.create-if-not-exist=false

# Shell 工具
liteflow.agent.shell.mode=disabled
liteflow.agent.shell.timeout=30s
liteflow.agent.shell.max-output-bytes=1048576

# ReAct 内部事件日志reason / act / error
liteflow.agent.logging.react-enabled=true

# ReAct 默认最大迭代次数
liteflow.agent.defaults.max-iterations=15

# Skills
liteflow.agent.skills.enabled=false
liteflow.agent.skills.path=./skills
liteflow.agent.skills.strict=true

# 平台凭据
liteflow.agent.openai.api-key=${OPENAI_API_KEY}
liteflow.agent.anthropic.api-key=${ANTHROPIC_API_KEY}
liteflow.agent.gemini.api-key=${GEMINI_API_KEY}
liteflow.agent.dashscope.api-key=${DASHSCOPE_API_KEY}
liteflow.agent.openai-compatible.deepseek.api-key=${DEEPSEEK_API_KEY}
liteflow.agent.openai-compatible.deepseek.base-url=https://api.deepseek.com/v1
liteflow.agent.anthropic-compatible.gateway.api-key=${ANTHROPIC_GATEWAY_API_KEY}
liteflow.agent.anthropic-compatible.gateway.base-url=https://anthropic-gateway.example.com

10. 安全建议

  1. 生产环境默认关闭 Shellliteflow.agent.shell.mode=disabled
  2. workspace.root 使用专门目录,不要和业务源码、日志、密钥目录混放。
  3. 不要直接把用户输入原样作为 conversationIdagentKey。建议使用业务 ID 拼接、哈希或映射。
  4. API Key 使用环境变量、配置中心或密钥管理系统,不要写入代码仓库。
  5. 根据业务体量设置 max-file-bytesmax-output-bytestimeoutmax-sessions,避免 Agent 调用消耗不可控。
  6. 选择 REDISMYSQL 记忆模式时,连接 Bean 由业务应用提供,权限和网络访问也应由业务应用控制。
  7. 多个 Agent 共享同一个 conversation workspace 时,建议约定各自的文件名前缀或子目录,避免并行写冲突。
  8. 开启 Skills 时,skills.path 建议使用只读、受版本管理或受发布流程控制的目录;不要让普通用户直接写入 SKILL.md 或其中声明的 Java 工具类。

11. 故障排查

现象 常见原因 处理方式
liteflow.agent.workspace.root is required 未配置 workspace 根目录 配置 liteflow.agent.workspace.root
cannot create workspace root 根目录不可写,或父目录权限不足 换到应用可写目录,或提前创建并授权
workspace root does not exist auto-create=false 且目录不存在 提前创建目录,或开启 auto-create
Missing API key: please configure liteflow.agent.openai.api-key 对应平台凭据未配置 配置对应平台的 api-key
多轮对话没有记忆 每次调用使用了不同 conversationId,或 agentKey() 被拼入了一次性值 传入稳定的 ExecuteOption.conversationId(...),或覆写 resolveConversationId() 返回稳定业务会话 ID同时保持同一 Agent 的 agentKey 稳定
同一条 chain 中多个 Agent 没有共享文件 它们解析到了不同 conversationId 让首个 Agent 写回的 slot.conversationId 被后续 Agent 复用,或显式传入同一个 ExecuteOption.conversationId(...)
重启后没有历史记忆 session.memory.mode=JVMNONE 改为 LOCAL_FILEREDISMYSQL
Redis 模式启动失败 未配置 Bean 名,或 Bean 类型与 client-type 不匹配 检查 bean-nameclient-type 和 classpath 依赖
MySQL 模式启动失败 未配置 DataSource Bean或 Bean 类型错误 配置正确的 data-source-bean-name
Shell 返回 command 'xxx' not allowed by whitelist 白名单模式下命令未放行 加入白名单,或继续保持禁用
path escapes workspace 文件工具收到绝对路径或越界路径 使用相对路径,并限制在当前 workspace 内
Skills root not found 开启 skills 后,liteflow.agent.skills.path 指向的目录不存在 创建 skills 根目录;或关闭 skills或在开发环境把 strict=false 以记录 warn 后继续
Declared skills not found: [...] 组件 skills() 声明了不存在的技能名,或 SKILL.md 中的 name 与预期不一致 检查技能目录与 SKILL.md frontmatter 的 name;严格模式下必须全部存在
references unknown tool class SKILL.mdtools 字段声明了 classpath 上找不到的 Java 工具类 确认工具类全限定名正确、依赖已打包、类有无参构造器
usedSkills() 为空 本轮 Agent 没有成功调用 load_skill_through_path,或读取时已离开 process() 生命周期 handleReply() 或工具回调中读取;确认模型确实加载了对应 skill
没有收到流式事件 本次调用没有使用 ExecuteOption.eventListener(...),或链路中没有 ReAct Agent 节点 注册 listener确认事件类型是否为 agent.reasoningagent.tool_resultagent.summaryagent.result
ctx() must be called during process() 在构造器、Bean 初始化、异步线程或 process() 结束后的生命周期中调用了 ctx() 只在 userPrompt()、工具回调、Hook 回调和 handleReply()process() 触发的调用链内读取;跨 invocation 缓存的对象不要保存 ReActAgentContext
注册 listener 后链路失败 eventListener 回调中抛出了异常,或执行了阻塞 I/O 导致上游超时 listener 内只做轻量处理并自行捕获异常;重型转发逻辑放到外部队列或线程池
WHEN 中多个 Agent 看起来没有并行 多个组件解析到了相同 (conversationId, agentKey),共用了同一把锁;或下游等待最慢分支 确保需要并行的 Agent 使用不同 agentKey();如还要隔离文件,则使用不同 conversation 或子目录

12. 参考位置

  • Core 模块:liteflow-react-agent/liteflow-react-agent-core/
  • OpenAI 模块:liteflow-react-agent/liteflow-react-agent-openai/
  • Anthropic 模块:liteflow-react-agent/liteflow-react-agent-anthropic/
  • Gemini 模块:liteflow-react-agent/liteflow-react-agent-gemini/
  • DashScope 模块:liteflow-react-agent/liteflow-react-agent-dashscope/
  • Skill 入口:liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/skill/
  • Skill 配置:liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SkillsConfig.java
  • 示例测试:liteflow-testcase-el/liteflow-testcase-el-react-agent/
  • 规则示例:liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml
  • Skill 示例:liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/skills/