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 提供了强大的检索能力:
- 标准化接口 - 实现 Runnable 接口,与其他组件保持一致
- 灵活集成 - 可以与多种向量存储集成
- 多种检索策略 - 支持相似度搜索、MMR 搜索等
- 过滤和排序 - 支持文档过滤和自定义排序
- LCEL 集成 - 可以无缝集成到 LCEL 管道中
- 批处理支持 - 支持批量查询处理
通过这些特性,VectorStoreRetriever 成为了构建检索增强生成(RAG)应用的核心组件,使得开发者能够轻松构建智能的问答和信息检索系统。
在下一章中,我们将探讨 ContextualCompressionRetriever:在检索后压缩上下文,了解如何优化检索结果的相关性。