Creating Your Own LLM Agent

LLMs
Agents
Demystifying agents so you can create your own from scratch. It’s simpler than you think.
Author

Salman Naqvi

Published

Monday, 24 November 2025

In this notebook, I’ll be demonstrating how simple LLM “agents” are, that they’re just LLMs in a while loop calling functions, and how to implement a tool-loop yourself using the Lisette and toolslm libraries.

This post is based on the contents of Lesson 2 of the SolveIt course led by Jeremy Howard who created the first modern LLM in 2016, and Johnothan Whitaker. SolveIt is also a brilliant problem-solving framework and interactive platform, that puts you in control–not the LLM.

In lesson 2, Jeremy unmasks the deceptively complicated mask that LLM agents wear iteratively through SolveIt, revealing that agents are just LLMs in a while-loop.

What are Agents?

“An AI agent is just an LLM in a while loop.”

That’s it. The LLM requests to run a function (or in other words, the tool), you run it, give back the result, and repeat until the LLM no longer requests. No fancy frameworks needed.

The Core Loop

At its core, it’s simple 4 step loop:

  1. Send message + available tools to the LLM
  2. LLM responds with either the answer OR tool request
  3. If tool is requested: execute the relevant function and add the result to the chat history
  4. Go back to step 1.

flowchart TD
    Start([Start]) --> Call[Call LLM with message + tools]
    Call --> Check{Tool requested?}
    Check -->|Yes| Execute[Execute tool function]
    Execute --> Add[Add result to history]
    Add --> Call
    Check -->|No| Done([Done])

Walkthrough

Step 1: Send Message and Available Tools to LLM

To do this, we first need to setup the tools by:

  • Tossing all the tools into a list
  • Defining the metadata about those tools
  • Making the tools available in a “namespace” through which they can directly be called.

Let’s first define two tools.

def multer(a:float,b:float)->float:
    'Take the product of two numbers'
    return a*b
multer(2,3)
6
def pow(a:float,b:float)->float:
    'Return a to the power of b'
    return a**b
pow(2,3)
8

And then add it to the list of available tools.

tools = [multer,pow]; tools
[<function __main__.multer(a: float, b: float) -> float>,
 <function __main__.pow(a: float, b: float) -> float>]

We’ll now create the metadata about this tool.

from lisette import *

Lisette is a wonderful library that abstracts away a lot of the boilerplate when working the LLMs in Python. The library is a wrapper around LiteLLM.

Later in the notebook, I’ll demonstrate how to use Lisette’s Chat class directly to make tool calls with an LLM. For now, I’m using some of the library’s other functions.

schemas = [lite_mk_func(t) for t in tools]; schemas
[{'type': 'function',
  'function': {'name': 'multer',
   'description': 'Take the product of two numbers\n\nReturns:\n- type: number',
   'parameters': {'type': 'object',
    'properties': {'a': {'type': 'number', 'description': ''},
     'b': {'type': 'number', 'description': ''}},
    'required': ['a', 'b']}}},
 {'type': 'function',
  'function': {'name': 'pow',
   'description': 'Return a to the power of b\n\nReturns:\n- type: number',
   'parameters': {'type': 'object',
    'properties': {'a': {'type': 'number', 'description': ''},
     'b': {'type': 'number', 'description': ''}},
    'required': ['a', 'b']}}}]
??lite_mk_func
def lite_mk_func(f):
    if isinstance(f, dict): return f
    return {'type':'function', 'function':get_schema(f, pname='parameters')}

File: /usr/local/lib/python3.12/site-packages/lisette/core.py

I’ll now define the namespace.

from toolslm.funccall import mk_ns

toolslm is a helper library used by Lisette.

ns = mk_ns(tools); ns
{'multer': <function __main__.multer(a: float, b: float) -> float>,
 'pow': <function __main__.pow(a: float, b: float) -> float>}
??mk_ns
def mk_ns(fs):
    if isinstance(fs, abc.Mapping): return fs
    merged = {}
    for o in listify(fs):
        if isinstance(o, dict): merged |= o
        elif callable(o) and hasattr(o, '__name__'): merged |= {o.__name__: o}
    return merged

