📌 前置知识:已完成第一课至第四课
🎯 本课目标:让 AI 不仅选择动作,还能指定参数,真正调用外部能力
💡 核心概念:工具接口 / 结构化工具调用 / 请求与执行分离


前言

上节课,我们让 AI 学会了做决策。

现在它能分析用户的输入,从一组选项中选出最合适的动作。比如用户说"帮我总结这篇文章",AI 选了 "summarize_text"

能用。但你仔细想想——它只告诉了你"要做什么",没告诉你"怎么做"。

你的代码大概长这样:

decision = agent.decide("帮我总结这篇文章", choices=["answer", "summarize", "translate"])

if decision == "summarize":
    summarize(???)  # 总结什么?原文在哪?
elif decision == "translate":
    translate(???, ???)  # 翻译什么?翻译成什么语言?

发现了吗?选项只是个名字,缺少关键信息:

  • 没有参数。 AI 说"要计算",但没说算什么、用哪个运算符。
  • 没有细节。 AI 说"要翻译",但没说翻译哪段文字、目标语言是什么。

如果 AI 能把这些细节也一起告诉你呢?不是只说"我要用计算器",而是说"用计算器,42 乘以 7"——工具名称 + 具体参数,一次说清楚。

这就是工具调用。


一、第四课还差什么?

回头看第四课做了什么:

decision = agent.decide(
    "What is 42 * 7?",
    choices=["answer_question", "calculate", "translate"]
)
# 输出: "calculate"

AI 知道该用 calculate——意图识别做对了。但接下来的事情就尴尬了:

if decision == "calculate":
    calculate(???)  # 算什么?算 42×7,但 AI 没告诉你

AI 只告诉你"要计算",但具体算什么、用什么运算符,它没说

这是因为第四课的决策输出只有动作名称,没有参数。你的代码拿到了 "calculate" 之后,还得自己想办法从用户输入里解析出数字和运算符——又回到了第三课之前的老问题:用规则去解析自然语言。

第五课要解决这个问题:让 AI 在选择工具的同时,把参数也一并提取出来。

用户: What is 42 * 7?
  ↓
AI 输出:
{
    "tool": "calculator",
    "arguments": {
        "a": 42,
        "b": 7,
        "operation": "multiply"
    }
}

工具名称有了,参数也有了,你的代码可以直接执行,不需要再自己解析用户输入。

对比一下:

第四课 第五课
AI 告诉你什么 “用 calculate” “用 calculator,a=42, b=7, multiply”
参数从哪来 你自己从用户输入里解析 AI 帮你提取好了
下游代码要做什么 if decision == "calculate": parse_and_calculate(user_input) execute(tool_call) 直接执行

第四课的输出是"意图"。第五课的输出是"意图 + 执行细节"。差的就是这个"执行细节"。

两课合在一起,完整的工作流就是:AI 理解意图 → 选择工具 → 提取参数 → 你的代码执行。


二、核心原则:AI 描述意图,你控制执行

第五课最重要的设计原则,用一句话说:

AI 没有能力,你有。

工具接口的定义完全在你的代码里——工具叫什么名字、接受什么参数、做什么事情,全是你说了算。AI 只能通过你给的接口描述来理解工具的存在。

这带来一个很实际的好处:添加新能力不需要重新训练模型。

想让 AI 查天气?加一个 weather 工具,在 prompt 里描述它的参数。想让 AI 搜索?加一个 search 工具。模型不需要微调,不需要额外数据——它只需要在 prompt 里看到新的接口描述就能使用。

移除能力也一样简单:从 prompt 里删掉工具描述,模型就"忘了"这个工具的存在。

甚至参数的行为也完全由你控制。AI 说"调用 calculator,a=42, b=7, multiply",但真正执行乘法的是你的代码。你想加日志、加权限检查、加参数校验,都可以——在 AI 看不到的地方做任何事。


三、代码实现

