从Function Calling到Code Mode:让大模型直接写代码的实战与选型指南 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(..., engineopenpyxl)跑通了。这类“报错—反馈—重试”的循环是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进程启动时带着几个强制参数timeout30CPU超时、memory_limit1GB内存上限、max_output_chars20000输出截断、workdir限定在临时目录。代码写入一个随机命名的.py文件然后用subprocess.run去跑捕获stdout和stderr。网络访问默认禁止。因为让模型写代码去访问公网等于把安全边界交给了模型的判断力这不太可靠。某些需要联网的任务我单独配置一个HTTP代理白名单只允许访问预设的API域名其他一律拦截。文件系统方面临时目录里的文件允许读写但目录之外只读。如果任务真的需要读取某个特定路径我会以环境变量方式把路径传进去而不是让代码自由拼接路径。这一套配置下来牺牲了部分灵活性换来了晚上能睡得着觉。很多初学者不太理解为什么搞这么复杂直到某次模型生成了一段删文件的代码才体会到沙箱的价值。5.2 反馈回路设计让模型自己修bugCode 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的实用价值还能再上一个台阶。到时候函数调用在数据分析这个领域可能就真的只剩历史意义了。