ReAct Agent for Conversational Commerce in ASP.NET Core
On this page
- What is a ReAct agent?
- Why conversational commerce needs more than RAG
- Architecture in ASP.NET Core
- Step 1: route by intent before spending tokens
- Step 2: define tools with strict schemas
- Step 3: run the loop with a hard step limit
- A real request, step by step
- Step 4: keep the model honest
- Step 5: combine the agent with an expert system
- Common mistakes to avoid
- Frequently asked questions
- What is a ReAct agent in simple terms?
- Do I need RAG if I already have tools?
- How do I stop an agent from looping forever?
- Which LLM works best for tool calling?
- Can I build this without ASP.NET Core?
- Key takeaways
A ReAct agent for conversational commerce lets a shopper say "something fresh for summer, under 800 MAD, and add the best one to my cart" and get a correct result. The agent reasons about the request, calls real store functions, reads what they return, and only then answers. This post shows how we built one in ASP.NET Core 8 for ALOud, a perfume store, and which design decisions mattered most.
What is a ReAct agent?
ReAct stands for Reasoning + Acting. Instead of answering in one shot, the model alternates between three steps:
- Thought: decide what is needed next.
- Action: call a tool, such as
search_productsoradd_to_cart. - Observation: read the tool result and decide again.
The loop ends when the model has enough facts to give a final answer. For a store this matters because prices, stock and cart contents change constantly. A plain chatbot has to guess them. A ReAct agent looks them up.
In practice we use the model's native function calling rather than parsing "Thought:" lines from text. The reasoning happens inside the model instead of being written out, but the loop is the same: call, observe, decide.
Why conversational commerce needs more than RAG
Retrieval-augmented generation (RAG) is great for knowledge questions: "What does oud smell like?" or "Which of these lasts longest?" ALOud indexes every perfume as text chunks, embeds them with Ollama, and stores the vectors in Qdrant. At question time we embed the query, retrieve the top five chunks, and let the LLM answer from that context.
RAG alone cannot do anything, though. It cannot add a bottle to the cart, and its index can be minutes or hours old. Conversational commerce needs both:
| Need | Best tool |
|---|---|
| Product knowledge, notes, comparisons | RAG over Qdrant |
| Cart changes, live totals, stock | Agent tools on live services |
| Personalised recommendations | Rule-based expert system (NRules), called as an agent tool |
Architecture in ASP.NET Core
Everything is a normal scoped service in Program.cs, so the agent reuses the same code paths as the website:
builder.Services.AddScoped<IChatOrchestratorService, ChatOrchestratorService>(); // RAG
builder.Services.AddScoped<RagToolDispatcher>(); // Tools
builder.Services.AddScoped<RagCartService>(); // Agent loop
builder.Services.AddScoped<IRagLLMClient>(sp =>
sp.GetRequiredService<LLMClientFactory>().CreateClient()); // Gemini, Groq or Grok
The LLM provider is chosen with an environment variable (LLM_PROVIDER), so you can switch between Google's Gemini, Groq (a fast inference provider for open models) and xAI's Grok without touching the agent. Cart state lives in Redis behind ICartService, the same service the website uses. A controller exposes the agent at POST /api/v1/ai/cart.
Step 1: route by intent before spending tokens
Not every message needs an agent loop. A lightweight keyword-based classifier labels each message first, and pure knowledge questions go straight to RAG:
if (intent is UserIntent.Search)
return await _ragAnswerService.AnswerAsync(userId, userMessage, new RagAnswerOptions { TopK = 5 });
Cart, recommendation and general requests enter the agent. Each intent also gets its own small tool list and short system prompt. Giving the model six cart tools instead of every tool cuts token usage and reduces wrong tool choices.
Step 2: define tools with strict schemas
A tool is a name, a description, and a JSON schema for its arguments. The schema is the contract the model must follow:
new RagToolDefinition
{
Name = "add_to_cart",
Description = "Add product to cart. Automatically checks stock.",
ParametersSchema = new
{
type = "object",
properties = new
{
productId = new { type = "string" },
quantity = new { type = "integer", minimum = 1, @default = 1 }
},
required = new[] { "productId" }
}
}
Keep descriptions short and literal. Models follow "Add product to cart" more reliably than a paragraph of prose.
Step 3: run the loop with a hard step limit
The loop sends the conversation, executes any requested tool, appends the observation, and asks again:
for (var step = 0; step < MaxToolCalls; step++)
{
var result = await _agentLlm.ExecuteAsync(new RagLLMRequest
{
SystemPrompt = systemPrompt,
UserMessage = userMessage,
Context = cartContext, // cart loaded fresh at the start of the request
ConversationHistory = history,
Tools = tools
});
if (!result.IsToolCall)
return new RagResponse { Answer = result.FinalAnswer ?? Fallback(intent) };
history.Add(AssistantCall(result.ToolCall!));
var observation = await _dispatcher.DispatchAsync(result.ToolCall!.Name, result.ToolCall.Arguments);
history.Add(ToolResult(result.ToolCall.Name, observation));
}
return new RagResponse { Answer = Fallback(intent) }; // budget exhausted
The cart is read from Redis when the request starts, and every cart tool returns the updated cart in its observation. The model never works from a stale snapshot, even after it changes the cart mid-loop.
Two details keep the loop bounded:
MaxToolCallsis 10. A confused model cannot loop forever or burn your API budget.- A deterministic fallback. If the budget runs out, the user gets a helpful message, not a stack trace.
A real request, step by step
Here is the opening example, traced through the loop:
User: Something fresh for summer, under 800 MAD, and add the best one to my cart.
Action: search_products { "query": "fresh summer fragrance", "maxPrice": 800 }
Observation: [ { "id": "p-102", "name": "Neroli Bloom", "price": 690, "inStock": true },
{ "id": "p-087", "name": "Citrus Veil", "price": 540, "inStock": true } ]
Action: add_to_cart { "productId": "p-102", "quantity": 1 }
Observation: { "ok": true, "name": "Neroli Bloom", "quantity": 1, "cartTotal": 690 }
Answer: I added Neroli Bloom (690 MAD) to your cart. It has bright neroli and
citrus notes that stay light in hot weather. Your cart total is 690 MAD.
Two tool calls, two observations, one grounded answer. Nothing in the reply was guessed.
Step 4: keep the model honest
Hallucinated products are the biggest risk in AI commerce. We use three guardrails:
- Ground every answer. The prompt says: use only the provided context, never invent products, prices or availability, and say "I don't know" when information is missing.
- Validate in the dispatcher. The model proposes
add_to_cart(productId), but the dispatcher loads the perfume from the database. If the ID does not exist, the tool returns{ Ok = false, Error = "Perfume not found" }and the model recovers. - Return structured observations. Tools return small JSON objects (name, quantity, total), not free text, so the next reasoning step has clean facts.
Provider errors get the same treatment: a rate limit from the LLM becomes a clear 429 response instead of a generic failure.
Step 5: combine the agent with an expert system
Recommendations should not depend on a language model's mood. ALOud uses NRules to score perfumes from a profile: climate, occasion, skin type, persona and sensitivities. Rules like "hot climate prefers fresh, light accords" are explicit, testable and explainable. The agent calls this engine as a tool and then explains the result in natural language.
The model handles conversation, and the rules handle judgement.
Common mistakes to avoid
- Letting the model own state. Cart state lives in Redis, behind
ICartService. The model only requests changes. - Sending every tool every time. Fewer tools means better accuracy and lower cost.
- Skipping identity. Our current prototype still uses a placeholder user ID in the controller. Wire the authenticated user in before launch, or carts will not persist per customer.
- Matching intent with keywords only. Our keyword classifier is cheap and fine to start, but a small classifier or an LLM router handles phrasing you did not predict.
- No tests. Test the dispatcher and the validation logic with a scripted fake LLM. You do not need a live model to prove the loop stops, recovers and falls back.
Frequently asked questions
What is a ReAct agent in simple terms?
A ReAct agent is an LLM that alternates between thinking, calling a tool, and reading the result until it can answer. It lets the model use live data and take actions instead of guessing from memory.
Do I need RAG if I already have tools?
Usually yes. Tools are best for live data and actions such as cart and stock. RAG is best for large amounts of descriptive text such as product notes and guides. Use each where it is strongest.
How do I stop an agent from looping forever?
Set a hard maximum number of steps, return a deterministic fallback when it is reached, and log every step. We cap ours at 10 tool calls per request.
Which LLM works best for tool calling?
Any model with reliable function calling works. Smaller models tend to invent tools, so give them a short, explicit tool list and test them. Keeping the provider behind an interface lets you swap models as better ones appear.
Can I build this without ASP.NET Core?
Yes, the pattern is framework-independent. ASP.NET Core is a good fit because dependency injection, background services and typed HTTP clients make the pieces easy to compose and test.
Key takeaways
- A ReAct agent for conversational commerce combines reasoning, tools and live data in a bounded loop.
- Use RAG for knowledge and tools for actions, and route between them by intent.
- Validate every tool call server-side. The model proposes, your code decides.
- Add step limits, fallbacks and tests before you add features.
Want to dig into the code? It is on GitHub. Try asking the assistant for "a fresh summer fragrance under 800 MAD" and watch it search, compare and fill your cart.