Short answer
Build it in six decisions: a narrow goal, a short list of tools, a loop that lets the model call those tools through function calling, a memory for what it has learned, firm stop rules, and a log of every step. Write each one down before you code, and the program itself becomes short: the worked research-summary agent below fits in about seventy lines of Python.
Follow-up question How do you decide what tools it gets?
An AI agent is a program in which a language model decides the next step toward a goal, uses a tool to take that step, looks at the result and decides again. The idea comes from research such as the 2022 ReAct paper, which showed that letting a model alternate between reasoning and actions, like looking something up, made its answers more grounded than reasoning alone.
Most failed agent projects fail at the planning stage, not in the code. The goal is too broad, the agent has too many tools, or nobody decided when it should stop. So this guide works through the plan first, then shows the code that follows from it.
Step 1: Write a one-sentence goal
A good agent goal names the input, the output and the boundary. Ours: Given a question, find the relevant documents in a local folder of articles, read them, and write a short summary that cites the files it used, saying so when the documents do not answer the question. It reads but never writes, sends or deletes, which keeps the first version safe.
Step 2: Choose the fewest tools that reach the goal
A tool here is a function in your program that the model may ask you to run. Our agent needs two: search_docs to find likely files, and read_doc to read one. With OpenAI's function calling, you describe each tool with a name, a plain-language description and its parameters in JSON Schema, a standard format for describing data. The model never runs anything itself; it replies with the name of a function and the arguments it wants, and your code decides whether to run it.
Step 3: Write the loop
Each pass sends the conversation and the tool list to the model. If the reply contains tool calls, your code runs them and adds each result as a message with the role tool. If it contains none, the model has written its answer. This version uses the openai Python library (version 1.x) with the tools parameter, as in OpenAI's own cookbook examples.
import json
from pathlib import Path
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
MODEL = "gpt-4o"
DOCS = Path("articles") # a folder of .txt files
LOG = Path("agent_log.jsonl")
def search_docs(query):
words = query.lower().split()
hits = []
for f in DOCS.glob("*.txt"):
text = f.read_text(encoding="utf-8").lower()
score = sum(text.count(w) for w in words)
if score:
hits.append((score, f.name))
hits.sort(reverse=True)
return json.dumps([name for _, name in hits[:5]])
def read_doc(name):
path = DOCS / Path(name).name # never leave the folder
if not path.exists():
return "Error: no document with that name."
return path.read_text(encoding="utf-8")[:6000]
TOOLS = [
{"type": "function", "function": {
"name": "search_docs",
"description": "Find documents that mention the query words. Returns up to 5 file names.",
"parameters": {"type": "object",
"properties": {"query": {"type": "string", "description": "A few keywords"}},
"required": ["query"]}}},
{"type": "function", "function": {
"name": "read_doc",
"description": "Return the text of one document (first 6,000 characters).",
"parameters": {"type": "object",
"properties": {"name": {"type": "string", "description": "A file name from search_docs"}},
"required": ["name"]}}},
]
FUNCTIONS = {"search_docs": search_docs, "read_doc": read_doc}
SYSTEM = ("You are a research assistant. Answer only from documents you have read "
"with read_doc, and cite their file names in brackets. If the documents "
"do not answer the question, say so. Keep summaries under 200 words.")
def log(entry):
with LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry) + "\n")
def run(question, max_steps=8, token_budget=40000):
messages = [{"role": "system", "content": SYSTEM},
{"role": "user", "content": question}]
used = 0
for step in range(1, max_steps + 1):
response = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS, temperature=0)
used += response.usage.total_tokens
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
log({"step": step, "final": msg.content, "tokens": used})
return msg.content
for call in msg.tool_calls:
func = FUNCTIONS.get(call.function.name)
try:
args = json.loads(call.function.arguments)
result = func(**args) if func else "Error: unknown tool."
except Exception as e:
args, result = call.function.arguments, f"Error: {e}"
log({"step": step, "tool": call.function.name, "args": args, "result": result[:200]})
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
if used > token_budget:
return "Stopped: token budget reached."
return "Stopped: step limit reached."
print(run("What do our articles say about heat pumps in cold climates?"))
Step 4: Decide what it remembers
The messages list is the agent's short-term memory: everything it has searched and read during this run. It disappears when the run ends. If you want the agent to remember across runs, save something deliberately, for example by appending each final summary to a file the agent can search next time. Start without long-term memory; add it only when you see a need.
Step 5: Set stop rules
Three are built in above: a final answer ends the run, max_steps caps the number of passes, and the token budget caps cost, since each call resends the whole conversation. Add a spending limit in your OpenAI account settings as a last safety net.
Step 6: Log everything
The agent_log.jsonl file records each tool call, its arguments and the start of its result, one line per event. When the summary is wrong, the log shows whether the model searched for the wrong words, skipped a file, or misread one. That is how you improve the descriptions and the system message.
Try it: Put five or six text files on one topic in an articles folder, run the script with a question they partly answer, then open agent_log.jsonl. Change one tool description, run it again, and compare the steps the model chose.
How do you decide what tools it gets?
Start from the goal and list the actions a careful person would need to reach it, then give the agent only those. Every extra tool is one more choice the model can get wrong, and one more thing to test. Two tools that each do one clear job beat one tool with many options.
Sort tools by risk. Read-only tools, such as searching or reading files, are safe to let the agent call freely. Tools with consequences, such as sending email, posting, buying or deleting, should pause and ask a person to confirm; OpenAI gave exactly that advice when it launched function calling. Keep parameters narrow, too: read_doc above accepts only a file name and strips any folder path, so the model cannot wander outside the folder you chose.
Finally, treat what tools return as untrusted. A document or web page can contain text that tries to instruct the model, a risk OpenAI also flagged. Clear tool descriptions, a firm system message and human approval for anything irreversible keep the agent doing the one job you designed it for.
Sources
- How to call functions with chat models (OpenAI Cookbook, commit a519708) — OpenAI (GitHub), May 17, 2024
- Function calling and other API updates — OpenAI, June 13, 2023
- ReAct: Synergizing Reasoning and Acting in Language Models — arXiv (Shunyu Yao and six co-authors), October 6, 2022