从0到1搭建具身智能数据飞轮:Ray + Kafka + LanceDB 全链路实战

从0到1搭建具身智能数据飞轮:机器人多模态数据闭环实战

技术栈:Ray + Kafka + LanceDB + Milvus (Zilliz Cloud) + MinIO + Iceberg

在调研具身智能领域的过程中,我发现一个被严重低估的瓶颈------不是模型不够大,不是算力不够强,而是数据管道根本跟不上迭代速度

具身智能的场景数据天然是多模态的:RGB-D 图像、LiDAR 点云、IMU 序列、关节位姿、轨迹路径、ROS Bag......研发团队每天产生数百 GB 原始数据,但三个月后再想找"那个机器人在仓库拐角撞到货架的场景",往往要在成百上千个 Bag 文件里翻找一整天。

带着这个问题,我用几天时间搭了 RobotLoop------一个面向具身智能的机器人多模态数据闭环平台技术验证原型。目标很清晰:验证"从数据采集到特征存储到语义检索"的完整链路是否能在开源技术栈上跑通。

全文围绕三个核心问题展开:

  1. 数据怎么进来? ------ 事件驱动,设备端零耦合
  2. 数据怎么处理? ------ Ray 分布式流水线,Actor 模型解耦
  3. 数据怎么检索? ------ 向量召回 + 结构化过滤,双引擎混合

一、架构全景:11 个服务的编排逻辑

整个系统通过 docker-compose 一键启动,共 11 个服务。下面是简化后的编排核心:

yaml 复制代码
services:
  # 基础设施
  zookeeper / kafka / minio / iceberg-rest
  
  # 初始化任务(一次性)
  kafka-init / minio-init / model-init
  
  # 计算层
  ray-head / ray-worker
  
  # 业务服务
  model-api / api / jupyter
  
  # 监控
  prometheus / grafana

几个关键设计决策:

设计点 实现方式 原因
事件驱动 MinIO Bucket Notification → Kafka 设备端只需上传文件,零消息客户端依赖
初始化分离 kafka-init / minio-init / model-init condition: service_completed_successfully 保证启动顺序
模型管理 CLIP 权重存 MinIO models bucket Worker 启动时自动下载缓存,版本化管理
存储隔离 Iceberg 使用独立 iceberg-warehouse bucket 避免写入触发 Kafka 通知,防止事件循环

MinIO 的通知配置是事件驱动的核心:

yaml 复制代码
minio:
  environment:
    MINIO_NOTIFY_KAFKA_ENABLE_robotloop: "on"
    MINIO_NOTIFY_KAFKA_BROKERS_robotloop: kafka:9092
    MINIO_NOTIFY_KAFKA_TOPIC_robotloop: "raw-data-ingest"

设备端上传 scene_metadata.jsonrobotloop-data bucket,MinIO 自动推送事件到 Kafka raw-data-ingest topic。Ray Worker 消费这个消息,触发完整的数据处理流水线。


二、Ray 计算层:Actor 模型解耦三阶段

数据处理的核心在 ray_pipeline.py。我没有把逻辑写成一个单体脚本,而是用 Ray Actor 把三个职责拆成了独立角色:

2.1 三个 Actor 的职责划分

python 复制代码
@ray.remote
class SceneProcessor:
    """场景处理器:调用模型 API 标注 + CLIP 向量化"""
    
@ray.remote
class LanceDBWriter:
    """列式存储写入器:批量追加到 LanceDB"""
    
@ray.remote
class MilvusIndexer:
    """向量索引器:插入 Milvus 集合"""

为什么用 Actor 而不是普通 Task?

每个 Actor 内部需要持有状态(CLIP 模型句柄、数据库连接、Milvus 客户端),如果用普通 Task,每次调度都要重新初始化,开销巨大。Actor 在创建时完成一次初始化,后续通过 .remote() 调用复用状态。

2.2 SceneProcessor:标注 + 向量化

