你随便问一个已经转行进大模型方向的朋友,第一周学什么最值,十有八九会提到 Function Calling。我第一次在自己电脑上跑通这个能力的时候,第一反应是:原来大模型不是只能聊天,它是真的能帮我干活了。这篇文章不绕弯子,直接从转行程序员的角度,把这个功能拆开讲清楚,同时给出一套可以直接抄走的业务流程示例。不管你是刚开始学 Prompt Engineering,还是已经在做 AI 应用落地,搞清楚 Function Calling 都会让后面学 Agent、学 RAG 轻松一大截。
1. 为什么转行学大模型,我建议你先盯住 Function Calling
1.1 先理解它解决了什么问题
很多人刚接触大模型时,会有一个错觉:模型什么都知道。实际用起来就露馅了——你问它“我的订单 1024 现在到哪了”,它只会给你一段编出来的物流轨迹;你问它“今天门店几点关门”,它连今天是星期几都得猜。
问题出在哪?大模型本质上是“根据训练数据预测下一个词”的系统,它没有连接你的数据库、订单系统、门店信息,也没有实时获取天气、库存、日期的能力。它不知道你的业务数据,更不可能替你执行操作。
Function Calling 解决的就是这个“模型与现实世界之间的连接问题”。它让模型在对话过程中,识别出“这个请求需要调用某个外部函数”,然后把调用意图、所需参数都整理成结构化数据返回给你。真正去查数据库、调接口、执行动作的,还是你自己写的代码。模型只负责判断“该调什么、参数是什么”,你的代码负责“真正把事办了”,最后把结果塞回对话里,让模型组织成用户能看懂的回答。
一句话概括:Function Calling 是大模型对外部世界的“手和脚”,让 AI 从“聊聊天”进化成“办办事”。
1.2 转行新手最容易踩的“只会聊天”误区
转行学大模型,最大的误区不是看不懂 Transformer,而是把大量时间花在“调 Prompt 让模型回答得更像人”上,却始终没有让模型触碰真实数据。我自己最早也这么干过,研究各种提示词花了两周,结果做一个库存查询 Demo 时,发现模型在编库存数字,整个产品根本没有可用性。
后来想明白一个道理:Chat 能力决定模型“会不会说话”,Function Calling 决定模型“能不能办事”。做工具型、业务型应用,后者的重要性远高于前者。现在市面上真正能落地的 AI 助手,十个里有九个是“意图识别 + 函数调用 + 业务系统”的组合。你与其天天琢磨花哨的 Prompt,不如先花一周把 Function Calling 跑透,这是性价比最高的入门路线。
2. 拆开看:Function Calling 的交互机制和关键设计
2.1 一次函数调用的完整链路
先看一次函数调用从发生到结束,消息是怎么流转的。假设用户问“帮我查一下订单 1024001 到哪了”,整个流程是这样的:
- 你调用模型接口,在请求里带上用户消息,同时带上一个“可用工具清单”,清单里描述了一个函数:get_order_status,参数是 order_id。
- 模型看完消息和工具清单,判断“这事得查系统”,于是返回一条特殊的 assistant 消息,消息里带 tool_calls,内容是:调用 get_order_status,参数是 {"order_id": "1024001"}。
- 你的代码收到这条消息,自己执行 get_order_status 函数,去数据库查出订单状态。
- 你把查询结果作为一条 role 为 tool 的消息发回给模型,这条消息要带上 tool_call_id,用于关联上一步的调用。
- 模型看到工具返回结果,把“已发货,预计明天下午送达”组织成自然语言回复给用户。
关键点在于:模型从头到尾没有直接访问你的数据库,它只是“提出了一个调用请求”。真正执行的是你的代码。这种设计的好处是安全可控——权限校验、日志审计、参数过滤,全都握在你自己手里,不会让模型裸奔去碰数据。
2.2 工具描述(Tools Schema)怎么写,直接影响成功率
Function Calling 的成败,很大程度上取决于你给模型看的工具描述写得够不够清楚。每个函数都要用标准 JSON Schema 描述,包含函数名、功能说明、参数类型、参数含义、哪些参数必填。
这里给一个典型的工具描述,用来查询门店营业时间:
{ "type": "function", "function": { "name": "get_store_hours", "description": "查询指定门店在指定日期的营业时间。当用户询问某家门店几点开门、几点关门、是否营业时,调用此函数。", "parameters": { "type": "object", "properties": { "store_id": { "type": "string", "description": "门店编号,例如 store_001" }, "date": { "type": "string", "description": "查询日期,格式为 YYYY-MM-DD" } }, "required": ["store_id", "date"] } } }有四个细节非常影响成功率:
第一,description 别写太短。只写“查询营业时间”的话,模型在某些模糊场景下会犹豫该不该调用;写清楚“用户询问开门、关门、是否营业时调用”,模型就更容易命中。
第二,参数要带单位、格式、取值范围。比如日期必须写成 YYYY-MM-DD,否则模型可能给你传一个“明天”“下周一”这种相对时间,你的代码没法处理。
第三,required 字段一定要标完整。是必填的参数就列进去,模型一般会遵守,但你不写它就可能不传。
第四,合理设置枚举值。如果某个参数只能取固定几个值,用 enum 写死,比让模型自由发挥稳定得多。
2.3 为什么 temperature、max_tokens 这些参数也要跟着调
很多转行的人习惯把 temperature 保持默认值,结果发现函数调用偶尔会出错。原因很简单:temperature 控制随机性,而这个场景需要的是“确定性”。
工具名选哪个、参数怎么填,本质上是一个结构化分类问题,不是创意写作。所以我做工具调用时,temperature 通常会设成 0 或者 0.2 这种低值,让模型输出更稳定。这不是玄学,你可以自己做一个实验:同一句话问 20 次,temperature 为 0 时基本每次都调用同一个函数;调到 1 以后,时不时就出现“该调函数时不调”的情况。
max_tokens 也要留足够余量。函数返回结果很长的时候,模型要“阅读”完再组织回答,如果输出长度上限太小,回答会被截断。另外,连续多轮调用时,消息列表会不断累积,系统里要及时清理或者压缩前面的历史内容,否则很快会触达上下文窗口上限。
3. 完整业务流程示例:做一个“订单查询助手”
3.1 需求梳理和工具清单
用一个最贴近业务的例子来演示:做一个门店订单查询助手,用户可以用自然语言查订单状态、查门店营业时间、取消订单。
梳理下来需要三个工具:
- get_order_status(order_id):查询订单当前状态和物流信息。
- get_store_hours(store_id, date):查询门店当天营业时间。
- cancel_order(order_id, reason):取消订单,需要说明原因。
每个工具背后都对应你自己的业务逻辑。为了示例清楚,我先用本地函数模拟,实际项目里换成数据库查询、后端接口调用即可。
3.2 核心代码实现:多轮对话里的函数调用循环
下面是核心代码。用简洁的 Python 演示,假设你的大模型服务提供标准的 Chat Completions 接口:
import json import requests MODEL = "your-model-name" API_URL = "https://api.your-provider.example/v1/chat/completions" API_KEY = "your-api-key" SYSTEM_PROMPT = "你是一个门店订单助手,负责查询订单、查询门店营业时间、协助用户取消订单。回答要简洁准确。" TOOLS = [ { "type": "function", "function": { "name": "get_order_status", "description": "查询订单的当前状态和物流信息。", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "订单号,例如 1024001"} }, "required": ["order_id"] } } }, { "type": "function", "function": { "name": "get_store_hours", "description": "查询指定门店在指定日期的营业时间。", "parameters": { "type": "object", "properties": { "store_id": {"type": "string", "description": "门店编号,例如 store_001"}, "date": {"type": "string", "description": "日期,格式 YYYY-MM-DD"} }, "required": ["store_id", "date"] } } }, { "type": "function", "function": { "name": "cancel_order", "description": "取消指定订单,需要提供取消原因。", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "订单号"}, "reason": {"type": "string", "description": "取消原因,例如 到货太慢、不想要了"} }, "required": ["order_id", "reason"] } } } ] def get_order_status(order_id: str) -> dict: if order_id == "1024001": return {"order_id": order_id, "status": "已发货", "eta": "2025-06-20 18:00", "logistics": "顺丰速运"} return {"order_id": order_id, "status": "未找到该订单"} def get_store_hours(store_id: str, date: str) -> dict: return {"store_id": store_id, "date": date, "open": "09:00", "close": "21:00"} def cancel_order(order_id: str, reason: str) -> dict: return {"order_id": order_id, "result": "success", "message": f"订单已取消,原因:{reason}"} FUNCTIONS = { "get_order_status": get_order_status, "get_store_hours": get_store_hours, "cancel_order": cancel_order, } def assistant_message_with_tool_calls(msg: dict) -> dict: assistant_msg = {"role": "assistant", "content": msg.get("content")} if msg.get("tool_calls"): assistant_msg["tool_calls"] = msg["tool_calls"] return assistant_msg def run_conversation(user_input: str, history: list | None = None): messages = [{"role": "system", "content": SYSTEM_PROMPT}] if history: messages.extend(history) messages.append({"role": "user", "content": user_input}) headers = {"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"} for _ in range(10): # 限制最大轮次,防止模型无限循环 payload = { "model": MODEL, "messages": messages, "tools": TOOLS, "temperature": 0.2 } resp = requests.post(API_URL, json=payload, headers=headers) resp.raise_for_status() data = resp.json() msg = data["choices"][0]["message"] messages.append(assistant_message_with_tool_calls(msg)) if not msg.get("tool_calls"): return msg.get("content"), messages for tc in msg["tool_calls"]: fn = tc["function"] fn_name = fn["name"] args = json.loads(fn["arguments"] or "{}") try: result = FUNCTIONS[fn_name](**args) except Exception as exc: result = {"error": f"执行函数 {fn_name} 失败: {str(exc)}"} messages.append({ "role": "tool", "tool_call_id": tc["id"], "content": json.dumps(result, ensure_ascii=False) }) return "已达最大调用轮次,请稍后重试。", messages这个实现里有几个点值得说明。
第一个是循环结构。模型第一次返回可能不是最终答案,而是“先调函数再回答”。所以代码要用 while 或者 for 循环不断处理 tool_calls,直到模型返回纯文本回复。我限制最多 10 轮,是防止模型陷入“调用-失败-再调用”的死循环,生产环境里这个上限建议做成可配置,并且配上告警。
第二个是 assistant 消息的构造。带 tool_calls 的 assistant 消息必须原样把 tool_calls 传给 API,否则模型不知道“刚才自己请求过什么”。内容字段可能为空,需要按接口要求兼容。
第三个是 tool 消息的关联。回传结果时,tool_call_id 必须和模型返回的 tool_calls[].id 保持一致,不然模型无法把结果对应到具体的调用请求上。
3.3 运行效果和边界情况
一次典型的多轮对话大概是这样的:
用户:帮我查一下订单 1024001 到哪了?
你的程序第一次请求模型,模型返回 tool_calls:调用 get_order_status,参数 {"order_id": "1024001"}。你执行完函数后把结果回传,模型最终回答:您的订单 1024001 已发货,由顺丰速运承运,预计明天 18:00 前送达。
用户接着问:那 store_001 明天几点开门?
这一次模型带着之前的对话历史继续推理,调用 get_store_hours,参数 {"store_id": "store_001", "date": "2025-06-21"},拿到结果后回答:store_001 明天营业时间是 09:00 到 21:00。
用户又说:这个订单我明天收不了,帮我取消吧。
此时模型需要调用 cancel_order,但它发现用户没有明确说取消原因。有的模型会直接给你编一个原因“用户要求取消”,这就错了。更稳的做法是让模型先反问“请问取消原因是什么”,拿到原因后再调用函数。这个差异就来自系统提示词。我实际测试下来,在 system 里明确写一句“取消订单必须先确认原因再调用函数”,模型就比较听话。这类“业务规则”放进系统提示里,比反复调 temperature 更管用。
边界情况也要注意:模型传了不存在的订单号、函数抛异常、参数缺了必填项,这些都可能导致整条链路失败。我建议所有工具函数返回值都统一用字典结构,并且在代码里做异常捕获。函数出错时不要直接崩溃,而是把错误信息返给模型,让模型用自然语言向用户解释“查询失败,请确认订单号是否正确”。这样用户侧的体验会好很多。
4. 真实项目里才遇得到的设计经验
4.1 工具返回结果的设计比函数本身更重要
很多人把 Function Calling 的重心放在“怎么写工具定义”上,跑通 Demo 后才发现,真正影响体验的是“工具返回什么”。
我自己的经验是:返回结果要尽量结构化、扁平化、可控大小。比如 get_order_status,不要把一个数据库里十几字段的原始对象全塞给模型,而是先整理成用户关心的核心字段:订单号、状态、预计送达时间、物流公司。字段一多,模型组织回答时容易挑错重点,而且 token 消耗也会变大。
建议所有工具函数统一返回字典,并且约定好“成功”和“失败”两种结构。成功时返回正常业务字段;失败时返回 {"error": "具体错误信息"}。模型看到 error 字段,会自动降低预期,告诉用户“查不到”而不是强行编一个结果。这一点值得反复强调:宁可返回“不知道”,也不允许模型编答案。
4.2 多工具场景下的冲突与命名
当工具数量超过五六个后,会出现两个新问题:一是模型选错工具,二是工具描述互相干扰。比如你同时有 get_order_status 和 get_order_detail,模型面对“帮我看看订单”这种模糊请求时,选择哪个就看 description 写得好不好。如果两个工具描述的差异不够清晰,模型很容易随机选一个。
我的建议是:把相似功能的工具合并,而不是盲目增加数量。能用参数区分的场景,优先用同一个函数加不同参数。比如 get_order_info(order_id, fields) 可以同时覆盖状态、详情、物流,比拆成三个函数稳定很多。
命名上也别偷懒。函数名本身要见名知意,get_order_status、cancel_order 这种命名,比 func1、queryOrder 这种缩写更能帮助模型理解。工具列表的顺序也有影响,把高频工具放在前面,模型更倾向于优先考虑。
4.3 安全、权限与脱敏:别让工具裸奔
工具一旦接上真实系统,安全意识必须跟上。核心原则是:工具背后一定要做权限校验和参数校验。不能因为模型是“AI”就信任它传的参数。虽然正常情况下模型会按描述填参,但用户可以通过构造特殊的对话内容,诱导模型调用不该调用的函数,或者给函数传危险参数。
比如取消订单这种高风险的函数,我的处理方式是:函数内部除了校验参数格式,还要校验当前用户身份和订单归属,确认订单确实属于这个用户才能执行。所有工具调用都要有日志,记录谁在什么时间请求了什么操作,方便事后审计。
返回结果也要脱敏。物流接口返回的完整信息可能包含内部仓库编号、客户手机号,这些不该暴露给模型。最好是让工具层提前过滤字段,只返回模型组织回答所需的最少信息。模型不需要知道的,就不要让它看到。
5. 常见问题与排查技巧实录
5.1 问题现象与排查思路速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 模型不调用工具,直接编答案 | temperature 太高;工具 description 太简单;用户表述未触发调用意图 | 调低 temperature;重写 description;在 system 提示里明确列出“什么时候必须调用工具” |
| 调用了不存在的函数名 | 工具定义和本地函数映射不一致;参数大小写问题 | 检查 TOOLS 里的 name 与 FUNCTIONS 字典的 key 是否完全一致 |
| 参数缺失或类型不对 | required 没标;类型定义太宽泛;模型输出被截断 | 严格定义 required;参数尽量用 string 并给出格式示例;确认 max_tokens 没截断 |
| 工具返回了结果,模型还是答非所问 | 返回结构太复杂;字段含义不清;返回内容过长 | 简化返回结构;给字段再加说明;控制返回字段数量 |
| 模型反复调用同一个工具,陷入循环 | 函数出错但没返回 error;业务规则存在矛盾 | 函数内加异常捕获;设置最大调用轮次;返回明确 error 让模型停止 |
| 多次调用后效果变差 | 上下文累积太长,关键信息被淹没;超出上下文窗口 | 清理早期消息;把必要信息浓缩进 system;限制历史长度 |
| 同一句话测试时好时坏 | temperature 偏高;工具描述不够具体 | 调低 temperature;扩充枚举值和描述 |
| 上线后表现和本地测试不一致 | 模型版本变化;服务端配置差异 | 锁定模型版本;把本地测试用例做成回归测试集,每次发布前跑一遍 |
这张表是我从实际项目中慢慢攒出来的,遇到问题先对着看,大多数情况都能快速定位。最隐蔽的一个坑是“模型版本悄悄变了”,很多大模型服务会不定期切换到新版本,函数调用表现可能差别很大。这也是为什么生产环境一定要锁定模型版本,并且维护一组覆盖关键业务场景的回归用例。
5.2 模型循环调用或者“答非所问”怎么办
在真实对话里,模型偶尔会陷入一个循环:调用工具,拿回结果,又调用同一个工具,又拿回同样结果,始终不给最终回答。我用两层机制来兜底。
第一层是代码层面设置最大轮数。前面的示例里已经写了一个 for 循环限制 10 次,超了就直接返回提示,不让对话卡死。
第二层是结果层面诱导“收尾”。工具返回结果里,如果明确包含“查询完成”“操作成功”这种信号,很多模型就知道该总结收尾了。如果你发现某个工具被反复调用,检查一下是不是返回结果里带了让模型误以为“还没有完成任务”的信息,比如过于详细的中间状态。
“答非所问”的问题,多半出在工具描述和用户意图匹配不精确。一个容易忽略的点是:模型能否理解用户说的业务黑话。比如门店系统里管取消订单叫“退单”,用户说“退单”时模型可能没意识到要调 cancel_order。解决办法是在工具 description 里把常见别名写进去:“当用户提到 退单、取消、不想要了 时调用此函数”。这样命中率会明显提升。
6. 从 Function Calling 出发的转行学习建议
6.1 怎么练:三个递进式练手项目
如果你刚入坑,不建议一上来就看 Agent 框架源码。我的学习路径是:先做纯 Function Calling,再做多轮对话版,最后才碰自动规划。对应三个练手项目,难度逐个递增。
第一个项目:单轮问答工具调用。做一个天气查询助手,只支持“城市 + 日期”查天气。目标是跑通“用户消息 → 模型返回 tool_calls → 本地执行 → 回传结果 → 模型回答”这条完整链路,理解消息结构。
第二个项目:带记忆的多轮业务助手。做一个订单助手,支持查订单、查门店、取消订单三种操作,并且要在连续对话里正确理解上下文。比如用户先说“查一下订单 1024001”,接着问“这个门店明天营业吗”,你要让模型知道“这个门店”是从前面哪条信息里来的。这个阶段会强迫你认真思考消息历史和上下文管理。
第三个项目:多函数协作流程。做一个会议安排助手,需要同时查询参会人空闲时间、创建日程、发送通知。这时候会出现“一次请求里连续多次调用函数”的情况,你要学会处理依赖关系和中间结果。做完这三个项目,你对 Function Calling 的理解就足够支撑绝大多数实际应用开发了。
6.2 一些个人习惯和后续扩展方向
最后分享几个我自己的习惯,不一定都对,但确实让我少踩了不少坑。
第一个习惯是建立“最小测试集”。每次改工具定义、系统提示、模型版本后,用固定的三五条测试输入跑一遍,看结果是否稳定。不要靠感觉,要留档记录。工具描述这类东西改起来很微妙,有时候多写一句话,准确率能提升好几个点,但也可能引入反效果。有回归测试,心里就有底。
第二个习惯是时刻留意 token 成本。函数定义本身也会消耗 token,工具越多越明显。单次请求里塞 10 个复杂工具定义,每次都要重新传给模型。如果你做的应用调用量很大,要评估是全部带上,还是按场景分组加载。也尽量精简 description,但不是删到没有,这个平衡要在实测里找。
第三个习惯是在 Function Calling 跑稳之后,再去接触 Agent 框架或者更深度的编排能力。你会发现底层原理其实就是“循环调用工具 + 维护上下文”,框架只是帮你把这个过程包装得更友好。基础扎实之后再上手,能看懂它在做什么,遇到问题也不会完全抓瞎。
顺便分享一个我在订单助手项目里用的小技巧:给每次工具调用都生成一个 request_id,写进返回结果和日志里。排查线上问题时,直接通过 request_id 对账,能快速定位是哪次调用、哪条消息、哪个环节出了问题。这个习惯看起来不起眼,但在真实排障时帮了大忙。
Function Calling 这个入口,价值被很多人低估了。它不像大模型基础理论那样炫酷,却是从“聊天玩具”走向“可用产品”的关键一步。把这条链路吃透,你后面学任何上层框架,都会有一种“原来是这么回事”的通透感。