---
title: "多模态帖子嵌入器"
description: "Grox mm_emb flow 的三种帖子嵌入：v5（截断到 1024 维）、v8.2（带作者画像与转写）、v8-search（含历史回填）"
---

> Documentation Index
> Fetch the complete documentation index at: https://x-algorithm.vergego.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 多模态帖子嵌入器

## 这一页回答什么

本页说明 Grox 如何把一条帖子（文本、图片、视频帧、语音转写）编码成一个稠密向量。本页还说明当前并行运行的 v5、v8.2、v8-search 三种嵌入器的区别，以及嵌入的写入位置和读取方。

## 核心结论

1. **三种嵌入器并行运行**：`MultimodalPostEmbedderV5`、`MultimodalPostEmbedderV82`、`MultimodalPostEmbedderV8Search`（含回填子类）分别由 `mm_emb_v5`、`mm_emb_v8_2`、`mm_emb_v8_search` 三组 Plan 调用。旧版的 `MultimodalPostEmbedderV2` 和“带 Grok 摘要”的嵌入 Plan 已删除。
2. **v5 仍截断到 1024 维**：模型为 `recsys-v5-embedding`。嵌入器截取输出的前 1024 维，再执行 L2 归一化（`grox/flows/mm_emb/embedder.py:15`、`grox/flows/mm_emb/embedder.py:42-52`）。
3. **v8 系列把作者画像写入嵌入输入**：v8.2 与 v8-search 的输入文本以作者块开头。作者块包含认证颜色、账号标签、NSFW 账号标记、粉丝数档位、注册时间、国家和语言（`grox/flows/mm_emb/renderer_v82.py:187-243`）。因此，同一段文字由不同账号发出时，得到的嵌入不同。
4. **v8 系列不截断**：输出维度取模型原生维度，嵌入器只执行 L2 归一化（`grox/flows/mm_emb/embedder_v82.py:28-32`）。
5. **回复单独成流**：三组嵌入都有 `_for_reply` 版本。回复嵌入只写入 Manhattan，不发送到 Kafka。回复所在线程中任一方是私密账号时，Plan 跳过该回复（`grox/flows/mm_emb/task_filter.py:10-50`）。
6. **嵌入是召回的输入**：Phoenix 的候选塔用帖子多模态嵌入生成语义 ID（`phoenix/README.md:110-114`）。Phoenix 训练数据支持的嵌入版本包括 `v5`、`v8`，均为 1024 维（`phoenix/xrex/data/recsys/recsys_batch.py:138-146`）。源码未直接写明这些版本名与 Grox 产出的对应关系。

## 嵌入 Plan 的链路

三组 Plan 结构相同。下面以 `PlanPostEmbeddingV82` 为例（`grox/flows/mm_emb/plan_post_embedding_v82.py:15-32`）：

```mermaid
flowchart LR
    RL["60 秒去重"] --> MH["媒体水合<br/>下载图片与视频帧"]
    MH --> ASR["语音转写<br/>TaskASRTranscription"]
    ASR --> EMB["嵌入<br/>v5 / v8.2 / v8-search"]
    EMB --> W["写入 Manhattan<br/>原帖再发 Kafka"]
```

回复版 Plan 在去重之后多一个 `TaskPostEmbeddingForReplyFilter`（`grox/flows/mm_emb/plan_post_embedding_v82_for_reply.py:26-35`）。`mm_emb_v5` 还多一个写入 Task，它把每条原帖和回复的 v5 嵌入都发送到 `grox-multimodal-embedding-v5-all` 主题（`grox/flows/mm_emb/plan_post_embedding_v5.py:19-37`、`grox/flows/mm_emb/constants.py:18`）。

## V5：截断到 1024 维

`MultimodalPostEmbedderV5`（`grox/flows/mm_emb/embedder.py:18-116`）的行为如下：

- 模型键为 `recsys-v5-embedding`，经 `XaiEmbeddingClientHttp` 调用。配置未写超时时，默认 60 秒。
- 系统提示词为：“Represent this X post for retrieving relevant X posts by content, topic, sentiment, or interest groups”（`grox/flows/mm_emb/embedder.py:14`）。
- **渲染**：`V5EmbedPostRenderer.render_for_embedding` 产出带占位符的文本和图片列表（`grox/flows/mm_emb/renderer.py:16-46`）。占位符 `<|vision_start|><|image_pad|><|vision_end|>` 插在第 N 张图在文本中应出现的位置。编码时，模型把第 N 张图填回该位置，从而保留图文顺序（`grox/flows/mm_emb/renderer.py:7`）。引用帖以 `Quoted post:` 接在后面。
- **转写**：`TaskMultimodalPostEmbeddingV5` 收集帖子中所有视频的 ASR 转写，用换行拼接，再以 `\nTranscript: ...` 追加到文本末尾（`grox/flows/mm_emb/task_multimodal_post_embedding.py:23-35`、`grox/flows/mm_emb/embedder.py:72-73`）。
- **编码**：调用 `encode_with_embedded_pads_async(text_with_pads, images)`（`grox/flows/mm_emb/embedder.py:84-86`）。
- **截断**：取前 1024 维并执行 L2 归一化。不足 1024 维时，只执行归一化（`grox/flows/mm_emb/embedder.py:42-52`）。
- 文本和图片都为空时，嵌入器返回空嵌入，Task 以 `TaskStopExecution` 跳过写入（`grox/flows/mm_emb/task_multimodal_post_embedding.py:42-47`）。