python 复制代码
@ray.remote
class SceneProcessor:
    def __init__(self):
        # 每个 Actor 实例加载一次 CLIP 模型
        self.model = _load_clip_model(
            MODEL_CACHE_DIR, S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY
        )

    def process(self, scene: Dict) -> Dict:
        # 1. 调用模型 API 获取场景描述和质量分
        resp = requests.post(
            f"{self.model_api_url}/annotate",
            json={"scene_id": scene["scene_id"]},
            timeout=30,
        )
        annot = resp.json()
        scene["scene_description"] = annot.get("description", "")
        scene["quality_score"] = annot.get("quality", 0.0)

        # 2. CLIP 文本编码 -> 512 维语义向量
        desc = scene.get("scene_description", "")
        emb = self.model.encode(desc).astype(np.float32).tolist()
        scene["scene_embedding"] = emb

        # 3. 模拟轨迹编码 -> 256 维(生产可替换为 TCN/Transformer)
        traj = np.random.randn(256).astype(np.float32)
        traj = traj / np.linalg.norm(traj)
        scene["trajectory_embedding"] = traj.tolist()
        
        return scene

CLIP 模型的加载策略值得展开:

python 复制代码
def _load_clip_model(cache_dir: str, endpoint: str, key: str, secret: str):
    model_dir = os.path.join(cache_dir, "clip-ViT-B-32")
    
    if not os.path.exists(model_dir):
        # 首次启动:从 MinIO models bucket 下载
        s3 = boto3.client("s3", endpoint_url=endpoint, ...)
        objects = s3.list_objects_v2(Bucket="models", Prefix="clip-ViT-B-32/")
        for obj in objects["Contents"]:
            s3.download_file("models", obj["Key"], target_path)
    else:
        # 后续启动:直接读取本地缓存
        pass
    
    return SentenceTransformer(model_dir)

模型权重通过 MinIO 版本化管理,Worker 首次启动自动下载,后续重启直接走本地缓存。这意味着扩缩容新节点时,模型自动分发,不需要手动拷贝或挂共享存储。

2.3 MilvusIndexer:向量集合的自动管理

python 复制代码
@ray.remote
class MilvusIndexer:
    def __init__(self):
        self.client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
        self._ensure_collection()

    def _ensure_collection(self):
        """集合不存在则自动创建,schema 不匹配则重建"""
        if self.client.has_collection(self.collection_name):
            info = self.client.describe_collection(self.collection_name)
            fields = {f["name"] for f in info.get("fields", [])}
            if "scene_id" in fields and "embedding" in fields:
                self.client.load_collection(self.collection_name)
                return
            else:
                self.client.drop_collection(self.collection_name)

        schema = MilvusClient.create_schema(auto_id=False, enable_dynamic_field=False)
        schema.add_field(
            field_name="scene_id", datatype=DataType.VARCHAR,
            is_primary=True, max_length=128
        )
        schema.add_field(
            field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=512
        )
        self.client.create_collection(
            collection_name=self.collection_name,
            schema=schema,
            metric_type="COSINE",
        )

这里做了防御性设计:集合存在但 schema 不匹配时自动重建。这在迭代开发阶段非常实用------改了字段定义后不需要手动删集合再重启。

2.4 流水线的主循环

python 复制代码
def main():
    init_ray()
    
    # 初始化三个 Actor
    processor = SceneProcessor.remote()
    lance_writer = LanceDBWriter.remote()
    milvus_indexer = MilvusIndexer.remote()

    # Kafka 消费者
    consumer = KafkaConsumer(
        "raw-data-ingest",
        bootstrap_servers=KAFKA_BOOTSTRAP,
        group_id="ray-worker",
        auto_offset_reset="earliest",
    )

    for msg in consumer:
        event = msg.value
        key = event["Records"][0]["s3"]["object"]["key"]
        
        if key == "scene_metadata.json":
            # 1. 从 MinIO 下载元数据
            scenes = ray.get(download_metadata.remote(bucket, key))
            
            # 2. 并行处理(每个场景一个 Ray Task)
            processed = ray.get([
                processor.process.remote(s) for s in scenes
            ])
            
            # 3. 三路并行写入
            ray.get([
                lance_writer.write.remote(processed),
                milvus_indexer.index.remote(processed),
                sync_to_iceberg.remote(processed),
            ])

