
用LangChain实现知识问答链连接检索器、上下文构建与答案输出这篇文章要解决的问题你有一批课程讲义或产品文档想做一个能回答“库存还剩多少”“退货政策是什么”这类问题的问答工具。直接问大语言模型它会编造一个看起来合理但实际不存在的答案。原因很简单模型没有读过你的文档。你需要的是一条“知识问答链”——先从你的文档里找到相关段落再把找到的内容作为上下文交给模型让模型基于这些内容回答。这篇文章用一个虚构的课程资料场景从零实现这条链。完成后你会得到一段可以运行的Python代码、一组可复现的验收测试以及判断“检索是否正常、答案是否可信”的具体方法。适用环境Python 3.10Linux/macOS终端Bash。模型服务使用OpenAI兼容接口通过环境变量配置。向量存储使用内存实现无需额外数据库服务。前置知识与案例输入这篇文章假设你知道Python的基本语法和pip install。你不需要事先了解LangChain——所有用到的类和方法都会在首次出现时解释。案例使用一组虚构的在线课程平台运营文档共4份。每份文档描述一个运营话题以下是完整内容你需要将它们保存为项目目录下的文件文件docs/shipping.md# 发货说明 标准发货订单确认后 2 个工作日内发出配送时间 3-5 个工作日。 加急发货订单确认后 24 小时内发出配送时间 1-2 个工作日额外费用 15 元。 偏远地区西藏、新疆、内蒙古配送时间在标准基础上增加 3-5 天。 单笔订单满 99 元免标准运费不包含加急费用。文件docs/returns.md# 退货政策 自签收之日起 7 天内可无理由退货商品需保持全新且包装完整。 退货运费由买家承担质量问题除外。 退款在仓库签收退货后 3-5 个工作日内原路退回。 虚拟商品、已拆封的软件授权卡不支持退货。文件docs/inventory.md# 库存清单虚构数据 Python 入门实战课库存 47 份售价 199 元。 数据分析进阶课库存 12 份售价 299 元。 机器学习基础课库存 0 份已售罄。 前端开发速成课库存 83 份售价 249 元。文件docs/membership.md# 会员等级说明 普通会员无门槛购买课程享受原价。 银卡会员累计消费满 500 元享受 95 折。 金卡会员累计消费满 2000 元享受 9 折且每季度获得一张 20 元无门槛券。 会员折扣不与限时活动价叠加。为什么用检索链而不是直接问模型直接向模型提问“机器学习基础课还有库存吗”模型会基于训练数据编造一个答案。检索增强生成Retrieval-Augmented GenerationRAG的思路是先从文档中取出可能包含答案的片段再把“问题 文档片段”一起交给模型并在指令中约束“只根据给定内容回答”。在LangChain中这条流程由三个组件串联而成检索器Retriever接收用户问题返回一组相关文档片段。提示模板Prompt Template把问题和检索到的片段拼接成模型可读的上下文。语言模型LLM根据提示生成答案。LangChain的旧版RetrievalQA链已从0.2版本标记为弃用并在后续版本中移除。当前推荐的方式是用LCELLangChain Expression Language把组件串联这样每一步都可见、可单独调试。我们用LCEL。完整实现项目文件结构如下文件用途docs/*.md知识源文档上方4份qa_chain.py主程序加载文档、构建检索器、拼装问答链test_qa.py验收测试脚本依赖安装在项目根目录执行pipinstalllangchain langchain-openai langchain-community python-dotenv标准库os、pathlib。第三方库langchain核心抽象与LCEL、langchain-openaiOpenAI兼容接口、langchain-community文档加载器。配置准备创建.env文件不要提交到版本控制OPENAI_API_KEY你的密钥 OPENAI_BASE_URLhttps://api.openai.com/v1 MODEL_NAMEgpt-4o-mini EMBEDDING_MODELtext-embedding-3-smallOPENAI_BASE_URL可替换为兼容OpenAI协议的其他服务端点。如果使用其他服务商模型名称需相应调整。缺少OPENAI_API_KEY时程序会在初始化模型时抛出明确的认证错误。主程序qa_chain.pyimportosfrompathlibimportPathfromdotenvimportload_dotenvfromlangchain_community.document_loadersimportTextLoaderfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain_openaiimportChatOpenAI,OpenAIEmbeddingsfromlangchain_core.vectorstoresimportInMemoryVectorStorefromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParserfromlangchain_core.runnablesimportRunnablePassthrough load_dotenv()# ---------- 1. 加载与切分文档 ----------defload_and_split(docs_dir:strdocs)-list:加载docs目录下所有.md文件切分为片段。all_docs[]forpathinsorted(Path(docs_dir).glob(*.md)):loaderTextLoader(str(path),encodingutf-8)all_docs.extend(loader.load())splitterRecursiveCharacterTextSplitter(chunk_size200,chunk_overlap40,separators[\n\n,\n,。,],)chunkssplitter.split_documents(all_docs)returnchunks# ---------- 2. 构建向量存储与检索器 ----------defbuild_retriever(chunks:list):将文档片段嵌入并存入内存向量库返回检索器。embeddingsOpenAIEmbeddings(modelos.getenv(EMBEDDING_MODEL,text-embedding-3-small))vector_storeInMemoryVectorStore.from_documents(chunks,embeddings)returnvector_store.as_retriever(search_kwargs{k:3})# ---------- 3. 构建问答链 ----------defbuild_qa_chain(retriever):用LCEL串联检索器、提示模板和模型。llmChatOpenAI(modelos.getenv(MODEL_NAME,gpt-4o-mini),temperature0,)promptChatPromptTemplate.from_template(根据以下上下文回答用户问题。只使用上下文中出现的信息。 如果上下文中没有答案回答“根据现有资料无法回答该问题”。 上下文 {context} 问题{question} 回答)defformat_docs(docs):return\n\n.join(doc.page_contentfordocindocs)chain({context:retriever|format_docs,question:RunnablePassthrough()}|prompt|llm|StrOutputParser())returnchain# ---------- 4. 入口 ----------defmain():chunksload_and_split()print(f文档片段数{len(chunks)})retrieverbuild_retriever(chunks)chainbuild_qa_chain(retriever)question机器学习基础课还有库存吗answerchain.invoke(question)print(f问题{question})print(f答案{answer})if__name____main__:main()运行方式python qa_chain.py中间结果说明程序首先输出文档片段数量。4份文档经切分后通常产生8-12个片段取决于分隔符匹配结果。然后输出问题和答案。对于问题“机器学习基础课还有库存吗”正确输出应包含“已售罄”或“库存为0”的含义。检索器为什么要返回3个片段search_kwargs{k: 3}表示每次检索返回最相似的3个片段。设得太小可能漏掉答案设得太大则把无关内容塞进上下文既增加成本也可能干扰模型判断。3是一个保守的起点。运行后如果发现检索经常漏掉相关片段可以调大如果上下文明显被无关内容占据可以调小。为什么提示模板中要写“如果上下文中没有答案”这是RAG中约束模型行为的关键。模型默认会基于训练知识“补充”答案。明确要求“只使用上下文中出现的信息”并指定“无法回答”时的输出格式能让测试有明确的预期。验收测试中会专门验证这一行为。验收与测试以下测试脚本验证三种情况正常问答、边界情况问题超出文档范围、检索行为。文件test_qa.pyfromqa_chainimportload_and_split,build_retriever,build_qa_chain chunksload_and_split()retrieverbuild_retriever(chunks)chainbuild_qa_chain(retriever)# ---------- 测试1正常场景 ----------q1金卡会员买数据分析进阶课能便宜多少钱a1chain.invoke(q1)print(f[正常] Q:{q1})print(f[正常] A:{a1})# 预期答案中应提及“9折”或“299×0.9269.1”且来源为membership和inventory相关片段# ---------- 测试2边界场景文档中无答案 ----------q2支持货到付款吗a2chain.invoke(q2)print(f[边界] Q:{q2})print(f[边界] A:{a2})# 预期答案应包含“无法回答”或明确表示不知道不应编造支付方式# ---------- 测试3检索行为验证 ----------q3退货要几天内docsretriever.invoke(q3)print(f[检索] Q:{q3})print(f[检索] 返回片段数{len(docs)})fori,dinenumerate(docs):print(f 片段{i1}:{d.page_content[:60]}...)# 预期返回3个片段且至少一个来自returns.md包含“7天”# ---------- 测试4检索与答案的一致性 ----------q4满99免运费吗docs4retriever.invoke(q4)a4chain.invoke(q4)print(f[一致性] Q:{q4})print(f[一致性] 检索到{docs4[0].page_content[:50]}...)print(f[一致性] 答案{a4})# 预期检索片段含“99元免标准运费”答案不应出现“加急免运费”等错误扩展执行python test_qa.py验收表场景测试目的输入预期结果判定方法正常跨文档合成答案金卡会员买数据分析进阶课能便宜多少钱答案提及9折数值约269.1元人工阅读答案确认包含“9折”或“269”边界文档外问题不编造支持货到付款吗回答表示无法从现有资料回答答案包含“无法回答”或“现有资料”检索检索器命中正确来源退货要几天内3个片段中至少1个来自returns.md含“7天”检查打印的片段内容一致性答案不超出检索内容满99免运费吗答案不将免运费扩大到加急场景答案不出现“加急免费”表述失败情况的诊断如果测试1的答案没有提及折扣可能的原因按排查顺序第一检索没有返回membership.md的片段。执行retriever.invoke(金卡会员)检查返回的片段中是否有membership.md的内容。如果没有可能是嵌入模型对“会员等级”这类中文语义的区分度不够或者k3太小导致正确片段被挤掉。第二检索返回了正确片段但模型忽略了。把format_docs的输出单独打印出来确认“金卡会员累计消费满2000元享受9折”确实在上下文中。如果它在但答案仍然错误可能是提示模板的约束力不够可以尝试把指令从“根据以下上下文”加强为“必须仅根据以下上下文回答不得使用任何外部知识”。第三模型本身的指令遵循问题。更换模型或降低temperature到0后重试。temperature0已在代码中设置。如果测试2中模型仍然编造了答案说明提示模板中的“如果无法回答”指令没有被有效执行。可以在提示中增加一个具体示例“如果上下文提到支付方式为空则回答‘根据现有资料无法回答该问题’。”但更好的做法是先检查检索是否返回了无关片段——如果返回了看似相关但实际不包含答案的片段模型可能“被误导”。适用边界与未覆盖部分这篇文章实现的是最基础的检索链固定切分、单一检索器、Stuff拼接、单一模型。以下情况未覆盖需要在实际使用时另行考虑文档量增大后的检索质量下降当文档超过几百份时固定k3可能不够。可以引入多查询检索器或压缩检索器来改善。PDF、HTML等非纯文本格式示例只处理Markdown。其他格式需要对应的Document Loader。多轮对话当前链每次独立处理问题不保留历史。多轮问答需要引入对话历史管理。回答中的引用溯源示例打印了检索片段但答案本身没有标注来源。生产场景通常要求答案附带来源链接或文档标识。验证状态已完成核验LangChain 0.2中RetrievalQA已弃用LCEL是当前推荐方式此信息已通过官方文档和源码交叉确认。代码中使用的导入路径langchain_community.document_loaders.TextLoader、langchain_core.vectorstores.InMemoryVectorStore、langchain_core.runnables.RunnablePassthrough与LangChain当前Python包结构一致。提示模板、链式组装的结构符合LCEL标准模式。未执行代码未在真实环境运行验证。原因是没有可用的模型服务凭证。依赖安装、启动命令和测试步骤已按静态一致性检查。嵌入模型的语义检索效果未实测。k3和chunk_size200是基于常见实践的选择实际最佳值取决于文档密度和问题类型。答案的准确性、拒答行为的有效性未通过真实模型调用验证。验收表中的预期结果基于逻辑推断和提示约束实际输出可能存在偏差。读者可执行的补充验证配置有效的API密钥后按本文的测试脚本运行记录实际输出对照验收表判断。如果测试2的拒答行为未触发优先调整提示模板并重新测试。