Build Your First Agent from Scratch: Core Loop, Function Calling & Prompt Engineering

This tutorial guides you through building a complete AI Agent from scratch without frameworks, covering environment setup, tool definition with JSON Schema, Function Calling, the core reasoning loop, common pitfalls, prompt engineering techniques for structured output, and modular code organization with hands-on practice tasks.

Coder Trainee
Coder Trainee
Coder Trainee
Build Your First Agent from Scratch: Core Loop, Function Calling & Prompt Engineering

Why Hand-Roll an Agent First?

The author describes a common learning trap: beginners copy-paste framework code (e.g., LangChain), get it running, but cannot debug when things break because the framework hides internals. The solution: build a minimal Agent manually to understand every line — how LLM output is parsed, how tool errors are returned, why loops hang, and what frameworks later automate.

Prerequisites

Python 3.10+

An LLM API key (OpenAI, DeepSeek, Qwen, Zhipu) using OpenAI-compatible interface

Code editor (VS Code/PyCharm) and terminal

Only dependency: pip install openai Basic Python: functions, dict/list, if/else/for/while, json module, try/except

Step 1: Baseline Chat Program

A simple chat function using client.chat.completions.create with a user message. It only chats; cannot access real-time data like weather.

Step 2: Define Tools as Python Functions

Tools are regular Python functions with two requirements: clear docstrings (for LLM understanding) and explicit type hints. Two examples:

def get_weather(city: str) -> str:
    """Query current weather for a city.
    Args:
        city: City name, e.g. "北京", "上海"
    Returns:
        Weather description string
    """
    fake_weather = {
        "北京": "晴,气温 5°C,西北风 3 级",
        "上海": "多云,气温 12°C,东南风 2 级",
        "广州": "小雨,气温 18°C,无持续风向"
    }
    return fake_weather.get(city, f"抱歉,暂时查不到{city}的天气")

def calculate(expression: str) -> str:
    """Calculate a math expression.
    Args:
        expression: Math expression string, e.g. "2 + 3 * 4"
    Returns:
        Result string
    """
    try:
        result = eval(expression)  # Note: eval has security risks in production
        return str(result)
    except Exception as e:
        return f"计算失败:{e}"

Key details: docstrings are for LLM, not humans; return values must be strings for prompt concatenation.

Step 3: Describe Tools with JSON Schema

LLMs cannot read Python code. Tools are described via JSON Schema passed in the tools parameter. Each tool entry includes type: "function", function.name (must match Python function), function.description, and function.parameters (JSON Schema with properties and required fields). Clear descriptions prevent misrouting (e.g., "查询指定城市的当前天气" vs vague "获取信息").

Step 4: Let LLM Decide Tool Calls

Call client.chat.completions.create with tools=tools and tool_choice="auto". The LLM returns a ChatCompletionMessage with tool_calls containing id, function.name, and function.arguments (a JSON string). The LLM only decides; your code executes.

Step 5: Execute Tools and Return Results

Process: parse arguments with json.loads(), route via TOOL_MAP, execute, catch exceptions, return string. Append the assistant's tool-call message and each tool result (role: "tool", tool_call_id, content) to message history. Then call LLM again for final answer.

Step 6: Add the Loop — The Real Agent

Wrap in a while loop with max_iterations (default 10) to prevent infinite loops. Each iteration: call LLM with tools; if no tool_calls, return content; else append message, execute each tool, append results, repeat. Example run for "北京现在天气怎么样?如果气温低于 10 度,帮我算一下 5 + 3 等于几" shows three iterations: get_weather → calculate → final answer.

Five Common Pitfalls

Forgetting to append the assistant's tool-call message before tool results (causes "orphan" tool result errors).

Treating arguments as dict instead of JSON string — must use json.loads().

Omitting max_iterations — leads to runaway loops burning tokens/money.

Returning non-strings from tools — tool results must be strings for message history.

Vague tool descriptions — causes wrong tool selection.

Prompt Engineering for Structured Output

Function Calling isn't supported by all models; sometimes you need custom JSON structures. Techniques:

Explicitly demand JSON format, forbid extra text, provide format example and concrete input example.

Few-shot: give multiple input→output pairs to anchor pattern matching.

System prompt: define role, task boundaries, output style, available tools.

"Three insurance" for production: (1) prompt requires JSON, (2) use response_format={"type": "json_object"} where supported, (3) wrap json.loads() in try/except with fallback regex extraction.

def safe_parse_json(text: str, default=None):
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        import re
        match = re.search(r'\{.*\}', text, re.DOTALL)
        if match:
            try:
                return json.loads(match.group())
            except:
                pass
        return default

Modular Project Structure

my_agent/
├── main.py      # entry point
├── tools.py     # tool definitions
├── agent.py     # agent loop
└── llm.py       # LLM client wrapper

Each file shown with complete code: llm.py wraps call_llm; tools.py holds functions, TOOL_MAP, TOOLS_SCHEMA; agent.py imports and runs loop; main.py provides interactive REPL.

Practice Tasks

Run the code and test three inputs: weather query, calculation, multi-step conditional.

Add a get_time tool with docstring, JSON Schema, registration, and test with time-based conditional.

Challenge: implement failure retry — on tool error, let LLM decide retry/switch/abort, limit retries to 3.

Summary & Next Episode

Five takeaways: (1) Agent heart is a while loop; (2) LLM decides, code executes, arguments are JSON strings; (3) tool docstrings/schemas are for LLM; (4) always set iteration limit; (5) never assume perfect LLM formatting — add fallbacks. Next episode adds memory (short-term, vector DB for long-term), RAG, and task decomposition.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PythonJSON SchemaLLMprompt engineeringAI agentFunction Callingtool useAgent Development
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.