大模型 Function Calling 完全指南:让 AI 真正能干活

做一个 AI 助手时撞上一件让我后背发凉的事:同事问它"我上个月订单金额是多少",它非常自信地答"¥3,280",可真实数据库里是 ¥0,那个数字是模型凭空编出来的 —— 编得有零有整语气笃定像真查过一样。我换问法问天气编天气问库存编库存,反复打磨"请不要编造"的提示词也压不住。卡了很久才想明白最朴素的事:我在求一个只会生成文字的模型,去做一件需要真的去查、去算、去调用的事,就像反复叮嘱一个锁在空房间里的人如实告诉我外面下不下雨 —— 他再诚实也只能猜,问题不在他诚不诚实,在我没给他一扇窗、也没给他一部能打电话问的电话。模型缺的不是更好的提示词,是一种能真的调用外部能力的机制,这就是 Function Calling。梳理:大模型本质是文字接龙引擎,给一段文字预测下一个最可能的词接上去再预测,能力锁死在生成文字里,由此两个硬边界 —— 不知道训练之后和接触不到的事(今天天气、你的订单、此刻汇率)、不能执行动作(查库、调 API、下单),最要命的是撞到边界时它不沉默而是编,问它无从知道的事会忠实履行接龙本能生成一个最像样的答案,这就是幻觉,靠 prompt 求它别编治标不治本因为它真的没有那个数据。Function Calling 最大的误解是模型会自己执行函数 —— 完全不是,模型自始至终没碰过你的函数,它的本质是职责分工:模型负责决策判断该调哪个函数传什么参数输出一个请求、你的程序负责执行真去跑函数查库调 API、模型再负责组织语言拿真实结果变成通顺回答,模型出脑子你的程序出手脚模型从不亲自动手。完整一个回合 5 步:发用户问题加工具说明给模型、模型返回结构化函数调用请求而非直接回答、你的程序真正执行函数拿到真实结果、把结果发回模型、模型拿到真东西生成最终回答,执行这步牢牢攥在你的程序手里安全可控。第一步定义工具给模型一份工具说明书,每个工具含名字、描述、参数 JSON Schema,决定成败的是那段描述模型完全靠它判断该不该用用哪个,要当成给新同事的工作交接来写,别一次塞几十个工具太多模型选起来犯晕。第二步跑通完整回合是一个循环不是一次性调用,只要模型还在要工具就继续执行加回填加再问直到它给最终答案,回填时消息角色必须是 tool 并带 tool_call_id,模型那条带 tool_calls 的 assistant 消息也要原样保留,每次调它都要带从头到现在的完整 messages,别忘给循环设上限防无限循环烧钱。工程要点:工具执行出错要把错误信息如实回填给模型别让程序崩、现代模型一轮可能返回多个 tool_calls 代码要遍历处理无依赖的可并行、权限安全卡在执行这一关模型请求调什么不等于你该执行什么敏感参数别信模型填的要用当前登录用户身份覆盖、工具不是越多越好描述质量大于工具数量、Function Calling 适合执行动作拿结构化实时数据 RAG 适合检索文档知识复杂应用常两者都用。正确做法是承认模型的能力边界,不徒劳地激励它教导它跨越,而是为那片空白架一座桥,让擅长思考的专心思考让擅长执行的专心执行,真正的强大不是假装没有边界而是看清边界然后在边界之外为自己接上一只手。

