1use lc_core::Runnable;
7use lc_prompts::PromptTemplate;
8use lc_providers::OpenAIChat;
9use lc_schema::Message;
10use lc_vector_stores::{Document, SearchResult};
11
12use crate::retriever::RetrieverTrait;
13use std::collections::HashMap;
14use std::sync::Arc;
15
16fn doc_content_hash(content: &str) -> String {
18 use std::hash::{Hash, Hasher};
19 let mut hasher = std::collections::hash_map::DefaultHasher::new();
20 content.hash(&mut hasher);
21 format!("{:016x}", hasher.finish())
22}
23
24#[derive(Debug)]
26pub enum MultiQueryError {
27 LLMError(String),
29
30 RetrieverError(String),
32
33 ParseError(String),
35}
36
37impl std::fmt::Display for MultiQueryError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 MultiQueryError::LLMError(msg) => write!(f, "LLM 错误: {}", msg),
41 MultiQueryError::RetrieverError(msg) => write!(f, "检索错误: {}", msg),
42 MultiQueryError::ParseError(msg) => write!(f, "解析错误: {}", msg),
43 }
44 }
45}
46
47impl std::error::Error for MultiQueryError {}
48
49pub struct MultiQueryConfig {
51 pub num_queries: usize,
53
54 pub k_per_query: usize,
56
57 pub final_k: usize,
59
60 pub prompt_template: String,
62}
63
64impl Default for MultiQueryConfig {
65 fn default() -> Self {
66 Self {
67 num_queries: 3,
68 k_per_query: 5,
69 final_k: 10,
70 prompt_template: DEFAULT_MULTI_QUERY_PROMPT.to_string(),
71 }
72 }
73}
74
75impl MultiQueryConfig {
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn with_num_queries(mut self, n: usize) -> Self {
81 self.num_queries = n;
82 self
83 }
84
85 pub fn with_k_per_query(mut self, k: usize) -> Self {
86 self.k_per_query = k;
87 self
88 }
89
90 pub fn with_final_k(mut self, k: usize) -> Self {
91 self.final_k = k;
92 self
93 }
94
95 pub fn with_prompt(mut self, prompt: String) -> Self {
96 self.prompt_template = prompt;
97 self
98 }
99}
100
101const DEFAULT_MULTI_QUERY_PROMPT: &str = r#"You are an AI language model assistant. Your task is to generate 3 different versions of the given user question to retrieve relevant documents from a vector database.
102
103By generating multiple perspectives on the user question, your goal is to help overcome some of the limitations of distance-based similarity search.
104
105Provide these alternative questions separated by newlines.
106
107Original question: {question}
108
109Alternative questions:"#;
110
111pub struct MultiQueryRetriever {
116 llm: OpenAIChat,
118
119 base_retriever: Arc<dyn RetrieverTrait>,
121
122 config: MultiQueryConfig,
124}
125
126impl MultiQueryRetriever {
127 pub fn new(llm: OpenAIChat, base_retriever: Arc<dyn RetrieverTrait>) -> Self {
128 Self {
129 llm,
130 base_retriever,
131 config: MultiQueryConfig::default(),
132 }
133 }
134
135 pub fn with_config(mut self, config: MultiQueryConfig) -> Self {
136 self.config = config;
137 self
138 }
139
140 pub fn with_num_queries(mut self, n: usize) -> Self {
141 self.config.num_queries = n;
142 self
143 }
144
145 pub fn with_k_per_query(mut self, k: usize) -> Self {
146 self.config.k_per_query = k;
147 self
148 }
149
150 pub fn with_final_k(mut self, k: usize) -> Self {
151 self.config.final_k = k;
152 self
153 }
154
155 async fn generate_queries(&self, original_query: &str) -> Result<Vec<String>, MultiQueryError> {
156 let template = PromptTemplate::new(&self.config.prompt_template);
157 let mut vars = HashMap::new();
158 vars.insert("question", original_query);
159 let prompt = template
160 .format(&vars)
161 .unwrap_or_else(|_| self.config.prompt_template.clone());
162
163 let messages = vec![Message::human(prompt)];
164
165 let response = self
166 .llm
167 .invoke(messages, None)
168 .await
169 .map_err(|e| MultiQueryError::LLMError(e.to_string()))?;
170
171 let content = response.content;
172
173 let queries: Vec<String> = content
174 .lines()
175 .filter(|line| !line.trim().is_empty())
176 .map(|line| line.trim().to_string())
177 .collect();
178
179 if queries.is_empty() {
180 return Err(MultiQueryError::ParseError(
181 "LLM 未生成有效的查询变体".to_string(),
182 ));
183 }
184
185 Ok(queries)
186 }
187
188 pub async fn retrieve_multi(&self, query: &str) -> Result<Vec<Document>, MultiQueryError> {
189 let queries = self.generate_queries(query).await?;
190
191 let all_queries: Vec<String> = std::iter::once(query.to_string()).chain(queries).collect();
192
193 let mut doc_scores: HashMap<String, (Document, f32)> = HashMap::new();
194
195 for q in &all_queries {
196 let results = self
197 .base_retriever
198 .retrieve_with_scores(q, self.config.k_per_query)
199 .await
200 .map_err(|e| MultiQueryError::RetrieverError(e.to_string()))?;
201
202 for result in results {
203 let doc_id = result
204 .document
205 .id
206 .clone()
207 .unwrap_or_else(|| doc_content_hash(&result.document.content));
208
209 doc_scores
210 .entry(doc_id)
211 .and_modify(|(_, score)| {
212 *score += result.score;
214 })
215 .or_insert((result.document.clone(), result.score));
216 }
217 }
218
219 let mut scored_docs: Vec<(Document, f32)> = doc_scores
220 .values()
221 .map(|(doc, score)| (doc.clone(), *score))
222 .collect();
223
224 scored_docs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
225
226 let final_docs: Vec<Document> = scored_docs
227 .into_iter()
228 .take(self.config.final_k)
229 .map(|(doc, _)| doc)
230 .collect();
231
232 Ok(final_docs)
233 }
234
235 pub async fn retrieve_multi_with_scores(
236 &self,
237 query: &str,
238 ) -> Result<Vec<SearchResult>, MultiQueryError> {
239 let queries = self.generate_queries(query).await?;
240
241 let all_queries: Vec<String> = std::iter::once(query.to_string()).chain(queries).collect();
242
243 let mut doc_scores: HashMap<String, (Document, f32, usize)> = HashMap::new();
244
245 for q in &all_queries {
246 let results = self
247 .base_retriever
248 .retrieve_with_scores(q, self.config.k_per_query)
249 .await
250 .map_err(|e| MultiQueryError::RetrieverError(e.to_string()))?;
251
252 for result in results {
253 let doc_id = result
254 .document
255 .id
256 .clone()
257 .unwrap_or_else(|| doc_content_hash(&result.document.content));
258
259 doc_scores
260 .entry(doc_id)
261 .and_modify(|(_, score, count)| {
262 *score += result.score;
264 *count += 1;
265 })
266 .or_insert((result.document.clone(), result.score, 1));
267 }
268 }
269
270 let mut scored_docs: Vec<SearchResult> = doc_scores
271 .values()
272 .map(|(doc, score, count)| {
273 let combined_score = score * (1.0 + 0.1 * *count as f32);
274 SearchResult {
275 document: doc.clone(),
276 score: combined_score,
277 }
278 })
279 .collect();
280
281 scored_docs.sort_by(|a, b| {
282 b.score
283 .partial_cmp(&a.score)
284 .unwrap_or(std::cmp::Ordering::Equal)
285 });
286
287 let final_results: Vec<SearchResult> =
288 scored_docs.into_iter().take(self.config.final_k).collect();
289
290 Ok(final_results)
291 }
292
293 pub async fn get_generated_queries(&self, query: &str) -> Result<Vec<String>, MultiQueryError> {
294 self.generate_queries(query).await
295 }
296}
297
298#[allow(clippy::type_complexity)]
300pub struct StaticQueryGenerator {
301 expansions: Vec<Box<dyn Fn(&str) -> Vec<String> + Send + Sync>>,
302}
303
304impl StaticQueryGenerator {
305 pub fn new() -> Self {
306 Self {
307 expansions: Vec::new(),
308 }
309 }
310
311 pub fn with_synonym_expansion(mut self, synonyms: HashMap<String, Vec<String>>) -> Self {
312 self.expansions.push(Box::new(move |query: &str| {
313 let mut expanded = Vec::new();
314 for (word, syns) in &synonyms {
315 if query.contains(word) {
316 for syn in syns {
317 expanded.push(query.replace(word, syn));
318 }
319 }
320 }
321 expanded
322 }));
323 self
324 }
325
326 pub fn with_prefix_expansion(mut self, prefixes: Vec<String>) -> Self {
327 self.expansions.push(Box::new(move |query: &str| {
328 prefixes
329 .iter()
330 .map(|p| format!("{} {}", p, query))
331 .collect()
332 }));
333 self
334 }
335
336 pub fn generate(&self, query: &str) -> Vec<String> {
337 self.expansions
338 .iter()
339 .flat_map(|exp| exp(query))
340 .filter(|q| q != query)
341 .collect()
342 }
343}
344
345impl Default for StaticQueryGenerator {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn test_static_query_generator_synonym() {
357 let synonyms: HashMap<String, Vec<String>> = HashMap::from([(
358 "数据库".to_string(),
359 vec!["DB".to_string(), "存储".to_string()],
360 )]);
361
362 let generator = StaticQueryGenerator::new().with_synonym_expansion(synonyms);
363
364 let queries = generator.generate("数据库连接失败");
365
366 assert!(queries.contains(&"DB连接失败".to_string()));
367 assert!(queries.contains(&"存储连接失败".to_string()));
368 }
369
370 #[test]
371 fn test_static_query_generator_prefix() {
372 let generator = StaticQueryGenerator::new()
373 .with_prefix_expansion(vec!["如何".to_string(), "怎么".to_string()]);
374
375 let queries = generator.generate("处理错误");
376
377 assert!(queries.contains(&"如何 处理错误".to_string()));
378 assert!(queries.contains(&"怎么 处理错误".to_string()));
379 }
380
381 #[test]
382 fn test_multi_query_config() {
383 let config = MultiQueryConfig::new()
384 .with_num_queries(5)
385 .with_k_per_query(10)
386 .with_final_k(20);
387
388 assert_eq!(config.num_queries, 5);
389 assert_eq!(config.k_per_query, 10);
390 assert_eq!(config.final_k, 20);
391 }
392
393 #[test]
394 fn test_multi_query_config_default() {
395 let config = MultiQueryConfig::default();
396
397 assert_eq!(config.num_queries, 3);
398 assert_eq!(config.k_per_query, 5);
399 assert_eq!(config.final_k, 10);
400 }
401}