黔东南苗族侗族自治州网站建设_网站建设公司_页面加载速度_seo优化
2025/12/23 0:58:08 网站建设 项目流程

本文档基于src/examples/retrieval/demo_retrieval1.py,详细讲解如何利用 LangChain 实现一个简单的 RAG (Retrieval-Augmented Generation) 系统。我们将重点展示每个步骤的代码实现、调试输出(Logs),并深度解析每一个关键函数及其参数。


语料背景说明

本项目使用的示例文本是《爱比克泰德金言录》 (The Golden Sayings of Epictetus)

  • 作者:爱比克泰德 (Epictetus),古希腊著名的斯多葛学派 (Stoicism) 哲学家。
  • 内容:核心思想是“区分我们能控制的和不能控制的”。他教导人们在面对无法改变的外部环境时,如何通过控制自己的判断来获得内心的宁静。
  • 用途:包含大量哲理段落的非结构化文本,适合演示 RAG 如何检索智慧。

整体流程图

Phase 2: Retrieval & Generation (检索与生成)
Phase 1: Indexing (构建知识库)
Filter
TextLoader
Splitter
Embedding Model
Index
Embedding Model
Similarity Search
Top K Docs & Scores
Format
Send
Response
Query Vector
User Query
Context
Prompt
Gemini LLM
Final Answer
Cleaned Text
Raw Text File
Documents
Chunks
Vectors
FAISS Vector Store

第一部分:构建 Vector Store

步骤 1: 数据准备 (Data Preparation)

获取原始文本,过滤无关内容(如版权声明),并保存为纯净的文本文件。

src_doc="src/examples/retrieval/docs/src_golden_hymns_of_epictetus.txt"output_doc="src/examples/retrieval/docs/output_golden_hymns_of_epictetus_new.txt"start_saving=Falsestop_saving=Falseline_to_save=[]withopen(src_doc,"r",encoding="utf-8")asf:fori,lineinenumerate(f.readlines()):ifi>2000:break# (Demo仅读取前2000行)if"Are these the only works of Providence within us?"inline:start_saving=Trueif"*** END OF THE PROJECT GUTENBERG EBOOK THE GOLDEN SAYINGS OF EPICTETUS"inline:stop_saving=Trueifstart_savingandnotstop_saving:line_to_save.append(line)logger.info("len of line_to_save:"+str(len(line_to_save)))withopen(output_doc,"w",encoding="utf-8")asf:f.writelines(line_to_save)

真实调试输出

INFO | len of line_to_save:1740

深度代码解析
这一步主要使用 Python 标准库进行文件处理,不涉及 LangChain 特定函数。

  • open(..., encoding="utf-8"): 确保以 UTF-8 编码读取和写入文件,防止处理非 ASCII 字符时出现乱码。
  • 过滤逻辑: 通过简单的字符串匹配 (if "..." in line) 来确定正文的起止位置。这是 RAG 流程中至关重要的数据清洗 (Data Cleaning)步骤。如果不过滤,RAG 可能会检索到版权声明等无用信息,干扰回答。

步骤 2: 加载数据 (Document Loading)

使用TextLoader加载文件。

fromlangchain_community.document_loadersimportTextLoader logger.info("======= load text data to langchain============")# 初始化加载器loader=TextLoader(file_path=output_doc)# 执行加载golden_saying_content=loader.load()logger.info("type of golden_saying_content: "+str(type(golden_saying_content)))logger.info("len of golden_saying_content: "+str(len(golden_saying_content)))

真实调试输出

INFO | ======= load text data to langchain============ INFO | type of golden_saying_content: <class 'list'> INFO | len of golden_saying_content: 1 INFO | type of golden_saying_conten's first element:<class 'langchain_core.documents.base.Document'>

深度代码解析

  • TextLoader(file_path):
    • 作用: 这是 LangChain 最基础的文档加载器,用于处理纯文本文件。
    • 参数:file_path指定了要读取的文件路径。
  • loader.load():
    • 作用: 执行读取操作,返回一个包含Document对象的列表。
    • 返回值:List[Document]。对于TextLoader,因为它不进行切分,所以列表里通常只有1 个Document 对象,包含了整个文件的内容。
    • Document 对象: 这是 LangChain 的核心数据结构,具有两个属性:
      • page_content: 文件的完整文本内容字符串。
      • metadata: 一个字典,默认包含{'source': '文件路径'}

步骤 3: 文本切分 (Text Splitting)

使用RecursiveCharacterTextSplitter将长文档切分为片段。

fromlangchain_text_splittersimportRecursiveCharacterTextSplitter logger.info("========== chunking =============================")text_splitter=RecursiveCharacterTextSplitter(chunk_size=1000,chunk_overlap=50,length_function=len,add_start_index=True)texts=text_splitter.split_documents(golden_saying_content)logger.info(texts[0])

真实调试输出

INFO | ========== chunking ============================= INFO | page_content='Are these the only works of Providence within us? What words suffice to\npraise or set them forth? ...' metadata={'source': 'src/examples/retrieval/docs/output_golden_hymns_of_epictetus_new.txt', 'start_index': 0}

深度代码解析

  • RecursiveCharacterTextSplitter(...): 这是处理通用文本的首选切分器。它按顺序尝试使用分隔符列表["\n\n", "\n", " ", ""]进行切分,目的是尽量保持段落、句子和单词的完整性。
    • chunk_size=1000:目标块大小。分割器会尽量让每个块的字符数接近这个值(不超过它,除非单个词太长)。设置过小会导致上下文丢失,设置过大会超出 Embedding 模型的窗口限制。
    • chunk_overlap=50:重叠量。相邻的两个块会有 50 个字符的重复内容。这非常重要,可以防止重要的关键词(如人名、概念)被切分在两个块的边界上,从而丢失上下文联系。
    • length_function=len: 用于计算长度的函数。默认是 Python 的len()(计算字符数)。如果你需要严格控制 Token 数量(如 OpenAI 的限制),可以使用token_counter函数。
    • add_start_index=True: 是否添加起始位置索引。设置为 True 后,每个 Chunk 的metadata会增加一个start_index字段,记录该片段在原文中的位置,这对调试和引用非常有用。
  • text_splitter.split_documents(documents):
    • 作用: 接收一个 Document 列表,应用上述切分规则,返回一个新的、包含更多(但更短)Document 对象的列表。

步骤 4: 向量化与存储 (Embedding & Indexing)

fromlangchain_community.vectorstoresimportFAISSfromlangchain_google_genaiimportGoogleGenerativeAIEmbeddings logger.info("========== text embedding =============================")# 1. 初始化 Embedding 模型embedding_model=GoogleGenerativeAIEmbeddings(model="models/embedding-001")# 2. 创建向量库vector_store=FAISS.from_documents(documents=texts,embedding=embedding_model)logger.info("========== embedding done =============================")

真实调试输出

INFO | ========== text embedding ============================= INFO | Checking GOOGLE_API_KEY: True INFO | ========== embedding done =============================

深度代码解析

  • GoogleGenerativeAIEmbeddings(...):
    • 作用: LangChain 提供的 Google Gemini Embedding 接口封装。
    • 参数model="models/embedding-001": 指定使用的具体模型版本。这是一个专门针对语义检索优化的模型。
    • 注意: 此类依赖GOOGLE_API_KEY环境变量。
  • FAISS.from_documents(...):
    • 这是一个便捷的工厂方法,它在后台执行了整个 Indexing 流程:
    1. Embed: 调用embedding_model.embed_documents(),将texts列表中的每个 chunk 文本转化为向量(例如 768 维的浮点数数组)。
    2. Index: 初始化一个 FAISS 索引(通常是IndexFlatL2),并将这些向量插入其中。
    3. Store: 在内存中建立一个映射,将向量 ID 映射回原始的 Document 对象(以便检索时能返回文本内容)。
    • 返回值: 一个初始化好的FAISS向量库对象。

