In LangChain, how does a ChatModel differ from the older text-completion LLM interface?
answer
- two base classes, not one
- roles in, object out
- the return type is the tell
- tool_calls and usage live on the response
- string input is just sugar
basics
~20 sA ChatModel takes a list of role-tagged messages and returns an AIMessage object; the LLM interface takes a plain string and returns a plain string. Tool calling, structured output and multimodal content exist only on the ChatModel path.
solid answer
~40 sLangChain has two model base classes. `BaseLLM` is the original text-completion abstraction: string in, string out. `BaseChatModel` — what `ChatOpenAI` and every current provider integration subclasses — takes a list of messages (`SystemMessage`, `HumanMessage`, `AIMessage`, `ToolMessage`) and returns an `AIMessage`. That return object is the real difference: it carries `.content`, plus `.tool_calls`, `.usage_metadata` and `.response_metadata`, so tool calling, structured output and token accounting all have somewhere to live. As a convenience a chat model's `invoke()` also accepts a bare string and wraps it in a `HumanMessage`, which is why beginners often can't tell the two apart. In LangChain 1.x the completion `LLM` path is effectively legacy — you meet it only with older completion endpoints or some local models — and everything new is built on chat models.
code
python · 12 linesfrom langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = model.invoke([
SystemMessage("You are terse."),
HumanMessage("Name one primary colour."),
])
print(type(response)) # AIMessage, not str
print(response.content)
print(response.usage_metadata)go deeper
Be able to say plainly that a chat model takes messages and returns an AIMessage while the LLM interface takes and returns strings, and name the four common message types.
Explain what lives on the returned AIMessage — content, tool_calls, usage_metadata, response_metadata — and why tool calling and structured output are only possible on that path.
Show what a migration off the completion interface actually costs: downstream code expecting strings, hand-assembled role prefixes, and logging or caching keyed on the old output shape.
Own the argument for standardizing on one abstraction across services: normalized message shapes are what make provider swaps, cost accounting and tracing uniform, and mixed LLM/ChatModel codebases forfeit all three.
## The two abstractions LangChain ships two model base classes in `langchain-core`. `BaseLLM` models a **text completion** endpoint: you hand it a string, you get a string back. `BaseChatModel` models a **chat completion** endpoint: you hand it an ordered list of messages, you get one `AIMessage` back. Provider packages implement one or both — `langchain-openai`, for example, exposes `OpenAI` (completion) and `ChatOpenAI` (chat). In LangChain 1.x essentially all active development, documentation and integrations target `BaseChatModel`. ## Messages, not strings The chat interface's input is a list of typed messages: - `SystemMessage` — instructions that frame the whole conversation. - `HumanMessage` — what the user said. - `AIMessage` — what the model previously said, including any tool calls it requested. - `ToolMessage` — the result of executing a tool call, tied back by `tool_call_id`. Each message's `content` may be a plain string or a list of content blocks, which is how images, files and other non-text parts are represented. LangChain 1.x also exposes a `content_blocks` view on messages so you can read those parts in a provider-neutral shape rather than each vendor's raw JSON. This typing is not decoration. Roles are what let a provider distinguish instruction from user input, what lets a conversation be replayed, and what lets a tool result be attributed to the request that produced it. A single flat prompt string can only fake all of this with delimiters. ## What comes back A chat model returns an `AIMessage`, not a string. Useful attributes: - `.content` — the text (or content blocks) the model produced. - `.tool_calls` — a normalized list of requested tool invocations, each with `name`, `args` and `id`. - `.usage_metadata` — standardized `input_tokens`, `output_tokens`, `total_tokens`. - `.response_metadata` — provider-specific extras such as finish reason and the raw usage payload. - `.id` — the provider's response identifier, useful for correlating traces and support tickets. Because the response is an object, LangChain can standardize across providers: OpenAI, Anthropic and Google all return their own wire formats, and the integration maps them into the same `AIMessage` shape. That normalization is most of what the chat abstraction buys you. ## The convenience that confuses people `model.invoke("hello")` works on a chat model. It does not mean the model is a completion LLM; the string is coerced into a single `HumanMessage`. Likewise `invoke()` accepts a list of `(role, content)` tuples or a prompt value produced by a prompt template. The output type is the tell: a chat model always returns an `AIMessage`, a completion LLM always returns `str`. ## Why the chat path won Every capability that matters in production is defined in terms of messages. Tool calling requires a place to put structured call requests and their results. Provider-native structured output is expressed as a tool schema or a JSON-schema response format attached to a chat request. Multimodal input is a list of content blocks on a message. Prompt caching, reasoning tokens and cached-input accounting all report through per-message usage metadata. None of that has a natural home in a string-in/string-out API, which is why providers themselves have deprecated or frozen their completion endpoints. ## When you still meet the LLM interface Some self-hosted and base (non-instruction-tuned) models are genuinely completion models, and a few local runtimes expose only that. Older code and tutorials are full of it. If you inherit such code, the practical migration is: swap the class for the chat equivalent, move any hand-rolled `"System: ... User: ..."` prompt assembly into real `SystemMessage`/`HumanMessage` objects, and change downstream code that assumed a string — most commonly by adding `StrOutputParser`, which pulls `.content` out of the `AIMessage`. ## Practical consequences - Anything downstream that wants a plain string must extract it; `StrOutputParser` exists for exactly this. - Token accounting, caching keys and tracing all key off messages, so message construction is not a formatting detail — it changes cost. - Conversation history is a list of messages you own and pass back in; the model itself is stateless between calls. ## Interview framing Say the difference in one line (messages and an `AIMessage` versus strings), then immediately name what the object buys: tool calls, usage metadata, multimodal content blocks. Add that the completion interface is legacy in LangChain 1.x. That ordering shows you understand *why* the abstraction exists rather than just what its signature is.
- If invoke() accepts a bare string on a chat model, what exactly does LangChain do with it?It coerces the string into a single `HumanMessage` and sends a one-message conversation. No system message is added, so any framing you rely on has to be passed explicitly. The return value is still an `AIMessage`, which is how you can tell you are on the chat path rather than the completion path.
- Your chain used to end in an LLM and downstream code expects a string. What changes when you move to a ChatModel?Downstream code now receives an `AIMessage`. Either read `.content` explicitly or append `StrOutputParser`, which extracts the text. Anything that logged or hashed the raw output needs updating too, since the object's repr differs. In exchange you gain `tool_calls`, `usage_metadata` and `response_metadata`, which the string interface never exposed.
- Where do multimodal inputs like images fit in this model?They are content blocks on a message rather than a separate API. A `HumanMessage` can carry a list of blocks mixing text with image or file parts, and LangChain 1.x exposes `content_blocks` so you can read them in a provider-neutral shape. The completion `LLM` interface has no representation for this at all.
A completion LLM is a note slipped under a door; a chat model is a transcript with named speakers, where the reply comes back on letterhead carrying its own metadata.
saying these in an interview costs you the question
- Saying ChatModel is just an LLM with a nicer prompt template
- Claiming invoke() returns a string from a chat model
- Treating the completion LLM interface as the current default
- Assuming the model remembers prior turns without you resending messages
- Thinking role tags are only prompt formatting with no API meaning