• 正文
  • 相关推荐
申请入驻 产业图谱

十分钟教程 | VisualClaw深度解读:把流式视频多模态智能体的成本砍到2%,是怎么做到的?

07/23 09:17
155
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

转载自公众号:敢敢AUTOHUB

1. 问题的起点:流式视频智能体的"不可能三角"

在过去两年,视觉语言模型(VLM)逐渐成为多模态智能体的通用接口。把图片塞进去、把视频帧塞进去,模型可以回答你"画面里发生了什么"、"接下来应该做什么"。但是当应用场景从离线问答切换到流式视频——比如戴在脸上的 AI 眼镜、24 小时持续运转的安防监控、长时间记录的可穿戴设备——三个相互矛盾的约束会同时跳出来:

第一是成本。前沿 VLM 按 token 计费,而视频帧编码后会迅速膨胀到几十万 token。以 30 分钟长视频为例,按 1 fps 上传,单次问答就要消耗大约 192 万输入 token,单次 API 调用成本就要数美元。第二是延迟。流式场景下用户期待秒级响应,但全帧上传到云端,再加上推理时间和移动网络的抖动,根本无法满足实时交互。第三是适应性。部署完成的智能体大多是"出厂即定型"的,遇到训练时没见过的失败模式只能继续犯错,没有从经验中改进的能力。

VisualClaw 这套来自 UC Santa Cruz、UNC、Google 和 UC Berkeley 联合团队的系统,给出了一个让三者同时收敛的工程方案。它的核心结论是:流式视频多模态智能体的 API 成本,可以从原来的水平降低 98%;同时通过失败积累驱动的自我进化机制,准确率反而提升 3.85% 到 15.80% 不等。整个过程没有动模型权重,全靠技能库和记忆库的演化完成。

项目主页:https://github.com/UCSC-VLAA/VisualClaw

上图来自 VisualClaw 论文首页,展示了系统的典型应用场景:AR 眼镜在购物场景中实时编码视频流,云端通过持续进化的记忆与技能库给出个性化回答和操作。左侧是真实使用场景的截帧时间线,可以看到只有少数关键帧(黄色标记)被识别为"值得上传",绝大多数帧在设备本地就被丢弃。

2. 三时间尺度的精妙设计

VisualClaw 把所有处理步骤拆成了三种时间尺度来执行,这是整个系统设计哲学的灵魂。边缘侧的逐帧过滤跑在微秒级,CPU 就能完成;云端的逐问题检索跑在毫秒级,由 VLM 处理;离线的逐会话进化跑在分钟级,由专门的进化器分析失败案例并更新技能库。这种分层架构把计算资源精确分配到最能发挥价值的时间窗口里,避免了"用一把锤子敲所有钉子"的资源浪费。

更关键的是,三层之间通过明确的接口解耦。边缘层只需要决定"这一帧要不要传",不需要理解语义;云端只需要在"上传的关键帧"基础上做推理,不需要看到完整视频流;离线进化器只需要从失败案例中提炼可复用的策略卡片,不需要修改 VLM 的任何权重。这种解耦让系统具备了三个不同维度的可扩展性:边缘可以换硬件、云端可以换 VLM 后端、离线进化器可以换更强的语言模型,三者互不影响。

这张架构图详细展开了三时间尺度系统的内部数据流。设备端通过级联门控大量过滤冗余帧,云端进行 Hot/Cold 双路技能注入并协同记忆库和技能库生成答案,离线端通过失败触发的进化器实现技能库自我更新。论文中给出的形式化数学表达把整个过程归纳为一个条件概率:

也就是说,答案  是冻结权重  的 VLM 在给定问题 、级联门控过滤后的关键帧集合 、检索到的 Top- 热技能 、冷技能目录 、以及从记忆库检索到的片段  这五个条件共同作用下采样得到的。这里  三个组件分别在三种不同的时间尺度上更新: 在每帧(约 ,边缘)更新, 和  在每问题(约 ,云端)更新,进化器在每会话(分钟级,离线)更新。

3. 边缘端:三步级联过滤掉 98% 的帧

整个成本压降的第一道防线,是部署在设备端的级联过滤器。它由三个 O(1) 复杂度的阶段组成,全程不需要 GPU、不需要深度网络、不依赖未来帧,完全可以跑在手机或眼镜的 CPU 上。论文给出的实测数据是:单帧处理时间小于 10 毫秒,CPU 占用极低,可以维持每秒 100 帧以上的吞吐。

3.1 第一步:dHash 感知哈希去重

第一道关卡叫 dHash,全称 difference hash。它的逻辑非常直接:把当前帧缩放到一个  的灰度小图,比较相邻像素的明暗关系,生成一个 64 位的哈希值。如果当前帧的哈希值与上一帧的汉明距离(即不同比特位的数量),就判定为视觉上近似重复,直接丢弃。这一步主要解决摄像头抖动、画面静止、轻微平移等"看起来变了但实际没变"的情况。在 VisualClaw 的开源实现中,这部分代码非常简洁:

def _dhash(jpeg_bytes: bytes, hash_size: int = 8) -> Optional[int]:
    """Compute 64-bit dHash from JPEG bytes. Returns None on failure."""
    from PIL import Image
    import io, numpy as np
    img = Image.open(io.BytesIO(jpeg_bytes)).convert("L")
    resized = img.resize((hash_size + 1, hash_size), Image.LANCZOS)
    pixels = list(np.array(resized).flat)
    h = 0
    for row in range(hash_size):
        for col in range(hash_size):
            idx = row * (hash_size + 1) + col
            if pixels[idx] < pixels[idx + 1]:
                h |= 1 << (row * hash_size + col)
    return h

class FrameHasher:
    def should_drop(self, jpeg_bytes: bytes) -> bool:
        h = _dhash(jpeg_bytes)
        if self._last_hash is not None:
            dist = bin(h ^ self._last_hash).count("1")
            if dist <= self._threshold:
                self._dropped += 1
                return True
        self._last_hash = h
        return False

这段代码的精妙之处在于"用最少的算力获得最大的过滤收益"。整张图片被压缩到 72 个像素,比较的是相邻像素的明暗序,对光照变化、JPEG 压缩噪声、轻微旋转都有天然鲁棒性。在实际部署中,连续录制的视频流大约有 40% 到 70% 的帧会在这一步直接被丢弃,连后续的特征提取都不会触发。

3.2 第二步:128 维 CPU 编码器

通过 dHash 去重的帧,会进入第二阶段:一个手工设计的 128 维特征编码器。这里没有用任何深度网络,而是把图像的统计特征显式抽出来——HSV 颜色直方图占 48 维、空间亮度网格占 64 维、边缘密度占 8 维、纹理能量占 8 维。这些特征加起来正好  维,归一化为单位向量  后可以直接用余弦距离  做比较。完整的实现如下:

def _extract_features(self, img: np.ndarray) -> np.ndarray:
    import cv2
    features = []
    # 1. HSV color histogram (48 dims)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    for ch, bins, rng in [(0, 16, [0, 180]), (1, 16, [0, 256]), (2, 16, [0, 256])]:
        h = cv2.calcHist([hsv], [ch], None, [bins], rng).flatten().astype(np.float32)
        if h.sum() > 0:
            h /= h.sum()
        features.append(h)
    # 2. Spatial luminance grid (64 dims: 8×8 block averages)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    bh, bw = gray.shape[0] // 8, gray.shape[1] // 8
    grid = np.zeros(64, dtype=np.float32)
    for r in range(8):
        for c in range(8):
            grid[r*8+c] = gray[r*bh:(r+1)*bh, c*bw:(c+1)*bw].mean() / 255.0
    features.append(grid)
    # 3. Edge density per block (8 dims, Canny)
    edges = cv2.Canny(gray, 50, 150)
    # 4. Texture energy (8 dims, local variance)
    # ...
    vec = np.concatenate(features).astype(np.float32)
    return vec / (np.linalg.norm(vec) + 1e-8)

为什么不用 CLIP 这类预训练编码器?因为部署在眼镜或手机的边缘设备上,每多一个矩阵乘法都是真金白银的功耗。手工特征虽然语义能力不如深度模型,但对"场景是否切换"这个二分类问题已经足够,而且功耗几乎可以忽略。在 VisualClaw 开源实现里,这段代码会先尝试 OpenCV 路径;如果 OpenCV 不可用,会自动降级到 Pillow 实现的纯 numpy 版本,保证在最简环境下也能跑起来。

3.3 第三步:自适应变化门控

通过前两步过滤的帧,最后会进入第三阶段:变化门控。这里维护两个参考向量——上一次 MAJOR 帧(关键帧)的特征  和上一次 MINOR 帧(次要帧)的特征 。当前帧的特征  与参考向量做余弦距离比较,得到一个  之间的距离值。如果 ,标记为 MAJOR,触发上传到 VLM;如果 ,标记为 MINOR,更新参考向量但不上传;如果 ,直接 SKIP。

