Building with LangChain Core: models, messages, tools, and the loop
langchain-core is the bottom layer, where you control everything. Here's how to actually use its building blocks — one model interface, typed messages, the @tool decorator, the hand-written loop, and structured output.
In the LangChain-ecosystem-layers post, langchain-core sits at the bottom: maximum control, minimum magic. This is the hands-on version — how to build with its five components and write the agent loop yourself. It's worth doing at least once, because everything above it (langgraph, create_agent) is sugar over exactly these parts.
Models: one interface to every LLM
A chat model is a single interface to every provider. You invoke it for a full response or stream it for tokens, and you swap providers by swapping the class — the calling code doesn't change.
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model='gpt-4o')
model.invoke('Explain embeddings in one line.') # or .stream(...) for tokens
# switch to ChatAnthropic(...) and the rest of your code is unchangedMessages: typed objects, not role dicts
Instead of raw {'role': 'user'} dictionaries, langchain-core uses typed message objects — SystemMessage, HumanMessage, AIMessage, ToolMessage. They carry structure (like the tool calls on an AIMessage) that plain dicts can't, which is what makes the loop below clean.
Tools: the @tool decorator
Decorate a plain function with @tool and its docstring and type hints become the schema the model sees — no hand-written JSON Schema.
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Return the weather for a city.""" # docstring + hints = the schema
return lookup(city)The loop: bind_tools, read tool_calls, return ToolMessage
At this layer the agent loop is yours. Bind the tools to the model, read the tool calls off the response, run each one, append a ToolMessage, and call the model again until it stops asking for tools. This is the exact loop create_agent and langgraph build for you — here you can see every step.
from langchain_core.messages import ToolMessage
model = ChatOpenAI(model='gpt-4o').bind_tools([get_weather])
ai = model.invoke(messages)
for call in ai.tool_calls: # the model requested these
result = run(call['name'], call['args'])
messages.append(ToolMessage(result, tool_call_id=call['id']))
# loop: invoke again until ai.tool_calls is empty, then return ai.contentStructured output: Pydantic via with_structured_output
When you need a typed object instead of prose, wrap the model with a Pydantic schema and it returns an instance, validated for you (the structured-outputs post covers why this matters).
from pydantic import BaseModel
class Extract(BaseModel):
name: str
urgency: str
structured = ChatOpenAI(model='gpt-4o').with_structured_output(Extract)
structured.invoke('...') # returns an Extract instanceWrite the loop by hand once, at the core layer, and every agent framework above it stops being magic — it's just this, with the plumbing hidden.