James Tsang

James Tsang

A developer.
github
twitter
tg_channel

ARTS 打卡第 5 天

A:268. 丢失的数字#

给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。
示例 1:
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 2:
输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 3:
输入:nums = [9,6,4,2,3,5,7,0,1]
输出:8
解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。
示例 4:
输入:nums = [0]
输出:1
解释:n = 1,因为有 1 个数字,所以所有的数字都在范围 [0,1] 内。1 是丢失的数字,因为它没有出现在 nums 中。

打卡时间不多了,写得很匆忙性能不太好。

function missingNumber(nums: number[]): number {
  const maxLength = nums.length
  const allNums: number[] = new Array(maxLength).fill(0).map((_, index) => index)
  allNums.push(maxLength)
  const missNums = allNums.filter(num => !nums.includes(num))
  if (missNums.length === 1) {
    return missNums[0]
  }
  throw new Error('no result')
};

提交结果为:

122/122 cases passed (700 ms)
Your runtime beats 5.49 % of typescript submissions
Your memory usage beats 21.98 % of typescript submissions (45.8 MB)

官方的解法更加高效,还使用了哈希集合,因为哈希集合添加和查找元素的时间复杂度都是 O(1),因此更加高效:

function missingNumber(nums: number[]): number {
  const set = new Set()
  const n: number = nums.length;
  for (let i = 0; i < n; i++) {
    set.add(nums[i])
  }
  let missing: number = -1
  for (let i = 0; i <= n; i++) {
    if (!set.has(i)) {
      missing = i
      break
    }
  }
  return missing
}

先将每一个元素记录到 Set 中,之后按数值顺序与 Set 中已有的值进行比较,缺失的就直接被找到了。感受到了哈希集合的好处。

R:Exploring Generative AI#

由于大语言模型(LLM)最近的火热,作者对大语言模型也产生了好奇,想知道这对工作会造成什么影响,怎么在软件交付的实践中使用 LLM。这是她经过一番探索实践后所做的分享记录。

工具链#

对于一个仍在进行模式演进的技术,需要建立一个心智模型来理解它是怎么工作的,这有助于处理涌来的海量信息。它用来解决哪种类型的问题?解决问题需要拼凑出哪些部分?它们怎么组合在一起的?

工具怎么分类的:
在心智模型中对工具做以下分类来支持编程工作:

  • 任务类型:在上下文中更快地查找信息;生成代码;关于代码的推理(解释或者找出问题);代码转换(例如得到文档或图表)
  • 交互模式:聊天窗口;行内助手(比如 Github Copilot);命令行
  • Prompt 组成:用户从零输入;结合用户输入和上下文
  • 模型属性:模型是否用于代码生成的任务?能生成什么语言的代码?它是什么时候训练的,信息有多新;模型的参数量;模型上下文长度限制;模型内容过滤是怎么样的,由谁进行 serving
  • Hosting 方式:是否是商业公司 host 的产品;哪些开源工具可以连接到大语言模型服务上;自建的工具,连接到大语言模型服务上;自建工具进行微调,自建大语言模型 API

作者按照这些维度对 Github Copilot, Github Copilot Chat, ChatGPT 等进行了分析:

image

中位数函数 - 三个函数的故事#

这是一个关于生成中位数函数的故事,可以用来表明 LLM 辅助变成的用处以及局限性。

通常作者都是搜索 "中位数的 JS 函数" 来实现,这次她尝试使用 Github Copilot 来辅助。Copilot 首先生成了正确的函数签名,然后给了三种不同的实现。

第一种实现是:先 sort 排序,然后后取中间位置的数,如果是偶数序列则取中间两个数的均值。目标是实现了的,问题在于 sort 方法不是 immutable 的,会改变原数组的排序顺序,这可能引入一些很难排查的 bug。

第二种实现是:原数组 slice 后再用第一种实现,这就不会造成原数组变更了,因此没什么问题。

