Agentic AI · L-ReAct
How to sample tokens (softmax, temperature, top-$k$/$p$), the ReAct loop that turns a chat model into an agent, message types, and the system prompt / chat template.
After this week you can:
UserMessage / AssistantMessage / ToolResultMessage.Recall. A language model outputs a distribution over the vocabulary $P(W_t\mid W_{1:t-1})$ (→ L-Intro). The lecture starts from that distribution — logits, softmax, sampling — and then places the model in a loop with tools.
ReAct is a loop in which a language model alternates reasoning steps with tool calls. Part I states how one token is selected from the model's output distribution, Part II how reasoning and acting are combined in one loop, Part III how a tool is described, requested and executed, and Part IV what the loop stores and sends to the model with every call:
A language model does not output the next token; it outputs a probability distribution over the whole token vocabulary. A sampling rule selects the next token from that distribution, and the parameters of the rule determine how deterministic the generated text is.
The softmax function converts the scores that the network computes for the tokens of the vocabulary into a probability distribution; the next token is sampled from that distribution. The network does not emit a token directly. One generation step:
$$ \underbrace{W_{1:t-1}}_{\text{previous tokens}}\;\xrightarrow{\text{Transformer}}\; \underbrace{\mathbf z\in\mathbb R^{|V|}}_{\text{logits: one score per token}}\; \xrightarrow{\text{softmax}}\; \underbrace{P(W_t\mid W_{1:t-1})}_{\text{distribution over }V}\; \xrightarrow{\text{sample}}\; w_t $$
Properties. Every entry of the softmax is positive and the entries sum to $1$, so the result is a valid probability distribution. The proof is an exercise.
The softmax yields a distribution $P(\cdot \mid W_{1:t-1})$ over the vocabulary $V$. Two rules select a token from it (Jurafsky & Martin, 2026, §7.6):
In code the categorical draw is one line:
w_t = np.random.choice(len(p), p=p) # draw a token, weighted by the probabilities p
Generation is autoregressive, one factor of the chain rule per step: $w_t$ is appended,
$w_{1:t}$ is given to the model again, the model returns the next distribution $P$ (p in the
code), and the draw is repeated until an end-of-sequence token is drawn.
A sampling rule — temperature scaling, top-$k$ or top-$p$ (Temperature Scaling, Truncated Sampling) — changes $P$ before the draw and thereby sets how deterministic or how diverse the generated text is.
Temperature scaling divides the logits by a parameter $T > 0$ before the softmax; $T$ sets how peaked the resulting distribution is, and thereby how deterministic the sampled text is:
$$ P_T(W_t=i \mid W_{1:t-1} )=\frac{e^{z_i/T}}{\sum_j e^{z_j/T}}, \qquad T>0 $$
temperature=0.0 therefore omits the softmax and the draw and returns $\arg\max_i z_i$ directly.Consequence for an agent.
The choice among several sampled candidates — beam search or sampling, best-of-$N$, self-consistency — is a search problem over the candidates.
Truncated sampling sets the probability of the least likely tokens to zero, renormalises the remaining probabilities and samples from them; it excludes the draw of a very unlikely token from the long tail of the distribution. Top-$k$ and top-$p$ sampling differ in which tokens they keep:
Example — probabilities $(0.4,\,0.3,\,0.25,\,0.03,\,0.02)$:
The rules are often combined, for example top-$p$ with temperature: temperature scaling is applied to the distribution before the truncation. Greedy search is the special case top-$k$ with $k{=}1$, and equally the limit $T\to 0$.
Constrained decoding restricts every sampling step to the tokens that keep the output valid under a formal grammar — a JSON schema, a regular expression or a context-free grammar — so that the finished output is guaranteed to parse.
During generation a parser tracks the state $s_t$ of the grammar. At each step $t$ the state defines the set of valid tokens $A(s_t) \subseteq V$. The probabilities of the valid tokens are renormalised and every other token receives probability zero. With the indicator $\mathbb{1}[\cdot]$, which is $1$ if its condition holds and $0$ otherwise, the constrained distribution $P'$ is:
$$ P'(W_t{=}i \mid W_{1:t-1}) \;=\; \frac{\mathbb{1}[\,i \in A(s_t)\,]\; P(W_t{=}i \mid W_{1:t-1})}{\sum_{j\in A(s_t)} P(W_t{=}j \mid W_{1:t-1})} $$
A prompt changes probabilities; a mask removes tokens. An instruction such as "Answer only in JSON" raises the probability of well-formed output, and every malformed continuation keeps a probability above zero. Constrained decoding assigns a malformed continuation the probability zero, so that it cannot be sampled.
Use in tool calling. A tool call is a structured request by which the model asks the runtime to execute a function (Function Calling: Defining & Advertising Tools). Where the call is generated under constrained decoding, its arguments conform to the JSON schema of the tool.
The sampling parameters are chosen for each task by the number of acceptable outputs: a task with one valid output is generated at $T=0$, a task whose goal is variety at $T\approx 1$.
| Use case | temperature |
top_p |
Reason |
|---|---|---|---|
| Tool calls, structured / JSON output, extraction, tests, linting | 0.0 (greedy) | — | one valid output, which must be repeatable |
| Factual Q&A, summarization, precise code edits | ~0.2–0.3 | ~0.9 | few acceptable outputs; small variation is tolerated |
| General writing, drafting, explanation | ~0.7 | ~0.9–0.95 | many acceptable outputs; variation of the wording is wanted |
| Brainstorming, creative, Tree-of-Thoughts candidates (→ L-ToT) | ~1.0+ | ~0.95 | diversity is the goal |
temperature=0.0, and a fixed
seed if the API offers one, so that runs are comparable.top_p normally stays at 0.9–0.95 and only temperature is
adjusted; when both are raised together the output is hard to predict.Design rule. The sampling parameters are one of two controls. The other is the prompt: clarity, examples, a role, structure (Anthropic, 2026), which usually has the stronger effect. The prompt is improved first, the temperature adjusted second.
Speculative decoding accelerates generation without changing the distribution that is sampled from: a small model drafts several tokens, and the large model verifies them in one pass. The sampling rules — temperature, top-$k$, top-$p$ — change the distribution; speculative decoding changes only the speed.
A modified rejection-sampling rule guarantees that the emitted tokens have exactly the distribution of the large model (Leviathan et al., 2023; Chen et al., 2023). The rule, its acceptance rate and the number of drafted tokens are derived in → L-Att.
Evidence. Leviathan et al. (2023) generate with T5-XXL as the large model and T5-small as the draft model, on English–German translation and on summarisation, and measure the wall-clock time of generation against T5-XXL alone: it is 2.3 to 3.4 times shorter. Chen et al. (2023) measure 2 to 2.5 times on Chinchilla, a model of 70 billion parameters. In both the output distribution is unchanged.
| Technique | Changes the output distribution? | Effect |
|---|---|---|
| temperature / top-$k$ / top-$p$ | yes — it is their purpose | determinism ↔ diversity |
| quantization (the weights are stored with fewer bits) | yes — the weights are approximated | the model needs less memory |
| constrained decoding | yes — invalid tokens receive probability 0 | a guaranteed format |
| speculative decoding | no — provably the same distribution | less wall-clock time, nothing else |
import numpy as np
def softmax(z, T=1.0):
z = np.asarray(z, float) / T
z = z - z.max() # numerical stability
e = np.exp(z)
return e / e.sum()
logits = np.array([2.0, 1.0, 0.2, -0.5])
for T in (0.5, 1.0, 2.0):
print(f"T={T}: {np.round(softmax(logits, T), 3)}")
def top_p(p, thresh=0.9):
sorted_indices = np.argsort(p)[::-1]
sorted_probs = p[sorted_indices]
cum_probs = np.cumsum(sorted_probs)
# Shift cumulative sum to include the first token that crosses the threshold
cutoff_mask = cum_probs - sorted_probs < thresh
keep_indices = sorted_indices[cutoff_mask]
masked_p = np.zeros_like(p)
masked_p[keep_indices] = p[keep_indices]
return masked_p / masked_p.sum()
p = softmax(logits, 1.0)
print("full :", np.round(p, 3))
print("top-p:", np.round(top_p(p, 0.9), 3))
Chain-of-thought makes a model write intermediate reasoning steps before its answer; ReAct interleaves such reasoning steps with tool calls, so that the observation returned by each call enters the context before the next step is generated.
Chain-of-thought (CoT) prompting makes the model write intermediate reasoning steps before its final answer (Wei et al., 2022). A few-shot exemplar (see e.g. Appendix G of Wei et al., 2022) is a solved example placed in the prompt. In CoT prompting each exemplar contains a worked chain of reasoning, so it has the form $\langle$input, chain of thought, output$\rangle$ instead of $\langle$input, output$\rangle$, and the model continues in the same form. The answer is then no longer produced in one generation step; it follows a sequence of written intermediate steps.
Chain-of-thought increases the amount of computation that a Transformer applies to a problem before it commits to an answer.
A chain of thought is text that the model generates; it need not state what determined the answer. Jacovi & Goldberg (2020, §2) distinguish two properties of an explanation that accompanies the output of a model:
| Property | Definition | What it relates | Assessed by |
|---|---|---|---|
| plausible | "how convincing the interpretation is to humans" | the explanation and its reader | a reader, from the text |
| faithful | "how accurately it reflects the true reasoning process of the model" | the explanation and the computation that produced the output | an experiment on the model |
The two properties are independent: an explanation can convince its reader and misstate the cause of the answer. A reader cannot assess faithfulness from the text, since the text does not contain the computation.
Evidence. Turpin et al. (2023) insert a bias into the prompt of multiple-choice questions from 13 tasks of the benchmark BIG-Bench Hard: the options of the few-shot examples are reordered so that the correct answer is always "(A)", or the user adds "I think the answer is (B)" with a random label. With the bias, the accuracy of the answers falls by up to 36 % (GPT-3.5, zero-shot CoT, suggested answer), and 1 of 426 reviewed explanations of a biased answer mentions the bias.
What the metric measures: This statistic tracks how often model explanations explicitly acknowledge the prompt bias. It does not prove whether the chain of thought actually drove the final answer: when the chain is deleted, the prediction changes in 20–72 % of the cases (Zaman & Srivastava, 2026). Consequently, the authors argue that omitting the bias makes a reasoning chain incomplete rather than entirely false.
Design rule — debug with the chain, conclude from the artifact. The chain is read to find the cause of a failure. A conclusion about what a run did is drawn from an artifact: the tool result, the test, the recomputation (→ L-Eval).
An open-loop controller executes a fixed plan and does not measure its result; a closed-loop controller measures each result and uses it to choose the next step. The two terms come from control theory.
CoT improves the reasoning, but the model writes the entire chain before any step is executed: CoT is open-loop.
Classic CoT is open-loop: the model reasons, but it cannot act on its environment, obtain new data, or detect that step 3 depends on a false premise from step 2. Every fact that it uses comes from its parametric memory (the training data), so an early error is carried to the end undetected.
An action changes the requirement. When a step has an effect — a query, a file write, an e-mail — its outcome is information that the model did not have, and the next step should depend on it. A loop that returns the outcome to the model is closed; this is the difference between CoT and ReAct.
ReAct (Reasoning + Acting; Yao et al., 2023) extends chain-of-thought to a closed loop: the model interleaves thoughts with actions, and the observation that each action returns enters the context before the next thought. A thought $t$ is a reasoning step as in CoT, an action $a$ is the execution of a tool, and an observation $o$ is the output of that tool:
$$\dots \longrightarrow\; t_k \;\longrightarrow\; a_k \;\longrightarrow\; o_k \;\longrightarrow\; t_{k+1} \; \longrightarrow\; \dots$$
| Paradigm | Reasons (CoT) | Acts (Tools) | Grounds on Feedback | Control Type |
|---|---|---|---|---|
| Chain-of-Thought (CoT) | ✅ | ❌ | ❌ | Open-loop (Static) |
| Act (the paper's own baseline) | ❌ | ✅ | ✅ | Closed-loop (Dynamic) |
| ReAct | ✅ | ✅ | ✅ | Closed-loop (Dynamic) |
Grounding. In ReAct a thought is generated after the observations of all earlier actions have entered the context. The thought can therefore use a returned fact instead of a recalled one, and it can respond to a tool call that failed.
ReAct is not a third mechanism: it runs the reasoning loop and the acting loop on the same language model (LM). The diagram is redrawn after Yao et al. (2023):
A reasoning trace is an arrow that leaves the LM and returns to it without passing through the
environment (Env), so it produces no observation.
The Core Definition of ReAct states this formally.
ReAct is defined by its action space: besides an action on the environment, the agent may emit a thought — an action in the space of language, which produces no observation (Yao et al., 2023, §2).
An agent at step $k$ receives an observation $o_k \in \mathcal{O}$ and takes an action $a_k \in \mathcal{A}$. The action is drawn from a policy $\pi(a_k \mid c_k)$, the distribution over actions given the context $c_k$, the input that the model receives at step $k$. The context is the whole history, preceded by a fixed prefix $c_0$: $$c_k = (c_0, o_1, a_1, \dots, o_{k-1}, a_{k-1}, o_k).$$
The prefix $c_0$ contains the system prompt and the descriptions of the available tools; it is the same at every step (System Prompt, Chat Template & Statelessness). The task is the first observation $o_1$. Yao et al. (2023, §2) write the context without $c_0$; the prefix is written here because it occupies part of the context that the model receives at every step.
ReAct augments the action space by free-form language:
$$\hat{\mathcal{A}} = \mathcal{A} \cup \mathcal{L}$$
An element $\hat a_k \in \hat{\mathcal{A}}$ is either an environment action ($a_k \in \mathcal{A}$) or a thought ($t_k \in \mathcal{L}$). Yao et al. (2023) use the symbol $\hat a_k$ for the thought alone; here it denotes any element of the augmented space.
A thought ($t_k \in \mathcal{L}$) is an action that does not act on the environment and returns no observation. Its only effect is to append text to the context. Where thoughts and actions alternate, step $k$ consists of one thought, one action and the observation that the action returns. The thought is drawn from $\pi(t_k \mid c_k)$, the action from $\pi(a_k \mid c_k, t_k)$, and the context of the next step is
$$c_{k+1} = (c_k, t_k, a_k, o_{k+1}).$$
The complete context at step $k$ is
$$c_k = (c_0,\; o_1, t_1, a_1,\; \dots,\; o_{k-1}, t_{k-1}, a_{k-1},\; o_k).$$
Thinking is therefore not a phase before acting. It is an action whose only effect is a write to the context. The message list, statelessness and the context budget of Part IV — Messages and the Context follow from this property.
Notation of the source. Yao et al. (2023), §2 write the update for a single thought as $c_{t+1} = (c_t, \hat a_t)$. Their symbol $\hat a_t \in \mathcal{L}$ denotes the thought alone, which is $t_k$ here, and not an arbitrary element of $\hat{\mathcal{A}}$; their index $t$ is the step, which the thought advances. Yao et al. do not write the complete context. Here the index $k$ counts observations, so that a thought does not advance it. A step without a thought contributes only $(a_k, o_{k+1})$.
A thought consumes context and execution time, so the frequency of thoughts is a design decision (Yao et al., 2023):
A current model produces the ReAct turn format without examples in its prompt, because the format was trained into its weights; the loop that executes the turns remains program code outside the model.
In-context learning (Yao et al., 2023) shows the format by few-shot examples in the prompt. Every request sends the examples again as tokens, the context window limits their number, and the answer depends on their choice, order and format (Dong et al., 2024).
Trajectory tuning is fine-tuning on complete agent runs (Song et al., 2024). One training example is a trajectory: the instruction, followed by the turns of thought, action and observation. Zeng et al. (2024) collected 1,866 trajectories over six tasks, Song et al. (2024) 51,287 over sixteen; Yang et al. (2024) generate them without an annotator, with a second prompt that writes the thought for a sampled action.
Evidence.
What post-training does not provide. Toolformer (Schick et al., 2023) learns when to call an API and with which arguments, and produces no sequence of calls: "there are no examples of chained tool use in the finetuning dataset." Trajectory tuning likewise trains the format of one turn. Executing the tools, counting the steps, stopping and handling errors remain control flow outside the model.
Consequence for the harness. Post-training removes the ReAct examples from the prompt and changes which errors occur; the architecture of the loop is unchanged.
Prompt-ToolLoop is the agent loop that the runtime executes; ReAct is the form of the turns that the model generates inside this loop. The name of the procedure states its two steps: the model call on the prompt (line 3) and the execution of the requested tools (lines 6–8). The procedure requires no thought. If every response contains a thought before its tool calls, the run is a ReAct run; without thoughts the same procedure executes the Act baseline of Yao et al. (2023).
$$ \begin{array}{l} \textbf{Prompt-ToolLoop}(\textit{user\_input}) \\ \hline \textbf{Input: } \textit{user\_input} \text{; the tool set; a step cap } \text{MAX\_STEPS} \text{;} \\ \phantom{\textbf{Input: }} \text{the } \textit{context} \text{ of the session, initially the prefix } c_0 = (\text{system prompt}, \text{tool descriptions}) \\ \textbf{Output: } \text{the assistant's final text} \\ \hline 1: \quad \textit{context}.\text{append}(\textit{user\_input}) \\ 2: \quad \textbf{for } \textit{step} = 1 \dots \text{MAX\_STEPS} \textbf{ do} \\ 3: \quad \qquad \textit{response} \gets \text{LLM}(\textit{context}) \\ 4: \quad \qquad \textit{context}.\text{append}(\textit{response}) \\ 5: \quad \qquad \textbf{if } \textit{response}.\text{tool\_calls} = \emptyset \textbf{ then return } \textit{response}.\text{text} \qquad \text{// a text-only turn IS the answer} \\ 6: \quad \qquad \textbf{for each } \textit{call} \in \textit{response}.\text{tool\_calls} \textbf{ do} \\ 7: \quad \qquad\quad \textbf{try } \textit{result} \gets \text{execute}(\textit{call}) \; \textbf{catch } e: \textit{result} \gets \text{"ERROR: "} + \text{message}(e) \\ 8: \quad \qquad\quad \textit{context}.\text{append}(\text{tool\_result}(\textit{call}.\text{id}, \textit{result})) \qquad \text{// the result as a message that carries the id of its call} \\ 9: \quad \textbf{return } \text{"stopped: step budget exhausted"} \\ \hline \end{array} $$
Prompt-ToolLoop terminates when the response of the model contains no tool call: the runtime
treats a text-only response as the final answer. The end of the run is thus inferred from the
form of the response. Yao et al. (2023) use an explicit action instead, finish[answer], which
ends the run.
A model cannot execute an action: it requests a tool by emitting a structured call that matches an advertised schema, and the runtime validates the call, executes it and returns the result as an observation.
Function calling is the mechanism by which a model requests a tool: the runtime advertises a schema for every tool — name, description, arguments — and the model emits a structured call that matches one of the schemas.
The model has no built-in knowledge of the available tools. The same base model serves many agents, each with a different tool set, so every model call has to state which tools exist for it.
The schemas are passed in the $\textit{tools}$ argument of the model call; the arguments of a tool are described in JSON Schema:
{ "type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"] } } }
The API fixes the form of a schema; the developer writes its content.
parameters follow the
JSON Schema standard.{id, name, arguments}
(arguments is a JSON string).tool_call_id. The id pairs each result with its request, which is necessary
when one turn contains several calls.The API is provided by an inference engine, the program that loads the weights of a model and
computes its output tokens. Ollama, llama.cpp and vLLM are inference engines for open-weight
models; vLLM describes itself as "a high-throughput and memory-efficient inference and serving
engine for LLMs" (vLLM Project, 2026a). Behind a hosted API the provider operates the
inference engine.
The inference engine is not part of the runtime. The runtime sends a request to the API and receives a response. The agent loop, the execution of the tools and the validation of the calls remain in the runtime. An inference engine is also called inference server or model runtime; in this course the word runtime always denotes the runtime of the agent.
A tool call is a string that the model sampled, so the runtime applies three checks to it before any tool code is executed:
jsonschema or with Pydantic, a Python library for data validation.Design rule. The three checks stay active when the provider applies constrained decoding or structured outputs (Constrained Decoding): the runtime does not depend on the provider to exclude an ill-formed call.
A validation failure is returned to the model as a tool result; it is not raised to the user as an exception. The runtime catches the error and appends the detailed validation message to the context as the tool result. The model observes its mistake and can repair the call in the next turn.
Validation is not policy. A call that passes validation is well-formed; whether it is permitted — access to a path, an allowed domain, a transaction limit — is a policy question, which is decided separately at the security boundary.
Native tool calling is an API-level mechanism: a model fine-tuned for tool use accepts the
schemas in a $\textit{tools}$ parameter and returns a structured response object (e.g.
$\textit{response}.\text{tool\_calls}$) instead of raw text. A provider may apply
constrained decoding to the call, so that arguments which violate the schema cannot be sampled
(OpenAI's strict mode, Ollama's format). Where it does not, the schema only raises the
probability of valid arguments, and a malformed call remains possible.
A tool is either specific — one predefined capability — or general: a single tool whose argument is code.
get_weather(city), send_email(to, body). Each has one
capability, so its calls are easy to validate and to restrict.run_python(code) whose argument is code. It is more expressive: one action can
contain a loop, compose several steps and use variables. It executes arbitrary code, so it
requires sandboxing (→ L-Gateway) and is harder to restrict.Both kinds use the same JSON tool call; for the code tool the arguments field contains code.
The ReAct loop is the same for both.
The Model Context Protocol (MCP) is an open protocol by which a server offers tools, data (resources) and prompt templates, and by which the runtime of an agent discovers and calls them (Anthropic, 2024).
Without such a protocol every tool is written into the runtime of one agent. MCP separates the tool provider from the agent: a tool that an MCP server offers once can be used by every MCP-capable client (the agent runtime), and no adapter code is written for the individual tool.
MCP is a matter of the runtime; the model has no knowledge of it. The model receives a list of tools and emits an ordinary tool call. The runtime performs discovery, transport (JSON-RPC, a protocol for remote procedure calls encoded in JSON), routing and security.
The Agent–Computer Interface (ACI) is the interface that a tool presents to a model: the name of the tool, its description, the names and types of its arguments, and the form of the result that is returned (Yang, Jimenez et al., 2024). It corresponds to the user interface that a program presents to a person.
These are the fields that the developer writes (Function Calling: Defining & Advertising Tools). The JSON envelope of the call is fixed by the API; the content of the fields is the design problem of the ACI.
A user interface is judged by whether a person can operate it without a manual; an ACI is judged by whether a model can operate it without guessing.
A practitioner's report. Schluntz & Zhang (2024), engineers at Anthropic, report that they invested as much effort in the ACI as in a user interface: for their agent on SWE-bench, a benchmark of GitHub issues to be resolved in real repositories, the tools required more optimisation effort than the prompt. The same article gives design rules for the ACI.
The model is stateless: the runtime stores the conversation as a list of role-tagged messages and sends the complete list with every model call, and the context window bounds the length of that list.
The conversation is stored as a list of role-tagged messages. The roles system, user,
assistant and tool are the strings that are transmitted to the API. In code each role has a
type of its own, because each carries different fields.
The history ($\textit{history}$) is the list of messages that the loop stores between calls. It is a part of the context: the context of a call contains in addition the tool schemas, the retrieved documents and the long-term memory, which the runtime adds at that call (Actual Context Contents). $\textit{history}$ is a list of these types:
| Role in the API | Type in code | Holds |
|---|---|---|
user |
UserMessage |
the user's text |
assistant |
AssistantMessage |
text and/or tool_calls (id, name, arguments) |
tool |
ToolResultMessage |
tool_call_id (links to the call) + the tool's output |
Pairing. An AssistantMessage may carry several tool_calls, and each call requires its own
ToolResultMessage with the matching tool_call_id.
A user asks "What's the weather in Berlin?"; a get_weather tool is available.
history: [ system, User("What's the weather in Berlin?") ]
turn 1 LLM(history) → Assistant(tool_calls=[get_weather(city="Berlin")]) # no text yet
runtime runs get_weather("Berlin") → "12°C, rainy"
history += Assistant(tool_call) , ToolResult("12°C, rainy")
turn 2 LLM(history) → Assistant("It's 12°C and rainy in Berlin.") # no tool call → STOP
As typed messages (Message Types), $\textit{history}$ now contains:
UserMessage("What's the weather in Berlin?")AssistantMessage(tool_calls=[{name:"get_weather", arguments:{city:"Berlin"}}])ToolResultMessage(tool_call_id=…, content="12°C, rainy")AssistantMessage("It's 12°C and rainy in Berlin.") ← returnedThe run has two turns and one tool call, and each turn sent the whole history.
tool_call_id¶Example of the pairing. A turn that requests get_weather("Berlin") and
get_weather("Paris") receives two ToolResultMessage objects; the tool_call_id of each states
which of the two requests it answers.
# The SAME round-trip as the trace above, in real code (one loop iteration).
import json
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
MODEL = "qwen2.5:7b"
def get_weather(city): # the actual tool (runtime side)
return f"12°C, rainy in {city}"
tools = [{"type": "function", "function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]}}}]
msgs = [{"role": "user", "content": "What's the weather in Berlin?"}]
# turn 1: model requests a tool
r = client.chat.completions.create(model=MODEL, messages=msgs, tools=tools)
msg = r.choices[0].message
if not msg.tool_calls:
# a small model sometimes answers in text instead of calling: there is nothing to feed back,
# so the second turn must not happen at all
print("Model responded with text instead of calling a tool:", msg.content)
else:
call = msg.tool_calls[0]
args = json.loads(call.function.arguments)
result = get_weather(**args) # the runtime executes it
# feed the result back; turn 2: model gives the final answer
msgs.append(msg) # assistant tool-call msg
msgs.append({"role": "tool", "tool_call_id": call.id, "content": result})
final = client.chat.completions.create(model=MODEL, messages=msgs, tools=tools)
print(final.choices[0].message.content)
A model call is stateless: the model keeps nothing between two calls, so the runtime sends the entire conversation with every call.
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
...<|im_end|>
<|im_start|>assistant
The format shown is ChatML (<|im_start|> … <|im_end|>). OpenAI introduced it; the Qwen
family, to which the models of the lab belong, uses it. Other model families (Llama, Mistral)
use different delimiters.
Tool use adds a tool role for results, and the tool request of the model is part of the
assistant turn. The tokens that mark both are model-specific.
The context of a model call contains more than the conversation: on every call the runtime adds the tool schemas, retrieved documents and long-term memory to the history. The loop maintains $\textit{history}$: the system prompt, all earlier turns, and the tool calls and tool results of the current turn.
context sent to the LLM =
history # system prompt + all turns + this turn's tool calls/results
+ tool schemas # the advertised tools
+ retrieved documents # RAG results for this query
+ long-term memory # relevant remembered facts
In the notation of The Core Definition of ReAct, the prefix $c_0$ is the system prompt, the first entry of $\textit{history}$, together with the tool schemas; the observations, thoughts and actions are the remaining entries of $\textit{history}$.
Context engineering is the decision what is assembled into the context of each call and, under a finite budget, what is retrieved and what is compacted.
$\textit{history}$ is constructed by the runtime; it is not a transcript. The runtime may
remove, merge or summarise turns — a redundant reply, a tool output that has been used —
provided that a tool_call and its tool_result stay together. The messages that are sent can
therefore differ from the conversation that took place.
The context window is a fixed number of tokens that all parts of the context share: more tool schemas or retrieved documents leave less room for $\textit{history}$, and the reverse.
Because the model is stateless, the whole context is assembled and sent again with every call. Each token therefore adds latency and cost at every turn, not once.
Design problem. What is included, retrieved or compacted so that the context stays inside the budget is a central problem of agent design. It is treated with embeddings (→ L-Embed), retrieval (→ L-RAG) and compaction (→ L-Memory).
Procedural instructions follow the same rule. An agent need not hold every instruction in the
system prompt. It can store them as skills: directories of which only a one-line
description is in the context until a task matches it; then the full instructions are loaded.
Tool schemas, retrieval and skills apply one principle: index first, load on demand.
get_weather:
a while loop, a growing message list and a json.loads for the arguments of each call.lecture-ReAct-exercises.ipynb) — pen and paper:lecture-ReAct-lab.ipynb) — tutorials: complete code in plain Python, without a
framework, which is read, executed and explained:run_agent() at the core of an agent
runtime, and the message history with its pairing invariant.tool_calls; the
runtime executes and feeds results back. MCP standardises exposing tools.UserMessage / AssistantMessage / ToolResultMessage in a persistent,
growing history.Peer-reviewed papers
Preprints & other non-peer-reviewed papers
Textbooks & monographs
Blogs, documentation & other web sources
llama-server implements, and the generic format for every other template. https://github.com/ggml-org/llama.cpp/blob/master/docs/function-calling.md<tools> tags in the system message, the call between <tool_call> tags. https://ollama.com/library/qwen2.5:7b/blobs/eb4402837c78[AVAILABLE_TOOLS], [TOOL_CALLS], [TOOL_RESULTS]. https://ollama.com/library/mistral/blobs/1ff5b64b61b9tool_choice="auto" the call is generated without a grammar unless a tool is marked strict. https://docs.vllm.ai/en/latest/features/tool_calling.htmlfinish[answer] as the only exit, think[…] as a no-op action, and stop sequences as turn boundaries.