当前位置: 首页 > article >正文

实践指南:从零开始搭建RAG驱动的智能问答系统

LLM 赋能的最强大的应用之一是复杂的问答 (Q&A) 聊天机器人。这些是可以回答关于特定来源信息问题的应用程序。这些应用程序使用一种称为检索增强生成的技术,或 RAG。本文将展示如何基于 LangChain 构建一个简单的基于非结构化数据文本数据源的问答应用程序。

温馨提示:本文搭配 Jupyter notebooks 食用更佳,在交互式环境中学习是更好地理解它们的好方法。

一、RAG 概述

RAG(Retrieval-Augmented Generation,检索增强生成)是一种通过结合外部知识检索与大型语言模型(LLM)生成能力的技术框架,旨在提升模型回答的准确性和时效性,解决传统模型的知识局限与“幻觉”问题。

RAG 系统包含三个核心模块:

  1. 索引(Indexing) ‌:

    • 将文档分块、向量化并存储至向量数据库(如FAISS、Milvus);
    • 优化策略包括数据粒度调整、元数据添加及混合检索(稠密检索+稀疏检索)。
  2. 检索(Retrieval) ‌:

    • 根据用户查询语义,从数据库召回最相关的文档片段(Top-k);
    • 预检索阶段通过查询重写、扩展等技术优化意图理解。
  3. 生成(Generation) ‌:

    • 将检索结果与原始查询组合为提示词(Prompt),输入LLM(如GPT-4、LLaMA-3)生成最终答案;
    • 后处理可能包含重排序(Reranking)或上下文压缩以减少噪声干扰。

二、RAG 实现流程

典型的 RAG 应用程序具有两个主要组件:

  • 索引:一个用于从源摄取数据并对其进行索引的管道。这通常离线发生。
  • 检索和生成:实际的 RAG 链,它在运行时获取用户查询,并从索引中检索相关数据,然后将其传递给模型。

Step1、索引

  1. 加载:首先我们需要加载数据。这通过 文档加载器 完成。
  2. 分割:文本分割器将大型 Documents 分解为更小的块。这对于索引数据和将其传递到模型中都很有用,因为大块数据更难搜索,并且无法容纳在模型的有限上下文窗口中。
  3. 存储:我们需要某个地方来存储和索引这些分割块,以便稍后可以对其进行搜索。这通常使用 向量存储嵌入模型 完成。

在这里插入图片描述

Step2、检索和生成

  1. 检索:给定用户输入,使用 检索器 从存储中检索相关分割块。
  2. 生成聊天模型/LLM 使用包含问题和检索数据的提示生成答案

在这里插入图片描述

一旦我们索引了数据,我们将使用 LangGraph 作为工作流编排框架来实现检索和生成步骤。

三、依赖项与组件

安装langchain依赖项:

pip install --quiet --upgrade langchain-text-splitters langchain-community langgraph

安装 LLM 依赖:

pip install -qU "langchain[openai]"

这里使用硅基流动平台的大模型服务,Qwen3-8B

from pydantic import SecretStr
import osos.environ["OPENAI_BASE_URL"] = "https://api.siliconflow.cn/v1/"
os.environ["OPENAI_API_KEY"] = "sk-xxx"from langchain.chat_models import init_chat_modelllm = init_chat_model("Qwen/Qwen3-8B", model_provider="openai")

安装 Embeddings 依赖:

pip install -qU langchain-openai

这里嵌入模型使用 BAAI/bge-large-zh-v1.5

from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="BAAI/bge-large-zh-v1.5")

安装 VectorStore 依赖:

pip install -qU langchain-core

这里使用内存作为向量存储,也可以使用轻量级向量数据库 Chroma

from langchain_core.vectorstores import InMemoryVectorStore
vector_store = InMemoryVectorStore(embeddings)# from langchain_chroma import Chroma# vector_store = Chroma(
#     collection_name="example_collection",
#     embedding_function=embeddings,
#     persist_directory="./chroma_langchain_db",  # Where to save data locally, remove if not necessary
# )

四、RAG 应用整体效果预览

创建一个简单的索引管道和 RAG 链,从古诗词网站中解析所需文本,根据文本相关内容进行提问

在这里插入图片描述

import bs4
from langchain import hub
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.graph import START, StateGraph
from typing_extensions import List, TypedDict# Load and chunk contents of the blog
loader = WebBaseLoader(web_paths=("https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx",),bs_kwargs=dict(parse_only=bs4.SoupStrainer(class_=("sons", "contyishang", "sonspic"))),
)
docs = loader.load()# 'ChunkSize' 1000 控制最终文档的最大大小(以字符数为单位)。'ChunkOverlap' 200 指定文档之间应该有多少重叠。
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
all_splits = text_splitter.split_documents(docs)# Index chunks
_ = vector_store.add_documents(documents=all_splits)# Define prompt for question-answering
prompt = hub.pull("rlm/rag-prompt")# Define state for application
class State(TypedDict):question: strcontext: List[Document]answer: str# Define application steps
def retrieve(state: State):retrieved_docs = vector_store.similarity_search(state["question"])return {"context": retrieved_docs}def generate(state: State):docs_content = "\n\n".join(doc.page_content for doc in state["context"])messages = prompt.invoke({"question": state["question"], "context": docs_content})response = llm.invoke(messages)return {"answer": response.content}# Compile application and test
graph_builder = StateGraph(State).add_sequence([retrieve, generate])
graph_builder.add_edge(START, "retrieve")
graph = graph_builder.compile()

若遇到报错: APIStatusError: Error code: 413 - {'code': 20042, 'message': 'input batch size 132 > maximum allowed batch size 32', 'data': None} ,这是因为文本块切割过多导致的,需要降低块数,解决参考 https://js.langchain.com.cn/docs/modules/indexes/text_splitters/examples/recursive_character

response = graph.invoke({"question": "创作背景"})
print(response["answer"])
《茅屋为秋风所破歌》创作于唐肃宗上元二年(761年)八月,杜甫在成都浣花溪畔的茅屋被秋风吹破、大雨侵袭之际所作。当时正值安史之乱未平,诗人因自身困境联想到天下寒士的苦难,抒发了忧国忧民的情怀。此诗通过个人遭遇折射时代动荡,体现了杜甫“关心人民疾苦”的现实主义精神。

五、步骤拆解

接下来我们逐步拆解分析以上示例是怎么实现的。

1. 索引

1.1加载文档

import bs4
from langchain_community.document_loaders import WebBaseLoader# Only keep post title, headers, and content from the full HTML.
bs4_strainer = bs4.SoupStrainer(class_=("sons", "contyishang", "sonspic"))
loader = WebBaseLoader(web_paths=("https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx",),bs_kwargs={"parse_only": bs4_strainer},
)
docs = loader.load()assert len(docs) == 1
print(f"Total characters: {len(docs[0].page_content)}")
Total characters: 1885

在本例中,我们使用 WebBaseLoader,它使用 urllibWeb URL 加载 HTML,并使用 BeautifulSoup 将其解析为文本。我们可以通过将参数传递到 BeautifulSoup 解析器(通过 bs_kwargs)来自定义 HTML -> 文本解析。在本例中,只有类为“sons”“contyishang”“sonspic”HTML 标签是相关的,因此我们将删除所有其他标签。

print(docs[0].page_content[:500])
茅屋为秋风所破歌杜甫〔唐代〕八月秋高风怒号,卷我屋上三重茅。茅飞渡江洒江郊,高者挂罥长林梢,下者飘转沉塘坳。南村群童欺我老无力,忍能对面为盗贼。公然抱茅入竹去,唇焦口燥呼不得,归来倚杖自叹息。俄顷风定云墨色,秋天漠漠向昏黑。布衾多年冷似铁,娇儿恶卧踏里裂。床头屋漏无干处,雨脚如麻未断绝。自经丧乱少睡眠,长夜沾湿何由彻!安得广厦千万间,大庇天下寒士俱欢颜!风雨不动安如山。呜呼!何时眼前突兀见此屋,吾庐独破受冻死亦足!(亦足 一作:意足)译文及注释译文八月秋深狂风大声吼叫,狂风卷走了我屋顶上好几层茅草。茅草乱飞渡过浣花溪散落在对岸江边,飞得高的茅草缠绕在高高的树梢上,飞得低的飘飘洒洒沉落到低洼的水塘里。南村的一群儿童欺负我年老没力气,竟狠心这样当面做“贼”抢东西,明目张胆地抱着茅草跑进竹林里去了。我费尽口舌也喝止不住,回到家后拄着拐杖独自叹息。不久后风停了天空上的云像墨一样黑,秋季的天空阴沉迷蒙渐渐黑了下来。布质的被子盖了多年又冷又硬像铁板似的,孩子睡相不好把被子蹬破了。如遇下雨整个屋子没有一点儿干燥的地方,雨

