For C# developers with no AI background · File: src/NexaFlowDemo.Core/LLM/ClaudeService.cs
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.
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.
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) |
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.
" 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.
{"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.
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.
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>
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." }
]
}
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);
}
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.
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.
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.
system string and a list of conversation messages.ILlmService / ClaudeService wraps that call. In tests, SimulatedLlmService replaces it — no API key needed.SplitPrompt() separates the system instructions from the conversation body before sending.AddHttpClient<ILlmService, ClaudeService> — not AddSingleton — to use IHttpClientFactory correctly.