☰
从Function Calling到Code Mode:AI Agent工具调用的新范式
2026/11/3 23:28:17 网站建设 项目流程

1. 先聊个真实感受:Function Calling 用久了真的会心累

如果你过去一年在做大模型应用,Function Calling 这个名字你不可能没听过。它几乎是 Agent 和工具调用的默认姿势:你把工具写成 JSON Schema,模型在推理时输出一段结构化的参数,你的代码再去执行对应的函数。刚上手那会儿确实觉得“哇,模型真的能自己决定调用什么工具了”,但用久了你会发现,这套东西的别扭之处全藏在日常里。

举一个我自己的例子。做一个内部数据看板,需要让模型根据用户的自然语言查询去聚合几十张表。第一版方案很常规,注册了一个 query_data 的 function,参数是表名、筛选条件、聚合方式、时间范围。刚开始测几个固定问题,效果还行。一旦用户问得稍微绕一点,比如“这个季度每个区域的毛利率环比变化,找出增速最慢的两个”,Function Calling 就开始露怯了。模型要生成非常复杂的嵌套参数,一个不小心就结构错误,参数漏字段、枚举值写错、日期格式不统一,各种问题轮番来。

更烦的是维护成本。业务上每加一种查询方式,就得改一遍 schema 描述,还要想方设法把字段说明写得足够清楚。模型有时理解不到位的,你甚至得在描述里写“如果用户提到 XX,则应当把条件转换为 XX”。这哪里是给模型用的接口,这分明是在用自然语言写业务规则。所以当 Code Mode 这个思路开始在圈子里扩散的时候,我几乎是第一时间就去试了。试完之后我的感觉是:Function Calling 确实该退休了,至少在有相当一部分场景里,它已经被更本质的做法取代了。

2. 为什么 Function Calling 会走到这一步

2.1 先复盘一下 Function Calling 到底解决了什么问题

我们回过头看 Function Calling 的初衷。在 GPT 这类模型刚接入业务系统时,一个大问题是模型只会说话,不会动作。你问它“帮我查一下订单数”,它只能给一段文字回复,不可能真的去碰你的数据库。Function Calling 做的事情,就是给模型一个“行动的接口”:模型知道自己有哪些工具可用,理解了用户的意图后,输出一个符合格式的调用请求,由你的业务代码真正去执行。

这套机制的核心优势是可控。工具的参数有 schema,枚举值有约束,模型输出的 JSON 可以被程序严格校验,错了还能重试。对于调用外部 API、执行简单查询、操作固定功能这类任务,Function Calling 非常合适。比如查天气、翻译一段话、发一条消息、创建一条工单,这种“功能边界清晰、参数数量有限”的场景,Function Calling 的表现稳定得让人放心。

它也带动了整个 Agent 生态的爆发。你看到的各种自动化工作流,底层几乎都是 Function Calling 的变体。模型做规划,调用一堆工具,再根据结果回填下一步。这套架构支撑了很多产品从 Demo 走向可用。要说 Function Calling 没有价值,那肯定是不客观的。

2.2 但问题的根源在于:它把“工具调用”变成了一个 JSON 生成任务

Function Calling 的真正尴尬之处,不是它不稳定,而是它让模型做了一件并不擅长的事:把复杂的操作意图,硬生生压缩成一层有限的 JSON 结构。

模型最擅长的是什么?是生成连贯的自然语言和代码。你让模型写一段 Python 脚本,它可以自动处理循环、判断、异常、变量传递,这些是它在海量代码语料里学出来的本能。但你让模型输出一份 JSON 参数,它要考虑的却是这个工具的入参怎么嵌套、缺失字段怎么处理、日期格式怎么表达。JSON 结构一旦变得稍微复杂一点,模型的错误率就开始飙升,你的代码必须花大量精力去兜底。

更重要的是,Function Calling 的每个工具都是静态的。你注册了什么,模型就只能用什么。业务逻辑越复杂,工具就越多,工具一多,模型的上下文就开始被工具的描述占满。我见过有些 Agent 项目,光工具描述就有两万多个 token,模型在真正的用户问题还没进来之前,就已经被系统提示词和工具清单拖住了。这个架构走到后期,就是在用堆工具的方式对抗复杂性,而复杂性的增长永远比你在 schema 上的打补丁快。

2.3 还有一层隐性问题:工具之间的逻辑编排极其笨拙

Function Calling 的另一个隐含成本是编排。如果你要完成的任务需要五步操作,每一步依赖上一步的结果,你需要在代码里写一个复杂的 orchestrator。比如模型先调 search_tool,拿到结果后你返回给模型,模型再决定调 summarize_tool,再然后你可能需要把结果格式化后调 send_email_tool。每一步都要经历一次完整的推理环节,中间任何一个环节的 JSON 解析出错,后面的链条就断了。

我在实际项目里还踩过另一个坑:当两个工具的参数结构非常相似时,模型经常会选错。比如“创建用户”和“更新用户”这两个工具,参数就差一个 user_id,模型有时候就会在这个字段上犯迷糊,把创建当更新用。为了规避这个问题,我们不得不在 schema 描述里反复强调两者的区别,甚至把工具名改得冗长拗口,就为了让模型分得清。这种做法修起来快,但明眼人都知道这是下策,它反映的正是 Function Calling 在表达复杂行为时的先天不足。

3. Code Mode 是什么?它为什么值得被叫一声“新神”

3.1 核心转变:从“预定义工具”到“现写代码”

Code Mode 的思路非常直接:别再让模型去输出 JSON 调用预定义函数了,直接让模型写代码,然后在受控环境里把代码跑起来拿到真实结果。

我第一次看到这个思路时的反应是“这不就是让模型写脚本吗”,跟之前那些“让 ChatGPT 生成 Python 代码,然后你手动跑”的玩法有什么区别?仔细了解之后才发现区别大了。Code Mode 不是让模型在聊天框里生成一段代码给你,而是让模型在一个执行环境里既能写代码,又能立刻执行代码,还能读取执行结果,根据结果继续修改代码。模型从一个“提出建议的人”变成了“亲自动手解决问题的人”。

以数据分析为例。传统 Function Calling 的做法是:你提前给它准备一堆分析函数,它选一个来调用。Code Mode 的做法是:用户直接说“我想看一下各区域最近三个月的销售趋势,异常值单独标出来”,模型自己写 Pandas 脚本,读数据、算指标、画图,把结果文件给你。中间没有任何人提前设计过这个分析流程,一切由模型临场发挥。这是两种完全不同的范式。

3.2 为什么代码比 JSON 更适合作为模型的操作语言

我用一个简单的类比来说。你雇了一个实习生,Function Calling 是给这个实习生一本员工手册,手册里规定了所有他能做的事,每件事的流程都写死了,他只能按手册执行。Code Mode 则是你告诉实习生目标,让他自己想办法,他可以上网查、写脚本、做实验,只要最后把事情办成就行。哪个实习生解决复杂问题的能力强?显然是后者。

代码本身就是一种极其灵活的表达语言。循环、条件、异常处理、模块组合,这些能力天然存在于代码里,模型生成 Python 脚本的时候,不需要像生成 JSON 那样刻意考虑 Schema 约束,只需要遵循代码语法和逻辑。模型在这方面的训练数据极其丰富,生成质量远比生成结构复杂的 JSON 稳定。而且代码是可执行的,执行之后产生的错误信息、输出结果又能反馈给模型,形成迭代闭环。模型可以看着报错信息自己修 bug,这比 Function Calling 中“解析失败——让你重试——再解析失败”的体验顺畅太多。

从我实际用下来的体验看,Code Mode 在处理开放式任务、多步骤任务、需要临时决策的任务时,效率优势非常明显。模型不再需要先声明“我要调用 search_file 工具、我要调用 aggregate_data 工具”,而是直接用一段脚本把这两个逻辑写在一起,一次执行搞定。

3.3 为什么叫“新神”:它重新定义了 Agent 的工作方式

圈子里有人调侃说,Function Calling 把模型限制成了“会说话的接口调用器”,Code Mode 则让模型真正变成了“会干活的程序员”。这个说法虽然有些夸张,但确实点中了要害。

