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

你用 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-openaiOpenAI 兼容接口的封装(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 的第一块积木。有了它,往后的路是:

  1. AgentLoopwhile True 循环 + 记忆,让 Agent 自主多轮干活
  2. 多工具路由:维护"工具名 → 函数"的注册表,按需分发
  3. 多 Agent 协作:规划 Agent + 执行 Agent + 验收 Agent 分工
  4. 上下文管理:对话过长时自动压缩(Context Engineering)

更多推荐