Code Modules in This Guide
Core/LLM
ClaudeService.cs ILlmService.cs LlmOptions.cs SimulatedLlmService.cs
Web
Program.cs appsettings.json appsettings.Development.json

1 What Is a Large Language Model?

A Large Language Model (LLM) is a program that was trained on a huge amount of text and learned one skill: given some text, predict what text should come next. That sounds simple, but because it was trained on essentially the whole internet plus many books, it learned grammar, facts, reasoning patterns, code, and much more.

C# analogy — think of it like this: Imagine a method string Complete(string prompt) that is so smart it can answer questions, write code, reason through problems, and follow instructions — all by returning a continuation of the text you gave it. That is all an LLM does. You send text in; you get text back.

The LLM itself has no internet access, no database, no memory between calls. It is a pure function: same input pattern → consistent style of output. Every time you call it, it starts fresh.

Key Point LLMs are stateless. Just like a static method, they know nothing about previous calls. Anything you want the model to "remember" must be included in the text you send it. (Guide 05 covers how we handle memory.)

What you can and cannot ask an LLM to do

This is the most important mental model to have before writing any AI feature. Keeping this table in mind prevents wasted effort.

Can do (out of the box)Cannot do — needs extra wiring
Answer questions about text you include in the prompt Query your database (no DB access — needs an MCP tool, Guide 03)
Reason, summarise, compare, classify, rewrite, translate Know the current date/time (no system clock — needs the datetime tool)
Write, debug, and explain C# code Remember anything from a previous API call (needs IConversationStore, Guide 05)
Follow a structured output format (XML, JSON, bullet list) Browse URLs or call REST APIs (needs a tool that does it for it)
Decide which tool to call, and with what parameters Actually execute the tool — the agent loop in your C# code does that
Answer from private documents if you inject them (RAG) Know about your internal docs or procedures without RAG (Guide 04)
The pattern in this project Every "Cannot do" item above is solved by one of the other guides. MCP tools (Guide 03) give the LLM read/write access to your data via a real MCP server subprocess. RAG (Guide 04) injects relevant private documents. IConversationStore (Guide 05) re-injects history. The LLM itself stays stateless — the surrounding C# code supplies everything it needs on every call.

2 Tokens, Not Characters

LLMs do not read characters — they read tokens. A token is the smallest unit of text the model processes. Tokens do not line up neatly with characters, words, or even syllables. The exact boundaries depend on a tokenizer — a lookup table of frequent text fragments built during training.

C# analogy — think of it like string interning or a compression dictionary: The tokenizer scans the training corpus and finds the most frequently occurring sequences of characters. Common patterns like " the", "ing", "tion", and entire common words get their own single token ID. Rare sequences are split into smaller pieces. The result is efficient: common text compresses well; rare text costs more tokens.

The rough rule of thumb

1 token ≈ 4 English characters 1 token ≈ ¾ of a word (so 100 tokens ≈ 75 words) 1 page of English text ≈ 500–750 tokens 1 token ≈ 2–3 characters in non-Latin scripts (Chinese, Arabic, etc.)

Seeing tokens in action — English words

"supply" supply 1 token — common enough to earn its own token
"NexaFlowDemo" NexaFlowDemo 3 tokens — invented name split at boundaries the tokenizer can infer
"McpServer" McpServer 2 tokens — PascalCase splits at capital letters
"StdioClientTransport" StdioClientTransport 4 tokens — long PascalCase identifier splits into fragments
Practical implication for this project Tool results are serialised to JSON before being added to the agent's history. A result like {"quantity": 987654, "price": 12.99, "sku": "NF-SKU-001"} might consume 20+ tokens despite looking short. Multiply that by 9 tool calls across a long ReAct loop and you are spending hundreds of tokens on structured data alone.

The context window

Claude Sonnet's context window is 200,000 tokens. That is the maximum payload size of one single HTTP call to the API — not a shared monthly quota. Think of it like RAM, not a data plan. Every call to ClaudeService.CompleteAsync() gets its own fresh 200k-token workspace.

200,000 tokens in real-world terms

Content typeApprox. token countFits in context?
System prompt for this project (9 tools listed)~550–800 tokensYes, easily
One tool result (JSON, ~100 chars)~40–80 tokensYes
10-iteration ReAct loop (full history)~3,000–8,000 tokensYes
One page of English text~600 tokensYes — fits ~333 pages
An average novel (80,000 words)~110,000 tokensYes — just fits

Who actually assembles "all conversation history + all tool results"?

The user types their question once. The agent code assembles the full input silently behind the scenes, completely invisible to the user. ReactAgent.BuildPrompt() concatenates: system prompt + every message in history + all previous tool results into one string, then sends it to Claude.

// What Claude receives on iteration 3 (user typed nothing after the first line)

You are a NexaFlow supply chain assistant...   ← system prompt (code adds this)
CRITICAL: Your ENTIRE response must be ONLY XML...  ← code adds this
<tools>...9 tool definitions...</tools>      ← code adds this

<user>
What is the stock level for NF-SKU-001?       ← user typed this
</user>

<assistant>
<thinking>I need to look up inventory.</thinking>
<tool_call><name>inventory_lookup</name>...</tool_call>  ← Claude replied (code stored this)
</assistant>

<tool>
<tool_result><name>inventory_lookup</name>
<output>{"sku":"NF-SKU-001","quantity":250,...}</output>  ← MCP tool ran (code stored this)
</tool_result>
</tool>

<assistant>
<final_answer>NF-SKU-001 has 250 units in stock...</final_answer>
</assistant>

3 The Anthropic API — One HTTP Call

The Anthropic API is one POST to one URL. You send a JSON body with your instructions (system) and the conversation so far (messages). You receive back a JSON body with the text Claude generated. That is all.

// What you POST to api.anthropic.com/v1/messages
{
  "model": "claude-sonnet-4-6",
  "max_tokens": 4096,
  "system": "You are a supply chain assistant...",
  "messages": [
    { "role": "user", "content": "What is the stock level for NF-SKU-001?" }
  ]
}

// What you get back
{
  "content": [
    { "type": "text", "text": "The stock level for NF-SKU-001 is 250 units." }
  ]
}

4 How ClaudeService Wraps That API

src/NexaFlowDemo.Core/LLM/ClaudeService.cs

Why wrap the API at all? You can swap the real Claude API for a fake one in tests (SimulatedLlmService) without changing any agent code. You configure the HTTP client, auth headers, and timeout in one place. Any future change (new model, retry logic) is one file change.

The interface has one method:

public interface ILlmService
{
    Task<string> CompleteAsync(string prompt, CancellationToken ct = default);
}

The SplitPrompt trick

ClaudeService splits the assembled prompt into the API's system field and the messages array using SplitPrompt(). Everything before the first <user> tag → system. Everything from <user> onward → messages array.

5 Configuration — LlmOptions

src/NexaFlowDemo.Core/LLM/LlmOptions.cs
src/NexaFlowDemo.Web/appsettings.json

The one setting you must understand: SimulationMode. When SimulationMode = true, no HTTP call is ever made to Anthropic. A fake service returns canned responses instantly. To connect to the live Claude API: set SimulationMode = false and put your sk-ant-... key in appsettings.Development.json.

6 The System Prompt — Your Instructions to Claude

The system prompt tells Claude: what role to play, what format to use, what tools are available and how to call them, and any constraints. The CRITICAL: return only XML instruction forces Claude to output only the structured format the code expects — without it, Claude wraps answers in prose and the XML parser fails.

7 Quick Summary