LLM 赋能的最强大的应用之一是复杂的问答 (Q&A) 聊天机器人。这些是可以回答关于特定来源信息问题的应用程序。这些应用程序使用一种称为检索增强生成的技术,或 RAG。本文将展示如何基于 LangChain
构建一个简单的基于非结构化数据文本数据源的问答应用程序。
温馨提示:本文搭配
Jupyter notebooks
食用更佳,在交互式环境中学习是更好地理解它们的好方法。
一、RAG 概述
RAG(Retrieval-Augmented Generation,检索增强生成)是一种通过结合外部知识检索与大型语言模型(LLM)生成能力的技术框架,旨在提升模型回答的准确性和时效性,解决传统模型的知识局限与"幻觉"问题。
RAG 系统包含三个核心模块:
-
索引(Indexing) :
- 将文档分块、向量化并存储至向量数据库(如FAISS、Milvus);
- 优化策略包括数据粒度调整、元数据添加及混合检索(稠密检索+稀疏检索)。
-
检索(Retrieval) :
- 根据用户查询语义,从数据库召回最相关的文档片段(Top-k);
- 预检索阶段通过查询重写、扩展等技术优化意图理解。
-
生成(Generation) :
- 将检索结果与原始查询组合为提示词(Prompt),输入LLM(如GPT-4、LLaMA-3)生成最终答案;
- 后处理可能包含重排序(Reranking)或上下文压缩以减少噪声干扰。
二、RAG 实现流程
典型的 RAG 应用程序具有两个主要组件:
- 索引:一个用于从源摄取数据并对其进行索引的管道。这通常离线发生。
- 检索和生成:实际的 RAG 链,它在运行时获取用户查询,并从索引中检索相关数据,然后将其传递给模型。
Step1、索引
- 加载 :首先我们需要加载数据。这通过
文档加载器
完成。 - 分割 :文本分割器将大型
Documents
分解为更小的块。这对于索引数据和将其传递到模型中都很有用,因为大块数据更难搜索,并且无法容纳在模型的有限上下文窗口中。 - 存储 :我们需要某个地方来存储和索引这些分割块,以便稍后可以对其进行搜索。这通常使用
向量存储
和嵌入模型
完成。

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

一旦我们索引了数据,我们将使用 LangGraph
作为工作流编排框架来实现检索和生成步骤。
三、依赖项与组件
安装langchain
依赖项:
bash
pip install --quiet --upgrade langchain-text-splitters langchain-community langgraph
安装 LLM
依赖:
bash
pip install -qU "langchain[openai]"
这里使用硅基流动平台的大模型服务,Qwen3-8B
python
from pydantic import SecretStr
import os
os.environ["OPENAI_BASE_URL"] = "https://api.siliconflow.cn/v1/"
os.environ["OPENAI_API_KEY"] = "sk-xxx"
from langchain.chat_models import init_chat_model
llm = init_chat_model("Qwen/Qwen3-8B", model_provider="openai")
安装 Embeddings
依赖:
bash
pip install -qU langchain-openai
这里嵌入模型使用 BAAI/bge-large-zh-v1.5
python
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="BAAI/bge-large-zh-v1.5")
安装 VectorStore
依赖:
bash
pip install -qU langchain-core
这里使用内存作为向量存储,也可以使用轻量级向量数据库 Chroma
python
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 链,从古诗词网站中解析所需文本,根据文本相关内容进行提问

