Un sistema de diálogo orientado a tareas (ToD) es un sistema que ayuda a los usuarios a: lograr una tarea en particularcomo reservar un restaurante, planificar un itinerario de viaje o pedir comida a domicilio.
Sabemos que instruimos a los LLM mediante indicaciones, pero ¿cómo podemos implementar estos sistemas ToD para que La conversación siempre gira en torno a la tarea que queremos que los usuarios realicen.Una forma de hacerlo es utilizando indicaciones, memoria y llamada de herramienta. Afortunadamente, LangChain + LangGraph pueden ayudarnos a unir todas estas cosas.
En este artículo, aprenderá a crear un sistema de diálogo orientado a tareas que ayude a los usuarios a crear historias de usuario con un alto nivel de calidad. El sistema se basa en el lenguaje de programación de LangGraph. Generación de indicaciones a partir de los requisitos del usuario tutorial.
En este tutorial, asumimos que ya sabes cómo usar LangChain. Una historia de usuario tiene algunos componentes como objetivo, criterios de éxito, plan de ejecución y entregables. El usuario debe proporcionar cada uno de ellos y debemos «tomarle la mano» para que los proporcione uno por uno. Hacerlo usando solo LangChain requeriría muchos «si» y «si no».
Con LangGraph podemos utilizar una abstracción gráfica para crear ciclos para controlar el diálogo. También tiene persistencia incorporadapor lo que no necesitamos preocuparnos por rastrear activamente las interacciones que ocurren dentro del gráfico.
La principal abstracción de LangGraph es la Gráfico de estadoque se utiliza para crear flujos de trabajo de gráficos. Cada gráfico debe inicializarse con un esquema_de_estado:una clase de esquema que cada nodo del gráfico utiliza para leer y escribir información.
El flujo de nuestro sistema constará de rondas de Máster en Derecho y usuario mensajes. El bucle principal contendrá estos pasos:
- El usuario dice algo
- LLM lee los mensajes del estado y decide si está listo para crear la Historia de Usuario o si el usuario debe responder nuevamente
Nuestro sistema es simple por lo que el esquema consta únicamente de los mensajes que se intercambiaron en el diálogo.
from langgraph.graph.message import add_messagesclass StateSchema(TypedDict):
messages: Annotated[list, add_messages]
El agregar_mensajes El método se utiliza para fusionar los mensajes de salida de cada uno nodo en la lista existente de mensajes en el estado del gráfico.
Hablando de nodos, otros dos conceptos principales de LangGraph son Nodos y Bordes. Cada nodo del gráfico se ejecuta una función y cada borde controla el flujo de un nodo a otro. También tenemos COMENZAR y FIN nodos virtuales para indicar al gráfico dónde iniciar la ejecución y dónde debe finalizar la ejecución.
Para ejecutar el sistema utilizaremos el .stream()
método. Después de construir el gráfico y compilarlo, Cada ronda de interacción pasará por el INICIO hasta el FIN del gráfico. y el camino que sigue (qué nodos deben ejecutarse o no) está controlado por nuestro flujo de trabajo combinado con el estado del gráfico. El siguiente código tiene el flujo principal de nuestro sistema:
config = {"configurable": {"thread_id": str(uuid.uuid4())}}while True:
user = input("User (q/Q to quit): ")
if user in {"q", "Q"}:
print("AI: Byebye")
break
output = None
for output in graph.stream(
{"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"
):
last_message = next(iter(output.values()))["messages"][-1]
last_message.pretty_print()
if output and "prompt" in output:
print("Done!")
En cada interacción (si el usuario no escribió “q” o “Q” para salir) ejecutamos graph.stream() pasando el mensaje del usuario usando el stream_mode “updates”, que transmite las actualizaciones del estado después de cada paso del gráfico (https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) Luego recibimos este último mensaje del state_schema y lo imprimimos.
En este tutorial seguiremos aprendiendo cómo crear los nodos y los bordes del gráfico, pero primero hablemos más sobre la arquitectura de los sistemas ToD en general y aprendamos cómo implementar uno con LLM (Máster en Derecho), indicaciones y llamada de herramienta.
Los componentes principales de un marco para construir Sistemas de diálogo orientados a tareas de extremo a extremo son [1]:
- Comprensión del lenguaje natural (NLU) para Extraer la intención y los espacios clave de los usuarios
- Seguimiento del estado del diálogo (DST) para Rastreo del estado de creencias de los usuarios diálogo dado
- Aprendizaje sobre políticas de diálogo (DPL) determinar el siguiente paso a tomar
- Generación de lenguaje natural (NLG) para Generando respuesta del sistema de diálogo
Al utilizar LLM, podemos combinar algunos de estos componentes en uno solo. PNL y el Gobierno Nacional Los componentes son muy fáciles de implementar utilizando LLM, ya que comprender y generar respuestas de diálogo es su especialidad.
Podemos implementar el Seguimiento del Estado del Diálogo (Horario de verano) y el Diálogo sobre Aprendizaje de Políticas (DPL) mediante el uso de LangChain Mensaje del sistema para preparar el comportamiento de la IA y pasar siempre este mensaje cada vez que interactuamos con el LLM. El estado del diálogo también debe pasarse siempre al LLM en cada interacción con el modelo. Esto significa que nos aseguraremos de que el diálogo siempre se centre en la tarea que queremos que complete el usuario. diciéndole siempre al LLM cuál es el objetivo del diálogo y cómo debe comportarse. Lo haremos primero usando un mensaje:
prompt_system_task = """Your job is to gather information from the user about the User Story they need to create.You should obtain the following information from them:
- Objective: the goal of the user story. should be concrete enough to be developed in 2 weeks.
- Success criteria the sucess criteria of the user story
- Plan_of_execution: the plan of execution of the initiative
- Deliverables: the deliverables of the initiative
If you are not able to discern this info, ask them to clarify! Do not attempt to wildly guess.
Whenever the user responds to one of the criteria, evaluate if it is detailed enough to be a criterion of a User Story. If not, ask questions to help the user better detail the criterion.
Do not overwhelm the user with too many questions at once; ask for the information you need in a way that they do not have to write much in each response.
Always remind them that if they do not know how to answer something, you can help them.
After you are able to discern all the information, call the relevant tool."""
Y luego agregar este mensaje cada vez que enviemos un mensaje al LLM:
def domain_state_tracker(messages):
return [SystemMessage(content=prompt_system_task)] + messages
Otro concepto importante de la implementación de nuestro sistema ToD LLM es llamada de herramienta. Si lees la última frase de la tarea_del_sistema_de_aviso De nuevo dice “Una vez que pueda discernir toda la información, llame a la herramienta relevante”. De esta manera, le estamos diciendo al LLM que cuando decide que el usuario proporcionó todos los parámetros de la historia de usuario, Debería llamar a la herramienta para crear la historia de usuario.Nuestra herramienta para eso se creará utilizando un modelo de Pydantic con los parámetros de la historia del usuario.
Con solo usar el indicador y la herramienta, podemos controlar nuestro sistema ToD. Hermoso, ¿verdad? En realidad, también necesitamos usar el estado del gráfico Para que todo esto funcione, lo haremos en la siguiente sección, donde finalmente construiremos el sistema ToD.
Bien, es hora de codificar un poco. Primero, especificaremos qué modelo LLM usaremos, luego estableceremos el mensaje y vincularemos la herramienta para generar la historia de usuario:
import os
from dotenv import load_dotenv, find_dotenvfrom langchain_openai import AzureChatOpenAI
from langchain_core.pydantic_v1 import BaseModel
from typing import List, Literal, Annotated
_ = load_dotenv(find_dotenv()) # read local .env file
llm = AzureChatOpenAI(azure_deployment=os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"),
openai_api_version="2023-09-01-preview",
openai_api_type="azure",
openai_api_key=os.environ.get('AZURE_OPENAI_API_KEY'),
azure_endpoint=os.environ.get('AZURE_OPENAI_ENDPOINT'),
temperature=0)
prompt_system_task = """Your job is to gather information from the user about the User Story they need to create.
You should obtain the following information from them:
- Objective: the goal of the user story. should be concrete enough to be developed in 2 weeks.
- Success criteria the sucess criteria of the user story
- Plan_of_execution: the plan of execution of the initiative
If you are not able to discern this info, ask them to clarify! Do not attempt to wildly guess.
Whenever the user responds to one of the criteria, evaluate if it is detailed enough to be a criterion of a User Story. If not, ask questions to help the user better detail the criterion.
Do not overwhelm the user with too many questions at once; ask for the information you need in a way that they do not have to write much in each response.
Always remind them that if they do not know how to answer something, you can help them.
After you are able to discern all the information, call the relevant tool."""
class UserStoryCriteria(BaseModel):
"""Instructions on how to prompt the LLM."""
objective: str
success_criteria: str
plan_of_execution: str
llm_with_tool = llm.bind_tools([UserStoryCriteria])
Como ya hemos comentado antes, el estado de nuestro gráfico consta únicamente de los mensajes intercambiados y una bandera para saber si la historia de usuario fue creada o no. Vamos a crear el gráfico primero usando Gráfico de estado y este esquema:
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messagesclass StateSchema(TypedDict):
messages: Annotated[list, add_messages]
created_user_story: bool
workflow = StateGraph(StateSchema)
La siguiente imagen muestra la estructura del gráfico final:
En la parte superior tenemos un hablar con el usuario Nodo. Este nodo puede:
- Finalizar el diálogo (ir a la finalizar_diálogo nodo)
- Decide que es hora de esperar la entrada del usuario (ve a la FIN nodo)
Dado que el bucle principal se ejecuta eternamente (Aunque es cierto), cada vez que el gráfico llega al nodo FIN, espera nuevamente la entrada del usuario. Esto quedará más claro cuando creemos el bucle.
Vamos a crear los nodos del gráfico, empezando por el hablar con el usuario Nodo. Este nodo debe realizar un seguimiento de la tarea (mantener el mensaje principal durante toda la conversación) y también mantener los intercambios de mensajes porque es donde se almacena el estado del diálogo. Este estado también mantiene qué parámetros de la historia de usuario ya se completaron o no utilizando los mensajes. Por lo tanto, este nodo debe agregar SystemMessage cada vez y adjuntar el nuevo mensaje del LLM:
def domain_state_tracker(messages):
return [SystemMessage(content=prompt_system_task)] + messagesdef call_llm(state: StateSchema):
"""
talk_to_user node function, adds the prompt_system_task to the messages,
calls the LLM and returns the response
"""
messages = domain_state_tracker(state["messages"])
response = llm_with_tool.invoke(messages)
return {"messages": [response]}
Ahora podemos agregar el hablar con el usuario Nodo de este gráfico. Lo haremos dándole un nombre y luego pasando la función que hemos creado:
workflow.add_node("talk_to_user", call_llm)
Este nodo debe ser el primer nodo en ejecutarse en el gráfico, así que especifiquémoslo con un borde:
workflow.add_edge(START, "talk_to_user")
Hasta ahora el gráfico se ve así:
Para controlar el flujo del gráfico, también utilizaremos las clases de mensajes de LangChain. Tenemos cuatro tipos de mensajes:
- Mensaje del sistema: Mensaje para preparar el comportamiento de la IA
- Mensaje humano: mensaje de un humano
- AIMessage: El mensaje devuelto desde un modelo de chat como respuesta a una solicitud.
- Mensaje de herramienta: mensaje que contiene el resultado de una invocación de herramienta, utilizado para pasar el resultado de la ejecución de una herramienta a un modelo
Nosotros lo usaremos El tipo del último mensaje de la estado del gráfico para controlar el flujo en el hablar con el usuario nodo. Si el último mensaje es un Mensaje AIM y tiene el llamadas_de_herramientas clave, luego iremos a la finalizar_diálogo nodo porque es momento de crear la historia de usuario. De lo contrario, deberíamos ir al nodo FIN nodo porque reiniciaremos el bucle ya que es hora de que el usuario responda.
El finalizar_diálogo El nodo debe construir el Mensaje de herramienta para pasar el resultado al modelo. id_de_llamada_de_herramienta El campo se utiliza para asociar la solicitud de llamada de herramienta con la respuesta de llamada de herramienta. Creemos este nodo y agreguémoslo al gráfico:
def finalize_dialogue(state: StateSchema):
"""
Add a tool message to the history so the graph can see that it`s time to create the user story
"""
return {
"messages": [
ToolMessage(
content="Prompt generated!",
tool_call_id=state["messages"][-1].tool_calls[0]["id"],
)
]
}workflow.add_node("finalize_dialogue", finalize_dialogue)
Ahora vamos a crear el último nodo, el crear_historia_de_usuario 1. Este nodo llamará al LLM utilizando el mensaje para crear la historia de usuario y la información que se recopiló durante la conversación. Si el modelo decidió que era hora de llamar a la herramienta, entonces los valores de la clave llamadas_de_herramientas Debe tener toda la información para crear la historia del usuario.
prompt_generate_user_story = """Based on the following requirements, write a good user story:{reqs}"""
def build_prompt_to_generate_user_story(messages: list):
tool_call = None
other_msgs = []
for m in messages:
if isinstance(m, AIMessage) and m.tool_calls: #tool_calls is from the OpenAI API
tool_call = m.tool_calls[0]["args"]
elif isinstance(m, ToolMessage):
continue
elif tool_call is not None:
other_msgs.append(m)
return [SystemMessage(content=prompt_generate_user_story.format(reqs=tool_call))] + other_msgs
def call_model_to_generate_user_story(state):
messages = build_prompt_to_generate_user_story(state["messages"])
response = llm.invoke(messages)
return {"messages": [response]}
workflow.add_node("create_user_story", call_model_to_generate_user_story)
Con todos los nodos creados, es hora de agregar el bordesAgregaremos un borde condicional a la hablar con el usuario Nodo. Recuerde que este nodo puede:
- Finalice el diálogo si es el momento de llamar a la herramienta (vaya a la finalizar_diálogo nodo)
- Decidimos que necesitamos recopilar información del usuario (ir a la FIN nodo)
Esto significa que solo comprobaremos si el último mensaje es un AIMessage y tiene la clave tool_calls; de lo contrario, debemos ir al nodo END. Creemos una función para comprobar esto y agréguela como una arista:
def define_next_action(state) -> Literal["finalize_dialogue", END]:
messages = state["messages"]if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:
return "finalize_dialogue"
else:
return END
workflow.add_conditional_edges("talk_to_user", define_next_action)
Ahora agreguemos los otros bordes:
workflow.add_edge("finalize_dialogue", "create_user_story")
workflow.add_edge("create_user_story", END)
Con esto, el flujo de trabajo del gráfico está listo. Es hora de compilar el gráfico y crear el bucle para ejecutarlo:
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)config = {"configurable": {"thread_id": str(uuid.uuid4())}}
while True:
user = input("User (q/Q to quit): ")
if user in {"q", "Q"}:
print("AI: Byebye")
break
output = None
for output in graph.stream(
{"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"
):
last_message = next(iter(output.values()))["messages"][-1]
last_message.pretty_print()
if output and "create_user_story" in output:
print("User story created!")
Probemos finalmente el sistema:
Con LangGraph y LangChain podemos construir sistemas que guíen a los usuarios a través de interacciones estructuradas reduciendo la complejidad para crearlas utilizando los LLM para ayudarnos a controlar la lógica condicional.
Con la combinación de indicaciones, gestión de memoria y llamada de herramientas podemos crear sistemas de diálogo intuitivos y efectivos, abriendo nuevas posibilidades para la interacción del usuario y la automatización de tareas.
Espero que este tutorial te ayude a comprender mejor cómo utilizar LangGraph (he pasado un par de días dándome cabezazos contra la pared para entender cómo funcionan juntas todas las piezas de la biblioteca).
Todo el código de este tutorial lo puedes encontrar aquí: dmesquita/sistema de diálogo orientado a tareas (langgraph) (github.com)
¡Gracias por leer!
[1] Qin, Libo, et al. “Diálogo orientado a tareas de extremo a extremo: un estudio de tareas, métodos y direcciones futuras”. Preimpresión de arXiv arXiv:2311.09008 (2023).
[2] Generación rápida a partir de los requisitos del usuarioDisponible en: https://langchain-ai.github.io/langgraph/tutorials/chatbots/information-gather-prompting