← Back to Blog
OpenAI / AI · LangChain

Building LangChain Agents with Tool Use, Memory & Multi-Step Reasoning

✍️ Hamza Bilal 📅 September 2024 ⏱ 13 min read
LangChainOpenAIAI AgentsPython

LangChain agents are the most powerful abstraction I've used for building AI that can take real actions — not just generate text. This guide covers how I built production-grade agents for a legal research platform (Signal Law Group) that could search case law, cross-reference statutes, and generate structured reports — autonomously.

What is a LangChain Agent?

A LangChain agent is a loop: the LLM decides which tool to call → the tool runs → the result goes back to the LLM → repeat until done. Unlike a simple chain (linear), an agent can decide its own path.

User input → Agent → [Thinks: "I need to search first"]
  → Calls search tool → Gets result
  → [Thinks: "Now I need to filter by date"]
  → Calls filter tool → Gets result
  → [Thinks: "I have enough to answer"]
  → Returns final answer

The Basic Setup

pip install langchain langchain-openai langchain-community
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool

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

Defining Custom Tools

Tools are just Python functions with a @tool decorator. The docstring becomes the tool description that the LLM reads to decide when to call it.

@tool
def search_case_law(query: str) -> str:
    """Search the case law database for relevant legal cases.
    Use this when the user asks about legal precedents or case references.
    Input: a search query string."""
    # Call your actual search API here
    results = legal_db.search(query, top_k=5)
    return "\n".join([f"- {r.title} ({r.year}): {r.summary}" for r in results])

@tool
def get_statute_text(statute_id: str) -> str:
    """Retrieve the full text of a statute by its ID.
    Use this when you have a specific statute reference to look up."""
    return statute_db.get(statute_id)

@tool
def create_report(title: str, findings: str, citations: str) -> str:
    """Create a structured legal research report.
    Use this as the final step when you have all findings ready."""
    report = LegalReport(title=title, findings=findings, citations=citations)
    report.save()
    return f"Report created: {report.id}"

Building the Agent with Memory

from langchain.memory import ConversationBufferWindowMemory

# Memory: keep last 10 turns in context
memory = ConversationBufferWindowMemory(
    memory_key="chat_history",
    return_messages=True,
    k=10
)

prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a legal research assistant. Your job is to:
1. Search for relevant case law using the search_case_law tool
2. Look up specific statutes when referenced
3. Synthesize findings into a structured report using create_report

Always cite your sources. When uncertain, search again rather than guessing."""),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

tools = [search_case_law, get_statute_text, create_report]

agent = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    memory=memory,
    verbose=True,
    max_iterations=10,  # prevent infinite loops
    handle_parsing_errors=True
)

Running the Agent

result = agent_executor.invoke({
    "input": "Research cases about AI liability in autonomous vehicle accidents from 2020-2024. Create a report."
})
print(result["output"])

The agent will: search for cases → retrieve relevant statute text → synthesize → call create_report → return the report ID. All automatically, without you specifying the steps.

Adding Persistent Memory with Redis

For production, in-memory conversation history is lost on restart. Use Redis-backed memory:

from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

store = {}

def get_session_history(session_id: str):
    return RedisChatMessageHistory(
        session_id=session_id,
        url="redis://localhost:6379"
    )

agent_with_history = RunnableWithMessageHistory(
    agent_executor,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history"
)

Production Gotchas

1. Always set max_iterations

Without a limit, a confused agent can loop indefinitely. I set max_iterations=15 and handle the AgentFinish vs AgentAction states explicitly.

2. Tool descriptions are critical

The LLM decides which tool to call based entirely on the docstring. Vague descriptions lead to wrong tool calls. Be explicit about when each tool should be used and what its input format is.

3. Rate limiting on tool calls

If your tools call external APIs, add rate limit handling inside the tool function itself — don't rely on the agent to space out calls.

4. Streaming for long-running agents

async for event in agent_executor.astream_events(
    {"input": user_query}, version="v1"
):
    if event["event"] == "on_tool_start":
        print(f"Calling tool: {event['name']}")
    elif event["event"] == "on_llm_stream":
        print(event["data"]["chunk"].content, end="", flush=True)

Real results from the Signal Law Group project: The agent reduced legal research time from 4–6 hours per topic to 8–12 minutes. Accuracy (verified by a paralegal) was 91% — comparable to junior associate research quality.

When to Use LangChain Agents vs Simple Chains

Need a custom AI agent for your research or automation workflow? I build these from scratch with production deployment. Let's talk.

Hire Me For This

I build custom AI agents with LangChain, OpenAI Assistants API, and Agno — autonomous, multi-tool, and production-ready.