微信扫码
添加专属顾问
我要投稿
探索AI模型如何执行复杂代码,实现智能化任务。 核心内容: 1. AI模型的高级能力:理解意图并执行复杂指令 2. 实现Function Calling机制,让模型调用工具和运行代码 3. 后端系统如何解析并执行模型请求的Python代码
在开发 MolaGPT 的过程中,我始终认为,单纯的文本对话只是 AI 模型能力的一种表象。而真正激动人心的是模型能够主动理解意图,和人类一样制定计划,执行复杂指令来解决复杂问题,比如调用工具、生成图表、分析数据等。
要实现这一点,就需要为模型构建一套完整的“工具箱”,还需要有强大的语言模型本身具有调用工具的能力,而这些能力就需要配套的后端接口和稳定的结果输出。
这段时间,我终于有机会着手开发一个基于 Function Calling(现在貌似 Tools Calling)的 Agent. 我的目标很明确:首先,赋予模型联网搜索的能力;其次,也是更关键的,让模型能够运行 Python 代码。
手搓复刻 OpenAI 的 Function Calling
我的后端整体的核心思路借鉴了 OpenAI 的 Function Calling 机制。其原理可以概括为:当模型识别到用户意图需要借助外部工具才能完成时,它不再直接生成最终答案,而是生成一个结构化的 JSON 对象。这个 JSON 对象精确描述了需要调用的函数名称(例如 execute_python_code)以及执行该函数所需的参数(例如,一段 Python 代码字符串)。
这个结构化的请求随后被发送到我的后端系统。后端接收到这个请求后,会解析 JSON 内容,根据 name 字段匹配预先定义好的可用函数列表 ($available_functions),找到对应的处理逻辑(比如说 execute_python_code)。后端执行完任务后,再将执行结果(比如代码的标准输出或错误信息)返回给模型进行二次请求,随后模型结合上下文,生成最终回复给用户。
举个例子,如果模型判断需要执行 Python 代码 print(2+2),它会向后端发送类似以下的 JSON:
{ "name": "execute_python_code", "arguments": { "code": "print(2+2)" }}后端解析出 name 为 execute_python_code,提取 arguments 中的 code 字段 "print(2+2)",然后将其交给一个隔离的 Docker 沙箱环境中的 Python 解释器执行。执行完毕后,捕获其 stdout,并将 "4" 这个结果返回给模型。这通常涉及两次与模型的交互:一次是模型输出 Function Call 请求,一次是后端返回执行结果供模型参考。
例如,在我的后端实现中,我为模型定义了两个核心函数:
search_web:用于联网搜索最新信息。
execute_python_code:用于执行 Python 代码,也是本文讨论的重点。
这是后端定义的函数描述信息,用于告知模型这两个工具的存在和使用方法:
$available_functions = [ [ "type" => "function", "function" => [ "name" => "search_web", "description" => "在网络上搜索最新信息,获取权威内容 (Search the web for the latest information and authoritative content)", "parameters" => [ "type" => "object", "properties" => [ "query" => [ "type" => "string", "description" => "搜索查询内容 (The search query)" ] ], "required" => ["query"] ] ] ], [ "type" => "function", "function" => [ "name" => "execute_python_code", "description" => "运行一段 Python 代码,并返回标准输出结果 (Execute a snippet of Python code and return the standard output)", "parameters" => [ "type" => "object", "properties" => [ "code" => [ "type" => "string", "description" => "要执行的 Python 代码 (The Python code to execute, e.g., print(2+2))" ] ], "required" => ["code"] ] ] ]];
模型会根据对话上下文,智能判断是否需要调用这些工具,并自动生成相应的参数请求,交由后端执行。
过程中遇到的几个问题和解决
1. 模型并非总会使用 print()
在初步测试 execute_python_code 函数时,我发现模型生成的代码经常执行失败。查看 Log 发现,问题在于模型生成的代码很多时候没有显式地使用 print() 函数来输出最终结果,模型也许把后端的 Python 环境当做了Jupyter Notebook 这种交互式环境了。例如,当我让模型计算 2+2 时,它可能会生成如下代码并请求执行:
2 + 2## 或者result = 2+2result
在 Jupyter Notebook 等交互环境中,这么写确实没问题,可是我的后端是单纯的拉取 Python 镜像人为构建的 Docker 环境,Python 执行器运行这段代码之后,由于没有 print() 语句,标准输出就为空。这样一来,模型就无法接收到计算结果 "4",导致其认为代码执行失败。
我最初的想法很简单:是不是可以在接收到的代码字符串末尾自动加上 print(...)?但很快意识到这种方法过于粗暴。并非所有代码执行都需要打印最后一个表达式的结果,例如代码可能只是定义函数、导入库等等,确实不需要 print()的存在。强行添加 print() 可能导致语法错误或非预期的行为。
经过一番资料查阅和向大模型“请教”,我决定采用 AST(Abstract Syntax Tree,抽象语法树)来智能地处理这个问题。AST 可以将代码解析成一个树状结构,其中的每个节点代表程序中的一种语法元素,比如表达式、语句、函数定义等。这个树的根是整个模块,而它的子节点可能是赋值语句、函数调用、条件判断等结构。
使用 AST 的主要目的就是精确地表达代码的语法和语义,忽略掉注释、空行等非实质性内容。而不是像之前那样去暴力添加 print().
import astclass AutoPrintTransformer(ast.NodeTransformer):"""自动为最后一个表达式添加 print() 的 AST 转换器"""def __init__(self):super().__init__()self.modified = Falsedef visit_Module(self, node):"""处理模块节点,为最后一个表达式添加 print()"""self.generic_visit(node)last_expr_index = -1for i in range(len(node.body) - 1, -1, -1):stmt = node.body[i]# 判断是否为 print 语句is_print_stmt = (isinstance(stmt, ast.Expr) andisinstance(stmt.value, ast.Call) andisinstance(stmt.value.func, ast.Name) and stmt.value.func.id == 'print')is_docstring = (isinstance(stmt, ast.Expr) andisinstance(stmt.value, ast.Constant) andisinstance(stmt.value.value, str) and i == 0)if not is_print_stmt and not is_docstring:last_expr_index = ibreak# 找到了需要添加 print 的表达式if last_expr_index != -1:expr_node = node.body[last_expr_index]print_call = ast.Call(func=ast.Name(id='print', ctx=ast.Load()), args=[expr_node.value], keywords=[])print_stmt = ast.Expr(value=print_call)ast.copy_location(print_stmt, expr_node)node.body[last_expr_index] = print_stmtself.modified = Truereturn node
这段代码的主要执行流程为:
ast 库,通过 ast.parse() 将模型生成的代码字符串解析成一个 AST 对象。(node.body)。ast.Expr 的语句节点。print()print() 调用,或者是否是一个字符串常量。print() 函数调用节点中,并替换掉原来的表达式语句节点。ast.unparse() 将修改后的 AST 转换回 Python 代码字符串,或者直接编译并执行。通过这种方式,即使模型没有写 print(),我们的后端也能找到所有漏掉的位置并动态补齐 print(),确保计算结果能够被正确捕获并返回。
1.3 随后遇到的新问题
在上线一段时间后,我发现 PHP 在处理 Python 代码的引号和换行时存在严重问题。由于直接将 Python 代码塞进 Python 模板字符串时,经常因为代码内部含有特殊字符(如单引号、双引号、反斜杠等)或复杂的多行结构,导致字符串提前中断或发生语法错误。这是我始料未及的,因为之前做 PHP 开发时也没有直接在 PHP 代码中写 Python,没想到会有这种问题。
为彻底解决这一问题,我决定进一步优化:在 PHP 中先将用户的 Python 代码进行 Base64 编码,这样就能确保传递到 Python 环境中的字符串是原生的且不受特殊字符干扰。
1.4 现行解决方案:利用 Base64 来保持代码完整性,随后再利用 AST 自动补全 print()
后端在接收模型提交的 Python 脚本后,使用 PHP 的内置函数 base64_encode 对 Python 代码进行编码。
$b64 = base64_encode($user_python_code);
Python 侧模板不再直接使用三重引号夹入用户脚本,而是接收一个 Base64 字符串,然后通过 Python 的内置库 base64.b64decode() 将其还原成原始代码,再利用之前开发的 AST 自动补全 print() 的方法进行处理。
import base64, ast, tracebackclass AutoPrintTransformer(ast.NodeTransformer):def __init__(self):self.has_modified = Falsedef visit_Module(self, node):self.generic_visit(node)last_expr = Nonefor i in range(len(node.body) - 1, -1, -1):stmt = node.body[i]if isinstance(stmt, ast.Expr) and not (isinstance(stmt.value, ast.Constant) and isinstance(stmt.value.value, str)):last_expr = (i, stmt)breakif last_expr:idx, expr = last_expris_print = (isinstance(expr.value, ast.Call) andisinstance(expr.value.func, ast.Name) andexpr.value.func.id == 'print')if not is_print:print_node = ast.Expr(value=ast.Call(func=ast.Name(id='print', ctx=ast.Load()),args=[expr.value],keywords=[]))ast.copy_location(print_node, expr)node.body[idx] = print_nodeself.has_modified = Truereturn nodedef transform_code(src):try:tree = ast.parse(src)transformer = AutoPrintTransformer()new_tree = transformer.visit(tree)ast.fix_missing_locations(new_tree)if transformer.has_modified:exec(compile(new_tree, '<string>', 'exec'), globals())else:exec(src, globals())except Exception as e:print(f"处理代码时出错: {e}")traceback.print_exc()exec(src, globals())# 从 Base64 解码恢复原始用户代码user_code = base64.b64decode('<Base64_encoded_string>').decode('utf-8')transform_code(user_code)
上述解决方法上线后,避免了 PHP 与 Python 代码交互过程中字符串意外截断的问题,带有复杂换行结构的 Python 可正确执行。
为了增强代码执行能力,我在构建 Docker 镜像时特意预制了 matplotlib 库,期望模型能够生成数据可视化图表。在测试中,模型确实能生成标准的绘图代码(例如绘制正弦波),并且也调用了 plt.show(),但是代码执行后,前端聊天界面却看不到任何图像,不过想想也对,这是当然的,因为 plt.show() 会尝试在图形用户界面(GUI)环境中打开一个窗口来显示图像。但在 Docker 沙箱中没有显示器或窗口系统,纯纯就是无头的环境 plt.show() 会静默失败或报错。
有人会觉得(这个人就是我):那能不能在 Prompt 中让模型保存图片呢?可这就出现了一个新的问题:即使模型改为调用 plt.savefig('plot.png') 将图像保存到文件,这个文件也只是存在于容器内,无法被外界直接访问,更何况一旦代码运行完毕,容器销毁之后数据就会丢失。
前置代码注入:执行模型代码前,在 header 区域注入一段作为补丁的代码,追踪所有创建的 Figure 对象,并设置 matplotlib 使用非交互式后端:
import osimport uuidimport matplotlibmatplotlib.use('Agg') # 使用非交互式后端import matplotlib.pyplot as pltfig_list = [] # 用于跟踪所有创建的图orig_figure = plt.figure # 保存原始的 plt.figure 方法def track_figure(*args, **kwargs):"""寻找生成的图形。"""fig = orig_figure(*args, **kwargs)fig_list.append(fig)return figplt.figure = track_figure
后置代码注入:在代码执行结束后,遍历所有图像对象,将它们保存为 PNG 文件,透传到本地目录中,并将路径转换为公网可访问的图像链接:
...saved_paths = []if 'fig_list' in globals() and fig_list:output_dir = "/output" # 输出目录os.makedirs(output_dir, exist_ok=True)for i, fig in enumerate(fig_list):unique_id = str(uuid.uuid4())[:8] # 生成唯一 IDfilename = os.path.join(output_dir, f"chart_{i}_{unique_id}.png") # 构建文件名try:fig.tight_layout()except Exception:passtry:fig.savefig(filename, dpi=100, bbox_inches='tight') # 保存图像saved_paths.append(filename) # 添加路径到列表except Exception as e:print("出错!!")finally:plt.close(fig) # 关闭图形if saved_paths:for img_path in saved_paths:print(f"###IMAGE_FILE_PATH###{img_path}")else:print("没有图表。")plt.close('all'
模型处理逻辑:并使用着重符号 ###IMAGE_FILE_PATH### 来提示模型当前位置有图片,使用让模型可以理解的自然语言来输出 Markdown 格式的图表:

