稀土掘金 稀土掘金

❤️ GitHub Copilot 读心术揭秘,Copilot 逆向工程笔记

总览

你是否好奇 GitHub Copilot 如何知道你想写的内容?有时候它聪明得甚至好像读过你项目里其他文件一样,不要怀疑,它确实读过。这篇文章记录了我阅读一个对 Copilot 的 逆向工程的笔记,一言以蔽之,Copilot 使用了 Jaccard 相似度获取用户最近访问过的页面里与当前编辑内容最相似的代码片段,并将其作为注释内容加入 prompt 中送给代码生成模型,以下是更加展开的讲解:

总体来说,copilot 分为两个部分:

  • 客户端:VS Code 扩展收集你键入的任何内容,拼装成 prompt,并将其发送到类似 Codex 的模型。无论模型返回什么,它都会显示在编辑器中。
  • 模型端:类似 Codex (现在或许是更先进的版本)的模型接受提示并返回完成提示的建议。

prompt 工程

prompt 示例

先看一个拼装好的 prompt 示例:

{
  "prefix": "# Path: codeviz\app.py\n# Compare this snippet from codeviz\predictions.py:\n# import json\n# import sys\n# import time\n# from manifest import Manifest\n# \n# sys.path.append(__file__ + "/..")\n# from common import module_codes, module_deps, module_categories, data_dir, cur_dir\n# \n# gold_annots = json.loads(open(data_dir / "gold_annotations.js").read().replace("let gold_annotations = ", ""))\n# \n# M = Manifest(\n#     client_name = "openai",\n#     client_connection = open(cur_dir / ".openai-api-key").read().strip(),\n#     cache_name = "sqlite",\n#     cache_connection = "codeviz_openai_cache.db",\n#     engine = "code-davinci-002",\n# )\n# \n# def predict_with_retries(*args, **kwargs):\n#     for _ in range(5):\n#         try:\n#             return M.run(*args, **kwargs)\n#         except Exception as e:\n#             if "too many requests" in str(e).lower():\n#                 print("Too many requests, waiting 30 seconds...")\n#                 time.sleep(30)\n#                 continue\n#             else:\n#                 raise e\n#     raise Exception("Too many retries")\n# \n# def collect_module_prediction_context(module_id):\n#     module_exports = module_deps[module_id]["exports"]\n#     module_exports = [m for m in module_exports if m != "default" and "complex-export" not in m]\n#     if len(module_exports) == 0:\n#         module_exports = ""\n#     else:\n#         module_exports = "It exports the following symbols: " + ", ".join(module_exports)\n#     \n#     # get module snippet\n#     module_code_snippet = module_codes[module_id]\n#     # snip to first 50 lines:\n#     module_code_snippet = module_code_snippet.split("\n")\n#     if len(module_code_snippet) > 50:\n#         module_code_snippet = "\n".join(module_code_snippet[:50]) + "\n..."\n#     else:\n#         module_code_snippet = "\n".join(module_code_snippet)\n#     \n#     return {"exports": module_exports, "snippet": module_code_snippet}\n# \n# #### Name prediction ####\n# \n# def _get_prompt_for_module_name_prediction(module_id):\n#     context = collect_module_prediction_context(module_id)\n#     module_exports = context["exports"]\n#     module_code_snippet = context["snippet"]\n# \n#     prompt = f"""\\n# Consider the code snippet of an unmodule named.\n# \nimport json\nfrom flask import Flask, render_template, request, send_from_directory\nfrom common import *\nfrom predictions import predict_snippet_description, predict_module_name\n\napp = Flask(__name__)\n\n@app.route('/')\ndef home():\n    return render_template('code-viz.html')\n\n@app.route('/data/<path:filename>')\ndef get_data_files(filename):\n    return send_from_directory(data_dir, filename)\n\n@app.route('/api/describe_snippet', methods=['POST'])\ndef describe_snippet():\n    module_id = request.json['module_id']\n    module_name = request.json['module_name']\n    snippet = request.json['snippet']\n    description = predict_snippet_description(\n        module_id,\n        module_name,\n        snippet,\n    )\n    return json.dumps({'description': description})\n\n# predict name of a module given its id\n@app.route('/api/predict_module_name', methods=['POST'])\ndef suggest_module_name():\n    module_id = request.json['module_id']\n    module_name = predict_module_name(module_id)\n",
  "suffix": "if __name__ == '__main__':\r\n    app.run(debug=True)",
  "isFimEnabled": true,
  "promptElementRanges": [
    { "kind": "PathMarker", "start": 0, "end": 23 },
    { "kind": "SimilarFile", "start": 23, "end": 2219 },
    { "kind": "BeforeCursor", "start": 2219, "end": 3142 }
  ]
}
  • 一个实际的案例如上图所示

    • 如果有后缀(suffix), 那么会启用插入模式 (模型使用 fill-in-middle 任务的 prompt),否则就是向后补全的模式

      • 根据 codex 官方:插入模式需要使用特殊 token 组装成正确的 prompt 格式,需要更大的 max_length,需要较大温度多采样几次直到 stop_reason 是 'stop',以便更好地连接到后缀代码
    • 可以看到,前缀(prefix)中除了当前光标之前的文档内容,还包含着项目中另一个文件的代码,该 # Compare this snippet from codeviz\\predictions.py: 行及其后续行指代的是与当前文档内容相似的代码片段。模型也就是通过这些相似片段加深对代码上下文的理解 thakkarparth007.github.io/copilot-exp…