3.1 请求工具:request_tool()

打开 agent/agent.py,找到 request_tool() 方法:

def request_tool(self, user_input: str) -> Optional[dict]:
    """
    让模型请求工具调用。
    
    第五课版本。
    
    Args:
        user_input: 用户的请求
        
    Returns:
        工具调用规范,如果请求失败则返回 None
    """
    user_prompt = f"""你是一个工具调用助手。当被问到数学问题时,你必须只返回 JSON。

可用工具:calculator
- 参数:a (数字), b (数字), operation ("add"、"subtract"、"multiply" 或 "divide")

规则:
1. 只返回有效的 JSON
2. 不要任何解释,不要 Markdown
3. 直接以 {{ 开头,以 }} 结尾

示例格式:
{{"tool": "calculator", "arguments": {{"a": 42, "b": 7, "operation": "multiply"}}}}

用户请求:{user_input}

请返回 JSON:"""

    for attempt in range(3):
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": self.system_prompt},
                {"role": "user", "content": user_prompt},
            ],
            temperature=0.0,
        )

        text = response.choices[0].message.content
        parsed = extract_json_from_text(text)

        if parsed and "tool" in parsed and "arguments" in parsed:
            return parsed

    return None

这段代码的设计,每一步都有前几课的影子:

JSON 输出 + extract_json_from_text() —— 第三课的技能直接复用。

重试 3 次 —— 第三课和第四课都用过的老模式。LLM 有随机性,第一次格式错了不代表第二次也错。

验证关键字段 —— 第四课验证 decision in choices,这里验证 toolarguments 都存在。同样的工程原则:始终验证模型输出。

temperature=0.0 —— 工具调用需要精确的参数提取(42 不能变成 43,multiply 不能变成 add),零温度保证稳定性。

Prompt 里的 few-shot 示例 —— 注意 User Prompt 里那行示例:{"tool": "calculator", "arguments": {"a": 42, "b": 7, "operation": "multiply"}}。给模型看一个正确的输出样例,比纯文字描述有效得多。这个技巧在第三课的常见问题里提过,这里直接用上了。

User Prompt 放工具描述 —— 和第四课一样,工具列表是动态内容,放在 User Prompt 里而不是 System Prompt 里。System Prompt 保持角色设定稳定。

3.2 执行工具:execute_tool_call()

def execute_tool_call(self, tool_call: dict) -> Any:
    """
    执行模型请求的工具调用。
    
    Args:
        tool_call: 带 "tool" 和 "arguments" 的字典
        
    Returns:
        工具执行的结果
    """
    return execute_tool(tool_call["tool"], tool_call["arguments"])

看起来简单,但注意——请求和执行是两个独立的方法。这不是偷懒,而是有意为之。


四、请求与执行:为什么必须分开?

# 第一步:AI 负责请求
tool_call = agent.request_tool("What is 42 * 7?")

# 第二步:你负责执行
result = agent.execute_tool_call(tool_call)

request_tool() 做的事是纯"文字工作":理解用户意图 → 选择工具 → 提取参数 → 组装 JSON。全在 AI 的能力范围内。

execute_tool_call() 做的事是"真刀真枪":验证参数 → 调用函数 → 返回结果。这是你的代码负责的。

为什么要分开?两个原因:

安全性。 AI 永远无法绕过你的代码直接执行操作。它不能访问文件系统,不能发起网络请求,不能修改数据库——除非你的代码明确允许。AI 是一个请求者,不是一个执行者。

你可能觉得现在只有一个 calculator,分不分开无所谓。但想想后面——当 AI 能调用搜索、发邮件、操作数据库的时候,这个分离就是你的安全网。

可控性。 你可以在执行前做任何事:验证参数类型、检查权限、记录日志。不需要 AI 知道,也不需要 AI 同意。

这个分离在后续课程中会越来越重要。第六课加循环、第七课加记忆、第八课加规划之后,AI 的行为会变得非常复杂。但如果"请求"和"执行"的边界始终清晰,系统就不会失控。