💡 深度解析:Embedding 模型详解

在这一步,我们使用了GoogleGenerativeAIEmbeddings(基于 LLM 的 Embedding)。你可能会问,为什么不直接用简单的算法,或者直接用 Chat Model?

1. 为什么要用 LLM (如 Google/OpenAI) 做 Embedding?
  • 上下文感知 (Contextual): LLM 基于 Transformer 架构,能理解“语境”。例如,它知道“Apple”“Apple pie”(食物)和“Apple Inc”(公司)中代表完全不同的含义,并会生成不同的向量。这是传统方法(如 Word2Vec)做不到的。
  • 语义理解强: 它们不仅仅是匹配关键词,而是真正“读懂”了句子。即使两个句子没有共同的词(如“手机没电了”和“屏幕黑了”),LLM 也能识别出它们在语义上是相关的。
2. 为什么不能直接用 Chat Model (如 GPT-4, Gemini Pro) 做 Embedding?

你可能已经在用强大的 Chat Model(对话模型),为什么不能直接用它生成向量?

  • 训练目标不同
    • Chat Model (生成模型):目标是“预测下一个字”,它的强项是生成流畅、合逻辑的文本
    • Embedding Model (表示模型):目标是“将语义压缩成向量”,它的强项是计算两个句子在数学空间上的距离(相似度)。
  • 输出格式不同
    • Chat Model 的 API 通常只返回生成的文本字符串
    • Embedding Model 的 API 返回的是浮点数列表(向量)。
  • 接口限制:虽然 Chat Model 内部也有向量(Hidden States),但绝大多数商业 API(如 OpenAI, Google)都不开放这个底层数据,只开放生成的文本。

因此,我们需要专门的Embedding Model来完成向量化工作。

3. 其他 Embedding 方法对比
方法例子优点缺点
LLM APIGoogle Gemini, OpenAI效果最好,无需维护模型,语义理解最强需要付费/联网,数据隐私顾虑
本地模型HuggingFace (如 all-MiniLM)免费,离线可用,数据隐私好消耗本地计算资源 (CPU/GPU),大模型跑不动
静态词向量Word2Vec, GloVe速度极快,资源消耗低不懂上下文(无法区分多义词),效果一般
统计方法TF-IDF, One-Hot简单直观,关键词匹配精准稀疏向量,完全不懂语义(不知道“开心”和“高兴”是近义词)

总结:在 RAG 应用中,为了保证检索的准确性(尤其是基于自然语言的模糊搜索),基于 LLM 或 HuggingFace Transformer 的 Embedding 是目前的标准选择


第二部分:基于 Vector Store 的查询

步骤 1: 语义检索 (Semantic Search)

手动调用检索接口并获取分数。

str_query="how can I practice mindfulness if I am always busy and distracted"logger.info("============= query from vector store =====================")# 执行检索docs_and_scores=vector_store.similarity_search_with_score(query=str_query,k=10)# 打印检索结果fori,(doc,score)inenumerate(docs_and_scores):logger.info(f"[Source{i+1}] Score:{score:.4f}, Content:{doc.page_content[:50]}...")

真实调试输出

INFO | ============= query from vector store ===================== INFO | type of rs:<class 'list'> len of rs: 10 INFO | [Source 1] Score: 0.9082, Content: XXX You must know that it is no easy thing for a ... INFO | [Source 2] Score: 0.9529, Content: One who has had fever, even when it has left him, ... INFO | [Source 3] Score: 0.9596, Content: _I move not without Thy knowledge!_ XXIX Conside... ...

讲解

  • Score: FAISS 默认通常使用 L2 距离(欧氏距离),分数越低代表距离越近,即越相似。如果是 Cosine Similarity,则分数越高越相似。这一点在解读日志时非常重要。(注:这里的 Score 0.9082 比 0.9529 小,说明 Source 1 更相似)。
  • Content: 打印前 50 个字符可以快速确认检索到的内容是否真的跟“正念”、“忙碌”有关。

