Skip to content

VectorStoreRetriever 作为 Runnable<Query, Document[]>

实现 .invoke() 即向量搜索

在现代检索增强生成(RAG)应用中,向量存储检索器(VectorStoreRetriever)是核心组件之一。LangChain V3 将 VectorStoreRetriever 实现为 Runnable<Query, Document[]>,使得它能够无缝集成到 LCEL 管道中。本章将深入探讨 VectorStoreRetriever 的设计和实现。

VectorStoreRetriever 的基本概念

VectorStoreRetriever 是连接向量存储和应用逻辑的桥梁,它负责接收查询并返回相关的文档:

typescript
interface Document {
  pageContent: string;
  metadata: Record<string, any>;
}

interface VectorStoreRetrieverArgs {
  vectorStore: VectorStore;
  k?: number; // 返回的文档数量
  filter?: (document: Document) => boolean;
  similarityMetric?: 'cosine' | 'euclidean' | 'dot';
}

class VectorStoreRetriever implements Runnable<string, Document[]> {
  private vectorStore: VectorStore;
  private k: number;
  private filter?: (document: Document) => boolean;
  private similarityMetric: 'cosine' | 'euclidean' | 'dot';
  
  constructor(args: VectorStoreRetrieverArgs) {
    this.vectorStore = args.vectorStore;
    this.k = args.k ?? 4;
    this.filter = args.filter;
    this.similarityMetric = args.similarityMetric ?? 'cosine';
  }
  
  async invoke(query: string, options?: RunnableConfig): Promise<Document[]> {
    // 执行向量搜索
    let documents = await this.vectorStore.similaritySearch(query, this.k);
    
    // 应用过滤器(如果有的话)
    if (this.filter) {
      documents = documents.filter(this.filter);
    }
    
    return documents;
  }
  
  async batch(queries: string[], options?: RunnableConfig): Promise<Document[][]> {
    // 批量查询处理
    return await Promise.all(queries.map(query => this.invoke(query, options)));
  }
  
  // VectorStoreRetriever 特有的方法
  async similaritySearch(query: string, k?: number): Promise<Document[]> {
    const searchK = k ?? this.k;
    let documents = await this.vectorStore.similaritySearch(query, searchK);
    
    if (this.filter) {
      documents = documents.filter(this.filter);
    }
    
    return documents;
  }
  
  async maxMarginalRelevanceSearch(
    query: string, 
    k?: number, 
    fetchK?: number
  ): Promise<Document[]> {
    const searchK = k ?? this.k;
    const searchFetchK = fetchK ?? searchK * 5;
    
    let documents = await this.vectorStore.maxMarginalRelevanceSearch(
      query, 
      searchK, 
      searchFetchK
    );
    
    if (this.filter) {
      documents = documents.filter(this.filter);
    }
    
    return documents;
  }
}

与不同向量存储的集成

VectorStoreRetriever 可以与多种向量存储集成:

内存向量存储

typescript
class MemoryVectorStore implements VectorStore {
  private documents: Document[] = [];
  private embeddings: number[][] = [];
  
  async addDocuments(documents: Document[]): Promise<void> {
    const newEmbeddings = await this.embedDocuments(documents.map(d => d.pageContent));
    this.documents.push(...documents);
    this.embeddings.push(...newEmbeddings);
  }
  
  async similaritySearch(query: string, k: number): Promise<Document[]> {
    // 生成查询的向量表示
    const queryEmbedding = await this.embedQuery(query);
    
    // 计算相似度
    const similarities = this.embeddings.map(embedding => 
      this.cosineSimilarity(queryEmbedding, embedding)
    );
    
    // 获取最相似的文档
    const indices = this.getTopKIndices(similarities, k);
    return indices.map(i => this.documents[i]);
  }
  
  private async embedDocuments(texts: string[]): Promise<number[][]> {
    // 实现文档嵌入逻辑
    // 这里简化处理,实际应用中会使用真实的嵌入模型
    return texts.map(text => this.simpleHashEmbedding(text));
  }
  
  private async embedQuery(text: string): Promise<number[]> {
    // 实现查询嵌入逻辑
    return this.simpleHashEmbedding(text);
  }
  
  private simpleHashEmbedding(text: string): number[] {
    // 简化的哈希嵌入(仅用于演示)
    const hash = this.hashCode(text);
    return [hash % 1000, (hash >> 10) % 1000, (hash >> 20) % 1000];
  }
  
  private hashCode(str: string): number {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 转换为 32 位整数
    }
    return Math.abs(hash);
  }
  
  private cosineSimilarity(a: number[], b: number[]): number {
    const dotProduct = a.reduce((sum, _, i) => sum + a[i] * b[i], 0);
    const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
    const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
    
    if (magnitudeA === 0 || magnitudeB === 0) {
      return 0;
    }
    
    return dotProduct / (magnitudeA * magnitudeB);
  }
  
  private getTopKIndices(similarities: number[], k: number): number[] {
    const indexedSimilarities = similarities.map((sim, index) => ({ sim, index }));
    indexedSimilarities.sort((a, b) => b.sim - a.sim);
    return indexedSimilarities.slice(0, k).map(item => item.index);
  }
}

与外部向量数据库集成

typescript
// 与 Pinecone 集成的示例
class PineconeVectorStore implements VectorStore {
  private pineconeClient: any;
  private indexName: string;
  private embeddingFunction: (texts: string[]) => Promise<number[][]>;
  
  constructor(
    pineconeClient: any,
    indexName: string,
    embeddingFunction: (texts: string[]) => Promise<number[][]>
  ) {
    this.pineconeClient = pineconeClient;
    this.indexName = indexName;
    this.embeddingFunction = embeddingFunction;
  }
  
  async addDocuments(documents: Document[]): Promise<void> {
    const texts = documents.map(d => d.pageContent);
    const embeddings = await this.embeddingFunction(texts);
    
    const vectors = documents.map((doc, index) => ({
      id: this.generateId(),
      values: embeddings[index],
      metadata: {
        ...doc.metadata,
        pageContent: doc.pageContent
      }
    }));
    
    await this.pineconeClient.upsert({
      indexName: this.indexName,
      vectors
    });
  }
  
  async similaritySearch(query: string, k: number): Promise<Document[]> {
    const queryEmbedding = await this.embeddingFunction([query]);
    
    const response = await this.pineconeClient.query({
      indexName: this.indexName,
      queryRequest: {
        vector: queryEmbedding[0],
        topK: k,
        includeMetadata: true
      }
    });
    
    return response.matches.map((match: any) => ({
      pageContent: match.metadata.pageContent,
      metadata: match.metadata
    }));
  }
  
  private generateId(): string {
    return Math.random().toString(36).substring(2, 15) + 
           Math.random().toString(36).substring(2, 15);
  }
}

实际应用示例

让我们看一个完整的实际应用示例,展示如何使用 VectorStoreRetriever:

typescript
// 创建一个文档问答系统
class DocumentQASystem {
  private retriever: VectorStoreRetriever;
  private llm: BaseChatModel;
  
  constructor(retriever: VectorStoreRetriever, llm: BaseChatModel) {
    this.retriever = retriever;
    this.llm = llm;
  }
  
  async answerQuestion(question: string): Promise<string> {
    try {
      // 第一步:检索相关文档
      console.log('检索相关文档...');
      const documents = await this.retriever.invoke(question);
      
      if (documents.length === 0) {
        return "抱歉,我没有找到相关的信息来回答您的问题。";
      }
      
      console.log(`找到 ${documents.length} 个相关文档`);
      
      // 第二步:构建上下文
      const context = this.formatDocuments(documents);
      
      // 第三步:生成答案
      console.log('生成答案...');
      const prompt = new PromptTemplate({
        template: `基于以下文档内容回答问题。如果文档中没有相关信息,请说明无法回答。
        
文档内容:
{context}

问题: {question}

答案:`,
        inputVariables: ["context", "question"]
      });
      
      const answer = await prompt
        .pipe(this.llm)
        .pipe(new StringOutputParser())
        .invoke({
          context,
          question
        });
      
      return answer;
    } catch (error) {
      console.error('问答系统错误:', error);
      return "抱歉,处理您的问题时出现了错误。";
    }
  }
  
  private formatDocuments(documents: Document[]): string {
    return documents
      .map((doc, index) => 
        `[文档 ${index + 1}]\n${doc.pageContent}\n来源: ${JSON.stringify(doc.metadata)}`
      )
      .join('\n\n');
  }
  
