到目前为止,我们已经探讨了一些LLM的基础知识——如Transformer、提示工程和一些流行的开源模型。在本章中,我们将深入探讨一些工具,这些工具允许你使用这些模型构建完整的系统——这将使我们能够超越与模型的简单聊天互动,建立能够从外部来源检索信息、执行各种应用程序、记住你与模型的个人互动历史,并根据用户特定的文档集合定制结果的互联系统。为了实现这一点,我们需要将文档存储在向量数据库中,从这些存储中检索相关文档以增强提示的上下文,将专门为特定任务定制的模型链接为“代理”,并记录我们的实验结果。在构建这些“代理”系统的过程中,我们还将探讨如何分析它们的输出并监控它们与用户的互动。
简而言之,本章将涵盖以下主题:
- LangChain生态系统
- 构建一个简单的LLM应用程序
- 使用LangGraph创建复杂的应用程序
让我们开始吧!
本章中展示的完整代码可以在我们的GitHub仓库中找到:github.com/PacktPublis…
LangChain生态系统
我们将讨论的主要LLM工具箱库是LangChain。LangChain(python.langchain.com/)是一套用于构建LLM应用程序的工具。这些工具包括核心的LangChain函数,允许你使用LLM构建应用程序,其中包括用于检索增强生成(RAG)的向量数据库,用于存储嵌入文档;LangSmith,它方便日志记录;以及LangGraph,提供构建代理的工具,代理代表用户执行命令,并支持在代理响应之间实现“记忆”。这个整体生态系统如图8.1所示:
我们在使用LangChain进行实验的第一步是设置一个LangSmith账户,以便跟踪我们的实验进展。
首先,我们需要创建一个账户,以便通过API密钥使用LangChain及其日志记录组件LangSmith。前往 www.langchain.com/langsmith 创建一个账户。
接下来,我们需要创建一个API密钥(图8.2),该密钥将允许我们在构建模型应用时记录输出。务必复制这个密钥,因为稍后你将需要它,并且出于安全原因,创建后你将无法再次访问该密钥。
我们稍后会回到仪表盘层查看LangSmith中的可用工具,但现在,让我们先创建我们的第一个“链”。
构建一个简单的LLM应用程序
为了开始使用LangChain构建应用程序,我们首先需要安装该库和所需的依赖项:
pip install -U langchain langchain-mistralai FastAPI langserve sse_starlette nest-asyncio pyngrok uvicorn
在这个例子中,我们将使用Mistral AI的一个模型;我们需要创建一个API密钥,在后续的代码中使用。你可以在图8.3所示的页面 console.mistral.ai/api-keys/ 上创建该密钥:
最后,我们希望能够在Python服务器中查看计算结果,因此我们将使用ngrok平台来托管我们的LLM应用程序。你可以在dashboard.ngrok.com/get-started…(图8.4)上创建一个ngrok账户;稍后你将需要一个令牌来提供你的应用程序服务。
现在我们已经获取了所有需要的令牌,接下来将它们设置为我们环境中的配置参数:
import os
os.environ["LANGCHAIN_TRACING"] = "true"
os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
os.environ["LANGCHAIN_API_KEY"] = "xxxxxxxx"
os.environ["MISTRAL_API_KEY"] = "xxxxxxxx"
作为第一步,我们将使用我们的Mistral账户(或选择其他LLM)来创建一个模型。如果你使用的是支持LangChain的其他模型,只需在脚本开始时更改导入的模块:
from langchain_mistralai import ChatMistralAI
model = ChatMistralAI(model="mistral-large-latest")
作为一个简单的例子,让我们创建一个LLM应用程序,它接受一条消息并将其翻译为目标语言。为此,我们将指定一个SystemMessage(构成模板或背景指令的提示内容)和一个HumanMessage(从用户获得的内容——这里是我们想要翻译的短语):
from langchain_core.messages import HumanMessage, SystemMessage
messages = [
SystemMessage(content="Translate the following from English into Italian"),
HumanMessage(content="hi!"),
]
model.invoke(messages)
如果我们在这个例子中调用invoke,你可以看到模型的输出:
AIMessage(content='Ciao!\n\nHere are a few other translations for "hi" in Italian:\n\n* Salve! (Formal)\n* Buongiorno! (Good morning)\n* Buon pomeriggio! (Good afternoon)\n* Buonasera! (Good evening)\n* Ehi! (Hey!)', additional_kwargs={}, response_metadata={'token_usage': {'prompt_tokens': 15, 'total_tokens': 87, 'completion_tokens': 72}, 'model': 'mistral-large-latest', 'finish_reason': 'stop'}, id='run-181f0a6c-20c0-4456-889f-da5c83dccfe9-0', usage_metadata={'input_tokens': 15, 'output_tokens': 72, 'total_tokens': 87})
这里有很多有用的信息:我们得到了实际的答案(content),关于令牌使用的信息(如果我们为每个令牌付费,这可能非常重要),输出大小,以及我们随提示发送的任何附加信息。所有这些信息都包含在一个AIMessage对象中,后续可以通过LangSmith存储以供回顾,正如我们将在本章的下一部分看到的那样。
现在我们已经创建了一个基本的LLM,让我们看看如何将这个模型与LangChain库的其他组件结合起来,形成一个完整的应用程序。这个“链”就是该库名称的来源。
创建LLM链
让我们在前面的例子基础上,创建一个完整的链。我们将再次创建一个提示模板,这次我们将允许用户输入一个语言。这里我们需要一些关键的组件,首先是FastAPI框架,用于构建Web应用程序,和Uvicorn服务器,在我们开发完成后,用来部署该应用程序;我们将通过add_routes函数在特定的URL上将模型暴露给最终用户。我们还需要PromptTemplate,它允许我们指定模型如何读取用户输入以及期望哪些变量,以及StrOutputParser,它将LLM的“message”对象转换为字符串。我们将使用nest_asyncio模块,在Colab笔记本的进程线程中运行我们的FastAPI应用程序。
我们还需要chain模块,将不同的LangChain函数连接成一系列顺序步骤。最后,我们将导入Mistral模型API来进行这个例子,但理论上,我们可以使用LangChain库中的任何LLM:
from fastapi import FastAPI
import uvicorn
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_mistralai import ChatMistralAI
from langserve import add_routes
import nest_asyncio
from langchain_core.runnables import chain
nest_asyncio.apply()
首先,我们将声明提示模板(我们将展示给用户的文本)和系统模板(自动提供给模型的文本,与用户输入一起)。请注意,在提示模板中,我们在{}中放置了一个占位符,用来表示需要提供的文本:
# Step 1: Create prompt template
system_template = "Translate the following into {language}:"
prompt_template = ChatPromptTemplate.from_messages([
('system', system_template),
('user', '{text}')
])
接下来,我们将创建模型:
# Step 2: Create model
model = ChatMistralAI(model="mistral-large-latest")
我们还需要一个解析器,将AIMessage对象转换为文本:
# Step 3: Create parser
parser = StrOutputParser()
现在,我们可以创建我们的链。在LangChain库中,个别操作可以使用管道操作符(|)在管道或链中创建。函数将从左到右执行,序列可以保存到变量中:
# Step 4: Create chain
chain = prompt_template | model | parser
这个链将接受一个提示模板(系统指令和用户输入的组合),在该输入上运行模型,并将输出解析为文本。现在我们已经定义了我们希望与LLM一起运行的操作顺序,让我们设置一个应用程序,将其托管在一个用户友好的界面上。
创建LLM应用程序
如果我们想将我们的链托管在云端,可以创建一个简单的FastAPI服务器:
# Step 5: App definition
app = FastAPI(
title="LangChain Server",
version="1.0",
description="A simple API server using LangChain's Runnable interfaces",
)
我们还将添加一个端点,这是网站上的子页面,用户可以在此访问LLM。如果我们在复杂的应用中托管多个不同的LLM页面,这会非常有用:
# Step 6: Adding chain route
add_routes(
app,
chain,
path="/chain",
)
接下来,我们需要使用ngrok来设置一个URL,部署后可以访问我们的服务器:
首先,我们将从我们的账户添加ngrok认证令牌到配置文件:
!ngrok config add-authtoken xxxxxxx
现在,我们可以将ngrok端点添加到端口8000上,并在公共URL上打印位置:
from pyngrok import ngrok
ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
现在,我们只需要在uvicorn Web服务器上启动我们的FastAPI应用程序。请注意,我们正在使用与刚才通过ngrok暴露的端口8000相同的端口:
if __name__ == "__main__":
uvicorn.run(app, host='0.0.0.0', port=8000, log_level="debug")
执行此代码后,你将拥有一个全新的LLM应用程序,通过上述ngrok URL访问,并且位于/chain/playground子页面,页面应该类似于图8.5所示。
现在我们已经将应用程序启动并运行起来,让我们看看如何使用它并记录结果。
将LLM结果记录到LangSmith
如果我们输入一个目标语言和一个我们想翻译的短语并点击“开始”,Mistral将返回一个响应,该响应将通过我们之前设置的链进行解析,如图8.6所示:
如果我们进入我们的LangSmith页面,我们会看到有一个默认账户,其中保存了到目前为止我们实验的结果:
如果我们查看默认项目,我们会看到我们对LLM进行的两次调用:第一次是使用Mistral模型在初始代码中翻译意大利语短语(在下面LangSmith界面表格中的ChatMistralAI),第二次是我们在用户界面中显示的法语翻译(在下面的表格中的/chain)。
如果我们点击进入/chain条目,该条目包含来自我们Web应用程序的输入,我们将看到一些有用的模型输入和输出日志:
到目前为止,我们已经创建了一个基本的翻译应用程序,将一个在Google Colab本地运行的服务器部署到一个公共端点,并浏览了LangSmith中的日志结果。
接下来,让我们在此应用程序的基础上进行扩展,使其更复杂:我们将添加一些功能,通过LangGraph库展示LangChain的文档嵌入和代理功能。
使用LangGraph创建复杂的应用程序
现在我们已经制作了一个基本的翻译应用程序,用户提供一个模板化的提示答案,LLM提供翻译。在下一个例子中,我们将以几个关键方式扩展这个框架,设计一个问答应用程序,将多个重要功能串联起来:
- 我们将启用通过聊天机器人进行开放式对话
- 我们将使用向量数据库从内部存储中检索与查询相关的文档
- 我们将添加一个记忆功能,使机器人能够跟踪与我们的互动
- 我们将提供一个人类反馈环节,允许用户提供反馈
- 我们将提供根据提示在互联网上寻找额外内容的能力
通过这些功能,我们将从指定一个链(命令按线性顺序处理)转向构建图(LLM输出用于决定在复杂过程中选择哪个分支)。LangChain中的LangGraph模块允许我们构建这些更复杂的工作流,并将它们托管在我们在简单链示例中看到的相同LangServe基础设施上。我们将展示如何构建每个功能,但首先,让我们先构建我们的聊天机器人前端。
添加聊天界面
我们界面的第一步将是与聊天机器人进行开放式对话,而不是我们刚才使用的模板化翻译示例,其中用户只能提供预定义的输入。
为了定义聊天机器人,我们首先需要定义一个状态(State)——一个容器,用于存储在LLM应用程序的各个组件之间共享的累积消息,允许我们在接收到消息时附加消息,并根据来自用户的最新提示执行操作。
让我们开始定义状态(State)作为一个类,包含一个元素——messages,它包含用户的提示:
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
graph_builder = StateGraph(State)
在这段代码中,我们定义了一个State对象,它是一个字典。它有一个键messages,这个键包含一个列表,并通过add_messages函数更新,这个函数将消息附加到该列表中。
接着,我们通过调用StateGraph来初始化将承载我们应用程序的图。我们的聊天机器人将是这个图的第一个元素或节点,我们将添加边(edges),将聊天机器人的输出路由到不同的下游任务。我们可以通过以下代码定义聊天机器人:
model = ChatMistralAI(model="mistral-large-latest")
def chatbot(state: State):
return {"messages": model.invoke(state["messages"])}
def input(question):
return {"messages": question}
def output(state: State):
return state["messages"][-1].content
我们像之前一样使用ChatMistral声明模型,并将其封装在一个chatbot函数中,该函数在图状态中的消息上调用模型。我们还将添加一个input函数,将用户的提示传递给聊天机器人,以及一个output函数,用于提取响应。接下来,我们声明图并添加聊天机器人。然后我们定义一个链,其中图是中间元素:
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()
assistant = RunnableLambda(input) | graph | RunnableLambda(output)
然后,我们可以像之前一样运行一个FastAPI应用程序,暴露一个REST API给助手:
app = FastAPI(
title="LangChain Server",
version="1.0",
description="A simple API server using LangChain's Runnable interfaces",
)
add_routes(
app,
assistant.with_types(input_type=str, output_type=str),
path="/assistant",
)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug")
然后,我们通过调用API和你之前声明的ngrok端点来验证这个链的功能:
import requests
result = requests.post(
"https://xxxxxxx.ngrok-free.app/assistant/invoke",
json={"input": "what is langgraph?"}
)
result.content
这为我们提供了一个简单的界面来查询聊天机器人。现在,让我们开始向这个图中添加一些额外的元素,从本地内容数据库开始。
添加用于RAG的向量存储
为了提高我们聊天机器人回答有关LangChain问题的能力,我们可以通过检索相关的代码片段来增强其功能。为此,让我们从GitHub下载LangChain库的内容,将其存储在一个向量数据库中,并在我们的图中添加一个检索步骤。通过将LangChain项目的实际代码存储在我们应用程序中的可访问数据库中,我们将能够检索相关的代码片段,在模型回答我们的问题时为其提供额外的背景信息,从而让它提供更具体和相关的回答。
首先,让我们使用GitLoader获取数据,然后仅筛选出包含Python代码的文件。接着,我们将文件拆分成重叠的块,并使用MistralAIEmbeddings模型进行嵌入,MistralAIEmbeddings将文本转换为可以搜索的数值向量。最后,我们将这些重叠的LangChain源代码向量嵌入添加到我们使用InMemoryVectorStore创建的本地内存向量数据库中:
from git import Repo
from langchain_community.document_loaders import GitLoader
from langchain_core.documents import Document
from langchain_mistralai import MistralAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
try:
repo = Repo.clone_from(
"https://github.com/langchain-ai/langchain",
to_path="./langchain"
)
except:
pass
branch = repo.head.reference
loader = GitLoader(
repo_path="./langchain/",
file_filter=lambda file_path: file_path.endswith(".py"),
branch=branch
)
code = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
all_splits = text_splitter.split_documents(code)
embeddings = MistralAIEmbeddings(
model="mistral-large-latest",
timeout=500.0
)
vector_store = InMemoryVectorStore(embeddings)
vector_store.add_documents(all_splits)
最后,当我们的文档被添加到向量存储中后,我们可以修改我们的状态图,以便有一个上下文列表来保存检索到的文档,并在我们向聊天机器人提问关于LangChain的问题时搜索相关的代码片段:
class State(TypedDict):
messages: Annotated[list, add_messages]
content: list
def retrieve(state: State):
retrieved_docs = vector_store.similarity_search(
state["messages"][-1].content
)
return {
"context": retrieved_docs,
"messages": state["messages"]
}
def generate(state: State):
docs_content = "\n\n".join(
doc.page_content for doc in state["context"]
)
response = model.invoke(
state["messages"][-1].content + docs_content
)
return {"messages": response}
def input(question):
return {"messages": question}
def output(state: State):
return state["messages"][-1].content
graph_builder = StateGraph(State)
graph_builder.add_node("retrieve", retrieve)
graph_builder.add_node("generate", generate)
graph_builder.add_edge(START, "retrieve")
graph_builder.add_edge("retrieve", "generate")
graph_builder.add_edge("generate", END)
graph = graph_builder.compile()
添加记忆线程
为了让我们的LLM应用变得更智能,我们需要保持它与我们互动的工作记忆——否则,它将对每个提示都没有任何关于我们之前互动的记忆。例如,它不会记得我们住在哪里或我们的兴趣是什么,这将使得开发可以使用我们个人信息提供更具吸引力和相关响应的LLM助手变得更加困难。如果我们不得不在每次交互中显式地传递这些个性化信息的上下文,而不是通过LangChain的记忆功能“免费”地保持它,这也使得编写个性化应用程序变得更加具有挑战性。记忆功能还允许我们为不同的用户保持不同的记忆,并在不同的“线程”上维护它们,这些线程可以在LangSmith中进行可视化和检索。幸运的是,这个工作记忆很容易添加,如下所示:
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
现在,我们已经添加了记忆检查点,我们可以使用配置在单独的记忆线程上执行LLM应用程序,并将其作为参数传递给invoke函数:
config = {"configurable": {"thread_id": "1"}}
assistant.invoke(
"what are the arguments to the langchain StateGraph constructor? Can you ask a human expert please?",
config
)
如果我们提供给LLM信息,它将在多个请求中保持这个上下文;如果我们切换到一个新线程,它将忘记这个上下文。这个记忆线程的另一个重要方面是,它允许我们暂停和重新启动执行,这将是允许在人类输入的过程中执行图的重要特性。
添加人工中断
为了扩展我们的RAG示例,我们可以添加一个分支,通过将用户问题路由到人工专家或互联网搜索来处理问题。我们先从人工专家开始;我们可以要求应用程序找一个人工专家回答关于LangGraph库功能的问题。
为此,我们需要为我们的模型添加工具,这些工具是模型可以执行的单独函数。LangChain有许多内置工具,包括我们将在下一节中查看的搜索功能。然而,暂时我们将使用@tool装饰器编写我们自己的工具和中断,用于从人工用户获取输入:
from langchain_core.tools import tool
@tool
def user_feedback(question):
"Get user response to results"
human_response = interrupt("")
return {"messages": [human_response["content"]]}
在图执行过程中,如果LLM认为我们正在寻求来自人工用户的信息,它将把图的控制权交给user_feedback函数,该函数会中断执行并等待用户输入。为了将这个工具添加到模型中,我们需要使用以下调用将其绑定:
tool = TavilySearchResults(max_results=2)
tools = [tool, user_feedback]
model_with_tools = model.bind_tools(tools)
TavilySearchResults工具将在下一节讨论,但目前,我们需要将这个工具添加到我们的图中,并在模型推断我们需要人工输入时有条件地执行它:
tool_node = ToolNode(tools=tools)
graph_builder = StateGraph(State)
graph_builder.add_node("retrieve", retrieve)
graph_builder.add_node("generate", generate)
graph_builder.add_edge(START, "retrieve")
graph_builder.add_edge("retrieve", "generate")
graph_builder.add_node("tools", tool_node)
graph_builder.add_conditional_edges(
"generate",
tools_condition,
)
graph_builder.add_edge("tools", "generate")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
请注意,我们再次添加了记忆,它将在展示人工中断的执行时变得重要。为了显示这个图的结构,我们可以使用get_graph将图打印成图片:
display(Image(graph.get_graph().draw_mermaid_png()))
正如你所看到的,RAG节点已经通过一个工具节点进行了增强,该节点在LLM推断提示与其工具之一相关时会有条件地触发。
为了演示人工输入的工作原理,让我们运行一个查询,触发模型请求人工专家的帮助:
config = {"configurable": {"thread_id": "1"}}
events = assistant.stream(
{
"role": "user",
"content": "what are the arguments to the langchain StateGraph constructor? Can you ask a human expert please?"
},
config,
)
for event in events:
print(event)
图在human_response工具上暂停——我们可以通过检查状态来验证这一点,传入配置以便我们可以访问包含此交互记忆的线程,这样我们就可以暂停和恢复:
snapshot = graph.get_state(config)
snapshot.next
现在,如果我们提供一个响应,我们将看到我们的输入与生成的输出结合在一起:
human_response = "The arguments to StateGraph are a and b"
events = graph.stream(Command(resume={"content":human_response}), config)
for event in events:
print(event)
添加搜索功能
除了通过@tool装饰器指定的自定义函数获取人工输入外,LangChain还提供了一个大量的现成工具库,列出了在 python.langchain.com/v0.1/docs/i… 上。我们将特别关注TavilySearch工具,这是一个专为LLM应用设计的搜索引擎。
要使用TavilySearch工具,我们需要获取一个API密钥,您可以通过访问 tavily.com/,创建一个账户,并从图8.11所示的页面复制API密钥来获取:
要在我们的图中使用此功能,我们只需为该密钥设置环境变量:
os.environ["TAVILY_API_KEY"] = "xxxxxxx"
然后,我们将这个搜索工具添加到绑定到LLM的工具中:
from langchain_community.tools.tavily_search import TavilySearchResults
tool = TavilySearchResults(max_results=2)
tools = [tool, user_feedback]
在这里,我们指定了我们希望对查询获取最多2个结果。现在,我们可以像之前一样编译图,但这次,我们将调用一个查询,触发搜索工具:
config = {"configurable": {"thread_id": "1"}}
assistant.invoke(
"what are the arguments to the langchain StateGraph constructor? Can you search the internet please?",
config
)
我们可以在输出中看到,模型现在已经从LangChain的在线文档中检索到关于StateGraph函数的详细信息,正如我们查询的那样。通过上述例子,你可以看到我们是如何从一个简单的LLM聊天交互发展到一个具有分支逻辑的应用程序。根据我们的问题,LLM可以执行不同的工具,从我们之前的交互中检索记忆,并获取其他人类用户的输入。这些动态系统是互动LLM系统的构建模块,能够与人类一起完成日常任务,记住重要的任务特定上下文,并通过执行程序和应用程序响应用户输入,与更广泛的世界互动。
总结
在本章中,我们快速且广泛地介绍了LangChain工具箱。首先,我们创建了一个基本的应用程序,仅接收用户在预定义字段中的输入,并将其部署到Web服务器上。接着,我们使用LangGraph创建了一个开放式聊天,并为模型添加了一个记忆线程,使其能够回忆起我们与它的先前互动信息。
然后,我们扩展了我们的开放式聊天应用程序,加入了RAG查询功能,以检索相关信息并将其包含在我们的提示中。我们从LangChain代码库中下载了这些信息,并将其存储在向量数据库中以进行相似性查找。最后,我们通过LLM增强了我们的RAG应用,添加了条件激活的工具节点,启用了人类参与的输入,并将自动化的Web搜索集成到应用程序中。我们将这些工具部署到FastAPI服务器上,为构建基于LLM的Web互动应用程序奠定了基础。
参考文献
LangChain文档:python.langchain.com/docs/introd…