# Why We Ditched LangChain and Built Our Own Python LLM Agent Framework

If you’ve ever tried wiring up LangChain for a non-trivial LLM agent and felt boxed in, you’re not alone. Our team hit a wall more than once—enough that we stopped patching and started building. If you’re wondering what it *really* takes to write your own agent framework in Python, and why you might want to, pull up a chair.

## Why We Walked Away from LangChain

LangChain’s a solid library. No denying that. If you need a quick proof-of-concept with OpenAI, it’s practically plug-and-play. But as our requirements grew, so did our headaches. We wanted custom agent behaviors, more explicit control over memory, and simple debugging. Instead, we got abstraction soup and a fair share of "Why did that just happen?"

A few pain points:

- Unexpected magic under the hood. Ever try stepping through LangChain’s agent execution?
- Tight coupling to certain APIs. Good luck swapping out LLM providers without rewriting chains.
- Chasing the latest updates. Sometimes breaking changes would break our flow—right in the middle of a sprint.

So, after the third time I spent a weekend chasing down a hidden bug in a chain, we decided: time to roll our own.

## What We Actually Needed

Before writing any code, we whiteboarded what we *really* needed from an agent framework. Here’s the shortlist that shaped our design:

- **Composable tools**: Add/remove tools (like web search, math, database) easily.
- **Explicit agent loop**: See every step, for debugging and logging.
- **Provider agnostic**: Swap GPT for an open-source model, no drama.
- **Stateless by default**: But let us wire up memory if we need it.

We didn’t want to reinvent the wheel—just drive it our way.

## Laying the Foundation: Agent Loop

The "agent loop" is the heart of any LLM agent. It’s essentially: get input, decide on an action, maybe call a tool, update state, repeat until done. Here’s the most stripped-down version we started with:

```python
import openai

def simple_agent(prompt, tools):
    """
    A minimal agent loop: feed prompt to LLM, parse response, call tool if needed.
    """
    # Step 1: Get LLM response
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",  # Replace with your model
        messages=[{"role": "user", "content": prompt}]
    )
    output = response.choices[0].message['content'].strip()
    print("LLM output:", output)
    
    # Step 2: Naively parse for tool call (just for demo)
    for tool_name, tool_fn in tools.items():
        if tool_name in output:
            # Extract argument (very basic, just an example)
            arg = output.split(tool_name)[-1].strip(" ()")
            result = tool_fn(arg)
            print(f"Tool '{tool_name}' called with '{arg}': {result}")
            return result
    return output

# Example tool: square a number
def square_tool(x):
    return float(x) ** 2

# Usage
tools = {"square": square_tool}
simple_agent("Please square 5 using the square tool.", tools)
```

*Key lines:*
- The agent asks the LLM for an action (a message).
- It checks if the output mentions a tool and calls it.
- This is intentionally naive—robust parsing comes later.

This version is *way* easier to debug than LangChain’s nested agents. If you’ve ever tried printing intermediate steps in a LangChain pipeline, you know why this matters.

## Adding Tool Abstraction

As our use case grew, we wanted to add or swap tools without touching agent logic. So, we wrapped tools in a simple class and added structured tool calls.

Here’s a more robust pattern:

```python
class Tool:
    """
    Simple abstraction for an agent tool.
    """
    def __init__(self, name, description, fn):
        self.name = name
        self.description = description
        self.fn = fn

    def call(self, arg):
        return self.fn(arg)

# Define some tools
def add(x):
    a, b = map(float, x.split(','))
    return a + b

def echo(x):
    return x

tools = [
    Tool("add", "Adds two numbers, input as 'num1,num2'", add),
    Tool("echo", "Repeats the input string.", echo)
]

# Updated agent loop
def agent_with_tools(prompt, tools):
    tool_dict = {t.name: t for t in tools}
    # For demo, ask user to specify tool name and arguments in response
    print("Prompt to LLM:", prompt)
    response = input("Simulate LLM (e.g., 'add 5,7'): ").strip()
    # Parse tool call
    tool_name, arg = response.split(" ", 1)
    if tool_name in tool_dict:
        result = tool_dict[tool_name].call(arg)
        print(f"Tool '{tool_name}' output: {result}")
        return result
    else:
        print("No matching tool found.")
        return None

# Usage
agent_with_tools("What is 5 plus 7?", tools)
```

*Key lines:*
- Each tool has a name, description, and function.
- The agent can call any registered tool by name.
- Tool registration is explicit and modular.

This might look super simple, but honestly, that’s the point. When you need to add a new tool, you just write a function and register it. No mysterious chain classes or hidden context.

## Swapping LLM Providers—No Drama

One thing that burned us with LangChain: swapping models or providers meant rewriting or subclassing core components. So, we abstracted the LLM call behind a simple interface.

Here’s an example of how we decouple the LLM provider:

```python
class LLMProvider:
    """
    Pluggable LLM provider interface.
    """
    def __init__(self, model):
        self.model = model

    def complete(self, prompt):
        # For demo, just echo prompt. Replace with actual API call.
        # To use OpenAI, uncomment below and set your API key.
        # import openai
        # response = openai.ChatCompletion.create(
        #     model=self.model,
        #     messages=[{"role": "user", "content": prompt}]
        # )
        # return response.choices[0].message['content'].strip()
        return f"Fake LLM response for prompt: {prompt}"

# Usage
llm = LLMProvider("gpt-3.5-turbo")
print(llm.complete("Tell me a joke."))
```

*Key lines:*
- The `LLMProvider` class wraps your provider.
- When you want to swap OpenAI for, say, HuggingFace, you just swap the implementation—no other code changes.
- No more rewriting chains or agents just to change the backend.

This saved us hours when we migrated to a self-hosted LLM for privacy reasons.

## The Debugging Aha Moment

I lost count of how many times I wished I could just see *exactly* what the agent received, decided, and did—without chasing logs through a framework. With our own code, you can print every step, state, and decision. You own the entire stack.

And when something breaks? You know where to look.

## Common Mistakes

I wish I could say we got it right the first time. Here are a few mistakes we made (and you might, too):

1. **Overcomplicating the agent loop**  
   It’s tempting to build “for everything” from day one—retries, tool chaining, memory, context, etc. Keep it simple until you *need* the complexity.

2. **Poor error handling in tool calls**  
   If a tool crashes, it can bring down your agent loop. Wrap tool calls in try/except, and return clear errors to the LLM (or user).

3. **Forgetting statelessness**  
   Agents should be stateless by default unless you *really* need memory. We wasted time debugging weird state leaks—usually because of hidden globals or lingering context.

## Key Takeaways

- Building your own agent framework is *easier* than it sounds—if you keep things simple.
- Explicit, composable tools beat magic chaining for debugging and extensibility.
- Decoupling the LLM provider saves time and future-proofs your code.
- Don’t try to outsmart yourself with premature complexity—add features as you actually need them.
- Owning the stack means faster debugging and clearer logic.

## Wrapping Up

If you’re tired of fighting the abstractions in popular LLM libraries, there’s no shame in owning your agent code. We haven’t looked back since building our own framework. You get total control, clarity, and—honestly—a bit more fun. If you’re facing similar pain points, try starting small. You might be surprised how far a little custom Python can take you.

---

*If you found this helpful, check out more programming tutorials on [our blog](https://pythonassignmenthelp.com/blog). We cover [Python](https://pythonassignmenthelp.com/programming-help/python), [JavaScript](https://pythonassignmenthelp.com/programming-help/javascript), [Java](https://pythonassignmenthelp.com/programming-help/java), [Data Science](https://pythonassignmenthelp.com/programming-help/data-science), and [more](https://pythonassignmenthelp.com/programming-help/gen-ai).*
