Agentic AIC#verifiedVerified
ReAct Agent Pattern in C#
Interleaves chain-of-thought Reasoning with Action execution, enabling LLMs to dynamically plan, act, and observe in a loop.
How to Implement the ReAct Agent Pattern in C#
1Step 1: Define the Tool and AgentStep types
public interface ITool
{
string Name { get; }
string Description { get; }
Task<string> ExecuteAsync(string input);
}
public record AgentStep(
string Thought, string Action,
string ActionInput, string Observation);2Step 2: Implement the ReAct reasoning loop
public static class ReactLoop
{
private const int MaxSteps = 10;
public static async Task<string> RunAsync(
string query, IReadOnlyList<ITool> tools,
Func<string, Task<string>> llm)
{
var steps = new List<AgentStep>();
for (var i = 0; i < MaxSteps; i++)
{
// Reason about what to do next
var prompt = BuildPrompt(query, tools, steps);
var response = await llm(prompt);
// Parse thought and action from response
var (thought, action, actionInput, isFinal, finalAnswer) =
ParseResponse(response);
if (isFinal) return finalAnswer;
// Execute the chosen tool
var tool = tools.FirstOrDefault(t => t.Name == action)
?? throw new InvalidOperationException(
$"Unknown tool: {action}");
var observation = await tool.ExecuteAsync(actionInput);
steps.Add(new AgentStep(thought, action, actionInput, observation));
}
return "Max steps reached without final answer.";
}3Step 3: Build the prompt and parse LLM responses
private static string BuildPrompt(
string query, IReadOnlyList<ITool> tools, List<AgentStep> steps)
{
var toolNames = string.Join(", ", tools.Select(t => t.Name));
return $"Query: {query}\nTools: {toolNames}";
}
private static (string Thought, string Action, string ActionInput,
bool IsFinal, string FinalAnswer) ParseResponse(string response)
{
return ("", "", "", false, "");
}
}using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.Extensions.Logging;
// [step] Define strongly-typed tool infrastructure
public record ToolResult(bool Success, string Data,
Dictionary<string, object>? Metadata = null);
public interface ITool
{
string Name { get; }
string Description { get; }
Dictionary<string, string> Parameters { get; }
Task<ToolResult> ExecuteAsync(
Dictionary<string, string> input, CancellationToken ct = default);
}
public record AgentStep(
string Thought, string Action,
Dictionary<string, string> ActionInput,
ToolResult Observation, long Timestamp);
public record AgentConfig(
int MaxSteps, string Model, double Temperature,
IReadOnlyList<ITool> Tools, string SystemPrompt);
public record AgentResult(
string Answer, IReadOnlyList<AgentStep> Steps,
int TotalTokens, long DurationMs);
// [step] Production ReAct agent with logging and cancellation
public sealed class ReActAgent(
AgentConfig config, ILogger<ReActAgent> logger)
{
private readonly List<AgentStep> _steps = [];
public async Task<AgentResult> RunAsync(
string query, CancellationToken ct = default)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
_steps.Clear();
var totalTokens = 0;
for (var i = 0; i < config.MaxSteps; i++)
{
ct.ThrowIfCancellationRequested();
logger.LogDebug("ReAct step {Step}/{Max}", i + 1, config.MaxSteps);
var messages = BuildMessages(query);
var response = await CallLlmAsync(messages, ct);
totalTokens += response.Tokens;
if (response.IsFinalAnswer)
{
logger.LogInformation(
"Agent completed in {Steps} steps", i + 1);
return new AgentResult(
response.FinalAnswer, _steps.AsReadOnly(),
totalTokens, sw.ElapsedMilliseconds);
}
var tool = config.Tools.FirstOrDefault(
t => t.Name == response.Action)
?? throw new InvalidOperationException(
$"Tool \"{response.Action}\" not found. " +
$"Available: {string.Join(", ", config.Tools.Select(t => t.Name))}");
var observation = await tool.ExecuteAsync(response.ActionInput, ct);
_steps.Add(new AgentStep(
response.Thought, response.Action, response.ActionInput,
observation, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
}
logger.LogWarning("Agent reached max steps without answer");
return new AgentResult(
"Reached maximum steps without a final answer.",
_steps.AsReadOnly(), totalTokens, sw.ElapsedMilliseconds);
}
private object BuildMessages(string query) =>
new { System = config.SystemPrompt, Query = query, History = _steps };
private Task<LlmResponse> CallLlmAsync(object messages, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(new LlmResponse(
"", "", new Dictionary<string, string>(),
true, "Mock response", 150));
}
private record LlmResponse(
string Thought, string Action,
Dictionary<string, string> ActionInput,
bool IsFinalAnswer, string FinalAnswer, int Tokens);
}ReAct Agent Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
ReAct Agent Pattern in the Real World
“Like a detective investigating a case: they form a hypothesis (Thought), gather evidence by interviewing witnesses or examining clues (Action), analyze what they found (Observation), and then refine their theory. They keep investigating until they solve the case.”