三步全部异步并行:download_metadataprocessor.process(批量并行)→ 三路写入并行。1000 条场景数据在单机上的处理时间约 2-3 分钟。

2.5 Iceberg 同步:面向数仓的前瞻设计

python 复制代码
@ray.remote
def sync_to_iceberg(scenes: List[Dict]):
    catalog = load_catalog("rest", **{
        "type": "rest",
        "uri": ICEBERG_CATALOG_URI,
        "warehouse": ICEBERG_WAREHOUSE,
        "s3.endpoint": S3_ENDPOINT,
        "s3.path-style-access": "true",
    })
    
    # 按小时分区,支持时间范围查询
    partition = PartitionSpec(
        PartitionField(
            source_id=2, field_id=1000,
            transform=HourTransform(), name="ts_hour"
        )
    )
    
    table.append(arrow_data)

Iceberg 的引入是为了对接 BI 工具(Superset、Metabase)。数据科学家可以通过标准 SQL 分析场景分布、质量趋势、标注覆盖率,不需要了解底层存储细节。


三、混合检索 API:向量召回 + 结构化过滤

检索是整个数据飞轮的"出口"。api/main.py 实现了混合检索,核心逻辑分两个阶段:

3.1 接口设计

python 复制代码
@app.post("/search")
def search_scenes(
    text_query: str = Query(None),      # 语义查询文本
    quality_min: float = Query(0.0),     # 质量分阈值
    scene_type: str = Query(None),       # 场景类型过滤
    top_k: int = Query(10),              # 返回数量
):

3.2 Phase 1:Milvus 向量召回

python 复制代码
if text_query:
    # CLIP 编码查询文本 -> 512 维向量
    query_emb = model.encode(text_query).tolist()
    
    # Milvus ANN 搜索,扩大 10 倍范围避免过滤后无结果
    results = client.search(
        collection_name=COLLECTION_NAME,
        data=[query_emb],
        limit=top_k * 10,
        output_fields=["scene_id"],
    )
    hit_ids = [hit["pk"] or hit["id"] for hit in results[0]]

3.3 Phase 2:LanceDB 精确过滤

python 复制代码
# 读取整个表为 Arrow Table
full_arrow = table.to_arrow()

# PyArrow 布尔掩码构建
mask = pc.field("scene_id").isin(hit_ids)
if quality_min > 0:
    mask = mask & (pc.field("quality_score") >= quality_min)
if scene_type:
    mask = mask & (pc.field("scene_type") == scene_type)

# 应用过滤
filtered = full_arrow.filter(mask)

# 按 Milvus 返回的相似度顺序排序
id_order = {sid: idx for idx, sid in enumerate(hit_ids)}
order_arr = pa.array([id_order[sid] for sid in filtered.column("scene_id").to_pylist()])
indices = pc.sort_indices(order_arr)
sorted_table = filtered.take(indices[:top_k])

为什么用 PyArrow 而不是 SQL?

LanceDB 底层就是 Arrow 格式,to_arrow() 是零拷贝读取,过滤操作在 Arrow 的列式内存结构上直接执行,比走 SQL 解析层更快。PyArrow 的表达式 API 也让代码可读性很好。

3.4 调试信息设计

python 复制代码
debug_info = {
    "input_params": {...},
    "table_status": f"initialized, rows={table.count_rows()}",
    "milvus_status": f"success, hits={len(hit_ids)}",
    "lancedb_filter": "scene_id in [...] AND quality>=0.8 AND type=warehouse",
    "lancedb_status": f"success, filtered_count={len(filtered)}",
}

每个环节的状态和参数都回传到 debug 字段,定位问题时一目了然。这在开发阶段非常实用------前端调用失败时,看一眼 debug 就知道是 Milvus 没数据还是 LanceDB 过滤条件太严。


