1. 从“看视频”到“玩视频”:StreamArena的愿景与挑战

最近在跟几个做多模态和智能体(Agent)的朋友聊天,大家不约而同地都在提一个词:“Streaming Video Understanding”。这听起来像是老生常谈的视频理解,但加上“Streaming”和“Agentic”这两个前缀,味道就完全变了。我们不再是讨论如何给一段已经下载好的、完整的视频打标签或做摘要,而是要让一个AI智能体,像人一样,实时地、连续地、带着明确目标地去“观看”一个正在直播或持续播放的视频流,并且能与之“互动”。这听起来有点像科幻电影里的场景,但“StreamArena”这个概念的提出,正是朝着这个方向迈出的关键一步。它瞄准的不是一个静态任务,而是一个动态的、长程的、交互式的智能体竞技场。

传统的视频理解模型,无论是基于CNN、Transformer还是最新的多模态大模型,其工作范式大多是“离线批处理”。给你一段5分钟的视频,模型吭哧吭哧算上几秒甚至几分钟,然后输出一个结果:这是踢足球,这里有只猫,人物情绪是开心的。这种模式对于事后分析、内容审核、视频检索很有用,但它存在一个根本性的“延迟”和“被动性”问题。智能体无法在事件发生的当下就做出反应,更无法因为自己的“观察”而主动改变“观看”策略,去聚焦、去追问、去探索视频流中未知的部分。

而“StreamArena”所描绘的“Continuous, Interactive, and Long-Horizon Agentic Streaming Video Understanding”,恰恰是要解决这三个核心痛点。 Continuous(连续) :意味着模型必须处理无休止的数据流,不能有明确的开始和结束,需要具备增量学习和记忆管理能力。 Interactive(交互) :意味着智能体不是被动的观察者,它可以根据当前的理解,主动发出“指令”,比如请求调整摄像头视角(在模拟环境中)、回放特定片段、或者向视频源(或一个控制接口)提问以获取额外信息。 Long-Horizon(长程) :意味着智能体需要维持长时间的状态,理解跨越多分钟甚至数小时的因果和时序关系,为了一个长远目标(比如“监控整个制造流程是否合规”或“理解一场足球比赛的战术演变”)而持续努力。

这不仅仅是把现有的视频理解模型跑在流式数据上那么简单。它涉及一整套新的评估基准、智能体架构、训练范式和环境设计。这也是为什么它被称为“Arena”(竞技场),这是一个供不同智能体策略进行测试、比较和进化的复杂环境。理解了这一点,我们就能明白为什么“Agentic RAG”、“Simulink Agentic Toolkit”这些热词会与之关联——它们分别从知识利用和仿真环境构建的角度,为打造这样的智能体提供工具和思路。

2. 解构StreamArena:核心组件与技术栈构想

要构建一个StreamArena,我们不能只停留在概念上,必须拆解出其核心的技术组件。虽然目前可能还没有一个叫“StreamArena”的开源项目,但根据其目标,我们可以勾勒出一个典型的系统架构,它主要由以下几个环环相扣的部分构成。

2.1 流式视频感知与编码层

这是智能体的“眼睛”。与处理完整视频不同,流式处理要求极低的端到端延迟和高效的内存使用。模型不能等到一帧完整编码后再处理,而是需要采用 流式编码器 。

  • 核心选择: 传统的3D CNN(如I3D)在流式场景下计算开销大。更可行的方案是使用 基于Transformer的时空编码器 ,但需要进行流式化改造。例如,使用滑动窗口机制,只对最近的一个时间窗口(如2秒)内的帧序列进行注意力计算。或者,采用 递归神经网络(RNN) 或 状态空间模型(SSM) 的变体,如Mamba,它们天生适合序列建模,能增量式地更新一个隐藏状态来表征历史视觉信息。
  • 实操细节: 视频帧以固定频率(如每秒5帧)送入编码器。编码器输出一个连续的“视觉特征流”。这里的关键是 特征对齐 。由于交互可能发生在任何时刻,智能体需要能快速定位到特征序列中的特定时间点。通常,我们会为每个特征向量打上高精度的时间戳。
  • 避坑点: 直接使用为图像预训练的ViT可能效果不佳,因为缺乏时间建模能力。一个实用的技巧是,在流式初始化时,先缓存几秒的视频,用一个小型网络快速生成初始的时间上下文,再进入真正的流式处理循环。

