Reducing Chat History with UseChatReducer
As a conversation grows, sending the entire chat history to the model on every request becomes expensive and eventually hits token limits. Microsoft.Extensions.AI addresses this with chat reduction: the UseChatReducer extension method adds a ReducingChatClient to the ChatClientBuilder pipeline. Every time GetResponseAsync/GetStreamingResponseAsync is called, the ReducingChatClient shrinks the message list via IChatReducer.ReduceAsync before it is sent to the actual model. As a result, even as the conversation history keeps growing, fewer messages (and fewer tokens) are sent to the model on each call.
Note that this feature is currently experimental, so the compiler warning MEAI001 must be suppressed to use it.
Two Built-in IChatReducer Implementations
- MessageCountingChatReducer: preserves the system message and keeps only the most recent N messages from the rest (a sliding-window approach). Older messages beyond the limit are discarded entirely.
- SummarizingChatReducer: summarizes older messages using an
IChatClient. While the number of messages decreases, the context is largely preserved because it is folded into a summary instead of being dropped.
References
- Chat reduction (experimental)
- ReducingChatClientBuilderExtensions.UseChatReducer
- MessageCountingChatReducer
- SummarizingChatReducer
Step-by-Step Walkthrough
1. Setup
The API key is read from the OPEN_AI_KEY environment variable, and a base IChatClient is created targeting the gpt-4o-mini model. This base client will later be wrapped with different reducers to demonstrate both strategies.
2. Example 1: Limiting Message Count with MessageCountingChatReducer
A MessageCountingChatReducer is created with targetCount: 4. This means the system message is always kept, and at most 4 additional messages (roughly the last 2 question/answer turns) are forwarded to the model; anything older beyond that limit is dropped completely.
The reducer is attached to the pipeline via UseChatReducer, so the ReducingChatClient applies it automatically on every call. In the example, the reducer is also invoked directly (without making a network call) purely to observe how many messages would actually be sent to the model at each turn, which is useful for demonstrating the sliding-window effect. Note that the local history list keeps growing regardless—the reducer only shrinks what is sent to the model, not the local in-memory history (unless you explicitly replace it with the reduced result).
3. Example 2: Summarizing Old Messages with SummarizingChatReducer
A SummarizingChatReducer is created with targetCount: 2 and threshold: 3. This means nothing happens until the total message count exceeds targetCount + threshold = 5. Once that threshold is crossed, the reducer kicks in: it summarizes the older messages (using the provided chat client) and collapses the history back down to about 2 messages (a summary plus any preserved messages such as the system message). Unlike MessageCountingChatReducer, information is not discarded—it is compressed into a summary, so context is retained even as the message count shrinks.
Full Code Example
Summary
UseChatReducer lets you keep sending long-running conversations to the model without exceeding token limits or paying for redundant context on every call. MessageCountingChatReducer is a simple, cheap sliding-window strategy that discards old messages outright, which is ideal when older context is no longer relevant. SummarizingChatReducer trades an extra model call for preserving the gist of older context through summarization, which is ideal when the conversation needs to "remember" details from far earlier turns. Both reducers plug into the same ChatClientBuilder pipeline via UseChatReducer, so switching strategies is just a matter of swapping the IChatReducer implementation.