1.2分割文档

from langchain_text_splitters import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(chunk_size=200,  # chunk size (characters)chunk_overlap=50,  # chunk overlap (characters)add_start_index=True,  # track index in original document
)
all_splits = text_splitter.split_documents(docs)print(f"Split blog post into {len(all_splits)} sub-documents.")
Split blog post into 21 sub-documents.

我们加载的文档有时候会文本过长,无法容纳到许多模型的上下文窗口中。即使对于那些可以容纳完整帖子在其上下文窗口中的模型,模型也可能难以在非常长的输入中找到信息。为了处理这个问题,我们将把 Document 分割成块,以便进行嵌入和向量存储。这应该有助于我们在运行时仅检索博客文章的最相关部分。

1.3存储文档

document_ids = vector_store.add_documents(documents=all_splits)print(document_ids[:3])
['ab3ec6c0-3583-4525-b86c-b08dbf2077e4', '25cf74fd-5cfa-4940-99ba-c0a4e85ff344', '0383b722-5723-433c-a776-f17b4ae1de8f']

2.检索和生成

现在让我们编写实际的应用程序逻辑。我们想要创建一个简单的应用程序,它接受用户问题,搜索与该问题相关的文档,将检索到的文档和原始问题传递给模型,并返回答案。

from langchain import hubprompt = hub.pull("rlm/rag-prompt")example_messages = prompt.invoke({"context": "(context goes here)", "question": "(question goes here)"}
).to_messages()assert len(example_messages) == 1
print(example_messages[0].content)
You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Question: (question goes here) 
Context: (context goes here) 
Answer:

我们将使用 LangGraph 将检索和生成步骤绑定到一个应用程序中。这将带来许多好处:

  • 我们可以定义一次应用程序逻辑,并自动支持多种调用模式,包括流式传输、异步和批量调用。
  • 我们通过 LangGraph 平台获得简化的部署。
  • 我们可以轻松地向我们的应用程序添加关键功能,包括 持久性人工参与审批,只需进行最少的代码更改。

要使用 LangGraph,我们需要定义三件事:

  • 我们应用程序的状态;
  • 我们应用程序的节点(即,应用程序步骤);
  • 我们应用程序的“控制流”(例如,步骤的排序)。

2.1状态

from langchain_core.documents import Document
from typing_extensions import List, TypedDictclass State(TypedDict):question: strcontext: List[Document]answer: str

我们应用程序的 状态 控制着哪些数据输入到应用程序、在步骤之间传输以及由应用程序输出。它通常是 TypedDict,但也可以是 Pydantic BaseModel。对于简单的 RAG 应用程序,我们可以只跟踪输入问题、检索到的上下文和生成的答案。

2.2节点(应用程序步骤)

def retrieve(state: State):retrieved_docs = vector_store.similarity_search(state["question"])return {"context": retrieved_docs}def generate(state: State):docs_content = "\n\n".join(doc.page_content for doc in state["context"])messages = prompt.invoke({"question": state["question"], "context": docs_content})response = llm.invoke(messages)return {"answer": response.content}

让我们从两个步骤的简单序列开始:检索和生成。我们的检索步骤只是使用输入问题运行相似性搜索,而生成步骤将检索到的上下文和原始问题格式化为聊天模型的提示。

2.3控制流

from langgraph.graph import START, StateGraphgraph_builder = StateGraph(State).add_sequence([retrieve, generate])
graph_builder.add_edge(START, "retrieve")
graph = graph_builder.compile()

最后,我们将我们的应用程序编译成单个 graph 对象。在本例中,我们只是将检索和生成步骤连接成一个序列。

# LangGraph 还附带内置实用程序,用于可视化您应用程序的控制流
from IPython.display import Image, displaydisplay(Image(graph.get_graph().draw_mermaid_png()))

在这里插入图片描述

构建 RAG 应用程序不需要 LangGraph,只是使用 LangGraph 有一些优势。实际上,我们可以通过调用各个组件来实现相同的应用程序逻辑:

question = "创作背景"retrieved_docs = vector_store.similarity_search(question)
docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)
prompt = prompt.invoke({"question": question, "context": docs_content})
answer = llm.invoke(prompt)

3.用法

LangGraph 支持多种调用模式,包括同步、异步和流式传输。

3.1同步/异步调用

# 同步调用
result = graph.invoke({"question": "创作背景"})print(f'Context: {result["context"]}\n\n')
print(f'Answer: {result["answer"]}')
Context: [Document(id='505afa2e-658a-4aac-a6a7-e74d936b38ab', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='茅屋为秋风所破歌\n\n\n杜甫〔唐代〕\n\n八月秋高风怒号,卷我屋上三重茅。茅飞渡江洒江郊,高者挂罥长林梢,下者飘转沉塘坳。南村群童欺我老无力,忍能对面为盗贼。公然抱茅入竹去,唇焦口燥呼不得,归来倚杖自叹息。俄顷风定云墨色,秋天漠漠向昏黑。布衾多年冷似铁,娇儿恶卧踏里裂。床头屋漏无干处,雨脚如麻未断绝。自经丧乱少睡眠,长夜沾湿何由彻!安得广厦千万间,大庇天下寒士俱欢颜!风雨不动安如山。呜呼!何时眼前突兀见此屋,吾庐独破受冻死亦足!(亦足 一作:意足)\n\n\n\n\n\n\n\n\n完善\n\n\n\n\n\n\n译文及注释'), Document(id='cadc99b2-ad1d-4c44-8f59-4be376c61eb4', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='完善\n\n\n\n\n\n\n译文及注释\n\n\n译文八月秋深狂风大声吼叫,狂风卷走了我屋顶上好几层茅草。茅草乱飞渡过浣花溪散落在对岸江边,飞得高的茅草缠绕在高高的树梢上,飞得低的飘飘洒洒沉落到低洼的水塘里。南村的一群儿童欺负我年老没力气,竟狠心这样当面做“贼”抢东西,明目张胆地抱着茅草跑进竹林里去了。我费尽口舌也喝止不住,回到家后拄着拐杖独自叹息。不久后风停了天空上的云像墨一样黑,秋季的天空阴沉迷蒙渐渐黑了下来。布质的被子盖了多年又冷又硬像铁板似的,孩子睡相不好把被子蹬破了。如遇下雨整个屋子没有一点儿干燥的地方,雨点像下垂的麻线一样不停地往下漏。自从安史之乱后我的睡眠时间就很少了,长夜漫漫屋子潮湿不\n展开阅读全文 ∨\n\n\n\n\n创作背景\n\n\n\u3000\u3000这首诗作于唐肃宗上元二年(公元761年)八月。公元760年春天,杜甫求亲告友,在成都浣花溪边盖起了一座茅屋,总算有了一个栖身之所。不料到了公元761年八月,大风破屋,大雨又接踵而至。当时安史之乱尚未平息,诗人感慨万千,写下了这篇脍炙人口的诗篇。 \n\n\n参考资料:完善'), Document(id='bac34791-2ca9-431c-8823-8befad3a7c04', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='参考资料:完善\n\n1、\n于海娣 等 .唐诗鉴赏大全集 .北京 :中国华侨出版社 ,2010 :181 .\n\n\n2、\n李静 等 .唐诗宋词鉴赏大全集 .北京 :华文出版社 ,2009 :118-119 .\n\n\n\n\n\n\n鉴赏\n\n\n\u3000\u3000这首诗可分为四节。\u3000\u3000第一段中共有五句,句句押韵,“号”、“茅”、“郊”、“梢”、“坳”五个开口呼的平声韵脚传来阵阵风声。\u3000\u3000“八月秋高风怒号,卷我屋上三重茅。”起势迅猛。“风怒号”三字,音响宏大,犹如秋风咆哮。一个“怒”字,把秋风拟人化,从而使下一句不仅富有动作性,而且富有浓烈的感情色彩——诗人好不容易盖了这座茅屋,刚刚定居下来,秋风却怒吼而来,卷起层层茅草,使得诗人焦急万分。\u3000\u3000“茅飞渡江洒江郊”的“飞”字紧承上句的“卷”字,“卷”起的茅草没有落在屋旁,却随风“飞”走,“飞”过江去,然后分散地、雨点似地“洒”在“江郊”:“高者挂罥长林梢”\n展开阅读全文 ∨\n\n\n\n\n简析'), Document(id='01b0b547-10a5-4ee9-b1fb-0f014559f32a', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='简析\n\n\n\u3000\u3000《茅屋为秋风所破歌》是一首歌行体古诗。此诗叙述了诗人的茅屋被秋风所破以致全家遭雨淋的痛苦经历,他从中生出万千感慨,体现了忧国忧民的思想情感,是杜诗中的典范之作。这首诗叙事与抒情并重,叙事部分大篇幅写实,诗人诉述自家之苦,情绪含蓄压抑;而后的抒情部分将苦难加以升华,直抒忧民之情,情绪激越轩昂,展现出诗人的崇高理想和境界。叙事部分的有力铺垫为全诗最后的言志奠定了坚实的基础,如此抑扬曲折的情绪变换,充分体现出杜诗“沉郁顿挫”的风格。\n\n\n\n\n\n\n\n\n杜甫\n\n\n\n杜甫(712-770),字子美,自号少陵野老,世称“杜工部”、“杜少陵”等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为“诗圣”,其诗被称为“诗史”。杜甫与李白合称“李杜”,为了跟另外两位诗人李商隐与杜牧即“小李杜”区别开来,杜甫与李白又合称“大李杜”。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念。► 1338篇诗文\u3000► 2728条名句\n\n\n\n\n\n\n完善')]Answer: 《茅屋为秋风所破歌》创作于唐肃宗上元二年(761年)八月,杜甫在成都浣花溪畔的茅屋被秋风吹破后,又遇大雨,生活困顿。当时安史之乱尚未平息,诗人因自身遭遇联想到百姓疾苦,抒发了忧国忧民的情怀。此诗反映了战乱年代的民生艰难与诗人匡世济民的理想。
# 异步调用
result = await graph.ainvoke(...)
# 或 
async for step in graph.astream(...):

3.2流式传输步骤

for step in graph.stream({"question": "这首古诗的创作背景?"}, stream_mode="updates"
):print(f"{step}\n\n----------------\n")
{'retrieve': {'context': [Document(id='505afa2e-658a-4aac-a6a7-e74d936b38ab', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='茅屋为秋风所破歌\n\n\n杜甫〔唐代〕\n\n八月秋高风怒号,卷我屋上三重茅。茅飞渡江洒江郊,高者挂罥长林梢,下者飘转沉塘坳。南村群童欺我老无力,忍能对面为盗贼。公然抱茅入竹去,唇焦口燥呼不得,归来倚杖自叹息。俄顷风定云墨色,秋天漠漠向昏黑。布衾多年冷似铁,娇儿恶卧踏里裂。床头屋漏无干处,雨脚如麻未断绝。自经丧乱少睡眠,长夜沾湿何由彻!安得广厦千万间,大庇天下寒士俱欢颜!风雨不动安如山。呜呼!何时眼前突兀见此屋,吾庐独破受冻死亦足!(亦足 一作:意足)\n\n\n\n\n\n\n\n\n完善\n\n\n\n\n\n\n译文及注释'), Document(id='cadc99b2-ad1d-4c44-8f59-4be376c61eb4', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='完善\n\n\n\n\n\n\n译文及注释\n\n\n译文八月秋深狂风大声吼叫,狂风卷走了我屋顶上好几层茅草。茅草乱飞渡过浣花溪散落在对岸江边,飞得高的茅草缠绕在高高的树梢上,飞得低的飘飘洒洒沉落到低洼的水塘里。南村的一群儿童欺负我年老没力气,竟狠心这样当面做“贼”抢东西,明目张胆地抱着茅草跑进竹林里去了。我费尽口舌也喝止不住,回到家后拄着拐杖独自叹息。不久后风停了天空上的云像墨一样黑,秋季的天空阴沉迷蒙渐渐黑了下来。布质的被子盖了多年又冷又硬像铁板似的,孩子睡相不好把被子蹬破了。如遇下雨整个屋子没有一点儿干燥的地方,雨点像下垂的麻线一样不停地往下漏。自从安史之乱后我的睡眠时间就很少了,长夜漫漫屋子潮湿不\n展开阅读全文 ∨\n\n\n\n\n创作背景\n\n\n\u3000\u3000这首诗作于唐肃宗上元二年(公元761年)八月。公元760年春天,杜甫求亲告友,在成都浣花溪边盖起了一座茅屋,总算有了一个栖身之所。不料到了公元761年八月,大风破屋,大雨又接踵而至。当时安史之乱尚未平息,诗人感慨万千,写下了这篇脍炙人口的诗篇。 \n\n\n参考资料:完善'), Document(id='bac34791-2ca9-431c-8823-8befad3a7c04', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='参考资料:完善\n\n1、\n于海娣 等 .唐诗鉴赏大全集 .北京 :中国华侨出版社 ,2010 :181 .\n\n\n2、\n李静 等 .唐诗宋词鉴赏大全集 .北京 :华文出版社 ,2009 :118-119 .\n\n\n\n\n\n\n鉴赏\n\n\n\u3000\u3000这首诗可分为四节。\u3000\u3000第一段中共有五句,句句押韵,“号”、“茅”、“郊”、“梢”、“坳”五个开口呼的平声韵脚传来阵阵风声。\u3000\u3000“八月秋高风怒号,卷我屋上三重茅。”起势迅猛。“风怒号”三字,音响宏大,犹如秋风咆哮。一个“怒”字,把秋风拟人化,从而使下一句不仅富有动作性,而且富有浓烈的感情色彩——诗人好不容易盖了这座茅屋,刚刚定居下来,秋风却怒吼而来,卷起层层茅草,使得诗人焦急万分。\u3000\u3000“茅飞渡江洒江郊”的“飞”字紧承上句的“卷”字,“卷”起的茅草没有落在屋旁,却随风“飞”走,“飞”过江去,然后分散地、雨点似地“洒”在“江郊”:“高者挂罥长林梢”\n展开阅读全文 ∨\n\n\n\n\n简析'), Document(id='01b0b547-10a5-4ee9-b1fb-0f014559f32a', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}, page_content='简析\n\n\n\u3000\u3000《茅屋为秋风所破歌》是一首歌行体古诗。此诗叙述了诗人的茅屋被秋风所破以致全家遭雨淋的痛苦经历,他从中生出万千感慨,体现了忧国忧民的思想情感,是杜诗中的典范之作。这首诗叙事与抒情并重,叙事部分大篇幅写实,诗人诉述自家之苦,情绪含蓄压抑;而后的抒情部分将苦难加以升华,直抒忧民之情,情绪激越轩昂,展现出诗人的崇高理想和境界。叙事部分的有力铺垫为全诗最后的言志奠定了坚实的基础,如此抑扬曲折的情绪变换,充分体现出杜诗“沉郁顿挫”的风格。\n\n\n\n\n\n\n\n\n杜甫\n\n\n\n杜甫(712-770),字子美,自号少陵野老,世称“杜工部”、“杜少陵”等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为“诗圣”,其诗被称为“诗史”。杜甫与李白合称“李杜”,为了跟另外两位诗人李商隐与杜牧即“小李杜”区别开来,杜甫与李白又合称“大李杜”。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念。► 1338篇诗文\u3000► 2728条名句\n\n\n\n\n\n\n完善')]}}----------------{'generate': {'answer': '\n\n《茅屋为秋风所破歌》创作于唐肃宗上元二年(761年)八月,杜甫在成都浣花溪畔的茅屋被秋风摧毁,随后遭遇暴雨,生活困顿。当时安史之乱尚未平息,诗人因个人际遇联想到天下寒士的苦难,抒发了忧国忧民的情怀。此诗反映了战乱背景下民生疾苦与诗人的高尚情怀。'}}----------------

3.3流式传输 TOKEN

for message, metadata in graph.stream({"question": "表达作者什么情感?"}, stream_mode="messages"
):print(message.content, end="|")
|作者|表达了|深切|的|忧|国|忧|民|情感|。|诗|中|通过|自身|茅|屋|被|破|的|困境|,|升华|出|对|百姓|疾|苦|的|关怀|与|对|国家|命运|的|牵挂|,|体现了|杜|甫|“|沉|郁|顿|挫|”的|创|作风|格|。|全|诗|情感|从|压抑|到|激|昂|的|转折|,|展现了|诗人|高尚|的人|格|理想|与|博|大|胸怀|。||

