资讯详情 大模型工具使用与函数调用:从文本生成到动手执行
📅 2026/10/20 5:00:54
最近我在整理某高校AI系统课程CS329Z的第四讲资料时把“工具使用与函数调用”这个主题从头到尾重新捋了一遍。说实话这一讲在我看来是整个AI应用开发里最容易被低估的章节——很多人觉得函数调用不就是让模型输出一段JSON吗但真正落地过的人都知道从“模型会输出参数”到“系统稳定地帮用户把事办了”中间隔着一整条工程链路。这一讲的核心命题是大语言模型如何从“只会生成文本”变成“真的能执行操作”。比如用户说“帮我把某城市明天的天气整理成出行建议”模型不能只靠训练数据里的常识硬编它需要知道该调用哪个天气接口、需要生成什么参数、拿到返回结果后该怎么组织语言。这就是工具使用与函数调用的价值——把模型的能力边界从“内部知识”扩展到“外部世界”。适合正在做AI应用、Agent系统或者准备入门大模型应用开发的读者这一讲值得反复看。为什么大模型需要“工具使用”——先从边界说起1.1 语言模型天生缺的三样东西我把这讲反复看了两遍发现整讲都在回答一个朴素问题模型什么都能聊为什么做不了实事根源在于三点。第一知识有截止时间。模型训练时用的语料是过去的你问它最新资讯、实时股价、明天的天气它只能根据历史规律“编一个像样的答案”这在很多场景下是不可接受的。第二精确计算不擅长。它本质上是在做概率预测不是真的在按数学规则演算算个复杂点儿的表达式就容易出错。第三它碰不到外部系统。就算模型知道该查数据库、该发请求、该调内部服务它也没有“手”去操作。我习惯用一个类比把大模型想成一个知识渊博但被绑在椅子上、没手机也没网络的专家。你问他任何知识性问题他都能答得头头是道但你说“帮我把这份报表发出去”他就只能干瞪眼。工具使用就是给这位专家松绑给他递手机、递电脑、递各种能实操的“外挂”。1.2 从“直接回答”到“动手执行”的范式变化早期大家解决这个问题的方式很笨把外部信息直接塞进提示词比如搜到的资料、查到的结果让模型基于这些内容回答。这种方式叫检索增强但它是一次性的、单向的模型没有“按需再查”的能力。后来出现了插件机制模型可以触发特定插件。但插件体系往往比较封闭新增一个插件就要做大量适配。函数调用不太一样——它不关心工具内部是怎么实现的只约定一个输入输出接口。模型说“我要调用get_weather参数city某城市”应用层去执行这个函数把结果拿回来。这就像给专家一本工具箱目录他看目录就知道哪个工具能干什么、需要什么参数具体工具内部是什么原理不用他操心。这种范式转移的核心变化是从“模型尽力猜答案”变成“模型决定找谁帮忙”。猜答案会错但调用正确工具之后拿到的真实结果基本不会错。1.3 第四讲在整个课程里的位置CS329Z这门课前面几讲讲了基础模型能力、上下文工程什么的第四讲直接进入“行动层”。我的理解是前面的内容都在讲模型怎么理解输入、怎么生成输出这一讲开始让模型“动手做事情”。在整个智能体系统里工具调用就是行动层的基础设施——没有它后面讲的规划、多步推理都无从落地因为规划完了你得能执行才行。这一讲的内容不深但实操密度很高。它默认你懂基本的API调用也默认你写过Python但不会要求你有深度学习背景。跟着走一遍基本能理解“一个带工具的Agent是怎么跑起来的”。函数调用的原理拆解一次调用背后发生了什么2.1 工具Schema本质上是一份“使用说明书”函数调用最关键的概念不是“函数”而是“工具描述”也就是Schema。每个工具要用一段结构化描述说清楚三件事这个工具叫什么、它是干什么的、调用它需要什么参数。我在这讲里看到最核心的示例就是一个天气查询工具的Schema简化后长这样{ name: get_weather, description: 查询某城市在指定日期的天气情况返回温度、降水概率、风力等, parameters: { type: object, properties: { city: { type: string, description: 城市名称如北京、上海 }, date: { type: string, description: 日期格式为YYYY-MM-DD } }, required: [city, date] } }你可能觉得这没啥技术含量但实际用起来这段描述的质量直接决定模型调用得准不准。我见过一个经典的坏例子某个工具描述写得特别含糊只写了一句“查询信息”结果模型什么鸡毛蒜皮的小问题都调它。描述写得越具体模型越能判断“什么时候该用、什么时候不该用”。这就像你给一个实习生安排任务你说“帮我处理一下数据”他肯定一脸懵你说“把这份Excel里销售额大于一万的订单筛选出来按日期排序输出”他就知道怎么干了。2.2 一次函数调用的完整旅程这一讲把整个调用流程拆成了五个环节我在项目里也是这么走的第一步用户输入自然语言请求。第二步应用层把可用的工具列表以结构化形式放进请求里模型看到“有哪些工具可用、各自什么能力”。第三步模型内部判断这个问题需要调用工具吗如果需要它会输出一个结构化的调用请求比如“调用get_weathercity某城市date明天”。注意到这一步模型并没有真的执行任何东西它只是“提出调用申请”。第四步应用层接收到这个结构化请求解析参数真正去执行对应的函数拿到真实返回值。第五步把返回值拼回对话上下文再次交给模型模型基于真实结果生成面向用户的最终回答。这个流程里最反直觉的地方是模型不是在“一步”里完成回答的而是至少经历了两次生成。第一次生成决定“要不要调用、调用哪个、参数是什么”第二次生成是基于调用结果的“收尾回答”。很多刚接触函数调用的人容易在这里犯迷糊——以为模型自己就把工具结果算出来了其实模型只负责“说”真正干活的是你的应用代码。2.3 训练侧和推理侧是怎么支撑这件事的有人可能会问凭什么模型就能输出符合格式的调用请求这讲也解释了这是训练时喂了大量“带工具调用的对话样例”的结果。训练数据里包含这样的模式用户提问→模型输出工具调用指令→系统返回工具结果→模型基于结果回答。模型见得多了自然就学会“遇到这类问题应该输出这种格式的调用”。推理侧也有约束。为了尽量保证模型输出的调用请求是合法JSON应用层通常会在解析时加容错甚至会做格式校验、参数校验。这也是函数调用比让模型“自由发挥”更可靠的原因——它不是靠模型自觉而是靠协议约束。2.4 三种工具调用范式怎么选这讲对比了几种不同做法我整理成一个表范式优点缺点适用场景函数调用结构稳定、参数明确、易于校验工具定义需要提前写好生产环境、复杂业务系统JSON模式生成灵活不需要预定义工具校验弱、容易输出无关字段快速原型、自由格式输出代码执行能力上限最高、能做任意计算安全风险大、资源管理复杂数据分析、科学计算、需要高自由度场景我的经验是能用函数调用解决的就别用自由JSON能用JSON就别直接让模型写代码执行。每多一层自由就多一层不可控这在生产环境里是致命的。一个最小可运行的“工具型Agent”实现3.1 准备三个有代表性的工具纸上谈兵没意思我照着这讲的思路用Python写了一个最小可运行的Demo。工具集选了三个有代表性的天气查询、精确计算、数据库查询。为什么选这三个因为它们分别对应模型最不擅长的三件事时效性信息、精确运算、访问私有系统。# 工具1天气查询模拟 def get_weather(city: str, date: str) - str: 模拟查询天气 data { (某城市, 2025-06-01): 晴28°C降水概率10%, (某城市, 2025-06-02): 多云25°C降水概率30%, } return data.get((city, date), 暂无数据) # 工具2精确计算 def calculator(expression: str) - str: 执行数学表达式计算 import ast try: # 仅允许最基本的表达式避免任意代码执行 tree ast.parse(expression, modeeval) return str(eval(compile(tree, , eval), {__builtins__: {}}, {})) except Exception as e: return f计算错误: {e} # 工具3数据库查询模拟只读 def query_database(sql: str) - str: 执行只读SQL查询返回结果集摘要 # 实际项目中这里应连接真实数据库并确保账号只读、超时控制 # 这里仅作模拟 sample { SELECT COUNT(*) FROM orders: 128393, SELECT * FROM orders LIMIT 1: [{order_id: 1, amount: 99.9, status: paid}], } return sample.get(sql.strip(), 查询无结果请检查表名和语法)每个工具都用自己的函数实现了并且我刻意在注释里标出了安全注意事项比如计算器只允许eval模式其实还不够安全真实生产应该用沙箱。这讲里也强调了工具能不能安全执行是工程侧的责任不能指望模型自觉。3.2 构造工具描述并发送给模型接下来把这些工具转换成前面说的Schema格式然后用一个抽象接口模拟请求模型。我故意没用具体的模型厂商SDK是为了让你专注理解机制本身tools [ { name: get_weather, description: 查询某城市指定日期的天气情况, parameters: { type: object, properties: { city: {type: string, description: 城市名称}, date: {type: string, description: 日期YYYY-MM-DD} }, required: [city, date] } }, { name: calculator, description: 执行数学表达式计算输入一个表达式字符串, parameters: { type: object, properties: { expression: {type: string, description: 数学表达式如 (1.09**12)} }, required: [expression] } }, { name: query_database, description: 执行只读SQL查询, parameters: { type: object, properties: { sql: {type: string, description: 只读SQL语句} }, required: [sql] } } ] def simulate_llm_with_tools(messages, tools): 模拟一个支持工具调用的LLM响应 # 在真实项目中这里调用模型API并把tools传入 # 为了演示我们用规则模拟模型决定调用哪个工具 user_request messages[-1][content] if 天气 in user_request: return { role: assistant, content: None, tool_calls: [ { name: get_weather, arguments: {city: 某城市, date: 2025-06-01} } ] } if 计算 in user_request or 多少 in user_request: return { role: assistant, content: None, tool_calls: [ { name: calculator, arguments: {expression: 1.09 ** 12} } ] } if 订单 in user_request or 数据库 in user_request: return { role: assistant, content: None, tool_calls: [ { name: query_database, arguments: {sql: SELECT COUNT(*) FROM orders} } ] } return {role: assistant, content: 这个问题不需要调用工具我可以直接回答。}这段代码的核心是模型输出中有个tool_calls字段里面是它想要调用的工具和参数。真实模型也是这么设计的只是内部决策基于概率而非规则。为了让你跑通理解我用规则代替了模型判断。3.3 编排主循环识别、执行、回填工具型Agent的核心是一个循环拿到模型输出如果有工具调用就执行把结果回填再交给模型。import json def run_agent(user_query): messages [{role: user, content: user_query}] max_rounds 3 for _ in range(max_rounds): # 第一次请求带工具列表 response simulate_llm_with_tools(messages, tools) # 情形一模型不再调用工具直接回答 if not response.get(tool_calls): return response[content] # 情形二模型请求调用工具 tool_results [] for call in response[tool_calls]: tool_name call[name] args call[arguments] if tool_name get_weather: result get_weather(args[city], args[date]) elif tool_name calculator: result calculator(args[expression]) elif tool_name query_database: result query_database(args[sql]) else: result f未知工具: {tool_name} # 把工具调用和结果都回填到消息里 messages.append({ role: assistant, content: None, tool_calls: [call] }) messages.append({ role: tool, tool_call_id: tool_name, content: str(result) }) tool_results.append(result) # 再次进入循环让模型基于工具结果生成回答或继续调用 messages.append({role: user, content: 请基于工具返回结果继续回答}) return 已达到最大调用轮数停止。 print(run_agent(帮我查一下某城市2025年6月1日的天气)) print(run_agent(计算1.09的12次方等于多少))跑一下这几行代码就能看到完整链路。这个Demo麻雀虽小五脏俱全有工具定义、有模型决策、有执行、有回填。真实生产里要替换的只是simulate_llm_with_tools这一部分——换成真正的模型API调用其他框架思路完全复用。3.4 工程侧的几个关键参数这讲末尾提了三个工程参数我在实际项目里也踩过展开说一下。第一是轮数上限。模型可能陷入循环比如工具返回错误后它反复重试所以必须设置最大调用轮数一般是3-5轮超过就终止。第二是token预算。工具返回结果可能很大比如数据库查询返回几千行直接拼进上下文马上超限。要做截断、摘要只回填前N行。第三是超时控制。单个工具执行可能很慢尤其外部API要给每个工具设置超时整体也要有总超时避免用户等半天没响应。三种高频实战场景拆解4.1 实时信息查询内建知识靠不住的地方第一个场景最典型天气查询。模型训练时不可能知道明天的天气所以必须走工具。实际操作中你会发现一个有意思的现象模型即使有工具可用偶尔也会尝试“直接回答”。比如你问它“某城市今天气温多少”它可能直接说“今天某城市气温约28度”而不会先调工具。这是因为训练数据的惯性让它觉得“自己知道答案”。我在项目里解决这个问题有几个手段一是在系统提示词里明确“涉及实时信息必须调用工具禁止凭记忆回答”二是如果工具返回空或失败宁可告诉用户获取不到也不要让模型编一个数字三是给工具描述加“时效性声明”例如“本工具返回最新实时数据”。这讲里强调的核心观点我特别认同对于事实性问题宁可有条件地拒绝也不要让模型自信地胡编。因为用户一旦发现数据是假的整个系统的信任就崩了。4.2 精确计算与代码执行器别让模型当计算器第二个场景是精确计算。看起来很简单但这是模型最典型的短板。我之前做过一个数学演示Demo让模型直接回答“1.09的12次方”它经常会算错有时候答案是3.09有时候是3.08误差不大但就是不准。更麻烦的是它自己算错的时候还特别自信。正确做法是给模型配一个计算器工具或者更进一步配一个代码执行器。模型负责把问题转成代码比如1.09**12再调用执行器算出来结果精确且可复核。这讲对这种场景的建议是能用工具就别让模型硬算因为模型的价值在于理解和推理不在于数值精确。不过代码执行器引入的安全风险也大——模型生成的代码不能直接放开跑必须有沙箱、资源隔离、执行时间限制。如果只是简单运算用我之前写的calculator那样做表达式白名单校验就够了。4.3 数据库查询与报表生成NL2SQL的完整闭环第三个场景是数据库查询。这是我认为函数调用最核心的生产级应用之一把用户的自然语言问题转成SQL执行后返回结果再生成报表。我在某个内部数据分析项目里就是这么做的流程和这一讲几乎完全一致。用户问“上个月退货率最高的三个产品是什么”模型会先判断需要查数据库生成一条SQL比如SELECT product_name, return_rate FROM monthly_returns WHERE month 2025-05 ORDER BY return_rate DESC LIMIT 3应用层执行这条SQL把结果回填模型再根据结果用自然语言回答甚至生成一个表格。但这里有几个必须做的工程约束。第一数据库账号必须是只读的绝不能让模型生成的SQL能改数据。第二所有查询必须在事务里跑加上超时防止模型生成了全表扫描这种恐怖查询。第三对敏感字段要脱敏比如用户手机号、身份证号模型能看到数据不代表应该看到全部数据。第四最好限制模型只能查特定的库和表这可以在Schema层面就约束清楚。高频踩坑与排查技巧实录5.1 问题一模型输出的JSON不合法这是我用函数调用以来遇到最频繁的问题。明明要求输出JSON结果可能多了个逗号、注释没删干净、字符串引号错误。我的排查三步法先看是不是模型版本的问题有些模型对复杂工具定义格式支持不好再看是不是上下文示范太少在提示词里加一个正确示例往往见效最快最后兜底方案是在代码里做容错解析比如用宽松的解析逻辑把代码块先剥离出来再转JSON。一个小技巧给模型的工具示例不要太多每个工具最多一两个因为示例太多会挤占上下文空间反而增加输出混乱概率。5.2 问题二模型陷入工具调用死循环我遇到过一个经典场景模型调用了一个工具工具返回“参数错误”模型看懂了又原样调用了一次还是“参数错误”继续调用一直循环到超时。原因是模型不知道“同样的错误应该换个方式处理”。解决手段有三个设置最大轮数、对完全相同的调用做去重、连续两次工具执行失败就强制终止并让模型基于已有信息回答。核心原则是工具调用必须有个“刹车机制”。5.3 问题三参数幻觉编造不存在的字段模型在调用工具时可能生成不存在的参数。比如数据库工具明明只有orders表它却生成了SELECT * FROM users看起来像模像样实际一执行就报错。解决思路是在工具Schema里尽量用枚举约束比如在描述里写明“可用表orders, products, customers”在应用层做严格参数校验当工具返回“表不存在”时把可选表清单带回去让模型基于真实信息重新生成。这一步非常关键模型不擅长凭空猜出你系统里有什么但很擅长在你给它清单之后做出正确选择。5.4 问题四上下文被工具结果撑爆工具返回的数据量通常远超你想象。比如数据库查询模型生成了一条不带LIMIT的SQL结果返回10万行直接塞进上下文第二次模型请求大概率直接超过token上限。方案是统一的回填截断策略默认只回填前10条或20条并附上“如需更多数据请再次查询”。还可以让模型自己控制粒度在工具描述里要求它必须写LIMIT否则工具就报错。让工具自己有防御性比事后截断更可靠。5.5 常见问题速查表症状原因解法JSON解析失败格式不规范、转义错误容错解析、提示词加示例反复调用同一工具模型陷入测试循环轮数上限、相同调用去重调用成功但结果不对参数幻觉、字段名错误Schema枚举约束、返回字段清单请求超时工具结果过大截断回填、强制LIMIT模型直接回答不调工具系统指令不明确强调实时/计算类问题必须调用工具5.6 安全边界工具能调用不代表能随便调用这讲虽然没有大篇幅谈安全但我在实际项目中体会很深。函数调用相当于把“手”交给了模型这个手能摸到哪些东西完全由你控制。最低要求是权限最小化数据库只读、外部API白名单、删除操作一律禁止。更严格一点敏感操作要加人工审批流程。我在某个项目里就做过一个“发邮件工具”工具本身只负责生成邮件草稿真正的发送动作必须人工确认。这个设计牺牲了一点自动化程度但换来了非常高的安全边际。另外工具返回的外部内容一律视为不可信数据直接拼接给用户前要做清洗防止意外注入。最后我对函数调用方向的实际体会把这一讲的笔记整理完之后我最大的感触是工具使用能力决定了整个Agent系统的上限。模型本身的水平是底座但底座再强如果工具层定义混乱、调用过程不稳最后效果一定拉胯。很多开发者在做Agent时70%的badcase其实出在工具定义不清晰——描述模棱两可、参数约束缺失、返回格式混乱。把这些工程细节打磨好效果往往比换一个更大的模型还要明显。另外我特别想提醒的是做工具调用之前先把可观测性做好。我在某项目里把工具调用链路全量打了日志每个工具入参、出参、耗时都记录在案后面定位问题简直是救命级的功能。模型这种非确定性系统你没法靠“看代码猜行为”必须靠日志回放。函数调用正在成为AI应用的一种标准基础设施它不像大模型训练那么玄学也不像前端交互那么琐碎更像是在“给模型建造可靠的双手”。如果你正在规划自己的智能体应用我建议把这一讲的机制彻底搞透——它值得你花一个下午亲手跑一遍再花很长时间在真实场景里打磨。