最巧妙的地方在于阈值是动态的。系统会用 EMA(指数移动平均)持续跟踪场景的"忙碌度":,其中  是平滑系数。根据当前的  与  的比值,系统自动给阈值乘上一个 factor 。在静止场景下(),factor 取上限 ,阈值被放大,避免传感器噪声触发误判;在动态场景下(),factor 取下限 ,阈值被压低,确保不错过关键转折。还有一个温度衰减机制:如果长时间没有触发 MAJOR,阈值会随时间自动衰减 ,防止系统在缓慢变化的场景里完全收不到帧。这部分的核心实现如下:

class SceneComplexityTracker:
    """Rolling EMA of cosine distances → multiplicative factor for thresholds.
    Static scene (low EMA)  → factor > 1.0 → raise thresholds.
    Dynamic scene (high EMA) → factor < 1.0 → lower thresholds.
    """
    def update(self, distance: float) -> None:
        if self._ema is None:
            self._ema = distance
        else:
            self._ema = self._alpha * distance + (1.0 - self._alpha) * self._ema
        self._n += 1

    def factor(self) -> float:
        if self._ema is None or self._n < 3 or self._tau_major <= 0:
            return 1.0
        ratio = self._ema / self._tau_major
        if ratio < 0.25:
            return self._low      # static scene → raise threshold
        if ratio > 1.0:
            return self._high     # dynamic scene → lower threshold
        t = (ratio - 0.25) / 0.75
        return self._low + t * (self._high - self._low)

三步走下来,每个问题平均只需要上传 1.13 到 5.41 帧。在 30 分钟的长视频上,过滤比例可以达到 99.3%。这意味着原本要传 3600 帧的 1 小时 AI 眼镜直播,现在只需要 5 到 20 次上传——这是从"不可能部署"到"移动数据套餐就能跑"的工程跨越。

4. 云端:技能库的冷热双路注入

视觉那边的成本压下来了,文本提示词那边的成本却容易反弹。VisualClaw 维护一个不断生长的技能库 S,每个技能是一张简短的 Markdown 卡片,包含名称、一行描述、编号的操作步骤、以及明确的反模式提示(即"不要这样做"的负面示例)。如果每次回答都把整个技能库平铺到 Prompt 里,token 开销会随着技能数量线性增长——当技能库累积到几十甚至上百条时,真正有用的信号会被噪音淹没。

VisualClaw 给出的解法是 Hot/Cold 双路注入。系统先用句子嵌入模型(all-MiniLM-L6-v2)对当前问题做编码,然后对所有技能的描述也做编码,按余弦相似度排序。排名前  的技能进入"热路",整张卡片包括操作步骤和反模式全部内联进 Prompt:;其余技能则进入"冷路",只保留名称和一行描述构成的目录:。如果 VLM 在推理过程中发现冷路目录里有更合适的技能名,可以主动按名调取完整版本。这样一来,每个问题的 Prompt 成本只由  决定,与技能库的总规模  无关。

# 简化的 Hot/Cold 双路注入逻辑
def inject_skills(question: str, skill_bank: list[Skill], k: int = 3) -> str:
    # Hot tier: top-k full skill bodies
    q_emb = sentence_encoder.encode(question)
    scores = [(s, cosine(q_emb, s.embedding)) for s in skill_bank]
    scores.sort(key=lambda x: -x[1])
    hot_skills = scores[:k]
    cold_skills = scores[k:]

    prompt = "## Available Skills (full)n"
    for skill, _ in hot_skills:
        prompt += f"### {skill.name}n{skill.body}nn"

    prompt += "## Other Skills (catalog only)n"
    for skill, _ in cold_skills:
        prompt += f"- {skill.name}: {skill.description}n"
    return prompt

论文里的消融实验给出了一个反直觉的发现: 的最优取值与 VLM 后端的强弱有关。在较弱的 Gemini 3 Flash 上,注入全部技能(All)效果最好,平均准确率达到 ;但在更强的 GPT-5.2 上, 才是最优值(),注入全部技能反而会下降到 。论文的解释是:强模型对无关上下文更敏感,过多的技能描述反而会干扰推理,类似"信息过载"的效应。

这个现象给工程实践提供了重要启示:当技能库需要在多个 VLM 后端之间迁移时,应当默认采用  这种相对保守的注入策略;只有当技能库是为单一弱后端专门训练时,才适合全量注入。

5. 离线进化:让记忆驱动技能演化,而不是直接喂给 VLM

整个系统最令人兴奋的部分是元进化机制。传统的记忆增强智能体大多采用"答题前把检索到的记忆拼接到 Prompt 里"的做法,但这种做法在多模态场景下有两个问题:第一,拼接的原始记忆变成了"VLM 看一次就过期"的狭窄上下文,无法持续产生价值;第二,每次答题都要付出额外的 Prompt 成本,与技能库注入叠加后开销难以控制。

VisualClaw 换了一个思路:让记忆去影响低频的"技能进化器",而不是高频的"答案生成器"。具体来说,系统维护一个情景记忆库 ,保存所有回答正确的高置信度案例,用 dense 向量索引。当累积失败 (默认 )时,离线进化器会被唤醒,从记忆库里检索与最近失败相关的成功案例 ,然后生成新的候选技能。

Cat. 变体把记忆直接拼接到进化 Prompt 末尾:

Guide 变体则在拼接前加入一段引导指令 ,要求进化器从失败相关上下文中提炼可复用的泛化策略:

其中  是 LLM 进化器, 是基础进化 Prompt, 是最近的失败批次, 是检索到的记忆, 表示文本拼接。技能库的更新规则是 ,再经过卫生过滤器  剔除冗余与低效条目。

论文给出了两种记忆注入变体。Cat. 变体(concat)直接把记忆拼接到进化 Prompt 后面,让进化器自行消化;Guide 变体则在拼接前加一段引导指令,明确告诉进化器"要从这些案例里提炼出泛化的推理模式,避免被场景特定细节带偏"。实验数据显示,两种变体在不同任务上各有所长:Guide 在静态视频问答上更稳,因为它逼迫进化器输出紧凑的推理范式;Cat. 在 Agent 工作流上更强,因为后端有编辑和验证循环,能把原始记忆当成结构化决策的素材来消化。

进化器本身是用 Claude Haiku 4.5 实现的,运行在 AWS Bedrock 上。从开源代码里可以看到失败样本的数据结构:

@dataclass
class FailedSample:
    """A failed turn used to drive skill evolution.
    Supports both gateway-style and visualclaw-test style field names.
    """
    prompt:         str                    = ""
    response:       str                    = ""
    images:         list[ImageMeta] | None = None
    image_descs:    list[str] | None       = None
    prompt_text:    str                    = ""   # alias for prompt
    response_text:  str                    = ""   # alias for response
    reward:         float                  = 0.0

每条失败样本包含原始 Prompt、VLM 的错误响应、相关图像的元数据和文本描述、以及奖励值。进化器会把这些信息组装成结构化的失败档案,让 Claude Haiku 分析"为什么失败",然后输出新的技能卡片。生成的技能卡片要通过两道卫生过滤器:F1 在进化时用 token-Jaccard 距离做去重,避免重复造轮子;F2 则跟踪每条技能的命中准确率,周期性地裁掉拖后腿的条目,防止技能库膨胀失控。

6. 工程实现:Gateway 模式的模块化设计

VisualClaw 不只是一篇论文,它的开源代码仓库展示了一套生产级的工程架构。整个系统采用 Gateway 模式部署:智能体框架(比如 Claude Code、Codex CLI)将所有 LLM 请求转发到 VisualClaw Gateway(默认监听 30100 端口),Gateway 在请求前后分别执行 Pre Pipeline 和 Post Pipeline,完成多模态预处理、技能注入、记忆检索、轨迹收集等动作。这种设计让 VisualClaw 可以作为透明中间件接入任意智能体框架,不需要修改上层应用代码。

Agent Framework
    │
    │  POST /v1/chat/completions  (Proxy 模式)
    │  POST /v1/skill/inject      (Plugin 直连模式)
    ▼
┌─────────────────────────────────────────────────────┐
│              Gateway  (port 30100)                   │
│                                                      │
│  Pre Pipeline (顺序执行)                              │
│  multimodal_image → multimodal_video → memory        │
│       → skill → rl                                   │
│                    │                                 │
│             Upstream LLM                             │
│                    │                                 │
│  Post Pipeline                                       │
│  skill (first) → memory ‖ rl ‖ multimodal ‖          │
│                    governance (并行)                  │
│                    │                                 │
│  SessionStore  ────┘                                 │
└─────────────────────────────────────────────────────┘

