📘 mcpgw 图解教程 · 目录 第二部分 · 检索深入 · 向量检索 05 / 15
第二部分 · 检索深入 · 向量检索

Embedder 抽象 & OpenAiEmbedder

要做向量检索,第一步是把文本变成向量。mcpgw 用一个与厂商无关Embedder trait 把「怎么变」这件事抽象掉,真实的 HTTP 实现放在独立的 embedder crate。 于是 retrieval 这个检索内核不引入任何 HTTP 依赖,可以在无网络下编译和测试。

抽象层:Embedder trait

🔬 细节 / 代码对应

trait 只有两个方法:embed一批文本各转成一个向量顺序一一对应all-or-nothing——要么整批成功,要么报错),dim 返回期望维度用于体检。 错误类型 EmbedError 也是与 provider 无关的,所以检索内核完全不知道背后是谁、用不用 HTTP。

crates/retrieval/src/embedder.rs · Embedder / EmbedError
// provider 无关,于是 retrieval 不需要任何 HTTP 依赖
pub enum EmbedError {
    Provider(String),                         // 厂商/网络/解码等失败
    Dimension { expected: usize, got: usize }, // 维度不符
}

pub trait Embedder: Send + Sync {
    // 一批文本 → 各一个向量,顺序对应;要么全成功要么 Err
    async fn embed(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, EmbedError>;
    fn dim(&self) -> usize; // 期望维度,用于校验
}

真实实现:OpenAiEmbedder

🔬 细节 / 代码对应

OpenAiEmbeddercrates/embedder/src/lib.rs)对接任何 OpenAI 兼容的 /embeddings 端点(OpenAI 本体,或 Ollama / LM Studio / vLLM 等同形状的本地服务)。它的 embed 做这几件事:

crates/embedder/src/lib.rs · OpenAiEmbedder::embed
async fn embed(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, EmbedError> {
    if texts.is_empty() { return Ok(Vec::new()); } // 空输入短路
    let resp = self.client
        .post(&format!("{}/embeddings", self.base_url))
        .bearer_auth(&self.api_key)                 // Bearer 密钥
        .json(&json!({ "model": self.model, "input": texts }))
        .send().await?;
    if !resp.status().is_success() {
        let snippet: String = body.chars().take(500).collect(); // ≤500 字符,绝不含 Authorization
        return Err(EmbedError::Provider(format!("HTTP {code} …: {snippet}")));
    }
    let mut data = parsed.data;
    data.sort_by_key(|d| d.index);                   // 按 index 还原输入顺序
    // …校验 data.len()==texts.len()、index 连续、可选 dim 一致…
}
⚠️ 注意:密钥
构造 OpenAiEmbedder 时传入的 api_key真实的 token 值, 而这个值来自环境变量。但配置文件里存的是env 变量名(不是值本身,详见第 08 课)。 于是任何错误信息只会提到变量名,绝不打印密钥值——HTTP 错误片段也刻意只截响应体、不含 Authorization 头

测试替身:MockEmbedder

🔬 细节 / 代码对应

MockEmbedder(在 retrievaltestkit feature 下)让测试不依赖网络: 它把每个 token 用 FNV 哈希分桶,在对应桶 +1,生成确定性的伪向量—— 共享 token 越多的文本,余弦相似度越高,所以语义检索行为可被稳定验证。它还暴露 calls(embed 被调用次数)和 seen(见过哪些文本),供缓存断言使用(第 06 课会用到); 另有 MockEmbedder::failing 专门让 embed 必报错,用来驱动降级测试。

✅ 关键要点
💡 设计亮点
这条 trait 边界把「HTTP / 厂商细节」与「检索逻辑」彻底解耦: HTTP 只活在 embedder 这一个 crate 里,于是 retrieval 可以在无网络下编译、用 MockEmbedder 确定性地测试, 换厂商(OpenAI / 本地服务)也只换一个实现,检索内核一行都不用动。