Code Mode 把 Agent 的工作方式从“模型——工具——再模型——再工具”变成了“模型——代码——执行——反馈——修改代码”。这中间少了一层工具注册的繁文缛节,多了一层执行反馈的闭环。AI Agent 不再是被动等待工具输出的调度器,而是一个主动写代码、调试代码、最终交付结果的执行者。这种变化,放在 Agent 演进的大背景里看,确实配得上“新神”这个称呼——它把模型在编程上的能力完整地释放到了自动化任务里。

4. 实操对比:同一个任务,两个方案的高下立见

4.1 场景设定:让模型分析一批 CSV 并输出洞察报告

为了说清楚差异,我设计一个最常见的任务。现在有一个 sales_data 文件夹,里面放了 12 个月的销售记录,每份文件包含日期、区域、品类、销售额、成本、数量这些字段。需求是:分析全年各区域的总销售额和毛利率,找出同比增长最快的品类,并输出一份 Markdown 报告。

我分别用 Function Calling 和 Code Mode 来实现这个需求,把过程中的真实体验记录下来,差异非常直观。

4.2 Function Calling 方案的实现过程

如果走 Function Calling 路线,我首先需要定义工具。大致要注册这些函数:

def list_files(directory: str) -> list[str]: ... def load_csv_to_dataframe(file_path: str) -> DataFrame: ... def compute_total_sales(df, group_by: str) -> DataFrame: ... def compute_gross_margin(df, group_by: str) -> DataFrame: ... def find_fastest_growing_category(df, start_month: str, end_month: str) -> str: ... def write_markdown_report(content: str, output_path: str) -> str: ...

每个函数的参数都需要定义 JSON Schema。比如 compute_total_sales 的 schema:

{ "name": "compute_total_sales", "description": "按指定列分组计算销售额总和", "parameters": { "type": "object", "properties": { "group_by": {"type": "string", "enum": ["region", "category"]}, "sort": {"type": "boolean", "description": "是否按销售额降序排列"} }, "required": ["group_by"] } }

工具准备好之后,调用链路是这样的:

  1. 用户提出需求;
  2. 模型分析后决定先调用 list_files 获取文件列表;
  3. 代码解析返回的 JSON,把文件列表送回模型;
  4. 模型决定调用 load_csv_to_dataframe,但你需要告诉它这个函数是加载某个文件并产生 DataFrame,问题在于“DataFrame 无法通过 JSON 返回”,于是你只能让它先把 dataframe 转成 csv 存到临时目录,再把路径传下去;
  5. 模型再调用 compute_total_sales,它需要知道之前生成的临时文件路径;
  6. 每一步都要维护一个状态上下文,把中间结果传来传去。

这里的麻烦点非常具体。第一,中间状态很难通过 JSON 传递,DataFrame、图片、模型对象这类二进制或内存对象,都得落盘或者序列化,路径管理变得复杂无比。第二,模型每一步的决策都需要解析一次 JSON 参数表,出错率跟步骤数量成正比。第三,我把“计算毛利率”“找增长最快品类”拆成了独立工具后,模型经常搞混参数,把 group_by 填反,或者把年月范围填错。

整个过程走下来,我最大的感受是:十个工具的 Function Calling 方案,比直接手写一个 50 行的 Pandas 脚本要慢得多,也脆弱得多。

4.3 Code Mode 方案的实现过程

换成 Code Mode 之后,整个流程就完全不一样了。我不需要预定义任何工具,直接把需求告诉模型就行。模型的执行逻辑大致长这样:

import pandas as pd import glob import os files = sorted(glob.glob("sales_data/*.csv")) df_list = [] for f in files: df = pd.read_csv(f) df_list.append(df) df = pd.concat(df_list, ignore_index=True) df["date"] = pd.to_datetime(df["date"]) df["month"] = df["date"].dt.to_month df["profit"] = df["sales"] - df["cost"] # 各区域总销售额与毛利率 region_stats = df.groupby("region").agg( total_sales=("sales", "sum"), total_cost=("cost", "sum") ) region_stats["gross_margin"] = (region_stats["total_sales"] - region_stats["total_cost"]) / region_stats["total_sales"] # 各品类同比增速 pivot = df.pivot_table(index="region", columns="category", values="sales", aggfunc="sum") growth = pivot.pct_change() * 100 # 生成报告 report_lines = [] for region, row in region_stats.iterrows(): report_lines.append(f"### {region}: 销售额 {row['total_sales']:.2f}, 毛利率 {row['gross_margin']:.1%}") fastest_categories = growth.sum().sort_values(ascending=False).head(3) report_lines.append(f"### 同比增长最快的品类: {fastest_categories.index.tolist()}") with open("report.md", "w") as f: f.write("\n".join(report_lines))

