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])
Creating Your Own LLM Agent
![]()
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:
- Send message + available tools to the LLM
- LLM responds with either the answer OR tool request
- If tool is requested: execute the relevant function and add the result to the chat history
- Go back to step 1.
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_funcdef 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_nstoolslm 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_nsdef 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 mergedFile: /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); rI 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'])); res72128954282026993
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, nsdef 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); rI’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!