建立聊天機器人
本教學先前使用 RunnableWithMessageHistory 抽象化。您可以在 v0.2 文件中存取該版本的文件。
自 LangChain v0.3 版本發布以來,我們建議 LangChain 使用者利用 LangGraph 持久性,將 memory
整合到新的 LangChain 應用程式中。
如果您的程式碼已經依賴 RunnableWithMessageHistory
或 BaseChatMessageHistory
,則不需要進行任何變更。我們不打算在近期內棄用此功能,因為它適用於簡單的聊天應用程式,並且任何使用 RunnableWithMessageHistory
的程式碼都將繼續按預期工作。
請參閱 如何遷移到 LangGraph 記憶體 以取得更多詳細資訊。
概述
我們將介紹如何設計和實作由 LLM 驅動的聊天機器人的範例。此聊天機器人將能夠進行對話,並記住與 聊天模型 的先前互動。
請注意,我們建立的這個聊天機器人僅使用語言模型進行對話。您可能正在尋找其他幾個相關概念
本教學將涵蓋基礎知識,這對於這兩個更進階的主題很有幫助,但如果您選擇,可以隨時直接跳到那裡。
設定
Jupyter Notebook
本指南(以及文件中大多數其他指南)使用 Jupyter Notebook,並假設讀者也是如此。Jupyter Notebook 非常適合學習如何使用 LLM 系統,因為通常情況下可能會出錯(意外輸出、API 關閉等),並且在互動式環境中閱讀指南是更好地理解它們的好方法。
本教學和其他教學最方便在 Jupyter Notebook 中執行。請參閱 此處 以取得安裝說明。
安裝
在本教學中,我們需要 langchain-core
和 langgraph
。本指南需要 langgraph >= 0.2.28
。
- Pip
- Conda
pip install langchain-core langgraph>0.2.27
conda install langchain-core langgraph>0.2.27 -c conda-forge
如需更多詳細資訊,請參閱我們的 安裝指南。
LangSmith
您使用 LangChain 建構的許多應用程式將包含多個步驟,其中包含多個 LLM 調用。隨著這些應用程式變得越來越複雜,能夠檢查您的鏈或代理內部究竟發生了什麼變得至關重要。執行此操作的最佳方法是使用 LangSmith。
在上面的連結註冊後,請確保設定您的環境變數以開始記錄追蹤
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."
或者,如果在 Notebook 中,您可以使用以下程式碼設定它們
import getpass
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = getpass.getpass()
快速入門
首先,讓我們學習如何單獨使用語言模型。LangChain 支援許多不同的語言模型,您可以互換使用 - 在下面選擇您想要使用的模型!
pip install -qU "langchain[openai]"
import getpass
import os
if not os.environ.get("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter API key for OpenAI: ")
from langchain.chat_models import init_chat_model
model = init_chat_model("gpt-4o-mini", model_provider="openai")
讓我們首先直接使用模型。ChatModel
是 LangChain "可執行物件" 的實例,這表示它們公開了一個用於與它們互動的標準介面。為了簡單地調用模型,我們可以將訊息列表傳遞給 .invoke
方法。
from langchain_core.messages import HumanMessage
model.invoke([HumanMessage(content="Hi! I'm Bob")])
AIMessage(content='Hi Bob! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 11, 'total_tokens': 21, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-5211544f-da9f-4325-8b8e-b3d92b2fc71a-0', usage_metadata={'input_tokens': 11, 'output_tokens': 10, 'total_tokens': 21, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
模型本身沒有任何狀態的概念。例如,如果您提出後續問題
model.invoke([HumanMessage(content="What's my name?")])
AIMessage(content="I'm sorry, but I don't have access to personal information about users unless it has been shared with me in the course of our conversation. How can I assist you today?", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 11, 'total_tokens': 45, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-a2d13a18-7022-4784-b54f-f85c097d1075-0', usage_metadata={'input_tokens': 11, 'output_tokens': 34, 'total_tokens': 45, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
讓我們看一下 LangSmith 追蹤範例
我們可以看到它沒有將先前的對話回合納入上下文,並且無法回答問題。這造成了糟糕的聊天機器人體驗!
為了繞過這個問題,我們需要將整個 對話歷史記錄 傳遞到模型中。讓我們看看當我們這樣做時會發生什麼
from langchain_core.messages import AIMessage
model.invoke(
[
HumanMessage(content="Hi! I'm Bob"),
AIMessage(content="Hello Bob! How can I assist you today?"),
HumanMessage(content="What's my name?"),
]
)
AIMessage(content='Your name is Bob! How can I help you today, Bob?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 33, 'total_tokens': 47, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-34bcccb3-446e-42f2-b1de-52c09936c02c-0', usage_metadata={'input_tokens': 33, 'output_tokens': 14, 'total_tokens': 47, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
現在我們可以看見我們得到了良好的回應!
這是聊天機器人對話互動能力背後的基本概念。那麼我們如何最好地實作它呢?
訊息持久性
LangGraph 實作了內建的持久層,使其成為支援多個對話回合的聊天應用程式的理想選擇。
將我們的聊天模型封裝在最小的 LangGraph 應用程式中,使我們能夠自動持久化訊息歷史記錄,簡化多回合應用程式的開發。
LangGraph 附帶一個簡單的記憶體檢查點,我們在下面使用它。請參閱其 文件 以取得更多詳細資訊,包括如何使用不同的持久性後端(例如,SQLite 或 Postgres)。
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, MessagesState, StateGraph
# Define a new graph
workflow = StateGraph(state_schema=MessagesState)
# Define the function that calls the model
def call_model(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": response}
# Define the (single) node in the graph
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
# Add memory
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
我們現在需要建立一個 config
,我們每次都將其傳遞到可執行物件中。此 config 包含不直接屬於輸入的一部分的資訊,但仍然很有用。在這種情況下,我們想要包含一個 thread_id
。它應該看起來像這樣
config = {"configurable": {"thread_id": "abc123"}}
這使我們能夠使用單個應用程式支援多個對話線程,這是您的應用程式有多個使用者時的常見需求。
然後我們可以調用應用程式
query = "Hi! I'm Bob."
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print() # output contains all messages in state
==================================[1m Ai Message [0m==================================
Hi Bob! How can I assist you today?
query = "What's my name?"
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
Your name is Bob! How can I help you today, Bob?
太棒了!我們的聊天機器人現在記住關於我們的訊息。如果我們變更 config 以參考不同的 thread_id
,我們可以看見它重新開始對話。
config = {"configurable": {"thread_id": "abc234"}}
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
I'm sorry, but I don't have access to personal information about you unless you've shared it in this conversation. How can I assist you today?
但是,我們始終可以返回到原始對話(因為我們將其持久化在資料庫中)
config = {"configurable": {"thread_id": "abc123"}}
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
Your name is Bob. What would you like to discuss today?
這就是我們如何支援聊天機器人與許多使用者進行對話的方式!
對於非同步支援,請更新 call_model
節點以成為非同步函數,並在調用應用程式時使用 .ainvoke
# Async function for node:
async def call_model(state: MessagesState):
response = await model.ainvoke(state["messages"])
return {"messages": response}
# Define graph as before:
workflow = StateGraph(state_schema=MessagesState)
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
app = workflow.compile(checkpointer=MemorySaver())
# Async invocation:
output = await app.ainvoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
目前,我們所做的只是在模型周圍添加一個簡單的持久層。我們可以透過添加提示範本來開始使聊天機器人更複雜和個人化。
提示範本
提示範本 有助於將原始使用者資訊轉換為 LLM 可以使用的格式。在這種情況下,原始使用者輸入只是一條訊息,我們將其傳遞給 LLM。現在讓我們使其稍微複雜一點。首先,讓我們添加一個包含一些自訂指示的系統訊息(但仍然將訊息作為輸入)。接下來,除了訊息之外,我們還將添加更多輸入。
為了添加系統訊息,我們將建立一個 ChatPromptTemplate
。我們將利用 MessagesPlaceholder
傳遞所有訊息。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt_template = ChatPromptTemplate.from_messages(
[
(
"system",
"You talk like a pirate. Answer all questions to the best of your ability.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
我們現在可以更新我們的應用程式以整合此範本
workflow = StateGraph(state_schema=MessagesState)
def call_model(state: MessagesState):
prompt = prompt_template.invoke(state)
response = model.invoke(prompt)
return {"messages": response}
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
我們以相同的方式調用應用程式
config = {"configurable": {"thread_id": "abc345"}}
query = "Hi! I'm Jim."
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
Ahoy there, Jim! What brings ye to these waters today? Be ye seekin' treasure, knowledge, or perhaps a good tale from the high seas? Arrr!
query = "What is my name?"
input_messages = [HumanMessage(query)]
output = app.invoke({"messages": input_messages}, config)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
Ye be called Jim, matey! A fine name fer a swashbuckler such as yerself! What else can I do fer ye? Arrr!
太棒了!現在讓我們使我們的提示稍微複雜一點。假設提示範本現在看起來像這樣
prompt_template = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful assistant. Answer all questions to the best of your ability in {language}.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
請注意,我們已在提示中新增了一個新的 language
輸入。我們的應用程式現在有兩個參數 - 輸入 messages
和 language
。我們應該更新我們應用程式的狀態以反映這一點
from typing import Sequence
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
from typing_extensions import Annotated, TypedDict
class State(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
language: str
workflow = StateGraph(state_schema=State)
def call_model(state: State):
prompt = prompt_template.invoke(state)
response = model.invoke(prompt)
return {"messages": [response]}
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "abc456"}}
query = "Hi! I'm Bob."
language = "Spanish"
input_messages = [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages, "language": language},
config,
)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
¡Hola, Bob! ¿Cómo puedo ayudarte hoy?
請注意,整個狀態都會持久化,因此如果不需要變更,我們可以省略 language
等參數
query = "What is my name?"
input_messages = [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages},
config,
)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
Tu nombre es Bob. ¿Hay algo más en lo que pueda ayudarte?
為了幫助您了解內部發生的情況,請查看 此 LangSmith 追蹤。
管理對話歷史記錄
建立聊天機器人時需要了解的一個重要概念是如何管理對話歷史記錄。如果管理不當,訊息列表將無限增長,並可能溢出 LLM 的上下文視窗。因此,務必新增一個步驟來限制您傳遞的訊息大小。
重要的是,您需要在提示範本之前執行此操作,但在從訊息歷史記錄載入先前訊息之後。
我們可以透過在提示前面新增一個簡單的步驟來完成此操作,該步驟適當地修改 messages
鍵,然後將新的鏈封裝在訊息歷史記錄類別中。
LangChain 附帶了一些內建的輔助程式,用於 管理訊息列表。在這種情況下,我們將使用 trim_messages 輔助程式來減少我們發送到模型的訊息數量。修剪器允許我們指定要保留多少 Token,以及其他參數,例如我們是否要始終保留系統訊息以及是否允許部分訊息
from langchain_core.messages import SystemMessage, trim_messages
trimmer = trim_messages(
max_tokens=65,
strategy="last",
token_counter=model,
include_system=True,
allow_partial=False,
start_on="human",
)
messages = [
SystemMessage(content="you're a good assistant"),
HumanMessage(content="hi! I'm bob"),
AIMessage(content="hi!"),
HumanMessage(content="I like vanilla ice cream"),
AIMessage(content="nice"),
HumanMessage(content="whats 2 + 2"),
AIMessage(content="4"),
HumanMessage(content="thanks"),
AIMessage(content="no problem!"),
HumanMessage(content="having fun?"),
AIMessage(content="yes!"),
]
trimmer.invoke(messages)
[SystemMessage(content="you're a good assistant", additional_kwargs={}, response_metadata={}),
HumanMessage(content='whats 2 + 2', additional_kwargs={}, response_metadata={}),
AIMessage(content='4', additional_kwargs={}, response_metadata={}),
HumanMessage(content='thanks', additional_kwargs={}, response_metadata={}),
AIMessage(content='no problem!', additional_kwargs={}, response_metadata={}),
HumanMessage(content='having fun?', additional_kwargs={}, response_metadata={}),
AIMessage(content='yes!', additional_kwargs={}, response_metadata={})]
為了在我們的鏈中使用它,我們只需要在將 messages
輸入傳遞到我們的提示之前執行修剪器。
workflow = StateGraph(state_schema=State)
def call_model(state: State):
trimmed_messages = trimmer.invoke(state["messages"])
prompt = prompt_template.invoke(
{"messages": trimmed_messages, "language": state["language"]}
)
response = model.invoke(prompt)
return {"messages": [response]}
workflow.add_edge(START, "model")
workflow.add_node("model", call_model)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
現在,如果我們嘗試詢問模型我們的名字,它將不知道,因為我們修剪了聊天歷史記錄的那部分
config = {"configurable": {"thread_id": "abc567"}}
query = "What is my name?"
language = "English"
input_messages = messages + [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages, "language": language},
config,
)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
I don't know your name. You haven't told me yet!
但是,如果我們詢問關於最後幾條訊息中的資訊,它會記住
config = {"configurable": {"thread_id": "abc678"}}
query = "What math problem did I ask?"
language = "English"
input_messages = messages + [HumanMessage(query)]
output = app.invoke(
{"messages": input_messages, "language": language},
config,
)
output["messages"][-1].pretty_print()
==================================[1m Ai Message [0m==================================
You asked what 2 + 2 equals.
如果您查看 LangSmith,您可以在 LangSmith 追蹤中準確地看見幕後發生的情況。
串流
現在我們有一個運作正常的聊天機器人。但是,聊天機器人應用程式的一個非常重要的 UX 考量是串流。LLM 有時可能需要一段時間才能回應,因此為了改善使用者體驗,大多數應用程式執行的一件事是串流回每個產生的 Token。這允許使用者看見進度。
實際上,這樣做非常容易!
預設情況下,我們 LangGraph 應用程式中的 .stream
串流應用程式步驟 - 在這種情況下,是模型回應的單一步驟。設定 stream_mode="messages"
允許我們串流輸出 Token 代替
config = {"configurable": {"thread_id": "abc789"}}
query = "Hi I'm Todd, please tell me a joke."
language = "English"
input_messages = [HumanMessage(query)]
for chunk, metadata in app.stream(
{"messages": input_messages, "language": language},
config,
stream_mode="messages",
):
if isinstance(chunk, AIMessage): # Filter to just model responses
print(chunk.content, end="|")
|Hi| Todd|!| Here|’s| a| joke| for| you|:
|Why| don|’t| skeleton|s| fight| each| other|?
|Because| they| don|’t| have| the| guts|!||
後續步驟
現在您了解了如何在 LangChain 中建立聊天機器人的基礎知識,您可能會對一些更進階的教學感興趣
如果您想更深入地了解具體細節,則值得查看的一些事項是
- 串流:串流對於聊天應用程式至關重要
- 如何添加訊息歷史記錄:深入探討與訊息歷史記錄相關的所有事項
- 如何管理大型訊息歷史記錄:管理大型聊天歷史記錄的更多技術
- LangGraph 主要文件:有關使用 LangGraph 建構的更多詳細資訊