架构概览
核心组件
| 组件 | 作用 | 说明 |
|---|
| Voyage AI | 图片 Embedding | voyage-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 配置
| 参数 | 值 | 说明 |
|---|
| model | voyage-multimodal-3.5 | 多模态 embedding 模型 |
| input_type | document | 输入类型 |
| output_dimension | 512 | 输出向量维度 |
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}'`);
}
完整流程图
相关文档