embedding 维度对不上导致检索报错:微软生成式 AI 入门课从写入端查起
现象:两种长得完全不一样的症状
同一个 bug,在 generative-ai-for-beginners 第八课里会以两副面孔出现。
一副是直接抛错。08-building-search-applications/typescript/search-app/src/main.ts 和 08-building-search-applications/js-githubmodels/app.js 的 cosineSimilarity 都在函数开头做了长度检查,抛出的错误文本两处逐字相同。下面这段抄自 js-githubmodels/app.js:
function cosineSimilarity(vector1, vector2) {
if (vector1.length !== vector2.length) {
throw new Error("Vector dimensions must match for cosine similarity calculation.");
}
main.ts 里是同一段逻辑的 TypeScript 版,签名带类型标注,写作 function cosineSimilarity(vector1: number[], vector2: number[]): number。
另一副面孔是不报错,但什么都查不出来。08-building-search-applications/python/aoai-solution.ipynb 里的 cosine_similarity 多了一段补零:
def cosine_similarity(a, b):
if len(a) > len(b):
b = np.pad(b, (0, len(a) - len(b)), 'constant')
elif len(b) > len(a):
a = np.pad(a, (0, len(b) - len(a)), 'constant')
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
短的那个向量被补零补齐,点积照样算得出来,只是分母里的模长把补零那一截也算了进去,相似度被摊薄。同一个 notebook 里有 SIMILARITIES_RESULTS_THRESHOLD = 0.75(这是仓库当前代码里的示例值,随版本可能变动),被摊薄后的分数过不了这道门槛,get_videos 返回空 DataFrame,界面上就是「查什么都没有结果」。
同一课的 python/oai-solution.ipynb 里的 cosine_similarity 没有这段补零逻辑。也就是说:你走 Azure OpenAI 那条路线会得到静默的空结果,走 OpenAI 那条路线会得到一个报错。 这两处的差异就写在仓库里,我们只把它们并排放着,不揣测作者为什么这么写。
怎么确认是这个问题:先量两端的长度
判定动作只有一个——把写入端和查询端各拿一个向量出来,量长度。
写入端的产物是 JSON 数组,每条记录的键在随仓自带的 08-building-search-applications/embedding_index_3m.json 里是这样:speaker、title、videoId、start、seconds、summary、ada_v2。向量装在 ada_v2 这个字段里,取一条记录 len(record["ada_v2"]) 就是写入端的维度。
查询端的写法仓库里现成就有。js-githubmodels/app.js 在遍历响应时直接把长度打了出来:
const { embedding, index } = item; // Destructure item for cleaner code
const length = embedding.length;
Python 侧同理,client.embeddings.create(input=query, model=model).data[0].embedding 拿到的就是一个 list,len() 一下即可。两个数字对不上,这篇讲的就是你的问题。
对得上但依然没结果的情况留到最后一节。
病根在写入端:模型名在三处被写死,且各写各的
第八课的写入端是 08-building-search-applications/scripts/transcript_enrich_embeddings.py。它这样决定用哪个模型:
EMBEDDINGS_DEPLOYMENT_NAME = os.getenv(
"AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT", "text-embedding-ada-002"
)
注意第二个参数是 fallback。同一个变量名 AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT,在仓库里另外两处的取值并不一致:
| 位置 | 取值方式 |
|---|---|
08-building-search-applications/scripts/transcript_enrich_embeddings.py | os.getenv(...),fallback 为 text-embedding-ada-002 |
08-building-search-applications/typescript/search-app/src/main.ts | process.env.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT || "text-embedding-3-small" |
仓库根目录 .env.copy | 该行的占位说明里举的例子是 text-embedding-3-small(原文写作 '<add your embeddings model deployment name here, e.g. text-embedding-3-small>') |
三处的默认值不一致,说明的是同一件事:不要依赖 fallback。 只要你没在环境里显式设这个变量,Python 写入端和 TypeScript 查询端就会各自走各自的默认,落到磁盘上的向量和查询时算出来的向量根本不是一个模型产的。
查询端还有更硬的写法。python/oai-solution.ipynb 和 python/oai-assignment.ipynb 直接把模型名钉在代码里:
model = 'text-embedding-ada-002'
而 python/aoai-solution.ipynb 读的是 os.environ['AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT']。所以「改一下 .env 换个模型」这个动作,只会影响 Azure OpenAI 那条路线,OpenAI 那条路线纹丝不动。
顺带一提,python/aoai-solution.ipynb 开头的说明文字里让你把 deployment 名字设成 AZURE_OPENAI_EMBEDDINGS_ENDPOINT,而下面的代码读的是 AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT。这两个名字对不上,照着说明文字设变量是设不进去的——查环境变量时按代码里读的那个名字为准。
换模型之后:基线必须重建,脚本不会替你做
这是最容易踩的一脚。transcript_enrich_embeddings.py 的工作线程里有这么一段:
if "ada_v2" in segment:
output_segments.append(segment.copy())
continue
已经有 ada_v2 字段的段落会被原样搬过去,不重新算。 这个设计对断点续跑很友好,对换模型是灾难:你换了 embedding 模型再跑一遍脚本,旧记录一条都不会更新,新增的记录用新模型算,结果是一个新旧混装的索引。这时候两端维度「有时对得上、有时对不上」,比全错更难查。
雪上加霜的是输出路径。脚本读的是 os.path.join(TRANSCRIPT_FOLDER, "output", "master_enriched.json"),写的也是同一个路径——就地覆盖,没有备份。而 argparse 只接受 -f/--folder 和 --verbose 两个参数,仓库里没有找到「强制重算」之类的开关。要重建基线,只能自己把旧文件里的向量字段清掉,或者从上游重新生成一份输入。
还有一处跟着模型走的东西是 tokenizer:
tokenizer = tiktoken.get_encoding("cl100k_base")
以及队列处理里那句 if len(tokenizer.encode(text)) > 8191: continue(这是仓库当前代码里硬编码的常量,随版本可能变动)。超长的段落被 continue 直接跳过,既不写入也不报错,输出里连一条对应记录都没有。所以换模型后如果发现索引条数变少了,别急着怀疑数据源,先想想这条静默丢弃的分支。编码名 cl100k_base 同样是写死的,换了模型不一定还是这个编码。
字段名本身也是历史包袱。ada_v2 这个名字在写入端、在随仓的 embedding_index_3m.json、在查询端的 video_vectors["ada_v2"] 三处都写死了。你换成别的模型,向量照样往 ada_v2 里塞,跑得通,但从字段名上你永远看不出里面装的是谁的向量。在索引里另存一个记录模型名的字段是个省事的做法——这属于通用工程做法,不是该课程的官方内容。
处置后怎么验证
第一步,量长度。重建完索引,取一条记录的 len(record["ada_v2"]),和查询端现场生成的向量长度比,必须相等。
第二步,用仓库自带的自比测试。python/aoai-assignment.ipynb 和 python/oai-assignment.ipynb 里的注释写明:同一个词跟自己比余弦相似度应当是 1.0,不同概念的词落在 0 到 1 之间。可以照它的写法先验一下:
automobile_embedding = client.embeddings.create(input='automobile', model=model).data[0].embedding
print(cosine_similarity(automobile_embedding, automobile_embedding))
自比拿不到 1.0 附近的数,说明你比的压根不是同一个向量,或者已经被补零那段逻辑改过了。
第三步,把 SIMILARITIES_RESULTS_THRESHOLD 临时调低再查一次。如果调低之后结果哗地出来了,说明维度是对的、只是分数普遍偏低;如果调到很低依然空,那还有别的问题。
以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
什么情况说明不是这个原因
报的是 BadRequestError(HTTP 400)。 写入脚本的重试装饰器里写着 retry=retry_if_not_exception_type(BadRequestError),这类错误被明确排除在重试之外,一次就失败。仓库里没有找到对这个选择的解释,但可以确定的是:脚本判定它不值得重试,说明它不是那种「再试一次可能好」的瞬时故障,而是请求本身的问题——先去核对 deployment 名与请求参数,别在维度上耗时间。(这一步的排查顺序属于通用做法,不是该课程的官方内容。)
两端长度完全一致却仍然空结果。 那就回到阈值和数据本身:load_dataset 里有 drop(columns=["text"], errors="ignore").fillna(""),正文列是被丢掉的,检索只靠向量;分数普遍够不到 0.75 时,问题在语料或查询表述,不在维度。
Windows 上改了配置没生效。 08-building-search-applications/scripts/README.md 给的是两套并存的做法:Windows 侧建议加到用户环境变量(开始菜单 → 编辑系统环境变量 → 环境变量 → 用户变量 → 新建),Linux/macOS 侧建议在 ~/.bashrc 或 ~/.zshrc 里 export。而脚本自己开头调的是 dotenv.load_dotenv(),也就是还会读 .env 文件。同一个变量有两条来源,改完一处没生效很常见。
还有一处更隐蔽:这份 README 列出的「必需环境变量」是 AZURE_OPENAI_API_KEY、AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_MODEL_DEPLOYMENT_NAME、GOOGLE_DEVELOPER_API_KEY 这几个,里面并没有 AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT——后者只在脚本代码里以 os.getenv 的形式出现。照着 README 把变量配齐,embedding 模型那一项依然是空的,脚本就静静地走 fallback。所以遇到「配了没生效」,先确认进程里实际拿到的是哪一个值、这个变量到底有没有被设上,再判断是不是维度问题。虚拟环境激活方式两边也不同,Windows 是 .venv\Scripts\activate,macOS 和 Linux 是 source .venv/bin/activate。
你走的是第三条 provider 路线。 目录名 js-githubmodels 里的前缀今天已经名不副实:js-githubmodels/app.js 的注释里逐字写着 GitHub Models 将于 2026 年 7 月底退役,00-course-setup/03-providers.md 也写明 githubmodels 这条路线实际需要的是 Microsoft Foundry Models 的 endpoint 与 key。代码里读的两个变量是 AZURE_INFERENCE_ENDPOINT 和 AZURE_INFERENCE_CREDENTIAL,跟 AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT 完全是另一套。这条路线的模型名写在 const modelName = "text-embedding-3-small";(仓库里的示例值),和 Python 写入端的默认不是一个东西——如果你的索引是 Python 脚本产的、查询走的是这条路线,维度对不上几乎是必然的。
选路线时可以这么记:Azure OpenAI 路线用 AZURE_OPENAI_* 那组变量,OpenAI 路线用 OPENAI_API_KEY 且模型名在 notebook 里写死,Microsoft Foundry Models(原 GitHub Models 路线)用 AZURE_INFERENCE_* 那组。三条路线的模型名来源各不相同,跨路线复用索引之前先量长度。
本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与生成质量的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。