LangChain实战:新手手撸RAG全记录(五)
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则是在给大模型看的同时,也把来源信息嵌进去,这样生成的答案如果需要溯源,可以直接引用也可以根据自己的需求选一个用,或者组合着用
更多推荐


所有评论(0)