Skip to main content

架构概览

核心组件

组件作用说明
Voyage AI图片 Embeddingvoyage-multimodal-3.5 模型,512 维向量
LanceDB向量数据库存储和搜索相似图片
内容审核图片审核检测 NSFW/违规内容

发布流程

1. 生成 Embedding

使用 Voyage AI 的多模态模型生成图片向量:
export const getImageEmbedding = async (imageId: string) => {
  const { originUrl } = await getMessageAllUrls(imageId);

  const response = await fetch("https://api.voyageai.com/v1/multimodalembeddings", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.VOYAGE_API_KEY}`,
    },
    body: JSON.stringify({
      model: "voyage-multimodal-3.5",
      input_type: "document",
      inputs: [
        {
          content: [{ type: "image_url", image_url: originUrl }],
        },
      ],
      output_dimension: 512,
    }),
  });

  const result = await response.json();
  return result.data?.[0]?.embedding;
};

2. 相似度检查

在 LanceDB 中检查是否有相似图片(只与当前用户的图片对比):
export async function hasSimilarInLanceDB(
  userId: string,
  embedding: number[],
  threshold: number = 0.25, // cosine distance < 0.25 = 相似度 > 75%
): Promise<boolean> {
  const table = await getFeedTable();

  const results = await table
    .search(embedding)
    .where(`user_id = '${userId}'`)
    .distanceType("cosine")
    .limit(1)
    .toArray();

  return results.length > 0 && results[0]._distance < threshold;
}
相似度阈值 0.25 表示 cosine distance,对应相似度约 75%。只与同一用户的图片对比,不同用户之间不去重。

3. 内容审核

使用图片审核服务检测违规内容:
const moderationPassed = await moderateImageUrl(newImageId);

4. 可见性判断

最终可见性由三个条件决定:
const userVisible = profile?.visible ?? false;  // 用户是否公开
const isSimilar = await hasSimilarInLanceDB(resourceId, embedding);
const moderationPassed = await moderateImageUrl(newImageId);

const visible = userVisible && !isSimilar && moderationPassed;
条件说明
userVisible用户 profile 中的公开设置
!isSimilar不与已有图片相似
moderationPassed内容审核通过

5. 存储 Embedding

发布成功后,将 embedding 存入 LanceDB:
if (!error && feedData && embedding) {
  await saveFeedEmbedding({
    id: feedData.id,
    user_id: resourceId,
    image_id: image.imageId,
    vector: embedding,
    created_at: new Date().toISOString(),
  });
}

LanceDB 配置

连接

const db = await lancedb.connect(process.env.LANCEDB_URI!, {
  apiKey: process.env.LANCEDB_API_KEY,
});

表结构

type FeedEmbedding = {
  id: string;        // feed ID
  user_id: string;   // 用户 ID
  image_id: string;  // 图片 ID
  vector: number[];  // 512 维向量
  created_at: string;
};

环境变量

LANCEDB_URI=<lancedb-cloud-uri>
LANCEDB_API_KEY=<api-key>
VOYAGE_API_KEY=<voyage-api-key>

Voyage AI 配置

参数说明
modelvoyage-multimodal-3.5多模态 embedding 模型
input_typedocument输入类型
output_dimension512输出向量维度
Voyage AI 按 token 计费,每次调用会消耗一定费用。建议在自动发布场景中谨慎使用。

Analytics 事件

发布时记录 PostHog 事件:
posthog.capture({
  distinctId: resourceId,
  event: "feed_published",
  properties: {
    feed_id: feedData?.id,
    image_id: image.imageId,
    feed_image_id: newImageId,
    visible,
    invisible_reason: !visible
      ? !userVisible
        ? "user_private"
        : isSimilar
          ? "similar_image"
          : "moderation_failed"
      : null,
  },
});

invisible_reason 枚举

说明
user_private用户设置为私密
similar_image与已有图片相似
moderation_failed内容审核不通过
null可见(无原因)

删除流程

删除 Feed 时同时删除 LanceDB 中的 embedding:
export async function deleteFeedEmbedding(feedId: string): Promise<void> {
  const table = await getFeedTable();
  await table.delete(`id = '${feedId}'`);
}

完整流程图

相关文档