Chains and Why They Are Used

Introduction

Prompting is considered the most effective method of interacting with language models as it enables querying information using natural language. We already went through the prompting techniques and briefly used chains earlier. In this lesson, the chains will explain the chains in more detail.

The chains are responsible for creating an end-to-end pipeline for using the language models. They will join the model, prompt, memory, parsing output, and debugging capability and provide an easy-to-use interface. A chain will 1) receive the user’s query as an input, 2) process the LLM’s response, and lastly, 3) return the output to the user.

It is possible to design a custom pipeline by inheriting the Chain class. For example, the LLMChain is the simplest form of chain in LangChain, inheriting from the Chain parent class. We will start by going through ways to invoke this class and follow it by looking at adding different functionalities.

LLMChain

Several methods are available for utilizing a chain, each yielding a distinct output format. The example in this section is creating a bot that can suggest a replacement word based on context. The code snippet below demonstrates the utilization of the GPT-3 model through the OpenAI API. It generates a prompt using the PromptTemplate from LangChain, and finally, the LLMChain class ties all the components. Also, It is important to set the OPENAI_API_KEY environment variable with your API credentials from OpenAI. Remember to install the required packages with the following command: pip install langchain==0.1.4 deeplake openai==1.10.0 tiktoken.

from langchain import PromptTemplate, OpenAI, LLMChain

prompt_template = "What is a word to replace the following: {word}?"

# Set the "OPENAI_API_KEY" environment variable before running following line.
llm = OpenAI(model_name="gpt-3.5-turbo-instruct", temperature=0)

llm_chain = LLMChain(
    llm=llm,
    prompt=PromptTemplate.from_template(prompt_template)
)

The most straightforward approach uses the chain class __call__ method. It means passing the input directly to the object while initializing it. It will return the input variable and the model’s response under the text key.

llm_chain("artificial")
The sample code.
{'word': 'artificial', 'text': '\n\nSynthetic'}
The output.

It is also possible to use the .apply() method to pass multiple inputs at once and receive a list for each input. The sole difference lies in the exclusion of inputs within the returned list. Nonetheless, the returned list will maintain the identical order as the input.

input_list = [
    {"word": "artificial"},
    {"word": "intelligence"},
    {"word": "robot"}
]

llm_chain.apply(input_list)
The sample code.
[{'text': '\n\nSynthetic'}, {'text': '\n\nWisdom'}, {'text': '\n\nAutomaton'}]
The output.

The .generate() method will return an instance of LLMResult, which provides more information. For example, the finish_reason key indicates the reason behind the stop of the generation process. It could be stopped, meaning the model decided to finish or reach the length limit. There is other self-explanatory information like the number of total used tokens or the used model.

llm_chain.generate(input_list)
The sample code.
LLMResult(generations=[[Generation(text='\n\nSynthetic', generation_info={'finish_reason': 'stop', 'logprobs': None})], [Generation(text='\n\nWisdom', generation_info={'finish_reason': 'stop', 'logprobs': None})], [Generation(text='\n\nAutomaton', generation_info={'finish_reason': 'stop', 'logprobs': None})]], llm_output={'token_usage': {'prompt_tokens': 33, 'completion_tokens': 13, 'total_tokens': 46}, 'model_name': 'gpt-3.5-turbo-instruct'})
The output.

The next method we will discuss is .predict(). (which could be used interchangeably with .run()) Its best use case is to pass multiple inputs for a single prompt. However, it is possible to use it with one input variable as well. The following prompt will pass both the word we want a substitute for and the context the model must consider.

prompt_template = "Looking at the context of '{context}'. What is an appropriate word to replace the following: {word}?"

llm_chain = LLMChain(
    llm=llm,
    prompt=PromptTemplate(template=prompt_template, input_variables=["word", "context"]))

llm_chain.predict(word="fan", context="object")
# or llm_chain.run(word="fan", context="object")
The sample code.
'\n\nVentilator'
The output.

