从零开始搭建RAG系统系列(七):组装完整的RAG Pipeline

129 阅读4分钟

步骤五:组装完整的RAG Pipeline

⽬标: 将前⾯实现的各个步骤(数据加载、分割、向量化、索引、检索、Prompt构建、LLM⽣成)顺畅地串联起来,形成⼀个端到端的、可执⾏的RAG⼯作流。LangChain的表达式语⾔(LangChain Expression Language, LCEL)使得这个过程非常简洁和声明式。

image.png

上图展⽰了使⽤LCEL构建⼀个典型RAG链的逻辑。⽤⼾查询和通过检索器获取并格式化的上下⽂,被组合输⼊到Prompt模板中,然后传递给LLM进⾏处理,最后通过输出解析器得到最终答案。LCEL的管道符 | 优雅地连接了这些处理单元。

from langchain_core.runnables import RunnablePassthrough, RunnableParallel
# 确保 retriever, prompt_template, llm, output_parser 已在前序步骤中定义并初始化成
# 定义⼀个辅助函数,将检索到的Document列表格式化为单个字符串
def format_docs(docs: List[Document]) -> str:"""将Document列表合并为⼀个⽤分隔符隔开的字符串。"""
return "\n\n---\n\n".join([d.page_content for d in docs])
if 'retriever' in locals() and retriever and \
'prompt_template' in locals() and prompt_template and \
'llm' in locals() and llm and \
'output_parser' in locals() and output_parser:
# 使⽤LCEL构建RAG链
# 链的输⼊是⽤⼾的查询字符串 (user_query)
# 1. retriever处理user_query得到相关⽂档 (relevant_docs)
# 2. format_docs将relevant_docs格式化为context_str
# 3. user_query通过RunnablePassthrough()直接传递
# 4. context_str和user_query被送⼊prompt_template
# 5. 格式化后的prompt送⼊llm
# 6. llm的输出送⼊output_parser得到最终字符串答案
rag_chain_simple = (
# RunnableParallel允许并⾏或按指定结构准备后续组件的输⼊字典
{"context_str": retriever | format_docs, "user_query": RunnablePasst
| prompt_template
| llm
| output_parser
)
print("\n简单的RAG链已构建 (rag_chain_simple)。")
# 同时构建⼀个可以返回源⽂档的链,⽅便溯源
rag_chain_with_sources = RunnableParallel(
{"context_docs": retriever, "user_query": RunnablePassthrough()}
).assign(
answer = RunnableParallel(
{"context_str": lambda x: format_docs(x["context_docs"]), "user_
) | prompt_template | llm | output_parser
)
print("带溯源信息的RAG链已构建 (rag_chain_with_sources)。")
# --- 测试完整的RAG Pipeline ---
test_query = "RAGFlow有哪些主要功能?" # 使⽤之前定义好的 user_query,或者新的测
print(f"\n--- 开始测试RAG链,查询: '{test_query}' ---")
# 测试简单链
try:
print("\n--- 测试 rag_chain_simple ---")
final_answer_simple = rag_chain_simple.invoke(test_query)
print("\n【最终RAG回答 (简单链)】:")
print(final_answer_simple)
except Exception as e:print(f"执⾏ rag_chain_simple 失败: {e}")
# 测试带溯源信息的链
try:
print("\n--- 测试 rag_chain_with_sources ---")
result_with_sources = rag_chain_with_sources.invoke(test_query)
print("\n【最终RAG回答 (带溯源信息)】:")
print(f"回答: {result_with_sources['answer']}")
print("\n【参考上下⽂来源】:")
if result_with_sources['context_docs']:
for i, doc_item in enumerate(result_with_sources['context_docs']
print(f" 来源 {i+1}: {doc_item.metadata.get('source', '未知'
print(f" 内容⽚段 (前50字符): {doc_item.page_content[:50].s
else:
print(" 未能检索到相关上下⽂。")
except Exception as e:
print(f"执⾏ rag_chain_with_sources 失败: {e}")
else:
print("\n由于⼀个或多个核⼼组件(retriever, prompt, llm, parser)未能初始化,⽆

说明与分析:

  • LCEL的威⼒: LangChain表达式语⾔(LCEL)通过管道操作符 | 使得链的构建⾮常直观。它允许将不同的“可运⾏单元(” Runnables,如检索器、Prompt模板、LLM模型、输出解析器等)组合起来。

  • 输⼊与输出流:

    • rag_chain_simple:输⼊是⼀个字符串(⽤⼾查询)。RunnablePassthrough() ⽤于将链的输⼊(即⽤⼾查询)直接作为 user_query字段的值传递给Prompt模板。 retriever | format_docs 这部分则先⽤原始查询通过 retriever 获取⽂档,然后通过 format_docs 函数处理成 context_str 。这两部分构成Prompt模板所需的输⼊字典。

    • rag_chain_with_sources :这个链稍微复杂⼀些,因为它旨在同时返回LLM的回答和检索到的源⽂档。它⾸先使⽤ RunnableParallel 来并⾏获取context_docs (通过 retriever )和 user_query (通过RunnablePassthrough )。然后,使⽤ .assign() ⽅法在保持原始context_docs 和 user_query 的同时,计算出⼀个新的字段 answer 。计算answer 的过程本⾝⼜是⼀个⼦链,它从⽗链的输出中提取 context_docs 和user_query ,格式化上下⽂,然后通过Prompt模板、LLM和输出解析器得到答案。

  • 可组合性与灵活性: LCEL的强⼤之处在于其⾼度的可组合性。你可以轻松地在链中插⼊或替换组件,例如加⼊查询改写步骤、重排步骤等,⽽⽆需⼤幅修改整体结构。

通过这五个步骤,我们已经成功搭建了⼀个基础但完整的RAG系统。它能够加载本地⽂档,构建向量索引,根据⽤⼾查询检索相关信息,并利⽤LLM⽣成基于这些信息的回答。接下来的章节将探讨如何部署这个系统以及如何进⼀步优化其性能。