RAG 进阶优化:提升检索质量的七个方向

上一篇文章介绍了基础 RAG 的搭建。基础 RAG 跑起来之后,你会发现效果差强人意——召回的内容不够准、回答有时候答非所问。这篇文章梳理提升 RAG 效果的常见优化方向。 方向 1:切块策略优化 基础的固定大小切块太粗糙,几个更好的策略: 按语义切块(Semantic Chunking): from langchain_experimental.text_splitter import SemanticChunker from langchain_openai import OpenAIEmbeddings splitter = SemanticChunker( OpenAIEmbeddings(), breakpoint_threshold_type="percentile" ) chunks = splitter.split_text(document) 语义切块基于 Embedding 相似度判断段落边界,比按字符数切更合理。 父子文档(Parent-Child):小块用于检索,大块用于生成: from langchain.retrievers import ParentDocumentRetriever retriever = ParentDocumentRetriever( vectorstore=vectorstore, docstore=InMemoryStore(), child_splitter=RecursiveCharacterTextSplitter(chunk_size=200), parent_splitter=RecursiveCharacterTextSplitter(chunk_size=2000), ) 小块召回精准,但上下文不足;大块提供足够上下文,但噪声多。父子文档两全其美。 方向 2:Query 改写 用户的提问往往不是最优的检索 query: async def rewrite_query(query: str) -> list[str]: prompt = f""" 生成 3 个不同角度的检索查询,帮助从文档库中找到回答以下问题的信息。 原始问题:{query} 输出格式:每行一个查询 """ response = await llm.ainvoke(prompt) queries = response.content.strip().split("\n") return [query] + queries # 原始查询 + 改写的查询 用多个 query 检索,再合并去重,召回率显著提升。 方向 3:HyDE(假设文档嵌入) 让模型先生成一个"假设的答案文档",用它来检索: async def hyde_retrieve(query: str) -> list[Document]: # 让模型生成一个假设的答案 hypothetical_doc = await llm.ainvoke( f"写一段简短的文章,回答以下问题(即使你不确定):{query}" ) # 用假设答案的向量来检索,而不是用问题的向量 docs = vectorstore.similarity_search(hypothetical_doc.content, k=4) return docs 假设答案的 Embedding 比问题的 Embedding 更接近文档的分布,检索效果往往更好。 ...

2024-05-22 · 2 min · Kada Liao

Prompt 工程实践:让 LLM 更听话的技巧

Prompt 工程这个词听起来很高大上,但它本质上就是"怎么跟模型说话才能让它给出你想要的结果"。这篇文章分享一些实际项目里积累的经验。 明确角色和背景 给模型一个清晰的角色定位: # 模糊 "帮我写一段代码" # 清晰 "你是一个 Python 高级工程师,专注于性能优化。 帮我优化以下函数,要求: 1. 保持功能不变 2. 减少不必要的循环 3. 添加类型注解 4. 代码风格遵循 PEP 8" 角色描述告诉模型应该以什么视角和知识储备来回答。 Few-shot:给例子比给说明更有效 告诉模型你想要什么格式,最好的方式是给例子: 将以下文本分类为:正面/负面/中性 示例: - "这个产品质量很好" → 正面 - "一般般,没什么特别" → 中性 - "完全是浪费钱" → 负面 现在分类以下文本: - "还行,比预期好一点点" Few-shot 比描述规则更直观,模型更容易理解你的意图。 Chain of Thought:让模型"想一想再说" 对于需要推理的任务,让模型展示推理过程: # 直接问(效果差) "一个工厂每天生产 500 个零件,工人效率提升 20% 后, 需要多少天生产 18000 个零件?" # 加上 "一步一步思考"(效果好) "一个工厂每天生产 500 个零件,工人效率提升 20% 后, 需要多少天生产 18000 个零件?请一步一步计算。" “Let’s think step by step” 或"一步一步思考"这类指令能显著提升推理准确率,这是有论文支撑的结论。 ...

2023-12-05 · 1 min · Kada Liao

RAG 系统从零搭建:检索增强生成的原理与实践

