qa_chain.py是整个RAG系统的大脑,它负责把用户输入的问题,经过清洗和格式化后,从向量库里检索出相关的文档,再交给大模型生成答案,最后把结果返回给用户,总的来说它的核心职责就是让模型更好的理解用户的每一个问题,增强模型的理解力和检索能力

5.1 问题处理模块

def process_question(question: str) -> str:
    """
    问题输入处理函数 
    
    Args:
        question: 原始问题文本
        
    Returns:
        str: 处理后的问题文本,直接用于向量检索
    """
    if not question or not isinstance(question, str):
        return ""
    
    # 去除首尾空格
    cleaned = question.strip()
    
    # 去除多余空格
    cleaned = re.sub(r'\s+', ' ', cleaned)
    
    # 去除特殊字符
    cleaned = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s]', '', cleaned)
    
    # 如果问题没有问号,自动添加问号
    if not (cleaned.endswith('?') or cleaned.endswith('?')):
        cleaned = cleaned + '?'
    
    return cleaned


def process_question_simple(question: str) -> str:
    """
    极致简化的版本 
    
    Args:
        question: 原始问题文本
        
    Returns:
        str: 处理后的问题文本
    """
    return question.strip() if question else ""

这里我提供了两个版本的问题处理函数,process_question适合对检索精度要求高的场景,它会去除特殊字符、合并多余空格、自动补全问号;而process_question_simple只做最基本的去空格处理,适合快速测试或者对延迟敏感的场景,可以根据自己的需求选择

5.2 提示词模板模块

# 提示词模板 
DETAILED_TEMPLATE = """你是一个专业的文档问答助手。请严格基于提供的上下文信息回答问题。

【上下文信息】
{context}

【用户问题】
{question}

【回答要求】
1. 答案必须基于上下文信息,不要编造
2. 如果上下文中没有相关信息,请说"根据提供的文档,无法回答该问题"
3. 答案要简洁准确,避免多余信息
4. 如果涉及数据,可以直接引用

【答案】"""


def get_prompt_template() -> str:
    """
    获取提示模板字符串
    """
    return DETAILED_TEMPLATE


def create_prompt() -> ChatPromptTemplate:
    """
    创建LangChain提示模板
    """
    return ChatPromptTemplate.from_template(DETAILED_TEMPLATE)


def create_qa_prompt(question: str, context: str) -> ChatPromptTemplate:
    """
    创建包含具体内容的问答提示
    """
    prompt = ChatPromptTemplate.from_template(DETAILED_TEMPLATE)
    return prompt

我把提示词单独抽离出来,这样后期如果想调整prompt效果,只需要改这一个地方。提示词里明确了四条回答要求,核心目的就是让大模型老老实实基于上下文回答问题,减少幻觉问题

5.3 检索模块

def retrieve_documents(
    query: str,
    vector_store,
    top_k: int = 5,
    filter_dict: Optional[Dict] = None
) -> List[Document]:
    """
    从向量数据库检索相关文档 - 最简单常用的方式
    
    Args:
        query: 查询文本(已处理过的问题)
        vector_store: Chroma向量存储实例
        top_k: 返回的文档数量
        filter_dict: 过滤条件,如 {"file_type": "pdf"}
        
    Returns:
        List[Document]: 相关文档列表
    """
    if filter_dict:
        docs = vector_store.similarity_search(query, k=top_k, filter=filter_dict)
    else:
        docs = vector_store.similarity_search(query, k=top_k)
    
    return docs


def retrieve_with_scores(
    query: str,
    vector_store,
    top_k: int = 5,
    filter_dict: Optional[Dict] = None
) -> List[tuple]:
    """
    检索相关文档并返回相似度分数
    
    Args:
        query: 查询文本
        vector_store: Chroma向量存储实例
        top_k: 返回的文档数量
        filter_dict: 过滤条件
        
    Returns:
        List[tuple]: 包含文档和相似度分数的列表,分数越小越相似
    """
    if filter_dict:
        docs_with_scores = vector_store.similarity_search_with_score(
            query, k=top_k, filter=filter_dict
        )
    else:
        docs_with_scores = vector_store.similarity_search_with_score(query, k=top_k)
    
    return docs_with_scores


def create_retriever(vector_store, top_k: int = 5):
    """
    创建LangChain检索器对象
    
    Args:
        vector_store: Chroma向量存储实例
        top_k: 返回的文档数量
        
    Returns:
        Retriever: LangChain检索器
    """
    return vector_store.as_retriever(
        search_kwargs={"k": top_k}
    )

