1. 为什么Function Calling开始显得力不从心
先说个背景。过去一年多,大家做LLM应用落地,几乎绕不开Function Calling这套东西。从早期的OpenAI函数调用,到各家国产模型跟进的结构化输出工具调用,本质上都是同一套逻辑:给模型一份函数清单,模型在回答时先输出一个结构化的调用意图,然后你的代码去执行对应的函数,再把结果拿回来继续交给模型。
这套机制解决过非常真实的问题。比如你做一个客服机器人,需要查订单、改地址、退换货,靠纯对话模型根本做不了实时数据的读写。Function Calling让模型有了“手”,能通过工具连接到外部世界。所以它一度被认为是LLM落地Agent的标配,业内聊起来都是“你要做Agent,先上Function Calling”。
但用的时间一长,问题就出来了。
最直接的问题是维护成本。每个业务动作都要预先写成JSON Schema格式的函数定义。字段既要完整,名字还得起得让模型“看得懂”,描述稍不到位,模型就会传错参数。等你定义了上百个函数,更新一个字段,牵一发动全身,调试起来比写业务代码还痛苦。
第二个问题是复杂逻辑根本塞不进函数。Function Calling适合的是“查询订单状态”“发送验证码”这类单个动作。可现实中的任务往往是一连串的步骤:先分析用户意图,再并行查多个数据源,然后根据结果生成筛选条件,最后还要比较两批数据的差异。这种流程如果拆成多个函数,模型就得来回调用好几次,每次调用都有失控的风险,上下文也会被中间结果塞得越来越满。
第三个问题更微妙:模型有概率“幻觉”出一个不存在的函数名,或者参数类型完全对不上。你当然可以在代码层做校验和重试,但做得越多,这套系统的复杂度越接近一个手写的运行框架,离“智能”越来越远。
我并不是说Function Calling一无是处。它在结构化场景、强约束场景下依然有效。但过去几个月,我越来越多地转向另一种做法,让模型直接写代码,而不是让模型调用我写好的函数。这种思路业内通常叫Code Mode,也就是代码模式。
第一次在真实项目里尝到甜头,是有一次要做一个批量数据清洗的活儿。用Function Calling的话,我得先把“去重、格式化、缺失值处理、异常检测”这些操作全定义成函数,还要写清入参出参。而改用Code Mode之后,我直接把数据样例和清洗要求丢给模型,让它生成一段Python脚本,我在沙箱里跑一遍,有问题就把报错信息贴回去让它改。整个流程从定义十几个函数变成了三轮对话。
从那之后,我就开始认真琢磨这两个方向的分界线。今天这篇就把我的实际体验和踩坑记录完整写出来,重点聊聊Code Mode为什么在不少场景里比Function Calling更好用,以及什么情况下你还是要老老实实回去用函数调用。
2. Code Mode到底换了个什么思路
2.1 从“调用我写好的函数”到“帮我写一段代码”
Code Mode的核心思路,是把模型从“参数选择器”变成一个“程序员”。
Function Calling的工作方式是:我预先定义了能力边界,模型在我的边界里做选择。它输出的是一个符合Schema的JSON,里面写着“我想调用哪个函数、用什么参数”。真正干活的是我写好的那部分代码。
Code Mode的工作方式则完全不同:我告诉模型目标、约束条件和可用数据,模型直接产出一段可执行代码。这段话不是JSON,不是结构化指令,而是一段真正的Python或JavaScript脚本。我的系统负责把它放进沙箱里执行,然后把执行结果或者报错信息再喂给模型。
用一句更直白的话总结:Function Calling是让模型按我的规则走,Code Mode是让模型自己定规则。
这个区别带来最直接的好处,就是表达能力的质变。函数调用的表达能力,被函数清单的颗粒度锁死了。我想让模型做一次“按月份聚合销售数据并计算环比增长”,如果走Function Calling,我需要预先定义一个聚合函数,还要约定好几个参数。如果需求变了,比如改为按周聚合,函数定义就得改,模型也得重新理解。而Code Mode下,这只是自然语言一句“按周再算一次环比”的事,模型会直接生成新的Pandas重采样代码,连改带跑,一次完成。
我最早接触这个思路是在看一些国外开发者分享他们用LLM做数据分析工具原型的视频。演示者没有定义任何工具函数,只是把CSV文件路径和一句“给我按地区分析一下销售趋势”丢给模型,接下来就看到生成了一段完整的数据处理脚本,跑完还自己画了图表。我当时的第一反应是:这不就是把Function Calling的整个中间层都省掉了吗。
省掉的确实是一整层“胶水代码”。你不需要再维护函数Schema,不需要写一堆解析JSON的校验代码,不需要处理“模型想表达复杂逻辑但你的函数表达不了”的尴尬。模型直接面对最终目标,用代码这种几乎是最高密度的指令语言来表达它的行动计划。
2.2 代码本身就是最好的“思维链”
还有一个更底层的因素值得说清楚。很多人以为模型直接生成代码只是换了个输出格式,但其实这触及了模型能力释放方式的变化。
大模型在训练阶段看过海量代码。你让它输出JSON格式的函数调用,它的能力只表现在“从有限选项里挑一个”,这其实是把绊马索绑在了模型腿上。你让它直接写代码,等于把它训练时的看家本领全部释放出来了。
我自己的观察是,模型在生成代码时,会展现出一种类似“思考过程”的东西:先写导入库的语句,再定义中间变量,再写核心处理逻辑,最后连输出格式都考虑到位了。这个链路本身就是一个可执行的、逻辑完整的问题解决方案。而Function Calling的输出永远是一小段JSON,它没有空间也没有能力去表达中间的推理过程。
这是Code Mode最迷人的地方:它把“思维”变成了可运行、可检查、可调试的东西。你不仅能看模型得出的结论,还能看到它是用什么代码路径得出这个结论的。出了问题,你可以直接读代码找原因,而不是去猜模型脑子里那点参数选择逻辑。
当然,这也带来了新的风险。模型写的代码不一定对,可能语法有误,可能有逻辑漏洞,甚至可能干了你不希望它干的事情。所以Code Mode从来不是“生成完就结束”,它必须和可靠的执行沙箱、充分的安全约束、以及有效的错误反馈回路搭配在一起。这部分我后面会详细讲。
3. 从Function Calling迁移到Code Mode:一次完整任务的实际推演
3.1 同一个任务,两种实现方式
理论说再多,不如实打实拿一个任务对比。我找一个这段时间真实做过的场景:给你一批销售订单Excel表格,要求按业务员分析退货率,并把退货率超过10%的业务员名单、各自退货金额和主要退货原因提取出来。
如果用Function Calling来做,我大概需要定义这么几个函数:
load_excel_data(file_path): 读取Excel数据filter_by_column(column, condition): 筛选特定条件数据group_by_and_aggregate(group_col, agg_col, agg_func): 分组聚合calculate_return_rate(order_amount, return_amount): 计算退货率get_high_return_merchants(rate_threshold): 筛出高退货率人员
然后模型要按顺序调用这几个函数好几次,我要在代码里保存每次的中间状态,最后还要用一个函数汇总结果。这个流程不仅繁琐,而且非常脆弱。只要模型哪一步调错了参数,整个链条就断了。
如果用Code Mode来做,流程简单得让人不太习惯:
- 我把Excel的列名和几行样例数据发给模型
- 我给出目标:“按业务员分析退货率,退货率=退货金额/销售金额,列出超过10%的人员”
- 模型生成一段十几行的Pandas代码,读表、分组、计算、筛选、输出一站式完成
- 我在沙箱里执行,核对结果
后面这个流程里,我不需要预判模型要做什么操作,也不需要提前为每一种可能的分析需求准备函数。模型根据我的请求,自由地用Python来表达整个分析过程。
3.2 中间有一步报错了怎么办
有人可能会问:模型生成的代码如果第一次就报错,效率会不会反而更低?
这个问题的答案是:确实可能报错,但修复成本远低于你想象。我这里举一个实际发生的例子。那次让模型分析退货率,它第一版代码里用了pd.read_excel(),但我的环境里没装xlrd库,直接抛了ImportError。走Function Calling流程的话,我得去检查是哪一步调用没生效、参数对不对,很可能还得翻日志。而Code Mode的反馈回路极其简单:我把报错信息原封不动粘给模型,补一句“环境里缺少这个依赖,用openpyxl方式读取”,它立刻生成第二版,改用pd.read_excel(..., engine='openpyxl'),跑通了。
这类“报错—反馈—重试”的循环,是Code Mode最有生产力的时刻。因为它把调试变成了对话,而不是人肉翻代码。模型基于错误信息来修正自己的输出,一次不行两次,两次不行三次。在我实测的项目里,一轮就通过的比例大概在六成左右,两三轮内通过的接近九成。
不过这里有一个必要的前提:生成代码和执行代码之间,必须有稳定的中间层帮你把报错信息传回给模型。如果只是人工复制粘贴,处理一两个问题是没问题,一旦要批量处理几十个数据文件,你就得把这套循环自动化。
我现在的做法是搭了一个很轻的执行器:模型生成代码,我固定往一个临时目录里写,然后subprocess跑一个受限的Python进程,把标准输出和标准错误全部捕获,作为下一轮对话的输入。这样模型自己就能看到它的代码跑了什么结果、报了什么错,然后自主修正。这一套自动化循环跑通之后,基本就没人愿意再回去维护那堆函数Schema了。
3.3 数据隐私、资源和失控风险
当然,Code Mode不是没有代价。最大的代价就是:让模型写代码,意味着你要在一个自动化的循环里执行一段不可完全预知的程序。这里面至少有三类风险是必须提前处理的。
第一类是数据隐私。你把任务数据喂给模型、让模型生成代码,这个过程中数据已经流向了模型接口。但需要注意的是,即使你用的是自部署模型,代码执行时对数据的读取和操作路径也要受控。我的建议是,给执行器一个完全隔离的临时目录,只挂载任务相关的输入文件,禁止访问其他路径。
第二类是资源开销。模型生成的代码可能极其低效,比如用双重循环处理十万行数据,或者往内存里塞巨大对象。如果执行环境不设资源上限,一个看起来平平无奇的清洗任务就能把你的服务拖垮。我的执行器里固定加了CPU时间限制、内存上限和最大输出长度,某一步超过阈值就直接中断并把超限信息反馈给模型让它重写。
第三类是模型的失控操作。比如模型生成一段代码,里面调用了shutil.rmtree("/")这类危险动作,或者尝试删除临时目录之外的文件。应对方法是:把执行环境做成沙箱,不只有Python层面的限制,还在系统层面用标准用户权限跑,必要时加容器隔离。
这三个层面的防护,属于从Function Calling迁移到Code Mode时必须补的功课。Function Calling天然安全一些,因为模型只能点你给它准备的按钮。Code Mode相当于你让一个实习生直接上手操作电脑,你当然得先把不相关的东西都锁起来。
4. 我的实际选型经验:什么任务该用谁
4.1 最适合Code Mode的场景
用了一段时间之后,我总结出了一套判断标准。如果你符合下面这些特征,Code Mode大概率比Function Calling省心得多。
第一个特征是任务需要“多步推理和灵活操作”。一切和数据处理、文件转换、统计分析沾边的事情,都是Code Mode的舒适区。你不需要预先知道用户会提什么分析需求,模型会用代码现场发挥。想想看,如果要在Function Calling里实现“任意分析需求”,你根本定义不完函数。
第二个特征是任务需要处理复杂返回内容。数据分析的返回结果可能是表格、图片、多维数组,这些用结构化JSON钳制反而难受。代码模式下,模型可以直接生成图表的base64编码字符串、生成Markdown表格、生成嵌套字典,返回什么格式都由当前任务说了算。
第三个特征是任务本身有明确的“正确答案”。比如写一段网络请求、解析一份JSON、批量重命名文件,这些任务的结果是可验证的。有可验证性,就可以用自动反馈循环来迭代,模型越改越准。
4.2 仍然该坚守Function Calling的场景
Code Mode并非万能,下面几种情况我建议保持Function Calling的架构。
第一种是强交互的流式对话系统。比如语音助手,用户说一句话,系统需要在几百毫秒内响应。让模型现写代码再执行,这个链路太长。而Function Calling加预置函数,响应时间可以压得很低,因为逻辑早已固化在服务端代码里。
第二种是涉及高价值或高风险的写操作。比如支付、转账、修改数据库、发送消息。这些操作绝对不能交给模型自由发挥生成代码,哪怕一次也不行。正确做法是用Function Calling点对点调用那些经过了充分测试的、权限受限的API。你要的是“确定”,不是“灵活”。
第三种是需要精细权限控制的内部系统。代码模式很难精细到“某个用户只允许执行这几类操作”。Function Calling的函数清单天然就是权限边界:没定义的函数,模型想调也调不了。这一点在合规性要求高的场景里非常重要。
第四种是极高频的重复任务。当任务已经被完全摸透了,每一步的输入输出都清清楚楚,根本没有必要让模型每次重新发明一遍轮子。这时候直接写死业务流程,把参数入口留出来,就是最优解。Code Mode的“自由发挥”这时候反而变成了不确定性。
4.3 混合架构才是当前的最优解
实际项目里,我越来越倾向于一个混合架构:拿Code Mode做分析和灵活探索,拿Function Calling做受控执行。大体的分工是,用户提一个复杂诉求,模型先写代码去数据层里取数、清洗、聚合、分析,得到一个可靠的结果之后,如果这个结果需要触发某个业务流程动作(比如发起退款、创建工作项),再通过Function Calling去调用那个受控的API。
这个分工的合理性在于:分析探索阶段,需要的是灵活和表达力,这是Code Mode的价值区;而最终动作阶段,需要的是安全和确定,这是Function Calling的价值区。让每个环节用它最擅长的方式,整体系统的可靠性和灵活性才能兼得。
我最近在做的内部数据助手就是这个架构。用户可以让它“对比这两个渠道近三十天的转化率差异,如果转化率差超过五个百分点,就创建一个跟进任务”。前半段的对比分析是模型写代码完成的,后半段的创建任务是走固定函数完成的。整体跑下来,用户感觉非常自然,技术侧也不觉得难维护。
5. 把Code Mode落地时必须处理的三个细节
5.1 沙箱执行器怎么搭才算够用
很多开发者第一次尝试Code Mode,都是直接在本地让模型生成代码然后exec()。在原型阶段这样没问题,一旦要往生产环境走,沙箱这关必须认真过。
我目前的执行器是一个Python进程,启动时带着几个强制参数:timeout=30(CPU超时)、memory_limit=1GB(内存上限)、max_output_chars=20000(输出截断)、workdir限定在临时目录。代码写入一个随机命名的.py文件,然后用subprocess.run去跑,捕获stdout和stderr。
网络访问默认禁止。因为让模型写代码去访问公网,等于把安全边界交给了模型的判断力,这不太可靠。某些需要联网的任务,我单独配置一个HTTP代理白名单,只允许访问预设的API域名,其他一律拦截。
文件系统方面,临时目录里的文件允许读写,但目录之外只读。如果任务真的需要读取某个特定路径,我会以环境变量方式把路径传进去,而不是让代码自由拼接路径。
这一套配置下来,牺牲了部分灵活性,换来了晚上能睡得着觉。很多初学者不太理解为什么搞这么复杂,直到某次模型生成了一段删文件的代码,才体会到沙箱的价值。
5.2 反馈回路设计:让模型自己修bug
Code Mode的体验上限,取决于你的反馈循环做得多好。最粗糙的版本是人看到报错,复制过去让模型改。好一点的版本是系统自动把报错带回去。更好的版本,是不仅带回报错,还把执行结果、甚至数据摘要信息一并带回。
我现在的做法是,执行完一段代码之后,回传给模型的不是简单的“执行成功”或“报错信息”,而是一个结构化的执行报告:代码是否运行成功、退出码、运行时长、内存消耗、输出前N行、错误堆栈、以及自动提取的数据概览(比如DataFrame的shape和前五行)。模型拿到这些信息,自己就知道下一步该怎么改了。
这个设计有个很实际的好处:模型生成的代码往往不止跑一遍,而是会跑好几遍。如果有一次执行失败,模型能看到自己和上次生成的代码有什么区别,修正起来更精准。它还学会了自己加打印语句来做中间检查,这让我觉得它真的在“调试”而不是碰运气。
反馈循环能跑通之后,我对模型生成代码的初始准确率要求反而降低了。即使只有五成把握,经过两三轮反馈修正,最后也能得到能跑的代码。这个“以反馈换准确率”的模式,就是Code Mode最核心的生产力来源。
5.3 别怕模型写出来的代码风格烂
最后说一个心理关。很多人看到模型生成的代码会难受,变量名是a、b、temp,注释写得像小学生作文,甚至有人格分裂式的无用导入。这在工程评审里是要被打回去重写的,但在Code Mode的执行链条里,代码风格真没那么重要。
代码只是模型执行任务的中间载体。只要它逻辑正确、结果可靠,哪怕写得丑一点,也是可以被接受的。我见过有同事非得强迫模型把变量名改成有意义的,折腾了半天,结果模型改了变量名之后引入了一个很隐蔽的bug。从那以后我就想明白了:别跟模型写的代码较劲风格问题,把注意力放在结果验证上。
当然,如果你想把生成代码沉淀成正式项目的一部分,那情况就不同了。这种时候我一般只把模型生成的代码当草稿,人在这个基础上重构一遍再入库。但纯落地的任务流程里,执行完、验证完、拿到结果,代码的生命周期也就结束了。够用就行,别追求完美。
6. 我的一线观察:这个转变意味着什么
6.1 开发的姿势变了
过去搭LLM应用,开发的焦点在接口设计上。你得把业务能力拆解成一个个可被模型调用的函数,这实际上是让业务去迁就模型的接口约束。Function Calling用久了,你会不自觉地把业务问题简化成“模型能调用的操作”,而不是“用户真正想要的结果”。
Code Mode把这个姿势掰过来了。开发焦点转移到了环境搭建、数据权限、安全沙箱、反馈机制这些更底层的设施上。业务问题不需要预先拆分,模型可以直接用代码语言去理解并执行。你做的事情更像是在给一个远程程序员搭工位:配好机器、给好数据、设好权限、盯紧输出。给他一个大方向,他会自己想办法。
这个姿势的变化,我个人认为比某个具体工具的兴衰重要得多。它代表LLM应用开发从“约束模型”逐步走向“释放模型”,从“给模型划好格子跳”走向“让模型自己找到路”。
6.2 仍然要警惕的边界
不管Code Mode多好用,有一点我得反复提醒:代码生成能力的增强,不代表模型可靠性的增强。模型写出来的代码依然可能在逻辑上完全没报错,但结果就是错的。
我之前遇到过模型生成了一段数据去重代码,语法完全正确,跑起来也不报错,但去重逻辑用了默认参数,把不应该合并的相似记录合并了。执行器不会发现这种错误,因为它只看运行结果有没有抛异常。要发现它,只能靠人对输出结果做抽样核验。
所以做Code Mode应用,无论如何都要保留人工复核节点。尤其是结果会影响决策、影响用户利益的场景,脑子里的那根弦不能松。机器写的代码跑得再流畅,也不等于它干得对。
6.3 下一步我打算怎么用
目前我手上的项目已经全面转向Code Mode优先,Function Calling退居二线只负责最终动作的执行。颠覆Function Calling的不是一个更聪明的模型,而是一种更聪明的使用方式:让模型用代码写思维过程,而不是用JSON写选择题。
下一步我准备把反馈回路再加强一些,加入自动测试断言机制。简单来说,就是让模型在生成代码的同时,也生成几个针对核心逻辑的断言用例,执行器在跑完代码之后自动验证这些断言。这样一来,不光语法层面有反馈,逻辑正确性也有了一层自动校验。等于给这个“远程程序员”配了个自动测试师。
这个方向如果跑通了,Code Mode的实用价值还能再上一个台阶。到时候函数调用在数据分析这个领域,可能就真的只剩历史意义了。