步骤 2 & 3: 构建上下文并生成回答 (Generation)

fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParserfromsrc.llm.gemini_chat_modelimportget_gemini_llm# 1. 格式化 Contextcontext_parts=[]fori,(doc,score)inenumerate(docs_and_scores):context_parts.append(f"[Source{i+1}] (Score:{score:.4f}):\n{doc.page_content}")formatted_context="\n\n".join(context_parts)# 2. 定义 Prompttemplate="""Answer the question based only on the following context. Please cite the sources you used for your answer (e.g., [Source 1], [Source 2]). Context: {context} Question: {question} """prompt=ChatPromptTemplate.from_template(template)# 3. 初始化 LLMllm=get_gemini_llm()# 4. 构建 LCEL 链chain=prompt|llm|StrOutputParser()# 5. 执行链response=chain.invoke({"context":formatted_context,"question":str_query})logger.info(f"LLM Response:\n{response}")

真实调试输出 (LLM Response)

INFO | ============= query with llm ============================= INFO | LLM Response: Based on the context provided, you can practice a form of mindfulness even when busy and distracted through the following methods: * **Reframe your perspective on your situation:** If you are alone, instead of calling it "solitude," you should call it "Tranquillity and Freedom." When in the company of many, rather than viewing it as a "wearisome crowd and tumult," consider it an "assembly and a tribunal" and accept it with contentment [Source 1]. * **Be self-sufficient and converse with yourself:** A person should be prepared to be "sufficient unto himself—to dwell with himself alone." You should be able to converse with yourself, not need others for distraction, and direct your thoughts toward the "Divine Administration" and your relation to everything else [Source 5]. * **Observe your inner state:** Take time to observe how past and present events have affected you, what things still have the power to hurt you, and how they might be cured or removed [Source 5]. * **Focus on breaking mental habits:** If you have a negative habit like anger, do not feed it or give it anything that helps it increase. A practical step is to keep quiet and count the days you are successful in avoiding the negative habit [Source 2]. * **Maintain your principles daily:** For a principle to become your own, you must maintain it each day and work it out in your life [Source 1]. * **Shift your focus:** Rather than spending all your time calculating and contriving for profit, you are encouraged to learn about the "administration of the World," your place in it, and what constitutes your own Good and Evil [Source 4]. **Sources:** * [Source 5] (Score: 0.9677) * [Source 4] (Score: 0.9674) * [Source 3] (Score: 0.9596) * [Source 2] (Score: 0.9529) * [Source 1] (Score: 0.9082)

深度代码解析

  • ChatPromptTemplate.from_template(template):
    • 作用: 从字符串模板创建一个 Chat Prompt 对象。
    • 占位符:{context}{question}是变量,后续invoke时会自动替换。这是 Prompt Engineering 的核心,我们显式地告诉 LLM “只基于 context 回答”,这是减少幻觉的关键。
  • get_gemini_llm():
    • 这是本项目自定义的辅助函数,用于初始化GeminiChatModel。它封装了读取 Config、设置 API Key 等繁琐步骤。
  • LCEL (LangChain Expression Language):
    • chain = prompt | llm | StrOutputParser()
    • |(Pipe Operator): 这是 LangChain 的特色语法,类似于 Unix 管道。数据从左向右流动:
      1. prompt: 接收输入字典,填充模板,生成PromptValue
      2. llm: 接收PromptValue,调用 Gemini API,返回AIMessage对象。
      3. StrOutputParser(): 接收AIMessage,提取其中的content文本字符串。
  • chain.invoke(...):
    • 作用: 触发整个链条的执行。
    • 输入: 一个字典{"context": ..., "question": ...},必须匹配 Prompt 中的占位符。
      可以看到 LLM 成功地:
    1. 理解了问题。
    2. 阅读了我们提供的 Context。
    3. 引用了来源([Source 1]),这证明它确实是用我们的数据在回答,而不是在瞎编。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询