spring-ai
spring-ai copied to clipboard
[ChatClient] Inconsistent handling of system messages
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:
SystemMessagebuilt fromsystem()UserMessagebuilt fromuser()
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 frommessages()SystemMessagebuilt fromsystem()UserMessagebuilt fromuser().
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:
SystemMessagebuilt fromsystem()List<Message>built frommessages()UserMessagebuilt fromuser().
When messages() is used and it DOES include any SystemMessage, I expect the following List<Message> passed to the Prompt:
List<Message>built frommessages()(including one or moreSystemMessage)SystemMessagebuilt fromsystem()UserMessagebuilt fromuser().
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.