Pre Pipeline 由五个模块组成,按固定顺序串行执行。multimodal/image 负责从请求里提取图像,multimodal/video 负责注入关键帧摘要,memory 负责检索相关记忆,skill 负责检索技能并写入 skill_generation 元数据,rl 负责根据 RL 训练状态决定使用哪个模型版本。每个模块都通过 importlib 动态加载,任何一个模块失败都不会阻塞整个 Gateway——这是生产环境必备的鲁棒性。

Post Pipeline 的设计更精巧。skill/collect 必须在所有其他 Post 处理器之前完成,因为它会更新 skill_generation 计数器;如果让 RL 模块与 skill 模块并行执行,就会出现"训练样本拿到旧的 skill_generation"的竞争条件,导致 RL 训练混入过期数据。其余模块(memory、rl、multimodal、governance)则用 asyncio.gather 并行执行,最大化 Post 阶段的吞吐。

视频管道是整个系统最复杂的部分,它被设计成五个阶段,前两个跑在边缘端,后三个跑在服务器端:

边缘端(手机或眼镜):
  Stage 1: FrameHasher         — dHash 64-bit,汉明距离 ≤ 阈值 → DROP
  Stage 2: LightweightEncoder  — 128 维 HSV + 亮度 + 边缘 + 纹理特征

服务器端(Gateway):
  Stage 3: ChangeGate     — 余弦距离 → SKIP / MINOR / MAJOR
  Stage 4: StateMemory    — 关键帧环形缓冲,按会话隔离
  Stage 5: ContextBuilder — 关键帧 + 描述 → LLM 上下文消息

VLM 只在 MAJOR 事件触发时才被调用,边缘端处理延迟控制在 5 毫秒以内。整套系统在 Meta Ray-Ban 智能眼镜的实测中可以维持稳定的 1 fps 输入速率,端到端延迟主要由云端往返决定,与级联本身无关。

7. 案例分析:技能与记忆如何纠错

数据图表之外,论文还给出了几个具体的失败救援案例,把"帧过滤 + 技能纠错 + 记忆纠错"的协同效应展示得非常清楚。

第一个案例来自 EgoSchema。180 帧的视频流经级联门控后只选出 1 个 MAJOR 帧(压缩比 1/180)上传到云端 VLM。基线 VLM 看到这一帧的第一反应是给出错误答案 C("picks paint",挑选油漆),它锁定了画面上最显眼的名词,忽略了问题里的目的从句。但技能库里已经进化出了"多项选择精度匹配"、"基于准确性的选项消除"等四条针对这种失败模式的技能。在 Hot Tier 注入这些技能后,VLM 重新检视问题与画面的对应关系,最终把答案翻转到正确的 D("loosens color intensity",降低颜色强度)。

第二个案例来自 NextQA,是一个非自车视角的因果推理任务。级联从 36 帧里筛出 1 帧,基线 VLM 选了错误选项 C("moving themselves",他们在移动自己)。这道题的难点在于推理"为什么登山者紧握绳索"——画面里看不到原因,需要常识推理。这里发挥关键作用的不是技能,而是 Memory bank 里两条历史正确答案——"为什么登山者紧握绳索?为了抵抗钟摆摆动"、"为什么骑手紧握把手?为了保持平衡"。这两条记忆提供了"hold-tight → stabilise"的因果推理范式,把答案从 C 翻成了正确的 B("hold for balance",为了保持平衡)。

这两个案例同时说明了一件事:技能和记忆是两套互补的纠错机制。技能擅长处理"答题格式"和"推理范式"层面的系统性偏差,记忆擅长提供"具体场景的因果模式"。两者结合起来,覆盖的失败模式比单一机制要广得多。

8. 结语

VisualClaw 的核心价值不是任何单一的技术点,而是把"如何让流式多模态智能体在真实部署环境里活下去"这个工程问题,拆解成了三个互相支撑的子问题,读完整篇论文和开源代码,最深的感受是:在大模型 API 时代,真正的工程壁垒不在于训出更强的模型,而在于设计能让既有模型可持续运转的系统架构。VisualClaw 没有训练任何新模型,但它用一套精巧的分层架构,把不可部署的方案变成了可以装进眼镜里的方案——这才是面向生产环境的多模态智能体应有的样子。

相关推荐