The model correctly suggested that a Ventilator would be a suitable replacement for the word fan in the context of objects. Furthermore, when we repeat the experiment with a different context, humans, the output will change the Admirer.

llm_chain.predict(word="fan", context="humans")
# or llm_chain.run(word="fan", context="humans")
The sample code.
'\n\nAdmirer'
The output.

The sample codes above show how passing single or multiple inputs to a chain and retrieving the outputs is possible. However, we prefer to receive a formatted output in most cases, as we learned in the “Managing Outputs with Output Parsers” lesson.

💡
We can directly pass a prompt as a string to a Chain and initialize it using the .from_string() function as follows. LLMChain.from_string(llm=llm, template=template).

Parsers

As discussed, the output parsers can define a data schema to generate correctly formatted responses. It wouldn’t be an end-to-end pipeline without using parsers to extract information from the LLM textual output. The following example shows the use of CommaSeparatedListOutputParser class with the PromptTemplate to ensure the results will be in a list format.

from langchain.output_parsers import CommaSeparatedListOutputParser

output_parser = CommaSeparatedListOutputParser()
template = """List all possible words as substitute for 'artificial' as comma separated."""

llm_chain = LLMChain(
    llm=llm,
    prompt=PromptTemplate(template=template, output_parser=output_parser, input_variables=[]),
    output_parser=output_parser)

llm_chain.predict()
The sample code.
['Synthetic',
 'Manufactured',
 'Imitation',
 'Fabricated',
 'Fake',
 'Simulated',
 'Artificial Intelligence',
 'Automated',
 'Constructed',
 'Programmed',
 'Processed',
 'Mechanical',
 'Man-Made',
 'Lab-Created',
 'Artificial Neural Network.']
The output.

Conversational Chain (Memory)

Depending on the application, memory is the next component that will complete a chain. LangChain provides a ConversationalChain to track previous prompts and responses using the ConversationalBufferMemory class.

from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory

output_parser = CommaSeparatedListOutputParser()
conversation = ConversationChain(
    llm=llm,
    memory=ConversationBufferMemory()
)

conversation.predict(input="List all possible words as substitute for 'artificial' as comma separated.")
The sample code.
'Synthetic, robotic, manufactured, simulated, computerized, programmed, man-made, fabricated, contrived, and artificial.'
The output.

Now, we can ask it to return the following four replacement words. It uses the memory to find the next options.

conversation.predict(input="And the next 4?")
The sample code.
'Automated, cybernetic, mechanized, and engineered.'
The output.

Sequential Chain

Another helpful feature is using a sequential chain that concatenates multiple chains into one. The following code shows a sample usage.

# poet
poet_template: str = """You are an American poet, your job is to come up with\
poems based on a given theme.

Here is the theme you have been asked to generate a poem on:
{input}\
"""

poet_prompt_template: PromptTemplate = PromptTemplate(
    input_variables=["input"], template=poet_template)

# creating the poet chain
poet_chain: LLMChain = LLMChain(
    llm=llm, output_key="poem", prompt=poet_prompt_template)

# critic
critic_template: str = """You are a critic of poems, you are tasked\
to inspect the themes of poems. Identify whether the poem includes romantic expressions or descriptions of nature.

Your response should be in the following format, as a Python Dictionary.
poem: this should be the poem you received 
Romantic_expressions: True or False
Nature_descriptions: True or False

Here is the poem submitted to you:
{poem}\
"""

critic_prompt_template: PromptTemplate = PromptTemplate(
    input_variables=["poem"], template=critic_template)

# creating the critic chain
critic_chain: LLMChain = LLMChain(
    llm=llm, output_key="critic_verified", prompt=critic_prompt_template)

In this example we define two processes in a chain: one for generating poems based on a given theme ("poet") and another for evaluating these poems on their romantic and natural elements ("critic"). The poet process uses a template to instruct an AI model to create poems, while the critic process analyzes these poems, flagging them for specific content. The setup utilizes prompt templates and chains to seamlessly integrate content generation with content verification.

