微信扫码
添加专属顾问
我要投稿
从零搭建RAG智能问答系统,手把手教你实现文档加载、向量存储到多轮问答全流程。核心内容: 1. 多格式文档加载与智能文本切分技术 2. 基于LangChain的端到端RAG实现方案 3. 使用Langfuse进行LLMOps全链路监控
本文基于本项目的实际实现,介绍基于langchain框架,从文档导入到向量存储、再到多轮检索问答的端到端RAG实践。使用Langfuse实现LLMOps监控,自动捕获链中每一步的输入输出。
整体框架
应用程序
一、文档加载与切分
针对不同文档格式使用对应的 Loader,保留原始结构与元数据(如页码、来源路径):
# document_processor.pyfrom langchain_community.document_loaders import PyPDFLoader, UnstructuredMarkdownLoaderdef load_document(file_path: str):ext = os.path.splitext(file_path)[1].lower()if ext == ".pdf":loader = PyPDFLoader(file_path) # 逐页加载,自动提取页码到 metadataelif ext == ".md":loader = UnstructuredMarkdownLoader(file_path) # 保留 Markdown 结构else:raise ValueError(f"不支持的文件格式: {ext}")return loader.load()
说明:
PyPDFLoader,每页生成一个 Document,metadata["page"] 自动记录页码UnstructuredMarkdownLoader,可保留标题层级语义文本切分是影响检索质量的关键环节。使用 RecursiveCharacterTextSplitter 按语义边界(段落→句子→词)逐级尝试切分:
from langchain_text_splitters import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter(chunk_size=1000, # 每块最大字符数chunk_overlap=100, # 相邻块重叠字符数,保留上下文连续性)chunks = splitter.split_documents(docs)# 过滤纯空白块,避免噪声进入向量库chunks = [c for c in chunks if c.page_content.strip()]
关键参数权衡:
说明:
chunk_size=800~1200,chunk_overlap=80~150chunk_size二、向量化与存储
本项目使用 text-embedding-v3 模型:
# config.pyfrom langchain_community.embeddings import DashScopeEmbeddingsdef get_embeddings():return DashScopeEmbeddings(model="text-embedding-v3",dashscope_api_key=DASHSCOPE_API_KEY,)
说明:
# vector_store.pyfrom langchain_community.vectorstores import FAISSdef create_vector_store(documents):embeddings = get_embeddings()vector_store = FAISS.from_documents(documents, embeddings)return vector_storedef get_retriever(vector_store, k=3):return vector_store.as_retriever(search_kwargs={"k": k})
FAISS 在内存中构建索引,适合中小规模知识库(万级以内文本块)。as_retriever(search_kwargs={"k": 3}) 表示每次检索返回相似度最高的 3 个文档块。
最佳实践:
k值推荐 3~5;过小会漏掉关键片段,过大则引入噪声,稀释 LLM 的注意力vector_store.save_local("faiss_index") # 保存FAISS.load_local("faiss_index", embeddings) # 加载
三、对话检索链
ConversationalRetrievalChain 将多轮对话与向量检索结合,分两步执行:
用户提问 + 对话历史│▼ Step 1: 问题改写[]将追问改写为独立的完整问题│▼ Step 2: 检索 + 生成[] → 相关文档块│[] → 最终回答
问题改写 Prompt:
_CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template("""根据以下对话历史和后续问题,将后续问题改写为一个独立的问题。对话历史:{chat_history}后续问题: {question}独立问题:""")
QA 回答 Prompt:
说明:
# qa_chain.pydef create_qa_chain(retriever):llm = get_llm()memory = ConversationBufferWindowMemory(k=5, # 保留最近 5 轮对话memory_key="chat_history",return_messages=True, # 以消息对象格式返回,兼容 Chat 模型output_key="answer", # 只将 answer 字段写入记忆,排除 source_documents)chain = ConversationalRetrievalChain.from_llm(llm=llm,retriever=retriever,memory=memory,condense_question_prompt=_CONDENSE_QUESTION_PROMPT,combine_docs_chain_kwargs={"prompt": _QA_PROMPT},return_source_documents=True, # 返回检索到的原始文档,用于来源标注)return chain
说明:
output_key="answer"配合return_source_documents=True时是必填项,否则记忆模块无法识别应写入哪个输出字段return_messages=True使记忆以 ChatMessage对象列表返回,与 Chat 类型 LLM(如 ChatOpenAI)原生兼容;若使用文本补全模型则应设为 Falsek=5控制记忆窗口;过大会使 Prompt 超过 LLM 上下文长度限制,推荐 3~8检索完成后,从 source_documents 中提取文件名附加在回答末尾:
def ask(chain, question: str) -> str:result = chain({"question": question}, callbacks=callbacks)answer = result["answer"]source_docs = result.get("source_documents", [])sources = set()for doc in source_docs:source = doc.metadata.get("source", "未知来源")sources.add(source)if sources:source_text = "、".join(sources)if source_text not in answer:answer += f"\n\n📄 来源: {source_text}"return answer
说明:
set() 对来源去重,避免同一文件被多个 chunk 命中时重复显示if source_text not in answer)四、LLM 接入
使用 ChatOpenAI + 自定义 base_url,可无缝接入任何兼容 OpenAI 协议的大模型(如华为云 GLM-5等):
# config.pyfrom langchain_openai import ChatOpenAIdef get_llm():return ChatOpenAI(model=LLM_MODEL_NAME, # 如 "glm-5"api_key=LLM_API_KEY,base_url=LLM_API_BASE, # 如 "https://api.modelarts-maas.com/openai/v1")
说明:
ChatOpenAI(Chat 模型)而非 LLM(文本补全模型),因为前者原生支持消息格式,与 ConversationalRetrievalChain 的多轮记忆机制兼容性更好.env 中的三个变量,无需改代码五、可观测性:Langfuse 集成
RAG 系统的质量问题往往难以定位,常见问题包括:
Langfuse 通过 LangChain CallbackHandler 自动捕获链中每一步的输入输出,在 Dashboard 中可视化展示完整的 Span 树。
Langfuse 3.x 通过环境变量自动配置,只需传入 CallbackHandler:
# config.py — 环境变量自动读取 LANGFUSE_PUBLIC_KEY / SECRET_KEY / HOSTfrom langfuse.langchain import CallbackHandlerdef get_langfuse_handler():if not LANGFUSE_ENABLED:return Nonereturn CallbackHandler(update_trace=True)# qa_chain.py — 将 handler 注入 chain 调用handler = get_langfuse_handler()callbacks = [handler] if handler else Noneresult = chain({"question": question}, callbacks=callbacks)
必须在 .env 中配置:
LANGFUSE_PUBLIC_KEY=pk-lf-xxxLANGFUSE_SECRET_KEY=sk-lf-xxxLANGFUSE_HOST=http://your-langfuse-host:3000
最佳实践:
LANGFUSE_ENABLED 标志(当 key 缺失时自动为 False)实现零侵入降级,不影响无 Langfuse 的部署环境flush(),trace 由后台线程异步上报update_trace=True会将链的输入/输出自动写入 trace 根节点,方便在 Dashboard 直接查看 Q&A 对六、配置管理最佳实践
load_dotenv()CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "1000"))CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "100"))TOP_K = int(os.getenv("TOP_K", "3"))MEMORY_ROUNDS = int(os.getenv("MEMORY_ROUNDS", "5"))
完整 .env 配置示例:
# LLM(必填)LLM_API_KEY=your_api_keyLLM_API_BASE=https://api.modelarts-maas.com/openai/v1LLM_MODEL_NAME=glm-5# 嵌入向量配置# 使用 'modelarts' 表示华为云 ModelArts bge-m3 嵌入向量EMBEDDINGS_PROVIDER=modelarts# 嵌入向量 API 配置EMBEDDINGS_API_BASE=https://api.modelarts-maas.com/v1EMBEDDINGS_MODEL=bge-m3# 文档处理(可选,有默认值)CHUNK_SIZE=1000CHUNK_OVERLAP=100TOP_K=3MEMORY_ROUNDS=5# Langfuse 可观测性(可选)LANGFUSE_PUBLIC_KEY=pk-lf-xxxLANGFUSE_SECRET_KEY=sk-lf-xxxLANGFUSE_HOST=http://localhost:3000
本文分享自华为云开发者社区《实现一个基于LangChain 的 RAG 智能问答Agent实践》,作者:华为云社区精选
开发者福利 · Time
领取专属云开发环境
扫描下方二维码
免费领取180小时云上开发环境
▼▼▼
了解更多:一图详解华为开发者空间
欢迎关注、点赞、分享、留言
发表更多观点
一起交流,共同进步!
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-03-26
都 2026 年了,为什么还有人分不清 LangChain 和 LangGraph?
2026-02-24
进阶指南:BrowserUse + AgentRun Sandbox 最佳实践
2026-02-11
LangGraph五真相
2026-02-10
langchain4j 新版混合检索来了,RAG 准确率直接拉满
2026-02-06
探秘 AgentRun丨为什么应该把 LangChain 等框架部署到函数计算 AgentRun
2026-02-04
Agent生态碎片化终结,.agents/skills统一所有工具
2026-01-29
自建一个 Agent 很难吗?一语道破,万语难明
2026-01-28
全球首个Skills Vibe Agents,AtomStorm技术揭秘:我是怎么用Context Engineering让Agent不"变傻"的
2026-01-05
2026-01-05
2026-01-29
2026-01-22
2025-12-28
2025-12-29
2026-01-28
2026-02-10
2026-02-04
2026-02-11
2026-03-26
2025-11-03
2025-10-29
2025-07-14
2025-07-13
2025-07-05
2025-06-26
2025-06-13