一、先问一个问题:大模型能"干活"吗?
你用 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 消息的四种角色
| 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 的第一块积木。有了它,往后的路是:
网硕互联帮助中心






评论前必须登录!
注册