沒有捷徑:RAG入門不推薦直接使用成熟框架
春節(jié)期間我在 Github 開源的 RAG 項(xiàng)目目前已經(jīng)攢了 134 個(gè) Star,盲猜可能也是因?yàn)樽铋_始用的就是 Ollama 本地部署 DeepSeek-r1:7b 的方案,年后當(dāng)本地部署知識(shí)庫和 deepseek火了起來之后,被動(dòng)蹭了一波流量。
1、為什么重復(fù)造輪子?
但是,在過去的一個(gè)月時(shí)間里也收到了很多網(wǎng)友的私信,詢問關(guān)于為什么市面上已經(jīng)有了類似 AnythingLLM、Cherry Studio、Dify、RAGFlow 等成熟的開源框架,還要重復(fù)造輪子去編一個(gè)不是很好用的 RAG 項(xiàng)目。
當(dāng)然與此同時(shí),也有很多網(wǎng)友在私信或者評(píng)論區(qū)中反饋上手調(diào)試過我這個(gè)簡(jiǎn)單的開源項(xiàng)目后,再去用其他框架更加得心應(yīng)手了。
1.1 開箱即用的”不友好“
其實(shí)我從 24 年 6 月份開始,就逐一深度試用了上述常見的幾個(gè)開源項(xiàng)目,但一段時(shí)間之后明顯發(fā)現(xiàn),作為一個(gè)剛?cè)腴T的人來說,雖然 AnyThingLLM、RAGFlow 等成熟框架提供了便捷的"開箱即用"體驗(yàn),但直接使用這些工具會(huì)讓人陷入"知其然,而不知其所以然"的困境。
1.2 從零構(gòu)建再到框架應(yīng)用
換句話說,就像學(xué)編程不應(yīng)該從框架開始,而是應(yīng)該從基礎(chǔ)語法入手一樣。學(xué)習(xí) RAG 技術(shù)實(shí)測(cè)也同樣適合先構(gòu)建基礎(chǔ)認(rèn)知框架,再應(yīng)用封裝工具。這不僅是技能學(xué)習(xí)的路徑,更多也是培養(yǎng)解決問題能力的過程。
2、項(xiàng)目特點(diǎn)與局限性
2.1 項(xiàng)目優(yōu)勢(shì)
我這個(gè)開源項(xiàng)目采用簡(jiǎn)潔的代碼展示了 RAG 的完整流程,通過親手調(diào)試這些組件,可以:
更有助于建立具象認(rèn)知
比如直觀理解文本分塊如何影響語義完整性,檢索策略如何決定召回質(zhì)量,以及重排序如何提升最終回答準(zhǔn)確度
掌握核心決策點(diǎn)
親身體驗(yàn)不同 chunk_size 對(duì)檢索效果的影響,感受不同嵌入模型的語義表達(dá)差異,理解為什么相同的 RAG 系統(tǒng)在不同場(chǎng)景下表現(xiàn)迥異
培養(yǎng)調(diào)試直覺
當(dāng)回答質(zhì)量不理想時(shí),可以通過控制變量法能相對(duì)準(zhǔn)確判斷是分塊策略不當(dāng),還是檢索精度不足,或是提示工程欠缺
掌握這些基礎(chǔ)后,再轉(zhuǎn)向 RAGFlow 或其他框架時(shí),眾多的配置選項(xiàng)就不再是很抽象的參數(shù),再做 API 調(diào)優(yōu)或者二次開發(fā)也會(huì)變得有的放矢。從而深度適配業(yè)務(wù)場(chǎng)景時(shí),也就可以針對(duì)性地做調(diào)整框架配置,而不是非盲目嘗試。
2.2 項(xiàng)目局限性
分塊策略
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=20
)
chunks = text_splitter.split_text(text)
使用 LangChain 的 RecursiveCharacterTextSplitter
塊大?。好繅K 200 字符(較小,一個(gè)字符相當(dāng)于 1 個(gè)漢字)
重疊度:20 字符
特點(diǎn):小塊大小有利于精準(zhǔn)定位信息,但可能損失上下文連貫性
檢索策略
results = COLLECTION.query(
query_embeddings=query_embedding,
n_results=5,
include=['documents', 'metadatas']
)
純語義檢索(沒有混合 BM25 等關(guān)鍵詞檢索)
未使用過濾條件或高級(jí)查詢功能
重排策略
代碼中沒有顯式的重排序步驟,但設(shè)計(jì)了一些相關(guān)邏輯:
# 檢測(cè)矛盾
conflict_detected = detect_conflicts(sources_for_conflict_detection)
# 獲取可信源
if conflict_detected:
credible_sources = [s for s in sources_for_conflict_detection
if s['type'] == 'web' and evaluate_source_credibility(s) > 0.7]
通過 evaluate_source_credibility 函數(shù)對(duì)信息源進(jìn)行可信度評(píng)分
在檢測(cè)到?jīng)_突時(shí),優(yōu)先考慮可信度高的來源
3、進(jìn)階版本
前幾天開通知識(shí)星球后,有些有一定實(shí)踐經(jīng)驗(yàn)的網(wǎng)友過來交流一些技術(shù)細(xì)節(jié)時(shí)發(fā)現(xiàn),基礎(chǔ)版本的開源項(xiàng)目已經(jīng)不能滿足他們當(dāng)前的需求。
我花了半天時(shí)間重構(gòu)了遍代碼,相比上一版多了600行,也是敲到手酸。這個(gè)進(jìn)階版屬于更符合 RAG 系統(tǒng)最佳實(shí)踐的完整版本,一共有10 個(gè)大類的優(yōu)化要點(diǎn),下述展示會(huì)按照對(duì)最終回答質(zhì)量的影響程度排序。需要說明的是,優(yōu)先實(shí)施以下前 3-5 項(xiàng)將能帶來最顯著的效果提升。
注:下述代碼優(yōu)化示例是針對(duì)前述我提到的自己開源項(xiàng)目而言
3.1 分塊策略優(yōu)化(最高優(yōu)先級(jí))
200 字符的塊過小,無法包含足夠上下文
可能導(dǎo)致語義割裂和信息碎片化
改進(jìn)方案:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # 增大到800-1000字符
chunk_overlap=150, # 增加重疊以保持連貫性
separators=["\n\n", "\n", "。", ",", " "] # 優(yōu)先在自然段落分割
)
預(yù)期效果:更連貫的上下文,減少信息丟失,提高回答質(zhì)量和相關(guān)性。
3.2 混合檢索策略
當(dāng)前問題:
純語義檢索可能忽略關(guān)鍵詞匹配
容易出現(xiàn)語義漂移
改進(jìn)方案:
# 引入BM25關(guān)鍵詞檢索
from rank_bm25 import BM25Okapi
tokenized_corpus = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
# 混合檢索結(jié)果
semantic_results = COLLECTION.query(query_embeddings, n_results=7)
bm25_results = bm25.get_top_n(query.split(), corpus, n=7)
combined_results = hybrid_merge(semantic_results, bm25_results)
預(yù)期效果:提高檢索準(zhǔn)確性,尤其對(duì)事實(shí)性和專業(yè)術(shù)語的問題。
3.3 重排序機(jī)制
當(dāng)前問題:
缺乏真正的重排序機(jī)制
相關(guān)度評(píng)分單一依賴向量相似度
改進(jìn)方案:
# 方案A: 使用交叉編碼器重排序
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
candidate_pairs = [[query, doc] for doc in retrieved_docs]
scores = reranker.predict(candidate_pairs)
reranked_results = [doc for _, doc in sorted(zip(scores, retrieved_docs), reverse=True)]
# 方案B: 使用LLM進(jìn)行相關(guān)性評(píng)分
def llm_rerank(query, docs, llm_client):
results = []
for doc in docs:
prompt = f"問題: {query}\n文檔: {doc}\n這個(gè)文檔與問題的相關(guān)性是否高? 0-10分打分:"
score = float(llm_client.generate(prompt).strip())
results.append((score, doc))
return [doc for _, doc in sorted(results, reverse=True)]
預(yù)期效果:更精準(zhǔn)的結(jié)果排序,將最相關(guān)的內(nèi)容放在前面,顯著提高答案質(zhì)量。
3.4 遞歸檢索與迭代查詢
當(dāng)前問題:
單次檢索可能不足以回答復(fù)雜問題
缺乏根據(jù)初步檢索結(jié)果調(diào)整查詢的機(jī)制
改進(jìn)方案:
def recursive_retrieval(initial_query, max_iteratinotallow=3):
query = initial_query
all_contexts = []
for i in range(max_iterations):
# 當(dāng)前查詢的檢索結(jié)果
current_results = retrieve_documents(query)
all_contexts.extend(current_results)
# 使用LLM分析還需要查詢什么
next_query_prompt = f"""
基于原始問題: {initial_query}
以及已檢索信息: {summarize(current_results)}
是否需要進(jìn)一步查詢? 如果需要,請(qǐng)?zhí)峁┬碌牟樵儐栴}:
"""
next_query = llm_client.generate(next_query_prompt)
if "不需要" in next_query or i == max_iterations-1:
break
query = next_query
return all_contexts
預(yù)期效果:能夠處理多跳推理問題,循序漸進(jìn)獲取所需信息。
3.5 上下文壓縮與總結(jié)
當(dāng)前問題:
檢索文檔過長時(shí)浪費(fèi) token
包含過多無關(guān)信息
改進(jìn)方案:
def compress_context(query, documents, max_tokens=2000):
compressed_docs = []
for doc in documents:
# 方案A: 使用map-reduce模式總結(jié)
summary_prompt = f"原文: {doc}\n請(qǐng)?zhí)崛∨c問題'{query}'最相關(guān)的信息,總結(jié)在100詞以內(nèi):"
compressed = llm_client.generate(summary_prompt)
compressed_docs.append(compressed)
# 方案B: 使用抽取式摘要選擇關(guān)鍵句子
# sentences = split_into_sentences(doc)
# scores = sentence_similarity(query, sentences)
# top_sentences = [s for s, _ in sorted(zip(sentences, scores), key=lambda x: x[1], reverse=True)[:5]]
# compressed_docs.append(" ".join(top_sentences))
return compressed_docs
預(yù)期效果:減少無關(guān)信息干擾,降低 token 消耗,提高模型對(duì)關(guān)鍵信息的關(guān)注度。
3.6 提示工程優(yōu)化
當(dāng)前問題:
現(xiàn)有提示模板相對(duì)簡(jiǎn)單
缺乏針對(duì)不同問題類型的專門指導(dǎo)
改進(jìn)方案:
def create_advanced_prompt(query, context, question_type):
if question_type == "factual":
template = """
你是一個(gè)精確的事實(shí)回答助手。以下是與問題相關(guān)的信息:
{context}
問題: {query}
請(qǐng)基于以上信息提供精確的事實(shí)回答。如信息不足,請(qǐng)明確指出。請(qǐng)?jiān)诨卮鹉┪矘?biāo)明信息來源。
"""
elif question_type == "analytical":
template = """
你是一個(gè)分析型助手。以下是相關(guān)參考信息:
{context}
問題: {query}
請(qǐng)分析以上信息,提供深入見解。注意分析信息的一致性與可靠性,標(biāo)明不同來源間的差異。最后給出綜合結(jié)論并注明信息來源。
"""
# 更多問題類型...
# 添加思維鏈指導(dǎo)
template += """
思考步驟:
1. 提取問題關(guān)鍵點(diǎn)
2. 識(shí)別相關(guān)文檔中最有價(jià)值的信息
3. 對(duì)比不同來源信息
4. 形成清晰、全面的回答
"""
return template.format(cnotallow=context, query=query)
預(yù)期效果:更加結(jié)構(gòu)化和針對(duì)性的回答,提高回答質(zhì)量和可信度。
3.7 元數(shù)據(jù)增強(qiáng)與過濾
當(dāng)前問題:
元數(shù)據(jù)利用不足
缺乏基于元數(shù)據(jù)的預(yù)過濾和后過濾
改進(jìn)方案:
# 豐富元數(shù)據(jù)
metadatas = [{
"source": file_name,
"doc_id": doc_id,
"date_processed": datetime.now().isoformat(),
"chunk_index": i,
"total_chunks": len(chunks),
"document_type": detect_document_type(text),
"language": detect_language(chunk),
"entities": extract_entities(chunk)
} for i, chunk in enumerate(chunks)]
# 檢索時(shí)使用元數(shù)據(jù)過濾
results = COLLECTION.query(
query_embeddings=query_embedding,
n_results=10,
where={"document_type": {"$in": ["report", "research_paper"]}},
include=['documents', 'metadatas']
)
預(yù)期效果:更精準(zhǔn)的檢索結(jié)果篩選,減少噪音,提高答案質(zhì)量。
3.8 向量化模型升級(jí)
當(dāng)前問題:
all-MiniLM-L6-v2 維度較低(384 維)
中文表示能力有限
改進(jìn)方案:
# 升級(jí)為更強(qiáng)大的雙語模型
from sentence_transformers import SentenceTransformer
EMBED_MODEL = SentenceTransformer('BAAI/bge-large-zh-v1.5') # 1024維,中文效果更好
# 或考慮OpenAI嵌入模型
import openai
embeddings = openai.Embedding.create(
input=chunks,
model="text-embedding-ada-002"
)
預(yù)期效果:提高語義理解能力和檢索精度,尤其是中文文檔。
3.9 評(píng)估與反饋機(jī)制
當(dāng)前問題:
缺乏回答質(zhì)量評(píng)估
無法迭代改進(jìn)
改進(jìn)方案:
def evaluate_response(query, response, retrieved_docs):
# LLM自評(píng)估
evaluation_prompt = f"""
問題: {query}
回答: {response}
請(qǐng)?jiān)u估這個(gè)回答的質(zhì)量(1-10分),考慮以下標(biāo)準(zhǔn):
1. 準(zhǔn)確性: 回答是否符合檢索文檔的事實(shí)?
2. 完整性: 回答是否全面覆蓋問題的各個(gè)方面?
3. 相關(guān)性: 回答是否切中問題要點(diǎn)?
4. 一致性: 回答內(nèi)部是否存在矛盾?
請(qǐng)?jiān)敿?xì)說明評(píng)分理由。
"""
feedback = llm_client.generate(evaluation_prompt)
# 存儲(chǔ)評(píng)估結(jié)果用于系統(tǒng)優(yōu)化
save_evaluation_result(query, response, retrieved_docs, feedback)
return feedback
預(yù)期效果:建立持續(xù)評(píng)估和改進(jìn)機(jī)制,累積數(shù)據(jù)用于系統(tǒng)優(yōu)化。
3.10 緩存與預(yù)計(jì)算策略
當(dāng)前問題:
重復(fù)問題重復(fù)計(jì)算
實(shí)時(shí)響應(yīng)不夠快
改進(jìn)方案:
# 問題-檢索結(jié)果緩存
import hashlib
import pickle
from functools import lru_cache
@lru_cache(maxsize=100)
def cached_retrieval(query):
# 計(jì)算查詢哈希
query_hash = hashlib.md5(query.encode()).hexdigest()
cache_file = f"cache/{query_hash}.pkl"
# 檢查緩存
try:
with open(cache_file, 'rb') as f:
return pickle.load(f)
except FileNotFoundError:
# 執(zhí)行檢索
results = perform_retrieval(query)
# 保存到緩存
os.makedirs("cache", exist_ok=True)
with open(cache_file, 'wb') as f:
pickle.dump(results, f)
return results
4、寫在最后
升級(jí)版本的項(xiàng)目源碼在目前開源版本的基礎(chǔ)上,主要完成了前三個(gè)方面(分塊策略、混合檢索策略、重排序機(jī)制)的代碼升級(jí),源碼發(fā)布在了知識(shí)星球內(nèi),供有一定實(shí)踐經(jīng)驗(yàn)的盆友測(cè)試。剩余其他 7 個(gè)維度的示例代碼,后續(xù)會(huì)持續(xù)發(fā)布,并結(jié)合真實(shí)案例進(jìn)行演示。
另外有個(gè)彩蛋是,為了讓大家更加可視化的了解整個(gè)調(diào)優(yōu)過程,我在代碼升級(jí)的同時(shí)做了 UI 優(yōu)化,相比開源項(xiàng)目新增了一個(gè)”分塊可視化“的選項(xiàng)卡,其中直觀展示了當(dāng)前代碼中的核心模型和方法,以及分塊之后的實(shí)際內(nèi)容預(yù)覽,對(duì)調(diào)試過程會(huì)更加便利些。