2024 年,我做一个 AI 助手时,撞上了一件让我哭笑不得、又后背发凉的事。这个助手要能回答用户问题,我接好了大模型,自测一切顺利。直到一个同事问它:"我上个月的订单金额是多少?"——它【非常自信地】回答了一个具体数字:"您上个月的订单总金额为 ¥3,280。"同事愣住了,因为他根本没下过几单。我去查数据库,发现真实数字是 ¥0。那个 ¥3,280,是模型【凭空编出来】的——它编得有零有整、语气笃定,像真的查过一样。我当时一身冷汗:它根本没有、也没法连我的数据库,可它没有说"我查不到",而是【编了一个】。我换了好几种问法,问天气它编天气,问库存它编库存,问汇率它编汇率。我一度以为是 prompt 写得不好,反复打磨那段"请不要编造"的提示词,可只要问题涉及"它不知道的实时数据",它该编还是编。我卡在这里很久,直到我想明白一件最朴素的事:我一直在求一个【只会生成文字】的模型,去做一件【需要真的去查、去算、去调用】的事。这就好比我反复叮嘱一个被锁在空房间里的人"请如实告诉我外面在下不下雨"——他再诚实,也只能猜。问题不在他诚不诚实,在于我【没给他一扇窗,也没给他一部能打电话问的电话】。模型缺的不是更好的提示词,是一种"能真的去调用外部能力"的机制。这件事逼着我把大模型为什么"干不了活"、Function Calling(函数调用 / 工具调用)到底是什么、它那个"请求-执行-回填"的闭环怎么转、以及工程上怎么把它做稳,彻底理清了一遍。本文是这份梳理的完整复盘。

问题背景:一个"张口就编"的 AI 助手

需求:做一个 AI 助手,能回答用户关于"自己的订单、天气、库存"的问题
事故现象:
- ★ 用户问"我上月订单金额" -> 模型自信地答"¥3,280"
- ★★ 真实数据库里的值是 ¥0 —— 这个数字是模型【凭空编的】
- ★ 换成问天气、问库存、问汇率,全都编 —— 有零有整、语气笃定

我试过但没用的办法:
- ★ 反复打磨 prompt,加"严禁编造""不知道就说不知道" -> 收效甚微
- ★ 它涉及"实时的、它不知道的数据"时,该编还是编

★★ 想明白的根:我在求一个【只会生成文字】的模型,去做
   一件【需要真的去查询、去调用】的事。它没有数据库连接、
   没有联网能力 —— 它对"我的订单"一无所知。
   但它的本能是"接着话往下写一个最像样的答案",
   于是它【编】了一个最像订单金额的数字。

★ 模型缺的不是更诚实的提示词,是一种机制:能让它在
  "需要外部数据/能力"时,真的去【调用一个外部函数】,
  拿到真实结果,再据此回答。

★ 这个机制,就是 Function Calling(函数调用 / 工具调用):
  - ★★ 它【不是】让模型自己去执行代码;
  - ★ 而是让模型能"请求":"我需要调用 query_order 这个
    函数,参数是 user=123, month=4" —— 由【你的程序】
    真的去执行它,再把结果还给模型。
  本文要把这个闭环,彻底讲透。

为什么纯大模型"干不了活"——它只会"生成下一个词"

# === ★ 先认清大模型的能力边界,才懂它为什么会"编" ===

# === ★★ 大模型的本质:一个"文字接龙"引擎 ===
# ★ 剥到最里面,一个大语言模型做的事,只有一件:给它
#   一段文字,它【预测下一个最可能的词】,然后把这个词
#   接上去,再预测下一个 —— 如此循环,生成一整段话。
# ★ ★ 它强,强在这个"预测"被训练得极好,好到生成的
#   文字逻辑通顺、知识丰富、像人写的。但它的能力,
#   【本质上就被锁死在"生成文字"这一件事里】。

# === ★ 由这个本质,推出它的两个"硬边界" ===
# ★ 边界 1:★★ 它不知道"训练之后"和"它接触不到"的事。
#   今天的天气、你数据库里的订单、此刻的汇率 —— 这些
#   都不在它的训练数据里,它【没有任何渠道】能知道。
# ★ 边界 2:★★ 它不能"执行动作"。它不能真的查数据库、
#   不能调 API、不能下单、不能发邮件。它能写出一段
#   "看起来像在下单"的文字,但没有任何动作真的发生。

# === ★★ 关键:撞到边界时,它不会"沉默",它会"编" ===
# ★ 这是最要命的一点。一个人不知道答案,会说"我不知道"。
#   但大模型的本能是【把话接下去、生成一个最像样的答案】。
# ★ ★ 所以当你问它一个它根本无从知道的事(你的订单),
#   它【不会停下来】,它会忠实地履行"文字接龙"的本能:
#   生成一串"最像一个订单金额"的数字。它不是在"撒谎",
#   它只是在做它唯一会做的事 —— 接龙。这就是【幻觉】。