结果在状态中的键名为 `v5_1`（`grox/flows/mm_emb/task_multimodal_post_embedding.py:48`）。

## V8.2：带作者画像

`MultimodalPostEmbedderV82`（`grox/flows/mm_emb/embedder_v82.py:19-114`）与 v5 的区别如下：

| 维度 | v5 | v8.2 |
|------|----|------|
| 模型键 | `recsys-v5-embedding` | `recsys-v82-mm-embedding` |
| 调用接口 | `encode_with_embedded_pads_async` | `embed_openai_async`（OpenAI 协议） |
| 指令 | 系统提示词 | 文本首行 `INSTRUCTION_PREFIX`：“Represent this X post (formerly Twitter) for recommendation: capture its topic, intent, key entities, and likely engagement.” |
| 作者信息 | 名字和 handle | 作者块：认证颜色、账号标签、私密标记、NSFW 账号标记、简介（≤ 500 字符）、粉丝数与关注数档位、订阅等级、注册年月、国家、语言、关联企业 |
| 引用帖作者 | 名字和 handle | 同样拉取引用帖作者的 core user 资料 |
| 媒体 | 每帖按预算取图 | 原帖与引用帖合计最多 4 张，每个视频最多 4 帧 |
| 转写 | 追加在文本末尾 | `Transcript:` 块，截断到 2000 字符 |
| 输出 | 截断到 1024 维 + 归一化 | 原生维度 + 归一化 |

出处：`grox/flows/mm_emb/renderer_v82.py:12-243`、`grox/flows/mm_emb/embedder_v82.py:16-91`。

作者块中的认证颜色规则为：`verifiedType` 为 Government 时记 Grey，为 Business 时记 Gold，蓝标记 Blue，后面附订阅档位（`grox/flows/mm_emb/renderer_v82.py:246-266`）。NSFW 标记来自 core user 的 `nsfwUser` 或 `nsfwAdmin`（`grox/flows/mm_emb/renderer_v82.py:277-284`）。core user 资料拉取失败时，作者块退化为帖子自带的字段，嵌入照常计算（`grox/flows/mm_emb/embedder_v82.py:34-43`）。

## V8-search：更多媒体与历史回填

`MultimodalPostEmbedderV8Search`（`grox/flows/mm_emb/embedder_v8_search.py:23-110`）沿用 v8.2 的作者块和指令。它与 v8.2 的差别在媒体与转写：

- 原帖与引用帖合计最多 16 张图片或帧。嵌入器按出现顺序分配预算，超出预算的视频减少帧数（`grox/flows/mm_emb/renderer_v8_search.py:34`、`grox/flows/mm_emb/renderer_v8_search.py:381-404`）。
- 有多个视频时，每个视频的转写紧跟该视频的帧。2000 字符预算在各视频间平分（`grox/flows/mm_emb/renderer_v8_search.py:194-208`）。
- 嵌入器不拉取引用帖作者的 core user 资料。
- 写入时，模型版本名为 `v8_search_1`（`grox/flows/mm_emb/constants.py:26`）。

**历史回填**。`grox-multimodal-embedding-backfill` 主题中的每条消息包含若干个 8 字节帖子 ID，生成器每 100 个 ID 打成一批（`grox/flows/mm_emb/generators.py:65-95`、`grox/flows/mm_emb/batched_fav_hydrator.py:14`）。`TaskBackfillBatchFanout` 去重并批量拉取赞数，再为每个帖子单独执行 `mm_emb_v8_search_backfill` Plan（`grox/flows/mm_emb/task_backfill_fanout.py:37-85`）。该 Plan 在嵌入前设有两道闸门（`grox/flows/mm_emb/task_backfill_gate.py:16-60`）：

1. **赞数闸门**：赞数低于动态阈值时，丢弃该帖子。阈值初始为 20,000，目标通过速率为每秒 5 个（环境变量 `MM_EMB_BACKFILL_TARGET_QPS`）。
2. **已存在闸门**：Manhattan 中已有 `v8_search_1` 嵌入时，丢弃该帖子。

