Skip to content

Commit f52ca45

Browse files
chat: fix system prompt accumulation across conversation turns (#1076)
## Problem In both `GetChatCompletionStream` and `GetChatCompletionCore`, the system prompt was added as a `ResponseItem.CreateSystemMessageItem(...)` in the input items list on every call. When `previous_response_id` is used on follow-up turns, OpenAI already holds the full conversation server-side — including the system message from turn 1. Every subsequent turn (and every tool-call leg) was adding it again, causing the system prompt to accumulate in context. ## Fix Move the system prompt to `ResponseCreationOptions.Instructions` in `CreateResponseOptionsAsync`, and remove the system message item from both call sites. Per the Azure OpenAI Responses API docs: > "When using along with `previous_response_id`, the instructions from a previous response will not be carried over to the next response. This makes it simple to swap out system (or developer) messages in new responses." `Instructions` is stateless across turns by design — applied fresh each call, never accumulated. ## Changes - **`CreateResponseOptionsAsync`**: Added `string? systemPrompt` parameter; sets `options.Instructions` from it (falling back to `_Options.SystemPrompt`). Changed from `static` to instance method to access `_Options`. - **`GetChatCompletionCore`**: Removed `systemPrompt` parameter; input list is always `[ResponseItem.CreateUserMessageItem(prompt)]`. - **`GetChatCompletionStream`**: Same simplification — no system message item in input list. - **`GetChatCompletion`**: Passes `systemPrompt` to `CreateResponseOptionsAsync` instead of downstream to `GetChatCompletionCore`. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com>
1 parent bc6948e commit f52ca45

1 file changed

Lines changed: 16 additions & 17 deletions

File tree

EssentialCSharp.Chat.Shared/Services/AIChatService.cs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ public AIChatService(IOptions<AIOptions> options, AISearchService searchService,
5454
bool enableContextualSearch = false,
5555
CancellationToken cancellationToken = default)
5656
{
57-
var responseOptions = await CreateResponseOptionsAsync(previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken);
57+
var responseOptions = await CreateResponseOptionsAsync(systemPrompt, previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken);
5858
var enrichedPrompt = await EnrichPromptWithContext(prompt, enableContextualSearch, cancellationToken);
59-
return await GetChatCompletionCore(enrichedPrompt, responseOptions, systemPrompt, mcpClient, cancellationToken);
59+
return await GetChatCompletionCore(enrichedPrompt, responseOptions, mcpClient, cancellationToken);
6060
}
6161

6262
/// <summary>
@@ -82,17 +82,12 @@ public AIChatService(IOptions<AIOptions> options, AISearchService searchService,
8282
bool enableContextualSearch = false,
8383
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
8484
{
85-
var responseOptions = await CreateResponseOptionsAsync(previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken);
85+
var responseOptions = await CreateResponseOptionsAsync(systemPrompt, previousResponseId, tools, reasoningEffortLevel, mcpClient: mcpClient, cancellationToken: cancellationToken);
8686
var enrichedPrompt = await EnrichPromptWithContext(prompt, enableContextualSearch, cancellationToken);
8787

88-
// Construct the user input with system context if provided
89-
var systemContext = !string.IsNullOrWhiteSpace(systemPrompt) ? systemPrompt : _Options.SystemPrompt;
90-
9188
// Create the streaming response using the Responses API
9289
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
93-
List<ResponseItem> responseItems = systemContext is not null
94-
? [ResponseItem.CreateSystemMessageItem(systemContext), ResponseItem.CreateUserMessageItem(enrichedPrompt)]
95-
: [ResponseItem.CreateUserMessageItem(enrichedPrompt)];
90+
List<ResponseItem> responseItems = [ResponseItem.CreateUserMessageItem(enrichedPrompt)];
9691
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
9792
var streamingUpdates = _ResponseClient.CreateResponseStreamingAsync(
9893
responseItems,
@@ -259,7 +254,8 @@ private async Task<string> EnrichPromptWithContext(string prompt, bool enableCon
259254
/// Creates response options with optional features
260255
/// </summary>
261256
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
262-
private static async Task<ResponseCreationOptions> CreateResponseOptionsAsync(
257+
private async Task<ResponseCreationOptions> CreateResponseOptionsAsync(
258+
string? systemPrompt = null,
263259
string? previousResponseId = null,
264260
IEnumerable<ResponseTool>? tools = null,
265261
ResponseReasoningEffortLevel? reasoningEffortLevel = null,
@@ -270,6 +266,14 @@ private static async Task<ResponseCreationOptions> CreateResponseOptionsAsync(
270266
var options = new ResponseCreationOptions();
271267
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
272268

269+
// Set the system prompt via Instructions — this is stateless across turns when using previous_response_id,
270+
// preventing accumulation of system messages in the conversation context.
271+
var resolvedSystemPrompt = !string.IsNullOrWhiteSpace(systemPrompt) ? systemPrompt : _Options.SystemPrompt;
272+
if (!string.IsNullOrWhiteSpace(resolvedSystemPrompt))
273+
{
274+
options.Instructions = resolvedSystemPrompt;
275+
}
276+
273277
// Add conversation context if available
274278
if (!string.IsNullOrEmpty(previousResponseId))
275279
{
@@ -318,17 +322,12 @@ private static async Task<ResponseCreationOptions> CreateResponseOptionsAsync(
318322
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
319323
ResponseCreationOptions responseOptions,
320324
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
321-
string? systemPrompt = null,
322325
McpClient? mcpClient = null,
323326
CancellationToken cancellationToken = default)
324327
{
325-
// Construct the user input with system context if provided
326-
var systemContext = !string.IsNullOrWhiteSpace(systemPrompt) ? systemPrompt : _Options.SystemPrompt;
327-
328+
// Create the response using the Responses API
328329
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
329-
List<ResponseItem> responseItems = systemContext is not null
330-
? [ResponseItem.CreateSystemMessageItem(systemContext), ResponseItem.CreateUserMessageItem(prompt)]
331-
: [ResponseItem.CreateUserMessageItem(prompt)];
330+
List<ResponseItem> responseItems = [ResponseItem.CreateUserMessageItem(prompt)];
332331
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
333332

334333
const int MaxToolCallIterations = 10;

0 commit comments

Comments
 (0)