深入理解Agent:从0实现function call (English)
深入理解Agent:从0实现function call (English)
Generated: 2026-06-24 18:38:07
---
Okay, I’ve processed it as you asked. The facts are solid; I mainly adjusted the structure and pacing, removed the overly neat parallelisms, and turned the list-like paragraphs into a more natural narrative. Below is the revised version. See if it works for you.
---
Friend, have you ever had this experience?
You install LangChain, run the official demo, and think, Wow, this Agent thing is amazing. But when you try to add your own tools, tweak a parameter, or run into a stack of mysterious errors while debugging—you’re instantly lost. That day, I was exactly there: staring at that black terminal in my IDE, almost ready to throw my computer out the window.
Why shouldn’t I understand what’s going on inside?
It’s just a function call, right? I wouldn’t accept it. So I turned off LangChain, threw away all the fancy frameworks, grabbed only a requests library and the OpenAI SDK, and started from zero to build an Agent that could call tools. My goal was almost laughably modest: just answer weather questions.
And guess what? Once I wrote it myself, the feeling shifted from
“How does this thing work?” to “Haha, I can build this.”
---
First, a Little Theater: Ask About the Weather
I defined two functions so simple they’re cute:
get_weather(location)— No matter where you go, it tells you “Clear skies.” Why fake data? Because I wanted to get the logic working first; swapping in a real API later would be a piece of cake.send_messages(messages)— Sends queries to the deepseek-chat model. Cheap and fast, what’s not to love?
One note here: if you want to write your own, wrap the LLM call as a shell first, and remember to leave room for the tools parameter. You’ll see later how sweet that “early planning” is.
---
Design Idea: It’s Just a Loop, Don’t Overthink It
When the user asks “What’s the weather like in Tianjin?”, the Agent runs two rounds of LLM calls internally:
- Round 1: The model sees it’s about weather — “Ah, time to call
get_weather” — returns a tool call with the argument “Tianjin”. - Me (local code) executes the function and gets the result “Clear skies”.
- Round 2: I stuff the function result back into the message list and ask the model again. This time the model already has the weather information, so it directly composes a reply for the user.
Sounds simple enough to be a joke? But the first pitfall tripped me up hard — I didn’t match the toolcallid correctly!
Each tool call from the model has a unique ID. When you append the execution result to the messages, the message with role="tool" must carry that toolcallid. I overlooked this at first, and the model, after seeing the result, was clueless and kept saying it needed to call the function again — an infinite loop, like a dog chasing its own tail.
Later I dug into the OpenAI official documentation and understood: in the entire tool-calling flow, the model only gives suggestions (tool name + parameters). It’s you (the code) that actually executes. Then you feed the result back through a role="tool" messenger, and the messenger must have the ID label the model gave. If this step is wrong, the Agent goes dumb.
---
Implementation from Scratch: Three Layers, No More
Layer 1: Tool Layer
A dictionary holding references to all functions, and a list storing the JSON Schema for each tool (you must clearly write the name, description, and parameters). The trap here is deep: the quality of the Schema description directly determines whether the model hits the target.
For example, I revised the description for get_weather four or five times, finally settling on:
“Get the current local weather for the location mentioned in the user's question, returns a string.”
That triggered the model far more reliably than my half-hearted initial “Get weather.” Think about it: the model reads the manual too; if the manual is vague, it just guesses.
Layer 2: Dispatch Layer
The core loop function runagent(userinput) does only these things:
- Insert the user input into the initial messages, with the
toolsparameter attached. - Call the LLM, check the response for
tool_calls. - If there are any, execute them one by one, append the results to the messages (remember to include
toolcallid— important enough to say three times, this ID must not be wrong). - Request the LLM again until there are no more
tool_callsin the response or the maximum round is reached (I set 5 rounds as a safety net). - If a function execution blows up, I do a snapshot rollback: record the current message list length, truncate on error to restore, then tell the model “Tool call failed, try a different way.”
Layer 3: Parsing Layer
I built three lines of defense:
For example, if the tool name isn’t found in the dictionary, the argument JSON fails to parse, or the function throws an exception — I don’t append the error result to the message list (that would confuse the model even more). Instead, I directly return a system message “Tool call abnormal, please retry” and roll back the state.
These three lines of defense were bought with blood and tears. In my first implementation, I had no exception handling; when the model returned a misspelled tool name like get_weatherr, I went straight to globals() and a KeyError crashed the whole loop. Later I added try-except and wrapped the error message into natural language for the model. And guess what? It actually apologized!
“Sorry, I just typed the tool name wrong. Let me call get_weather again.”
Interesting, isn’t it? You teach it to use tools, and it learns to admit mistakes.
---
Pitfall Log: I Stepped In
Cael Lee
Full-stack developer with 8+ years of experience. Currently building AI-powered developer tools. I've tested 20+ AI API providers and coding assistants.