简介:本资源是一套面向高校计算机及相关专业学生的毕业设计/课程设计实战项目,聚焦大语言模型与RAG(检索增强生成)技术落地,以LangChain框架为核心构建智能情感识别与知识检索应用。项目覆盖从环境配置、数据预处理(含多份CSV情感标注语料)、LangChain链式调用开发(多个Python主程序及模块化脚本)、Prompt工程优化到可视化模型结构(drawio图、PDF与JPG图示)的完整流程,配套设计文档、README说明及详细Markdown技术解析。压缩包共27个文件,含7个核心Python脚本、10个CSV数据集、2个Markdown文档、2个PNG/JPG图表、1个PDF模型图、1个DOC设计文档等,整体6.12MB,结构清晰、模块解耦,便于学习复现或二次开发。已有147人下载学习,提供稳定可运行代码、完整测试数据与远程技术支持,特别适合AI方向初学者系统入门,也适合作为毕设选题参考或教学演示案例。
1. 毕业设计选 LangChain + RAG 不是堆概念,而是用最小闭环验证「知识可检索、回答可溯源、逻辑可调试」的三重能力
很多同学把毕业设计做成“调 API 出结果”的演示幻灯片:上传 PDF → 输入问题 → 返回一段文字。但答辩时被问“为什么召回了这篇文档?”“重排序依据是什么?”“LLM 是怎么把 chunk 拼成答案的?”,立刻卡壳。本项目标题里的「LangChain+RAG应用示例开发」,核心不在“用了什么框架”,而在构建一个可拆解、可干预、可验证的 RAG 流水线——从原始文本切片、嵌入向量化、多路召回(关键词+向量+元数据)、重排序(cross-encoder rerank),到 LLM 提示工程与引用标注,每一步都有明确输入输出、可替换模块、可观测日志。它适合两类人:一是需要在 4~6 周内交付可运行、可讲解、可扩展的毕设系统的学生;二是想跳过“Hello World”式教程,直接上手调试真实 RAG 痛点(如召回漂移、幻觉抑制、上下文截断)的初阶开发者。源码不是黑盒打包,而是按ingest/retrieval/generation/eval/四个目录组织,每个.py文件对应一个可独立测试的组件。
2. 用 LangChain 搭建 RAG 最小可行流水线:从文档加载到向量存储的 5 步实操
RAG 的根基不在大模型,而在数据管道的确定性。LangChain 提供了模块化组装能力,但直接套用VectorstoreIndexCreator会掩盖关键决策点。我们采用显式分步法,确保每步可查、可调、可替换。
2.1 文档加载与结构化切片:避免“一刀切”导致语义断裂
PDF、Word、Markdown 等格式需差异化处理。本项目使用UnstructuredPDFLoader(需pip install unstructured[all-docs])保留标题层级,而非简单按字符数切分:
from langchain_community.document_loaders import UnstructuredPDFLoader from langchain_text_splitters import MarkdownHeaderTextSplitter loader = UnstructuredPDFLoader( file_path="data/thesis_guidelines.pdf", mode="elements", # 保留段落、标题、表格等结构信息 strategy="fast" # 平衡速度与精度,对毕业设计文档足够 ) docs = loader.load() # 按 Markdown 标题层级切片,比固定 token 切分更符合语义 headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ] splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on) splits = splitter.split_text(docs[0].page_content) # 注意:此处传入单个 Document 的 content提示:
mode="elements"会将 PDF 解析为带category(如Title,NarrativeText,Table)的元素列表,MarkdownHeaderTextSplitter能识别这些结构并按标题嵌套关系切分。若用RecursiveCharacterTextSplitter,默认按\n\n、\n、 、""四级切分,易在公式、代码块中间断开。
2.2 嵌入模型选型与本地部署:避开 OpenAI 依赖,用 sentence-transformers 实现离线向量化
毕业设计常受限于网络环境与 API 配额。本项目默认使用all-MiniLM-L6-v2(384维,CPU 推理 < 100ms/文档),通过HuggingFaceEmbeddings封装:
from langchain_huggingface import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings( model_name="sentence-transformers/all-MiniLM-L6-v2", model_kwargs={'device': 'cpu'}, # 显存不足时强制 CPU encode_kwargs={'normalize_embeddings': True} # 向量单位化,提升余弦相似度计算稳定性 )参数说明:
normalize_embeddings=True是关键。LangChain 默认不归一化,而 FAISS 等向量库的余弦相似度计算要求向量模长为 1;若未归一化,高维空间下欧氏距离与余弦距离偏差显著,导致召回结果不可靠。此参数必须显式设置。
2.3 向量存储选型对比:FAISS 适合单机调试,Chroma 支持元数据过滤
FAISS 是 Facebook 开源的高效近似最近邻库,零依赖、纯 Python 加载,适合毕业设计本地调试:
from langchain_community.vectorstores import FAISS vectorstore = FAISS.from_documents( documents=splits, embedding=embeddings, # 可选:添加元数据用于后续过滤 metadatas=[{"source": "thesis_guidelines.pdf", "page": 5}] * len(splits) ) vectorstore.save_local("faiss_index") # 保存为本地文件,下次直接加载若需支持按source、date等字段过滤(如“只检索 2023 年后的政策文件”),则切换为 Chroma:
from langchain_community.vectorstores import Chroma vectorstore = Chroma.from_documents( documents=splits, embedding=embeddings, persist_directory="chroma_db", # 自动持久化 collection_name="thesis_docs" ) # 查询时可加 metadata filter retriever = vectorstore.as_retriever( search_kwargs={"filter": {"source": "thesis_guidelines.pdf"}} )2.4 检索器配置:启用多路召回(Multi-Query + HyDE)提升泛化能力
基础向量检索易受 query 表达偏差影响。本项目集成两种增强策略:
- Multi-Query:让 LLM 生成 3 个变体 query,分别检索后合并结果
- HyDE(Hypothetical Document Embeddings):先让 LLM 生成“假设答案”,再对该答案向量化检索
from langchain.retrievers.multi_query import MultiQueryRetriever from langchain.chains import create_history_aware_retriever from langchain_core.prompts import ChatPromptTemplate # 使用本地 LLM(如 Ollama 的 llama3)生成多 query llm = ChatOllama(model="llama3", temperature=0) multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, prompt=ChatPromptTemplate.from_messages([ ("system", "你是一个学术助手,请基于用户问题生成 3 个不同角度的搜索 query,用换行符分隔。"), ("human", "{question}") ]) )注意:
MultiQueryRetriever默认返回去重后的 top-k 文档,但未做重排序。实际使用中需配合reranker(见 3.3 节)。
2.5 构建可追溯的 RetrievalQA 链:让 LLM 引用来源,而非自由发挥
避免“幻觉式回答”,关键在提示词约束与输出解析:
from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_core.prompts import ChatPromptTemplate # 系统提示强制引用 system_prompt = ( "你是一个严谨的毕业设计指导助手。请严格基于以下检索到的文档片段回答问题。" "每个答案必须标注来源:【{source} P{page}】。" "若文档中无相关信息,回答'根据提供的资料无法确定'。" "\n\n{context}" ) prompt = ChatPromptTemplate.from_messages([ ("system", system_prompt), ("human", "{input}"), ]) document_chain = create_stuff_documents_chain(llm, prompt) retrieval_chain = create_retrieval_chain( retriever=multi_retriever, combine_docs_chain=document_chain ) # 执行查询 response = retrieval_chain.invoke({"input": "毕业论文查重率要求是多少?"}) print(response["answer"]) # 输出含【thesis_guidelines.pdf P3】的引用答案关键点:
create_retrieval_chain将retriever与document_chain绑定,response中context字段即为被选中的 Document 列表,answer字段由 LLM 生成。通过解析response["context"]可验证召回质量,这是答辩时展示“可解释性”的核心证据。
3. RAG 流程详解:从数据注入到答案生成的 7 个关键节点与调试方法
一个完整 RAG 流程不是线性执行,而是包含数据预处理、检索、重排序、生成、评估五个阶段。本节以ingest.py→retrieval.py→generation.py→eval.py四个脚本为线索,逐节点说明输入输出、常见失败现象及定位命令。
3.1 数据注入阶段(ingest.py):验证切片质量与向量一致性
该阶段输出faiss_index/目录。调试重点是确认切片是否保留关键信息:
# 查看切片数量与平均长度 python -c " from langchain_community.vectorstores import FAISS v = FAISS.load_local('faiss_index', embeddings=None) print(f'总切片数: {len(v.docstore._dict)}') print(f'平均长度: {sum(len(d.page_content) for d in v.docstore._dict.values()) / len(v.docstore._dict):.0f} 字符') "典型问题:PDF 表格被转为乱码、公式丢失、页眉页脚混入正文。解决方案:
UnstructuredPDFLoader的strategy="hi_res"(需安装unstructured[pdf]和pdfminer.six)可提升表格识别,但速度下降 3 倍;或预处理 PDF 用pdfplumber提取文本+表格分离存储。
3.2 检索阶段(retrieval.py):用 similarity_score_threshold 过滤低置信召回
默认as_retriever()返回 top-k 文档,但可能包含相似度仅 0.2 的噪声。显式设置阈值:
retriever = vectorstore.as_retriever( search_type="similarity_score_threshold", search_kwargs={ "k": 5, "score_threshold": 0.5 # 余弦相似度 > 0.5 才返回 } )调试命令:直接调用
vectorstore.similarity_search_with_score()查看原始分数:docs_and_scores = vectorstore.similarity_search_with_score("查重率标准", k=5) for doc, score in docs_and_scores: print(f"Score: {score:.3f} | {doc.page_content[:50]}...")若最高分 < 0.4,说明嵌入模型或 query 表达需优化。
3.3 重排序阶段(rerank.py):用 cross-encoder 替代 BM25 提升相关性
向量检索后,用sentence-transformers/ms-marco-MiniLM-L-6-v2对 query-doc pair 进行精排:
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_huggingface import HuggingFaceCrossEncoder compressor = CrossEncoderReranker( model=HuggingFaceCrossEncoder(model_name="cross-encoder/ms-marco-MiniLM-L-6-v2"), top_n=3 ) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever )参数说明:
top_n=3表示从向量检索返回的 5 个文档中,选出最相关的 3 个送入 LLM。ms-marco-MiniLM-L-6-v2是专为重排序微调的模型,在 TREC-DL 数据集上 MRR@10 达 0.32,显著优于 BM25 规则。
3.4 生成阶段(generation.py):控制 token 截断与引用锚点
LLM 上下文窗口有限,需精确控制context长度。本项目用TokenTextSplitter动态截断:
from langchain.text_splitter import TokenTextSplitter def truncate_context(context_docs, max_tokens=2000): """按 token 数截断 context,保留完整 Document""" splitter = TokenTextSplitter(chunk_size=max_tokens, chunk_overlap=0) full_text = "\n\n".join([d.page_content for d in context_docs]) truncated = splitter.split_text(full_text)[0] # 取第一个 chunk return [Document(page_content=truncated, metadata=context_docs[0].metadata)] # 在 prompt 中插入截断后 context prompt = ChatPromptTemplate.from_messages([ ("system", "基于以下资料回答,引用格式【{source}】:\n{context}"), ("human", "{input}") ])注意:
TokenTextSplitter比字符切分更准确,其chunk_size单位为 token(经 tiktoken 计算)。若用llama3模型,1 个中文字符 ≈ 1.3 token,故max_tokens=2000约容纳 1500 字中文。
3.5 评估阶段(eval.py):用 answer relevancy 和 faithfulness 指标量化效果
毕业设计需量化指标。本项目集成langchain-community的评估链:
from langchain.evaluation import load_evaluator # 评估答案相关性(Answer Relevancy) relevancy_evaluator = load_evaluator( "labeled_score_string", criteria={"answer_relevancy": "答案是否直接回应问题?1-5 分"} ) # 评估忠实度(Faithfulness):答案是否被 context 支持? faithfulness_evaluator = load_evaluator( "labeled_score_string", criteria={"faithfulness": "答案中每个事实是否能在 context 中找到依据?1-5 分"} ) # 示例评估 result = faithfulness_evaluator.evaluate( predictions=response["answer"], input="毕业论文查重率要求是多少?", reference_contexts=[doc.page_content for doc in response["context"]] ) print(f"Faithfulness Score: {result['score']}")指标含义:
faithfulness低于 3 分,说明 LLM 编造内容;answer_relevancy低于 3 分,说明 context 未覆盖问题核心。这两个指标比 BLEU、ROUGE 更贴合 RAG 场景。
3.6 元数据过滤实战:按文档类型/时间/章节精准召回
Chroma 支持复杂过滤,例如“只检索‘格式规范’章节中的内容”:
# 在 ingest 阶段为每个 Document 添加结构化元数据 for doc in splits: if "格式规范" in doc.metadata.get("header", ""): doc.metadata["section"] = "format" # 检索时过滤 retriever = vectorstore.as_retriever( search_kwargs={"filter": {"section": "format"}} )调试技巧:用
vectorstore.get()查看某 Document 的完整元数据:doc = vectorstore.docstore._dict[list(vectorstore.docstore._dict.keys())[0]] print(doc.metadata) # 输出 {'source': 'thesis_guidelines.pdf', 'page': 5, 'section': 'format'}
3.7 故障排查速查表:5 类高频报错与修复命令
| 报错现象 | 根本原因 | 修复命令 |
|---|---|---|
ValueError: max() arg is an empty sequence | 向量库为空,ingest.py未成功执行 | ls faiss_index/确认存在index.faissindex.pkl |
torch.cuda.OutOfMemoryError | embedding 模型加载到 GPU 但显存不足 | 在HuggingFaceEmbeddings中添加model_kwargs={'device': 'cpu'} |
KeyError: 'source' | prompt 中引用{source}但 Document 无该 metadata | print(docs[0].metadata)检查字段名,或统一设doc.metadata["source"] = "xxx" |
Context length exceeded | LLM 输入超限 | 在create_stuff_documents_chain前调用truncate_context() |
No module named 'langchain_huggingface' | LangChain 0.1+ 版本拆分包 | pip install langchain-huggingface |
4. RAG 多路召回与 Agent 开发进阶:用 LangGraph 实现动态路由与人工审核回路
当毕业设计需体现“智能性”而非“自动化”,应引入决策逻辑。LangGraph 作为 LangChain 的有状态图框架,可构建带人工审核节点的 RAG 工作流,这比单纯调用AgentExecutor更可控。
4.1 构建带审核节点的 RAG 图:定义 state 与 conditional edge
核心是定义State数据结构,包含question、context、answer、needs_review四个字段:
from typing import TypedDict, Annotated, Sequence import operator from langgraph.graph import StateGraph, END class RAGState(TypedDict): question: str context: Sequence[str] answer: str needs_review: bool # 是否需人工审核 def retrieve_node(state: RAGState) -> RAGState: docs = retriever.invoke(state["question"]) return { "question": state["question"], "context": [d.page_content for d in docs], "answer": "", "needs_review": False } def generate_node(state: RAGState) -> RAGState: # 调用 LLM 生成答案 answer = llm.invoke(f"基于以下资料回答:{state['context']}\n问题:{state['question']}") # 规则:若答案含“可能”、“大概”、“建议咨询”等模糊词,标记审核 needs_review = any(word in answer.content for word in ["可能", "大概", "不确定", "建议"]) return { "question": state["question"], "context": state["context"], "answer": answer.content, "needs_review": needs_review } def human_review_node(state: RAGState) -> RAGState: print(f"【人工审核】问题:{state['question']}\n答案:{state['answer']}") user_input = input("是否通过?(y/n): ") return {"needs_review": False if user_input.lower() == "y" else True} # 定义图 workflow = StateGraph(RAGState) workflow.add_node("retrieve", retrieve_node) workflow.add_node("generate", generate_node) workflow.add_node("review", human_review_node) workflow.set_entry_point("retrieve") workflow.add_edge("retrieve", "generate") # 条件边:根据 needs_review 决定是否进入 review def should_review(state: RAGState) -> str: return "review" if state["needs_review"] else END workflow.add_conditional_edges("generate", should_review) workflow.add_edge("review", END)优势说明:此图将“是否审核”决策外置,答辩时可演示:输入“答辩PPT模板在哪里下载?”,因答案含“建议访问教务处网站”,触发
needs_review=True,暂停等待人工确认;而输入“查重率上限是多少?”,直接输出带引用的答案。这体现了对不确定性边界的认知,远超静态 RAG。
4.2 集成外部工具:用 requests 调用校内教务系统 API 补充实时数据
RAG 的知识库是静态的,但毕业流程涉及动态信息(如查重系统维护时间)。LangGraph 支持工具调用:
import requests def fetch_maintain_time() -> str: """调用校内 API 获取查重系统维护时间""" try: resp = requests.get("https://jwxt.xxx.edu.cn/api/maintenance", timeout=5) return resp.json().get("next_maintenance", "暂无维护计划") except: return "网络连接失败" # 在 generate_node 中调用 def generate_node(state: RAGState) -> RAGState: # ... 先用 RAG 生成答案 if "查重系统" in state["question"]: maintain_info = fetch_maintain_time() answer += f"\n【实时信息】{maintain_info}" # ...安全实践:生产环境需添加
requests.Session()复用连接、retry重试机制,并将 API 地址存于.env文件,避免硬编码。
4.3 可视化执行轨迹:用langgraph.checkpoint.sqlite记录每步 state
答辩时需展示“系统如何思考”。启用 SQLite 检查点:
from langgraph.checkpoint.sqlite import SqliteSaver checkpointer = SqliteSaver.from_uri("checkpoints.db") app = workflow.compile(checkpointer=checkpointer) # 执行后查询历史 import sqlite3 conn = sqlite3.connect("checkpoints.db") cursor = conn.cursor() cursor.execute("SELECT * FROM checkpoints ORDER BY thread_ts DESC LIMIT 5") for row in cursor.fetchall(): print(f"Step: {row[2]}, State: {row[3][:100]}...") # row[2] 是 step, row[3] 是 state json输出示例:
Step: 2, State: {"question": "查重率要求", "context": ["根据《本科生毕业论文管理规定》第5条...", "附件1:查重率阈值表..."], "answer": "本科毕业论文查重率不得高于15%。", "needs_review": false}—— 这就是可写进毕设论文“系统实现”章节的原始日志。
5. 毕业设计落地技巧:3 个让答辩老师眼前一亮的细节处理
答辩不是功能演示,而是展现工程思维。以下三个细节无需额外代码,但能极大提升专业感。
5.1 在 UI 层显示召回依据:用 HTML 渲染带高亮的 source 文本
Streamlit 前端中,不只显示答案,还展开被引用的原文片段:
import streamlit as st def render_source(doc, query): # 用正则高亮 query 在 doc 中的匹配位置 highlighted = re.sub(f"({re.escape(query)})", r"<mark>\1</mark>", doc.page_content[:200]) return f"""<div style="border-left: 3px solid #4CAF50; padding-left: 10px; margin: 10px 0;"> <small><b>来源:</b>{doc.metadata.get('source', '未知')} P{doc.metadata.get('page', '?')}</small><br> {highlighted}... </div>""" # 在 st.write() 中渲染 for i, doc in enumerate(response["context"]): st.markdown(render_source(doc, user_question), unsafe_allow_html=True)效果:老师能看到“答案来自哪一页、哪一段”,且关键词自动高亮。这比单纯打印
doc.metadata更直观,证明你理解了 RAG 的“可追溯”本质。
5.2 用 requirements.txt 锁定版本:避免答辩现场环境不一致
requirements.txt必须精确到 patch 版本,而非langchain>=0.1.0:
langchain==0.1.16 langchain-huggingface==0.0.1 sentence-transformers==2.3.0 faiss-cpu==1.9.0 pymupdf==1.23.22 unstructured==0.10.22验证命令:答辩前在干净虚拟环境中测试:
python -m venv env && source env/bin/activate && pip install -r requirements.txt && python main.py若报错,立即修正——这是答辩时最不该出现的失误。
5.3 设计可复现的测试用例:覆盖边界场景
在tests/目录下提供 3 个.md测试用例,每个含question、expected_source、expected_answer_fragment:
<!-- tests/test_highlight.md --> ## 测试用例:查重率阈值 - **Question**: 本科毕业论文查重率上限是多少? - **Expected Source**: thesis_guidelines.pdf P3 - **Expected Answer Fragment**: "不得超过15%"执行脚本:
test_runner.py自动遍历测试用例,调用 RAG 链,比对response["answer"]是否含expected_answer_fragment,并检查response["context"][0].metadata["source"]是否匹配。答辩时可当场运行python test_runner.py,证明系统鲁棒性。
最后,打开main.py,确认if __name__ == "__main__":下的入口函数调用顺序为ingest()→retrieval()→generation()→eval(),且每个函数有清晰的print("✅ Ingest completed")日志。这便是毕业设计最扎实的收尾——没有炫技,只有可验证、可讲解、可复现的工程闭环。
本文还有配套的精品资源,点击获取