from langchain.chains import SimpleSequentialChain

overall_chain = SimpleSequentialChain(chains=[poet_chain, critic_chain])

The SimpleSequentialChain will start running each chain from the first index and pass its response to the next one in the list.

# Run the poet and critic chain with a specific theme
theme: str = "the beauty of nature"
review = overall_chain.run(theme)

# Print the review to see the critic's evaluation
print(review)

The sample code.

poem: Nature's Beauty
Romantic_expressions: False
Nature_descriptions: True

The output.

Debug

It is possible to trace the inner workings of any chain by setting the verbose argument to True. As you can see in the following code, the chain will return the initial prompt and the output. The output depends on the application. It may contain more information if there are more steps.

template = """List all possible words as substitute for 'artificial' as comma separated.

Current conversation:
{history}

{input}"""

conversation = ConversationChain(
    llm=llm,
    prompt=PromptTemplate(template=template, input_variables=["history", "input"], output_parser=output_parser),
    memory=ConversationBufferMemory(),
    verbose=True)

conversation.predict(input="")
The sample code.
> Entering new ConversationChain chain...
Prompt after formatting:
List all possible words as substitute for 'artificial' as comma separated.

Current conversation:


Answer briefly. write the first 3 options.

> Finished chain.
'Synthetic, Imitation, Manufactured, Fabricated, Simulated, Fake, Artificial, Constructed, Computerized, Programmed'
The output.

Custom Chain

The LangChain library has several predefined chains for different applications like Transformation Chain, LLMCheckerChain, LLMSummarizationCheckerChain, and OpenAPI Chain, which all share the same characteristics mentioned in previous sections. It is also possible to define your chain for any custom task. In this section, we will create a chain that returns a word's meaning and then suggests a replacement.

It starts by defining a class that inherits most of its functionalities from the Chain class. Then, the following three methods must be declared depending on the use case. The input_keys and output_keys methods let the model know what it should expect, and a _call method runs each chain and merges their outputs.

from langchain.chains import LLMChain
from langchain.chains.base import Chain

from typing import Dict, List


class ConcatenateChain(Chain):
    chain_1: LLMChain
    chain_2: LLMChain

    @property
    def input_keys(self) -> List[str]:
        # Union of the input keys of the two chains.
        all_input_vars = set(self.chain_1.input_keys).union(set(self.chain_2.input_keys))
        return list(all_input_vars)

    @property
    def output_keys(self) -> List[str]:
        return ['concat_output']

    def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
        output_1 = self.chain_1.run(inputs)
        output_2 = self.chain_2.run(inputs)
        return {'concat_output': output_1 + output_2}
The sample code.

Then, we will declare each chain individually using the LLMChain class. Lastly, we call our custom chain ConcatenateChain to merge the results of the chain_1 and chain_2.

prompt_1 = PromptTemplate(
    input_variables=["word"],
    template="What is the meaning of the following word '{word}'?",
)
chain_1 = LLMChain(llm=llm, prompt=prompt_1)

prompt_2 = PromptTemplate(
    input_variables=["word"],
    template="What is a word to replace the following: {word}?",
)
chain_2 = LLMChain(llm=llm, prompt=prompt_2)

concat_chain = ConcatenateChain(chain_1=chain_1, chain_2=chain_2)
concat_output = concat_chain.run("artificial")
print(f"Concatenated output:\n{concat_output}")
The sample code.
Concatenated output:


Artificial means something that is not natural or made by humans but rather created or produced by artificial means.

Synthetic
The output.

Conclusion

This lesson taught us about LangChain and its powerful feature, chains, which combine multiple components to create a coherent application. The lesson initially showed the usage of several predefined chains from the LangChain library. Then, we built up by adding more features like parsers, memory, and debugging. Lastly, the process of defining custom chains was explained.

In the next lesson, we will do a hands-on project summarizing Youtube videos.

Resources

You can find the code of this lesson in this online Notebook.