云计算百科
云计算领域专业知识百科平台

从零学会 ToolCall:让大模型真正“动手干活“-Day14

一、先问一个问题:大模型能"干活"吗?

你用 ChatGPT / DeepSeek 聊天时,它只能输出文字——它不知道自己"现在几点"、 不知道今天的实时天气、更没法帮你读写文件或操作电脑。

那怎么让 AI 真正"干活"(查天气、查数据、操作工具)?

答案就是 ToolCall(工具调用):让大模型"提议"调用某个函数, 我们自己的代码真正执行那个函数,再把结果交回给模型,由模型组织成最终回答。

用户提问


大模型 ──► 输出:"我想调用 get_pet_info 工具,参数 pet=cat"
▲ │
│ ▼
│ 我们的代码执行 get_pet_info("cat")
│ │
│ ▼ 返回 "猫:一天睡12~16小时……"
└── 模型看到结果,组织成最终回答 ──► 输出给用户

核心一句话:模型只负责"提议",执行权永远在我们手里。 这就是 Agent(智能体)区别于普通聊天机器人的本质。

二、四课递进:从"手搓"到"框架"

我写了一个教学项目 simpleAgent,用 四课递进 的方式讲透 ToolCall, 示例主题是"宠物图鉴"(查猫/狗/仓鼠/兔子的趣味知识)。

课程实现方式一句话概括依赖
第一课 字符串协议 手工模拟模型输出,split 解析 零依赖
第二课 Prompt 协议 接真实模型,提示词约定格式 + 正则解析 langchain
第三课 LangChain 原生 @tool 装饰器,框架全自动解析 langchain
第四课 底层透视 OpenAI SDK 打印原始 JSON,看清本质 openai

下面逐课拆解。

三、第一课:字符串协议(最朴素的 ToolCall)

思想:约定一种文字格式,模型按格式输出,我们用字符串解析来执行工具。

# 1. 工具本体:一个普通 Python 函数
def get_pet_info(pet: str) > str:
facts = {"cat": "猫:一天睡12~16小时……", "dog": "狗:鼻纹独一无二……"}
return facts.get(pet, "图鉴里没有这个宠物~")

# 2. 约定协议:模型想调工具时输出 "工具名:参数JSON"
model_output = 'get_pet_info:{"pet": "cat"}'

# 3. 我们解析这段文字
tool_name, args_text = model_output.split(":", maxsplit=1)
args = json.loads(args_text) # {"pet": "cat"} 字符串 -> 字典
print(get_pet_info(**args)) # ** 把字典展开成关键字参数

知识点:

  • 模型输出就是"协议报文",解析协议是 Agent 的老本行
  • maxsplit=1 很关键——参数里也可能有冒号,只切第一刀
  • **args 是把字典 {"pet": "cat"} 展开成 get_pet_info(pet="cat") 的语法糖

局限:格式脆弱。模型一高兴输出错格式,解析就失败。

四、第二课:Prompt 协议(接真实模型 + 正则解析)

思想:不依赖模型的原生工具能力,而是在提示词里"教育"模型按格式输出, 再用正则表达式稳妥地提取。

SYSTEM_PROMPT = """
你是一个宠物图鉴助手。系统里有一个工具 get_pet_info。
当用户询问宠物时,不要直接回答,必须严格输出:

<Tool>get_pet_info</Tool>
<Args>{"pet":"cat"}</Args>
""".strip()

# 调用真实模型(LangChain 封装,DeepSeek 等 OpenAI 兼容服务商通用)
response = ChatOpenAI(model="deepseek-chat", base_url="https://api.deepseek.com/v1",
api_key=API_KEY, temperature=0).invoke([
SystemMessage(content=SYSTEM_PROMPT), # 协议:怎么输出
HumanMessage(content="我想了解猫的知识"), # 问题:用户说了啥
])

# 正则解析:从模型输出里"抠出"工具名和参数
tool_match = re.search(r"<Tool>(.*?)</Tool>", str(response.content), re.DOTALL)
args_match = re.search(r"<Args>(.*?)</Args>", str(response.content), re.DOTALL)

知识点:

  • temperature=0:协议解析场景要"最严谨",随机度归零
  • 正则 <Tool>(.*?)</Tool> 中 .*? 是非贪婪匹配,只取标签之间的内容
  • re.DOTALL:让 . 也能匹配换行,防止模型输出里夹了换行导致提取失败
  • System Prompt 就是"给模型定的规矩",这是 Prompt Engineering 的入门动作

局限:模型不保证永远守规矩。而"原生 ToolCall"让模型直接返回结构化数据,天然不会错。

五、第三课:LangChain 原生 ToolCall(框架全自动)

思想:用 @tool 装饰器把函数变成"模型可调用"的工具, LangChain 自动完成:协议生成 → 解析 → 参数校验 → 结果回传。手写 40 行变 10 行。

from langchain_core.tools import tool

@tool
def get_pet_info(pet: str) > str:
"""查询宠物的趣味知识。参数 pet:cat、dog、hamster、rabbit。"""
facts = {"cat": "猫:一天睡12~16小时……", "dog": "狗:鼻纹独一无二……"}
return facts.get(pet, "图鉴里没有这个宠物~")

llm = ChatOpenAI(model="deepseek-chat", base_url="…", api_key=API_KEY, temperature=0)
llm_with_tools = llm.bind_tools([get_pet_info]) # 一行绑定,注册工具

