
Agent 30 课程开发指南写到第7课这个节点我必须放慢速度细讲。前面几课如果是在教模型“听懂人话”从第7课开始才真正教它“动手干活”。这一步跨过去Agent才从聊天窗口里的复读机变成一个能查数据、能写日历、能操作业务系统的执行者。这节课的内容适合两类人一类是跟过前面6课、正在做课程作业的学员另一类是已经会调大模型接口但一直没搞明白工具调用到底怎么落地的开发者。看完你应该能自己给Agent接上第一个外部工具并且知道出了问题时去哪里排查。1. 先看清楚第7课在整个课程里的位置1.1 Agent 30 课程路线回顾我做这套30天课程的初衷很简单市面上讲Agent概念的资料太多能一步步带着动手的太少。前6课我刻意没有碰工具调用因为基础不牢工具调用就是空中楼阁。到第7课学员已经掌握了大模型API的基本用法、提示词写作、上下文窗口管理、思维链拆解、结构化输出以及一个简单的记忆模块。我习惯把课程路线分成三段第7课刚好是第二段的第一站阶段对应课程核心目标基础期第1课 API接入与基本对话能发起一次模型请求基础期第2课 提示词工程能稳定拿到想要的回答格式基础期第3课 上下文窗口管理知道消息历史怎么组织、截断基础期第4课 思维链与任务拆解让模型学会把大问题拆成小步骤基础期第5课 结构化输出让模型输出可解析的JSON基础期第6课 记忆与状态让Agent能记住之前的对话内容工具期第7课 工具调用让Agent具备与外部世界交互的能力工具期第8课 检索增强与知识库让Agent调用大规模业务知识工具期第9课 多Agent协作让多个Agent之间分工配合应用期第10-30课 实战项目与部署把学到的能力放进真实场景从表格能看出来第7课卡在承上启下的位置。如果跳过这节课后面第8课要在问答机器人里接知识库第9课要几个Agent互相传消息学员会完全不知道怎么下手。1.2 没有工具的Agent本质上只是一个格式化聊天机器很多初学者会把“会用大模型”和“能干活”画等号这是个很大的误区。大模型本身只能做一件事根据输入文本预测下一段文本。你想让它帮忙发一封邮件它可以给你写出邮件正文但真正把邮件发出去的动作它做不了。你想让它查询今天库存是多少它如果没接入数据源只能凭记忆和概率编一个数字。工具调用的价值就是把模型从“嘴巴”变成“嘴巴加手”。模型负责理解用户意图、决定该调用哪个工具、生成调用参数外部程序负责真正执行再把执行结果喂回给模型模型基于真实结果生成最终回答。这个闭环一旦跑通Agent能做的事就远不止聊天了。我见过太多人第一步就做错了他们试图在提示词里写“当你需要查询时调用查询接口”然后等着大模型自己“神奇地”去调接口。大模型不会主动发起任何HTTP请求它能做的只是输出一段符合格式的JSON。真正的调用动作必须由你写的代码完成。第7课要训练的正是把这个闭环里每一环都焊死的能力。2. 工具调用的底层逻辑一次“请求—决策—执行—反馈”循环2.1 模型不是在执行函数而是在生成调用指令理解这个底层逻辑很重要。很多人一听到Function Calling就以为模型内部能执行代码其实不是。整个过程中模型只做一件它擅长的事根据对话历史和工具定义生成一段结构化的文本。这段文本里包含了它“想”调用的函数名和参数。至于这个函数到底怎么跑、跑完是什么结果模型一概不知。所以你可以把工具调用理解成这样一个场景你给一位只会出主意、不会动手的顾问配了一个秘书。顾问在纸上写出“请去档案室把张三的入职材料拿过来”秘书负责真的跑去档案室把材料拿回来放在顾问桌上顾问再根据材料继续给出判断。大模型就是那个顾问你的程序就是秘书工具就是档案室。这个认知能帮你避开90%的坑。比如当模型返回了一个工具调用但参数很奇怪你应该先检查工具描述是否足够清晰而不是去怀疑模型“笨”。模型只是在按它看到的说明书工作说明书写得不清楚它自然会犯错。2.2 一次工具调用拆解成四个阶段我在课程里习惯把这个过程拆成四个阶段学员照着这个理解写代码逻辑会顺很多。阶段一把用户消息和工具定义一起发送给模型。工具定义是一份JSON里面说明有哪些函数、每个函数接收什么参数。模型读到这里就相当于看到了“秘书的通讯录”。阶段二模型判断应该调用工具时不会直接返回最终答案而是在返回消息中带一个特殊标记标记里有函数名和参数列表。这个标记通常叫做tool_call。如果没有需要调用工具模型就直接返回普通回答流程结束。阶段三你的代码收到tool_call自己去执行对应的函数。注意这一步是大模型与外部世界唯一的连接点完全掌握在你手里。你可以执行数据查询、写文件、调接口但也可以决定拒绝执行并返回一个“没有权限”的结果。阶段四把函数执行结果作为一条新的消息追加到对话里再次请求模型。模型看到真实结果后会把它整理成自然语言回复给用户。如果结果不够模型可能继续发起下一轮工具调用这时候就形成一个循环直到它认为信息足够为止。我在课程里让学生把上面四步画在一张纸上再对照代码去写。如果你现在看到这里有点懵问题不大下一章我会带你把每个阶段用代码走一遍。2.3 工具描述写得越具体调用成功率越高工具调用的成功率很大程度上不取决于模型能力而取决于你写的函数描述。同一个函数描述写得好不好效果能差出一大截。举一个反面例子。你定义一个查询待办事项的函数描述却只写了“查询待办”。模型接到用户“帮我把周五的任务列出来”时它不知道该传星期几不知道该过滤已完成还是未完成最后只能猜而猜的参数经常是错的。我一般推荐工具描述按照这个模板来写这个工具负责什么事情 在什么场景下适合使用 有哪些需要注意的参数规则。以待办查询为例可以写成“查询指定日期的待办事项列表可按完成状态过滤。当用户提到日程、计划、任务时优先使用”。参数描述也要写清楚日期格式是“YYYY-MM-DD”状态字段可选的取值是什么。模型是按照描述做选择的你把描述写到能直接“照着做”的程度调用成功率会明显提升。3. 第7课实操给Agent接上第一个待办查询工具3.1 确定课程交付目标与最小闭环第7课的作业我安排得比较克制不要求学生做那种功能花哨的机器人只要求完成一个最小闭环用户询问某天的待办事项Agent调用一个外部函数拿到真实数据再基于真实数据回答用户。这个闭环虽然简单但包含工具调用的全部关键环节。先准备好运行环境。我建议直接用Python语言本身不挑SDK市面上主流的大模型服务商基本都提供工具调用参数写法大同小异。下面示例中的llmsdk是为了避免绑定某个具体服务商而写的占位客户端你实际用时换成自己日常使用的SDK即可结构和字段基本都能对应上。import json # 这是你需要跟着SDK的说明替换的客户端初始化 from llmsdk import LLMClient client LLMClient( provider你的服务商, model当前使用的模型, api_key你的密钥, )3.2 第一步写一个真正可执行的函数工具调用的前提是这个函数必须真实存在、能跑、有副作用。我们的待办查询函数先从一个固定数据源里取数以后可以替换成数据库或接口。def get_todo_items(date: str, status: str ) - str: # 模拟数据真实场景可换成数据库查询 todo_db { 2025-01-15: [ {id: 1, title: 写第7课讲义, status: done}, {id: 2, title: 完成工具调用demo, status: pending}, ] } items todo_db.get(date, []) if status: items [item for item in items if item[status] status] if not items: return 当天没有符合条件的待办事项 lines [f- [{item[id]}] {item[title]} ({item[status]}) for item in items] return \n.join(lines)代码非常简单我甚至推荐你在课程演示时故意把模拟数据做得少一点这样模型很快会碰到“查不到数据”的情况能顺便观察它如何应对空结果。很多Agent会在拿到空结果后继续编数据这是课程里非常好的讨论点。3.3 第二步把函数“翻译”成模型能读懂的JSON Schema模型不知道你Python函数长什么样它只能读JSON格式的函数描述。这里的关键是把参数结构、类型、枚举值、是否必填都写清楚。tools [ { type: function, function: { name: get_todo_items, description: 查询指定日期的待办事项列表可按完成状态过滤。当用户提到日程、计划、任务时优先使用。, parameters: { type: object, properties: { date: { type: string, description: 日期格式为 YYYY-MM-DD, }, status: { type: string, enum: [pending, done, ], description: 按状态过滤空字符串表示不过滤, }, }, required: [date], additionalProperties: False, }, }, } ]注意这里的additionalProperties我刻意设成了False。如果不写这个字段模型有可能自己脑补一个并不存在的参数比如“负责人”或“优先级”然后你的程序拿不到这个参数就会报错。把额外属性关掉相当于告诉模型你只能按我规定的字段来别自由发挥。还要注意枚举值。状态字段我只允许“pending”、“done”和空字符串模型就不会随机生成一个“进行中”这种模糊值。减少变量就是减少排查成本。3.4 第三步组装对话并处理模型返回的tool_call接下来是核心循环。我们的逻辑是把消息和工具定义发给模型检查返回结果是不是工具调用标记。如果是就执行对应函数拿到结果后追加到对话里然后带着追加后的消息再次请求模型直到模型不再要求调用工具。MAX_TOOL_ROUNDS 5 def call_tool(name: str, args: dict) - str: if name get_todo_items: return get_todo_items(**args) return 未知工具 messages [ {role: user, content: 帮我看看今天有什么待办} ] for _ in range(MAX_TOOL_ROUNDS): resp client.chat.completions.create( model你的模型名, messagesmessages, toolstools, ) msg resp.choices[0].message reason resp.choices[0].finish_reason # 模型不再要求调用工具直接输出最终答复 if reason ! tool_calls: print(最终答复:, msg.content) break # 保留模型返回的消息包含tool_call标记 messages.append(msg.model_dump()) # 遍历所有工具调用 for call in msg.tool_calls: func_name call.function.name try: args json.loads(call.function.arguments) except json.JSONDecodeError as e: print(参数解析失败:, e) continue print(调用工具:, func_name, 参数:, args) result call_tool(func_name, args) # 工具执行结果以tool角色回填到对话里 messages.append({ role: tool, tool_call_id: call.id, content: result, })这里最容易被初学者忽略的是那一行操作把模型返回的原始消息完整追加进messages。因为这条消息里包含了tool_call的id和原始参数是工具结果回填时对得上号的关键。如果漏掉这一步模型看到工具结果时会不知道这个结果回答的是哪个调用表现就会变得很怪。另一个技巧是轮次上限。我在课程里把MAX_TOOL_ROUNDS设为5给模型足够的“多轮工具调用”空间也避免它陷入调用工具的循环。你可以根据业务场景调整但一定不能不做限制。3.5 第四步观察一次真实的调用链路如果一切正常你会在控制台看到类似下面的输出调用工具: get_todo_items 参数: {date: 2025-01-15, status: } 最终答复: 2025年1月15日共有2条待办事项其中一项“写第7课讲义”已经完成另一项“完成工具调用demo”还没有完成。注意整个过程中模型并没有自己看到待办数据它是在拿到工具返回的内容之后用自然语言组织出来的答案。这就引出一个课堂重点模型会不会篡改结果如果数据源里明明有3条待办模型却只总结了2条怎么办解决办法有两个方向一是要求工具结果里带一条总数字段二是把问题拆细让模型把结果原样罗列而不是自由发挥。我在第5课结构化输出里教的JSON模式在这里可以配合使用。4. Agent工具调用实战中的高频问题排查4.1 模型反复调用同一个工具死循环出不来这是学员问我最多的问题。现象是模型一直在调用get_todo_items每次都传回相同参数最后把请求次数耗完。原因通常有三个一是工具返回的结果里没有模型需要的确认信息它觉得“没查到再查一次”二是工具结果格式太乱模型没看清楚三是上面的轮次限制没加导致无限循环。我的排查顺序一般是先看工具返回的字符串是不是被截断了或者是不是返回了空结果。如果是空结果要在返回内容里写清楚“未找到请检查日期或改问其他日期”给模型一个明确的下一步指令。同时无论如何都要加上MAX_TOOL_ROUNDS限制这是兜底。如果模型连续调用同一个工具超过3次我会怀疑问题出在工具描述与用户问题不匹配而不是模型“笨”。4.2 模型生成的参数不合规范导致解析失败参数解析失败的现象有两种一种是模型生成了残缺的JSON另一种是生成了没有定义的字段。残缺JSON大多出现在模型非常不确定用户意图的时候。此时我建议通过丰富description来引导比如明确说“用户只提供了今天就用今天日期”。也可以要求模型在拿不准时先反问用户而不是强行调用。至于多出来的字段前面我们把additionalProperties设成False已经能挡住一部分。但总会有模型不遵守的情况所以程序里必须做严格的参数校验。我习惯在call_tool里加一层白名单校验先检查函数名是否在允许列表里再检查传入的参数是否和定义匹配不匹配就返回错误信息给模型。让模型看到错误后自行修正这是一种很实用的人机协作方式。4.3 工具结果太长把上下文窗口撑爆如果你的工具要返回一批明细数据动辄几十上百条将它们全部塞给模型不仅浪费token还会让模型抓不住重点。我的习惯是先对工具结果做摘要只返回关键信息总量和若干个代表样本。比如待办查询如果查出了200条事项返回字符串可以写成“共200条其中未完成130条已完成70条。最近截止的是开会、写周报、修Bug”。这样模型既有足够信息回答用户又不会迷失在细节里。如果你的场景必须让模型看到完整数据那就需要配合第3课讲的上下文窗口管理做分页或切片处理。4.4 工具权限与安全边界一定要提前设计工具调用把外部世界交给了Agent权限设计必须跑在功能开发前面。我在课程里给学生立了几个规矩第一Agent能调用的函数必须有一个白名单绝不能让它任意执行系统命令或访问敏感文件。第二写入、删除、转账、发消息这类有实际影响的动作必须加人工确认环节也就是所谓的human-in-the-loop界面里弹一个确认框再执行。第三工具返回的数据如果包含用户隐私要提前脱敏不许把完整的手机号、地址交给模型。危险操作安全替代做法允许Agent直接执行Shell命令只开放白名单命令并且每次执行要记录日志允许Agent删除数据库记录改成“标记删除”或先进入回收站允许Agent读取环境变量中的密钥禁止工具函数访问密钥密钥只留在客户端允许Agent代表用户发邮件先生成草稿由用户手动点击发送这些安全边界讲起来不刺激但真实项目里出事故基本都是在这些地方松懈。第7课我希望学员从第一天就养成“工具最小权限”的思维习惯后面做复杂项目会省掉大量麻烦。5. 从第7课走出去面试官常问的多工具与进阶能力5.1 多个工具组合起来才是完整的Agent如果你只给Agent一个工具它能做的事非常有限。真正有价值的Agent通常要同时掌握查询工具、写入工具、搜索工具、计算工具甚至呼叫外部API。课程里我常用一个会议安排场景来演示多工具协同。用户说“帮我约明天下午3点的会议室顺便看看天气要不要带伞”。Agent内部可能会这样走先调用日历工具查询明天下午3点是否有空如果空了就调用会议室预订工具去占位再调用天气工具查明天降水概率最后把所有结果整理成一句答复。这个过程中每个工具执行结果都会作为消息反馈给模型模型再决定下一步调用哪个工具。我让学生特别注意工具调用的顺序有些工具必须先查再写顺序错了数据就脏了。5.2 并行工具调用与工具数量控制现在不少主流模型的接口都支持一次返回多个tool_call也就是说模型觉得需要同时查日历、查天气、查会议室列表的时候会一次性输出三个调用。你的循环代码需要遍历所有tool_call再统一回填结果。这对性能很好但也意味着你更容易遭遇“工具爆炸”。工具数量越多模型选择错误的概率就越高。我个人的经验是单个Agent的工具数量最好控制在10个以内超过10个就应该考虑拆分成多个Agent每个Agent只负责一个子域。这正好是第9课“多Agent协作”要解决的问题。另外不要把功能相似的多个工具同时挂给Agent比如已经有了“查询天气”就不要再加“查询明天天气”合并成一个带日期参数的工具就好。工具多而全不如少而精。5.3 课后作业与进阶路线第7课作业要求每个学员把自己的待办查询工具扩展成“至少一个查询类工具加一个写入类工具”写入类工具必须带人工确认。做完这个作业工具调用闭环就基本吃透了。学有余力的人我建议继续往下走三件事第一把工具调用日志记录下来至少记录函数名、参数、耗时、返回摘要以后排查问题能救命第二尝试把一次查询的结果缓存起来重复问题直接命中缓存省token也降延迟第三试着做一个HTTP请求工具让Agent能去查一个真实的外部接口这一步会直接引出第8课的知识库检索。我做了这么多Agent项目最大的感受是决定一个Agent上限的往往不是模型本身有多聪明而是你给了它多少高质量的手。第7课就是在打磨这些手。工具描述里动词开头、参数名用下划线、枚举值写满、返回结果精简这几个习惯我从第一天就要求自己养成现在写复杂Agent也依然受用。你把这节课练熟后面进入知识库和多Agent协作时会明显感觉路好走很多。