LLM 有两个核心局限:知识有截止日期、无法访问私有数据。RAG(Retrieval-Augmented Generation)是目前解决这两个问题最主流的方案。 RAG 的基本流程 文档 → 切块 → Embedding → 存入向量库 ↓ 用户问题 → Embedding → 向量检索 → 召回相关块 → 组合 Prompt → LLM → 回答 四个核心步骤:文档处理、向量化、检索、生成。 文档切块 文档太长无法直接塞给模型,需要切成小块: from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, # 相邻块有重叠,避免信息割裂 separators=["\n\n", "\n", "。", ",", " "] ) chunks = splitter.split_documents(docs) chunk_overlap 是个容易忽略的参数——它让相邻的块有内容重叠,避免一句话被硬切断导致语义丢失。 Embedding 与向量存储 from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 将文档块向量化并存入 Chroma vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db" ) 向量数据库本质是做高维空间的近邻搜索。Chroma 适合本地开发,生产环境可以考虑 Pinecone、Weaviate 或自建 Milvus。 检索与生成 from langchain.chains import RetrievalQA retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4} # 召回最相关的 4 个块 ) qa_chain = RetrievalQA.from_chain_type( llm=ChatOpenAI(model="gpt-3.5-turbo"), retriever=retriever, return_source_documents=True ) result = qa_chain.invoke({"query": "公司的请假政策是什么?"}) print(result["result"]) 几个影响效果的关键点 切块策略:chunk_size 太小,单块缺乏上下文;太大,引入噪声。通常 300-600 tokens 是个比较合适的范围。 ...

2023-08-15 · 1 min · Kada Liao

LangChain 入门:用 Python 构建你的第一个 LLM 应用

2023 年初,LangChain 在 GitHub 上的星数以惊人的速度增长,一夜之间成为 LLM 应用开发的标配。这篇文章梳理一下它的核心设计和基础用法。 为什么需要 LangChain 直接调用 OpenAI API 能做很多事,但当应用变复杂时,你会发现需要反复造轮子: 多轮对话的历史管理 Prompt 模板化 链式调用多个 LLM 步骤 连接外部数据源 LangChain 把这些封装成了可组合的组件。 核心概念 Chain Chain 是 LangChain 的核心抽象,把多个操作串联起来: from langchain.chains import LLMChain from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-3.5-turbo") prompt = PromptTemplate( input_variables=["topic"], template="用 3 句话解释{topic},面向初学者" ) chain = LLMChain(llm=llm, prompt=prompt) result = chain.invoke({"topic": "向量数据库"}) print(result["text"]) Memory Memory 组件负责维护对话历史: from langchain.memory import ConversationBufferMemory from langchain.chains import ConversationChain memory = ConversationBufferMemory() conversation = ConversationChain(llm=llm, memory=memory) conversation.predict(input="我叫 Kada") conversation.predict(input="你还记得我叫什么吗?") # 能记住 Document Loaders 加载外部文档,是构建 RAG 的基础: from langchain_community.document_loaders import TextLoader, PyPDFLoader # 加载文本文件 loader = TextLoader("./doc.txt", encoding="utf-8") docs = loader.load() # 加载 PDF pdf_loader = PyPDFLoader("./report.pdf") pages = pdf_loader.load_and_split() LCEL:新的链式语法 LangChain 0.1 之后推荐用 LCEL(LangChain Expression Language)写链: ...

2023-03-20 · 1 min · Kada Liao

OpenAI API 初体验:ChatGPT 背后的接口是什么样的

2022 年 11 月底,ChatGPT 上线,刷屏了所有技术圈的朋友圈。作为工程师,第一反应自然是——这东西能怎么用在项目里? 基础概念 OpenAI 的核心接口是 Chat Completions API,接受一个消息列表,返回模型的回复。 from openai import OpenAI client = OpenAI(api_key="your-api-key") response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "你是一个 Python 专家"}, {"role": "user", "content": "解释一下 Python 的 GIL"} ] ) print(response.choices[0].message.content) 消息列表里有三种角色: system:给模型设定人格和行为准则 user:用户输入 assistant:模型历史回复(多轮对话时需要带上) Token 是什么 模型按 token 计费,而不是按字符。英文大约 4 个字符 = 1 token,中文大约 1-2 个字符 = 1 token。 # 用 tiktoken 计算 token 数 import tiktoken enc = tiktoken.encoding_for_model("gpt-3.5-turbo") tokens = enc.encode("Hello, world!") print(len(tokens)) # 4 max_tokens 控制回复的最大长度,temperature 控制随机性(0 最确定,2 最随机)。 多轮对话的实现 模型本身是无状态的,多轮对话需要客户端维护历史消息: history = [] def chat(user_input): history.append({"role": "user", "content": user_input}) response = client.chat.completions.create( model="gpt-3.5-turbo", messages=history ) reply = response.choices[0].message.content history.append({"role": "assistant", "content": reply}) return reply 历史越长,消耗的 token 越多,成本越高。实际项目里需要做历史截断或摘要。 ...

2022-12-10 · 1 min · Kada Liao