2.2 智能体决策与交互核心

这是智能体的“大脑”。它接收视觉特征流、内部记忆、以及任务目标,然后决定两个事情:1)当前对视频的理解是什么?2)接下来要执行什么动作?

  • 架构范式: 这非常适合 “感知-规划-行动”循环 或 基于大语言模型(LLM)的智能体框架 。LLM作为核心推理引擎的趋势越来越明显。
    • 方案A(传统强化学习路线): 将视觉特征作为状态,定义一组交互动作(如“聚焦区域A”、“加速播放”、“请求物体识别”),任务奖励由Arena环境给出。这种方法需要大量仿真训练,样本效率低,但可能学到更优的策略。
    • 方案B(LLM驱动路线): 这是当前更热的方向,即“Agentic RAG”思路的延伸。LLM作为中央控制器。视觉特征经过一个“视觉语言适配器”(如一个线性层或小型MLP)被转换成文本标记,与历史对话、任务指令一起构成LLM的输入上下文。LLM输出两种内容:一是对当前视频内容的自然语言描述(理解),二是一个结构化动作命令(交互)。例如,输入:“[视觉特征]... 当前直播画面中,主持人正在展示一个电路板。” LLM输出:“理解:主持人正在讲解电路板的电源模块。动作:{“type”: “zoom”, “region”: [0.3, 0.4, 0.5, 0.6]}”
  • 为什么是LLM路线? 因为它能利用强大的世界知识和推理能力,零样本或少量样本就能理解复杂指令,并生成可解释的动作。这对于研究初期快速构建原型、定义复杂的交互逻辑至关重要。Simulink Agentic Toolkit这类工具,其价值就在于为LLM智能体提供了与仿真环境(Simulink模型)的标准交互接口,可以无缝迁移到视频流环境。

2.3 记忆与状态管理模块

长程理解的关键在于记忆。智能体不能是“金鱼脑”,它必须记住几分钟前发生的关键事件。

  • 记忆类型:
    1. 工作记忆: 存储最近几十秒的高细节特征和事件,用于实时推理。通常用固定长度的队列或RNN状态实现。
    2. 长期记忆: 存储压缩后的关键事件摘要、对象轨迹、语义概念。这通常是一个可外部检索的存储器,比如一个向量数据库。这就是“Agentic RAG”的用武之地。智能体可以将当前场景与长期记忆中的片段进行检索比对,实现关联推理。例如,看到球员A现在传球,能记起他10分钟前的一次类似传球失误。
  • 实现技巧: 长期记忆的更新策略是个难点。是每秒钟都摘要一次?还是检测到“重要事件”(通过场景变化检测或LLM判断)才更新?实践中,可以采用双触发机制:定时(如每30秒)强制摘要,加上基于视觉/语义变化度的自适应触发。

2.4 交互动作空间与环境反馈

这是智能体与视频流“世界”交互的接口。动作需要被映射到视频流控制器或仿真环境的具体API上。

  • 典型动作集:
    • seek(timestamp) : 跳转到指定时间点(对于可回放的流)。
    • change_playback_rate(speed) : 改变播放速度。
    • focus_region(x1,y1,x2,y2) : 指示虚拟摄像头关注某个区域,后续视觉特征可能主要来自该区域。
    • query(query_text) : 向一个假设的“视频元信息接口”提问,例如“当前说话者的名字是什么?”(这在实际系统中可能需要接入ASR和知识库)。
    • request_analysis(analysis_type) : 请求对当前画面进行特定深度分析,如“检测所有工具并列出”。
  • 环境反馈: 执行动作后,环境需要给出反馈。例如,执行 focus_region 后,后续的视觉特征流应该主要来自该区域。执行 query 后,环境需要返回答案文本。这个反馈环是智能体学习有效交互策略的基础。

3. 构建一个最小可行原型:从零到一的实践路径

理论讲了很多,现在我们动手搭建一个最简单的StreamArena智能体原型。我们将采用LLM驱动的方案,因为它开发迭代最快。我们的目标是:让智能体观看一个技术产品发布会的直播流,并能够根据我们的指令与之交互。

3.1 环境搭建与数据模拟

由于真实的、可交互的视频流API难以获取,我们首先构建一个仿真环境。

# stream_arena_simulator.py
import cv2
import time
from typing import Dict, Any, List
import numpy as np

class StreamingVideoSimulator:
    """模拟一个可交互的视频流源"""
    def __init__(self, video_path: str):
        self.cap = cv2.VideoCapture(video_path)
        self.fps = self.cap.get(cv2.CAP_PROP_FPS)
        self.current_frame_idx = 0
        self.playback_speed = 1.0 # 1x正常速度
        self.focus_region = None # (x1, y1, x2, y2) 归一化坐标
        self.is_paused = False

    def get_current_frame(self):
        """获取当前帧,应用聚焦区域(模拟)"""
        if self.is_paused:
            # 暂停时返回上一帧
            ret, frame = True, self.last_frame
        else:
            ret, frame = self.cap.read()
            if not ret:
                return None
            self.last_frame = frame
            self.current_frame_idx += int(self.playback_speed)

        # 模拟聚焦:如果设置了区域,则裁剪并放大该区域(这里简单返回原图,实际可裁剪)
        if self.focus_region:
            h, w = frame.shape[:2]
            x1, y1, x2, y2 = [int(coord * dim) for coord, dim in zip(self.focus_region, [w, h, w, h])]
            cropped = frame[y1:y2, x1:x2]
            # 简单上采样回原尺寸(模拟变焦)
            frame = cv2.resize(cropped, (w, h), interpolation=cv2.INTER_LINEAR)
        return frame

    def execute_action(self, action: Dict[str, Any]):
        """执行智能体发出的动作"""
        action_type = action.get("type")
        if action_type == "seek":
            target_idx = int(action["timestamp"] * self.fps)
            self.cap.set(cv2.CAP_PROP_POS_FRAMES, target_idx)
            self.current_frame_idx = target_idx
            return f"跳转到 {action['timestamp']} 秒"
        elif action_type == "change_speed":
            self.playback_speed = action["speed"]
            return f"播放速度调整为 {action['speed']}x"
        elif action_type == "focus":
            self.focus_region = action["region"]
            return f"聚焦区域设置为 {action['region']}"
        elif action_type == "pause":
            self.is_paused = True
            return "视频已暂停"
        elif action_type == "resume":
            self.is_paused = False
            return "视频已恢复播放"
        else:
            return f"未知动作: {action_type}"

    def get_video_info(self) -> str:
        """模拟查询视频元信息"""
        # 这里可以返回一些预设的元数据,如演讲者列表、产品目录
        info = {
            "title": "TechProduct Launch 2024",
            "speakers": ["John Doe (CEO)", "Jane Smith (CTO)"],
            "products": ["AlphaPhone", "BetaTab", "GammaWatch"]
        }
        return str(info)

3.2 智能体核心:LLM与视觉编码集成

我们使用轻量级的视觉编码器(如CLIP的ViT)和开源LLM(如Qwen2.5-7B-Instruct)来搭建核心。

# agent_core.py
import torch
from transformers import AutoProcessor, AutoModelForVision2Seq, LlamaForCausalLM, AutoTokenizer
from PIL import Image
import base64
from io import BytesIO

