Skip to content

[ChatClient] Inconsistent handling of system messages #873

Closed
@ThomasVitale

Description

@ThomasVitale

Bug description
The ChatClient API makes it possible to pass system and user messages via the system() and user() clauses.
In that case, the final List<Message> passed to the Prompt contains the following:

  • SystemMessage built from system()
  • UserMessage built from user()

The API also allows passing a list of messages directly via messages().
In that case, the final List<Message> passed to the Prompt contains the following:

  • List<Message> built from messages()
  • SystemMessage built from system()
  • UserMessage built from user().

There is inconsistency in the two scenarios about where will the SystemMessage built from system() end up in the chat history.

Now, imagine using the few-shot prompting strategy and using the messages() clause to pass the few-shot examples (list of UserMessage and AssistantMessage pairs). In that case, the SystemMessage built from system() ends up at the bottom of the list, which makes the few-shot prompting strategy not working in many cases due to the wrong position of the SystemMessage.

A possible workaround is to pass the desired SystemMessage via messages() together with the few-shot examples, perhaps even failing the ChatClient call request if both messages() and system()+user() are defined, but that would limit the convenience of the API.

Environment

  • Spring AI 1.0.0-SNAPSHOT
  • Java 22

Steps to reproduce

Single system message:

var content = chatClient.prompt()
				.system("System text")
				.messages(
						new UserMessage("My question"),
						new AssistantMessage("Your answer")
				)
				.call().content();

Multiple system messages:

var content = chatClient.prompt()
				.system("System text")
				.messages(
						new SystemMessage("Historical system text"),
						new UserMessage("My question"),
						new AssistantMessage("Your answer")
				)
				.call().content();

Expected behavior

I would expect the use of system() to result in a SystemMessage always placed on the top of the message list, unless another one already exists passed via messages().

When messages() is used and it does NOT include any SystemMessage, I expect the following List<Message> passed to the Prompt:

  • SystemMessage built from system()
  • List<Message> built from messages()
  • UserMessage built from user().

When messages() is used and it DOES include any SystemMessage, I expect the following List<Message> passed to the Prompt:

  • List<Message> built from messages() (including one or more SystemMessage)
  • SystemMessage built from system()
  • UserMessage built from user().

There's room for introducing more structured support for few-shot prompting via the Advisor API. I'm working on a few proposals in that direction, but this issue of the SystemMessage might need fixing first.

I have a PR ready for implementing what described above, but I'm not 100% sure it's a good expected behaviour. It might be worth considering this issue in more general terms, including other common prompting strategies and the handling of the chat memory.

@tzolov what do you think? I'd be happy to discuss further about it and perhaps share a few experiments I've been working one.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions