Microsoft.Extensions.AI 10.10 如何用 Reducer 縮減聊天記錄佔用的上下文窗口
Your Chat History Is Eating the Context Window — Fix It With a Reducer
在 Microsoft.Extensions.AI 10.10 中,IChatReducer、ReducingChatClient 和 UseChatReducer 可用於縮減送入模型的聊天記錄。
Turn nine of a support chat feels fine. Turn ninety is a different animal. Every request resends the system prompt, three agent loops of tool noise, the user's third restatement of the same bug, and yesterday's dark-mode tangent. The model does not mind. Your invoice does — and attention quality usually dies before the hard token limit.
messages.TakeLast(20) in one controller is not a policy. The second entry point forgets it. A tool-using agent keeps a function call without its result. Someone asks why the bot forgot the account ID from turn four.
Microsoft.Extensions.AI 10.10 (experimental MEAI001) gives you IChatReducer + ReducingChatClient + UseChatReducer. This post is the short "what breaks first" guide. The full offline-tested project (counting, summarizing, token-aware reducers, session store, tests) is in Tech Skill Builder — $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10.
What a reducer actually does
Task<IEnumerable<ChatMessage>> ReduceAsync(
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken);
ReducingChatClient runs that on the inbound list, then forwards the reduced list to the inner client for both normal and streaming calls:
IChatClient client = new ChatClientBuilder(inner)
.UseChatReducer(reducer)
.Build();
Reduction is about what the model sees. Keep the full transcript in your session store for GET /history. Do not delete the system of record because you trimmed the wire payload.
Trap 1: Orphan tool pairs
Naive "keep last N" walks into this the first week you enable tools. A FunctionCallContent without its matching FunctionResultContent (or the reverse) gets you provider 400s that look like model flakiness.
Built-in counting and summarizing reducers exclude tool call/result messages from the reduced view. Your raw store can still keep them for debug. If you write a custom reducer, plant a mixed history in a unit test and assert neither content type survives unless you deliberately opt out.
Trap 2: Counting messages when finance counts tokens
MessageCountingChatReducer(targetCount) keeps the first system message and up to targetCount recent non-system turns. Cheap and predictable for FAQ / short troubleshooting. Six huge pasted stack traces still blow the bill.
A custom TokenAwareChatReducer estimates tokens (characters÷4 is fine as a guardrail), always keeps the first system prompt, walks newest → oldest until the budget fills, and drops tool pairs by default:
public sealed class TokenAwareChatReducer : IChatReducer
{
public TokenAwareChatReducer(int tokenBudget, bool dropToolPairs = true) { /* ... */ }
public Task<IEnumerable<ChatMessage>> ReduceAsync(
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken)
{
// keep system → walk newest→oldest under budget → drop incomplete tool pairs
// full implementation in the complete project
return Task.FromResult<IEnumerable<ChatMessage>>(/* reduced */);
}
}
Trap 3: Summarizing with the flagship model
SummarizingChatReducer(summarizer, targetCount, threshold) fires when unsummarized non-system messages exceed targetCount + threshold. Older turns become one assistant summary; recent turns stay verbatim; system stays; tool content stays out of summarizer input.
Point the summarizer at a small, cheap (or offline) client. Reusing your frontier model for every compaction invents a second invoice while you celebrate the first savings. Also: summarization may stamp __summary__ into AdditionalProperties on a boundary message. Preview endpoints should clone messages first so you do not mutate the live session store.
Trap 4: Forgetting the system prompt is sacred
Silence the system message to "save tokens" and you have a different product. Every reducer shape above keeps it. If the system prompt alone is huge, fix the prompt — do not quietly drop it in production.
Minimal session surface
| Method | Path | Purpose |
|---|---|---|
| POST | /api/sessions |
create session |
| POST | /api/sessions/{id}/messages |
chat (returns before/after counts) |
| GET | /api/sessions/{id}/history |
raw transcript |
| GET | /api/sessions/{id}/reduced |
counting / summarizing / token preview |
Config: mode, target count, threshold, token budget, system prompt. One pipeline shared by API, worker, and CLI. Swap the offline inner client for OpenAI/Azure/Ollama later — keep the reducer layer.
Pin Microsoft.Extensions.AI 10.10 and treat MEAI001 as usable-but-evolving. Experimental does not mean toy; it means names can move.
Want the runnable .NET 10 package with counting / summarizing / token-aware modes, an offline summarizer, and session APIs? The complete working project is in Tech Skill Builder — $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10.
來源:DEV Community · dev.to