skip to content

In LangChain, how does a ChatModel differ from the older text-completion LLM interface?

level: juniorimportance: must knowfreq 78%

answer

  1. two base classes, not one
  2. roles in, object out
  3. the return type is the tell
  4. tool_calls and usage live on the response
  5. string input is just sugar

basics

~20 s

A 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 s

LangChain 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 lines
python
from 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context