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
- Use agents when the path to the answer is unknown upfront and the LLM needs to decide which tools to use
- Use simple chains when the steps are fixed and predictable (prompt → OpenAI → parse → done)
- Use agents when you need multi-step reasoning across different data sources
- Use chains when latency is critical — agents add overhead from the decision loop
Need a custom AI agent for your research or automation workflow? I build these from scratch with production deployment. Let's talk.