四、部署验证

docker-compose up -d 一键启动,首次初始化约 2-3 分钟。验证指标:

验证项 指标 说明
端到端流水线 ~2-3 min 1000 条场景数据从上传到全部存储写入完成
LanceDB 写入吞吐 > 5000 条/秒 批量列式追加,P99 < 50ms
Zilliz 向量插入 ~2000 条/秒 含网络往返
混合检索 P95 延迟 < 120ms 10 万数据集,top_k=20
内存占用 ~6GB 全部服务运行中(64GB PC 实测)

各组件访问地址:

  • Ray Dashboardhttp://localhost:8265
  • MinIO Consolehttp://localhost:9001
  • Jupyter Notebookhttp://localhost:8888(无密码)
  • API Docs (Swagger)http://localhost:8000/docs
  • Prometheushttp://localhost:9090
  • Grafanahttp://localhost:3000

五、从原型到生产:已知局限与演进

作为技术验证原型,有几个明确的扩展方向:

方向 当前状态 演进路径
多模态处理 仅文本向量 + 模拟图像路径 NVENC GPU 解码、SAM 分割、图像 CLIP 编码、PointNet 点云
轨迹编码 随机向量模拟 TCN / Transformer 编码器
VLM 场景描述 HTTP Mock 返回模拟描述 Qwen-VL、LLaVA 真实模型替换
分布式部署 Ray 本地模式、Kafka 单节点 K8s + Kafka 多副本 + Ray Autoscaler

Ray 的 GPU 资源调度语义(@ray.remote(num_gpus=1))已经为多模态扩展预留了接口,模型 API 的接口设计也保持兼容,替换成本很低。


六、写在最后

RobotLoop 是一个从实际问题出发的技术验证项目。在具身智能这个快速发展的领域,数据基础设施的建设往往被算法的光环所掩盖,但我越来越确信:谁掌握了高质量、可检索、可复用的数据闭环,谁就能在迭代速度上赢得数量级优势。

这个项目让我把流处理、分布式计算、向量数据库、列式存储等技术串起来实践了一遍,也让我对"AI 基础设施"这个赛道有了更切身的理解。如果你也在关注具身智能数据平台方向,欢迎交流。

项目代码已开源在 GitHub,包含完整的 Docker Compose 配置和 Jupyter 演示 Notebook:

GitHub : github.com/pftn/robotl...


目前正在关注具身智能 / 机器人数据平台方向的架构与开发机会,如果你所在团队正好在搭建类似的数据基础设施,非常期待深入交流。

相关推荐
无忧智库6 小时前
智慧银行反欺诈大数据管控平台建设方案:从规则拦截到图谱智能,重构银行实时风控中枢(PPT)
大数据
湘美书院--湘美谈教育7 小时前
湘美谈教育湘美书院大湘西文学系列:AI时代的武侠小说怎么写
大数据·人工智能·深度学习·机器学习·生活
VortMall8 小时前
『平台去经营化』平台治理能力全新重构|VortMall微服务商城系统v1.3.10
java·大数据·微服务·商城系统·开源商城·vortmall·去经营化
workflower8 小时前
模型中心主义路径
大数据·人工智能·深度学习·机器学习·自动化·制造
Urbano8 小时前
卫衣全工序自动化智造科普:替代工位、设备选型与产能升级方案
大数据·人工智能·自动化
lupai8 小时前
手机空号过滤 API 新手实战指南
大数据·智能手机
嘉立创FPC苗工8 小时前
FPC 柔性线路板,解锁智能眼镜轻量化与高性能新赛道
大数据·人工智能·制造·fpc·电路板
嘻嘻的AI日记8 小时前
AI 知识库更合规:政企数据安全检索与智能应用合规体系
大数据·人工智能
xiaopai9458 小时前
从立项到回款,工程项目管理系统能管哪些流程?
大数据·建米软件·工程项目管理系统