class StreamArenaAgent:
    def __init__(self, llm_model_name="Qwen/Qwen2.5-7B-Instruct", vision_model_name="openai/clip-vit-base-patch32"):
        # 1. 视觉编码器 (使用CLIP,简单高效)
        from transformers import CLIPProcessor, CLIPModel
        self.clip_processor = CLIPProcessor.from_pretrained(vision_model_name)
        self.clip_model = CLIPModel.from_p_pretrained(vision_model_name)
        self.clip_model.eval()

        # 2. LLM (这里示例使用文本LLM,实际需多模态LLM或适配器)
        self.llm_tokenizer = AutoTokenizer.from_pretrained(llm_model_name)
        self.llm_model = LlamaForCausalLM.from_pretrained(llm_model_name, torch_dtype=torch.float16, device_map="auto")
        # 注意:实际中需要将视觉特征投影到LLM的嵌入空间,这里为简化,我们用文本描述代替特征。

        # 3. 记忆
        self.short_term_memory = [] # 存储最近N个视觉描述
        self.long_term_memory = [] # 存储关键事件摘要
        self.max_short_term = 10

    def encode_frame(self, frame_image: Image.Image) -> str:
        """将一帧图像编码为文本描述(简化版,实际应输出特征向量)"""
        # 使用CLIP的文本编码器来“描述”图像?不,CLIP本身不生成描述。
        # 更合理的简化:使用一个图像描述生成模型(如BLIP),或直接使用LLM的多模态版本。
        # 此处为演示,我们模拟一个描述生成。
        inputs = self.clip_processor(images=frame_image, return_tensors="pt")
        with torch.no_grad():
            image_features = self.clip_model.get_image_features(**inputs)
        # 实际中,image_features需要与文本特征对齐或输入给LLM。这里我们跳过,用一个模拟函数。
        return self._simulate_caption(frame_image)

    def _simulate_caption(self, image):
        """模拟生成图像描述 - 实际项目需替换为真正的图像描述模型"""
        # 这里可以接入BLIP、LLaVA等模型
        # 返回模拟描述
        return "画面显示一位演讲者站在舞台上,背后是大屏幕,屏幕上显示着'AlphaPhone'的字样和产品图片。"

    def update_memory(self, current_description: str, timestamp: float):
        """更新短期记忆,并判断是否存入长期记忆"""
        self.short_term_memory.append((timestamp, current_description))
        if len(self.short_term_memory) > self.max_short_term:
            self.short_term_memory.pop(0)

        # 简单的长期记忆更新策略:如果描述包含关键词,则存档
        keywords = ["新品", "发布", "价格", "演示", "故障"]
        if any(keyword in current_description for keyword in keywords):
            summary = f"[{timestamp}s] {current_description}"
            self.long_term_memory.append(summary)

    def reason_and_act(self, task_instruction: str, current_frame_desc: str, env_feedback: str = "") -> Dict:
        """核心推理:根据任务、当前画面、记忆和反馈,决定理解和动作"""
        # 构建给LLM的提示词
        memory_context = "\n".join([f"- {desc}" for _, desc in self.short_term_memory[-3:]]) # 最近3条短期记忆
        long_term_context = "\n".join(self.long_term_memory[-5:]) # 最近5条长期记忆

        prompt = f"""你是一个流式视频理解智能体。你的任务是:{task_instruction}

环境反馈:{env_feedback}

近期画面记忆:
{memory_context}

过往关键事件:
{long_term_context}

当前画面描述:{current_frame_desc}

请按以下格式输出:
理解:[你对当前视频内容的理解,联系任务和记忆]
动作:{{"type": "动作类型", "参数": "值"}}  # 如果没有需要执行的动作,则 type 为 "none"

可用的动作类型:
- seek(timestamp): 跳转到指定秒数
- change_speed(speed): 调整播放速度 (0.5, 1, 2)
- focus(region): 聚焦区域,region为[x1,y1,x2,y2]归一化坐标
- pause: 暂停
- resume: 恢复播放
- query: 向环境提问,参数是问题文本
"""
        # 调用LLM生成回复(此处为模拟)
        llm_response = self._call_llm_simulate(prompt)
        # 解析响应,提取“理解”和“动作”部分
        lines = llm_response.strip().split('\n')
        understanding = ""
        action_str = "{\"type\": \"none\"}"
        for line in lines:
            if line.startswith("理解:"):
                understanding = line[3:].strip()
            elif line.startswith("动作:"):
                action_str = line[3:].strip()

        import json
        try:
            action = json.loads(action_str)
        except json.JSONDecodeError:
            action = {"type": "none"}

        return {"understanding": understanding, "action": action}

    def _call_llm_simulate(self, prompt):
        """模拟LLM调用 - 实际项目需接入真实LLM API或本地模型"""
        # 这是一个非常简单的规则模拟,真实情况复杂得多。
        if "价格" in prompt:
            return """理解:当前演讲者可能在介绍产品,但未提及价格。根据任务需要找到价格信息,我建议回放到之前可能公布价格的环节。
动作:{"type": "seek", "timestamp": 120}""" # 假设120秒处有价格
        else:
            return """理解:演讲者正在介绍AlphaPhone的外观设计。任务要求跟踪新品亮点,当前画面符合。
动作:{"type": "none"}"""

