What Actually Happens When Spring AI's ChatClient Calls a Model
Misreading `call()` as the network trigger leads to misplaced breakpoints, wrong latency measurements, and confusion about when Advisors execute. Knowing that `content()` is the real trigger and that the Advisor Chain wraps the model call explains where to inject logging, retries, and tool-calling logic.
Spring AI 2.0's fluent API looks deceptively simple: four method calls and you get a string back from an LLM. Tracing the source reveals that `call()` does not invoke the model at all. It constructs a `DefaultCallResponseSpec` and assembles an Advisor Chain, appending `ChatModelCallAdvisor` and `ChatModelStreamAdvisor` to the end. The actual network request waits until `content()`, `chatResponse()`, or `entity()` is called.
Before that point, `prompt()` clones a fresh `DefaultChatClientRequestSpec` from the client's defaults, and `user("你好")` merely stores the text string. Message assembly into a `UserMessage` and `Prompt` object happens later inside `DefaultChatClientUtils#toChatClientRequest()`, which packages both the `Prompt` and an Advisor context map into a `ChatClientRequest` record. The Advisor Chain then processes this request in a classic around-chain pattern, with `ChatModelCallAdvisor` at the tail extracting the `Prompt` and delegating to the vendor-agnostic `ChatModel` interface.
`OpenAiChatModel` translates Spring AI's `Prompt` into the OpenAI Java SDK's `ChatCompletionCreateParams`, fires the HTTP request, and maps the response back through `ChatResponse` → `Generation` → `AssistantMessage` → text. The `ChatClient.Builder` itself is a prototype-scoped bean, so every `build()` call produces an independent `DefaultChatClient` with its own default configuration.
Spring AI's decision to name the method `call()` when it does not call the model is a persistent source of confusion that the framework could clarify with a rename like `prepare()` or `build()`.
The prototype-scoped Builder pattern gives flexibility at the cost of debugging indirection: a bean-creation problem now requires tracing through Builder, auto-configuration, and the final `DefaultChatClient`.
Placing `ChatModelCallAdvisor` as a mandatory tail Advisor rather than a direct call inside `ChatClient` is a deliberate design choice that makes every request pass through the same around-chain, enabling uniform cross-cutting concerns but adding a layer of abstraction that obscures the call site.