datawhale 组队学习 RAG技术全栈指南task1打卡
先把章节内容切块 → 建索引 → 用问题做向量检索拿到相关片段 → 把这些片段塞进 Prompt → 让 DeepSeek 在这些上下文内回答问题”。
用 LangChain + 本地 markdown 文档 + 中文向量检索 + DeepSeek 大模型,搭一个最小 RAG:
“先把章节内容切块 → 建索引 → 用问题做向量检索拿到相关片段 → 把这些片段塞进 Prompt → 让 DeepSeek 在这些上下文内回答问题”。
问题是:“文中举了哪些例子?”
1. 环境与依赖配置部分
from dotenv import load_dotenv from langchain_community.document_loaders import UnstructuredMarkdownLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_core.vectorstores import InMemoryVectorStore from langchain_core.prompts import ChatPromptTemplate from langchain_deepseek import ChatDeepSeek from langchain_core.messages import HumanMessage import os
-
dotenv.load_dotenv:用于从.env文件中加载环境变量(比如DEEPSEEK_API_KEY)。 -
UnstructuredMarkdownLoader:负责从 markdown 文件里 读文档。 -
RecursiveCharacterTextSplitter:按字符层级 切分文本(分块,避免太长)。 -
HuggingFaceEmbeddings:用 HuggingFace 的中文 Embedding 模型做向量表示。 -
InMemoryVectorStore:内存版的向量数据库,用来做相似度检索。 -
ChatPromptTemplate:LangChain 的 Prompt 模板系统,用来把上下文和问题拼进统一的提示词。 -
ChatDeepSeek:DeepSeek 的聊天模型封装(LangChain 模型类)。 -
HumanMessage:LangChain 的 Message 类型,表示“用户消息”。
load_dotenv()
-
从
.env读取各种KEY之类的配置,比如DEEPSEEK_API_KEY。
2. 加载 Markdown 文档
markdown_path = "../../data/C1/markdown/easy-rl-chapter1.md" assert os.path.exists(markdown_path), f"文件不存在: {markdown_path}"
-
markdown_path:指定你要读的那一章的 markdown 文件路径。 -
assert os.path.exists(...):如果文件不存在,直接抛异常,避免后面加载空文档。
loader = UnstructuredMarkdownLoader(markdown_path) docs = loader.load() print(f"加载到文档 {len(docs)} 篇")
-
UnstructuredMarkdownLoader(markdown_path):构造一个 Loader。 -
loader.load():真正执行加载,返回一个docs列表,每个元素通常是一个Document对象。-
Document.page_content:正文 -
Document.metadata:元信息(文件名、路径等)
-
-
len(docs)一般是 1(整章作为一个文档),也可能多个。
3. 文本分块(Text Splitting)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100) chunks = text_splitter.split_documents(docs) print(f"分块 {len(chunks)} 个")
-
RecursiveCharacterTextSplitter:-
chunk_size=800:每块最多 800 字符。 -
chunk_overlap=100:相邻块重叠 100 字符,保证语义连续性。
-
-
split_documents(docs):-
把上一步的
docs切成多个小Document,每块长度≈800 字符。
-
-
chunks就是后面要进向量库的文本单元(RAG 的最小检索单位)。
好处:
-
太长的文本 → 切成多个小段,更适合做向量检索。
-
重叠 → 减少“边界信息丢失”的问题。
4. 创建中文 Embedding 模型
embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} )
-
model_name="BAAI/bge-small-zh-v1.5":使用 BAAI 的 bge 中文小模型。-
非常适合中文语义检索。
-
-
model_kwargs={'device': 'cpu'}:在 CPU 上跑 embedding(方便在没有 GPU 的环境测试)。 -
encode_kwargs={'normalize_embeddings': True}:-
对向量做 L2 归一化,方便之后用余弦相似度等。
-
这个对象的核心功能就是:
embeddings.embed_query("我是谁") embeddings.embed_documents([text1, text2, ...])
→ 把文本变成向量。
5. 构建向量存储(Vector Store)
vectorstore = InMemoryVectorStore(embeddings) vectorstore.add_documents(chunks)
-
InMemoryVectorStore(embeddings):-
建立一个“内存版向量数据库”,保存「文本 + 向量」。
-
会在内部记住:如何用
embeddings对文本进行编码。
-
-
add_documents(chunks):-
把前面切好的每个小
Document计算 embedding 向量并存储。 -
之后就能
similarity_search。
-
之后就可以这么用:
vectorstore.similarity_search("强化学习有哪些例子?", k=3)
会返回与这个问题语义最相似的 3 个文本块。
6. 构造 Prompt 模板
prompt = ChatPromptTemplate.from_template("""请根据下面提供的上下文信息来回答问题。 请确保你的回答完全基于这些上下文。 如果上下文中没有足够的信息来回答问题,请直接回答:“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。” 上下文: {context} 问题: {question} 回答:""")
-
这是一个 带占位符的模板,有两个变量:
{context}和{question}。 -
模板的约束逻辑:
-
回答必须完全基于「上下文」。
-
如果上下文中信息不足 → 必须回答那句固定话。
-
-
后面你会用:
prompt.format(question=question, context=docs_content)生成一个真正给 LLM 的字符串。
这就是 RAG 的经典范式:
“历史 + 检索结果 + 指示” 拼成一个 prompt 再丢给 LLM。
7. 配置 DeepSeek 大模型(LLM)
llm = ChatDeepSeek( model="deepseek-chat", temperature=0.7, max_tokens=6000, api_key=os.getenv("DEEPSEEK_API_KEY") )
-
model="deepseek-chat":使用 DeepSeek 的 chat 模型。 -
temperature=0.7:-
控制“随机性”/创造力。0 越确定,1 越发散。
-
-
max_tokens=6000:-
最多生成 6000 tokens 的内容(上限较高)。
-
-
api_key=os.getenv("DEEPSEEK_API_KEY"):-
从环境变量读取密钥(
load_dotenv()就是为这个服务的)。
-
这个 llm 对象是 LangChain 中一个 Chat 模型包装,有 invoke() 等方法。
8. 提问 & 向量检索
question = "文中举了哪些例子?" retrieved_docs = vectorstore.similarity_search(question, k=3) docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs) print("检索命中内容:\n", docs_content[:500])
-
question:RAG 的用户查询。 -
similarity_search(question, k=3):-
先对
question做 embedding。 -
在向量库中找出相似度最高的 3 个文本块。
-
-
docs_content:把这 3 个块的正文用\n\n拼在一起,作为 Prompt 的{context}。 -
print:方便你 debug,看看检索到底命中了什么内容。
如果这里打印是空字符串/乱七八糟 → 说明问题不在 LLM,而在 检索没命中。
9. 调用大模型生成回答
response = llm.invoke([HumanMessage(content=prompt.format(question=question, context=docs_content))]) print("回答:\n", response.content)
这一行很关键,做了几件事:
-
prompt.format(...)-
把
question和docs_content填进 Prompt 模板,返回一个 完整字符串,比如:请根据下面提供的上下文信息来回答问题。 ... 上下文: (这里是检索到的段落) 问题: 文中举了哪些例子? 回答:
-
-
HumanMessage(content=...)-
把这个字符串包装成 LangChain 的消息对象,角色是“人类”。
-
-
llm.invoke([...])-
调用 DeepSeek:
-
把此消息作为对话历史中唯一的一条 user 消息发给 DeepSeek。
-
DeepSeek 返回一个
AIMessage。
-
-
-
response.content-
拿到模型真正的回答文本并打印。
-
如果上下文确实包含了“举的例子”,模型就会按你的提示进行回答;
如果上下文里没有找到任何“例子”的词句,模型按照你写的规则,只能输出那句:
“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”
10. 这一段代码整体就是一个 最小 RAG Demo
流程总结成图就是:
-
加载 markdown → docs -
docs → 切分 → chunks -
chunks → embedding → InMemoryVectorStore -
用户问题 → embedding → 相似度检索 → top-k chunks -
PromptTemplate + {context, question} → 完整 prompt -
调用 DeepSeek Chat → 得到回答
更多推荐
所有评论(0)