Messages, not strings
A chat model takes a list of role-tagged messages and returns one. Understanding that list — and that the model remembers nothing between calls — explains most surprising behaviour.
Three roles and one reply
A chat request is a list. system carries the standing instruction, human is a turn from the user, ai is a previous reply from the model. The model reads the whole list and returns a single new ai message. That is the entire protocol.
The list is also the model’s only memory. Nothing is retained between calls: if you want the model to know what it said two turns ago, you send those turns again. A conversation is a growing list that you keep and resend, not a session the model holds.
A two-turn conversation makes the mechanics obvious. After the first reply, you append the assistant message and the new question to the list and send the whole thing again. The model sees its own earlier sentence as an ai message, not as a memory it kept. Remove that message and the follow-up “and the second one?” has nothing to refer to, even though the same model process answered a second earlier.
Statelessness is a feature
Because each call is independent, you can replay it, test it and change it. You can send a different history and get a different answer, which is exactly what makes evaluation possible. A stateful model would make every request depend on everything that came before it on that machine.
It also means the bill grows with the conversation. Every turn resends the earlier ones, so a long chat costs more per message than a short one. That is why trimming and summarising history are part of the design, not an optimisation added later.
Invoking a chat model
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
messages = [
SystemMessage("Answer in one sentence."),
HumanMessage("What does a vector store do?"),
]
reply = model.invoke(messages)
print(type(reply).__name__) # AIMessage
print(reply.content)Render a message list
Run it, then add a third turn where the user says “and the second one?”. Notice that the fake model only sees the list you pass — nothing is remembered for you.
What the model sees is what you sent
When a reply is strange, print the message list rather than the prompt string. A missing system turn, two human turns in a row, or a tool message with no matching call id all look fine in a flattened transcript and wrong in the list. The model has no other view; the list is the entire request.
A common mistake is assuming the framework remembers the system message for you. It does not; you pass it every time. The symptom is an assistant that follows the rules in the first turn and forgets them in the third, because a history-building function dropped the system entry when it trimmed the list.
Temperature is a dial, not a personality
Low temperature makes the model pick the most likely continuation, which is what you want for extraction, classification and anything with a correct answer. Higher temperature samples less likely continuations, which is useful for brainstorming and harmful for parsing.
It does not make a model “creative” in the human sense and it cannot fix a bad prompt. If the instructions are ambiguous, a low temperature gives you the same wrong answer every time, which at least makes the bug reproducible.
Log the exact message list you sent, not just the final prompt string. When a reply is odd, the cause is usually a message that was not what you assumed — a missing system turn, or two human turns in a row.
The reply is an object
A response carries more than text: the role, sometimes tool calls, token usage and a finish reason. Reading .content alone is fine until you need to know whether the model stopped because it finished or because it hit the length limit.
A finish reason of length means the answer was cut off mid-sentence. Treating that as a complete answer is a common source of “the model ignored my instructions” — it did not; it ran out of room.
Read the finish reason before the content. A truncated reply can still parse, still look confident, and still be wrong. The check is one line and it prevents a whole class of confusing downstream failures. The same object also carries token usage, which is the only reliable way to see what a conversation costs before the invoice arrives.
The model remembers nothing
Call `answer` twice with the same question but different histories and compare. Then remove the history argument and see how the second reply loses the context.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Which role carries the standing instruction?