python
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: str
context: 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}
,这是因为文本块切割过多导致的,需要降低块数,解决参考 js.langchain.com.cn/docs/module...
python
response = graph.invoke({"question": "创作背景"})
print(response["answer"])
《茅屋为秋风所破歌》创作于唐肃宗上元二年(761年)八月,杜甫在成都浣花溪畔的茅屋被秋风吹破、大雨侵袭之际所作。当时正值安史之乱未平,诗人因自身困境联想到天下寒士的苦难,抒发了忧国忧民的情怀。此诗通过个人遭遇折射时代动荡,体现了杜甫"关心人民疾苦"的现实主义精神。
五、步骤拆解
接下来我们逐步拆解分析以上示例是怎么实现的。
1. 索引
1.1加载文档
python
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)}")
yaml
Total characters: 1885
在本例中,我们使用 WebBaseLoader
,它使用 urllib
从 Web URL
加载 HTML
,并使用 BeautifulSoup
将其解析为文本。我们可以通过将参数传递到 BeautifulSoup
解析器(通过 bs_kwargs
)来自定义 HTML
-> 文本解析。在本例中,只有类为"sons"
、"contyishang"
或"sonspic"
的 HTML
标签是相关的,因此我们将删除所有其他标签。
python
print(docs[0].page_content[:500])
scss
茅屋为秋风所破歌
杜甫〔唐代〕
八月秋高风怒号,卷我屋上三重茅。茅飞渡江洒江郊,高者挂罥长林梢,下者飘转沉塘坳。南村群童欺我老无力,忍能对面为盗贼。公然抱茅入竹去,唇焦口燥呼不得,归来倚杖自叹息。俄顷风定云墨色,秋天漠漠向昏黑。布衾多年冷似铁,娇儿恶卧踏里裂。床头屋漏无干处,雨脚如麻未断绝。自经丧乱少睡眠,长夜沾湿何由彻!安得广厦千万间,大庇天下寒士俱欢颜!风雨不动安如山。呜呼!何时眼前突兀见此屋,吾庐独破受冻死亦足!(亦足 一作:意足)
译文及注释
译文八月秋深狂风大声吼叫,狂风卷走了我屋顶上好几层茅草。茅草乱飞渡过浣花溪散落在对岸江边,飞得高的茅草缠绕在高高的树梢上,飞得低的飘飘洒洒沉落到低洼的水塘里。南村的一群儿童欺负我年老没力气,竟狠心这样当面做"贼"抢东西,明目张胆地抱着茅草跑进竹林里去了。我费尽口舌也喝止不住,回到家后拄着拐杖独自叹息。不久后风停了天空上的云像墨一样黑,秋季的天空阴沉迷蒙渐渐黑了下来。布质的被子盖了多年又冷又硬像铁板似的,孩子睡相不好把被子蹬破了。如遇下雨整个屋子没有一点儿干燥的地方,雨
1.2分割文档
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_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.")
vbnet
Split blog post into 21 sub-documents.
我们加载的文档有时候会文本过长,无法容纳到许多模型的上下文窗口中。即使对于那些可以容纳完整帖子在其上下文窗口中的模型,模型也可能难以在非常长的输入中找到信息。为了处理这个问题,我们将把 Document
分割成块,以便进行嵌入和向量存储。这应该有助于我们在运行时仅检索博客文章的最相关部分。
1.3存储文档
python
document_ids = vector_store.add_documents(documents=all_splits)
print(document_ids[:3])
css
['ab3ec6c0-3583-4525-b86c-b08dbf2077e4', '25cf74fd-5cfa-4940-99ba-c0a4e85ff344', '0383b722-5723-433c-a776-f17b4ae1de8f']
2.检索和生成
现在让我们编写实际的应用程序逻辑。我们想要创建一个简单的应用程序,它接受用户问题,搜索与该问题相关的文档,将检索到的文档和原始问题传递给模型,并返回答案。
python
from langchain import hub
prompt = 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)
vbnet
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状态
python
from langchain_core.documents import Document
from typing_extensions import List, TypedDict
class State(TypedDict):
question: str
context: List[Document]
answer: str
我们应用程序的 状态 控制着哪些数据输入到应用程序、在步骤之间传输以及由应用程序输出。它通常是 TypedDict
,但也可以是 Pydantic BaseModel
。对于简单的 RAG 应用程序,我们可以只跟踪输入问题、检索到的上下文和生成的答案。
2.2节点(应用程序步骤)
python
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控制流
python
from langgraph.graph import START, StateGraph
graph_builder = StateGraph(State).add_sequence([retrieve, generate])
graph_builder.add_edge(START, "retrieve")
graph = graph_builder.compile()
最后,我们将我们的应用程序编译成单个 graph 对象。在本例中,我们只是将检索和生成步骤连接成一个序列。
python
# LangGraph 还附带内置实用程序,用于可视化您应用程序的控制流
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))