一系列处理后,模型所绘制的图像就能在前端被展示给用户了。
如上图所示,用户提出让模型绘制一个正四棱台,模型理解了用户的意图并绘制出了一个美丽的图像,通过后端预处理逻辑被成功透传到公网目录中。
3. 总结
不管温度参数设定如何,由于 Python 语法的等效性,模型依然可能会不使用 print();或是 matplotlib 图像不显示。这时候就需要开发者在后端设计好相应的预处理、执行和后处理机制,就能有效地弥补 Agent 开发过程中的局限性,为模型“擦好屁股”。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2025-11-19
LoAR做Fine-Tuning微调原理到底是什么?
2025-11-05
2张4090竟能本地微调万亿参数Kimi K2!趋境联合清华北航把算力门槛击穿了
2025-11-05
基于昇腾NPU的Qwen3量化因子代码生成微调实战
2025-10-21
从零教你微调一个专属领域大模型,看完小白也能学会炼丹!(完整版)
2025-10-14
用Macbook微调Qwen3!手把手教你用微调给Qwen起一个新名字
2025-10-12
微调已死?Google 和斯坦福论文指出AI 学习新范式
2025-09-25
如何将 AI 代码采纳率从30%提升到80%?
2025-09-24
一位淘宝工程同学的大模型LoRA微调尝试
2025-10-12
2025-08-25
2025-08-23
2025-10-14
2025-09-07
2025-09-04
2025-09-09
2025-10-21
2025-09-20
2025-09-24