模型在执行环境里跑这段脚本,如果中途报错,比如日期格式不匹配、列名写错,执行环境的报错信息会回传给模型,模型自己就能修改脚本重新跑。整个过程不再需要我注册任何工具、定义任何 schema、维护任何状态。我只需要给一个目标,然后等它交付报告文件。

我自己还试过更复杂的场景,比如“把每个区域的销售额按周做平滑,然后画趋势图并标注异常点”。Code Mode 可以自己调用 matplotlib 画图、自己写一个简单的移动平均函数、自己判断异常点的阈值,最后生成一张图。这在 Function Calling 里几乎不可能做到,因为你需要预先定义一整套图表生成的工具,还要把“异常点怎么定义”这种主观判断写成工具参数,光想想就头大。

4.4 两个方案的成本对比

我整理了一个直观的对比表:

对比维度Function CallingCode Mode
工具注册成本每个功能都要写 schema、维护描述无需注册,直接描述需求
中间状态传递需要落盘/序列化/上下文管理代码变量直接传递
复杂逻辑支持依赖模型拆解步骤,容易断链模型直接写循环、判断、异常处理
错误恢复参数解析失败后由外部重试执行报错自动反馈给模型修改
动态决策只能调用预先定义好的操作模型可按需生成任何操作
上下文开销工具描述占用大量 token无需工具描述,token 更聚焦

单看这张表,Code Mode 在几乎所有维度上都更轻便、更灵活。这也是为什么它一出现就让很多做 Agent 的开发者感到兴奋。

5. 落地 Code Mode 要注意的那些坑

5.1 第一个必须解决的问题:安全隔离

让模型写代码然后直接执行,第一个反应必然是:它写出来的代码安全吗?这确实是 Code Mode 最大的争议点。如果模型生成的代码在你的主服务器上直接运行,一旦它被提示注入或者生成了一段恶意的系统操作,后果不堪设想。

我自己的做法是,给 Code Mode 搭一个沙箱执行环境。最省事的方式是用 Docker 容器,把执行环境限制在一个无网络或受限网络的容器里,只挂载必要的输入输出目录,限制 CPU 和内存。我用的配置大概是这样的:

docker run --rm \ --network none \ --memory 2g \ --cpus 2 \ --read-only \ -v /data/input:/workspace/input:ro \ -v /data/output:/workspace/output:rw \ sandbox-image python /workspace/run.py

几个关键点:--network none是为了阻止模型生成的代码访问外部网络,防止数据外泄;--read-only让根文件系统只读,容器里改不了系统文件;内存和 CPU 限制是为了防止模型写了一个死循环或者内存爆炸的脚本拖垮宿主机。输入目录只读挂载,输出目录才是唯一可写入的地方。

如果不用 Docker,也有基于 Jupyter Kernel 的隔离方案,或者用 Deno 的权限沙箱跑 JavaScript 代码。但不管用什么方案,安全隔离是 Code Mode 的第一道防线,绝对不能省。

5.2 第二个问题:执行反馈的质量决定迭代效率

Code Mode 之所以能自我纠错,靠的是执行环境返回的反馈。反馈质量越高,模型的修复效率越高。这里面有个很容易踩的坑:如果你只是把一个 Python traceback 原样丢给模型,它往往能修,但有时候会绕圈子。更好的做法是把标准输出、标准错误、当前工作目录的文件列表、以及执行耗时一起返回给模型。

我在实现反馈逻辑的时候,会把反馈整合成一个结构化的文本再交给模型:

feedback = f""" ## 执行输出 {stdout[:2000]} ## 错误信息 {stderr[:1000]} ## 当前目录文件 {file_list} ## 说明 如果脚本已经正确完成任务,请直接总结结果;如果报错,请修复后重新执行。 """