4.自定义提示

如上所示,我们可以从提示中心加载提示(例如,此 RAG 提示)。提示也可以轻松自定义。

from langchain_core.prompts import PromptTemplatetemplate = """Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
Use three sentences maximum and keep the answer as concise as possible.
Always say "thanks for asking!" at the end of the answer.{context}Question: {question}Helpful Answer:"""
custom_rag_prompt = PromptTemplate.from_template(template)

六、查询分析

到目前为止,我们正在使用原始输入查询执行检索。但是,允许模型生成用于检索目的的查询有一些优势。例如:

  • 除了语义搜索之外,我们还可以构建结构化过滤器(例如,“查找自 2020 年以来的文档。”);
  • 模型可以将用户查询(可能是多方面的或包含不相关的语言)重写为更有效的搜索查询。

查询分析 使用模型从原始用户输入转换或构建优化的搜索查询。我们可以轻松地将查询分析步骤合并到我们的应用程序中。为了说明目的,让我们向向量存储中的文档添加一些元数据。我们将向文档添加一些(人为的)部分,我们稍后可以在其上进行过滤。

total_documents = len(all_splits)
# 地板除,向下取整
third = total_documents // 3for i, document in enumerate(all_splits):if i < third:document.metadata["section"] = "beginning"elif i < 2 * third:document.metadata["section"] = "middle"else:document.metadata["section"] = "end"all_splits[0].metadata
{'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx','section': 'beginning'}

我们将需要更新向量存储中的文档。我们将为此使用简单的 InMemoryVectorStore,因为我们将使用它的一些特定功能(即,元数据过滤)。

from langchain_core.vectorstores import InMemoryVectorStorevector_store = InMemoryVectorStore(embeddings)
_ = vector_store.add_documents(all_splits)

接下来,让我们为搜索查询定义一个模式。我们将为此目的使用结构化输出。在这里,我们将查询定义为包含一个字符串查询和一个文档部分(“开头”、“中间”或“结尾”)。

from typing import Literalfrom typing_extensions import Annotatedclass Search(TypedDict):"""Search query."""query: Annotated[str, ..., "Search query to run."]section: Annotated[Literal["beginning", "middle", "end"],...,"Section to query.",]

最后,我们向 LangGraph 应用程序添加一个步骤,以从用户的原始输入生成查询

class State(TypedDict):question: strquery: Searchcontext: List[Document]answer: strdef analyze_query(state: State):structured_llm = llm.with_structured_output(Search)query = structured_llm.invoke(state["question"])return {"query": query}def retrieve(state: State):query = state["query"]retrieved_docs = vector_store.similarity_search(query["query"],filter=lambda doc: doc.metadata.get("section") == query["section"],)return {"context": retrieved_docs}def generate(state: State):docs_content = "\n\n".join(doc.page_content for doc in state["context"])messages = prompt.invoke({"question": state["question"], "context": docs_content})response = llm.invoke(messages)return {"answer": response.content}graph_builder = StateGraph(State).add_sequence([analyze_query, retrieve, generate])
graph_builder.add_edge(START, "analyze_query")
graph = graph_builder.compile()
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))

在这里插入图片描述

我们可以通过专门要求从文章末尾获取上下文来测试我们的实现。请注意,模型在其答案中包含不同的信息。