🌟 准备 prompt 的过程

那么 prompt 是如何生成的呢,我们一步步来看看:

  1. Entry point:对给定文档和光标位置提取 prompt。 prompt 生成的主要入口点是 extractPrompt(ctx, doc, insertPos)
  2. 从 VSCode 查询当前文档的相对路径和语言 ID  getPromptForRegularDoc(ctx, doc, insertPos).

  1. 获取相关文档:借助 VSCode 的插件 API 等能力,拿到最近访问的 20 个相同语言的文件,参阅  getPromptHelper(ctx, docText, insertOffset, docRelPath, docUri, docLangId)

  1. 设置一些选项:

    1. suffixPercent:百分之多少的 token 用于后缀
    2. fimFuffixLengthThreshold:启用 fill-in-middle 的最小后缀长度,默认 -1,也就是后缀里面有字符就会启用
  2. 前缀计算:创建一个 Wishlist,添加不同的“元素”及其优先级,发生在  getPrompt(fs, curFile, promptOpts = {}, relevantDocs = [])

    1. 六种不同的元素:BeforeCursor, AfterCursor, SimilarFile, ImportedFile, LanguageMarker, PathMarker 

      1. languageMarker:e.g., #!/usr/bin/env python3 for python, or Language: 

      2. PathMarker:当前文件的 path

      3. ImportedFile:add local import context to the wishlist (似乎只对 typescript 使用)

      4. similarFile:Compare this snippet from path/to/file:

        1. getNeighborSnippers:找出符合条件的片段
        2. addSnippersToWishlist:遍历前者,加入 Wishlist
  1.  Wishlist 会按优先级和插入顺序排序,将元素添加到 prompt 中直到达到输入 token 数量限制  PromptWishlist.fulfill(tokenBudget)
  2. 一些选项,例如,NeighboringTabsOption 控制从其他文件中提取片段的力度。某些选项仅针对某些语言定义,如 LocalImportContextOption 仅针对 Typescript 定义。
  3. 后缀计算,只需要用光标后缀 token 填充到 token budget

🌟 片段提取,Jaccard 评分

继续往里看,重要模块:jaccard-scorer、window-matcher

从最近文档提取片段

getNeighborSnipates 主要的处理流程:忽略太长或空的文件 ➡️ 选最近 20 个文件 ➡️ 切片找出所有匹配的片段 (默认使用 bestMatch ,即每个文件找一个最匹配的片段) (注意:nbrMatcher 代表着正在键入的文件的类,每次和一个 curRelFile (相关文件)去匹配出代码片段 snippets 列表和分数)➡️ 滤掉分数低于阈值的 ➡️ 按分数排序 ➡️ 取分数最高的 n 个 matches ➡️ 格式化

nbrMatcher

默认情况下,nbrMatcher 使用  “Fixed window Jaccard matcher”,将给定文件(要从中提取片段)切成固定大小的滑动窗口。然后,它会计算每个窗口和参考文件(用户正在键入的文件,存在 this.referenceTokens中)之间的 Jaccard 相似性。仅从每个“相关文件”返回最佳窗口(尽管存在返回前 K 代码段的规定,但从未使用过)。默认情况下,窗口大小是 60 行。

  • 使用的 nbrMatcher 类默认是 fixedxxxxxmatcher,构建该类时,使用用户当前键入的文件生成分词器和词元集合,以供 Jarccard 相似度计算;注意 getWindowsDelineadtions 输入的是文件内容列表,每行是一个列表元素,输出的也是列表,每个元素表示窗口的开始位置和结束位置,后面会用到;

  • fixedxxxxxmatcher 继承的原始类型,默认使用 findBestMatch,输入 e (待匹配的相关文件),按行切分成列表,另一边通过 retrieveAllSnippets 将 相关文件 切分成固定窗口的片段,按相似度得分降序排列,最后输出相似度最高的片段

  • 切分片段,计算相似度,并排序的代码如下,首先尝试获取缓存中的代码片段,如果缓存为空,对每行代码分词存入 u。注意外面的 e 和里面的 e 意义不一样(折磨。。。)

  • 分词类,缓存类

  • 计算 Jaccard 相似性
