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 用于"检索文档知识";复杂应用常两者都用
口诀:模型只出脑子(决定调什么),你的程序出手脚(真的去调)
模型从不亲自执行,"执行"那一关是你的安全闸门
避坑清单
- 大模型本质是文字接龙引擎,只会生成文字,不知道它接触不到的实时数据,也不能真的执行动作
- 模型撞到能力边界时不会沉默而是编,问它无从知道的事它会忠实接龙生成一个最像样的答案
- 靠 prompt 加严禁编造治标不治本,模型真的没有那个数据,正解是给它一个拿到真实数据的渠道
- Function Calling 不是让模型自己执行代码,模型自始至终没碰过你的函数,它只输出一个请求
- 本质是职责分工:模型出脑子决定调什么传什么参数,你的程序出手脚真正去执行,模型不动手
- 工具描述决定成败,模型完全靠那段描述判断该不该用用哪个,要当成给新同事的工作交接来写
- 跑通是一个循环不是一次性调用,只要模型还在要工具就继续执行加回填加再问,直到它给最终答案
- 回填工具结果消息角色必须是 tool 并带 tool_call_id,模型那条带 tool_calls 的消息也要原样保留
- 工具执行出错要把错误信息如实回填给模型别让程序崩,模型需要知道失败了而不是收到假的空结果
- 权限安全卡在执行这一关,模型请求调什么不等于你该执行什么,敏感参数别信模型填的要用真实身份覆盖
总结
这一趟把 Function Calling 彻底理清的过程,纠正了我一个关于"智能"的、藏得极深的错觉。在我撞见那个 ¥3,280 的假数字之前,我心里对大模型,其实有一种近乎迷信的期待:它那么"聪明",知识那么渊博,语言那么流畅,我下意识地以为,它是一个【全能】的东西——只要我把问题问对、把提示词打磨得足够好,它就【什么都能办到】。所以当它编造订单金额时,我的第一反应,不是"它办不到",而是"它没办好"——我以为是我没把它"教"对,于是我一遍遍去改那段"请不要编造"的提示词,像是在教导一个聪明但偷懒的学生。我从没想过,我面对的根本不是一个"偷懒"的问题,而是一个"它真的没有这个器官"的问题。它编,不是因为它不诚实,是因为我问了它一个它【物理上无法回答】的问题,而它唯一会做的事,就是接着把话说圆。我一直以为它是个全才,其实它是个【极度偏科】的天才——它在"理解语言、组织语言"这一科上是绝顶高手,但在"感知真实世界、执行真实动作"这一科上,它是零分,是一片彻底的空白。复盘到最深,我意识到 Function Calling 这个机制,真正教给我的,不是一套 API 的调用范式,而是一种关于"如何与一个'能力有边界'的伙伴协作"的思路。它的设计哲学,朴素得惊人:它没有试图把模型"修"成全能的,没有徒劳地去填补那片空白;它做的,是【清醒地承认那片空白的存在】,然后【为这片空白,架一座桥】。模型不会查数据库?没关系,不强求它会——而是让它学会"说出'我需要查数据库'这件事",剩下的,交给真正会查数据库的程序。这是一种极其成熟的分工:让擅长思考的,专心思考;让擅长执行的,专心执行;中间用一个清晰的协议(结构化的调用请求)把它们连起来。它不追求"一个全能的个体",它追求"一个各司其职的系统"。这个认知,后来彻底改变了我用 AI、甚至是带团队、做架构的方式。我不再期待任何【单一的一个东西】是全能的——无论是一个模型、一个人、还是一个模块。我学会了先冷静地为它画一条能力边界线:这条线以内,是它的天赋,让它尽情发挥;这条线以外,是它的空白,我不再徒劳地去"激励"它、"教导"它跨越,而是去想——我该为这片空白,配上一个什么样的"工具"、搭一座什么样的"桥"。一个再好的程序员,也需要一个 IDE、一个调试器、一个文档库;一个再聪明的大模型,也需要一套它能调用的工具。"承认边界",听起来像是认输,但我后来明白,它恰恰是更高级的智慧:你只有先诚实地、不带幻想地看清一个东西"不能做什么",你才有可能真正地、可靠地用好它"能做什么"。这次最大的收获,是我给自己换掉了一个有害的念头。我不再追问"怎样才能让它无所不能",我转而问自己两个清醒得多的问题:"它真正的天赋,是什么?"以及,"它做不到的那部分,我该为它架一座怎样的桥?"那个凭空编出来的 ¥3,280 教给我的,从来不是一个模型的缺陷,而是一句让我受用很久的话:真正的强大,不是假装没有边界,而是看清边界,然后在边界之外,为自己接上一只手。
—— 别看了 · 2026