用 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}

  • 模板的约束逻辑:

    1. 回答必须完全基于「上下文」。

    2. 如果上下文中信息不足 → 必须回答那句固定话。

  • 后面你会用:

    
      

    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)

    1. 先对 question 做 embedding。

    2. 在向量库中找出相似度最高的 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)

这一行很关键,做了几件事:

  1. prompt.format(...)

    • questiondocs_content 填进 Prompt 模板,返回一个 完整字符串,比如:

      
          

      请根据下面提供的上下文信息来回答问题。 ... 上下文: (这里是检索到的段落) 问题: 文中举了哪些例子? 回答:

  2. HumanMessage(content=...)

    • 把这个字符串包装成 LangChain 的消息对象,角色是“人类”。

  3. llm.invoke([...])

    • 调用 DeepSeek:

      • 把此消息作为对话历史中唯一的一条 user 消息发给 DeepSeek。

      • DeepSeek 返回一个 AIMessage

  4. response.content

    • 拿到模型真正的回答文本并打印。

如果上下文确实包含了“举的例子”,模型就会按你的提示进行回答;
如果上下文里没有找到任何“例子”的词句,模型按照你写的规则,只能输出那句:

“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”


10. 这一段代码整体就是一个 最小 RAG Demo

流程总结成图就是:

  1. 加载 markdown → docs

  2. docs → 切分 → chunks

  3. chunks → embedding → InMemoryVectorStore

  4. 用户问题 → embedding → 相似度检索 → top-k chunks

  5. PromptTemplate + {context, question} → 完整 prompt

  6. 调用 DeepSeek Chat → 得到回答

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