第三种实现是:原数组 slice 后直接取 Math.floor(sorted.length / 2) 的数,这在数组长度为偶数时会有问题,没有取中间两个数的均值。

从这三个函数的表现来看,理解我们写的函数在做什么,然后写一些充分的测试用例来测生成的代码还是很重要的。

使用 Copilot 生成代码和搜索然后复制粘贴不一样吗?搜索然后复制粘贴我们能知道这个代码的来源,从而通过一些比如 Stackoverflow 上的投票数还有评论来帮助判断粘贴的代码是否可靠,但 Copilot 生成的代码我们缺少判断依据。

那我们是生成测试用例、还是代码还是都生成呢?作者用 Copilot 生成了这个中位数函数的测试用例,效果确实不错,对于中位数函数这个复杂度的任务,她愿意用 Copilot 同时生成用例和代码。但对于更复杂的函数,她还是宁愿自己写测试用例以确保质量,同时更好地组织测试用例的结构,避免即使是把一部分内容给到 Copilot 去生成而引起的遗漏。

Copilot 能帮我修复生成代码中的错误吗?作者要求 Copilot 进行重构后,它的确给出了一些合理的建议,包括给 ChatGPT,ChatGPT 都能直接指出其中的错误,不过这一切的前提都是我知道这段代码仍需要改进和修正。

结论:

  • 你必须清楚地知道自己在做什么,才能判断生成的代码如何。就像上面例子中必须知道中位数是什么要考虑什么边界情况,才能得到合理的测试用例和代码
  • Copilot 自身是能够再次改进有问题的代码的,这是否意味着在使用 AI 工具时我们需要循环和 AI 进行对话
  • 即使对生成的测试用例和代码质量有所怀疑,但可以选择不采用它的代码,仅仅是用它生成的代码来帮自己覆盖测试用例遗漏的场景或者帮助推断自己写的代码

代码行内助手在什么时候更有用?#

对于行内代码助手,说有用说没有用的人都有,这取决于具体的上下文还有对它的预期。

“有用” 具体指什么?在这里有用是指用了它以后能以相当的质量帮我更快地解决问题,这不只是写代码的时候,也包括后续的人工 review 还有后续的返工处理、质量问题。

影响生成内容有用性的因素#

以下因素我只记录了相对客观的部分,但原文中的每一点作者都还阐述了相对主观的见解,感兴趣请阅读原文

更流行的技术栈

使用的技术栈越流行,模型中关于这个技术栈的数据集越丰富,这意味着 Java 和 JS 的数据比 Lua 更多。但也有同事在 Rust 这种数据不那么多的语言上也能取得不错的测试结果。

简单和常见的问题

简单常见的问题一般有下面几种例子:问题本身很简单;在上下文中比较常见的解决模式;模板化的代码;重复的模式。

这对经常手写重复代码的场景是有帮助的,但对熟悉了 IDE 的高级特性、快捷键、多光标操作的人来说,Copilot 帮他们减少的重复性工作意义就没那么大了,而且还可能减少大家重构的动力。

更小的问题

规模越小的问题越好 review 生成的代码,规模大了以后问题和代码都会更难理解,往往还要分成多步,这就增加测试覆盖不足的风险而且可能引入我们不需要的内容。

更有经验的开发者

经验丰富的开发者能更好地判断生成代码的质量然后高效使用它们。

更大的错误容忍度

判断生成代码的质量和正确性是非常重要的,但还有一个问题就是我们的场景里对质量和正确问题的容忍程度。对于错误容忍度高的场景,可以更高接受度地采纳它的建议,但对于比如安全策略的低容忍度场景,比如 Content-Security-Policy HTTP 头这样的内容,我们还是更难去轻易采纳 Copilot 的建议。

结论:
用行内代码助手是有适合用的地方的,但影响有用性的因素也有很多,使用它的技巧也不是通过一个训练课程或博客文章就能说清楚的。只有多用,甚至探索到有用的边界之外,才能更好地使用这个工具。

