RAG(Retrieval-Augmented Generation)技术正在重塑我们与大型语言模型交互的方式。想象一下,你面前站着一位学识渊博但记忆有限的教授——他能够即兴创作优美的文章,却经常记不清具体的数据细节。RAG就像给这位教授配备了一个智能图书管理员,在需要时快速递上准确的参考资料。这种结合了信息检索与文本生成的技术架构,正在企业知识管理、智能客服、学术研究等领域展现出惊人的实用价值。
我首次接触RAG是在开发一个法律咨询系统时。纯LLM方案经常产生"一本正经胡说八道"的法律条款引用,而传统检索系统又缺乏自然语言理解能力。RAG的混合架构完美解决了这个痛点:它先通过语义搜索锁定相关法律条文,再让LLM基于这些准确素材生成回答。这种工作模式使得系统既保持了语言模型的流畅性,又具备了事实准确性。
文档切片(Chunking)是RAG流水线的第一个关键环节。就像厨师准备食材时需要根据菜式决定切块大小,我们也要根据后续的检索需求确定最佳切片方案。常见的策略包括:
我在金融报告处理中发现,表格数据的处理尤为棘手。采用PyPDFLoader提取文本后,简单的按页切割会导致财务报表被拦腰截断。最终方案是:
python复制from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?"]
)
单纯的文本切片会丢失重要上下文信息。给每个分块添加元数据就像给图书馆的每本书贴上书签:
markdown复制{
"document_id": "2023-Q3-Report",
"page_range": "45-47",
"section_title": "现金流分析",
"keywords": ["营运资本","投资活动","融资活动"]
}
使用LlamaIndex时的典型配置:
python复制from llama_index import Document
doc = Document(
text=chunk_text,
metadata={
"source": "annual_report.pdf",
"page_num": page_number
}
)
实践提示:对于技术文档,建议添加API名称、版本号等元数据;法律文书则需标注条款编号和生效日期。
选择嵌入模型就像为图书馆选择编目系统。以下是主流选项的性能对比:
| 模型名称 | 维度 | 适合场景 | 计算成本 |
|---|---|---|---|
| BAAI/bge-small | 384 | 通用场景 | 低 |
| text-embedding-ada-002 | 1536 | 多语言混合 | 中 |
| multilingual-e5-large | 1024 | 跨语言检索 | 高 |
实测发现,对于中文法律文本,bge-large-zh的表现优于OpenAI的嵌入模型:
python复制from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-large-zh')
embeddings = model.encode(["抵押权设立的要件"], normalize_embeddings=True)
向量数据库的选择需要考虑规模、实时性和成本三个维度:
小型知识库(<10万条):FAISS足够轻量
python复制import faiss
index = faiss.IndexFlatIP(768)
index.add(embeddings_array)
生产级系统:Milvus或Pinecone提供分布式支持
python复制from pinecone import Pinecone
pc = Pinecone(api_key="YOUR_KEY")
index = pc.Index("legal-docs")
index.upsert(vectors=zip(ids, vectors))
实时更新需求:Weaviate支持动态schema
graphql复制mutation {
create(
data: {
content: "保证合同要式性要求"
vector: [0.12, -0.45, ..., 0.67]
}
)
}
性能陷阱:余弦相似度计算在超过1万条记录时会出现延迟,建议对高频查询建立HNSW索引。
单纯的向量搜索可能漏掉关键词完全匹配的重要文档。我们的解决方案是:
python复制def hybrid_search(query):
# 关键词检索
bm25_results = bm25_index.search(query)
# 向量检索
query_embedding = embed_model.encode(query)
vector_results = vector_db.similarity_search(query_embedding)
# 结果融合
combined = reciprocal_rank_fusion(bm25_results, vector_results)
return combined[:5]
其中reciprocal_rank_fusion算法实现:
python复制def reciprocal_rank_fusion(results1, results2, k=60):
scores = {}
for doc in results1:
scores[doc.id] = scores.get(doc.id, 0) + 1/(60 + doc.rank)
# 相同逻辑处理results2...
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
直接拼接检索结果会导致LLM的注意力分散。我们采用这些优化策略:
相关性重排序:
python复制reranker = CrossEncoder('BAAI/bge-reranker-large')
scores = reranker.predict([(query, doc.text) for doc in retrieved])
上下文压缩:
python复制from langchain.document_transformers import EmbeddingsRedundantFilter
filter = EmbeddingsRedundantFilter(embeddings=embed_model)
compressed_docs = filter.filter_documents(retrieved_docs)
提示词工程:
text复制你是一位专业法律顾问,请严格根据以下条款回答问题:
[条款1内容...]
[条款2内容...]
问题:{用户提问}
注意:如果条款未明确提及,请回答"根据现有资料无法确定"
在部署医疗问答系统时,我们遇到响应时间超过5秒的问题。通过火焰图分析发现:
UnstructuredFileLoader优化后的架构:
mermaid复制graph TD
A[用户提问] --> B{缓存检查}
B -->|命中| C[直接返回]
B -->|未命中| D[并行执行]
D --> E[关键词检索]
D --> F[向量检索]
E --> G[结果融合]
F --> G
G --> H[LLM生成]
建立多维度的评估方案:
| 指标类型 | 评估方法 | 目标值 |
|---|---|---|
| 检索准确率 | 人工标注top-k命中率 | >80% |
| 生成相关性 | ROUGE-L分数 | >0.6 |
| 事实一致性 | SelfCheckGPT检测 | <5%幻觉 |
| 响应延迟 | 端到端耗时 | <2s |
自动化测试脚本片段:
python复制def test_retrieval_accuracy():
test_cases = load_json("eval_cases.json")
for case in test_cases:
results = retrieval_chain(case["query"])
assert any(gold in r.text for r in results[:3] for gold in case["gold_standard"])
传统RAG的静态检索可能错过最新信息。我们正在试验:
查询扩展:使用LLM生成搜索同义词
python复制def expand_query(query):
prompt = f"生成3个'{query}'的专业同义词,用逗号分隔"
return llm.invoke(prompt).split(",")
递归检索:根据初步结果发起二次查询
python复制first_pass = vector_db.search(initial_query)
refined_query = llm.generate(f"基于{first_pass}优化查询:{initial_query}")
second_pass = vector_db.search(refined_query)
微软的REPLUG等框架开始支持联合训练检索器和生成器:
python复制from transformers import RagTokenizer, RagRetriever, RagSequenceForGeneration
tokenizer = RagTokenizer.from_pretrained("facebook/rag-sequence-nq")
retriever = RagRetriever.from_pretrained(...)
model = RagSequenceForGeneration.from_pretrained(..., retriever=retriever)
inputs = tokenizer("合同法第52条解释", return_tensors="pt")
outputs = model.generate(input_ids=inputs["input_ids"])
这种方案在专业术语一致性上比流水线式RAG提升约15%,但需要大量领域数据微调。