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