行内代码助手在什么时候会成为阻碍因素?#

上面说了 Copilot 有用的情况,那么自然就有没用的情况。比如:

扩大不好的或过时的实践

由于 Copilot 会参考关联上下文中的任何内容来做参考,比如打开的相同语言的文件,找一些相似的代码片段,因此它也可能把坏的代码示例搬过来。

还有比如我们想对代码库做一些重构,但 Copilot 还总是会采用老的模式,因为代码库里老模式的代码还是广泛存在的,作者把这种情况叫作 “中毒上下文”,这种情况目前还没有好的解决办法。

结论:
AI 希望通过代码库的代码来改善 Prompt 上下文既有好处也有坏处,这是开发者不再相信 Copilot 的因素之一。

Review 生成代码的疲劳

用 Copilot 意味着一遍又一遍地 review 生成的小块代码内容。通常在编程时的心流是持续不断第书写实现我们脑中的解决方案。用 Copilot 后中间就需要持续阅读、review 生成的代码,这是不同的认知方式,并且这种方式比持续地产出代码更缺少乐趣,这会引起 review 疲劳,感觉心流被打断。如果我们不处理这种 review 疲劳,我们可能就会对生成代码进行判断开始打马虎眼。

此外还可能会有一些影响:

  • 自动化偏见:一旦我们在生成式 AI 获得了良好的体验,我们就会过度信任它们
  • 沉没成本:一旦我们花时间用 Copilot 生成了一些局部有问题的代码,后续我们可能也更倾向于花 20 分钟用 Copilot 让这块代码工作,而不是自己花 5 分钟重写
  • 锚定效应:Copilot 给出的建议很可能会锚定我们的思维,让我们后续的思考都受它的影响。因此摆脱这种思维影响,不要被它锚定住也很重要

结论:
使用 Copilot 时不要被它限制思维很重要,也需要跳脱出它的限制,否则我们就会成为导航把我们导向湖里,我们就把车开进湖里的人。

代码助手无法取代结对编程#

虽然行内代码助手还有聊天机器人可以很大程度上像人一样和开发者进行互动,但作者还是不认为这种实践能取代结对编程。

认为编程助手和机器人能取代结对编程的看法可能是产生了一些概念性的偏差,以下是结对编程的优点:

image

编程助手能够产生明显作用的是上图中的第一个区域:“1+1>2”。它可以帮我们摆脱困难,更快启动和得到可工作的成果,让我们更加专注在设计整体解决方案上,同时也能共享给我们更多知识。

但结对编程不只是分享代码中的显示知识,还有这个代码库的演进历史等隐性知识,这在大语言模型中是获取不到的。此外结对编程还可以改进团队的工作流、避免时间浪费、让持续集成更加容易。它还帮助我们锻炼了沟通、同理心、给予和接受反馈等写作技能。它还在远程工作中给团队提供了宝贵的产生连接的机会。

结论:
编程助手只能够 cover 结对编程中的一小部分目标和好处,因为结对编程不只是帮助个人,还是帮助团队做整体性提升的。结对编程可能提高整个团队的沟通协作水平,改善工作流,增强代码的 owner 意识。此外结对编程还没有使用编程助手时遇到的上述提及的坏处。

使用 Github Copilot 的 TDD#

使用 AI 编程助手后,我们就不再需要测试了吗?TDD 是否过时了。为了回答这个问题,我们用 TDD 在软件开发中带来的两个帮助来测试:1. 提供良好的反馈;2. 解决问题时能采用分而治之的思想

提供良好反馈

良好的返回需要是快速和精确的。无论是手动测试、文档、code review 都没有单元测试能给的反馈快。因此无论是人工手写的代码还是 AI 生成的代码,都需要快速反馈来验证正确性和质量。

分而治之