File: /usr/local/lib/python3.12/site-packages/toolslm/funccall.py

Step 2: Make the First API Call

We’ll now send a message to the LLM endpoint asking it to complete a somewhat complex calculation.

from fastcore.all import *
os.environ['MOONSHOT_API_BASE'] = 'https://api.moonshot.cn/v1'

Here I’ve just changed my API end-point from 'https://api.moonshot.ai/v1' to 'https://api.moonshot.cn/v1' as my API key is only compatible with servers based in China.

from litellm import completion
hist = [{'role': 'user', 'content': 'Calculate 2*2353**5*1/2'}]
r = completion(model='moonshot/kimi-k2-0711-preview', messages=hist, tools=schemas); r

I need to calculate 2 * 2353^5 * 1/2.

Let me break this down: - First, I’ll calculate 2353^5 - Then multiply by 2 - Then multiply by 1/2 (which is the same as dividing by 2)

Let me start with 2353^5:

🔧 pow({“a”: 2353, “b”: 5})

  • id: chatcmpl-6923a5f5ce7148a79dfc95ba
  • model: moonshot/kimi-k2-0711-preview
  • finish_reason: tool_calls
  • usage: Usage(completion_tokens=93, prompt_tokens=111, total_tokens=204, completion_tokens_details=None, prompt_tokens_details=None)
print(r)
ModelResponse(id='chatcmpl-6923a5f5ce7148a79dfc95ba', created=1763943925, model='moonshot/kimi-k2-0711-preview', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='tool_calls', index=0, message=Message(content="I need to calculate 2 * 2353^5 * 1/2.\n\nLet me break this down:\n- First, I'll calculate 2353^5\n- Then multiply by 2\n- Then multiply by 1/2 (which is the same as dividing by 2)\n\nLet me start with 2353^5:", role='assistant', tool_calls=[ChatCompletionMessageToolCall(index=0, function=Function(arguments='{"a": 2353, "b": 5}', name='pow'), id='pow:0', type='function')], function_call=None, provider_specific_fields={'refusal': None}), provider_specific_fields={})], usage=Usage(completion_tokens=93, prompt_tokens=111, total_tokens=204, completion_tokens_details=None, prompt_tokens_details=None), service_tier=None)

Step 3: Check if a Tool Call was Requested

We can see that a tool call was requested.

r.choices[0].finish_reason
'tool_calls'

Step 4: Execute It

All that’s left is, well, to execute it.

tc = r.choices[0].message.tool_calls[0].to_dict(); tc
{'index': 0,
 'function': {'arguments': '{"a": 2353, "b": 5}', 'name': 'pow'},
 'id': 'pow:0',
 'type': 'function'}
import json
res = ns[tc['function']['name']](**json.loads(tc['function']['arguments'])); res
72128954282026993

Step 5: Build History

hist.append(r.choices[0].message)
hist.append(mk_tc_result(tc, str(res))); hist
[{'role': 'user', 'content': 'Calculate 2*2353**5*1/2'},
 Message(content="I need to calculate 2 * 2353^5 * 1/2.\n\nLet me break this down:\n- First, I'll calculate 2353^5\n- Then multiply by 2\n- Then multiply by 1/2 (which is the same as dividing by 2)\n\nLet me start with 2353^5:", role='assistant', tool_calls=[ChatCompletionMessageToolCall(index=0, function=Function(arguments='{"a": 2353, "b": 5}', name='pow'), id='pow:0', type='function')], function_call=None, provider_specific_fields={'refusal': None}),
 {'tool_call_id': 'pow:0',
  'role': 'tool',
  'name': 'pow',
  'content': '72128954282026993'}]

And that’s all!

Now we just have to keep looping until the query is solved.

while r.choices[0].finish_reason!='stop':
    mtcd = r.choices[0].message.tool_calls[0].to_dict()
    o = ns[mtcd['function']['name']](**json.loads(mtcd['function']['arguments']))
    tc_res = mk_tc_result(mtcd, str(o))
    hist.append(r.choices[0].message)
    hist.append(tc_res)
    r = completion(model='moonshot/kimi-k2-0711-preview', messages=hist, tools=schemas)