response = llm_with_tools.invoke(messages)
print(response.tool_calls)
# 自动解析出:{'name': 'get_pet_info', 'args': {'pet': 'cat'}, 'id': 'call_xxx'}

# 执行工具,结果通过 ToolMessage 放回历史,再交给模型出最终回答
result = get_pet_info.invoke(response.tool_calls[0]["args"])
messages.append(response)
messages.append(ToolMessage(content=result, tool_call_id=response.tool_calls[0]["id"]))
final = llm_with_tools.invoke(messages)

@tool 装饰器自动生成了什么?

  • 函数名 → 工具名 name
  • 函数注释 docstring → 给模型看的 description
  • 参数类型注解 pet: str → 参数 JSON Schema

为什么 @tool 更稳? 模型返回的是结构化字段 tool_calls(name/args/id),不是自由文本, 不用正则、不怕格式错——这是工业级做法。

六、第四课:掀开盖子,看 API 原始返回

思想:第三课太"魔法"了?这一课用最底层的 OpenAI SDK 发一次带 tools 的请求, 把返回的原始 JSON 完整打印出来,亲眼看看 tool_calls 长什么样。

from openai import OpenAI
client = OpenAI(base_url="https://api.deepseek.com/v1", api_key=API_KEY)

response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "我想了解猫的知识"}],
tools=[{
"type": "function",
"function": {
"name": "get_pet_info",
"description": "查询宠物的趣味知识。",
"strict": True,
"parameters": {
"type": "object",
"properties": {"pet": {"type": "string", "description": "宠物英文名"}},
"required": ["pet"],
"additionalProperties": False,
},
},
}],
temperature=0,
)

模型返回的核心结构(亲手跑一遍能看到完整 JSON):

{
"choices": [{
"message": {
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_pet_info",
"arguments": "{\\"pet\\": \\"cat\\"}"
}
}]
}
}]
}

逐字段解读:

字段含义注意
id 本次调用的唯一编号 结果回传时必须带上,模型靠它"对上号"
name 工具名 对应我们注册的 get_pet_info
arguments 参数 是 JSON 字符串,要 json.loads 解析成字典才能用
content 文字内容 纯工具调用时为 null,没有文字回答

看懂这个 JSON,你就理解了:第二课的正则、第三课的 @tool, 本质上都是在跟同一个结构打交道。

七、核心知识点总结

7.1 ToolCall 的本质

模型输出"工具意图"(结构化 or 文本协议) → 代码解析 → 执行 → 结果交回 → 最终回答。

7.2 三种实现方式对比

方式模型输出解析方式稳定性
字符串协议 工具名:参数 文本 split
Prompt 协议 <Tool>/<Args> 文本 正则 re.search
原生 ToolCall 结构化 tool_calls 框架自动

7.3 消息的四种角色

role谁何时出现
system 人设/规则 对话开头,约定行为
user 用户 每次提问
assistant 模型 每次回答;调工具时这条消息必须原样放回历史
tool 工具结果 每次工具执行后,必须带 tool_call_id

协议要求:模型"带工具意图"的消息和工具结果消息都要追加进历史, 模型下一轮才能"看到"结果继续回答。漏掉会报错。

7.4 用到的库速查

库作用
langchain-core 消息对象(SystemMessage/ToolMessage)、@tool 装饰器
langchain-openai OpenAI 兼容接口的封装(DeepSeek/通义等换 base_url 通用)
openai 最底层的 OpenAI 官方 SDK(LangChain 底层也用它)
python-dotenv 读取 .env 配置文件,避免把 Key 写死在代码里

7.5 常见坑

  • 401:API Key 错/没配置;402:余额不足;404:模型名错
  • arguments 是字符串,忘 json.loads 会直接崩
  • tool_call_id 不匹配会被模型拒绝
  • 工具调用循环要加轮数上限(防死循环)

八、动手实验(配套代码)

simpleAgent 代码仓库

git clone https://github.com/honumi-commits/simpleAgent.git
cd simpleAgent
python3 -m venv .venv && source .venv/bin/activate
pip install langchain-core langchain-openai openai python-dotenv
cp .env.example .env # 填入你的 API Key(DeepSeek 注册送额度)

cd toolcall
python3 01_string_protocol.py # 第一课:零依赖,直接跑
python3 02_prompt_protocol_real_model.py # 第二课:正则 + 真实模型
python3 03_langchain_native_toolcall.py # 第三课:@tool 原生
python3 04_true_output.py # 第四课:看原始 JSON

推荐尝试:把 get_pet_info 换成你自己的函数(查天气 API、读文件、查数据库), 再包一个 while True 循环接住多轮对话——你就拥有一个能"干活"的 Agent 雏形了。

九、下一步:从 ToolCall 到 Agent

ToolCall 是 Agent 的第一块积木。有了它,往后的路是:

  • AgentLoop:while True 循环 + 记忆,让 Agent 自主多轮干活
  • 多工具路由:维护"工具名 → 函数"的注册表,按需分发
  • 多 Agent 协作:规划 Agent + 执行 Agent + 验收 Agent 分工
  • 上下文管理:对话过长时自动压缩(Context Engineering)
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从零学会 ToolCall:让大模型真正“动手干活“-Day14
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!