分而治之是解决大问题的快速思路。这还让持续集成、基于主干的开发、持续交付都得以实施。

即使在 AI 生成代码的情况下,也还是采用迭代开发的模式,更重要的是,LLM 也有类似 CoT Prompt 可以提升模型输出的质量这种说法,这也正是 TDD 中所推崇的思想,这正是很好契合的。

使用 Github Copilot 做 TDD 的技巧#

  1. 开始

从一个空的测试文件开始不代表从空的上下文一开,一般都还会有相关的用户故事笔记以及和结对编程伙伴之间的讨论。

这些都是 Copilot “看不见” 的,它只能处理拼写、语法之类的错误。因此我们需要给他提供这些上下文:

  • 提供 mock
  • 写下通过验收的标准
  • 假设性指导:比如不需要 GUI、使用面向对象编程或者函数式编程

此外 Copilot 使用打开的文件作为上下文,因此我们要保持测试文件和实现文件都打开。

  1. Red

一开始先写一个描述性的测试用例名称,名称越具有描述性,Copilot 生成的测试代码表现就越好。

Given-When-Then 的结构能够从三个方面帮助我们:首先,它提醒我们要给出业务上下文;其次,它给了 Copilot 生成有表现力的用例命名的机会;最后,这能够让我们看出 Copilot 对这个问题的理解。

比如在写测试用例命名时,Copilot 帮我们不全的是 “假设用户... 点击购买按钮”,这意味着 Copilot 没有足够理解我们的目的,这就可以在文件顶部的上下文描述中加上 “假设不需要 GUI”、“这是一个 Python Flask 应用的 API 测试套件”。

  1. Green

现在可以开始实现代码了,一个已有的、富有表达性和可读性的测试用例可以最大程度地发挥 Copilot 的潜能。这时候 Copilot 有了更多可以发挥的输入,就不需要像婴儿一样自己走路了。

回填测试用例:Copilot 在这时很可能不会再 “小步走”,而是会生成大块的代码,这些代码很可能没有完整的测试用例。这种情况可以回去补充测试用例,虽然不是 TDD 的标准流程,但目前看出现太大的问题。

删掉重新生成:对于要重新实现的代码,让 Copilot 有效果最好的方式是删除实现并让它重写,因为有测试用例在,即使重写也相对安全。如果这失败了,删除内容并分步写出注释可能会有帮助。如果还是不行,可能就需要关闭 Copilot 自己来写了。

  1. Refactor

在 TDD 中重构意味着用增量改变去改善代码的可维护性和可扩展性,同时保持一切行为的一致。

对于这一点,Copilot 可能会有一些能力局限,考虑下面两种场景:

  1. “我知道想做什么重构”:通过 IDE 的快捷键和特性,比如多光标、函数提取、重命名等做重构可能比用 Copilot 重构更快;
  2. “我不知道从哪里开始重构”:对于小的、局部范围的重构 Copilot 是能够提供建议的,但大规模的重构建议 Copilot 还不能做到很好。

在一些我们知道要做什么,只是想不起语法和 API 的场景 Copilot 可以很好地帮助我们,写一点代码注释就可以自动帮我们完成本来要搜索才能搞定的事情。

结论:
俗话说 "garbage in, garbage out",这对数据工程师和生成式 AI LLM 来说都是一样的。在作者的实践中,TDD 保障了代码库的高质量,这些高质量的输入让 Copilot 取得了更好的表现,因此建议在使用 Copilot 时使用 TDD。

T:umami 网站访问统计分析#

一个网站访问统计分析的框架,可以自己部署使用。目前用的 xLog 博客就支持配置 umami,本站用了自己部署的服务。

S:阅读《李笑来:我的读书经验》笔记#

阅读两个最基本的价值观:
・需要认清现实并思考未来;
・更喜欢有繁殖能力的知识。


Reference:

  1. ARTS 打卡活动
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。