# === ★ 所以"靠 prompt 让它别编"治标不治本 ===
# ★ 你加 "不知道就说不知道",能压制一部分,但压不住根 ——
#   因为模型【真的没有】那个数据,你让它"如实说",它
#   也没有"实"可说。★★ 真正的解法,不是求它"别编",
#   而是【给它一个能拿到真实数据的渠道】 —— 让它不必编。

# === ★ 这就是 Function Calling 要补的能力 ===
# ★ 既然模型只会"生成文字",那就别指望它自己去查、去做。
#   ★★ 换个思路:让它生成一种【特殊的文字】 —— 一个
#   "我想调用某函数、参数是什么"的【结构化请求】。
#   真正的"查"和"做",交给【你的程序】。
# ★ 模型负责"决定调什么、传什么参数",程序负责"真的
#   去执行" —— 各干各擅长的。下一节细讲这个机制。

# === 小结 ===
# ★ 大模型的本质是"文字接龙"引擎:给一段文字它预测
#   下一个最可能的词接上去再预测,能力本质锁死在"生成
#   文字"里。★★ 由此两个硬边界:① 它不知道训练之后和
#   接触不到的事(今天天气、你的订单、此刻汇率);② 它
#   不能执行动作(查库、调 API、下单)。★★ 最要命的是
#   撞到边界时它不沉默而是"编"—— 它的本能是把话接下去
#   生成一个最像样的答案,问它无从知道的事它会忠实履行
#   接龙本能生成一串最像订单金额的数字,这就是幻觉。
#   所以靠 prompt 求它别编治标不治本,它真的没有那个
#   数据。★ 真正的解法是给它一个拿到真实数据的渠道 ——
#   让它生成一个"我想调用某函数、参数是什么"的结构化
#   请求,真正的查和做交给你的程序,这就是 Function Calling。

Function Calling 的核心机制:模型只"请求",不"执行"

# === ★ 把 Function Calling 这件事的本质,一次说清 ===

# === ★★ 最大的误解:"模型会自己执行函数" ===
# ★ 很多人一听 Function Calling,以为是模型获得了"执行
#   代码"的能力。★★ 完全不是。模型【自始至终】都没碰过
#   你的函数,它连你的数据库长什么样都不知道。
# ★ ★ Function Calling 真正的机制,是一次【职责的分工】:
#  - ★ 模型负责【决策】:根据用户的问题,判断"这事我得
#    调用哪个函数、该传什么参数",然后【输出一个请求】。
#  - ★ 你的程序负责【执行】:真正去跑那个函数(查库、
#    调 API),拿到结果。
#  - ★ 模型再负责【组织语言】:拿到你给它的真实结果,
#    把它变成一句通顺的、给用户看的回答。
# ★ ★★ 一句话:模型出"脑子"(决定调什么),你的程序出
#   "手脚"(真的去调)。模型从不亲自动手。

# === ★ 完整的一个回合,长这样(记住这 5 步)===
# ★ ① 你把【用户问题】+【你有哪些工具的说明】一起发给模型;
# ★ ② 模型判断:这个问题需要工具。它【不回答用户】,
#   而是返回一个结构化的【函数调用请求】:要调
#   query_order,参数 {user:123, month:4};
# ★ ③ ★★ 你的程序收到这个请求,【真正执行】query_order
#   函数,从数据库查出真实结果(比如 ¥0);
# ★ ④ 你把这个【真实结果】,再发回给模型(作为一条
#   "工具返回"的消息);
# ★ ⑤ 模型这次拿到了真实数据,生成最终回答:"您上月
#   订单金额为 ¥0。" —— 这次它【没编】,因为它手里
#   有真东西了。

# === ★ 为什么这个设计是安全且可控的 ===
# ★ ★ 因为"执行"这一步,牢牢攥在【你的程序】手里。
#   模型只是"请求"调用,真不真的调、怎么调、要不要
#   先做权限校验和参数检查 —— 全是你的代码说了算。
# ★ 模型说"帮我调 delete_user",你的程序完全可以
#   审一道、拒掉。模型【没有】越过你的程序的能力。

