https://python.langchain.com.cn/docs/expression_language/cookbook/retrieval

超简单RAG讲解:只盯1个案例,全程看数据怎么变(初学者版)

这次咱们只抓一个核心案例——“查Harrison在哪工作”,全程不跳步、不新增场景,每一步都告诉你:现在有什么数据、要做什么、做完后数据变成什么样,连函数里的参数来源、中间结果都给你打印出来看,绝对不丢数据、不模糊。

一、先明确最终目标

咱们就做一件事:让AI基于“外部文本(只有1句话:harrison worked at kensho)”回答问题,流程是:
用户问“where did harrison work?” → 从外部文本里找到相关内容 → AI用找到的内容回答。

二、完整代码+逐步数据拆解(全程跟着数据走)

下面代码是“从0到1跑通”的完整版本,每个步骤都标了“数据说明”,你复制到编辑器里,跟着注释跑,能看到每一步的中间数据。

# -------------------------- 步骤1:装工具(第一次运行要执行) --------------------------
# 作用:安装需要的库(向量库、AI模型、嵌入工具)
%pip install --upgrade --quiet langchain langchain-openai faiss-cpu tiktoken

# -------------------------- 步骤2:导入必须的工具 --------------------------
# 只导入这次要用的,不多导,避免混乱
from langchain_community.vectorstores import FAISS  # 存文本向量的库
from langchain_core.prompts import ChatPromptTemplate  # 提示模板
from langchain_core.output_parsers import StrOutputParser  # 把AI输出转成字符串
from langchain_openai import ChatOpenAI, OpenAIEmbeddings  # AI模型+文本转向量工具
from langchain_core.runnables import RunnablePassthrough  # 传递数据用的工具

# -------------------------- 步骤3:准备“外部文本”和“向量库” --------------------------
# 数据说明1:咱们只有1条外部文本,就是要让AI参考的内容
external_text = ["harrison worked at kensho"]  # 列表格式,里面是要检索的文本

# 数据说明2:初始化“文本转向量工具”——把文字变成电脑能比较的数字
embedding_tool = OpenAIEmbeddings()

# 数据说明3:把外部文本转成向量,存到“向量库”里
# 输入:external_text(要存的文本)、embedding_tool(转向量的工具)
# 输出:vectorstore(向量库,里面存着“文本向量”和对应的原文)
vectorstore = FAISS.from_texts(external_text, embedding=embedding_tool)

# 打印看看向量库有什么(初学者必看!)
print("步骤3:向量库存储的原文是:", vectorstore.similarity_search("随便问个问题")[0].page_content)
# 输出:harrison worked at kensho(证明向量库存对了)

# -------------------------- 步骤4:准备“检索器”——找相关文本用 --------------------------
# 数据说明:从向量库生成“检索器”,作用是“输入问题,返回向量库中最像的文本”
retriever = vectorstore.as_retriever()

# 先测试检索器好不好用(初学者必看!)
# 输入:问题“where did harrison work?”
# 输出:检索到的相关文本(Document对象,page_content里是原文)
retrieved_docs = retriever.invoke("where did harrison work?")
print("\n步骤4:检索器找到的文本是:", retrieved_docs[0].page_content)
# 输出:harrison worked at kensho(证明检索器能找到正确内容)

# -------------------------- 步骤5:写“合并文档函数”——把检索到的文本变干净 --------------------------
# 问题:检索器返回的是“Document对象列表”(像这样:[Document(page_content="xxx")]),AI看不懂
# 作用:把这个列表转成AI能看懂的纯字符串
def _combine_documents(docs):
    # 数据说明:docs是检索器返回的Document列表(比如步骤4里的retrieved_docs)
    print("\n步骤5:_combine_documents函数的输入(检索到的Document列表):", docs)
    
    # 提取每个Document里的“page_content”(就是原文),拼成一个字符串
    clean_text = "\n\n".join([doc.page_content for doc in docs])
    
    print("步骤5:_combine_documents函数的输出(干净的纯文本):", clean_text)
    return clean_text  # 输出:"harrison worked at kensho"

# 测试这个函数(初学者必看!)
clean_text = _combine_documents(retrieved_docs)
# 输出:步骤5的输入是[Document(...)], 输出是"harrison worked at kensho"