  // 流式问答
  async *streamAnswer(question: string): AsyncGenerator<string> {
    try {
      // 检索文档
      const documents = await this.retriever.invoke(question);
      
      if (documents.length === 0) {
        yield "抱歉,我没有找到相关的信息来回答您的问题。";
        return;
      }
      
      const context = this.formatDocuments(documents);
      
      // 流式生成答案
      const prompt = new PromptTemplate({
        template: `基于以下文档内容回答问题。如果文档中没有相关信息,请说明无法回答。
        
文档内容:
{context}

问题: {question}

答案:`,
        inputVariables: ["context", "question"]
      });
      
      const stream = await prompt
        .pipe(this.llm)
        .pipe(new StringOutputParser())
        .stream({
          context,
          question
        });
      
      for await (const chunk of stream) {
        yield chunk;
      }
    } catch (error) {
      console.error('流式问答错误:', error);
      yield "抱歉,处理您的问题时出现了错误。";
    }
  }
}

// 创建和初始化系统
async function createQASystem() {
  // 创建向量存储
  const vectorStore = new MemoryVectorStore();
  
  // 添加示例文档
  const sampleDocuments: Document[] = [
    {
      pageContent: "LangChain 是一个用于开发由语言模型驱动的应用程序的框架。它可以帮助开发者将语言模型与其他数据源和计算逻辑结合起来。",
      metadata: { source: "langchain-docs", category: "introduction" }
    },
    {
      pageContent: "LCEL (LangChain Expression Language) 是 LangChain V3 中引入的表达式语言。它允许开发者使用管道操作符 (|) 来组合不同的组件。",
      metadata: { source: "langchain-docs", category: "lcel" }
    },
    {
      pageContent: "Runnable 是 LangChain V3 中的核心概念。所有组件都实现了 Runnable 接口,包括 PromptTemplate、LLM、OutputParser 等。",
      metadata: { source: "langchain-docs", category: "core-concepts" }
    },
    {
      pageContent: "VectorStoreRetriever 是用于从向量存储中检索相关文档的组件。它实现了 Runnable<string, Document[]> 接口。",
      metadata: { source: "langchain-docs", category: "retrieval" }
    }
  ];
  
  await vectorStore.addDocuments(sampleDocuments);
  
  // 创建检索器
  const retriever = new VectorStoreRetriever({
    vectorStore,
    k: 3,
    filter: (doc) => doc.metadata.category !== 'internal' // 过滤内部文档
  });
  
  // 创建问答系统
  const qaSystem = new DocumentQASystem(
    retriever,
    new ChatOpenAI({ modelName: "gpt-3.5-turbo" })
  );
  
  return qaSystem;
}

// 使用示例
async function demonstrateQASystem() {
  const qaSystem = await createQASystem();
  
  // 普通问答
  const answer = await qaSystem.answerQuestion("什么是 VectorStoreRetriever?");
  console.log('答案:', answer);
  
  // 流式问答
  console.log('\n流式回答:');
  for await (const chunk of qaSystem.streamAnswer("LangChain 的核心概念是什么?")) {
    process.stdout.write(chunk);
  }
  console.log('\n');
}

高级检索策略

VectorStoreRetriever 还支持更高级的检索策略:

typescript
// 支持多种检索模式的增强版检索器
class AdvancedVectorStoreRetriever extends VectorStoreRetriever {
  private searchType: 'similarity' | 'mmr' | 'hybrid';
  private fetchK?: number; // MMR 搜索时获取的候选文档数
  
  constructor(args: VectorStoreRetrieverArgs & {
    searchType?: 'similarity' | 'mmr' | 'hybrid';
    fetchK?: number;
  }) {
    super(args);
    this.searchType = args.searchType ?? 'similarity';
    this.fetchK = args.fetchK;
  }
  
  async invoke(query: string, options?: RunnableConfig): Promise<Document[]> {
    switch (this.searchType) {
      case 'similarity':
        return await this.similaritySearch(query, this.k);
      
      case 'mmr':
        return await this.maxMarginalRelevanceSearch(
          query, 
          this.k, 
          this.fetchK
        );
      
      case 'hybrid':
        return await this.hybridSearch(query, this.k);
      
      default:
        return await this.similaritySearch(query, this.k);
    }
  }
  