# === ★ "请求"是结构化的,这点很关键 ===
# ★ 模型返回的不是一句自然语言"你帮我查下订单吧",而是
#   一个【机器能直接解析的结构化数据】(JSON):函数名
#   叫什么、每个参数叫什么、值是什么。
# ★ ★ 正因为它是结构化的,你的程序才能【可靠地、自动地】
#   解析它、执行它。这是 Function Calling 能工程化的前提。

# === 小结 ===
# ★ 最大的误解是"模型会自己执行函数"——完全不是,模型
#   自始至终没碰过你的函数。★★ Function Calling 的本质
#   是职责分工:模型负责决策(判断该调哪个函数传什么
#   参数、输出一个请求)、你的程序负责执行(真去跑函数
#   查库调 API)、模型再负责组织语言(拿真实结果变成
#   通顺回答)—— 模型出脑子你的程序出手脚,模型从不
#   亲自动手。★ 完整一个回合 5 步:① 发用户问题+工具
#   说明给模型;② 模型返回结构化函数调用请求而非直接
#   回答;③★★ 你的程序真正执行函数拿到真实结果;④ 把
#   结果发回模型;⑤ 模型拿到真东西生成最终回答,这次
#   没编。★ 执行这步牢牢攥在你的程序手里,模型只是
#   请求,真不真调、要不要先做权限校验全是你的代码说
#   了算 —— 安全可控。请求是结构化 JSON,你的程序才
#   能可靠自动地解析执行。

第一步:定义工具——给模型一份"工具说明书"

# === ★ 模型要会"用工具",前提是它知道"有哪些工具" ===

# === ★ 你要给模型一份"工具清单 + 说明书" ===
# ★ 模型不会凭空知道你的系统里有个 query_order 函数。
#   每次请求,你都要把【可用工具的描述】一起发给它。
#   每个工具的描述,包含三样东西:
# ★ ① ★★ 名字:函数叫什么(如 query_order);
# ★ ② ★★ 描述:这个函数【是干什么的、什么时候该用它】
#   —— 用自然语言写,这是给模型【看的】;
# ★ ③ ★★ 参数:它需要哪些参数、每个参数什么类型、
#   哪些是必填 —— 通常用 JSON Schema 描述。

# === ★★ 决定成败的,是那段"描述" ===
# ★ 模型【完全靠这段描述】,来判断"用户这个问题,该不该
#   用这个工具、该用哪个工具"。描述写得含糊,模型就会
#   选错工具、或该用时不用。
# ★ ★ 描述要写清:① 这个工具到底做什么;② 什么场景下
#   【应该】用它;③ 必要时还可以写明什么场景【不要】用。
#   把它当成"写给一个新同事的工作交接"那样去写。

# === ★ 参数描述,同样要清晰 ===
# ★ 每个参数,也要有自己的小描述。比如参数 month,描述
#   写"月份,1-12 的整数" —— 模型才知道该从用户的
#   "上个月"里,推算出正确的数字填进去。
# ★ ★ 必填参数要标明。如果一个必填参数,用户的问题里
#   没提供(比如查订单要 user_id,但对话里没有),好的
#   模型会【反问用户】要,而不是瞎填一个。

# === ★ 不要一次性塞几十个工具 ===
# ★ ★ 工具不是越多越好。给模型挂太多工具,它【选起来
#   会犯晕】,容易选错。
# ★ 经验:一次对话,给模型的工具控制在一个合理数量
#   (比如十几个以内);如果业务工具特别多,考虑先用
#   一层逻辑,按场景【筛选出本次可能用到的】那几个,
#   再交给模型。