# -------------------------- 步骤6:准备“提示模板”——告诉AI怎么回答 --------------------------
# 作用:强制AI“只看检索到的文本(context)回答,不瞎编”
# 占位符:{context}(检索到的干净文本)、{question}(用户的问题)
prompt_template = ChatPromptTemplate.from_template("""
Answer the question based only on the following context:
{context}
Question: {question}
""")

# -------------------------- 步骤7:准备“AI模型”——生成回答用 --------------------------
ai_model = ChatOpenAI()  # 初始化AI模型(用OpenAI的模型)

# -------------------------- 步骤8:构建完整链——把所有步骤串起来 --------------------------
# 核心:数据从左到右流,每一步都告诉你“x是什么”“输出是什么”
chain = (
    # 步骤8.1:处理输入——把用户的问题,变成后续步骤需要的格式
    # 数据说明:此时“x”是用户传入的问题(比如"where did harrison work?")
    # 输出:一个字典,里面有2个键:
    # - "context":用检索器找到文本,再用_combine_documents转成干净字符串
    # - "question":用户的问题(原封不动传过来)
    lambda x: {
        "context": _combine_documents(retriever.invoke(x)),  # x是用户问题,先检索再合并
        "question": x  # 原封不动传递用户问题
    }
    # 步骤8.2:填充提示模板——把上面的字典(context+question)放进模板里
    # 数据说明:输入是步骤8.1的字典,输出是“完整提示”(比如:“Answer based on... context: harrison... question: where...”)
    | prompt_template
    # 步骤8.3:AI生成回答——用完整提示让AI写回答
    # 数据说明:输入是完整提示,输出是AI的回答(AIMessage对象)
    | ai_model
    # 步骤8.4:转成字符串——把AI的回答(AIMessage)转成普通人能读的文字
    # 数据说明:输入是AIMessage对象,输出是纯字符串(比如"Harrison worked at Kensho.")
    | StrOutputParser()
)

# -------------------------- 步骤9:测试链——看最终结果 --------------------------
# 数据说明:输入用户的问题(字符串)
user_question = "where did harrison work?"
final_answer = chain.invoke(user_question)

# 打印最终结果
print("\n步骤9:最终回答:", final_answer)
# 输出:Harrison worked at Kensho.(完美!AI用外部文本回答对了)

三、关键问题:每个参数/中间数据都给你说透

你之前看不懂的“参数传递”“中间数据”,这里逐个拆解:

1. _combine_documents函数的参数docs是什么?

  • 来源:是retriever.invoke(用户问题)的返回值,格式是“Document对象列表”,比如:
    [Document(page_content="harrison worked at kensho")]
  • 为什么要这个参数:因为检索器返回的是“带格式的对象”,AI看不懂,需要提取里面的page_content(纯文本);
  • 输出:纯字符串,比如"harrison worked at kensho"

2. 链里的lambda x: {...}是什么意思?x从哪来?

  • x的来源x就是你最后调用chain.invoke(用户问题)时传入的“用户问题”(比如"where did harrison work?");
  • 作用:把x(用户问题)转换成后续步骤需要的“字典格式”(必须有contextquestion两个键,因为提示模板里要用到这两个占位符);
  • 中间结果:比如x"where did harrison work?",这个lambda的输出是:
    {"context": "harrison worked at kensho", "question": "where did harrison work?"}

3. 整个流程的“数据变化”是怎样的?(一句话总结)

用户问题(字符串)→ 检索器找相关文本(Document列表)→ _combine_documents转成干净文本(字符串)→ 拼成完整提示(字符串)→ AI生成回答(AIMessage对象)→ 转成纯字符串(最终回答)。

四、为什么这次你能看懂?

  1. 只盯1个案例:全程就查“Harrison在哪工作”,不换场景,你不用记多个案例的逻辑;
  2. 每个步骤都打印中间数据:步骤3、4、5、9都打印了数据,你能亲眼看到“输入→输出”的变化,不是凭空想象;
  3. 函数参数说透来源:不模糊说“参数是文档列表”,而是告诉你“参数来自检索器的返回值,格式是XXX”,连例子都给你;
  4. 代码不分散:从安装工具到最终测试,所有代码连在一起,你复制就能跑,不用前后翻找。

结尾:帮你做“可交互的中间数据查看版”

要不要我帮你整理一份“带实时打印中间数据的代码”?在每个关键步骤都加print,你跑的时候能看到“数据从用户问题变成context,再变成提示,最后变成回答”的全过程,就像“看着数据一步步走”,彻底不用猜中间是什么。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