我提供了三种检索方式,retrieve_documents是最常用的,直接返回文档列表;retrieve_with_scores会同时返回相似度分数,可以用来做结果后处理,比如过滤掉分数太低的文档;create_retriever则是为了兼容LangChain的标准检索器接口另外这三个函数都支持filter_dict参数,可以按文件类型、时间等条件过滤检索结果

5.4 答案生成模块

def generate_answer(
    question: str,
    context_docs: List[Document],
    llm,
    prompt_template: str = None
) -> str:
    """
    使用LLM生成答案 - 最简单常用的方式
    
    Args:
        question: 用户问题
        context_docs: 检索到的相关文档列表
        llm: 语言模型实例
        prompt_template: 自定义提示模板,None则使用默认模板
        
    Returns:
        str: 生成的答案
    """
    # 使用提示模板
    if prompt_template is None:
        prompt_template = DETAILED_TEMPLATE
    
    # 格式化上下文
    context = format_context(context_docs)
    
    # 创建提示
    prompt = ChatPromptTemplate.from_template(prompt_template)
    formatted_prompt = prompt.format(context=context, question=question)
    
    # 调用LLM生成答案
    response = llm.invoke(formatted_prompt)
    
    # 提取答案内容
    if hasattr(response, 'content'):
        return response.content
    else:
        return str(response)


def generate_answer_with_sources(
    question: str,
    context_docs: List[Document],
    llm
) -> Dict[str, Any]:
    """
    生成答案并返回来源信息 - 适合需要展示"答案来自哪"的场景
    
    Args:
        question: 用户问题
        context_docs: 检索到的相关文档
        llm: 语言模型实例
        
    Returns:
        Dict: 包含答案和来源的字典
    """
    # 使用提示模板
    prompt = ChatPromptTemplate.from_template(DETAILED_TEMPLATE)
    
    # 格式化上下文
    context = format_context_with_sources(context_docs)
    formatted_prompt = prompt.format(context=context, question=question)
    response = llm.invoke(formatted_prompt)
    
    answer = response.content if hasattr(response, 'content') else str(response)
    
    # 提取来源信息
    sources = []
    for doc in context_docs:
        source = doc.metadata.get('source', '未知来源')
        if source not in sources:
            sources.append(source)
    
    return {
        'answer': answer,
        'sources': sources,
        'document_count': len(context_docs)
    }

这两个生成函数满足了两种常见需求,generate_answer简单直接,只返回答案文本;generate_answer_with_sources会额外返回答案的来源信息,方便做溯源展示。我特别处理了大模型返回值的兼容性问题,不管返回的是字符串还是带content属性的对象,都能正确提取答案,来源信息也做了去重,避免同一个文件显示多次

5.5 格式化工具模块

def format_retrieved_docs(docs: List[Document]) -> str:
    """
    格式化检索结果用于LLM上下文
    
    Args:
        docs: 检索到的文档列表
        
    Returns:
        str: 格式化的上下文字符串
    """
    contexts = []
    
    for i, doc in enumerate(docs, 1):
        source = doc.metadata.get('source', '未知来源')
        content = doc.page_content.strip()
        
        context = f"[文档{i}] 来自: {source}\n{content}\n"
        contexts.append(context)
    
    return "\n".join(contexts)


def format_context(docs: List[Document]) -> str:
    """
    简单格式化文档为上下文字符串
    
    Args:
        docs: 文档列表
        
    Returns:
        str: 格式化的上下文
    """
    contexts = []
    
    for i, doc in enumerate(docs, 1):
        content = doc.page_content.strip()
        contexts.append(f"[{i}] {content}")
    
    return "\n\n".join(contexts)


def format_context_with_sources(docs: List[Document]) -> str:
    """
    格式化文档为包含来源的上下文字符串
    
    Args:
        docs: 文档列表
        
    Returns:
        str: 格式化的上下文
    """
    contexts = []
    
    for i, doc in enumerate(docs, 1):
        content = doc.page_content.strip()
        source = doc.metadata.get('source', '未知来源')
        contexts.append(f"[{i}] 来自《{source}》:\n{content}")
    
    return "\n\n".join(contexts)

这里我写了三个格式化函数,其实是在干同一件事——把检索出来的文档组装成大模型能看懂的文字,format_context最简洁,只保留文档内容;format_retrieved_docs会带上来源信息,方便调试时知道每个文档是从哪来的,format_context_with_sources则是在给大模型看的同时,也把来源信息嵌进去,这样生成的答案如果需要溯源,可以直接引用也可以根据自己的需求选一个用,或者组合着用

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