3.3 运行主循环与交互测试

将环境与智能体连接起来,形成一个完整的交互循环。

# main_demo.py
from stream_arena_simulator import StreamingVideoSimulator
from agent_core import StreamArenaAgent
import cv2
from PIL import Image
import time

def main():
    # 初始化
    simulator = StreamingVideoSimulator("demo_launch.mp4") # 准备一个示例视频
    agent = StreamArenaAgent()
    task = "持续观看产品发布会直播,找出并记住所有新产品的名称和其核心卖点。如果看到价格信息,请特别标注。"

    print(f"任务: {task}")
    print("启动流式视频理解智能体...")

    env_feedback = ""
    for i in range(100): # 模拟处理100帧
        frame = simulator.get_current_frame()
        if frame is None:
            break

        # 将OpenCV BGR帧转为PIL RGB图像
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        pil_image = Image.fromarray(frame_rgb)

        # 智能体处理
        frame_desc = agent.encode_frame(pil_image)
        agent.update_memory(frame_desc, i / simulator.fps) # 模拟时间戳

        result = agent.reason_and_act(task, frame_desc, env_feedback)

        print(f"\n--- 帧 {i} ---")
        print(f"画面: {frame_desc[:50]}...")
        print(f"智能体理解: {result['understanding']}")
        print(f"智能体动作: {result['action']}")

        # 执行动作并获取环境反馈
        if result['action']['type'] != 'none':
            env_feedback = simulator.execute_action(result['action'])
            print(f"环境反馈: {env_feedback")
        else:
            env_feedback = ""

        # 简单显示画面(可选)
        cv2.imshow('Stream Arena Demo', frame)
        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

        # 模拟实时流,控制处理速度
        time.sleep(0.1 / simulator.playback_speed)

    cv2.destroyAllWindows()
    print("\n长期记忆总结:")
    for mem in agent.long_term_memory:
        print(f"  {mem}")

if __name__ == "__main__":
    main()

这个原型虽然简陋,但它清晰地展示了StreamArena智能体的工作流: 感知(编码帧)-> 记忆更新 -> 推理(LLM)-> 决策(生成动作)-> 执行与环境交互 -> 反馈循环 。你可以替换其中的模拟函数,接入真实的视觉描述模型(如LLaVA)和强大的LLM(如GPT-4V或本地部署的Qwen2-VL),立刻就能得到一个有实际能力的原型。

4. 从原型到生产:关键技术挑战与优化方向

搭建出原型只是第一步,要让StreamArena智能体真正强大、可靠,我们还需要攻克一系列工程和研究上的挑战。这些挑战也正是当前“Agentic Streaming Video”领域的前沿问题。

4.1 延迟与效率的极致平衡

流式理解的核心是“实时”。智能体思考的时间如果比事件发生的时间还长,那就失去了意义。

  • 挑战: 视觉编码和LLM推理都是计算密集型任务。高精度模型(如ViT-Large、千亿参数LLM)的延迟无法满足实时交互需求(通常要求<500ms端到端延迟)。
  • 优化策略:
    1. 模型蒸馏与量化: 使用小尺寸但能力强的模型。例如,采用蒸馏后的MiniGPT-4或MobileVLM作为视觉理解器,使用量化到INT4甚至INT2的LLM(如用AWQ、GPTQ量化后的Qwen或Llama模型)作为推理核心。在精度损失可接受的范围内,换取数倍的推理加速。
    2. 异步流水线与推测执行: 不要等LLM完全输出再执行动作。可以将智能体的推理-动作循环设计成异步的。视觉编码器持续工作,将特征送入队列。LLM并行消费队列进行推理。甚至可以采用“推测执行”,在LLM输出完整动作前,先执行高概率的初始动作(如继续维持当前聚焦区域)。
    3. 分层处理与自适应计算: 并非每一帧都需要深度分析。可以运行一个轻量级的“显著性检测”模型,只有当检测到画面有重大变化(如新人物入场、出现文字标题)时,才触发重型LLM进行深度理解和决策。大部分时间,智能体可以处于低功耗的“跟踪”模式。

