← Back to DevBytes

Migrating from Legacy Frameworks to LangChain

Introduction: What is LangChain and Why Migrate?

LangChain is an open-source framework designed to simplify the creation of applications powered by large language models (LLMs). It provides a standard interface for interacting with dozens of LLM providers, composable chains for orchestrating multi-step workflows, built-in memory management, and a rich ecosystem of tools and agents. For developers still relying on legacy frameworks—such as custom scripts, monolithic wrappers around a single LLM provider, or ad-hoc prompt management—migration to LangChain unlocks scalability, maintainability, and access to cutting-edge patterns like retrieval-augmented generation (RAG) and autonomous agents.

Legacy frameworks often evolved organically from quick experiments. They typically hard-code a specific model provider (like OpenAI or Anthropic), manage prompts with raw string interpolation, and lack any standardized way to chain multiple calls, store conversational context, or integrate external tools. As applications grow, these ad-hoc solutions become brittle, hard to test, and nearly impossible to extend without significant refactoring. Migrating to LangChain gives you a unified, declarative way to express LLM workflows, making your codebase future-proof and drastically reducing the effort needed to swap models, add features, or debug issues.

Understanding the Legacy Landscape

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before migrating, it's helpful to recognize the common patterns found in legacy LLM applications:

These patterns work for small prototypes, but they quickly become technical debt as the application expands. LangChain abstracts each of these concerns into reusable components that can be composed declaratively.

Why Migrating to LangChain Matters

Adopting LangChain offers concrete, immediate benefits over legacy implementations:

Migration isn’t just about switching libraries; it’s about adopting a design philosophy that separates concerns, promotes testing, and accelerates feature development.

How to Approach Migration: A Step-by-Step Guide

Migration should be incremental. You don’t need to rewrite the entire application overnight. Instead, identify the core functionalities, map them to LangChain abstractions, and replace one piece at a time while keeping the legacy system operational as a fallback.

Step 1: Identify Core Components

Inventory your legacy codebase. List every place where you call an LLM, construct a prompt, manage chat history, or integrate an external tool. Typical categories include:

For each category, decide which LangChain component will replace it. This mapping will guide the migration order—start with the simplest, most isolated parts.

Step 2: Replace Direct API Calls with LangChain LLM Wrappers

Your legacy code likely contains something like this:

import openai

def generate_response(prompt_text, model="gpt-3.5-turbo"):
    response = openai.ChatCompletion.create(
        model=model,
        messages=[{"role": "user", "content": prompt_text}]
    )
    return response.choices[0].message.content

Replace it with LangChain's ChatOpenAI wrapper. This gives you a consistent interface, automatic retry logic, and seamless integration with other LangChain components.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

def generate_response(prompt_text):
    # LangChain expects messages as a list, but for a single user message you can invoke directly
    return llm.invoke(prompt_text).content

Now you can swap models simply by changing the instantiation, e.g., ChatAnthropic or ChatCohere, without touching the business logic.

Step 3: Convert Prompt Management to Prompt Templates

Legacy prompts often look like:

def build_summary_prompt(document_text, style="concise"):
    return f"Please summarize the following text in a {style} style:\n\n{document_text}"

This is fragile and mixes instructions with data. LangChain's PromptTemplate separates the template structure from the input variables, enabling validation, serialization, and easy reuse.

from langchain_core.prompts import PromptTemplate

summary_template = PromptTemplate(
    template="Please summarize the following text in a {style} style:\n\n{text}",
    input_variables=["style", "text"]
)

def build_summary_prompt(document_text, style="concise"):
    return summary_template.format(style=style, text=document_text)

For chat-based models, use ChatPromptTemplate to define system, human, and AI message roles explicitly.

Step 4: Chain Simple Tasks with LCEL

Legacy sequential logic often looks like:

def summarize_then_translate(text, target_language):
    summary_prompt = f"Summarize: {text}"
    summary = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": summary_prompt}]
    ).choices[0].message.content
    
    translation_prompt = f"Translate to {target_language}: {summary}"
    translation = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": translation_prompt}]
    ).choices[0].message.content
    return translation

With LangChain, you can express this as a runnable chain using LCEL (LangChain Expression Language), making the flow explicit and easily composable.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough

llm = ChatOpenAI(model="gpt-3.5-turbo")

summarize_prompt = ChatPromptTemplate.from_template("Summarize the following text:\n\n{text}")
translate_prompt = ChatPromptTemplate.from_template("Translate the following summary to {language}:\n\n{summary}")

chain = (
    {"summary": summarize_prompt | llm}
    | RunnablePassthrough.assign(
        language=lambda _: target_language  # you'd pass this as input
    )
    | translate_prompt | llm
)

# Usage:
result = chain.invoke({"text": "Your long document here...", "language": "French"})

Note: In a real migration you'd likely use a simpler sequential chain or a custom pipeline; the above demonstrates the LCEL approach for clarity.

Step 5: Introduce Memory for Conversational Context

Legacy chat applications often store conversation history in a custom list:

class LegacyChatbot:
    def __init__(self):
        self.history = []
    
    def chat(self, user_input):
        self.history.append({"role": "user", "content": user_input})
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=self.history
        ).choices[0].message.content
        self.history.append({"role": "assistant", "content": response})
        return response

LangChain offers multiple memory classes that handle storage, retrieval, and context window management. Replace the custom history list with ConversationBufferMemory:

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

memory = ConversationBufferMemory(return_messages=True)
llm = ChatOpenAI(model="gpt-3.5-turbo")
conversation = ConversationChain(llm=llm, memory=memory)

def chat(user_input):
    response = conversation.invoke(user_input)
    return response["response"]

Now you get automatic history trimming, summary memory, or even persistent storage with a simple configuration change.

Step 6: Upgrade Complex Workflows to Agents

If your legacy code includes decision logic like "if the user asks for weather, call the weather API; if they ask for a calculation, run eval()" you've essentially built a primitive agent. LangChain's Agent framework standardizes this with a planner that decides which tool to call based on the user input.

Legacy pseudo-agent:

def handle_user_query(query):
    if "weather" in query:
        return call_weather_api(query)
    elif "calculate" in query:
        return eval_math(query)
    else:
        return generic_chat(query)

Migrate to a LangChain agent with tools:

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain.tools import Tool

llm = ChatOpenAI(model="gpt-4", temperature=0)

def weather_tool(query):
    # implementation that fetches weather
    return "Sunny, 72°F"

def calculator_tool(expression):
    # safely evaluate math
    return str(eval(expression))

tools = [
    Tool(name="Weather", func=weather_tool, description="Get the current weather for a location"),
    Tool(name="Calculator", func=calculator_tool, description="Evaluate a mathematical expression")
]

agent = create_openai_tools_agent(llm, tools, prompt="You are a helpful assistant. Use tools when necessary.")
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

def handle_user_query(query):
    result = executor.invoke({"input": query})
    return result["output"]

The agent now autonomously decides which tool to call, can chain multiple tool calls, and handles error recovery—all without hard-coded if-else branches.

Step 7: Add Observability and Tracing

Legacy debugging relies on scattered print statements. LangChain integrates with LangSmith (or simple callback handlers) to provide full trace trees of every chain execution. Start by setting environment variables for LangSmith or attaching a callback:

from langchain.callbacks import StdOutCallbackHandler

# Attach a handler to see logs in the console
executor = AgentExecutor(agent=agent, tools=tools, callbacks=[StdOutCallbackHandler()])

For production, switch to LangSmith tracing to inspect latency, token usage, and intermediate steps in a web UI.

Best Practices for a Smooth Migration

Common Pitfalls and How to Avoid Them

Conclusion

Migrating from legacy frameworks to LangChain is a strategic investment in the maintainability, extensibility, and observability of your LLM-powered applications. By systematically replacing direct API calls with standardized wrappers, converting ad-hoc prompts into templates, composing steps into chains, and upgrading static logic to agents, you transform fragile code into a modular, testable system. The incremental approach—starting with low-risk components, running in parallel, and leveraging LangChain’s rich toolset—ensures a smooth transition without service disruption. Embrace the framework’s philosophy of composition and abstraction, and you’ll find that what once required hundreds of lines of brittle code can now be expressed clearly in a few declarative statements, ready to evolve with the fast-moving landscape of AI.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles