【手搓 AI Agent 从 0 到 1】第五课:让 AI 调用工具
前置知识已完成第一课至第四课本课目标让 AI 不仅选择动作还能指定参数真正调用外部能力核心概念工具接口 / 结构化工具调用 / 请求与执行分离前言上节课我们让 AI 学会了做决策。现在它能分析用户的输入从一组选项中选出最合适的动作。比如用户说帮我总结这篇文章AI 选了summarize_text。能用。但你仔细想想——它只告诉了你要做什么没告诉你怎么做。你的代码大概长这样decisionagent.decide(帮我总结这篇文章,choices[answer,summarize,translate])ifdecisionsummarize:summarize(???)# 总结什么原文在哪elifdecisiontranslate:translate(???,???)# 翻译什么翻译成什么语言发现了吗选项只是个名字缺少关键信息没有参数。AI 说要计算但没说算什么、用哪个运算符。没有细节。AI 说要翻译但没说翻译哪段文字、目标语言是什么。如果 AI 能把这些细节也一起告诉你呢不是只说我要用计算器而是说用计算器42 乘以 7——工具名称 具体参数一次说清楚。这就是工具调用。一、第四课还差什么回头看第四课做了什么decisionagent.decide(What is 42 * 7?,choices[answer_question,calculate,translate])# 输出: calculateAI 知道该用calculate——意图识别做对了。但接下来的事情就尴尬了ifdecisioncalculate:calculate(???)# 算什么算 42×7但 AI 没告诉你AI 只告诉你要计算但具体算什么、用什么运算符它没说。这是因为第四课的决策输出只有动作名称没有参数。你的代码拿到了calculate之后还得自己想办法从用户输入里解析出数字和运算符——又回到了第三课之前的老问题用规则去解析自然语言。第五课要解决这个问题让 AI 在选择工具的同时把参数也一并提取出来。用户: What is 42 * 7? ↓ AI 输出: { tool: calculator, arguments: { a: 42, b: 7, operation: multiply } }工具名称有了参数也有了你的代码可以直接执行不需要再自己解析用户输入。对比一下第四课第五课AI 告诉你什么“用 calculate”“用 calculatora42, b7, 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 说调用 calculatora42, b7, 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_promptf你是一个工具调用助手。当被问到数学问题时你必须只返回 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}请返回 JSONforattemptinrange(3):responseself.client.chat.completions.create(modelself.model,messages[{role:system,content:self.system_prompt},{role:user,content:user_prompt},],temperature0.0,)textresponse.choices[0].message.content parsedextract_json_from_text(text)ifparsedandtoolinparsedandargumentsinparsed:returnparsedreturnNone这段代码的设计每一步都有前几课的影子JSON 输出 extract_json_from_text()—— 第三课的技能直接复用。重试 3 次—— 第三课和第四课都用过的老模式。LLM 有随机性第一次格式错了不代表第二次也错。验证关键字段—— 第四课验证decision in choices这里验证tool和arguments都存在。同样的工程原则始终验证模型输出。temperature0.0—— 工具调用需要精确的参数提取42 不能变成 43multiply 不能变成 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_callagent.request_tool(What is 42 * 7?)# 第二步你负责执行resultagent.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 agentAgent(modelqwen2.5:7b)tool_callagent.request_tool(What is 42 * 7?)print(fTool request:{tool_call})iftool_call:resultagent.execute_tool_call(tool_call)print(fTool 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 课