转载自公众号:敢敢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 没有训练任何新模型,但它用一套精巧的分层架构,把不可部署的方案变成了可以装进眼镜里的方案——这才是面向生产环境的多模态智能体应有的样子。
155