r.choices[0].finish_reason
'stop'
from pprint import pprint as pp
pp(hist)
[{'content': 'Calculate 2*2353**5*1/2', 'role': 'user'},
 Message(content="I need to calculate 2 * 2353^5 * 1/2.\n\nLet me break this down:\n- First, I'll calculate 2353^5\n- Then multiply by 2\n- Then multiply by 1/2 (which is the same as dividing by 2)\n\nLet me start with 2353^5:", role='assistant', tool_calls=[{'index': 0, 'function': {'arguments': '{"a": 2353, "b": 5}', 'name': 'pow'}, 'id': 'pow:0', 'type': 'function'}], function_call=None, provider_specific_fields={'refusal': None}),
 {'content': '72128954282026993',
  'name': 'pow',
  'role': 'tool',
  'tool_call_id': 'pow:0'},
 Message(content="I need to calculate 2 * 2353^5 * 1/2.\n\nLet me break this down:\n- First, I'll calculate 2353^5\n- Then multiply by 2\n- Then multiply by 1/2 (which is the same as dividing by 2)\n\nLet me start with 2353^5:", role='assistant', tool_calls=[{'index': 0, 'function': {'arguments': '{"a": 2353, "b": 5}', 'name': 'pow'}, 'id': 'pow:0', 'type': 'function'}], function_call=None, provider_specific_fields={'refusal': None}),
 {'content': '72128954282026993',
  'name': 'pow',
  'role': 'tool',
  'tool_call_id': 'pow:0'}]

Refactored

def set_tools(tools):
    tools = listify(tools)
    schemas = [lite_mk_func(t) for t in tools]
    ns = mk_ns(tools)
    return tools, schemas, ns
def make_query(m,h,s):
    return completion(model=m, messages=h, tools=s)
def tc2dict(r):
    return r.choices[0].message.tool_calls[0].to_dict()
def run_tool(tc,ns):
    return ns[tc['function']['name']](**json.loads(tc['function']['arguments']))
def build_hist(h,r,tc_res):
    h.append(r.choices[0].message)
    h.append(mk_tc_result(r.choices[0].message.tool_calls[0],str(tc_res)))
hist = [{'role':'user', 'content':'Determine 2*3**4**5'}]
tools, schemas, ns = set_tools([multer,pow])
r = make_query('moonshot/kimi-k2-0711-preview',hist,schemas); r

I’ll solve this step by step, following the order of operations for exponentiation (which is right-associative). The expression 2*345 means 2×(3(45)).

🔧 pow({“a”: 4, “b”: 5})

  • id: chatcmpl-6923a600bd9f0794473f54b0
  • model: moonshot/kimi-k2-0711-preview
  • finish_reason: tool_calls
  • usage: Usage(completion_tokens=65, prompt_tokens=108, total_tokens=173, completion_tokens_details=None, prompt_tokens_details=None, cached_tokens=108)
while r.choices[0].finish_reason != 'stop':
    tc = tc2dict(r)
    tc_res = run_tool(tc, ns)
    build_hist(hist,r,tc_res)
    r = make_query('moonshot/kimi-k2-0711-preview', hist, schemas)