# === 小结 ===
# ★ 模型要会用工具前提是知道有哪些工具 —— 每次请求都
#   要把可用工具的描述发给它,每个工具描述含三样:
#   ①名字、②★★描述(干什么、什么时候该用,自然语言
#   写给模型看)、③参数(哪些参数什么类型哪些必填,
#   用 JSON Schema)。★★ 决定成败的是那段描述:模型
#   完全靠它判断该不该用、用哪个,描述含糊就选错或该
#   用不用,要写清做什么、什么场景该用、必要时什么场景
#   不要用,当成给新同事的工作交接来写。★ 参数也要各有
#   清晰小描述(month 写"1-12 的整数"),必填参数要标明、
#   缺了好模型会反问用户而非瞎填。★ 别一次塞几十个工具,
#   太多模型选起来犯晕容易选错,业务工具多就先按场景
#   筛出本次可能用到的几个再交给模型。
# ★ 定义工具:给模型一份"工具说明书"(OpenAI tools 格式)
tools = [{
    'type': 'function',
    'function': {
        'name': 'query_order',
        # ★★ 描述:模型完全靠这段话,决定"要不要用、何时用"
        'description': '查询某个用户在指定月份的订单总金额。'
                       '当用户询问自己的订单、消费、账单金额时使用。',
        'parameters': {
            'type': 'object',
            'properties': {
                'user_id': {
                    'type': 'integer',
                    'description': '用户的唯一 ID',
                },
                'month': {
                    'type': 'integer',
                    'description': '月份,1-12 的整数',   # ★ 参数也要描述清楚
                },
            },
            'required': ['user_id', 'month'],   # ★ 必填:缺了模型应反问用户
        },
    },
}]

# ★ 工具对应的真实函数 —— 这才是真正去查数据库的地方
def query_order(user_id, month):
    row = db.query(
        'SELECT COALESCE(SUM(amount),0) AS total FROM orders '
        'WHERE user_id=%s AND MONTH(created_at)=%s', (user_id, month))
    return {'total': float(row['total'])}      # ★ 返回真实结果

第二步:跑通完整回合——请求、执行、回填的闭环

# === ★ 工具定义好了,怎么把那个"5 步回合"跑起来 ===

# === ★ 核心是一个【循环】,不是一次性调用 ===
# ★ ★ 新手最容易错的地方:以为"调一次模型"就完事了。
#   不是。Function Calling 是一个【可能要转好几圈】的
#   循环:
#  - 你调模型 -> 模型说"我要调工具 A" -> 你执行 A、
#    把结果喂回去 -> 你【再调一次模型】 -> 模型可能说
#    "我还要调工具 B" -> ... -> 直到模型【不再要求调
#    工具,而是直接给出最终答复】,循环才结束。
# ★ 所以代码结构是 while 循环:只要模型还在要工具,
#   就继续"执行 + 回填 + 再问"。

# === ★ 怎么判断模型这次是"要工具"还是"给答案" ===
# ★ ★ 模型每次返回,你要看它的返回类型 / finish_reason:
#  - ★ 如果它返回的是【工具调用请求】(tool_calls)——
#    那就去执行工具,把结果回填,继续循环;
#  - ★ 如果它返回的是【普通文字内容】—— 那就是最终
#    答案了,循环结束,把这段话给用户。

# === ★★ 回填结果时,消息的"角色"不能错 ===
# ★ 你把工具执行结果发回给模型时,这条消息要标成一个
#   特殊角色(role: 'tool'),并带上【是哪个工具调用
#   的结果】(tool_call_id)。
# ★ ★ 而且,模型那条"要求调用工具"的消息(assistant
#   带 tool_calls 的那条),你也必须【原样保留】在对话
#   历史里。顺序是:assistant(要调工具)-> tool(工具
#   结果)。少了任何一条,或顺序乱了,模型就接不上。

# === ★ 每一轮,完整的对话历史都要带上 ===
# ★ 模型是无状态的,它不"记得"上一轮。所以每次调它,
#   你都要把【从头到现在的完整 messages 列表】发过去 ——
#   包括最初的用户问题、模型的工具请求、工具的返回结果。
#   模型靠这份完整历史,才知道"现在进行到哪了"。

# === ★ 别忘了给循环设一个"上限" ===
# ★ ★ 万一模型陷入"反复要求调工具"的怪圈(工具一直返回
#   它不满意的结果),循环就停不下来。要设一个最大轮次
#   (如 5 轮),到顶就强制跳出,避免无限循环烧钱。