4.2 长程记忆的精准检索与遗忘

智能体不能变成“记忆垃圾场”,如何高效存储和检索是关键。

  • 挑战: 视频流信息量巨大,全部记住不可能也不必要。如何判断什么是“关键事件”?如何从海量记忆中快速找到与当前场景相关的信息?
  • 解决方案:
    1. 基于LLM的记忆摘要与索引: 定期(或基于事件触发)将短期记忆喂给一个LLM,让其生成一段凝练的摘要,并提取关键实体(人物、物体、动作)和关系。将这些摘要和实体存入向量数据库(如ChromaDB、Weaviate),并用时间戳和语义标签进行索引。这就是“Agentic RAG”在视频领域的核心应用。
    2. 记忆重要性评分: 设计一个可学习的重要性评分网络。输入当前帧特征、历史记忆和任务目标,输出一个重要性分数。只有高分事件才进入长期记忆。这个网络本身可以通过强化学习来训练,奖励那些后来被证明对完成任务有帮助的记忆存储行为。
    3. 结构化记忆图谱: 不仅仅存储文本摘要,可以构建一个知识图谱。节点是实体,边是时空关系(在X时间后,A做了B动作)。这样的结构化表示更利于进行复杂的多跳推理,比如“找到导致当前故障的初始事件”。

4.3 评估基准与奖励函数的构建

如何评价一个StreamArena智能体的好坏?这本身就是一个开放的研究问题。

  • 挑战: 不同于图像分类有准确率,目标检测有mAP,流式交互式理解缺乏公认的评估指标。
  • 构建思路:
    1. 基于任务的综合评分: 设计一系列具有明确终局目标的任务。例如,在“监控装配线”任务中,目标是“发现所有违规操作”。评估指标可以包括: 发现率 (找到的违规数/总违规数)、 平均发现时间 (从违规发生到智能体报告的时间)、 误报率 。这需要构建高质量的仿真环境或标注极其精细的真实数据集。
    2. 交互效率指标: 衡量智能体“聪明”程度的指标。例如, 任务完成所需的交互次数 。一个更聪明的智能体应该用更少的 seek 、 query 动作就能找到所需信息。或者, 信息获取的精确度 ,比如它请求聚焦的区域是否正好包含了关键物体。
    3. 仿真环境的必要性: 这就是“Simulink Agentic Toolkit”这类工具的价值。它们提供了可编程的、确定性的仿真环境,可以低成本、大规模地生成各种测试场景和标注数据,用于训练和评估智能体。在视频领域,我们需要类似的“视频流仿真环境”,可以程序化地生成包含特定事件序列、可交互控制的视频流。

4.4 多模态理解的深度融合

当前很多方法还是“视觉编码器 + LLM”的松耦合,视觉特征只是被简单投影为LLM的输入标记。

  • 未来方向: 真正的深度融合需要 下一代多模态大模型 具备原生的流式处理能力。模型架构上需要支持:
    • 任意分辨率与长宽比输入: 以适应不同的聚焦区域和画面裁剪。
    • 时序建模原生支持: 不再是处理单个图像或固定长度的视频片段,而是能处理理论上无限长的视觉标记序列,并高效更新内部表示。
    • 交错生成视觉与文本: 智能体不仅输出文本动作,未来或许能直接输出对视频流的“编辑指令”或生成辅助理解的视觉标记(如在原视频上画框、高亮)。

在我自己的实验过程中,最大的体会是 不要一开始就追求完美的端到端模型 。采用 松耦合的模块化设计 ,比如独立的视觉描述模型、独立的LLM、独立的内存向量库,虽然会引入一些信息损失和延迟,但在开发初期极大地提升了调试效率和灵活性。你可以单独优化视觉描述模块的准确性,单独测试不同LLM的推理能力,单独改进记忆检索的策略。当每个模块都相对稳定后,再考虑如何将它们更紧密地集成,甚至用端到端的方式重新训练。这种“先分解,再集成”的思路,对于攻克StreamArena这类复杂系统问题,往往更加务实和有效。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