hist
[{'role': 'user', 'content': 'Determine 2*3**4**5'},
 Message(content="I'll solve this step by step, following the order of operations for exponentiation (which is right-associative). The expression 2*3**4**5 means 2×(3^(4^5)).", role='assistant', tool_calls=[{'index': 0, 'function': {'arguments': '{"a": 4, "b": 5}', 'name': 'pow'}, 'id': 'pow:0', 'type': 'function'}], function_call=None, provider_specific_fields={'refusal': None}),
 {'tool_call_id': 'pow:0', 'role': 'tool', 'name': 'pow', 'content': '1024'},
 Message(content="Now I'll calculate 3^1024:", role='assistant', tool_calls=[{'index': 0, 'function': {'arguments': '{"a": 3, "b": 1024}', 'name': 'pow'}, 'id': 'pow:1', 'type': 'function'}], function_call=None, provider_specific_fields={'refusal': None}),
 {'tool_call_id': 'pow:1',
  'role': 'tool',
  'name': 'pow',
  'content': '373391848741020043532959754184866588225409776783734007750636931722079040617265251229993688938803977220468765065431475158108727054592160858581351336982809187314191748594262580938807019951956404285571818041046681288797402925517668012340617298396574731619152386723046235125934896058590588284654793540505936202376547807442730582144527058988756251452817793413352141920744623027518729185432862375737063985485319476416926263819972887006907013899256524297198527698749274196276811060702333710356481'},
 Message(content="Finally, I'll multiply this by 2:", role='assistant', tool_calls=[{'index': 0, 'function': {'arguments': '{"a": 2, "b": 373391848741020043532959754184866588225409776783734007750636931722079040617265251229993688938803977220468765065431475158108727054592160858581351336982809187314191748594262580938807019951956404285571818041046681288797402925517668012340617298396574731619152386723046235125934896058590588284654793540505936202376547807442730582144527058988756251452817793413352141920744623027518729185432862375737063985485319476416926263819972887006907013899256524297198527698749274196276811060702333710356481}', 'name': 'multer'}, 'id': 'multer:2', 'type': 'function'}], function_call=None, provider_specific_fields={'refusal': None}),
 {'tool_call_id': 'multer:2',
  'role': 'tool',
  'name': 'multer',
  'content': '746783697482040087065919508369733176450819553567468015501273863444158081234530502459987377877607954440937530130862950316217454109184321717162702673965618374628383497188525161877614039903912808571143636082093362577594805851035336024681234596793149463238304773446092470251869792117181176569309587081011872404753095614885461164289054117977512502905635586826704283841489246055037458370865724751474127970970638952833852527639945774013814027798513048594397055397498548392553622121404667420712962'}]

Using Lisette

Lisette is pretty concise. In only 2 lines of code, you can have a tool-loop up and running

m = 'moonshot/kimi-k2-0711-preview'
c = Chat(m); c
<lisette.core.Chat at 0x7f45865da300>
r = c('Who are you?')
from IPython.display import display
c = Chat(m, tools=[multer,pow])
o = c("What's the result of (13**2)*25.5?", max_steps=10, return_all=True); display(*o)

I’ll calculate this step by step. First, I’ll compute 13 squared (13**2), and then multiply the result by 25.5.

🔧 pow({“a”: 13, “b”: 2})

  • id: chatcmpl-6923a854bd9f0794473f563c
  • model: moonshot/kimi-k2-0711-preview
  • finish_reason: tool_calls
  • usage: Usage(completion_tokens=51, prompt_tokens=112, total_tokens=163, completion_tokens_details=None, prompt_tokens_details=None, cached_tokens=112)
{'tool_call_id': 'pow:0', 'role': 'tool', 'name': 'pow', 'content': '169'}

Now I’ll multiply 169 by 25.5:

🔧 multer({“a”: 169, “b”: 25.5})

  • id: chatcmpl-6923a861655ccdf1f78a3fd1
  • model: moonshot/kimi-k2-0711-preview
  • finish_reason: tool_calls
  • usage: Usage(completion_tokens=35, prompt_tokens=180, total_tokens=215, completion_tokens_details=None, prompt_tokens_details=None)
{'tool_call_id': 'multer:1',
 'role': 'tool',
 'name': 'multer',
 'content': '4309.5'}

The result of (13²)×25.5 is 4309.5.

  • id: chatcmpl-6923a864ce7148a79dfc97f4
  • model: moonshot/kimi-k2-0711-preview
  • finish_reason: stop
  • usage: Usage(completion_tokens=18, prompt_tokens=236, total_tokens=254, completion_tokens_details=None, prompt_tokens_details=None)

The only thing to note here is the max_steps parameter. That determines the length of the tool-loop.

Conclusion

Building a LLM agent is simpler than it looks seem—at its core, it’s just a LLM calling functions in a loop.

By repeatedly calling the model, executing requested tools, and feeding results back into the conversation history, you create a system that can solve complex multi-step problems.

If you have any comments, questions, suggestions, feedback, criticisms, or corrections, please do post them down in the comment section below!

Back to top