Giving LLMs Hands and Feet: Agent Architecture and Code Practice

    Agents combine a large language model that decides the next action, tools that fetch data or perform work, and a control loop that handles iteration and errors. This article explains why tools matter, how the reason-act-observe loop works, how to define tools and implement them in Python with the OpenAI-compatible API, when to use a framework versus a custom loop, how to debug failed tool calls, and what to watch in cloud deployments.

    Start with the conclusion

    An agent has three parts: the large language model decides what to do next, tools retrieve information or execute actions, and the program owns the loop and error handling. All three are required. The rest of this article follows that order and includes a complete code example.

    Why models need tools

    A model’s knowledge stops at the cutoff of its training data. It cannot read today’s news or query an internal database. With tools, it can look up external information before answering, or run an action after confirmation. Tool calling turns the model from “answering questions” into “completing tasks.”

    The basic agent loop

    Agents typically follow reason, call, observe:

    1. The model reads the user question and conversation history and decides whether a tool is needed.
    2. If so, it emits a tool name and arguments; the program runs the matching function.
    3. The program appends the result to the conversation; the model judges again.
    4. When the model no longer requests tools, the loop ends and the final answer is returned.

    Always set a maximum number of steps. Models sometimes call the same tool repeatedly; an unbounded loop burns API cost.

    Defining tool descriptions

    The model only sees the description when deciding whether to call a tool, so purpose and parameters must be explicit. Vague descriptions lead to wrong calls or bad arguments. OpenAI-compatible APIs use a schema like this:

    1. [
    2. {
    3. "type": "function",
    4. "function": {
    5. "name": "fetch_latest_news",
    6. "description": "Get latest news titles and summaries for a tech category. Use when the user asks about recent technology developments.",
    7. "parameters": {
    8. "type": "object",
    9. "properties": {
    10. "category": {
    11. "type": "string",
    12. "description": "Tech category, e.g. artificial intelligence, cloud computing, chips"
    13. }
    14. },
    15. "required": ["category"]
    16. }
    17. }
    18. }
    19. ]

    name must match the registered function. description states when to use the tool. parameters is JSON Schema; required lists mandatory fields.

    Implementing tools and the agent loop

    Tool functions should handle success, network timeouts, and invalid arguments. On failure, return structured error text instead of raising and aborting the loop, so the model can read the cause and adapt.

    1. import json
    2. import os
    3. import requests
    4. from openai import OpenAI
    5. NEWS_API_URL = os.environ.get("NEWS_API_URL", "https://api.example.com/news")
    6. MAX_STEPS = 8 # max reasoning rounds per conversation
    7. def fetch_latest_news(category: str) -> str:
    8. """GET is idempotent, so retry on transient failures."""
    9. last_error = None
    10. for _ in range(3):
    11. try:
    12. resp = requests.get(NEWS_API_URL, params={"category": category}, timeout=5)
    13. resp.raise_for_status()
    14. return resp.text[:4000] # truncate to avoid filling the context
    15. except requests.RequestException as e:
    16. last_error = e
    17. return json.dumps({"error": f"tool execution failed: {last_error}"}, ensure_ascii=False)
    18. TOOL_REGISTRY = {"fetch_latest_news": fetch_latest_news}
    19. TOOL_DEFINITIONS = [
    20. {
    21. "type": "function",
    22. "function": {
    23. "name": "fetch_latest_news",
    24. "description": "Get latest news titles and summaries for a tech category. Use when the user asks about recent technology developments.",
    25. "parameters": {
    26. "type": "object",
    27. "properties": {
    28. "category": {
    29. "type": "string",
    30. "description": "Tech category, e.g. artificial intelligence, cloud computing, chips"
    31. }
    32. },
    33. "required": ["category"]
    34. }
    35. }
    36. }
    37. ]
    38. def agent_run(user_query: str, llm_client, model_name: str, tool_definitions: list) -> str:
    39. messages = [
    40. {"role": "system", "content": "You are an assistant that can use tools."},
    41. {"role": "user", "content": user_query},
    42. ]
    43. for _ in range(MAX_STEPS):
    44. response = llm_client.chat.completions.create(
    45. model=model_name,
    46. messages=messages,
    47. tools=tool_definitions,
    48. )
    49. message = response.choices[0].message
    50. # No tool call: return the final answer
    51. if not message.tool_calls:
    52. return message.content
    53. # Convert SDK objects to dicts before appending
    54. messages.append({
    55. "role": "assistant",
    56. "content": message.content or "",
    57. "tool_calls": [
    58. {
    59. "id": tc.id,
    60. "type": "function",
    61. "function": {"name": tc.function.name, "arguments": tc.function.arguments},
    62. }
    63. for tc in message.tool_calls
    64. ],
    65. })
    66. for tc in message.tool_calls:
    67. name = tc.function.name
    68. try:
    69. args = json.loads(tc.function.arguments)
    70. except json.JSONDecodeError:
    71. result = json.dumps({"error": "arguments are not valid JSON"}, ensure_ascii=False)
    72. else:
    73. func = TOOL_REGISTRY.get(name)
    74. if func is None:
    75. result = json.dumps({"error": f"unknown tool: {name}"}, ensure_ascii=False)
    76. else:
    77. try:
    78. result = func(**args)
    79. except TypeError as e:
    80. result = json.dumps({"error": f"argument error: {e}"}, ensure_ascii=False)
    81. messages.append({
    82. "role": "tool",
    83. "tool_call_id": tc.id,
    84. "content": str(result),
    85. })
    86. return "Reached the maximum number of reasoning steps; stopped calling tools."
    87. if __name__ == "__main__":
    88. client = OpenAI(api_key=os.environ["LLM_API_KEY"], base_url=os.environ.get("LLM_BASE_URL"))
    89. print(agent_run("What is the news in AI today?", client, "your-model-name", TOOL_DEFINITIONS))

    Install with pip install openai requests. Read API keys from environment variables; do not hard-code them. TOOL_DEFINITIONS is the tool list from the previous section.

    Three loop details matter. First, MAX_STEPS caps rounds. Second, assistant messages are converted to dicts so SDK objects do not break serialization. Third, JSON parse, tool lookup, and argument checks each have failure paths; errors go back to the model as text.

    Framework or a custom loop

    LangChain, LlamaIndex, and similar libraries wrap this loop. They help for prototypes or multi-vendor models. A custom loop is transparent, has fewer dependencies, and is easier to debug. Both still need step limits, argument validation, and timeouts. If you need an auditable call path, get a native SDK flow working first, then decide whether to add a framework.

    Debugging failed tool calls

    • Model never calls tools: Check that description states when to use the tool and that tools is actually sent.
    • Bad argument format: Print the raw tc.function.arguments string. Often field names do not match the schema, or the model emitted invalid JSON.
    • Tool succeeds but the result is useless: Check length and structure. Long pages or logs fill the context; truncate or summarize first.
    • Loop never stops: See whether MAX_STEPS was hit, and log each round’s tool name and arguments to find repeated calls.

    Cloud deployment notes

    When you run an agent in containers or other cloud services:

    • Least privilege: Grant the service account only what the task needs. Prefer read-only. Writes and deletes should require human confirmation.
    • Secrets: Store model API keys and tool credentials in the platform’s secret manager and inject them via environment variables or mounts.
    • Log redaction: Logging model I/O helps debugging. Redact phone numbers, IDs, and similar PII before writing logs.
    • Cost alerts: Alert on usage and spend; cap rounds and output length per request.
    • Rollback: Keep the previous image so you can revert quickly.

    Object storage can hold long-term memory and user uploads; logging services collect reasoning and tool traces. The platform provides these; the agent code does not need to reinvent them.

    Summary

    Giving a model “hands” is not about how many tools you wire up. Clear tool descriptions, errors the model can read, a bounded loop, and tight permissions determine whether a prototype can run in production.