# === 小结 ===
# ★ 跑通回合的核心是一个【循环】不是一次性调用 —— 新手
#   最容易错。你调模型→模型说要调工具 A→你执行 A 把结果
#   喂回→★再调一次模型→模型可能说还要调 B→…→直到模型
#   不再要工具而是直接给最终答复,循环才结束,代码结构
#   是 while。★ 判断模型这次要工具还是给答案:看返回
#   类型 —— 返回 tool_calls 就执行工具回填继续循环,
#   返回普通文字就是最终答案结束。★★ 回填时消息角色
#   不能错:工具结果标成 role:tool 并带 tool_call_id,
#   模型那条带 tool_calls 的 assistant 消息也必须原样
#   保留,顺序是 assistant→tool,少一条或乱序模型就
#   接不上。★ 模型无状态不记得上一轮,每次调它都要带
#   从头到现在的完整 messages。★ 别忘给循环设上限(如
#   5 轮)防模型陷入反复要工具的怪圈无限循环烧钱。
import json

# ★ Function Calling 完整闭环:一个"执行+回填+再问"的循环
def chat_with_tools(user_question, user_id):
    messages = [
        {'role': 'system', 'content': '你是助手。需要数据时调用工具,不要编造。'},
        {'role': 'user', 'content': user_question},
    ]
    tool_map = {'query_order': query_order}    # 名字 -> 真实函数

    for _ in range(5):                          # ★ 循环上限 5 轮,防无限循环
        resp = client.chat.completions.create(
            model='gpt-4o', messages=messages, tools=tools)
        msg = resp.choices[0].message
        messages.append(msg)                    # ★★ 模型的消息原样保留进历史

        # ★ 模型没要工具 —— 这就是最终答案,结束
        if not msg.tool_calls:
            return msg.content

        # ★ 模型要调工具 —— 逐个真正执行,把结果回填
        for call in msg.tool_calls:
            fn = tool_map[call.function.name]
            args = json.loads(call.function.arguments)
            args.setdefault('user_id', user_id)         # ★ 敏感参数由程序兜底
            result = fn(**args)                          # ★★ 你的程序真正执行
            messages.append({
                'role': 'tool',                          # ★★ 角色必须是 tool
                'tool_call_id': call.id,                 # ★★ 对应哪次调用
                'content': json.dumps(result, ensure_ascii=False),
            })
        # ★ 循环回去,带着工具结果再问一次模型

    return '抱歉,处理超时,请稍后再试。'        # ★ 兜底:超过轮次上限

工程要点:多工具、并行、错误处理与选型

# === ★ Demo 跑通容易,做稳要注意这几件事 ===

# === ★ 要点 1:工具执行出错,要"如实回填",别崩 ===
# ★ ★ 工具函数会失败(数据库超时、API 报错、参数非法)。
#   关键:出错时,【不要让程序直接崩】,而是把"出错了、
#   错在哪"这个信息,当成工具结果【正常回填给模型】。
# ★ ★ 模型拿到"工具执行失败"的信息,能据此做合理反应 ——
#   换个参数重试、或如实告诉用户"暂时查不到"。它需要
#   知道失败了,而不是收到一个假装成功的空结果。

# === ★ 要点 2:并行工具调用 ===
# ★ 现代模型,一轮可能【一次返回多个工具调用】(比如同时
#   要"查天气"和"查日程")。你的代码要能【遍历处理】这
#   一批 tool_calls,而不是只处理第一个。
# ★ ★ 彼此无依赖的工具,可以并行执行,省时间。

# === ★★ 要点 3:权限和安全,卡在"执行"这一关 ===
# ★ ★ 永远记住:模型【请求】调用什么,不等于你就该
#   【执行】什么。模型可能被用户的话诱导,请求一个
#   越权操作(查别人的订单、删数据)。
# ★ ★ 你的程序在【真正执行前】,必须做检查:
#  - ★ 敏感参数(如 user_id)别信模型填的,用【当前
#    登录用户】的真实身份去覆盖 —— 见上一节代码里的
#    args.setdefault('user_id', user_id);
#  - ★ 写操作、危险操作,要有独立的权限校验,甚至
#    需要用户二次确认。
# ★ ★ "执行"这一关是你的安全闸门,绝不能因为"模型让我
#   调"就放行。

# === ★ 要点 4:工具不是越多越好,描述质量 > 工具数量 ===
# ★ 与其挂 50 个描述潦草的工具,不如挂 10 个描述精准的。
#   模型选不对工具,九成是描述没写好,不是工具不够。

# === ★ 要点 5:Function Calling vs RAG,什么时候用哪个 ===
# ★ ★ 两者都是"给模型补充外部信息",但场景不同:
#  - ★ RAG:适合【从一大堆文档/知识里,检索相关文字】。
#    问题是"知识型"的(公司制度、产品文档)。
#  - ★ Function Calling:适合【执行一个明确的动作 / 拿
#    一个结构化的实时数据】。问题是"操作型/实时型"的
#    (查订单、下单、查天气、算数)。
# ★ ★ 复杂的 AI 应用,常常【两者都用】:用 Function
#   Calling 查实时数据,用 RAG 查静态知识。

# === 认知 ===
# ★ Demo 易做稳要注意:① 工具执行出错要如实回填别崩 ——
#   出错时把"出错了、错在哪"当成工具结果正常回填,模型
#   拿到失败信息才能换参数重试或如实告诉用户,别给它
#   假装成功的空结果;② 并行工具调用 —— 现代模型一轮
#   可能返回多个 tool_calls,代码要遍历处理这一批而非
#   只处理第一个,无依赖的可并行执行;③★★ 权限安全卡
#   在"执行"这一关 —— 模型【请求】调什么不等于你该
#   【执行】什么,它可能被诱导请求越权操作,真正执行前
#   必须检查:敏感参数(user_id)别信模型填的用当前登录
#   用户身份覆盖、写操作危险操作要独立权限校验甚至二次
#   确认;④ 工具不是越多越好,描述质量 > 工具数量;
#   ⑤ Function Calling 适合执行动作/拿结构化实时数据
#   (查订单查天气),RAG 适合检索文档知识,复杂应用
#   常两者都用。

命令速查

Function Calling 一个完整回合(5 步)
=============================================================
① 发送   用户问题 + 工具说明书(tools) -> 模型
② 请求   模型不答用户,返回结构化的"工具调用请求"
③ 执行   ★你的程序真正跑那个函数(查库/调 API),拿真实结果
④ 回填   结果以 role:tool 的消息发回模型(带 tool_call_id)
⑤ 回答   模型拿到真实数据,生成最终答复(可能再循环回②)

关键概念速查
-------------------------------------------------------------
tools         发给模型的工具说明书:名字+描述+参数 Schema
description   工具描述,模型靠它决定用不用、用哪个 —— 最关键
tool_calls    模型返回的工具调用请求(结构化 JSON)
role: tool    回填工具结果时,消息必须用的角色
tool_call_id  标明这条结果对应哪一次工具调用
循环上限      防模型反复要工具陷入死循环,设最大轮次(如 5)

选型:Function Calling 用于"执行动作/拿实时结构化数据"
      RAG 用于"检索文档知识";复杂应用常两者都用

口诀:模型只出脑子(决定调什么),你的程序出手脚(真的去调)
      模型从不亲自执行,"执行"那一关是你的安全闸门

避坑清单

  1. 大模型本质是文字接龙引擎,只会生成文字,不知道它接触不到的实时数据,也不能真的执行动作
  2. 模型撞到能力边界时不会沉默而是编,问它无从知道的事它会忠实接龙生成一个最像样的答案
  3. 靠 prompt 加严禁编造治标不治本,模型真的没有那个数据,正解是给它一个拿到真实数据的渠道
  4. Function Calling 不是让模型自己执行代码,模型自始至终没碰过你的函数,它只输出一个请求
  5. 本质是职责分工:模型出脑子决定调什么传什么参数,你的程序出手脚真正去执行,模型不动手
  6. 工具描述决定成败,模型完全靠那段描述判断该不该用用哪个,要当成给新同事的工作交接来写
  7. 跑通是一个循环不是一次性调用,只要模型还在要工具就继续执行加回填加再问,直到它给最终答案
  8. 回填工具结果消息角色必须是 tool 并带 tool_call_id,模型那条带 tool_calls 的消息也要原样保留
  9. 工具执行出错要把错误信息如实回填给模型别让程序崩,模型需要知道失败了而不是收到假的空结果
  10. 权限安全卡在执行这一关,模型请求调什么不等于你该执行什么,敏感参数别信模型填的要用真实身份覆盖