  private async hybridSearch(query: string, k: number): Promise<Document[]> {
    // 结合关键词搜索和向量搜索
    const [vectorResults, keywordResults] = await Promise.all([
      this.similaritySearch(query, k * 2),
      this.keywordSearch(query, k * 2)
    ]);
    
    // 合并和重新排序结果
    const combinedResults = this.mergeResults(vectorResults, keywordResults, k);
    return combinedResults;
  }
  
  private async keywordSearch(query: string, k: number): Promise<Document[]> {
    // 简化的关键词搜索实现
    const queryTerms = query.toLowerCase().split(/\s+/);
    
    const scoredDocuments = (await this.vectorStore.getAllDocuments())
      .map(doc => {
        const content = doc.pageContent.toLowerCase();
        let score = 0;
        
        for (const term of queryTerms) {
          const matches = content.match(new RegExp(term, 'g'));
          score += matches ? matches.length : 0;
        }
        
        return { doc, score };
      })
      .filter(item => item.score > 0)
      .sort((a, b) => b.score - a.score)
      .slice(0, k);
    
    return scoredDocuments.map(item => item.doc);
  }
  
  private mergeResults(
    vectorResults: Document[], 
    keywordResults: Document[], 
    k: number
  ): Document[] {
    // 简单的合并策略:交替选择
    const merged: Document[] = [];
    const usedIds = new Set<string>();
    
    let i = 0, j = 0;
    while (merged.length < k && (i < vectorResults.length || j < keywordResults.length)) {
      // 添加向量搜索结果
      if (i < vectorResults.length) {
        const doc = vectorResults[i++];
        const id = this.getDocumentId(doc);
        if (!usedIds.has(id)) {
          merged.push(doc);
          usedIds.add(id);
        }
      }
      
      // 添加关键词搜索结果
      if (j < keywordResults.length && merged.length < k) {
        const doc = keywordResults[j++];
        const id = this.getDocumentId(doc);
        if (!usedIds.has(id)) {
          merged.push(doc);
          usedIds.add(id);
        }
      }
    }
    
    return merged;
  }
  
  private getDocumentId(doc: Document): string {
    // 生成文档唯一标识符
    return doc.metadata.id || 
           this.hashCode(doc.pageContent + JSON.stringify(doc.metadata));
  }
  
  private hashCode(str: string): string {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash).toString();
  }
}

与 LCEL 的集成

VectorStoreRetriever 可以无缝集成到 LCEL 管道中:

typescript
// 创建完整的 RAG 链
async function createRAGChain() {
  const vectorStore = new MemoryVectorStore();
  // ... 初始化向量存储
  
  const retriever = new VectorStoreRetriever({
    vectorStore,
    k: 4
  });
  
  const prompt = new PromptTemplate({
    template: `使用以下文档内容回答问题:
    
文档:
{context}

问题: {question}

答案:`,
    inputVariables: ["context", "question"]
  });
  
  const llm = new ChatOpenAI({ modelName: "gpt-3.5-turbo" });
  const parser = new StringOutputParser();
  
  // 构建 LCEL 链
  const ragChain = 
    RunnableMap.from({
      question: (input: string) => input,
      context: retriever.pipe((docs: Document[]) => 
        docs.map(doc => doc.pageContent).join('\n\n')
      )
    })
    .pipe(prompt)
    .pipe(llm)
    .pipe(parser);
  
  return ragChain;
}

// 使用 RAG 链
async function useRAGChain() {
  const ragChain = await createRAGChain();
  
  const answer = await ragChain.invoke("什么是 LCEL?");
  console.log('RAG 链回答:', answer);
}

总结

VectorStoreRetriever 作为 Runnable<Query, Document[]> 的实现,为 LangChain V3 提供了强大的检索能力:

  1. 标准化接口 - 实现 Runnable 接口,与其他组件保持一致
  2. 灵活集成 - 可以与多种向量存储集成
  3. 多种检索策略 - 支持相似度搜索、MMR 搜索等
  4. 过滤和排序 - 支持文档过滤和自定义排序
  5. LCEL 集成 - 可以无缝集成到 LCEL 管道中
  6. 批处理支持 - 支持批量查询处理

通过这些特性,VectorStoreRetriever 成为了构建检索增强生成(RAG)应用的核心组件,使得开发者能够轻松构建智能的问答和信息检索系统。

在下一章中,我们将探讨 ContextualCompressionRetriever:在检索后压缩上下文,了解如何优化检索结果的相关性。