这个做法的好处是,模型能同时看到输出和报错,自己判断是继续修改还是任务已经完成。输出截断也很重要,不然模型生成一段死循环脚本时,stdout 可能膨胀到十几 MB,直接把上下文撑爆。控制上限是一个必要的保护措施。

5.3 第三个问题:不是所有场景都适合 Code Mode

尽管 Code Mode 在灵活任务上表现神勇,但它不是万能药。低延迟的轻量级交互场景,比如用户问“明天的天气怎么样”,你让模型现写一段代码然后跑起来,那纯属脱裤子放屁。Function Calling 在这种场景下更快、更稳,性能也更好。还有强约束的业务场景,比如调用支付 API、发送短信,这些操作涉及资金和安全,不可能让模型自由发挥去写代码,固定的工具调用反而更可控。

我目前的经验是,如果任务的逻辑可以提前穷举,就用 Function Calling;如果任务的逻辑是开放的、依赖数据的、需要灵活应变的,就用 Code Mode。不要搞“万物皆 Code Mode”,但也不要死守 Function Calling 不放。聪明的做法是两者结合:用 Function Calling 做稳定可控的基础操作,用 Code Mode 处理开放式分析和复杂编排。

5.4 第四个问题:工具注册和代码生成的协作

我实际用的方案是 Hybrid。系统里同时启用 Function Calling 和 Code Mode。模型先判断任务属于哪一类:如果需要调用固定 API,就走 Function Calling;如果用户的需求需要多步骤数据操作或复杂计算,就走 Code Mode。在 Code Mode 的执行环境里,模型也可以通过固定的功能函数去调用那部分已经定义好的工具。

这样做的好处很明显:稳定部分用 schema 约束,动态部分用代码自由发挥,两边都处在自己最擅长的位置。代码里甚至可以支持 JSON 格式的 Agent 工具调用,因为模型在代码里写一个call_tool("search_product", {"keyword": "..."})的过程,远比让模型试图“同时生成多个工具调用 JSON”稳定得多。

6. 一些实操体验和参数建议

6.1 执行循环的次数怎么控制

Code Mode 天然是一个多轮迭代的过程。模型写代码、跑代码、报错、改代码,可能循环很多轮。如果你不限制循环次数,遇到模型陷入死循环改不好的情况,资源会被白白消耗掉。我自己一般会设置最大迭代次数为 5 次,超过 5 次就返回失败并附上最后一次的错误信息,让上层逻辑决定怎么处理。

在模型参数上,temperature 建议设置低一点。写代码讲究确定性,我用 0.1 到 0.2 之间。如果你用 0.8,模型可能在同一问题上每次想出不同的解决思路,容易出现前一轮的修复把原来的正确逻辑也改坏了的情况。低温度可以让修复过程更收敛。

6.2 代码生成的上下文管理

还有一个容易被忽略的细节:每次把代码交给执行环境跑之前,最好先把模型生成的代码展示给用户确认。不是说要用户审批每一行代码,而是把代码作为一种可追溯的记录呈现出来。这样出了问题你知道模型到底做了什么,而不是黑盒操作。

我在实际产品里的做法是:界面左侧显示模型的“思考过程”,右侧显示它生成的代码和执行结果,用户可以直接看到模型每一步在干什么。这种方式比一个黑盒 Agent 更让人放心,也让 Code Mode 的“能力感”更强。

6.3 从“工具调用”到“代码执行”的心态转型

最后想说的是,Function Calling 和 Code Mode 之争,本质上不是哪个技术更高级,而是我们如何看待模型的角色。把模型当成一个“连接器”,你会很自然地用 Function Calling;把模型当成一个“执行者”,你会走向 Code Mode。我个人的体会是,随着模型代码能力的持续增强,未来会有越来越多的工作流直接交给模型以代码的形式完成,工具注册和 schema 维护会逐渐变薄。

我自己在切换到 Code Mode 之后,最明显的变化是:我不再为“每个新需求都新增一个工具”而焦虑。用户来一个从来没见过的分析需求,我不需要去设计新工具,只需要把环境和数据权限给它,剩下的交给模型。这种从“给模型造工具”到“给模型一个环境”的转变,才是 Code Mode 让我真正觉得“新神”的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询