tool calling 没那么玄:我用 Prompt 手搓一遍
步入《从零写一个通用 Agent 内核》系列的第 0 步,在编写任何一行工具分发代码之前,我并未动笔,纯粹通过对话形式先行实践了 tool calling。

一段「不太对劲」的对话
我仅有一个最为基础的 agent 循环,它由 DeepSeek 的 OpenAI 兼容接口与一个 read_file 工具构成。我与模型之间达成了如同君子般的约定:
复制代码You: 你是一名天气专家。当我问某地天气时,你用 get_weather(地点) 来回复,
然后我会告诉你那个地方的天气。AI: 明白!现在我就是你的专属天气专家,随时准备调用 get_weather(地点)。You: 北京天气怎么样?AI: 好的,正在为您查询北京天气,请稍等!
get_weather(北京)
观察 API 返回,发现一个关键细节:模型似乎“调用”了 get_weather,但此轮的 finish_reason 被标识为 stop,而非 tool_calls。那一行 get_weather(北京) 仅仅是模型打印出的普通文本,并未发起任何真正的工具调用。
那么,谁真正“执行”了这个 get_weather?是我自己。
复制代码You: 微风小雨 ← 这一步是我在扮演 get_weather,把"查询结果"喂回去AI: 北京此刻微风伴小雨,适合披件薄外套撑伞散步~(根据我喂的值产出最终答复)
一个完整的 tool calling 循环就这样在手动操作下完成了。
别急着喊「翻车」,这是我故意的
模型并未真正调用工具,但这恰恰是我需要的:通过最原始的方式,亲手走一遍平时被 API 黑盒化的过程。tool calling 听起来复杂,然而剥开来看,只涉及三个角色:
| 角色 | 这场实验里是谁 | 真实 agent 里是谁 |
|---|---|---|
| 决定调用、发出调用、解读返回值 | 模型 | 模型(这层永远是模型的活) |
| 真正执行工具、把结果递回去 | 我(人肉后端) | 执行器代码 |
| 调用和结果怎么来回传递(协议) | 普通聊天文本 | tool_calls / role:tool / tool_call_id |
模型那一列从未改变,变化的只是中间那行:谁来执行以及结果如何传回。这正是 agent 框架的全部职责:将人肉执行器替换为代码,将聊天文本转换为结构化协议。
真机长什么样:换一个「真的」工具
为了对比,我提出了一个 read_file 工具能够处理的问题:
复制代码You: agent.py 文件是做什么的?
这次模型的返回中,finish_reason 变成了 tool_calls:
复制代码tool_calls = [ read_file(arguments='{"path": "agent.py"}') ]
这是一次结构化的、真正的工具调用。随后,我的 Python 代码执行了它:
复制代码for tool_call in assistant_message.tool_calls:
tool_name = tool_call.function.name # "read_file"
tool_args = json.loads(tool_call.function.arguments) # {"path": "agent.py"}
result = read_file(**tool_args) # 真正的执行在这行
conversation.append({
"role": "tool",
"content": str(result),
"tool_call_id": tool_call.id, # 协议把结果递回去
})
结果通过 role: tool 喂回模型,模型读取文件后产出分析。
同样的循环,这一次没有“我”的位置——那个执行者已被 read_file(**tool_args) 这行代码取代。
划重点:那条最容易栽的边界
两次运行唯一的差别在于:天气实验中,get_weather 从未进入 tools 列表;而在第二次操作中,read_file 被纳入了 tools 列表。仅此一点,结果却天差地别:前者只能通过文字假装调用,后者则触发了真正的 tool_calls。这解释了为何在 prompt 中苦心撰写“你拥有以下能力”常常不奏效。
接下来:把「人肉执行器」工业化
这场手动实验为整个系列确定了主线。当你认真替代“那个人”时,会发现人肉执行过程中那些下意识完成却从未显式记录的动作,代码必须逐一处理:
- 模型给出的参数是格式错误的 JSON?(人会皱眉重读,代码直接崩溃)
- 工具运行了 10 秒仍未返回?(人会停止等待,代码默认死等)
- 工具返回 5000 行结果全部塞给模型?(人会提炼重点,代码可能淹没模型)
- 此操作将删除文件,是否需要先确认?(人有常识,代码需要一道“确认门”)
- 出错时返回 "Error" 还是 "file not found, did you mean config.yaml"?(前者使模型困惑,后者帮助其自我纠错)
这些属于横切关注点,不归属于任何具体工具,而是“执行”本身的相关事务。下一篇,我们将开始把这些关注点抽离出来,统一纳入一条执行管道——这条管道将成为决定 agent 智能程度的地基。
系列《从零写一个通用 Agent 内核》持续记录这个小核心从零到一的成长过程,不讲术语,只呈现其本来面貌、遇到的问题以及如何演进。后续篇章将继续深入。