五、运行示例

查看 complete_example.py 中的 lesson_05_tools() 方法:

from agent.agent import Agent

agent = Agent(model="qwen2.5:7b")

tool_call = agent.request_tool("What is 42 * 7?")
print(f"Tool request: {tool_call}")

if tool_call:
    result = agent.execute_tool_call(tool_call)
    print(f"Tool result: {result}")

运行效果:

Tool request: {"tool": "calculator", "arguments": {"a": 42, "b": 7, "operation": "multiply"}}
Tool result: 294

整个流程走一遍:

用户: What is 42 * 7?
  ↓
模型收到 prompt(包含 calculator 工具描述)
  ↓
模型输出: {"tool": "calculator", "arguments": {"a": 42, "b": 7, "operation": "multiply"}}
  ↓
代码验证: tool 存在 ✓,arguments 存在 ✓
  ↓
代码执行: multiply(42, 7) → 294
  ↓
用户收到: 294

注意一件事:模型根本没有做数学运算。 它不知道 42 × 7 等于多少。它只是识别出"这是一个计算需求",选了 calculator 工具,从输入中提取了数字和运算符。真正的计算由你的代码完成。

这就是"AI 描述意图,你控制执行"的具体体现。


六、工具的能力上限 = 你的代码能力上限

第五课有一个很容易被忽略但非常深刻的洞察:

模型不会做微积分?没关系,只要你的 calculator 支持微积分就行。
模型不懂 SQL?没关系,只要你的 database 工具能接收 SQL 查询就行。

模型是一个通用的"意图到接口"翻译器。它负责理解用户想做什么、选对工具、提取对参数。至于工具具体能做什么、做到什么程度——完全取决于你的代码实现。

你提供多少接口,AI 就有多少能力。不需要重新训练模型,只需要写代码、加接口。

这也是为什么本课的标题是"让 AI 调用工具"而不是"给 AI 添加能力"——能力一直是你的,AI 只是学会了请求使用它们


七、常见问题

Q:模型请求了一个不存在的工具怎么办?

A:在执行前根据可用工具列表验证工具名称。在 prompt 里清晰列出可用工具(就像代码里做的那样),能大幅减少这种情况。

Q:模型传的参数类型不对怎么办?

A:在 execute_tool_call() 里加参数校验。比如 calculator 期望数字,模型传了字符串,就报错并重试。另外,在工具描述里明确类型(a (number) 而不是 a (any))也能帮助模型输出正确格式。

Q:该用工具的时候模型直接回答了怎么办?

A:在 prompt 里加更强的约束,比如 “You MUST use the tool for math questions”。提供 few-shot 示例(代码里已经做了)也很有效。

Q:怎么添加新工具?

A:两步走:① 在代码里实现工具函数;② 在 prompt 的工具描述里添加名称和参数说明。模型会自动理解并使用。


八、下期预告

第六课:智能体循环——让 AI 持续思考和行动

前五课,AI 每次只做一件事:回答一个问题,做一个决策,调用一个工具。拿到结果就结束了。

但真实的智能体不是这样的。它应该能反复思考、反复行动——调用工具拿到结果,分析结果,决定下一步,再调用工具……直到任务完成。

下一课,我们把决策和工具调用放进一个循环里。这是 Agent 真正"活"起来的时刻。

敬请期待!


完整代码获取

本课涉及的完整代码包括:

  • request_tool() 方法——带验证和重试的工具请求系统
  • execute_tool_call() 方法——安全的工具执行层
  • calculator 工具的完整实现
  • 多种测试用例

完整代码获取,请参考 第一篇 最后


标签

#Python #AI Agent #LLM #工具调用 #Function Calling #Ollama #Qwen #大模型 #手搓Agent


本文为《手搓 AI Agent 从 0 到 1》系列教程第 5 课

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