Niveau 0
Votre boîte à outils
EventBus
Les quatre pièces
-
Le modèle
modelLe personnage
Lit du texte, écrit du texte. Il sait beaucoup de choses grâce à son entraînement, mais rien de votre app, de votre utilisateur ni d'aujourd'hui.
-
Le system prompt
systemÉquipement
Des instructions placées en tête de chaque requête : qui il est, ses règles, son ton et les faits qu'il ne peut pas connaître seul.
-
Les messages
messages[]Sac
La conversation jusqu'ici. Votre code garde cette liste et l'envoie en entier à chaque appel.
-
Les outils
toolsCompétences
Des cartes qui décrivent des fonctions que le modèle peut demander. Il ne peut que demander : c'est votre code qui les exécute.
Le modèle ne se souvient de rien
C'est ce qui surprend le plus. Un modèle n'a aucune mémoire d'un appel à l'autre. Chaque requête repart de zéro, et il ne sait que ce qui se trouve dans cette requête : le system prompt, les messages et la liste d'outils.
Dans l'animation, la deuxième question arrive seule et l'Oracle demande « quelle ville ? », alors qu'on vient de le lui dire. Il ne « se souvient » que lorsque votre code renvoie les messages précédents. Les applications de chat ont l'air de se souvenir parce qu'elles renvoient toute la conversation à chaque fois.
Deux conséquences. L'historique est à vous : c'est à vous de le garder, de l'élaguer et de le stocker. Et chaque message que vous gardez est renvoyé à chaque appel, donc une longue conversation coûte un peu plus à chaque fois.
Du texte, ou une demande
Avec des outils dans sa liste, une réponse peut être l'une de deux choses : du texte pour l'utilisateur, ou une
demande d'appel d'un outil avec certaines entrées. Le modèle n'exécute jamais rien. Il écrit
get_forecast(city, date) et s'arrête ; l'exécuter, c'est le travail de votre code.
Regardez la date : le modèle a transformé « demain » en 2026-09-26 parce que le system prompt lui a
dit quel jour on est. Les faits qu'il ne peut pas connaître seul ont leur place là.
Le code
Avec astorlm : Un agent astorlm possède les mêmes quatre pièces. Il garde l'historique pour vous
d'un appel à run() à l'autre, et quand le modèle demande un outil, il l'exécute et lui renvoie le
résultat. Cette boucle, c'est le niveau 2.
À partir de zéro : Les quatre pièces et une seule requête, pas encore de boucle. Remplissez le
bloc LLM avec votre propre endpoint, votre modèle et votre clé.
import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'
// A tool: the card the model reads (name, description, schema) plus your code behind it.
const getForecast = tool({
name: 'get_forecast',
description: 'Daily forecast for one city: rain chance and min/max temp. date is YYYY-MM-DD.',
schema: z.object({ city: z.string(), date: z.string() }),
execute: async ({ city, date }) => forecastLine(city, date), // your code; the model never sees it
})
const agent = await createLocalAgent({
// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
provider: new OpenAIProvider({
baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
apiKey: 'YOUR_API_KEY', // local servers usually ignore it
}),
systemPrompt: 'You are Nimbus, a weather assistant. Today is 2026-09-25. Answer in one short line.',
contextFiles: [], // by default astorlm also appends AGENTS.md and CLAUDE.md from the working folder
tools: [getForecast],
})
// The agent keeps the history for you, so the second run knows about the first.
await agent.run('I’m in Buenos Aires.')
await agent.run('Will it rain tomorrow?') // asks for get_forecast(Buenos Aires, 2026-09-26), runs it, answers
console.log(agent.getMessages().length) // every message so far, resent on every call
// The four pieces, with no agent yet: one request in, one reply out. Plain fetch, no SDK.
// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = {
baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
apiKey: 'YOUR_API_KEY', // local servers usually ignore it
}
type ToolCall = { id: string; function: { name: string; arguments: string } }
type Message =
| { role: 'user'; content: string }
| { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
// 1. The system prompt: who it is, its rules, and facts it can't know on its own.
const system = 'You are Nimbus, a weather assistant. Today is 2026-09-25. Answer in one short line.'
// 2. The history. The model remembers nothing between calls: this array IS its memory.
const messages: Message[] = []
// 3. A tool, as the model sees it: a name, a description and its inputs. Never the code.
const tools = [
{
type: 'function',
function: {
name: 'get_forecast',
description: 'Daily forecast for one city: rain chance and min/max temp. date is YYYY-MM-DD.',
parameters: {
type: 'object',
properties: { city: { type: 'string' }, date: { type: 'string' } },
required: ['city', 'date'],
},
},
},
]
// 4. The model: every call sends ALL of the above, and gets back one message.
async function ask(text: string): Promise<Message> {
messages.push({ role: 'user', content: text })
const res = await fetch(`${LLM.baseURL}/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${LLM.apiKey}` },
body: JSON.stringify({ model: LLM.model, messages: [{ role: 'system', content: system }, ...messages], tools }),
})
const reply: Message = (await res.json()).choices[0].message
messages.push(reply) // keep it, or the next call won't know it happened
return reply
}
await ask('I’m in Buenos Aires.') // "Got it! How can I help?"
const reply = await ask('Will it rain tomorrow?')
// The reply is either text (reply.content) or a tool request (reply.tool_calls):
// get_forecast({ city: "Buenos Aires", date: "2026-09-26" })
// Running it and sending the result back, in a loop, is level 2.
# The four pieces, with no agent yet: one request in, one reply out. Standard library only, no SDK.
import json
import urllib.request
# Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy...
LLM = {
"base_url": "http://localhost:11434/v1", # e.g. Ollama's default address
"model": "your-model", # e.g. "llama3.1", "gpt-4o-mini"
"api_key": "YOUR_API_KEY", # local servers usually ignore it
}
# 1. The system prompt: who it is, its rules, and facts it can't know on its own.
SYSTEM = "You are Nimbus, a weather assistant. Today is 2026-09-25. Answer in one short line."
# 2. The history. The model remembers nothing between calls: this list IS its memory.
messages = []
# 3. A tool, as the model sees it: a name, a description and its inputs. Never the code.
TOOLS = [
{
"type": "function",
"function": {
"name": "get_forecast",
"description": "Daily forecast for one city: rain chance and min/max temp. date is YYYY-MM-DD.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}, "date": {"type": "string"}},
"required": ["city", "date"],
},
},
}
]
# 4. The model: every call sends ALL of the above, and gets back one message.
def ask(text):
messages.append({"role": "user", "content": text})
request = urllib.request.Request(
f"{LLM['base_url']}/chat/completions",
data=json.dumps({
"model": LLM["model"],
"messages": [{"role": "system", "content": SYSTEM}, *messages],
"tools": TOOLS,
}).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {LLM['api_key']}"},
)
with urllib.request.urlopen(request) as response:
reply = json.load(response)["choices"][0]["message"]
messages.append(reply) # keep it, or the next call won't know it happened
return reply
ask("I'm in Buenos Aires.") # "Got it! How can I help?"
reply = ask("Will it rain tomorrow?")
# The reply is either text (reply["content"]) or a tool request (reply["tool_calls"]):
# get_forecast(city="Buenos Aires", date="2026-09-26")
# Running it and sending the result back, in a loop, is level 2.
Points de vigilance
- Gardez le system prompt court et précis. Il voyage avec chaque appel. Rôle, règles, ton et les faits dont le modèle a besoin ; pas un manuel.
- Décidez ce que garde l'historique. Tout renvoyer indéfiniment devient lent et coûteux, et finit par ne plus tenir. L'élaguer et le résumer est un pattern à part entière.
- Un modèle sans outils répondra quand même. Interrogez-le sur des données en temps réel et il devinera, avec aplomb. Si la réponse dépend de quelque chose qu'il ne peut pas voir, donnez-lui un outil.