use lc_core::language_models::BaseChatModel;
use lc_core::tools::ToolDefinition;
use lc_rag::RetrieverTrait;
use lc_schema::Message;
use lc_vector_stores::Document;
use serde_json::json;
mod prompts;
#[cfg(test)]
mod tests;
mod types;
pub use types::{AdaptiveRAGError, AdaptiveRAGResult, RagDecision};
use prompts::{GENERATE_SYSTEM_PROMPT, MULTI_QUERY_PROMPT, ROUTING_PROMPT};
pub struct AdaptiveRAG<M: BaseChatModel, R: RetrieverTrait> {
llm: M,
retriever: R,
retrieve_k: usize,
multi_query_count: usize,
}
impl<M: BaseChatModel, R: RetrieverTrait> AdaptiveRAG<M, R> {
pub fn new(llm: M, retriever: R) -> Self {
Self {
llm,
retriever,
retrieve_k: 4,
multi_query_count: 3,
}
}
pub fn with_retrieve_k(mut self, k: usize) -> Self {
self.retrieve_k = k;
self
}
pub fn with_multi_query_count(mut self, count: usize) -> Self {
self.multi_query_count = count;
self
}
pub async fn invoke(&self, query: &str) -> Result<AdaptiveRAGResult, AdaptiveRAGError> {
let decision = self.route(query).await?;
match decision {
RagDecision::NoRetrieval => self.generate_no_retrieval(query).await,
RagDecision::SingleSearch => self.generate_single_search(query).await,
RagDecision::MultiQuery => self.generate_multi_query(query).await,
}
}
pub async fn stream(
&self,
query: &str,
) -> Result<
std::pin::Pin<
Box<dyn futures_util::Stream<Item = crate::streaming::AgentStreamEvent> + Send>,
>,
AdaptiveRAGError,
> {
use crate::streaming::AgentStreamEvent;
let mut events: Vec<AgentStreamEvent> = Vec::new();
events.push(AgentStreamEvent::PipelineStep {
step: "routing".to_string(),
detail: Some("Classifying query...".to_string()),
});
let decision = self.route(query).await?;
events.push(AgentStreamEvent::PipelineStep {
step: "routed".to_string(),
detail: Some(format!("Decision: {}", decision)),
});
let result = match decision {
RagDecision::NoRetrieval => {
events.push(AgentStreamEvent::PipelineStep {
step: "generating".to_string(),
detail: Some("No retrieval needed, generating directly...".to_string()),
});
self.generate_no_retrieval(query).await?
}
RagDecision::SingleSearch => {
events.push(AgentStreamEvent::PipelineStep {
step: "retrieving".to_string(),
detail: Some("Single search retrieval...".to_string()),
});
let result = self.generate_single_search(query).await?;
events.push(AgentStreamEvent::PipelineStep {
step: "generating".to_string(),
detail: Some(format!("Sources: {} documents", result.sources.len())),
});
result
}
RagDecision::MultiQuery => {
events.push(AgentStreamEvent::PipelineStep {
step: "multi_query".to_string(),
detail: Some("Generating multiple queries...".to_string()),
});
let result = self.generate_multi_query(query).await?;
events.push(AgentStreamEvent::PipelineStep {
step: "generating".to_string(),
detail: Some(format!("Sources: {} documents", result.sources.len())),
});
result
}
};
events.push(AgentStreamEvent::FinalAnswer {
content: result.answer,
});
Ok(Box::pin(futures_util::stream::iter(events)))
}
async fn route(&self, query: &str) -> Result<RagDecision, AdaptiveRAGError> {
let prompt = ROUTING_PROMPT.replace("{query}", query);
let messages = vec![Message::human(&prompt)];
let structured = crate::structured::chat_structured(
&self.llm,
Some(route_tool()),
messages,
None,
&crate::retry::RetryConfig::default(),
)
.await
.map_err(|e| AdaptiveRAGError::Llm(e.to_string()))?;
if let Some(args) = &structured.tool_args {
if let Some(decision) = args.get("decision").and_then(|v| v.as_str()) {
return parse_decision(decision);
}
}
parse_decision(&structured.content)
}
async fn generate_no_retrieval(
&self,
query: &str,
) -> Result<AdaptiveRAGResult, AdaptiveRAGError> {
let messages = vec![Message::human(query)];
let result = self
.llm
.chat_with_system(GENERATE_SYSTEM_PROMPT.to_string(), messages)
.await
.map_err(|e| AdaptiveRAGError::Llm(e.to_string()))?;
Ok(AdaptiveRAGResult {
answer: result.content,
decision: RagDecision::NoRetrieval,
sources: Vec::new(),
})
}
async fn generate_single_search(
&self,
query: &str,
) -> Result<AdaptiveRAGResult, AdaptiveRAGError> {
let docs = self.retriever.retrieve(query, self.retrieve_k).await?;
let context = build_context(&docs);
let user_msg = format!("Context:\n{}\n\nQuestion: {}\n\nAnswer:", context, query);
let messages = vec![Message::human(&user_msg)];
let result = self
.llm
.chat_with_system(GENERATE_SYSTEM_PROMPT.to_string(), messages)
.await
.map_err(|e| AdaptiveRAGError::Llm(e.to_string()))?;
Ok(AdaptiveRAGResult {
answer: result.content,
decision: RagDecision::SingleSearch,
sources: docs,
})
}
async fn generate_multi_query(
&self,
query: &str,
) -> Result<AdaptiveRAGResult, AdaptiveRAGError> {
let alternative_queries = self.generate_queries(query).await?;
let all_queries: Vec<String> = std::iter::once(query.to_string())
.chain(alternative_queries)
.collect();
let docs = self.retrieve_and_merge(&all_queries).await?;
let context = build_context(&docs);
let user_msg = format!("Context:\n{}\n\nQuestion: {}\n\nAnswer:", context, query);
let messages = vec![Message::human(&user_msg)];
let result = self
.llm
.chat_with_system(GENERATE_SYSTEM_PROMPT.to_string(), messages)
.await
.map_err(|e| AdaptiveRAGError::Llm(e.to_string()))?;
Ok(AdaptiveRAGResult {
answer: result.content,
decision: RagDecision::MultiQuery,
sources: docs,
})
}
async fn generate_queries(&self, query: &str) -> Result<Vec<String>, AdaptiveRAGError> {
let prompt = MULTI_QUERY_PROMPT
.replace("{count}", &self.multi_query_count.to_string())
.replace("{question}", query);
let messages = vec![Message::human(&prompt)];
let result = crate::retry::retry_chat(
&self.llm,
messages,
None,
&crate::retry::RetryConfig::default(),
)
.await
.map_err(|e| AdaptiveRAGError::Llm(e.to_string()))?;
let queries: Vec<String> = result
.content
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| line.trim().to_string())
.collect();
Ok(queries)
}
async fn retrieve_and_merge(
&self,
queries: &[String],
) -> Result<Vec<Document>, AdaptiveRAGError> {
let futures: Vec<_> = queries
.iter()
.map(|q| self.retriever.retrieve(q, self.retrieve_k))
.collect();
let all_results = futures_util::future::join_all(futures).await;
let mut seen_content: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut merged: Vec<Document> = Vec::new();
for result in all_results {
let docs = result?;
for doc in docs {
let key = {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
hasher.write(doc.content.as_bytes());
format!("{:016x}", hasher.finish())
};
if seen_content.insert(key) {
merged.push(doc);
}
}
}
Ok(merged)
}
}
fn route_tool() -> ToolDefinition {
ToolDefinition::new(
"route_decision",
"判断查询是否需要检索及检索策略:no_retrieval / single_search / multi_query",
)
.with_parameters(json!({
"type": "object",
"properties": {
"decision": {
"type": "string",
"enum": ["no_retrieval", "single_search", "multi_query"]
}
},
"required": ["decision"]
}))
}
fn parse_decision(response: &str) -> Result<RagDecision, AdaptiveRAGError> {
let lower = response.to_lowercase();
if lower.contains("no_retrieval") {
return Ok(RagDecision::NoRetrieval);
}
if lower.contains("multi_query") {
return Ok(RagDecision::MultiQuery);
}
if lower.contains("single_search") {
return Ok(RagDecision::SingleSearch);
}
Err(AdaptiveRAGError::DecisionParse(response.to_string()))
}
fn build_context(docs: &[Document]) -> String {
docs.iter()
.enumerate()
.map(|(i, doc)| format!("[Document {}]: {}", i + 1, doc.content))
.collect::<Vec<_>>()
.join("\n\n")
}