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="你的模型名", messages=messages, tools=tools, ) 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协作时,会明显感觉路好走很多。