tool calling 没那么玄:我用 Prompt 手搓一遍

时间:2026-07-04 08:33:46 来源:互联网

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

tool calling 没那么玄:我用 Prompt 手搓了一遍

一段「不太对劲」的对话

我仅有一个最为基础的 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 中苦心撰写“你拥有以下能力”常常不奏效。

接下来:把「人肉执行器」工业化

这场手动实验为整个系列确定了主线。当你认真替代“那个人”时,会发现人肉执行过程中那些下意识完成却从未显式记录的动作,代码必须逐一处理:

  1. 模型给出的参数是格式错误的 JSON?(人会皱眉重读,代码直接崩溃)
  2. 工具运行了 10 秒仍未返回?(人会停止等待,代码默认死等)
  3. 工具返回 5000 行结果全部塞给模型?(人会提炼重点,代码可能淹没模型)
  4. 此操作将删除文件,是否需要先确认?(人有常识,代码需要一道“确认门”)
  5. 出错时返回 "Error" 还是 "file not found, did you mean config.yaml"?(前者使模型困惑,后者帮助其自我纠错)

这些属于横切关注点,不归属于任何具体工具,而是“执行”本身的相关事务。下一篇,我们将开始把这些关注点抽离出来,统一纳入一条执行管道——这条管道将成为决定 agent 智能程度的地基。


系列《从零写一个通用 Agent 内核》持续记录这个小核心从零到一的成长过程,不讲术语,只呈现其本来面貌、遇到的问题以及如何演进。后续篇章将继续深入。