for step in graph.stream({"question": "文章结尾表达了什么内容?"},stream_mode="updates",
):print(f"{step}\n\n----------------\n")
{'analyze_query': {'query': {'query': '文章结尾表达了什么内容?', 'section': 'end'}}}----------------{'retrieve': {'context': [Document(id='174ce77a-e941-478d-a0fa-9469b43cd74b', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx', 'section': 'end'}, page_content='参考资料:完善\n\n1、\n于海娣 等 .唐诗鉴赏大全集 .北京 :中国华侨出版社 ,2010 :181 .\n\n\n2、\n李静 等 .唐诗宋词鉴赏大全集 .北京 :华文出版社 ,2009 :118-119 .\n\n\n\n\n\n\n鉴赏\n\n\n\u3000\u3000这首诗可分为四节。\u3000\u3000第一段中共有五句,句句押韵,“号”、“茅”、“郊”、“梢”、“坳”五个开口呼的平声韵脚传来阵阵风声。\u3000\u3000“八月秋高风怒号,卷我屋上三重茅。”起势迅猛。“风怒号”三字,音响宏大,犹如秋风咆哮。一个“怒”字,把秋风拟人化,从而使下一句不仅富有动作性,而且富有浓烈的感情色彩——诗人好不容易盖了这座茅屋,刚刚定居下来,秋风却怒吼而来,卷起层层茅草,使得诗人焦急万分。\u3000\u3000“茅飞渡江洒江郊”的“飞”字紧承上句的“卷”字,“卷”起的茅草没有落在屋旁,却随风“飞”走,“飞”过江去,然后分散地、雨点似地“洒”在“江郊”:“高者挂罥长林梢”\n展开阅读全文 ∨\n\n\n\n\n简析'), Document(id='127bac17-a9fc-43e0-b399-1b9053191765', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx', 'section': 'end'}, page_content='简析\n\n\n\u3000\u3000《茅屋为秋风所破歌》是一首歌行体古诗。此诗叙述了诗人的茅屋被秋风所破以致全家遭雨淋的痛苦经历,他从中生出万千感慨,体现了忧国忧民的思想情感,是杜诗中的典范之作。这首诗叙事与抒情并重,叙事部分大篇幅写实,诗人诉述自家之苦,情绪含蓄压抑;而后的抒情部分将苦难加以升华,直抒忧民之情,情绪激越轩昂,展现出诗人的崇高理想和境界。叙事部分的有力铺垫为全诗最后的言志奠定了坚实的基础,如此抑扬曲折的情绪变换,充分体现出杜诗“沉郁顿挫”的风格。\n\n\n\n\n\n\n\n\n杜甫\n\n\n\n杜甫(712-770),字子美,自号少陵野老,世称“杜工部”、“杜少陵”等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为“诗圣”,其诗被称为“诗史”。杜甫与李白合称“李杜”,为了跟另外两位诗人李商隐与杜牧即“小李杜”区别开来,杜甫与李白又合称“大李杜”。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念。► 1338篇诗文\u3000► 2728条名句\n\n\n\n\n\n\n完善'), Document(id='d092f4ab-2c6f-44af-a5c3-4959a0ed09ed', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx', 'section': 'end'}, page_content='完善\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n望江南·梳洗罢\n\n\n温庭筠〔唐代〕\n\n梳洗罢,独倚望江楼。过尽千帆皆不是,斜晖脉脉水悠悠。肠断白蘋洲。\n\n\n\n\n\n\n\n\n完善\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n宿五松山下荀媪家\n\n\n李白〔唐代〕\n\n我宿五松下,寂寥无所欢。田家秋作苦,邻女夜舂寒。跪进雕胡饭,月光明素盘。令人惭漂母,三谢不能餐。\n\n\n\n\n\n\n\n\n完善\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n赠药山高僧惟俨二首\n\n\n李翱〔唐代〕\n\n练得身形似鹤形,千株松下两函经。我来问道无馀说,云在青霄水在瓶。(无馀 一作:无余;青霄 一作:青天)选得幽居惬野情,终年无送亦无迎。有时直上孤峰顶,月下披云啸一声。\n\n\n\n\n\n\n\n\n完善')]}}----------------{'generate': {'answer': '\n\n文章结尾表达了杜甫深切的忧国忧民情怀,由个人苦难升华至对天下寒士的关怀,体现其“安得广厦千万间,大庇天下寒士俱欢颜”的崇高理想。'}}----------------

完整示例代码如下:

from typing import Literalimport bs4
from langchain import hub
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.graph import START, StateGraph
from typing_extensions import Annotated, List, TypedDict# Load and chunk contents of the blog
loader = WebBaseLoader(web_paths=("https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx",),bs_kwargs=dict(parse_only=bs4.SoupStrainer(class_=("sons", "contyishang", "sonspic"))),
)
docs = loader.load()text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
all_splits = text_splitter.split_documents(docs)# Update metadata (illustration purposes)
total_documents = len(all_splits)
third = total_documents // 3for i, document in enumerate(all_splits):if i < third:document.metadata["section"] = "beginning"elif i < 2 * third:document.metadata["section"] = "middle"else:document.metadata["section"] = "end"# Index chunks
vector_store = InMemoryVectorStore(embeddings)
_ = vector_store.add_documents(all_splits)# Define schema for search
class Search(TypedDict):"""Search query."""query: Annotated[str, ..., "Search query to run."]section: Annotated[Literal["beginning", "middle", "end"],...,"Section to query.",]# Define prompt for question-answering
prompt = hub.pull("rlm/rag-prompt")# Define state for application
class State(TypedDict):question: strquery: Searchcontext: List[Document]answer: strdef analyze_query(state: State):structured_llm = llm.with_structured_output(Search)query = structured_llm.invoke(state["question"])return {"query": query}def retrieve(state: State):query = state["query"]retrieved_docs = vector_store.similarity_search(query["query"],filter=lambda doc: doc.metadata.get("section") == query["section"],)return {"context": retrieved_docs}def generate(state: State):docs_content = "\n\n".join(doc.page_content for doc in state["context"])messages = prompt.invoke({"question": state["question"], "context": docs_content})response = llm.invoke(messages)return {"answer": response.content}graph_builder = StateGraph(State).add_sequence([analyze_query, retrieve, generate])
graph_builder.add_edge(START, "analyze_query")
graph = graph_builder.compile()
result = graph.invoke({"question": "文章中间介绍了什么?"})print(f'Context: {result["context"]}\n\n')
print(f'Answer: {result["answer"]}')
Context: [Document(id='ff08dce6-c55a-4fcc-958d-6dd157afa3be', metadata={'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx', 'section': 'middle'}, page_content='完善\n\n\n\n\n\n\n译文及注释\n\n\n译文八月秋深狂风大声吼叫,狂风卷走了我屋顶上好几层茅草。茅草乱飞渡过浣花溪散落在对岸江边,飞得高的茅草缠绕在高高的树梢上,飞得低的飘飘洒洒沉落到低洼的水塘里。南村的一群儿童欺负我年老没力气,竟狠心这样当面做“贼”抢东西,明目张胆地抱着茅草跑进竹林里去了。我费尽口舌也喝止不住,回到家后拄着拐杖独自叹息。不久后风停了天空上的云像墨一样黑,秋季的天空阴沉迷蒙渐渐黑了下来。布质的被子盖了多年又冷又硬像铁板似的,孩子睡相不好把被子蹬破了。如遇下雨整个屋子没有一点儿干燥的地方,雨点像下垂的麻线一样不停地往下漏。自从安史之乱后我的睡眠时间就很少了,长夜漫漫屋子潮湿不\n展开阅读全文 ∨\n\n\n\n\n创作背景\n\n\n\u3000\u3000这首诗作于唐肃宗上元二年(公元761年)八月。公元760年春天,杜甫求亲告友,在成都浣花溪边盖起了一座茅屋,总算有了一个栖身之所。不料到了公元761年八月,大风破屋,大雨又接踵而至。当时安史之乱尚未平息,诗人感慨万千,写下了这篇脍炙人口的诗篇。 \n\n\n参考资料:完善')]Answer: 文章中间介绍了秋风破屋、茅草被吹散的情景,以及诗人因年老体弱无力阻止孩童抢夺茅草的无奈。随后描写风雨交加的困苦处境,如被子湿冷、屋漏雨急等细节。这些内容展现了诗人对生活艰难的感叹与对社会现实的感慨。

七、适应对话式交互和多步骤检索过程

在许多问答应用中,我们希望允许用户进行来回对话,这意味着应用程序需要某种形式的“记忆”来记住过去的问题和答案,以及一些逻辑来将这些内容融入到当前的思考中。这里我们重点关注添加用于整合历史消息的逻辑。 这涉及到 聊天记录的管理

以下将介绍两种方法:

  • 链,其中我们最多执行一个检索步骤;
  • Agents,其中我们赋予 LLM 自主权来执行多个检索步骤。

八、链

让我们首先回顾一下我们在第一部分中构建的向量存储,它索引了 古诗词网站 的一篇 古诗《茅屋为秋风所破歌》。

import bs4
from langchain import hub
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from typing_extensions import List, TypedDict# Load and chunk contents of the blog
loader = WebBaseLoader(web_paths=("https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx",),bs_kwargs=dict(parse_only=bs4.SoupStrainer(class_=("sons", "contyishang", "sonspic"))),
)
docs = loader.load()text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
all_splits = text_splitter.split_documents(docs)
# Index chunks
_ = vector_store.add_documents(documents=all_splits)

在 RAG 教程的第一部分中,我们将用户输入、检索到的上下文和生成的答案表示为状态中的单独键。对话式体验可以使用一系列消息自然地表示。除了来自用户和助手的信息外,检索到的文档和其他工件可以通过工具消息合并到消息序列中。这促使我们使用消息序列来表示 RAG 应用程序的状态。具体来说,我们将有:

  • 用户输入作为 HumanMessage
  • 向量存储查询作为带有工具调用的 AIMessage
  • 检索到的文档作为 ToolMessage
  • 最终响应作为 AIMessage

这种状态模型非常通用,LangGraph 提供了内置版本以方便使用:

from langgraph.graph import MessagesState, StateGraphgraph_builder = StateGraph(MessagesState)

利用工具调用与检索步骤交互还有另一个好处,那就是检索的查询是由我们的模型生成的。这在对话设置中尤其重要,在对话设置中,用户查询可能需要根据聊天历史记录进行情境化。例如,考虑以下交流:

  • 用户:“什么是任务分解?”
  • AI:“任务分解涉及将复杂任务分解为更小更简单的步骤,以使 Agent 或模型更容易管理。”
  • 用户:“有哪些常见的方法?”

在这种情况下,模型可以生成诸如 “任务分解的常用方法” 之类的查询。工具调用自然地促进了这一点。正如 RAG 教程的查询分析部分中所述,这允许模型将用户查询重写为更有效的搜索查询。它还支持不涉及检索步骤的直接响应(例如,响应来自用户的通用问候)。

让我们将检索步骤转换为工具:

from langchain_core.tools import tool@tool(response_format="content_and_artifact")
def retrieve(query: str):"""Retrieve information related to a query."""retrieved_docs = vector_store.similarity_search(query, k=2)serialized = "\n\n".join((f"Source: {doc.metadata}\n" f"Content: {doc.page_content}")for doc in retrieved_docs)return serialized, retrieved_docs

我们的图将包含三个节点:

  • 一个节点,用于处理用户输入,要么生成检索器的查询,要么直接响应;
  • 一个用于检索器工具的节点,用于执行检索步骤;
  • 一个节点,用于使用检索到的上下文生成最终响应。

我们在下面构建它们。请注意,我们利用了另一个预构建的 LangGraph 组件 ToolNode,它执行工具并将结果作为 ToolMessage 添加到状态。

from langchain_core.messages import SystemMessage
from langgraph.prebuilt import ToolNode# Step 1: Generate an AIMessage that may include a tool-call to be sent.
def query_or_respond(state: MessagesState):"""Generate tool call for retrieval or respond."""llm_with_tools = llm.bind_tools([retrieve])response = llm_with_tools.invoke(state["messages"])# MessagesState appends messages to state instead of overwritingreturn {"messages": [response]}# Step 2: Execute the retrieval.
tools = ToolNode([retrieve])# Step 3: Generate a response using the retrieved content.
def generate(state: MessagesState):"""Generate answer."""# Get generated ToolMessagesrecent_tool_messages = []for message in reversed(state["messages"]):if message.type == "tool":recent_tool_messages.append(message)else:breaktool_messages = recent_tool_messages[::-1]# Format into promptdocs_content = "\n\n".join(doc.content for doc in tool_messages)system_message_content = ("You are an assistant for question-answering tasks. ""Use the following pieces of retrieved context to answer ""the question. If you don't know the answer, say that you ""don't know. Use three sentences maximum and keep the ""answer concise.""\n\n"f"{docs_content}")conversation_messages = [messagefor message in state["messages"]if message.type in ("human", "system")or (message.type == "ai" and not message.tool_calls)]prompt = [SystemMessage(system_message_content)] + conversation_messages# Runresponse = llm.invoke(prompt)return {"messages": [response]}

最后,我们将我们的应用程序编译成一个单独的 graph 对象。在本例中,我们只是将步骤连接成一个序列。我们还允许第一个 query_or_respond 步骤“短路”,并在不生成工具调用的情况下直接响应用户。这使我们的应用程序能够支持对话式体验——例如,响应可能不需要检索步骤的通用问候。

from langgraph.graph import END
from langgraph.prebuilt import ToolNode, tools_conditiongraph_builder.add_node(query_or_respond)
graph_builder.add_node(tools)
graph_builder.add_node(generate)graph_builder.set_entry_point("query_or_respond")
graph_builder.add_conditional_edges("query_or_respond",tools_condition,{END: END, "tools": "tools"},
)
graph_builder.add_edge("tools", "generate")
graph_builder.add_edge("generate", END)graph = graph_builder.compile()
from IPython.display import Image, displaydisplay(Image(graph.get_graph().draw_mermaid_png()))

在这里插入图片描述

测试:

input_message = "你好"for step in graph.stream({"messages": [{"role": "user", "content": input_message}]},stream_mode="values",
):step["messages"][-1].pretty_print()
================================ Human Message =================================你好
================================== Ai Message ==================================你好!有什么可以帮助你的吗?

当执行搜索时,我们可以流式传输步骤以观察查询生成、检索和答案生成:

input_message = "茅屋为秋风所破歌这首古诗的创作背景是什么时候?"for step in graph.stream({"messages": [{"role": "user", "content": input_message}]},stream_mode="values",
):step["messages"][-1].pretty_print()
================================ Human Message =================================茅屋为秋风所破歌这首古诗的创作背景是什么时候?
================================== Ai Message ==================================
Tool Calls:retrieve (0196ce3d93f1ce354cd790063fa1956c)Call ID: 0196ce3d93f1ce354cd790063fa1956cArgs:query: 茅屋为秋风所破歌 创作背景
================================= Tool Message =================================
Name: retrieveSource: {'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}
Content: 简析《茅屋为秋风所破歌》是一首歌行体古诗。此诗叙述了诗人的茅屋被秋风所破以致全家遭雨淋的痛苦经历,他从中生出万千感慨,体现了忧国忧民的思想情感,是杜诗中的典范之作。这首诗叙事与抒情并重,叙事部分大篇幅写实,诗人诉述自家之苦,情绪含蓄压抑;而后的抒情部分将苦难加以升华,直抒忧民之情,情绪激越轩昂,展现出诗人的崇高理想和境界。叙事部分的有力铺垫为全诗最后的言志奠定了坚实的基础,如此抑扬曲折的情绪变换,充分体现出杜诗“沉郁顿挫”的风格。杜甫(712-770),字子美,自号少陵野老,世称“杜工部”、“杜少陵”等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为“诗圣”,其诗被称为“诗史”。杜甫与李白合称“李杜”,为了跟另外两位诗人李商隐与杜牧即“小李杜”区别开来,杜甫与李白又合称“大李杜”。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念。► 1338篇诗文 ► 2728条名句================================== Ai Message ==================================《茅屋为秋风所破歌》创作于唐代安史之乱后,杜甫寓居成都期间(约760年左右)。此时期杜甫生活困顿,因战乱流离失所,诗中通过描写自身茅屋被秋风吹破的遭遇,抒发了对民生疾苦的深切关怀。该诗反映了杜甫在困顿中仍抱有“安得广厦千万间”的济世理想,体现了其“沉郁顿挫”的诗风。

九、聊天记录的状态管理

在生产环境中,问答应用程序通常会将聊天记录持久化到数据库中,并且能够适当地读取和更新它。

LangGraph 实现了内置的 持久化层,使其成为支持多轮对话的聊天应用程序的理想选择。

要管理多轮对话和线程,我们所要做的就是在编译我们的应用程序时指定一个 检查点。由于我们图中的节点正在将消息附加到状态,因此我们将在多次调用中保持一致的聊天记录。

LangGraph 配备了一个简单的内存中检查点,我们在下面使用它。

from langgraph.checkpoint.memory import MemorySavermemory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)# Specify an ID for the thread
config = {"configurable": {"thread_id": "abc123"}}

我们现在可以像以前一样调用

input_message = "茅屋为秋风所破歌这首古诗的创作背景是什么时候?"for step in graph.stream({"messages": [{"role": "user", "content": input_message}]},stream_mode="values",config=config,
):step["messages"][-1].pretty_print()
================================ Human Message =================================茅屋为秋风所破歌这首古诗的创作背景是什么时候?
================================== Ai Message ==================================
Tool Calls:retrieve (0196ce743c01f68656e9b5e68fd56443)Call ID: 0196ce743c01f68656e9b5e68fd56443Args:query: 茅屋为秋风所破歌 创作背景
================================= Tool Message =================================
Name: retrieveSource: {'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}
Content: 简析《茅屋为秋风所破歌》是一首歌行体古诗。此诗叙述了诗人的茅屋被秋风所破以致全家遭雨淋的痛苦经历,他从中生出万千感慨,体现了忧国忧民的思想情感,是杜诗中的典范之作。这首诗叙事与抒情并重,叙事部分大篇幅写实,诗人诉述自家之苦,情绪含蓄压抑;而后的抒情部分将苦难加以升华,直抒忧民之情,情绪激越轩昂,展现出诗人的崇高理想和境界。叙事部分的有力铺垫为全诗最后的言志奠定了坚实的基础,如此抑扬曲折的情绪变换,充分体现出杜诗“沉郁顿挫”的风格。杜甫(712-770),字子美,自号少陵野老,世称“杜工部”、“杜少陵”等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为“诗圣”,其诗被称为“诗史”。杜甫与李白合称“李杜”,为了跟另外两位诗人李商隐与杜牧即“小李杜”区别开来,杜甫与李白又合称“大李杜”。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念。► 1338篇诗文 ► 2728条名句================================== Ai Message ==================================《茅屋为秋风所破歌》创作于唐代宗大历二年(767年)前后,杜甫流寓成都期间。此时他居住的草屋因秋风破败,生活困顿,借此抒发对民生疾苦的深切关怀,体现了安史之乱后社会动荡背景下诗人的忧国忧民情怀。
input_message = "期间还创作了什么其他古诗?"for step in graph.stream({"messages": [{"role": "user", "content": input_message}]},stream_mode="values",config=config,
):step["messages"][-1].pretty_print()
================================ Human Message =================================期间还创作了什么其他古诗?
================================== Ai Message ==================================
Tool Calls:retrieve (0196ce76200f4e785113ac2ae97eee5a)Call ID: 0196ce76200f4e785113ac2ae97eee5aArgs:query: 
================================= Tool Message =================================
Name: retrieveSource: {'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}
Content: 茅屋为秋风所破歌杜甫〔唐代〕八月秋高风怒号,卷我屋上三重茅。茅飞渡江洒江郊,高者挂罥长林梢,下者飘转沉塘坳。南村群童欺我老无力,忍能对面为盗贼。公然抱茅入竹去,唇焦口燥呼不得,归来倚杖自叹息。俄顷风定云墨色,秋天漠漠向昏黑。布衾多年冷似铁,娇儿恶卧踏里裂。床头屋漏无干处,雨脚如麻未断绝。自经丧乱少睡眠,长夜沾湿何由彻!安得广厦千万间,大庇天下寒士俱欢颜!风雨不动安如山。呜呼!何时眼前突兀见此屋,吾庐独破受冻死亦足!(亦足 一作:意足)译文及注释译文八月秋深狂风大声吼叫,狂风卷走了我屋顶上好几层茅草。茅草乱飞渡过浣花溪散落在对岸江边,飞得高的茅草缠绕在高高的树梢上,飞得低的飘飘洒洒沉落到低洼的水塘里。南村的一群儿童欺负我年老没力气,竟狠心这样当面做“贼”抢东西,明目张胆地抱着茅草跑进竹林里去了。我费尽口舌也喝止不住,回到家后拄着拐杖独自叹息。不久后风停了天空上的云像墨一样黑,秋季的天空阴沉迷蒙渐渐黑了下来。布质的被子盖了多年又冷又硬像铁板似的,孩子睡相不好把被子蹬破了。如遇下雨整个屋子没有一点儿干燥的地方,雨点像下垂的麻线一样不停地往下漏。自从安史之乱后我的睡眠时间就很少了,长夜漫漫屋子潮湿不创作背景这首诗作于唐肃宗上元二年(公元761年)八月。公元760年春天,杜甫求亲告友,在成都浣花溪边盖起了一座茅屋,总算有了一个栖身之所。不料到了公元761年八月,大风破屋,大雨又接踵而至。当时安史之乱尚未平息,诗人感慨万千,写下了这篇脍炙人口的诗篇。 ================================== Ai Message ==================================杜甫在成都期间(约760-761年)还创作了《春夜喜雨》《蜀相》《旅夜书怀》等诗作。其中《春夜喜雨》描写成都春夜雨景,抒发对自然与生活的喜爱;《蜀相》则凭吊诸葛亮,表达对贤相的追思与对时局的感慨。这些作品均体现了杜甫对民生疾苦的关怀与个人情感的抒发。

请注意,模型在第二个问题中生成的查询包含了对话上下文。

十、Agents

Agents 利用 LLM 的推理能力在执行过程中做出决策。使用 Agents 允许您卸载检索过程的额外自主权。尽管它们的行为不如上面的“链”那样可预测,但它们能够执行多个检索步骤来服务于查询,或者迭代单个搜索。

下面我们组装一个最小的 RAG Agent。使用 LangGraph 的 预构建 ReAct Agent 构造器,我们可以在一行中完成此操作。

from langgraph.prebuilt import create_react_agentagent_executor = create_react_agent(llm, [retrieve], checkpointer=memory)

让我们检查一下图

from IPython.display import Image, displaydisplay(Image(agent_executor.get_graph().draw_mermaid_png()))

在这里插入图片描述

与我们之前的实现的主要区别在于,这里的工具调用循环回到原始 LLM 调用,而不是结束运行的最终生成步骤。然后,模型可以使用检索到的上下文回答问题,或者生成另一个工具调用以获取更多信息。

让我们测试一下。我们构建一个通常需要迭代检索步骤序列才能回答的问题:

config = {"configurable": {"thread_id": "def234"}}input_message = ("茅屋为秋风所破歌这首古诗的创作背景是什么时候?\n\n""一旦你得到答案,查找该古诗表达了作者什么情感。"
)for event in agent_executor.stream({"messages": [{"role": "user", "content": input_message}]},stream_mode="values",config=config,
):event["messages"][-1].pretty_print()
================================ Human Message =================================茅屋为秋风所破歌这首古诗的创作背景是什么时候?一旦你得到答案,查找该古诗表达了作者什么情感。
================================== Ai Message ==================================《茅屋为秋风所破歌》的创作背景与杜甫的个人经历及唐代社会现状密切相关:---### **一、创作背景**
1. **时间**:  此诗创作于**唐肃宗上元元年(760年)秋**,是杜甫晚年的重要作品之一。  此时杜甫寓居成都(今四川省成都市),在友人严武的帮助下,在城西浣花溪畔建成“茅屋”(即“杜甫草堂”)。  - **历史背景**:安史之乱(755-763年)后,唐朝社会动荡,民生凋敝,诗人亲身经历战乱离散,目睹百姓苦难。2. **个人境遇**:  - 茅屋因秋风破败,屋漏雨湿,诗人一家生活困顿。  - 村民趁其年老体弱,抢夺茅草(“南村群童欺我老无力”),进一步加剧困苦。  - 这种生活困境成为诗人心中“个人苦难”的缩影。---### **二、表达的情感**
此诗通过叙事与抒情结合,表达了杜甫复杂而深刻的情感:1. **个人悲苦**:  - 描写茅屋破败、风雨交加的实景(如“布衾多年冷似铁”“长夜沾湿何由彻”),展现诗人对自身处境的无奈与辛酸。2. **社会关怀**:  - 从个人困境转向对天下苍生的悲悯(“安得广厦千万间,大庇天下寒士俱欢颜”),体现杜甫“**穷则独善其身,达则兼济天下**”的理想。  - 通过“风雨不动安如山”的比喻,将个人苦难与社会动荡联系,隐含对国家安定的渴望。3. **忧国忧民情怀**:  - 诗中“**朱门酒肉臭,路有冻死骨**”(虽非本诗,但同属杜甫现实主义风格)的批判精神贯穿其作品,此诗亦通过“茅屋”象征百姓疾苦,反映战乱对民生的摧残。---### **总结**  
这首诗以个人遭遇为切入点,既抒发了杜甫对自身困顿的哀叹,更升华至对天下寒士的深切同情,最终以“**安得广厦千万间**”的呐喊,彰显其“**诗史**”般的现实主义精神与忧国忧民的情怀。

请注意,Agent 实际的步骤:

  • 生成查询 以 搜索 第一个问题的答案;
  • 接收到答案后,生成第二个查询以搜索第二个问题的答案;
  • 在收到所有必要的上下文后,回答问题。

参考资料

  • https://python.langchain.ac.cn/docs/tutorials/rag/#indexing
  • https://python.langchain.ac.cn/docs/tutorials/qa_chat_history/

相关文章:

实践指南:从零开始搭建RAG驱动的智能问答系统

LLM 赋能的最强大的应用之一是复杂的问答 (Q&A) 聊天机器人。这些是可以回答关于特定来源信息问题的应用程序。这些应用程序使用一种称为检索增强生成的技术&#xff0c;或 RAG。本文将展示如何基于 LangChain 构建一个简单的基于非结构化数据文本数据源的问答应用程序。 温…...

边缘计算服务器

边缘计算服务器的核心要点解析&#xff0c;综合技术架构、应用场景与部署方案&#xff1a; 一、核心定义与技术特性‌ 本质定位‌ 部署在网络边缘侧的专用计算设备&#xff08;如工厂车间、智慧路灯等&#xff09;&#xff0c;直接处理终端设备&#xff08;传感器、摄像头等…...

矩阵的偏导数

设 X ( x i j ) m n X (x_{ij})_{m \times n} X(xij​)mn​&#xff0c;函数 f ( X ) f ( x 11 , x 12 , … , x 1 n , x 21 , … , x m n ) f(X) f(x_{11}, x_{12}, \ldots, x_{1n}, x_{21}, \ldots, x_{mn}) f(X)f(x11​,x12​,…,x1n​,x21​,…,xmn​) 是一个 m n…...

第R9周:阿尔茨海默病诊断(优化特征选择版)

文章目录 1. 导入数据2. 数据处理2.1 患病占比2.2 相关性分析2.3 年龄与患病探究 3. 特征选择4. 构建数据集4.1 数据集划分与标准化4.2 构建加载 5. 构建模型6. 模型训练6.1 构建训练函数6.2 构建测试函数6.3 设置超参数 7. 模型训练8. 模型评估8.1 结果图 8.2 混淆矩阵9. 总结…...

电动螺丝刀-多实体拆图建模案例

多实体建模要注意下面两点&#xff1a; 多实体建模的合并结果一定要谨慎在实际工作中多实体建模是一个非常好的思路&#xff0c;先做产品的整体设计&#xff0c;再将个体零件导出去做局部细节设计 电动螺丝刀模型动图展示 爆炸视图动图展示 案例素材点击此处获取 建模步骤 1. …...

当丰收季遇上超导磁测量:粮食产业的科技新征程

麦浪藏光阴&#xff0c;心田种丰年&#xff01;又到了一年中最令人心潮澎湃的粮食丰收季。金色的麦浪随风翻滚&#xff0c;沉甸甸的稻穗谦逊地低垂着&#xff0c;处处洋溢着丰收的喜悦。粮食产业&#xff0c;无疑是国家发展的根基与命脉&#xff0c;是民生稳定的压舱石。在现代…...

电子电气架构 --- 什么是功能架构?

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...

Android四大组件通讯指南:Kotlin版组件茶话会

某日&#xff0c;Android王国举办Kotlin主题派对。Activity穿着Jetpack Compose定制礼服&#xff0c;Service戴着协程手表&#xff0c;BroadcastReceiver拿着Flow喇叭&#xff0c;ContentProvider抱着Room数据库入场。它们正愁如何交流&#xff0c;Intent举着"邮差"牌…...

C++.OpenGL (11/64)材质(Materials)

材质(Materials) 真实感材质系统 #mermaid-svg-NjBjrmlcpHupHCFQ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-NjBjrmlcpHupHCFQ .error-icon{fill:#552222;}#mermaid-svg-NjBjrmlcpHupHCFQ .error-text{fill:…...

AudioRelay 0.27.5 手机充当电脑音响

—————【下 载 地 址】——————— 【​本章下载一】&#xff1a;https://pan.xunlei.com/s/VOS4MvfPxrnfS2Zu_YS4egykA1?pwdi2we# 【​本章下载二】&#xff1a;https://pan.xunlei.com/s/VOS4MvfPxrnfS2Zu_YS4egykA1?pwdi2we# 【百款黑科技】&#xff1a;https://uc…...

会计 - 合并1- 业务、控制、合并日

一、业务 1.1 业务的定义以及构成要素 业务,是指企业内部某些生产经营活动或资产的组合,该组合一般具有投入、加工处理过程和产出能力,能够独立计算其成本费用或所产生的收入。 (1)投入,指原材料、人工、必要的生产技术等无形资产以及构成产出能力的机器设备等其他长期资…...

前端项目eslint配置选项详细解析

文章目录 1. 前言2、错误级别3、常用规则4、目前项目使用的.eslintrc.js 1. 前言 ‌ESLint‌ 是一个可配置的 JavaScript 代码检查工具&#xff0c;旨在帮助开发者发现并修复代码中的潜在问题&#xff0c;包括语法错误、逻辑错误以及风格不一致等问题。以下是其核心功能和特点…...

NVIDIA Dynamo:数据中心规模的分布式推理服务框架深度解析

NVIDIA Dynamo&#xff1a;数据中心规模的分布式推理服务框架深度解析 摘要 NVIDIA Dynamo是一个革命性的高吞吐量、低延迟推理框架&#xff0c;专为在多节点分布式环境中服务生成式AI和推理模型而设计。本文将深入分析Dynamo的架构设计、核心特性、代码实现以及实际应用示例&…...

第十三节:第四部分:集合框架:HashMap、LinkedHashMap、TreeMap

Map集合体系 HashMap集合的底层原理 HashMap集合底层是基于哈希表实现的 LinkedHashMap集合的底层原理 TreeMap集合的底层原理 代码&#xff1a; Student类 package com.itheima.day26_Map_impl;import java.util.Objects;public class Student implements Comparable<Stu…...

Spring AI之RAG入门

目录 1. 什么是RAG 2. RAG典型应用场景 3. RAG核心流程 3.1. 检索阶段 3.2. 生成阶段 4. 使用Spring AI实现RAG 4.1. 创建项目 4.2. 配置application.yml 4.3. 安装ElasticSearch和Kibana 4.3.1. 安装并启动ElasticSearch 4.3.2. 验证ElasticSearch是否启动成功 …...

应用案例 | 设备分布广, 现场维护难? 宏集Cogent DataHub助力分布式锅炉远程运维, 让现场变“透明”

在日本&#xff0c;能源利用与环保问题再次成为社会关注的焦点。越来越多的工业用户开始寻求更高效、可持续的方式来运营设备、管理能源。而作为一家专注于节能与自动化系统集成的企业&#xff0c;日本大阪的TESS工程公司给出了一个值得借鉴的答案。 01 锅炉远程监控难题如何破…...

C#中的密封类与静态类:特性、区别与应用实例

深入解析两类特殊类的设计哲学与实战应用 在面向对象编程领域中&#xff0c;C#提供了多种特殊的类类型以满足不同设计需求。其中密封类&#xff08;sealed class&#xff09;和静态类&#xff08;static class&#xff09;是最常用的两种特殊类类型。本文将从设计理念、应用场…...

LINUX 66 FTP 2 ;FTP被动模式;FTP客户服务系统

19&#xff0e; 在vim中将所有 abc 替换为 def&#xff0c;在底行模式下执行©&#xff1f;D A、s/abc/def B、s/abc/def/g C、%s/abc/def D、%s/abc/def/g FTP连接 用户名应该填什么 [rootcode ~]# grep -v ^# /etc/vsftpd/vsftpd.conf anonymous_enableNO local_enab…...

网心云 OEC/OECT 笔记(2) 运行RKNN程序

目录 网心云 OEC/OECT 笔记(1) 拆机刷入Armbian固件网心云 OEC/OECT 笔记(2) 运行RKNN程序 RKNN OEC/OEC-Turbo 使用的芯片是 RK3566/RK3568, 这个系列是内建神经网络处理器 NPU 的, 利用 RKNN 可以部署运行 AI 模型利用 NPU 硬件加速模型推理. 要使用 NPU, 首先需要在电脑使…...

vue-21 (使用 Vuex 模块和异步操作构建复杂应用)

实践练习:使用 Vuex 模块和异步操作构建复杂应用 Vuex 模块提供了一种结构化的方式来组织你的应用程序状态,特别是当应用程序变得复杂时。命名空间模块通过防止命名冲突和提高代码可维护性来增强这种组织。异步操作对于处理从 API 获取数据等操作至关重要,这些操作在现代 W…...

#开发环境篇:postMan可以正常调通,但是浏览器里面一直报403

本地header代理下面内容即可 headers: { // 添加必要的请求头 ‘Host’: ‘服务端域名’, ‘Origin’: https://服务端域名, ‘Referer’: https://服务端域名 }, devServer: {// 本地开发代理API地址proxy: {^/file: {target: https://服务端域名,changeOrigin: true, // 是否…...

将word文件转为kindle可识别的azw3文件的方法

亚马逊在中国停服后&#xff0c;要将word文件传送到kindle设备上进行阅读就不能通过电子邮件的方式了&#xff0c;只能通过将word文件进行转换后通过数据线传到kindle的方式来实现&#xff0c;通过线上或线下的转换工具可将word文件转化为azw文件&#xff0c;但通过数据线将转换…...

动态规划之01背包

首要 由于自己的个人原因(说白了就是懒)&#xff0c;忙于各种事情&#xff0c;实在忙不过来(哭)&#xff0c;只能把发文分享的事情一推再推&#xff0c;直到某天良心发现产生了想发文的想法&#xff0c;于是就写下了这篇文章&#xff0c;请各位大佬轻喷 背包问题 背包问题是一…...

Lua和JS的继承原理

JavaScript 和 Lua 都是动态语言&#xff0c;支持面向对象编程&#xff08;OOP&#xff09;&#xff0c;但它们的 继承机制 实现方式不一样。下面分别介绍它们的继承实现原理和方式&#xff1a; &#x1f536; JavaScript 的继承机制 JavaScript 使用的是 基于原型&#xff08…...

灵活控制,modbus tcp转ethernetip的 多功能水处理方案

油田自动化和先进的油气行业软件为油气公司带来了诸多益处。其中包括&#xff1a; 1.自动化可以消除多余的步骤、减少人为错误并降低运行设备所需的能量&#xff0c;从而降低成本。 2.油天然气行业不断追求高水平生产。自动化可以更轻松地减少计划外停机时间&#xff0c;从而…...

boost::qvm 使用示例

boost::qvm 使用示例 boost::qvm (Quaternions, Vectors and Matrices) 是 Boost 库中的一个组件&#xff0c;专门用于处理向量、矩阵和四元数运算。以下是几个常见的使用示例&#xff1a; 基本向量操作 #include <boost/qvm/vec.hpp> #include <boost/qvm/vec_ope…...

go语言学习 第6章:错误处理

第6章&#xff1a;错误处理 在任何编程语言中&#xff0c;错误处理都是一个至关重要的环节。Go语言以其简洁而强大的错误处理机制而闻名&#xff0c;这使得开发者能够以一种优雅且高效的方式处理程序中可能出现的错误情况。本章将深入探讨Go语言中的错误处理机制&#xff0c;包…...

VMware 安装 CentOS8详细教程 (附步骤截图)附连接公网、虚拟机yum源等系统配置

1 下载安装镜像 centos8官方源已下线,旧的下载地址已不可用,需要切换centos-vault源 华为云CentOS8镜像下载地址 阿里云CentOS8镜像下载地址 中科大CentOS8镜像下载地址 2 安装CentOS8 2.1 创建虚拟机 打开VMware Workstation 左上角 文件-新建虚拟机...

Editing Language Model-based Knowledge Graph Embeddings

基于语言模型的知识图谱嵌入 原文链接&#xff1a;https://arxiv.org/abs/2301.10405 Comment: AAAI 2024.03 摘要 基于语言模型的KG嵌入通常部署为静态工件&#xff0c;这使得它们在部署后如果不重新训练就很难修改。在本文中提出了一个编辑基于语言模型的 KG 嵌入的新任务。…...

深入了解linux系统—— 进程池

前言&#xff1a; 本篇博客所涉及到的代码以同步到本人gitee&#xff1a;进程池 迟来的grown/linux - 码云 - 开源中国 一、池化技术 在之前的学习中&#xff0c;多多少少都听说过池&#xff0c;例如内存池&#xff0c;线程池等等。 那这些池到底是干什么的呢&#xff1f;池…...