How to build a LangGraph agent from scratch
LangGraph installed, Python 3.10+
What this does
Building a LangGraph agent from scratch creates a stateful, graph-based AI agent that uses a defined workflow of nodes and edges to process user requests. Unlike linear chain agents, LangGraph agents support branching logic, cyclical reasoning patterns, and conditional routing between nodes. The agent maintains state across steps, enabling multi-turn interactions with memory. The graph structure makes the agent's decision path transparent and debuggable.
Steps
Define the agent's state schema using TypedDict: class AgentState(TypedDict): messages: Annotated[list, add_messages]; next_step: str; tool_results: dict. Initialize the LLM: from langchain_ollama import ChatOllama; llm = ChatOllama(model="llama3", temperature=0). Define the tool set. Create a simple calculator tool and a web search tool using the @tool decorator: from langchain.tools import tool; @tool def calculator(expression: str) -> str: return str(eval(expression)). Bind tools to the LLM: llm_with_tools = llm.bind_tools([calculator, web_search]). Build the graph. Import StateGraph and define nodes. The agent node calls the LLM with the current messages and returns the response: def agent_node(state: AgentState): response = llm_with_tools.invoke(state["messages"]); return {"messages": [response]}. The tool_executor node detects tool calls in the last message and executes them: def tool_node(state: AgentState): last_msg = state["messages"][-1]; results = []; for tool_call in last_msg.tool_calls: result = tool_map[tool_call["name"]].invoke(tool_call["args"]); results.append(ToolMessage(content=result, tool_call_id=tool_call["id"])); return {"messages": results}. Add a should_continue routing function: def route(state): return "tool_executor" if state["messages"][-1].tool_calls else END. Create the graph: workflow = StateGraph(AgentState); workflow.add_node("agent", agent_node); workflow.add_node("tool_executor", tool_node); workflow.set_entry_point("agent"); workflow.add_conditional_edges("agent", route); workflow.add_edge("tool_executor", "agent"). Compile: app = workflow.compile(). Run: result = app.invoke({"messages": [HumanMessage(content="What is 25 * 4?")]}).
Record the local run evidence. Save the exact command, runtime or package version, model name if applicable, and observed output so the result can be reproduced later.
Confirm the local starting state. Print the active binary, package version, model name, or configuration path before changing the workflow.
Run the smallest complete path. Execute the minimum command or script that proves the guide works end to end on the local machine.
Compare against expected output. Check the final line, status code, generated artifact, or model response against the verification section before expanding the setup.
Record the local run evidence. Save the exact command, runtime or package version, model name if applicable, and observed output so the result can be reproduced later.
Verification
Run the agent with a simple arithmetic query and verify the final answer contains the correct result. Check the intermediate messages in result["messages"]—there should be HumanMessage, AIMessage with tool_calls, ToolMessage, and final AIMessage. Run a web search query and verify the tool was called. Test a query that requires no tools: "Hello" should return a greeting without triggering the tool executor. Verify the agent handles multiple sequential tool calls in one run by asking "What is 5+3 and then multiply that by 2?"
Common failures
LLM fails to call tools: Verify the model supports function calling—use bind_tools() correctly and check that the tool schema format matches the model's expectations. Infinite tool-calling loop: Add a max iteration check in the should_continue function: if iteration_count > 10: return END. Tool execution errors crash the graph: Wrap tool calls in try/except in the tool_node and return error messages as ToolMessage so the agent can self-correct. State not accumulating messages: Use Annotated[list, add_messages] in the TypedDict definition for the messages field. Model outputs malformed JSON for tool args: Set strict=True in the tool definition and add a format instruction to the system prompt.
- Version mismatch - The installed package or runtime differs from the command shown; check the version first and rerun the smallest verification command.
- Local environment drift - Another service, virtual environment, model, or path is being used; print the active binary path and configuration before changing the guide steps.
Related guides
- build-multi-agent-supervisor-workflow
- setup-agent-tool-use-function-calling
- build-code-generation-agent-local-models