Episode 3 – Visual Guide to Core Concepts: Model, Tool, and Agent Loop
This article breaks down the three foundational elements of agent programming—Model, Tool, and the Agent Loop—showing how to obtain and configure models, define tools with TypeBox, and manage the reasoning‑tool‑feedback cycle, complete with code examples and a flow diagram.
Model: A Unified Interface for the Model Universe
First, obtain a model object in two lines of code:
const model = getModel("anthropic", "claude-sonnet-4-20250514");The getModel function has the signature:
function getModel<P extends KnownProvider>(provider: P, modelId: string): Model;It takes two parameters:
provider – a provider name such as "anthropic", "openai", "google", or "xai".
modelId – the specific model identifier, e.g. "claude-sonnet-4-20250514".
The returned Model object contains metadata (id, name, provider, api, baseUrl, contextWindow, maxTokens, reasoning, input, cost) and is used by the stream and complete functions to decide the request endpoint, protocol, and pricing.
Pi ships with a registry of over 2000 models across 20+ providers. You can list all models for a provider with getModels:
import { getModels } from "@mariozechner/pi-ai";
const anthropicModels = getModels("anthropic");
for (const m of anthropicModels) {
console.log(`${m.id} — context: ${m.contextWindow}`);
}
// claude-sonnet-4-20250514 — context: 200000
// ...Switching providers only requires changing the first argument; the underlying request URL and protocol are handled automatically by the library.
Custom Model: Using a Local Ollama Instance
You can manually construct a Model object to point to a local Ollama server:
import { stream } from "@mariozechner/pi-ai";
const ollamaModel: Model<"openai-completions"> = {
id: "llama-3.1-8b",
name: "Llama 3.1 8B (Ollama)",
api: "openai-completions",
provider: "ollama",
baseUrl: "http://localhost:11434/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000,
};
await stream(ollamaModel, context, { apiKey: "dummy" });The library abstracts away the differences between Ollama and cloud APIs; your code stays the same.
Tool: Functions as Tools
Tools define what actions an agent can perform. A tool is declared with a name, description, and a TypeBox schema for its parameters:
import { Type, Tool } from "@mariozechner/pi-ai";
const weatherTool: Tool = {
name: "get_weather",
description: "获取某个地点的当前天气",
parameters: Type.Object({
location: Type.String({ description: "城市名称或坐标" }),
units: Type.Optional(Type.Union([
Type.Literal("celsius"),
Type.Literal("fahrenheit")
])),
}),
};When a tool array is passed to complete or stream, the following flow occurs:
The library serialises the tool definition into the provider‑specific schema.
The LLM reads the user query and tool description, decides whether to call a tool.
If a tool call is chosen, the response contains a block with type: "toolCall".
After receiving a toolCall, you must execute the corresponding function yourself and feed the result back to the LLM as a toolResult message. The core loop that repeats this process is provided by pi-agent-core, not by pi-ai itself.
Tool Calls in Streaming Mode
When using stream, tool call events are emitted incrementally:
for await (const event of stream(model, contextWithTools)) {
switch (event.type) {
case "toolcall_start":
console.log(`开始调用 ${event.toolCall.name}`);
break;
case "toolcall_delta":
if (event.partial.content[0]?.arguments?.location) {
console.log(`定位到: ${event.partial.content[0].arguments.location}`);
}
break;
case "toolcall_end":
console.log(`完成: ${JSON.stringify(event.toolCall.arguments)}`);
break;
}
}Some models (e.g., OpenAI gpt-4o) emit the tool name before all arguments are generated, allowing early preparation.
Agent Loop: The Core Cycle
The Agent Loop ties model reasoning and tool execution together. The diagram below illustrates a single iteration of the loop:
Why loop? An LLM cannot perform actions directly; it only states what it wants to do. When it decides to call a tool, it outputs the tool name and arguments, but the actual HTTP request, file I/O, or database query must be performed by your code. The result is then fed back to the LLM for further reasoning, forming the cycle generate → tool call → feed result → generate.
Termination Conditions
"stop"– model stops voluntarily, yielding the final answer. "length" – token limit reached; the response may be truncated. "toolUse" – model requests a tool; the loop continues after execution. "error" – an error or user abort; requires exception handling.
In pi-agent-core, the Agent class automatically handles these stop reasons: on toolUse it runs the tool and returns the result; on stop it returns the final reply to the user.
Traditional Programming vs. Agent Programming
Traditional code follows a fixed sequence:
input → functionA → functionB → functionC → outputAgent programming is dynamic:
input → LLM decides what to do → execute tool → feed result → LLM decides next step → …Key differences:
You describe what you can do (tools) rather than hard‑coding the exact flow.
Error handling shifts from try/catch around each call to validating tool inputs/outputs and letting the LLM retry.
Observability becomes crucial; you should log each tool call, LLM response, and token usage (available via pi-ai 's Usage and event system).
Code Demo for This Episode
The accompanying files demonstrate the concepts: tool-demo.ts – defines two tools, shows how to call them, and processes the tool‑call cycle (single‑turn only).
/**
* Tool definition and tool calling demo
* Shows how to define tool parameters with TypeBox and parse tool call results.
* This file demonstrates a single tool call; the full Agent loop is in the next episode.
*/
import { getModel, complete, Type, Tool } from "@mariozechner/pi-ai";
// Tool 1: get_weather
const getWeatherTool: Tool = {
name: "get_weather",
description: "获取某个地点的当前天气",
parameters: Type.Object({
location: Type.String({ description: "城市名称或坐标" }),
units: Type.Optional(Type.Union([
Type.Literal("celsius"),
Type.Literal("fahrenheit")
])),
}),
};
// Tool 2: get_city_info
const getCityInfoTool: Tool = {
name: "get_city_info",
description: "获取一个城市的简要信息,包括人口和时区",
parameters: Type.Object({
city: Type.String({ description: "城市名称" }),
language: Type.Optional(Type.String({ description: "返回信息的语言,默认中文", default: "中文" })),
}),
};
function executeTool(name: string, args: Record<string, unknown>): string {
switch (name) {
case "get_weather": {
const location = args.location as string;
const weathers: Record<string, string> = {
"北京": "晴,22°C,空气质量良",
"东京": "多云,18°C",
"New York": "Rainy, 15°C",
};
return weathers[location] ?? `${location},晴,25°C`;
}
case "get_city_info": {
const city = args.city as string;
const infos: Record<string, string> = {
"北京": "人口 2154 万,时区 UTC+8,中国首都",
"东京": "人口 1396 万,时区 UTC+9,日本首都",
};
return infos[city] ?? `${city},人口待查`;
}
default:
return "不支持的 tool";
}
}
async function main() {
const model = getModel("deepseek", "deepseek-v4-pro");
const context = {
systemPrompt: "你是一个贴心的旅行助手。如果需要查询信息,请使用提供的工具。",
messages: [{ role: "user", content: "北京今天天气怎么样?顺便帮我查一下北京的基本信息。", timestamp: Date.now() }],
tools: [getWeatherTool, getCityInfoTool],
};
const response = await complete(model, context);
console.log("🤖 初始回复:
");
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
} else if (block.type === "toolCall") {
console.log(`
🛠️ 调用工具: ${block.name}`);
console.log(` 参数: ${JSON.stringify(block.arguments)}`);
const resultText = executeTool(block.name, block.arguments);
console.log(` ✅ 结果: ${resultText}`);
const final = await complete(model, {
systemPrompt: context.systemPrompt,
messages: [...context.messages, response, { role: "toolResult", toolCallId: block.id, content: [{ type: "text", text: resultText }] }],
tools: [getWeatherTool, getCityInfoTool],
});
console.log("
🤖 最终回答:
");
for (const fb of final.content) {
if (fb.type === "text") console.log(fb.text);
}
console.log(`
⏹️ 停止原因: ${final.stopReason}`);
console.log(`📊 Token: ${final.usage.totalTokens}`);
}
}
}
main().catch(console.error);The next episode will introduce pi-agent-core and show how its Agent class automatically manages the full loop.
All the Model and Tool knowledge presented here can be reused directly when you start working with the full agent framework.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodePath
Focused on specific functional points, dedicated to concise, high-quality content, covering Java development, Linux source code, Spring source code, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
