skip to content

What does invoking a LangChain ChatPromptTemplate return, and why?

level: middleimportance: should knowfreq 38%

answer

  1. not a string, a wrapper
  2. ChatPromptValue versus StringPromptValue
  3. to_messages and to_string
  4. the model picks the shape it wants
  5. invoke takes a dict, format takes kwargs

basics

~20 s

It returns a PromptValue — a ChatPromptValue wrapping the rendered messages — not a string and not a raw list. The wrapper exposes to_messages() and to_string(), so the same rendered prompt can feed either a chat model or a text model.

solid answer

~50 s

Prompt templates are Runnables, so `prompt.invoke({"name": "Ada"})` goes through the same interface as everything else in a chain. What comes out is a `PromptValue`: `ChatPromptValue` from a chat template, `StringPromptValue` from a string template. The reason it is a wrapper rather than the bare rendered output is model-agnosticism — a chat model calls `to_messages()` on whatever it receives, a text model calls `to_string()`, so one prompt object works with either without the chain knowing which sits downstream. If you want the raw output directly, the format methods give it to you: `format()` returns a string, `format_messages()` returns the message list, and `format_prompt()` returns the PromptValue. In a chain, this is invisible plumbing — you rarely name the type — but it explains why printing the result of `prompt.invoke(...)` shows a wrapper object rather than the text you expected.

code

python · 14 lines
python
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "Be brief."),
    ("human", "Say hi to {name}"),
])

value = prompt.invoke({"name": "Ada"})
print(type(value).__name__)   # ChatPromptValue
print(value.to_messages())    # [SystemMessage(...), HumanMessage(...)]
print(value.to_string())      # 'System: Be brief.\nHuman: Say hi to Ada'

# direct formatting, no wrapper
print(prompt.format_messages(name="Ada"))

go deeper

for a junior

Know that invoking a prompt gives you a PromptValue object, and that to_messages() or to_string() gets the actual content out of it.

for a middle

Explain why the wrapper exists — the consumer picks the shape — and distinguish invoke (one dict, returns PromptValue) from format/format_messages (keywords, return raw output).

for a senior

Use it as a debugging lever: invoke the prompt alone and print the message list to see exactly what reaches the provider, and know that flattened-string token counts understate real per-message overhead.

for a principal

Treat the PromptValue boundary as the seam that keeps prompts model-agnostic, so swapping providers or model shapes is a wiring change rather than a prompt rewrite across the codebase.

## The Runnable contract, applied to prompts A prompt template implements the same interface as models and parsers, which is why it can sit at the head of a chain and be invoked, batched, or streamed like anything else. The interface says: take one input, return one output. The input for a prompt is a mapping of variable names to values; the output is a `PromptValue`. ``` prompt = ChatPromptTemplate.from_messages([("human", "Say hi to {name}")]) value = prompt.invoke({"name": "Ada"}) type(value).__name__ # 'ChatPromptValue' value.to_messages() # [HumanMessage(content='Say hi to Ada')] value.to_string() # 'Human: Say hi to Ada' ``` ## Why a wrapper rather than the rendered thing The rendered prompt has two possible shapes — text or messages — and the consumer decides which it wants. If a prompt emitted a bare string, a chat model would have to guess how to split it into messages; if it emitted a bare message list, a text model would have to guess how to flatten it. The `PromptValue` defers that decision to the consumer: it carries the richer representation and knows how to degrade to the poorer one. Concretely, `StringPromptValue` holds text; `to_string()` returns it, `to_messages()` wraps it in a single human message. `ChatPromptValue` holds messages; `to_messages()` returns them, `to_string()` renders a buffer with role prefixes such as `Human:` and `AI:`. Every model implementation accepts a `PromptValue` and calls the method matching its own API shape. This is what makes prompts portable. Swapping the model at the end of a chain does not require touching the prompt, and the same prompt can be reused across a chat model in one path and a text-shaped consumer in another. ## Format methods versus invoke The older, direct methods still exist and are often what you want in a test or a debugging session: - `format(**kwargs)` — string prompts; returns `str`. - `format_messages(**kwargs)` — chat prompts; returns `list[BaseMessage]`. - `format_prompt(**kwargs)` — either; returns the `PromptValue`. - `invoke(dict)` — the Runnable entry point; returns the `PromptValue`. Note the calling convention differs: the format methods take keyword arguments, `invoke` takes a single dict. A very common early mistake is `prompt.invoke(name="Ada")`, which does not match the Runnable signature. Another is `prompt.invoke({"name": "Ada"})` followed by string operations on the result — you have a wrapper, so call `to_string()` or `to_messages()` first. ## What this means when debugging Because the prompt step is a Runnable that returns an inspectable value, you can cut a chain in half and look at exactly what the model will receive: ``` print(prompt.invoke(inputs).to_messages()) ``` That single line resolves a large share of "the model is ignoring my instructions" reports. It shows the actual message list — whether the system message survived, whether history was spliced in where you thought, whether a variable rendered as the literal string `None`, whether a placeholder silently contributed nothing because its key was misspelled and it was optional. It also matters for token accounting. Counting tokens on `to_string()` of a chat prompt is an approximation, because providers add per-message overhead the flattened text does not represent. If you need a real count, count against the message list with the provider's own accounting rather than the flattened string. ## Validation timing Invoking is also where missing variables surface. A template does not check at construction that you will supply everything; it checks when it formats. Passing a dict lacking a declared variable raises then, naming the key. That is a good failure — loud and precise — and it is the reason declared variables beat string concatenation, where the same mistake produces a prompt with a hole in it and no error at all. ## Practical shape in a chain In ordinary use the PromptValue never appears in your code: it is produced by the prompt step and consumed by the model step immediately after. You meet it when you invoke a prompt alone — in a unit test, in a notebook, or when writing a custom step that sits between the prompt and the model. Recognising the type, and knowing that `to_messages()` is the accessor you want, is the whole practical takeaway.

  • Why does invoke take a dict while format takes keyword arguments?
    Because `invoke` is the shared Runnable entry point: every step in a chain receives exactly one input value, so a prompt's many variables have to arrive bundled as a mapping. The `format` family predates and sits below that contract and is a plain Python call, so keywords are natural there. Mixing them up — `prompt.invoke(name="Ada")` — is a frequent first error.
  • How do you inspect exactly what a chain will send to the model?
    Invoke the prompt on its own and print `to_messages()`. You see the real message list — whether the system message is present, where history landed, whether a variable rendered as an empty string or the literal `None`. It is the fastest triage step for "the model is ignoring my instructions", and it needs no model call at all.
  • Is counting tokens on to_string() of a chat prompt accurate?
    Only approximately. Providers add per-message framing overhead that the flattened buffer does not represent, and role prefixes like `Human:` in the flattened text are an artifact of the rendering, not something the provider bills. For a real budget, count against the message list using the provider's own accounting rather than the concatenated string.

saying these in an interview costs you the question

  • Expecting invoke to return a plain string
  • Calling prompt.invoke(name="Ada") with keywords
  • Assuming to_string() output is what a chat model receives
  • Thinking missing variables are caught at construction time
  • Treating StringPromptValue and ChatPromptValue as interchangeable content

context