构建 RAG 应用程序不需要 LangGraph
,只是使用 LangGraph
有一些优势。实际上,我们可以通过调用各个组件来实现相同的应用程序逻辑:
python
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同步/异步调用
python
# 同步调用
result = graph.invoke({"question": "创作背景"})
print(f'Context: {result["context"]}\n\n')
print(f'Answer: {result["answer"]}')
bash
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年)八月,杜甫在成都浣花溪畔的茅屋被秋风吹破后,又遇大雨,生活困顿。当时安史之乱尚未平息,诗人因自身遭遇联想到百姓疾苦,抒发了忧国忧民的情怀。此诗反映了战乱年代的民生艰难与诗人匡世济民的理想。
python
# 异步调用
result = await graph.ainvoke(...)
# 或
async for step in graph.astream(...):
3.2流式传输步骤
python
for step in graph.stream(
{"question": "这首古诗的创作背景?"}, stream_mode="updates"
):
print(f"{step}\n\n----------------\n")
bash
{'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
python
for message, metadata in graph.stream(
{"question": "表达作者什么情感?"}, stream_mode="messages"
):
print(message.content, end="|")
|作者|表达了|深切|的|忧|国|忧|民|情感|。|诗|中|通过|自身|茅|屋|被|破|的|困境|,|升华|出|对|百姓|疾|苦|的|关怀|与|对|国家|命运|的|牵挂|,|体现了|杜|甫|"|沉|郁|顿|挫|"的|创|作风|格|。|全|诗|情感|从|压抑|到|激|昂|的|转折|,|展现了|诗人|高尚|的人|格|理想|与|博|大|胸怀|。||
4.自定义提示
如上所示,我们可以从提示中心加载提示(例如,此 RAG 提示)。提示也可以轻松自定义。
python
from langchain_core.prompts import PromptTemplate
template = """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 年以来的文档。");
- 模型可以将用户查询(可能是多方面的或包含不相关的语言)重写为更有效的搜索查询。
查询分析 使用模型从原始用户输入转换或构建优化的搜索查询。我们可以轻松地将查询分析步骤合并到我们的应用程序中。为了说明目的,让我们向向量存储中的文档添加一些元数据。我们将向文档添加一些(人为的)部分,我们稍后可以在其上进行过滤。
python
total_documents = len(all_splits)
# 地板除,向下取整
third = total_documents // 3
for 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
rust
{'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx',
'section': 'beginning'}
我们将需要更新向量存储中的文档。我们将为此使用简单的 InMemoryVectorStore
,因为我们将使用它的一些特定功能(即,元数据过滤)。
python
from langchain_core.vectorstores import InMemoryVectorStore
vector_store = InMemoryVectorStore(embeddings)
_ = vector_store.add_documents(all_splits)
接下来,让我们为搜索查询定义一个模式。我们将为此目的使用结构化输出。在这里,我们将查询定义为包含一个字符串查询和一个文档部分("开头"、"中间"或"结尾")。
python
from typing import Literal
from typing_extensions import Annotated
class Search(TypedDict):
"""Search query."""
query: Annotated[str, ..., "Search query to run."]
section: Annotated[
Literal["beginning", "middle", "end"],
...,
"Section to query.",
]
最后,我们向 LangGraph
应用程序添加一个步骤,以从用户的原始输入生成查询
python
class State(TypedDict):
question: str
query: Search
context: List[Document]
answer: str
def 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()
python
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))

我们可以通过专门要求从文章末尾获取上下文来测试我们的实现。请注意,模型在其答案中包含不同的信息。
python
for step in graph.stream(
{"question": "文章结尾表达了什么内容?"},
stream_mode="updates",
):
print(f"{step}\n\n----------------\n")
bash
{'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文章结尾表达了杜甫深切的忧国忧民情怀,由个人苦难升华至对天下寒士的关怀,体现其"安得广厦千万间,大庇天下寒士俱欢颜"的崇高理想。'}}
----------------
完整示例代码如下:
python
from typing import Literal
import 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 // 3
for 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: str
query: Search
context: List[Document]
answer: str
def 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()
python
result = graph.invoke({"question": "文章中间介绍了什么?"})
print(f'Context: {result["context"]}\n\n')
print(f'Answer: {result["answer"]}')
bash
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 自主权来执行多个检索步骤。
八、链
让我们首先回顾一下我们在第一部分中构建的向量存储,它索引了 古诗词网站
的一篇 古诗《茅屋为秋风所破歌》。
python
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)
python
# Index chunks
_ = vector_store.add_documents(documents=all_splits)
在 RAG 教程的第一部分中,我们将用户输入、检索到的上下文和生成的答案表示为状态中的单独键。对话式体验可以使用一系列消息自然地表示。除了来自用户和助手的信息外,检索到的文档和其他工件可以通过工具消息合并到消息序列中。这促使我们使用消息序列来表示 RAG 应用程序的状态。具体来说,我们将有:
- 用户输入作为
HumanMessage
; - 向量存储查询作为带有工具调用的
AIMessage
; - 检索到的文档作为
ToolMessage
; - 最终响应作为
AIMessage
。
这种状态模型非常通用,LangGraph
提供了内置版本以方便使用:
python
from langgraph.graph import MessagesState, StateGraph
graph_builder = StateGraph(MessagesState)
利用工具调用与检索步骤交互还有另一个好处,那就是检索的查询是由我们的模型生成的。这在对话设置中尤其重要,在对话设置中,用户查询可能需要根据聊天历史记录进行情境化。例如,考虑以下交流:
- 用户:"什么是任务分解?"
- AI:"任务分解涉及将复杂任务分解为更小更简单的步骤,以使 Agent 或模型更容易管理。"
- 用户:"有哪些常见的方法?"
在这种情况下,模型可以生成诸如 "任务分解的常用方法" 之类的查询。工具调用自然地促进了这一点。正如 RAG 教程的查询分析部分中所述,这允许模型将用户查询重写为更有效的搜索查询。它还支持不涉及检索步骤的直接响应(例如,响应来自用户的通用问候)。
让我们将检索步骤转换为工具:
python
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
添加到状态。
python
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 overwriting
return {"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 ToolMessages
recent_tool_messages = []
for message in reversed(state["messages"]):
if message.type == "tool":
recent_tool_messages.append(message)
else:
break
tool_messages = recent_tool_messages[::-1]
# Format into prompt
docs_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 = [
message
for 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
# Run
response = llm.invoke(prompt)
return {"messages": [response]}
最后,我们将我们的应用程序编译成一个单独的 graph
对象。在本例中,我们只是将步骤连接成一个序列。我们还允许第一个 query_or_respond
步骤"短路",并在不生成工具调用的情况下直接响应用户。这使我们的应用程序能够支持对话式体验------例如,响应可能不需要检索步骤的通用问候。
python
from langgraph.graph import END
from langgraph.prebuilt import ToolNode, tools_condition
graph_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()
python
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))

测试:
python
input_message = "你好"
for step in graph.stream(
{"messages": [{"role": "user", "content": input_message}]},
stream_mode="values",
):
step["messages"][-1].pretty_print()
ini
================================ Human Message =================================
你好
================================== Ai Message ==================================
你好!有什么可以帮助你的吗?
当执行搜索时,我们可以流式传输步骤以观察查询生成、检索和答案生成:
python
input_message = "茅屋为秋风所破歌这首古诗的创作背景是什么时候?"
for step in graph.stream(
{"messages": [{"role": "user", "content": input_message}]},
stream_mode="values",
):
step["messages"][-1].pretty_print()
yaml
================================ Human Message =================================
茅屋为秋风所破歌这首古诗的创作背景是什么时候?
================================== Ai Message ==================================
Tool Calls:
retrieve (0196ce3d93f1ce354cd790063fa1956c)
Call ID: 0196ce3d93f1ce354cd790063fa1956c
Args:
query: 茅屋为秋风所破歌 创作背景
================================= Tool Message =================================
Name: retrieve
Source: {'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}
Content: 简析
《茅屋为秋风所破歌》是一首歌行体古诗。此诗叙述了诗人的茅屋被秋风所破以致全家遭雨淋的痛苦经历,他从中生出万千感慨,体现了忧国忧民的思想情感,是杜诗中的典范之作。这首诗叙事与抒情并重,叙事部分大篇幅写实,诗人诉述自家之苦,情绪含蓄压抑;而后的抒情部分将苦难加以升华,直抒忧民之情,情绪激越轩昂,展现出诗人的崇高理想和境界。叙事部分的有力铺垫为全诗最后的言志奠定了坚实的基础,如此抑扬曲折的情绪变换,充分体现出杜诗"沉郁顿挫"的风格。
杜甫(712-770),字子美,自号少陵野老,世称"杜工部"、"杜少陵"等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为"诗圣",其诗被称为"诗史"。杜甫与李白合称"李杜",为了跟另外两位诗人李商隐与杜牧即"小李杜"区别开来,杜甫与李白又合称"大李杜"。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念。► 1338篇诗文 ► 2728条名句
================================== Ai Message ==================================
《茅屋为秋风所破歌》创作于唐代安史之乱后,杜甫寓居成都期间(约760年左右)。此时期杜甫生活困顿,因战乱流离失所,诗中通过描写自身茅屋被秋风吹破的遭遇,抒发了对民生疾苦的深切关怀。该诗反映了杜甫在困顿中仍抱有"安得广厦千万间"的济世理想,体现了其"沉郁顿挫"的诗风。
九、聊天记录的状态管理
在生产环境中,问答应用程序通常会将聊天记录持久化到数据库中,并且能够适当地读取和更新它。
LangGraph
实现了内置的 持久化层
,使其成为支持多轮对话的聊天应用程序的理想选择。
要管理多轮对话和线程,我们所要做的就是在编译我们的应用程序时指定一个 检查点。由于我们图中的节点正在将消息附加到状态,因此我们将在多次调用中保持一致的聊天记录。
LangGraph
配备了一个简单的内存中检查点,我们在下面使用它。
python
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
# Specify an ID for the thread
config = {"configurable": {"thread_id": "abc123"}}
我们现在可以像以前一样调用
python
input_message = "茅屋为秋风所破歌这首古诗的创作背景是什么时候?"
for step in graph.stream(
{"messages": [{"role": "user", "content": input_message}]},
stream_mode="values",
config=config,
):
step["messages"][-1].pretty_print()
yaml
================================ Human Message =================================
茅屋为秋风所破歌这首古诗的创作背景是什么时候?
================================== Ai Message ==================================
Tool Calls:
retrieve (0196ce743c01f68656e9b5e68fd56443)
Call ID: 0196ce743c01f68656e9b5e68fd56443
Args:
query: 茅屋为秋风所破歌 创作背景
================================= Tool Message =================================
Name: retrieve
Source: {'source': 'https://www.gushiwen.cn/shiwenv_8e9ecc95d6a4.aspx'}
Content: 简析
《茅屋为秋风所破歌》是一首歌行体古诗。此诗叙述了诗人的茅屋被秋风所破以致全家遭雨淋的痛苦经历,他从中生出万千感慨,体现了忧国忧民的思想情感,是杜诗中的典范之作。这首诗叙事与抒情并重,叙事部分大篇幅写实,诗人诉述自家之苦,情绪含蓄压抑;而后的抒情部分将苦难加以升华,直抒忧民之情,情绪激越轩昂,展现出诗人的崇高理想和境界。叙事部分的有力铺垫为全诗最后的言志奠定了坚实的基础,如此抑扬曲折的情绪变换,充分体现出杜诗"沉郁顿挫"的风格。
杜甫(712-770),字子美,自号少陵野老,世称"杜工部"、"杜少陵"等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为"诗圣",其诗被称为"诗史"。杜甫与李白合称"李杜",为了跟另外两位诗人李商隐与杜牧即"小李杜"区别开来,杜甫与李白又合称"大李杜"。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念。► 1338篇诗文 ► 2728条名句
================================== Ai Message ==================================
《茅屋为秋风所破歌》创作于唐代宗大历二年(767年)前后,杜甫流寓成都期间。此时他居住的草屋因秋风破败,生活困顿,借此抒发对民生疾苦的深切关怀,体现了安史之乱后社会动荡背景下诗人的忧国忧民情怀。
python
input_message = "期间还创作了什么其他古诗?"
for step in graph.stream(
{"messages": [{"role": "user", "content": input_message}]},
stream_mode="values",
config=config,
):
step["messages"][-1].pretty_print()
ini
================================ Human Message =================================
期间还创作了什么其他古诗?
================================== Ai Message ==================================
Tool Calls:
retrieve (0196ce76200f4e785113ac2ae97eee5a)
Call ID: 0196ce76200f4e785113ac2ae97eee5a
Args:
query:
================================= Tool Message =================================
Name: retrieve
Source: {'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
构造器,我们可以在一行中完成此操作。
python
from langgraph.prebuilt import create_react_agent
agent_executor = create_react_agent(llm, [retrieve], checkpointer=memory)
让我们检查一下图
python
from IPython.display import Image, display
display(Image(agent_executor.get_graph().draw_mermaid_png()))

与我们之前的实现的主要区别在于,这里的工具调用循环回到原始 LLM 调用,而不是结束运行的最终生成步骤。然后,模型可以使用检索到的上下文回答问题,或者生成另一个工具调用以获取更多信息。
让我们测试一下。我们构建一个通常需要迭代检索步骤序列才能回答的问题:
python
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()
markdown
================================ Human Message =================================
茅屋为秋风所破歌这首古诗的创作背景是什么时候?
一旦你得到答案,查找该古诗表达了作者什么情感。
================================== Ai Message ==================================
《茅屋为秋风所破歌》的创作背景与杜甫的个人经历及唐代社会现状密切相关:
---
### **一、创作背景**
1. **时间**:
此诗创作于**唐肃宗上元元年(760年)秋**,是杜甫晚年的重要作品之一。
此时杜甫寓居成都(今四川省成都市),在友人严武的帮助下,在城西浣花溪畔建成"茅屋"(即"杜甫草堂")。
- **历史背景**:安史之乱(755-763年)后,唐朝社会动荡,民生凋敝,诗人亲身经历战乱离散,目睹百姓苦难。
2. **个人境遇**:
- 茅屋因秋风破败,屋漏雨湿,诗人一家生活困顿。
- 村民趁其年老体弱,抢夺茅草("南村群童欺我老无力"),进一步加剧困苦。
- 这种生活困境成为诗人心中"个人苦难"的缩影。
---
### **二、表达的情感**
此诗通过叙事与抒情结合,表达了杜甫复杂而深刻的情感:
1. **个人悲苦**:
- 描写茅屋破败、风雨交加的实景(如"布衾多年冷似铁""长夜沾湿何由彻"),展现诗人对自身处境的无奈与辛酸。
2. **社会关怀**:
- 从个人困境转向对天下苍生的悲悯("安得广厦千万间,大庇天下寒士俱欢颜"),体现杜甫"**穷则独善其身,达则兼济天下**"的理想。
- 通过"风雨不动安如山"的比喻,将个人苦难与社会动荡联系,隐含对国家安定的渴望。
3. **忧国忧民情怀**:
- 诗中"**朱门酒肉臭,路有冻死骨**"(虽非本诗,但同属杜甫现实主义风格)的批判精神贯穿其作品,此诗亦通过"茅屋"象征百姓疾苦,反映战乱对民生的摧残。
---
### **总结**
这首诗以个人遭遇为切入点,既抒发了杜甫对自身困顿的哀叹,更升华至对天下寒士的深切同情,最终以"**安得广厦千万间**"的呐喊,彰显其"**诗史**"般的现实主义精神与忧国忧民的情怀。
请注意,Agent 实际的步骤:
- 生成查询 以 搜索 第一个问题的答案;
- 接收到答案后,生成第二个查询以搜索第二个问题的答案;
- 在收到所有必要的上下文后,回答问题。