📌前置知识:已完成第一课至第四课
🎯本课目标:让 AI 不仅选择动作,还能指定参数,真正调用外部能力
💡核心概念:工具接口 / 结构化工具调用 / 请求与执行分离
前言
上节课,我们让 AI 学会了做决策。
现在它能分析用户的输入,从一组选项中选出最合适的动作。比如用户说"帮我总结这篇文章",AI 选了"summarize_text"。
能用。但你仔细想想——它只告诉了你"要做什么",没告诉你"怎么做"。
你的代码大概长这样:
decision=agent.decide("帮我总结这篇文章",choices=["answer","summarize","translate"])ifdecision=="summarize":summarize(???)# 总结什么?原文在哪?elifdecision=="translate":translate(???,???)# 翻译什么?翻译成什么语言?发现了吗?选项只是个名字,缺少关键信息:
- 没有参数。AI 说"要计算",但没说算什么、用哪个运算符。
- 没有细节。AI 说"要翻译",但没说翻译哪段文字、目标语言是什么。
如果 AI 能把这些细节也一起告诉你呢?不是只说"我要用计算器",而是说"用计算器,42 乘以 7"——工具名称 + 具体参数,一次说清楚。
这就是工具调用。
一、第四课还差什么?
回头看第四课做了什么:
decision=agent.decide("What is 42 * 7?",choices=["answer_question","calculate","translate"])# 输出: "calculate"AI 知道该用calculate——意图识别做对了。但接下来的事情就尴尬了:
ifdecision=="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()方法:
defrequest_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:"""forattemptinrange(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)ifparsedand"tool"inparsedand"arguments"inparsed:returnparsedreturnNone这段代码的设计,每一步都有前几课的影子:
JSON 输出 +extract_json_from_text()—— 第三课的技能直接复用。
重试 3 次—— 第三课和第四课都用过的老模式。LLM 有随机性,第一次格式错了不代表第二次也错。
验证关键字段—— 第四课验证decision in choices,这里验证tool和arguments都存在。同样的工程原则:始终验证模型输出。
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()
defexecute_tool_call(self,tool_call:dict)->Any:""" 执行模型请求的工具调用。 Args: tool_call: 带 "tool" 和 "arguments" 的字典 Returns: 工具执行的结果 """returnexecute_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()方法:
fromagent.agentimportAgent agent=Agent(model="qwen2.5:7b")tool_call=agent.request_tool("What is 42 * 7?")print(f"Tool request:{tool_call}")iftool_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 课