Skip to content

Should You Start With LangChain or Raw API Calls for Building AI Agents?

Problem

I jumped straight into LangChain when I started building AI agents. I thought using a framework would make things easier. Instead, I spent weeks confused about how agents actually worked.

Here’s what happened:

  • When my agent failed, I couldn’t debug it because I didn’t understand what LangChain was doing under the hood
  • I struggled to customize behavior beyond what the framework allowed
  • I felt lost whenever I needed to switch to a different model or framework
  • Simple edge cases broke my agent and I had no idea why

I was building on top of abstractions I didn’t understand.

What happened?

I found a Reddit thread where experienced agent developers shared their learning paths. The top-voted advice was clear: skip LangChain at first.

One developer (wilzerjeanbaptiste, score 10) said:

“Skip LangChain at first. Start with raw API calls to Claude or GPT with tool use (function calling). Build a simple loop: the model decides what tool to call, you execute it, feed the result back, repeat until it’s done. That’s an agent. Once you understand that core loop deeply, then pick up a framework if you want the convenience.”

Another developer (Challseus, score 5) added:

“Don’t start with a framework, start with an sdk, like OpenAI’s. Frameworks will obscure some pretty important things.”

This explained my problems. I was trying to use a framework without understanding what it was abstracting away.

How to solve it?

I rebuilt my understanding from scratch using raw API calls. The core agent loop turned out to be surprisingly simple:

  1. Define tools - Functions your agent can call
  2. Send prompt + tools to model - The model decides if it needs to call a tool
  3. Execute the tool call - You run the actual function
  4. Feed result back - Send the tool result to the model
  5. Repeat until done - Model either calls another tool or returns final answer

Building with OpenAI SDK

Here’s the minimal agent I built using OpenAI’s SDK:

openai_agent.py
from openai import OpenAI
import json
client = OpenAI()
# Define a simple tool
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
}]
def get_weather(city: str) -> str:
# Your actual weather API call here
return f"Weather in {city}: Sunny, 72F"
def run_agent(user_message: str):
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools,
tool_choice="auto"
)
assistant_message = response.choices[0].message
messages.append(assistant_message)
# Check if model wants to call a tool
if assistant_message.tool_calls:
for tool_call in assistant_message.tool_calls:
if tool_call.function.name == "get_weather":
args = json.loads(tool_call.function.arguments)
result = get_weather(args["city"])
# Feed result back to model
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
else:
# No tool call - we have our answer
return assistant_message.content
# Usage
answer = run_agent("What's the weather in San Francisco?")
print(answer)

This ~50 lines of code taught me more than weeks of LangChain tutorials.

Building with Claude API

I also tested with Claude’s API to understand the differences:

claude_agent.py
import anthropic
import json
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}]
def get_weather(city: str) -> str:
return f"Weather in {city}: Sunny, 72F"
def run_agent(user_message: str):
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages,
tools=tools
)
# Check for tool use
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
if block.name == "get_weather":
result = get_weather(block.input["city"])
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": result
}]
})
else:
# Get text response
for block in response.content:
if hasattr(block, "text"):
return block.text
# Usage
answer = run_agent("What's the weather in Tokyo?")
print(answer)

Both APIs work similarly. The key insight is seeing exactly how the loop works: send message, check for tool call, execute, feed back, repeat.

The reason

Building with raw API calls gave me several benefits:

Debugging superpowers: When my agent failed, I could see exactly where in the loop it went wrong. Was the tool definition incorrect? Did the model not understand when to call it? Was the result not fed back correctly? I could trace each step.

Framework independence: Once I understood the core loop, I could use LangChain, LlamaIndex, or raw API with equal confidence. The framework became a convenience, not a requirement.

Customization ability: I could add features frameworks didn’t support. For example, adding custom logging, modifying the loop for specific use cases, or handling errors in framework-agnostic ways.

Better architecture decisions: I understood tradeoffs at a fundamental level. When should I use multiple tools? When should I use multiple agents? How should I structure state?

Common mistakes to avoid

Mistake 1: Starting with multi-agent frameworks

I initially tried CrewAI before understanding single agents. This was like trying to build a distributed system before understanding how a single process works.

MISTAKE - Don't start here
from crewai import Agent, Task, Crew
# This abstracts too much for a beginner
researcher = Agent(
role="Researcher",
goal="Find information",
backstory="Expert researcher"
)
# You won't understand HOW the agent actually works

Mistake 2: Copying tutorial code without understanding

I followed LangChain tutorials that “just worked” but couldn’t modify them for my needs. I was cargo-culting patterns I didn’t understand.

Mistake 3: Assuming frameworks handle all edge cases

My LangChain agent broke on edge cases the framework didn’t handle. Without understanding the underlying mechanics, I couldn’t fix them.

When to use LangChain

After building my raw API understanding, I now use LangChain for productivity gains. But I use it differently:

langchain_after_understanding.py
from langchain.agents import initialize_agent, Tool
from langchain_openai import ChatOpenAI
# Now I understand what initialize_agent does internally
# I can debug it, modify it, or replace it if needed
def search_tool(query: str) -> str:
return f"Results for: {query}"
tools = [
Tool(
name="Search",
func=search_tool,
description="Search for information"
)
]
llm = ChatOpenAI(model="gpt-4", temperature=0)
agent = initialize_agent(
tools,
llm,
agent="zero-shot-react-description",
verbose=True # Now I can understand the verbose output
)
result = agent.run("What is the weather in San Francisco?")
print(result)

I now see LangChain as a convenience layer on top of patterns I already understand.

Summary

In this post, I explained why you should start with raw API calls instead of LangChain when learning AI agent development. Build the core agent loop yourself using OpenAI SDK or Claude API. Once you understand how function calling, tool execution, and state management work at the API level, then adopt a framework for productivity.

The ~50 lines of raw API code taught me more than weeks of framework tutorials. Frameworks are great once you know what they’re doing for you.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments