در این پست یک مثال از نحوه طراحی و پیادهسازی یک چتبات با استفاده از یک LLM
را بررسی خواهیم کرد. این چتبات قادر به انجام مکالمه و به خاطر سپاری تعاملات قبلی است. در نظر داشته باشید که برای تولید مکالمات پیچیده تر میتوانید از قابلیت های عامل (Agent
) و RAG
که از طریق پکیج LangChain
قابل دسترس هستند استفاده کنید.
آماده سازی محیط #
برای این آموزش به langchain-core
و langgraph
نیاز خواهیم داشت:
1pip install langchain langchain-core langgraph>0.2.27 langchain_openai
یا با استفاده از Conda
:
1conda install langchain langchain-core langgraph>0.2.27 langchain_openai -c conda-forge
برای جزئیات بیشتر، به مستندات راهنمای نصب مراجعه کنید.
LangSmith #
بسیاری از برنامههایی که با LangChain
میسازید شامل مراحل متعددی با فراخوانیهای متعدد LLM
هستند. با پیچیدهتر شدن این برنامهها، توانایی بررسی دقیق آنچه در زنجیره یا عامل (Agent
) شما اتفاق میافتد بسیار مهم میشود. بهترین راه برای انجام این کار استفاده از LangSmith است.
پس از ثبتنام در لینک بالا، مطمئن شوید که متغیرهای محیطی خود را برای شروع Tracing
تنظیم کردهاید:
یا، اگر در یک نوتبوک هستید، میتوانید آنها را به این صورت تنظیم کنید:
پیادهسازی #
ابتدا میخواهیم یاد بگیریم که چگونه از یک مدل زبانی به تنهایی برای پیاده سازی چتبات استفاده کنیم. LangChain
از مدلهای زبانی مختلفی پشتیبانی میکند که میتوانید به صورت متناوب از آنها استفاده کنید - مدل مورد نظر خود را در زیر انتخاب کنید!
برای اجرای کدهای زیر ابتدا باید یک کلید API را از طریق پنل کاربری گیلاس تولید کنید. برای این کار ابتدا یک حساب کاربری جدید بسازید یا اگر صاحب حساب کاربری هستید وارد پنل کاربری خود شوید. سپس، به صفحه کلید API بروید و با کلیک روی دکمه “ساخت کلید API” یک کلید جدید برای دسترسی به Gilas API بسازید.
1from langchain_openai import ChatOpenAI
2
3model = ChatOpenAI(
4 api_key="GILAS_API_KEY",
5 base_url="https://api.gilas.io/v1/",
6 model="gpt-4o-mini")
بیایید ابتدا مدل را مستقیماً استفاده کنیم. ChatModel
ها نمونههایی از “Runnables” LangChain
هستند، به این معنی که یک رابط استاندارد برای تعامل با آنها ارائه میدهند. برای فراخوانی ساده مدل، میتوانیم لیستی از پیامها را به متود .invoke
ارسال کنیم.
1from langchain_core.messages import HumanMessage
2
3model.invoke([HumanMessage(content="Hi! I'm Bob")])
1AIMessage(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': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_1bb46167f9', 'finish_reason': 'stop', 'logprobs': None}, id='run-149994c0-d958-49bb-9a9d-df911baea29f-0', usage_metadata={'input_tokens': 11, 'output_tokens': 10, 'total_tokens': 21})
مدل به تنهایی هیچ درکی از state
ندارد یا به عبارت دیگر هر درخواست به مدل به صورت state-less
پردازش میشود. به عنوان مثال، اگر یک سوال پیگیری بپرسید:
1model.invoke([HumanMessage(content="What's my name?")])
1AIMessage(content="I'm sorry, but I don't have access to personal information about individuals unless you've shared it with me in this conversation. How can I assist you today?", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 30, 'prompt_tokens': 11, 'total_tokens': 41, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_1bb46167f9', 'finish_reason': 'stop', 'logprobs': None}, id='run-0ecab57c-728d-4fd1-845c-394a62df8e13-0', usage_metadata={'input_tokens': 11, 'output_tokens': 30, 'total_tokens': 41})
میتوانیم ببینیم که چت بات پیغامهای قبلی را در نظر نمیگیرد و نمیتواند به سوال پاسخ دهد. برای حل این مشکل، باید کل تاریخچه مکالمه را به مدل منتقل کنیم. بیایید ببینیم وقتی این کار را انجام میدهیم چه اتفاقی میافتد:
1from langchain_core.messages import AIMessage
2
3model.invoke(
4 [
5 HumanMessage(content="Hi! I'm Bob"),
6 AIMessage(content="Hello Bob! How can I assist you today?"),
7 HumanMessage(content="What's my name?"),
8 ]
9)
1AIMessage(content='Your name is Bob! How can I help you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 12, 'prompt_tokens': 33, 'total_tokens': 45, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_1bb46167f9', 'finish_reason': 'stop', 'logprobs': None}, id='run-c164c5a1-d85f-46ee-ba8a-bb511cfb0e51-0', usage_metadata={'input_tokens': 33, 'output_tokens': 12, 'total_tokens': 45})
و اکنون میتوانیم ببینیم که پاسخ خوبی دریافت میکنیم! حال سوال این است که چگونه میتوانیم انتقال مکالمات قبلی به چت بات را به بهترین شکل پیادهسازی کنیم؟
نگهداری پیام #
پکیج LangGraph یک لایه حافظه داخلی پیادهسازی میکند که آن را برای برنامههای چت که باید مکالمات قبلی را به خاطر بسپارند، ایدهآل میکند.
استفاده از LangGraph
به ما امکان میدهد تا تاریخچه پیامها را به طور خودکار حفظ کنیم که باعث تسهیل توسعه چت بات هامیشود.
پکیج LangGraph
با یک چکپوینتر ساده در حافظه ارائه میشود که در زیر از آن استفاده میکنیم. برای جزئیات بیشتر، از جمله نحوه استفاده از بکاندهای مختلف (مانند SQLite
یا Postgres
) به مستندات آن مراجعه کنید.
1from langgraph.checkpoint.memory import MemorySaver
2from langgraph.graph import START, MessagesState, StateGraph
3
4# Define a new graph
5workflow = StateGraph(state_schema=MessagesState)
6
7
8# Define the function that calls the model
9def call_model(state: MessagesState):
10 response = model.invoke(state["messages"])
11 return {"messages": response}
12
13
14# Define the (single) node in the graph
15workflow.add_edge(START, "model")
16workflow.add_node("model", call_model)
17
18# Add memory
19memory = MemorySaver()
20app = workflow.compile(checkpointer=memory)
اکنون باید یک config
ایجاد کنیم که هر بار به runnable
منتقل شود. این پیکربندی شامل اطلاعاتی است که مستقیماً بخشی از ورودی نیست، اما همچنان مفید است. در این مورد، میخواهیم یک thread_id
را تعریف کنیم که به صورت زیر است:
1config = {"configurable": {"thread_id": "abc123"}}
این کار به چت بات این امکان را میدهد تا چندین مکالمهی همزمان را به طور جداگانه مدیریت کند.
سپس میتوانیم برنامه را فراخوانی کنیم:
1query = "Hi! I'm Bob."
2
3input_messages = [HumanMessage(query)]
4output = app.invoke({"messages": input_messages}, config)
5output["messages"][-1].pretty_print() # output contains all messages in state
1query = "What's my name?"
2
3input_messages = [HumanMessage(query)]
4output = app.invoke({"messages": input_messages}, config)
5output["messages"][-1].pretty_print()
عالی! چتبات ما اکنون چیزهایی درباره ما به خاطر میآورد. اگر پیکربندی را تغییر دهیم تا به یک thread_id
متفاوت اشاره کند، میتوانیم ببینیم که مکالمه را تازه شروع میکند.
1config = {"configurable": {"thread_id": "abc234"}}
2
3input_messages = [HumanMessage(query)]
4output = app.invoke({"messages": input_messages}, config)
5output["messages"][-1].pretty_print()
1Ai Message:
2
3I'm sorry, but I don't have access to personal information about you unless you provide it. How can I assist you today?
با این حال، چون مکالمهی قبلی را در حافظ نگه داشته ایم٬ همیشه میتوانیم به آن برگردیم.
1config = {"configurable": {"thread_id": "abc123"}}
2
3input_messages = [HumanMessage(query)]
4output = app.invoke({"messages": input_messages}, config)
5output["messages"][-1].pretty_print()
پس به این طریق میتوانیم چتباتی داشته باشیم که با بسیاری از کاربران مکالمه دارد و تاریخچهی هر مکالمه را به طور جداگانه به خاطر دارد!
برای پشتیبانی از async
، تابع call_model
را تبدیل به یک تابع async
کنید و هنگام فراخوانی برنامه از .ainvoke
استفاده کنید:
1# Async function for node:
2async def call_model(state: MessagesState):
3 response = await model.ainvoke(state["messages"])
4 return {"messages": response}
5
6
7# Define graph as before:
8workflow = StateGraph(state_schema=MessagesState)
9workflow.add_edge(START, "model")
10workflow.add_node("model", call_model)
11app = workflow.compile(checkpointer=MemorySaver())
12
13# Async invocation:
14output = await app.ainvoke({"messages": input_messages}, config)
15output["messages"][-1].pretty_print()
در حال حاضر، تمام کاری که انجام دادهایم اضافه کردن یک لایه حافظهی ساده به مدل است. حال میتوانیم با اضافه کردن یک قالب پراپمت، مکالمات را پیچیدهتر و شخصیتر کنیم.
قالبهای پراپمت #
قالبهای پراپمت به تبدیل اطلاعات خام کاربر به فرمتی که LLM
میتواند با آن کار کند کمک میکنند. در حال حاضر ورودی خام کاربر فقط یک پیام است که ما به LLM
ارسال میکنیم. بیایید اکنون آن را کمی پیچیدهتر کنیم. ابتدا، میخواهیم یک پیام سیستمی با دستورالعملهای سفارشی را به مدل اضافه کنیم (اما همچنان پیامها را به عنوان ورودی بپذیریم). سپس، ورودیهای بیشتری را در کنار پیامها به مدل خواهیم افزود.
برای اضافه کردن یک پیام سیستمی، یک ChatPromptTemplate
ایجاد خواهیم کرد و از MessagesPlaceholder
برای ارسال همه پیامها استفاده میکنیم.
1from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
2
3prompt = ChatPromptTemplate.from_messages(
4 [
5 (
6 "system",
7 "You talk like a pirate. Answer all questions to the best of your ability.",
8 ),
9 MessagesPlaceholder(variable_name="messages"),
10 ]
11)
اکنون میتوانیم برنامه خود را بهروزرسانی کنیم تا از این قالب استفاده کند:
1workflow = StateGraph(state_schema=MessagesState)
2
3
4def call_model(state: MessagesState):
5 # change-start
6 chain = prompt | model
7 response = chain.invoke(state)
8 # change-end
9 return {"messages": response}
10
11
12workflow.add_edge(START, "model")
13workflow.add_node("model", call_model)
14
15memory = MemorySaver()
16app = workflow.compile(checkpointer=memory)
حال برنامه را به همان روش قبلی فراخوانی میکنیم:
1config = {"configurable": {"thread_id": "abc345"}}
2query = "Hi! I'm Jim."
3
4input_messages = [HumanMessage(query)]
5output = app.invoke({"messages": input_messages}, config)
6output["messages"][-1].pretty_print()
1Ai Message:
2
3Ahoy there, Jim! What brings ye to these treacherous waters today? Be ye seekin’ treasure, tales, or perhaps a bit o’ knowledge? Speak up, matey!
1query = "What is my name?"
2
3input_messages = [HumanMessage(query)]
4output = app.invoke({"messages": input_messages}, config)
5output["messages"][-1].pretty_print()
Ai Message:
Ye be callin' yerself Jim, if I be hearin' ye correctly! A fine name for a scallywag such as yerself! What else can I do fer ye, me hearty?
عالی! بیایید اکنون پراپمت خود را کمی پیچیدهتر کنیم. فرض کنیم که قالب پراپمت اکنون به این شکل است:
1prompt = ChatPromptTemplate.from_messages(
2 [
3 (
4 "system",
5 "You are a helpful assistant. Answer all questions to the best of your ability in {language}.",
6 ),
7 MessagesPlaceholder(variable_name="messages"),
8 ]
9)
توجه داشته باشید که یک ورودی جدید language
به پراپمت اضافه کردهایم. برنامه ما اکنون دو پارامتر دارد - ورودی messages
و language
. باید وضعیت برنامه خود را بهروزرسانی کنیم تا این را منعکس کند:
1from typing import Sequence
2
3from langchain_core.messages import BaseMessage
4from langgraph.graph.message import add_messages
5from typing_extensions import Annotated, TypedDict
6
7
8class State(TypedDict):
9 messages: Annotated[Sequence[BaseMessage], add_messages]
10 language: str
11
12
13workflow = StateGraph(state_schema=State)
14
15
16def call_model(state: State):
17 chain = prompt | model
18 response = chain.invoke(state)
19 return {"messages": [response]}
20
21
22workflow.add_edge(START, "model")
23workflow.add_node("model", call_model)
24
25memory = MemorySaver()
26app = workflow.compile(checkpointer=memory)
1config = {"configurable": {"thread_id": "abc456"}}
2query = "Hi! I'm Bob."
3language = "Farsi"
4
5input_messages = [HumanMessage(query)]
6output = app.invoke(
7 {"messages": input_messages, "language": language},
8 config,
9)
10output["messages"][-1].pretty_print()
برای کمک به درک آنچه در داخل اتفاق میافتد، به این LangSmith trace نگاهی بیندازید.
مدیریت تاریخچه مکالمه #
یکی از مفاهیم مهم در ساخت چتباتها، نحوه مدیریت تاریخچه مکالمه است. اگر تاریخچهی مکالمات مدیریت نشود، لیست پیامها به طور نامحدود رشد میکند و ممکن است پنجره زمینه (context window
) LLM
را پر کند. بنابراین، مهم است که مرحلهای اضافه کنید که اندازه پیامهایی که ارسال میکنید را محدود کند.
مهم است که این کار را قبل از ساخت قالب پراپمت و بعد از بارگذاری پیامهای قبلی از تاریخچه پیام انجام دهید.
میتوانیم این کار را با اضافه کردن یک مرحله ساده در جلوی پراپمت انجام دهیم که کلید messages
را تغییر دهد و سپس زنجیره جدید را در کلاس تاریخچه پیام wrap
کنیم.
پکیج LangChain
با چندین تابع کمکی داخلی برای مدیریت لیست پیامها ارائه میشود. در این مورد، از تابع trim_messages برای کاهش تعداد پیامهایی که به مدل ارسال میکنیم، استفاده خواهیم کرد. این ابزار به ما اجازه میدهد تا مشخص کنیم که چند توکن از حافظه را میخواهیم نگه داریم. همچنین پارامترهای دیگری مانند اینکه آیا میخواهیم پیام سیستمی را همیشه نگه داریم و آیا اجازه پیامهای جزئی را بدهیم یا خیر نیز قابل تنظیم هستند.
1from langchain_core.messages import SystemMessage, trim_messages
2
3trimmer = trim_messages(
4 max_tokens=65,
5 strategy="last",
6 token_counter=model,
7 include_system=True,
8 allow_partial=False,
9 start_on="human",
10)
11
12messages = [
13 SystemMessage(content="you're a good assistant"),
14 HumanMessage(content="hi! I'm bob"),
15 AIMessage(content="hi!"),
16 HumanMessage(content="I like vanilla ice cream"),
17 AIMessage(content="nice"),
18 HumanMessage(content="whats 2 + 2"),
19 AIMessage(content="4"),
20 HumanMessage(content="thanks"),
21 AIMessage(content="no problem!"),
22 HumanMessage(content="having fun?"),
23 AIMessage(content="yes!"),
24]
25
26trimmer.invoke(messages)
برای استفاده از آن در زنجیره خود، فقط باید قبل از ارسال ورودی messages
به پراپمت، از trimmer
استفاده کنیم.
1workflow = StateGraph(state_schema=State)
2
3
4def call_model(state: State):
5 chain = prompt | model
6 # change-start
7 trimmed_messages = trimmer.invoke(state["messages"])
8 response = chain.invoke(
9 {"messages": trimmed_messages, "language": state["language"]}
10 )
11 # change-end
12 return {"messages": [response]}
13
14
15workflow.add_edge(START, "model")
16workflow.add_node("model", call_model)
17
18memory = MemorySaver()
19app = workflow.compile(checkpointer=memory)
اکنون اگر از مدل نام خود را بپرسیم جواب را نمیداند زیرا آن بخش از تاریخچه چت را برش دادهایم:
1config = {"configurable": {"thread_id": "abc567"}}
2query = "What is my name?"
3language = "English"
4
5input_messages = messages + [HumanMessage(query)]
6output = app.invoke(
7 {"messages": input_messages, "language": language},
8 config,
9)
10output["messages"][-1].pretty_print()
اما اگر درباره اطلاعاتی که در چند پیام آخر بین ما و مدل رد و بدل شده است سوالی بپرسیم، مدل آن را به خاطر میآورد:
1config = {"configurable": {"thread_id": "abc678"}}
2query = "What math problem did I ask?"
3language = "English"
4
5input_messages = messages + [HumanMessage(query)]
6output = app.invoke(
7 {"messages": input_messages, "language": language},
8 config,
9)
10output["messages"][-1].pretty_print()
اگر به LangSmith trace نگاهی بیندازید، میتوانید دقیقاً ببینید که در پشت پرده چه اتفاقی میافتد.
استریم #
اکنون یک چتبات کاربردی داریم. با این حال، یکی از ملاحظات بسیار مهم UX برای برنامههای چتبات، استریم است. گاهی اوقات ممکن است مدتی طول بکشد تا LLM
ها پاسخ را تولید کنند، بنابراین برای بهبود تجربه کاربر، یکی از کارهایی که اکثر برنامهها انجام میدهند این است که هر توکن را به محض تولید استریم کنند. این به کاربر اجازه میدهد تا تولید پاسخ را سریعتر ببیند.
به طور پیشفرض، .stream
در برنامه LangGraph
ما مراحل برنامه را استریم میکند. تنظیم stream_mode="messages"
به ما اجازه میدهد تا توکنهای خروجی را به جای آن استریم کنیم:
1config = {"configurable": {"thread_id": "abc789"}}
2query = "Hi I'm Todd, please tell me a joke."
3language = "English"
4
5input_messages = [HumanMessage(query)]
6for chunk, metadata in app.stream(
7 {"messages": input_messages, "language": language},
8 config,
9 stream_mode="messages",
10):
11 if isinstance(chunk, AIMessage): # Filter to just model responses
12 print(chunk.content, end="|")