function computeScore(e, t) {
  const n = new Set();
  e.forEach((e) => {
    if (t.has(e)) {
      n.add(e);
    }
  });
  return n.size / (e.size + t.size - n.size);
}

Jaccard分数(Jaccard score),也称为Jaccard相似系数(Jaccard similarity coefficient),是一种用于比较两个集合相似性的度量指标。它衡量的是两个集合中共有元素占总元素数量的比例。

Jaccard分数的计算方法如下:

1. 定义两个集合的交集为共有元素的集合,定义两个集合的并集为两个集合中所有不重复的元素的集合。

2. Jaccard分数等于交集的大小除以并集的大小。

数学公式表示为:

Jaccard分数 = |A ∩ B| / |A ∪ B|

Jaccard分数的取值范围是0到1之间,其中0表示两个集合没有共有元素,1表示两个集合完全相同。分数越高表示两个集合越相似。

  • 另一个似乎未使用过的类: “Indentation based Jaccard Matcher”

模型调用

使用两个 UI 提供补全, (a) Inline/GhostText and (b) Copilot Panel

Inline/GhostText

Main module

  • 提供少量建议(1-3)快速响应
  • 清除快速输入时的抖动
  • 限制某些情况下的请求:如果用户位于一行中间,则仅当光标右侧的字符为空格、右大括号等时,才会发送请求。

通过 Contextual Filter 防止 poor requests

prompt 生成后,该模块会检查 prompt 是否足够好 “good enough”决定是否发出请求调用模型。该模块会计算基于一个简单逻辑回归模型计算 “contextual filter score”,如果分数低于阈值(默认为 15%),则不会发出请求。模型有 11 个简单特征,例如语言、以前的建议是否被接受/拒绝、先前接受/拒绝之间的持续时间、提示中最后一行的长度、光标前的最后一个字符等。模型权重包含在扩展代码本身中。 included in the extension code itself.

例如,以右括号结尾的 prompt 得分就远低于以左括号结尾的 prompt,因为前者一般是用户输入完成了,不希望模型还进行补全。(在插件里还使用了一个机器学习模型也是挺让人惊讶的的)

Copilot Panel

Main module,  Core logic 1,  Core logic 2.

不显示无帮助的补全

  1. 如果输出重复,建议被丢弃  repeatitive
  2. 如果用户已经输入建议了,丢弃该建议  already typed 

遥测

copilot 说:目前有 40% 的代码都是在 ai 帮助下写的?这是怎么测的呢?

如何评测哪些建议被接受?

在插入后的不同时间长度中检查被接受的建议是否存在于代码中;

精确搜索过于严格,因此测量编辑距离,如果插入和串口之间的单词级别编辑距离小于 50%,则建议被视为仍在代码中;

参考

thakkarparth007.github.io/copilot-exp…

玻璃钢生产厂家宜春玻璃钢雕塑供应商浙江常见商场美陈售价龙岩园林玻璃钢雕塑公司北京商场美陈图片上饶个性化玻璃钢雕塑生产厂家咸阳玻璃钢价值观雕塑自贡玻璃钢雕塑定制浙江走廊商场美陈报价凯里玻璃钢雕塑设计公司广东景区玻璃钢雕塑七台河动物玻璃钢雕塑公司美陈玻璃钢人物雕塑费用是多少唐河广场玻璃钢雕塑西安玻璃钢雕塑工厂定西玻璃钢景观雕塑价格广西玻璃钢牛动物雕塑厂家菏泽制作玻璃钢雕塑报价哪家好东莞玻璃钢花盆制品厂家直销江苏玻璃钢雕塑企业兰州广场玻璃钢雕塑泰州玻璃钢人物雕塑价格河南卡通人玻璃钢雕塑摆件杨浦区玻璃钢雕塑推荐阳光玻璃钢雕塑公司河源户外玻璃钢雕塑松原玻璃钢雕塑定制厂家宝丰玻璃钢雕塑加工厂家浙江抽象玻璃钢雕塑图片玻璃钢异形雕塑设计河北玻璃钢人物雕塑公司香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化