1use lc_core::language_models::BaseChatModel;
7use lc_core::tools::ToolDefinition;
8use lc_prompts::PromptTemplate;
9use lc_providers::ProviderError;
10use lc_schema::Message;
11use lc_vector_stores::{Document, SearchResult};
12use serde_json::json;
13
14use crate::retriever::RetrieverTrait;
15use crate::structured::chat_structured;
16use std::collections::HashMap;
17use std::sync::Arc;
18
19fn doc_content_hash(content: &str) -> String {
24 use std::hash::{Hash, Hasher};
25 let mut hasher = fnv::FnvHasher::default();
26 content.hash(&mut hasher);
27 format!("{:016x}", hasher.finish())
28}
29
30#[derive(Debug)]
32#[non_exhaustive]
33pub enum MultiQueryError {
34 LLMError(String),
36
37 RetrieverError(String),
39
40 ParseError(String),
42}
43
44impl std::fmt::Display for MultiQueryError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 MultiQueryError::LLMError(msg) => write!(f, "LLM error: {}", msg),
48 MultiQueryError::RetrieverError(msg) => write!(f, "Retriever error: {}", msg),
49 MultiQueryError::ParseError(msg) => write!(f, "Parse error: {}", msg),
50 }
51 }
52}
53
54impl std::error::Error for MultiQueryError {}
55
56pub struct MultiQueryConfig {
58 pub num_queries: usize,
60
61 pub k_per_query: usize,
63
64 pub final_k: usize,
66
67 pub prompt_template: String,
69}
70
71impl Default for MultiQueryConfig {
72 fn default() -> Self {
73 Self {
74 num_queries: 3,
75 k_per_query: 5,
76 final_k: 10,
77 prompt_template: DEFAULT_MULTI_QUERY_PROMPT.to_string(),
78 }
79 }
80}
81
82impl MultiQueryConfig {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn with_num_queries(mut self, n: usize) -> Self {
90 self.num_queries = n;
91 self
92 }
93
94 pub fn with_k_per_query(mut self, k: usize) -> Self {
96 self.k_per_query = k;
97 self
98 }
99
100 pub fn with_final_k(mut self, k: usize) -> Self {
102 self.final_k = k;
103 self
104 }
105
106 pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
108 self.prompt_template = prompt.into();
109 self
110 }
111}
112
113const 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.
114
115By generating multiple perspectives on the user question, your goal is to help overcome some of the limitations of distance-based similarity search.
116
117Provide these alternative questions separated by newlines.
118
119Original question: {question}
120
121Alternative questions:"#;
122
123pub struct MultiQueryRetriever {
128 llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
132
133 base_retriever: Arc<dyn RetrieverTrait>,
135
136 config: MultiQueryConfig,
138}
139
140impl MultiQueryRetriever {
141 pub fn new<L>(llm: L, base_retriever: Arc<dyn RetrieverTrait>) -> Self
143 where
144 L: BaseChatModel + Send + Sync + 'static,
145 L::Error: Into<ProviderError>,
146 {
147 Self {
148 llm: lc_providers::wrap_chat_model(llm),
149 base_retriever,
150 config: MultiQueryConfig::default(),
151 }
152 }
153
154 pub fn new_arc(
156 llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
157 base_retriever: Arc<dyn RetrieverTrait>,
158 ) -> Self {
159 Self {
160 llm,
161 base_retriever,
162 config: MultiQueryConfig::default(),
163 }
164 }
165
166 pub fn with_config(mut self, config: MultiQueryConfig) -> Self {
168 self.config = config;
169 self
170 }
171
172 pub fn with_num_queries(mut self, n: usize) -> Self {
174 self.config.num_queries = n;
175 self
176 }
177
178 pub fn with_k_per_query(mut self, k: usize) -> Self {
180 self.config.k_per_query = k;
181 self
182 }
183
184 pub fn with_final_k(mut self, k: usize) -> Self {
186 self.config.final_k = k;
187 self
188 }
189
190 async fn generate_queries(&self, original_query: &str) -> Result<Vec<String>, MultiQueryError> {
191 let template = PromptTemplate::new(&self.config.prompt_template);
192 let mut vars = HashMap::new();
193 vars.insert("question", original_query);
194 let prompt = template
195 .format(&vars)
196 .unwrap_or_else(|_| self.config.prompt_template.clone());
197
198 const MAX_RETRIES: usize = 1;
201 let mut current_prompt = prompt;
202
203 for attempt in 0..=MAX_RETRIES {
204 let result = chat_structured(
205 self.llm.as_ref(),
206 Some(queries_tool()),
207 vec![Message::human(¤t_prompt)],
208 )
209 .await
210 .map_err(|e| MultiQueryError::LLMError(e.to_string()))?;
211
212 if let Some(args) = &result.tool_args {
214 if let Some(queries) = parse_queries(args) {
215 if !queries.is_empty() {
216 return Ok(queries);
217 }
218 }
219 }
220
221 let queries = parse_query_lines(&result.content, self.config.num_queries);
223 if !queries.is_empty() {
224 return Ok(queries);
225 }
226
227 if attempt < MAX_RETRIES {
228 current_prompt = format!(
229 "上次的输出不是有效的查询列表。请重新为原问题生成 {} 个不同的查询变体,\
230 每行一个,不要编号、不要项目符号、不要解释或多余文字。\n\n原问题:{}\n\n\
231 上次输出(无效):\n{}\n\n新的查询变体:",
232 self.config.num_queries, original_query, result.content
233 );
234 }
235 }
236
237 Err(MultiQueryError::ParseError(
238 "LLM did not generate valid query variants".to_string(),
239 ))
240 }
241
242 pub async fn retrieve_multi(&self, query: &str) -> Result<Vec<Document>, MultiQueryError> {
244 let queries = self.generate_queries(query).await?;
245
246 let all_queries: Vec<String> = std::iter::once(query.to_string()).chain(queries).collect();
247
248 let mut doc_scores: HashMap<String, (Document, f32)> = HashMap::new();
249
250 for q in &all_queries {
251 let results = self
252 .base_retriever
253 .retrieve_with_scores(q, self.config.k_per_query)
254 .await
255 .map_err(|e| MultiQueryError::RetrieverError(e.to_string()))?;
256
257 for result in results {
258 let doc_id = result
259 .document
260 .id
261 .clone()
262 .unwrap_or_else(|| doc_content_hash(&result.document.content));
263
264 doc_scores
265 .entry(doc_id)
266 .and_modify(|(_, score)| {
267 *score += result.score;
269 })
270 .or_insert((result.document.clone(), result.score));
271 }
272 }
273
274 let mut scored_docs: Vec<(Document, f32)> = doc_scores
275 .values()
276 .map(|(doc, score)| (doc.clone(), *score))
277 .collect();
278
279 scored_docs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
280
281 let final_docs: Vec<Document> = scored_docs
282 .into_iter()
283 .take(self.config.final_k)
284 .map(|(doc, _)| doc)
285 .collect();
286
287 Ok(final_docs)
288 }
289
290 pub async fn retrieve_multi_with_scores(
292 &self,
293 query: &str,
294 ) -> Result<Vec<SearchResult>, MultiQueryError> {
295 let queries = self.generate_queries(query).await?;
296
297 let all_queries: Vec<String> = std::iter::once(query.to_string()).chain(queries).collect();
298
299 let mut doc_scores: HashMap<String, (Document, f32, usize)> = HashMap::new();
300
301 for q in &all_queries {
302 let results = self
303 .base_retriever
304 .retrieve_with_scores(q, self.config.k_per_query)
305 .await
306 .map_err(|e| MultiQueryError::RetrieverError(e.to_string()))?;
307
308 for result in results {
309 let doc_id = result
310 .document
311 .id
312 .clone()
313 .unwrap_or_else(|| doc_content_hash(&result.document.content));
314
315 doc_scores
316 .entry(doc_id)
317 .and_modify(|(_, score, count)| {
318 *score += result.score;
320 *count += 1;
321 })
322 .or_insert((result.document.clone(), result.score, 1));
323 }
324 }
325
326 let mut scored_docs: Vec<SearchResult> = doc_scores
327 .values()
328 .map(|(doc, score, count)| {
329 let combined_score = score * (1.0 + 0.1 * *count as f32);
330 SearchResult {
331 document: doc.clone(),
332 score: combined_score,
333 }
334 })
335 .collect();
336
337 scored_docs.sort_by(|a, b| {
338 b.score
339 .partial_cmp(&a.score)
340 .unwrap_or(std::cmp::Ordering::Equal)
341 });
342
343 let final_results: Vec<SearchResult> =
344 scored_docs.into_iter().take(self.config.final_k).collect();
345
346 Ok(final_results)
347 }
348
349 pub async fn get_generated_queries(&self, query: &str) -> Result<Vec<String>, MultiQueryError> {
351 self.generate_queries(query).await
352 }
353}
354
355fn queries_tool() -> ToolDefinition {
357 ToolDefinition::new(
358 "generate_queries",
359 "为原问题生成多个不同的检索查询变体,返回查询字符串数组",
360 )
361 .with_parameters(json!({
362 "type": "object",
363 "properties": {
364 "queries": {
365 "type": "array",
366 "items": { "type": "string" }
367 }
368 },
369 "required": ["queries"]
370 }))
371}
372
373fn parse_queries(args: &serde_json::Value) -> Option<Vec<String>> {
375 args.get("queries")?
376 .as_array()?
377 .iter()
378 .map(|v| v.as_str().map(|s| s.trim().to_string()))
379 .collect()
380}
381
382fn parse_query_lines(content: &str, limit: usize) -> Vec<String> {
388 content
389 .lines()
390 .map(|line| line.trim())
391 .filter(|line| !line.is_empty())
392 .map(|line| {
393 let stripped = line.trim_start_matches(['-', '•', '*', ' ']);
394 let stripped = stripped.trim_start_matches(|c: char| {
395 c.is_ascii_digit() || c == '.' || c == '、' || c == ')' || c == ' '
396 });
397 stripped
398 .trim_matches(['"', '\'', '“', '”'])
399 .trim()
400 .to_string()
401 })
402 .filter(|q| !q.is_empty())
403 .take(limit)
404 .collect()
405}
406
407#[allow(clippy::type_complexity)]
409pub struct StaticQueryGenerator {
410 expansions: Vec<Box<dyn Fn(&str) -> Vec<String> + Send + Sync>>,
411}
412
413impl StaticQueryGenerator {
414 pub fn new() -> Self {
416 Self {
417 expansions: Vec::new(),
418 }
419 }
420
421 pub fn with_synonym_expansion(mut self, synonyms: HashMap<String, Vec<String>>) -> Self {
423 self.expansions.push(Box::new(move |query: &str| {
424 let mut expanded = Vec::new();
425 for (word, syns) in &synonyms {
426 if query.contains(word) {
427 for syn in syns {
428 expanded.push(query.replace(word, syn));
429 }
430 }
431 }
432 expanded
433 }));
434 self
435 }
436
437 pub fn with_prefix_expansion(mut self, prefixes: Vec<String>) -> Self {
439 self.expansions.push(Box::new(move |query: &str| {
440 prefixes
441 .iter()
442 .map(|p| format!("{} {}", p, query))
443 .collect()
444 }));
445 self
446 }
447
448 pub fn generate(&self, query: &str) -> Vec<String> {
450 self.expansions
451 .iter()
452 .flat_map(|exp| exp(query))
453 .filter(|q| q != query)
454 .collect()
455 }
456}
457
458impl Default for StaticQueryGenerator {
459 fn default() -> Self {
460 Self::new()
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_static_query_generator_synonym() {
470 let synonyms: HashMap<String, Vec<String>> = HashMap::from([(
471 "数据库".to_string(),
472 vec!["DB".to_string(), "存储".to_string()],
473 )]);
474
475 let generator = StaticQueryGenerator::new().with_synonym_expansion(synonyms);
476
477 let queries = generator.generate("数据库连接失败");
478
479 assert!(queries.contains(&"DB连接失败".to_string()));
480 assert!(queries.contains(&"存储连接失败".to_string()));
481 }
482
483 #[test]
484 fn test_static_query_generator_prefix() {
485 let generator = StaticQueryGenerator::new()
486 .with_prefix_expansion(vec!["如何".to_string(), "怎么".to_string()]);
487
488 let queries = generator.generate("处理错误");
489
490 assert!(queries.contains(&"如何 处理错误".to_string()));
491 assert!(queries.contains(&"怎么 处理错误".to_string()));
492 }
493
494 #[test]
495 fn test_multi_query_config() {
496 let config = MultiQueryConfig::new()
497 .with_num_queries(5)
498 .with_k_per_query(10)
499 .with_final_k(20);
500
501 assert_eq!(config.num_queries, 5);
502 assert_eq!(config.k_per_query, 10);
503 assert_eq!(config.final_k, 20);
504 }
505
506 #[test]
507 fn test_multi_query_config_default() {
508 let config = MultiQueryConfig::default();
509
510 assert_eq!(config.num_queries, 3);
511 assert_eq!(config.k_per_query, 5);
512 assert_eq!(config.final_k, 10);
513 }
514
515 #[test]
517 fn test_queries_tool_schema() {
518 let tool = queries_tool();
519 assert_eq!(tool.function.name, "generate_queries");
520 let params = tool.function.parameters.expect("parameters should exist");
521 assert_eq!(params["properties"]["queries"]["type"], "array");
522 }
523
524 #[test]
526 fn test_parse_queries() {
527 let args = json!({ "queries": ["数据库连接失败怎么办", "DB 连接错误排查"] });
528 let queries = parse_queries(&args).expect("should parse successfully");
529 assert_eq!(queries.len(), 2);
530 assert_eq!(queries[0], "数据库连接失败怎么办");
531 }
532
533 #[test]
535 fn test_parse_queries_missing_key() {
536 let args = json!({ "other": 1 });
537 assert!(parse_queries(&args).is_none());
538 }
539
540 #[test]
542 fn test_parse_query_lines_cleanup() {
543 let content = "1. 数据库连接失败\n- 如何排查 DB 错误\n• \"连接超时怎么办\"\n\n补充解释";
544 let queries = parse_query_lines(content, 3);
545 assert_eq!(
546 queries,
547 vec![
548 "数据库连接失败".to_string(),
549 "如何排查 DB 错误".to_string(),
550 "连接超时怎么办".to_string(),
551 ]
552 );
553 }
554
555 #[test]
557 fn test_parse_query_lines_empty() {
558 assert!(parse_query_lines(" \n\n", 3).is_empty());
559 }
560
561 #[test]
563 fn test_parse_query_lines_capped() {
564 let content = "a\nb\nc\nd";
565 assert_eq!(
566 parse_query_lines(content, 2),
567 vec!["a".to_string(), "b".to_string()]
568 );
569 }
570}