总结

这一趟把 Function Calling 彻底理清的过程,纠正了我一个关于"智能"的、藏得极深的错觉。在我撞见那个 ¥3,280 的假数字之前,我心里对大模型,其实有一种近乎迷信的期待:它那么"聪明",知识那么渊博,语言那么流畅,我下意识地以为,它是一个【全能】的东西——只要我把问题问对、把提示词打磨得足够好,它就【什么都能办到】。所以当它编造订单金额时,我的第一反应,不是"它办不到",而是"它没办好"——我以为是我没把它"教"对,于是我一遍遍去改那段"请不要编造"的提示词,像是在教导一个聪明但偷懒的学生。我从没想过,我面对的根本不是一个"偷懒"的问题,而是一个"它真的没有这个器官"的问题。它编,不是因为它不诚实,是因为我问了它一个它【物理上无法回答】的问题,而它唯一会做的事,就是接着把话说圆。我一直以为它是个全才,其实它是个【极度偏科】的天才——它在"理解语言、组织语言"这一科上是绝顶高手,但在"感知真实世界、执行真实动作"这一科上,它是零分,是一片彻底的空白。复盘到最深,我意识到 Function Calling 这个机制,真正教给我的,不是一套 API 的调用范式,而是一种关于"如何与一个'能力有边界'的伙伴协作"的思路。它的设计哲学,朴素得惊人:它没有试图把模型"修"成全能的,没有徒劳地去填补那片空白;它做的,是【清醒地承认那片空白的存在】,然后【为这片空白,架一座桥】。模型不会查数据库?没关系,不强求它会——而是让它学会"说出'我需要查数据库'这件事",剩下的,交给真正会查数据库的程序。这是一种极其成熟的分工:让擅长思考的,专心思考;让擅长执行的,专心执行;中间用一个清晰的协议(结构化的调用请求)把它们连起来。它不追求"一个全能的个体",它追求"一个各司其职的系统"。这个认知,后来彻底改变了我用 AI、甚至是带团队、做架构的方式。我不再期待任何【单一的一个东西】是全能的——无论是一个模型、一个人、还是一个模块。我学会了先冷静地为它画一条能力边界线:这条线以内,是它的天赋,让它尽情发挥;这条线以外,是它的空白,我不再徒劳地去"激励"它、"教导"它跨越,而是去想——我该为这片空白,配上一个什么样的"工具"、搭一座什么样的"桥"。一个再好的程序员,也需要一个 IDE、一个调试器、一个文档库;一个再聪明的大模型,也需要一套它能调用的工具。"承认边界",听起来像是认输,但我后来明白,它恰恰是更高级的智慧:你只有先诚实地、不带幻想地看清一个东西"不能做什么",你才有可能真正地、可靠地用好它"能做什么"。这次最大的收获,是我给自己换掉了一个有害的念头。我不再追问"怎样才能让它无所不能",我转而问自己两个清醒得多的问题:"它真正的天赋,是什么?"以及,"它做不到的那部分,我该为它架一座怎样的桥?"那个凭空编出来的 ¥3,280 教给我的,从来不是一个模型的缺陷,而是一句让我受用很久的话:真正的强大,不是假装没有边界,而是看清边界,然后在边界之外,为自己接上一只手。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

JWT 与 Session 完全指南:Web 登录态方案的工程选型

2026-5-21 12:14:48

技术教程

分布式锁完全指南:Redis 分布式锁的正确打开方式

2026-5-21 12:27:06

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索