`AdaptiveMinFav` 调整动态阈值：它每 30 秒计算一次实际通过速率。偏离目标超过 10% 时，它按 `阈值 × (1 + 0.3 × 相对误差)` 调整阈值，上限 20,000，下限 0。启动后的前 60 秒内，它不调整阈值（`grox/flows/mm_emb/adaptive_min_fav.py:6-63`）。结果是：系统先回填最热的历史帖子，处理能力有富余时再逐步降低门槛。回填使用单独的模型键 `recsys-v8-search-backfill-embedding`（`grox/flows/mm_emb/embedder_v8_search.py:112-113`）。

## 写入

`TaskWriteMMEmbeddingSinkBase` 的所有子类都遵循以下规则（`grox/flows/mm_emb/task_write_mm_embedding_sink.py:26-250`）：

- 只在 mm-emb 生产环境执行（`DisableTaskForNonMmEmbProd`）。
- 自带 3 次重试，间隔为 1 秒和 2 秒。
- 写入 Manhattan：键为帖子 ID 加模型版本（`v5_1`、`v8_2`、`v8_search_1`）。
- 原帖额外发送到 Kafka：`grox-multimodal-embedding-v5`、`grox-multimodal-embedding-v8-2`、`grox-multimodal-embedding-v8-search`（`grox/flows/mm_emb/constants.py:17-20`）。回复只写入 Manhattan（`grox/flows/mm_emb/task_write_mm_embedding_sink.py:77-88`）。

## 设计决策

| 决策 | 理由 |
|------|------|
| 并行产出三种嵌入 | 不同下游（推荐召回、搜索）可各自选择版本；新版本上线不影响旧消费方 |
| v5 截断到 1024 维 | 控制存储与检索成本；归一化后点积等于余弦相似度，适合召回 |
| v8 把作者画像写入输入 | 让嵌入同时编码“谁发的”；对推荐而言，作者身份本身是强信号 |
| 回复只写入 Manhattan | 回复量远大于原帖；回复嵌入不进入实时 Kafka 消费方，只供按 ID 查询 |
| 回复遇到私密账号时跳过 | 不为私密线程产出可被检索的内容表示 |
| 回填使用动态赞数阈值 | 在固定吞吐下优先处理最热的历史帖子 |

## FAQ

**Q：同一条帖子会被嵌入几次？**
A：原帖进入 `grox-multimodal-embedding-requests-with-summary` 主题后，v5、v8.2、v8-search 三个生成器各消费一次，得到三份嵌入（`grox/flows/mm_emb/generators.py:47-150`）。恢复主题和回填主题会补算缺失的嵌入。

**Q：v8 嵌入包含作者的粉丝数，这会不会让大号的帖子更容易被召回？**
A：源码只说明粉丝数档位进入了嵌入输入文本，没有说明召回如何使用这部分信息。“放大大号优势”属于推测，需要线上实验才能确认。

## 源码锚点

- `grox/flows/mm_emb/embedder.py:14-116` —— v5 嵌入器与 1024 维截断
- `grox/flows/mm_emb/renderer.py:7-46` —— v5 渲染与图片占位符
- `grox/flows/mm_emb/embedder_v82.py:16-114` —— v8.2 嵌入器
- `grox/flows/mm_emb/renderer_v82.py:12-284` —— v8.2 指令与作者块
- `grox/flows/mm_emb/embedder_v8_search.py:23-113` —— v8-search 与回填子类
- `grox/flows/mm_emb/renderer_v8_search.py:157-404` —— v8-search 渲染与帧预算
- `grox/flows/mm_emb/task_backfill_gate.py:16-60` —— 回填闸门
- `grox/flows/mm_emb/adaptive_min_fav.py:6-63` —— 动态赞数阈值
- `grox/flows/mm_emb/task_write_mm_embedding_sink.py:26-250` —— 写入与 Kafka 分流

## 相关页面

- [Grox 服务架构](https://x-algorithm.vergego.ai/concepts/grox-architecture/) —— 嵌入 Plan 的执行框架
- [Grox 内容分类器](https://x-algorithm.vergego.ai/concepts/grox-classifiers/) —— Grox 的另一类产出：内容分类
- [图片与视频模型](https://x-algorithm.vergego.ai/concepts/media-models/) —— 另一套媒体嵌入：CLIP 媒体嵌入
- [Phoenix 召回：双塔检索架构](https://x-algorithm.vergego.ai/concepts/phoenix-retrieval/) —— 用多模态嵌入生成语义 ID 的召回模型
- [VM Ranker](https://x-algorithm.vergego.ai/concepts/vm-ranker/) —— 读取多模态嵌入的另一个排序组件
- [哈希嵌入](https://x-algorithm.vergego.ai/concepts/hash-based-embeddings/) —— Phoenix 模型侧的 ID 哈希嵌入

Source: https://x-algorithm.vergego.ai/concepts/multimodal-embedders/index.mdx
