v0.1
引言
我们接着讲Problem Solving。我们今天看第四章。搭建解决方案。换句话说就是实际的携带码开发,或者设计解决方案。今天这一章里面讲的东西比较碎一些,比较零散一些,反正就一个点一个点往下讲,不像是之前比较有结构性的。主要也就是在开发当中常常遇到的一些细节,稍微看到大家容易犯错的,或者容易踩坑的地方,给它整理一下大概过一下。
- 本章主题是“搭建解决方案”,关注开发实践中的常见问题。
- 内容以点状展开,覆盖多个开发中的易错细节。
第一个,one at a time,就是一次搞一个,或者说小步快跑。按照那个精益创业那种说法,小步快跑。这也可以说是在搭建解决方案当中一个比较通用的一个大原则。就是一次搞一个,一次不要搞太多,然后不要一口气吃成胖子。
- 强调“小步快跑”的开发原则,避免贪多求快。
- 每次专注解决一个问题,降低复杂性。
常见的反例的话就比如说,比如常见的比如说,开发一个什么功能的过程当中,顺便觉得说那里好像我发现有个bug,我顺便把那个bug一起改了。这就是一种反例。但是具体比率不好说,可能比如说五五开吧。五五开的情况下面可能50%,你还真是顺手就改掉了就还好。另外50%的情况就是顺手改着改着,你改出新bug来了。然后你本来是要实现那个功能的,然后整个就跑偏了。然后去改那个bug的时候,就改出了新bug。然后又去改那个新改出来的bug,然后整个就跑偏了。
所以这就是一种常见的坑。当然也不是说改bug不好,就是合适的时候去改一些bug也是合好,也是应该做的事情。就只是说一口气吃成胖子的时候,有时候会把事情搞复杂,简单的事情搞复杂。其实更多的时候,其实一个事情一个一个去解决就可以。然后真的遇到bug了,真的顺手想改那个bug的话,其实就记一笔。像我的话,我实际写代码的过程当中,我会我会旁边,我会开一个文档去记东西,就是我我会焦点在我要实际解决的那个问题上面。当然顺手如果发现有其他问题,我就在那个笔记里面记笔。然后我把这个问题解决完了,我回头再看我一下笔记,我设路了,刚才看到了一些什么其他这个问题,就改一下。
这是一个,其他的例子的话,其实后面的基本上都可以契合到这个大原则里面。所以我们一点点再再展开讲。
- 在实现功能时顺手修bug,可能导致偏离原任务。
- 50%成功率的“顺手改”存在较大风险。
- 建议记录非当前重点的问题,稍后集中处理。
然后这个原则其实背后主要的问题就是,因为实际开发到开发过程当中有很多不确定性存在。就是说或者说有很多的有很多的假设。就是我们以为自己是对的,但是其实可能会犯错的地方。
比如说写一个代码写的代码里面,可能会出错的问题是,代码本身能继承过来的代码,原始那个代码有没有问题。有可能在那个代码逻辑,或者说你对这个业务逻辑的理解是有问题。虽然有可能你业务逻辑的理解是没问题,但是写出来的那个代码是有问题的。就是有可能是写出来的代码,没有完全按照你理解的那个业务逻辑来实现。然后也有可能比如说是有语法错误的。然后也有可能是比如说,也对那个业务理解不准确的。所以你即便代码写对了,但是那个业务本身理解是错的话,那也是错的。
就是其实你一套代码写出来,虽然看上去你只是在写代码,但其实背后涉及到的东西方方面面有很多。然后包括比如说如果说要去调用什么第三方的库,第三方的API接口,这里面有可能说你的用法用错了。比如说那个接口是这样的,然后你理解成这样,但其实它应该是那样用的。这里面其实也可能犯错。然后甚至还有可能出问题的是,像比如说之前跟阿姆斯传播出的那个SenderGrid它的API,有可能是它的API接口它本身就有问题的,或者它的包本身就有问题的。可能不是你的问题是第三方的问题。
就是看上去只是写了一部分代码,但是其实背后有许多多有各种各样可能会出错的环节。所以一次只搞一个东西,最主要的目的就是为了缩小这种出错的可能性。就把那个同时出现两三个错,或者同时两三个地方出问题的情况下,尽量避免。尽量只是一次只出一个问题,然后一次只出一个问题,然后修那个问题,然后纠正那个问题就可以。
因为另外一种很常见的情况,或者很常见的可能就是,一段代码里面同时有两个bug,这种bug是最难修的。因为同时有两个,你修好了这个你发现你那个程序还是坏的。然后你修好了那个,你会发现这个程序还是坏的。要同时两个都修好,这个程序才能跑得起来。所以一旦出现这种一段代码里面有两个bug,或者有两个问题的情况,或者说另外一种可能性,就比如说刚才说的,可能是一方面你对业务理解本身就有偏差,那个业务逻辑理解本身有偏差。同时你可能,那个代码的语法可能也不太熟悉。这时候同时有两个地方可能出错的时候,你要去纠正那个东西是比较难的,因为可能出现你这个稍微纠正对了,但是因为那个错了,所以还是没跑通。然后或者说这方面对了,语法对了,但是逻辑还是不对。所以还是没跑通。
同时修两个或者多个以上的东西,就会极大的提高这个开发还有纠错的难度。OK,这个可以说是一个大原则吧。然后后面一些比较常见的,我自己整理总结出来的一些,最重要的,最佳实践吧,可以算是。
- 开发过程中充满不确定性和假设,易出错。
- 错误来源包括业务理解、代码逻辑、语法、第三方依赖等。
- 一次只改一个点,可以更清晰地定位并修复问题。
- 同时存在多个bug时难以判断根本原因,极大增加修复难度。
常见错误来源表
| 错误来源 | 描述 |
|---|---|
| 业务理解错误 | 开发者对业务逻辑理解有偏差,导致实现目标错误 |
| 逻辑实现错误 | 虽然理解正确,但代码逻辑未能正确反映业务需求 |
| 语法错误 | 编程语言层面的语法问题,导致代码无法正常运行 |
| 第三方接口误用 | 调用第三方API时用法错误,例如参数格式或调用方式不符合文档说明 |
| 第三方接口缺陷 | 第三方库本身存在问题,非开发者自身问题 |
一个是拿到一个问题呢,可以先先把它做简化。然后去做原型。这个其实也是符合刚才的说的,不要一口气吃成胖子,不用想着一步到位。
- 简化问题并制作原型有助于避免过度设计或卡在复杂实现中。
- 与“一次只做一件事”的原则相辅相成。
像这里举个例子的话,比如说我们现在要解决一个问题,要实现一个二分搜索算法。我不知道这边对那个算法熟不熟悉,或者有没有听说过,二分搜索这种东西。这种算法,这不光是算法,是算法和数据结构。然后再比如要用这个二分搜索去处理一个列表或者数组的排序。然后要用Python这个语言。然后一些其他的细节,比如说这样一个任务,不知道你这边,特别是你不熟悉的情况下,同时一下子要把这些东西全都做出来,其实是蛮有难度的。因为你一方面要去理解二分搜索是什么,另外一方面可能如果你对Python不太熟的话,你要了解一下Python的语法。然后这个sorted类似这个排序的列表,或者排序清单这个变量类型,你可能也需要熟悉一下。然后如果说再加上额外的要去做,要去输出日志的话,你可能还要再去熟悉一下,这个日志库怎么用,怎么去把它那个日志写出来等等之类的。
所以像这种问题一拿过来,第一步呢可能就是我们刚才说了嘛,是一次解决一个问题。当然如果你这些都很熟悉,你又熟悉那个二分搜索,你又知道Python,然后这些相关的日志库,你也都熟悉的话,那当然是可以一次性解决的。但如果不熟悉的情况下,去把它剥离出来,做原型和简化这个问题,就可以一定程度上帮助你,不至于一下子陷入很多问题,然后摸不着头绪那种情况。
- 不熟悉时,应将任务按技能模块(如算法、语法、数据结构、日志)分解处理。
- 原型设计是降低难度、快速验证思路的有效方式。
另一个举一个例子就是之前我们这边,有一个程序员,他遇到的问题就是说,布置他的一个任务让他,其实就是我们的那个CP Ad Manager系统,就是布置他的任务,写一段代码让每一笔交易,就是有客户充钱的时候,充值的时候,就在Telegram发一个提示,然后显示说有客户在那个系统里面充值了,这么一个功能。然后同时要去写对应的测试代码。所以当时他,遇到的问题就是卡在那边卡好久。但是拆开来看的话,其实当中又遇到很多问题。因为第一方面他,他可能不太熟悉那个框架,第二呢他不太熟悉业务逻辑,整个那个,CPI那边就是广告系统的一个业务逻辑,也不是特别熟悉。然后第三Telegram的接口,怎么去调用他也不熟悉。然后第四测试代码的这个框架,这个项目是Ruby的,测试代码框架其实就是RSpec和Cucumber,这些都不熟悉。然后他想要一次性给它做出来的时候,其实就遇到很多瓶颈很多卡点。特别是做到那个测试那一块的时候,他卡了好几周时间。
这种情况其实就是说,怎么去简化这个问题,特别是在有很多不确定性的情况下,有很多不熟的东西的时候,怎么去简化这个问题,把它按部就班的一个一个去,一部分一部分去解决。像这里比如说一个简单的拆分方式,或者说第一步就是说,或者也可以结合上面那个例子,那就是先比如说先去学一下,那个二分搜索。然后第一个例子的话,可能第一步先简化,这个核心可能就是二分搜索这个算法,我们先去熟悉一下二分搜索的算法,甚至不一定要一上来就用Python。比如说你不熟悉Python,那你熟悉别的编程语言,那你可以先用那个你熟悉的编程语言,去实现一下那个二分搜索算法,看能不能实现出来。又或者呢,比如说你可能也没有什么特别熟悉的,你可以去找一个教程,二分搜索的教程。然后你先跟着那个教程,这个简化这个问题先一步步来一点,讲完了之后再套回这个大问题里面。
同样的这个写这个Telegram的测试代码,情况也是一样,可能第一步就先简化这个问题,比如说我不熟悉这个测试框架,那我先不一定要测试Telegram的,这个发送信息的这个接口能不能测得通,我可能先测一个比较简单的一个API调用,能先看看这个东西能不能测得通。然后测完,把基本的那个原型的测试代码跑通了之后,然后再把它替换掉换成,换成比如说Telegram,那个实际去调Telegram接口,看它能不能测得通等等之类的。
OK像这一套其实时间有限,所以没有办法准备一个更具体的例子。下次有机会如果再讲这一章的话,我可能或者说我回头再去补的话,我可能会文字补一些东西,找一些更合适的例子,但是大概意思就是这样。一个特别是复杂的问题,或者说特别是不熟悉的问题,出来之后,先考虑怎么简化它,然后先考虑拿个原型给它做起来。
- 面对复杂任务时,逐步拆解为框架熟悉、业务理解、接口调用、测试框架等多个模块。
- 每个模块可独立学习、实验,再组合为完整方案。
- 原型设计有助于逐步探索并减少卡顿时间。
另外一种常见的原型就是说,比如说你要去用一个第三方的库,一口气吃成胖子的方法就是说,反正过了一下文档,然后就直接开始想办法把这个库套用,在现在的那个use case上面,那个应用场景里面。分步走的话就可以说是,比如说这个时候这个库我不熟悉,那我先用这个库,我先跟着他的教程走。然后跟着他教程里面的例子,先把那些例子全部实现一遍,然后先保证那个例子是能跑得起来。因为还有种情况就是可能,他那个库本身那个例子,他可能都给的是不对的。当然一般来说比较流行的,用的人比较多的库,不太会出现这种情况,但是越是那种小众的库,越是有可能出现这种情况。甚至有一些其实用的多的库,多多少少那个文档里面,可能也会有bug,他可能那个文档没有及时更新啊,什么之类的。所以文档里面的用法合适了,实际的那个用法,可能也是会有出入的。
所以这里面如果说,我们去给他抽离原型,给他做一个简化的话,有的时候简化的第一步就是,先把那个库用一下看看,按照他的那个例子用一下看看,看看首先这个例子跑得通跑不通,是不是对的。因为另一种就是很容易卡点的地方就是,就是他那个例子,你例子本身有问题,或者说你对那个例子的理解是有问题,或者说,他给的例子可能是这个版本的,但是他的库可能已经到另外一个版本了,或者说你装的那个库的版本,和你看到那个例子的那个库的版本,是不一样的。如果理所当然的以为,我就参考他这个例子,然后就写到,直接套用到我的代码里面,就可以一步跑通的话,可能80%的情况下是能跑通,但是剩下来那个20%就很坑了。你以为是没问题,然后一用一用发现,跑不通怎么调都跑不通。你就会出现这种情况。
所以虽然看起来好像省时间,你一步到位可以省时间,但反过来说的话,那剩下来的20%的情况被坑,被卡住的情况也是很讨厌的。所以反过来,如果先去用一下那个例子保证说,这个例子本身是能跑得通的,然后我当天装的那个版本的例子,也是能跑得通的。然后再去转化成那个实际的,我们应用场景里面的代码,这样虽然看起来是不错的,但是分两步走了,但是同时出现两个错误的几率就减小了。这是一个思路。
- 原型先行有助于验证第三方库是否可用,是否版本兼容。
- 示例代码可能存在版本差异、语义误解或文档错误,需逐步验证。
- 分步实现降低问题堆叠几率,提高集成稳定性。
使用第三方库失败的常见原因表
| 失败原因 | 描述 |
|---|---|
| 示例代码本身有误 | 第三方库附带的示例代码存在错误,导致运行失败 |
| 文档未及时更新 | 官方文档描述的用法与当前库版本不一致,造成使用上的偏差 |
| 库版本不兼容 | 示例代码与当前安装的库版本不匹配,导致接口或行为不同 |
| 示例逻辑理解偏差 | 开发者对示例代码中的意图或逻辑理解错误,造成使用方式错误 |
| 依赖项或环境差异 | 示例运行环境与本地开发环境不同(如依赖库、系统设置等),导致执行失败 |
然后另一个思路,实际开发还有写代码的时候,去用具体的例子给它带入进去,这里的例子和刚才那个例子还不一样。刚才说的例子是第三方的那种库的示例代码,这里说的例子更多是类似于测试用例的感觉。
- 本节聚焦于通过具体例子(测试用例风格)来演练复杂逻辑。
- 与第三方库文档中的示例代码不同,更注重业务逻辑推演与验证。
还是拿刚才那个CP Ad Manager我们那个系统来举例子。当时我们开发的前期,遇到了一个讨论蛮长时间的问题,就是处理那个CP Ad Manager充值系统里面账本计算的问题。因为一个账户里面会有好几笔账,一个是它整个充进去的钱,第二个是我们给它的coupon,比如说我们给它的credit,给它的额外的那些额度,它可以免费使用的,比如说试用的。像比如说用Facebook的话,一上来会给你比如500块钱,如果没记错500块钱的那个广告投放的一个免费的额度,可以投。一样的思路,我们这个系统里面有这个新注册的用户,有给它一个免费的额度,第二个有它充值自己充进去的钱,然后第三个,有当时考虑到有的客户可能是可以预付款,不是也不预付,是可以后付款,就可以先投广告后付款,相当于有一个信用卡的系统一样,我们可以给它一个额度,然后它可以先用完之后,30天之后再付钱。所以在这个场景底下,当时就需要梳理这个逻辑,就是当去扣款的时候,如果说一个人既有那个免费的额度,同时还充了钱,同时还有那个信用额度,可以后付款的信用额度,那么当他去购买一个广告campaign的时候,比如1000块钱的campaign的时候,这1000块钱从哪里扣。当时就遇到这个情况需要梳理这个逻辑。
那么这里就涉及到开发过程当中,去梳理业务逻辑。有的说业务逻辑比较复杂,当遇到比较复杂的业务逻辑的时候,比较好的一个拆分的方法就是小步快跑,先第一步先不要一下子吃成胖子,先不要一下子把那个很复杂的业务逻辑,一下子把它梳理清楚,而是给它拆分开来一小步一小步。先拿具体的例子,当时我们做的事情就是,拿了个清单,拿了个Google Sheet清单,然后把能想到的具体的情况给它一一列出来,比如说最简单的情况就是,假如说一个人他往里面充了1000块钱,然后其他额度都没有,那这个逻辑比较简单。如果就是充了多少钱用多少钱的话,那他就是如果他要扣一个500块钱的campaign,那就从1000块钱里面扣500块钱。如果要扣1000块钱的campaign,那就1000块钱全部扣完。如果要扣个1500的campaign,那1000块钱余额不足,那么就提示报错说那个余额不足。就这样一条一条给它梳理出来。但是从最简单的情况开始,然后到复杂的情况,当时比如说要考虑的最复杂的情况就是,既有免费的给到的额度,又充了钱,同时还有信用额度这种三个额度都有的情况。当然这里面那个逻辑就复杂了。我这边不具体展开,就这个例子就是想说,当涉及到要梳理业务逻辑,特别是复杂的业务逻辑的时候,一个分步走的办法就是说,拿一个具体的例子来walkthrough。
- 示例演练帮助分步拆解复杂业务逻辑(如扣款顺序)。
- 从最简单的充值情况出发,再逐步加入优惠额度、信用额度等条件。
- 推荐使用表格或清单将不同场景一一列出以便分析。
表格:下方为“CP Ad Manager 扣款优先级示例表”
| 情况编号 | 免费额度 | 充值金额 | 信用额度 | 购买金额 | 扣款顺序 | 是否成功 | 说明 |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 1000 | 0 | 500 | 充值金额 → 500 | 成功 | 余额充足 |
| 2 | 0 | 1000 | 0 | 1500 | 充值金额(1000) | 失败 | 余额不足 |
| 3 | 200 | 800 | 0 | 900 | 免费额度→充值金额 | 成功 | 总额足够,优先用免费额度 |
| 4 | 100 | 500 | 600 | 1000 | 免费→充值→信用额度 | 成功 | 涉及三种额度,逻辑较复杂 |
| 5 | 100 | 200 | 0 | 400 | 免费→充值 | 失败 | 总额度不足 |
同样的其实在我们初中高中的数学里面也常常,解决数学解题的时候也有类似的思路,就是类似于这种递归函数,就是什么像这种,像解这种题然后简化,然后求和或者求什么的。标准有那个什么等比数列等差数列,然后后来的话,高中数学会,是高中还是初中数学来着,会讨论更复杂的情况就是,非等比也非等差的但是那种,用递归表示的,什么什么F1等于10,Fn等于2分G的Fn-1,这个其实就是个等比数列,哪个时候会有更复杂的形式,就跟解中数学题思路也是一样的,一般来说老师会教就是说,第一步先别把问题想复杂,先带几个数进去,先把F1带进去算一下,再把F2带进去算一下,把F3带进去算一下,你先算个头五个数字,看一下看看有没有规律,或者看看有没有感觉。然后再去把它那个,再去把它那个抽象出来,然后去算它,到Fn的情况是怎么样的。我不知道大家这些高中,初中高中数学的东西还有没有印象,但是思路是一样的。其实在这里面,阶乘也是那样的,对对对,就是遇到复杂的这种,这种问题的时候,先带几个数字进去先,先看看先,先把那个例子作为,作为那个垫脚石,先给它试一下先找找感觉。然后通过这些具体的例子,能够梳理出来一个大概的业务逻辑,然后再去做抽象。不是说一上来就做抽象,当然遇到一些很熟悉的问题,或者说已经很有经验的方面的问题,的话那可以一步到位。但是更多的情况下面不太熟悉的时候,或者不是特别有把握的时候,先带入具体例子,然后用,脑袋里面去算一遍,然后再去简化,然后再去给它抽象,算法或者代码逻辑,这样也是可以,减小那个同时出错的风险。
说到这个就顺便提一下,这边的反例就是说,一步到位一步到位之后,就有可能以为自己,业务逻辑都梳理清楚了,然后写代码,然后最后可能就出现的是,其实业务逻辑还没梳理清楚。可能考虑了百分之九十的情况,可能百分之十没考虑到。然后同时又写了代码,代码里面可能又有一些语法错误也好,代码里面或者实现的逻辑有点问题也好,这时候就,也是刚才说的一样,有可能同时出现两个问题,然后这时候要调的时候就很头疼。所以尽量给它拆分开来,分步走。
- 数学解题中的“代入实例找规律”思路同样适用于编程逻辑梳理。
- 面对不熟悉的问题时,先跑几个例子,再做抽象,降低同时出错风险。
- 警惕一步到位的幻觉:业务逻辑与实现同时存在盲点时,调试成本剧增。
OK,然后第三个点,这个就是做抽象,或者说,这里其实涉及到两个方向,一个方向是做抽象,就是说刚才我们讲了这个,先拿点具体的例子给它,业务逻辑梳理一遍。然后梳理完之后写代码的时候,当然就不能用具体情况去写,不能说A等于1 B等于2,然后这样写。最后变量还得抽象出来成为变量,所以有一个抽象的过程,有一个那个,Generalization的过程,把具体的变成抽象的一个算法出来。当然另外一方面,就是遇到问题的时候,也需要有这个思路,就是能把具体的问题,也给它抽象成一个,更加笼统的问题。所以这里又是两个方向,一个方向是,有时候要把复杂的问题,要给它能够具象出一个具体的例子来。但反过来有的时候又需要把那个,具体的问题给它抽象出一个,更通用的一个场景出来。
- 抽象思维包括两个方向:
- 从具体例子中归纳出变量或通用结构。
- 将特定问题提炼为更广泛、通用的问题形式。
- 编程中常见的抽象化过程包括:变量提炼、函数提炼、通用组件封装。
然后这个目的呢,把它抽象出来的目的,就是为了更好地去搜索。当然现在的话呢,已经不太那么需要,因为现在有那个,有大语言模型了嘛,所以基本上,你拿具体的问题问,它也能给你回答的七七八八,差不多。但没有大语言模型之前,其实很多时候需要搜嘛,需要自己去搜,Google搜的时候如果拿着具体问题搜,就是有太特定的情境底下了。
- 抽象问题的目的是为了获得更好的信息检索效果。
- 与搜索引擎交互时,具体细节太多可能影响搜索匹配度。
- 即使是现在使用 AI 进行问答,清晰的问题结构依然有助于得到更精确的解答。
像这里,这里又简单放了个例子,就是说比如说怎么把,怎么把一张这个五乘三的表,给它转成,CSV的格式用Python转成CSV的格式比如说这样。像这个例子比如说五和三,这就是很具体的。如果五和三进去搜,去Google里面搜的话,你可能不一定搜得出来很合适的结果。你可能真的要去搜的时候,你可能就是简单搜,怎么把一张Table给它format成CSV的格式,怎么把一张Table给它format成CSV的格式,可能就是这样。把它去做抽象的话,应该就是这么问。当然其实应该有更好的例子,我一下子想不出来。有个红红人的,有个红红人的,有个红红人的,很多页面都要用的时候,我们就把它抽象出来,只是给它一个参数,对对对,对对对。这也是一种抽象出来的一种形式,这也是一种抽象出来的一种形式,具体的表现形式。所以这种抽象能力其实还是,方方面面都会用到。
- 示例说明:
- ❌ 低效关键词:「用 Python 把 5x3 表格转成 CSV」
- ✅ 高效关键词:「Python table to CSV」或「Python write CSV from 2D list」
- 抽象关键词有助于覆盖更多通用资料,避免受限于特定数值或场景。
图表:「搜索表达抽象层级图」,按具体性从高到低排序:
| 表达层级 | 示例关键词 |
|---|---|
| 极具体 | “把一个 5x3 表格转成 CSV 格式(Python)” |
| 中等具体 | “Python 将表格写入 CSV” |
| 抽象通用 | “Python export table to CSV” 或 “Python CSV writer” |
另外一个就是,我这边主要提的就是说,去搜索去找解决方案的时候,因为不一定我们代码全部自己写,写大部分情况下面,如果能想到,还是回到这个例子吧,比如说这个是个具体的例子,一个具体的案例。然后你要去搜解决方案的时候,假如说我不知道解决方案是什么,你要搜的时候需要给它做一个抽象。然后我就只是简单搜,用Python怎么去把表给它转成,CSV的格式就可以。然后同样的其他东西的话,需要,有那个感觉,但这个东西感觉也很难说,我不知道有机会的话我可能再梳理一下,具体所谓的感觉到底是什么一个东西。但是,就是去搜索,去锤炼那个搜索关键字,或者说,或者说ChatGPT的话现在是,提炼那个Prompt,提炼那个问好问题的能力。像是反正,其实我们平时开发当中,很多时候会遇到,就是我们要解决的问题,很有可能其他人已经解决过了。只不过我们更具体的,我们可能有具体的一个场景,然后又带入一个具体的数字,或者带入一个具体的变量,但是怎么能把那个,变量我们这里特定的,特定的东西给它,剥离出去。然后只是把那个通用的,拿着那个通用的去找解决方案,去搜那个关键字,对。
- 搜索能力的本质,是抽象能力在知识检索中的实际应用。
- ChatGPT 与 Google 都依赖输入信息的质量,抽象程度合适时结果质量更高。
- “提问清晰”≒“抽象合理”→是解决问题的起点。
当然,现在其实我另外一个想尝试的机会是,看看ChatGPT或者现在的大语言模型能不能提炼搜索关键字。具体来说,就是可能把一个具体的问题问一下,看看ChatGPT能不能把它抽象出来,让它去Google里面提炼出搜索关键字。然后我们用这些关键字去Google里面搜索这个问题。我觉得这是一个可以参考的思路。
- LLM(大语言模型)可用于提炼问题背后的通用结构与关键词,辅助后续搜索。
- 建议将其作为“搜索前的语义整理器”使用。
有时候,可能自己问问题问得不是特别到位,特别是Google。现在ChatGPT这些大语言模型就好很多,即便你问的问题不是特别好,它也能八九不离十地回答上来。当然,有时候它回答得很离谱是另外一回事,或者它产生幻觉(hallucinate)也是另外一回事。但比起只有Google的时候,一旦问题问不好,或者搜索关键字没选好,你可能压根就搜不到你想要的结果。
- 即便 ChatGPT 可能偶尔“幻觉”,其容错能力仍优于传统搜索引擎。
- 提问时结构越清晰,返回的回答也越稳定可靠。
所以,怎么把通用的那一部分,也就是别人也实现过的部分,抽象出来呢?比如在这个例子中,最常见的通用功能像是Authentication(权限管理)。之前跟Amos也在讨论,权限管理这种东西,基本上只要是个网站,只要是有用户,大概都会有权限管理的功能。如果说我拿着具体的需求,比如A类用户有这些权限,B类用户只能做这些事情,然后去Google上搜,那大概率是搜不出好结果的。
但如果可以抽象一层,因为权限管理是个抽象的概念,绝大部分网站都会用到。那我就可以用Google去搜索这个框架的权限管理解决方案是什么。然后把这个抽象出来的通用结果,重新适配到我们具体的使用场景中。这种提炼搜索关键字或提炼问题的能力,我感觉抽象是更加难的。
- 示例说明:将“某框架中 A/B 用户权限细节”抽象为“权限管理机制”更易获得现成资料。
- 搜索时避免带入业务特殊性,先提取领域通用性关键词。
我觉得,按道理来说,应该也可以像一堂课一样,把这个东西再具体拆分一下,然后再展开讲一讲。但我暂时觉得想不清楚这个问题,有机会我也学的,可能会再想一想,可能再单独开一个workshop,专门讲这个抽象能力。我觉得有时候抽象就是一些比较普遍的,比如你刚才讲到的验证功能,直接搜一搜可能就可以了。如果你不想搜,也可以直接问人工智能,它几次生成的也大同小异。
- 抽象能力本身可以教学、练习,但很少被系统化培训。
- 建议将“抽象力”作为一个独立工作坊或专题讲解内容。
如果是比如说你想把这个人工智能加入后,怎么样管理数据库里面已有的用户权限,假如你搜的话,但你这个例子里面那些模型,比如这个模型和那个模型的关系,和我想考虑的就是不一样。比如我想要的问题是一对多的模型,但网上给出的例子是一对一的模型。还有就是我自己做过的情况,要么是一对多,要么是这个一对多子模型上还可以没有复模型的那种。这种情况比较特殊,直接照搬可能不太好,至少需要问一下,但有时候思路有点混乱,所以要把这个问题问清楚。
- 案例特异性越高,越难直接复用网上方案。
- 抽象有助于理清“本质结构”与“实现差异”,进而组合已有解决方案。
这个抽象能力其实蛮复杂的,或者说归纳和演绎的能力。就是找不同又找相同,到底哪一部分是相同的可以套用,哪一部分又是不一样的,不能直接套用需要变化。这里面其实蛮多需要去找感觉的,或者我觉得应该也能整理出来一些体系性的东西。可能我恰巧遇到的是这一类情况中一个比较特殊的情况,或者比较偏难的点,但也有可能是另外一类中偏简单的。
- 抽象能力可视为“找共性”和“识别变量”的双重过程。
- 技术迁移和代码复用依赖于对相似结构的敏感度和判断力。
需要去分辨当前遇到的情况是属于特殊案例还是更通用的案例。这里反正蛮多可以去考究的东西,OK,这里先简单带过。我觉得这个本身也是一大块,真的要考虑的话有好多都可以展开的,而且我自己都还没想清楚。那反正不管怎么样,有一个这个意识至少说,能够把一个问题抽象出来,然后能够比较合适地去Google上面搜索,或者去找挑战。
- 一个良好的抽象判断框架应包含:
- 当前问题是否属于常见问题范式?
- 是否已有广泛使用的解决模型?
- 哪些要素可以剥离为参数?
- 哪些部分需定制实现?
[现实/业务问题]
↓
【步骤1】识别关键信息
- 明确输入、输出、数据结构、上下文
↓
【步骤2】剥离上下文细节
- 去除无关数字、特定名称(如“5行3列表” → “任意二维表格”)
↓
【步骤3】提炼通用关键词
- 归纳任务本质,如“格式转换”、“权限管理”、“递归算法”
↓
【步骤4】组织搜索表达
- 将关键词组合为简洁搜索语句或 LLM 问题,例如:
→ “Python write 2D list to CSV”
→ “Django role-based access control”
→ “recursive function sum example”
↓
【步骤5】执行搜索或提问
- 选择搜索引擎或大语言模型作为工具
↓
【步骤6】匹配 &调整
- 判断结果是否匹配预期
- 如无效,回退至步骤3,优化关键词
此表展示了不同抽象层级下的问题表述对搜索效果的影响,用于训练“关键词提炼能力”。
| 原始问题表达(输入) | 抽象后关键词表达 | 预期搜索命中率 | 备注说明 |
|---|---|---|---|
| 如何用 Python 把一个 5x3 表格转成 CSV? | Python write table to CSV | ⭐⭐⭐⭐☆ | 去除具体行列数,有利于匹配所有通用解决方案 |
| Python 怎么样把一个 list 写成 CSV? | Python write list to CSV | ⭐⭐⭐⭐⭐ | 最简洁常见表达,命中率最高 |
| Django 里怎么让 A 类用户能上传,B 类用户只能看? | Django role-based access control (RBAC) | ⭐⭐⭐⭐☆ | 用通用术语 RBAC 提升匹配度 |
| 如何处理一对多模型下的嵌套权限验证逻辑? | Django nested permissions model | ⭐⭐⭐☆ | 特定术语组合,适用于中等复杂场景 |
| Fn = 2*Fn-1 + 1 这种要怎么写? | Python recursive function example | ⭐⭐⭐⭐☆ | 使用“recursive function”作为关键词最具普适性 |
| F1=1, F2=2, F3=?, F4=?, 怎么求? | recursive sequence problem example | ⭐⭐⭐⭐ | 数列类抽象关键词更适合搜索教育类、刷题类内容 |
OK,跑题了,回来讲这个,拆分这个逻辑错误和语法错误。这里,语法错误,这里举了一个简单的例子,比如说这些不严格算语法错误,更多的是语义错误。
[Example]
if (a = 1)
比如说写一个a = 1,其实一般人正常可以知道正常的语法应该是怎么写的。正常来说应该是a == 1,就比较一下a是不是跟1相等。只有一个等号的话就变成赋值了。这个特别是刚开始接触编程的,很容易出现这种打错一个等号就卡半天的情况。
但这也不只是出现在新手,有时候老手偶尔也会犯这个错误,然后卡半天。你觉得代码也写的是对的,怎么就跑出来是错的。有的时候可能就是圈了东北的梗,但这种可以算是语法上的或者语义上的错误。这时候就要做一个分辨,就是说你可能觉得这个东西报错了,或者有时候还不报错,他就是跑出来结果不对。想了半天,想破脑袋可能都觉得,我这个逻辑挺对的呀,我不觉得我这代码逻辑有什么问题。然后就反复地去调自己的逻辑,就感觉好像逻辑有问题,但其实呢其实是语法错误,或者说只是打错了个字,或者语义上有错误。
- 常见误区:“语法错误 ≈ 初学者的问题” 实则不然,经验开发者也会犯低级错。
- 示例
if (a = 1)实际为赋值,非条件判断,属于典型语义层面错误。 - 错误症状常被误判为“逻辑错”,导致调试时间浪费。
所以有这个意识把逻辑和语法分开。然后有的时候不要,特别是卡住卡了很久之后,有时候需要跳出来先想一想,是不是有可能完全不是我想的那个方向错了。特别是像这个例子的话,你可能花了一个小时,一整天就在想,我这个代码逻辑是不是有问题,调了半天就调不对。其实这时候就需要跳出来想一想,是不是有可能是语法语义上有问题,打错了一个等号的情况。其实这个逻辑是没问题的,因为打错了等号,本来是做那个比较的,变成了赋值,然后就错了。
当然了,实际后面问题,实际调的真的用debugger去调,一般来说一下子就会跳出来,就发现这里是语法错误。但是,就先抽象一下,有刻意的去分离这个逻辑,代码逻辑业务逻辑,和这个代码本身。所以反过来有可能就是说,有可能是这个逻辑错了,但是你一直以为这个语法错了,调了半天调不出来,也是有可能出现反过来的情况。
- 遇到调试困难时,建议设立“分离假设”:当前是逻辑错?还是语法错?
- Debugger 可加速定位,但前提是已意识到两者可独立出错。
- 推荐形成“排查路径”:先检查语法结构 → 再验证业务逻辑。
图表:「错误类型排查决策树」:
[程序运行结果异常]
↓
报错信息清晰 → 是语法错?(如缺括号、等号错误)
↓
未报错但输出异常 → 是逻辑错?(如条件判断、边界遗漏)
那一旦你熟悉到一定程度,其实一般来说不太会出现语法错误,更多是语义错误。还有一种常见的情况就是用法错误,就是还是用到第三方的包也好,用第三方的库啊,调第三方的API的时候,你的业务逻辑是完全没问题的。但是你去调第三方的包的时候,第三方的API的时候,那个调法调错了,导致它发挥回来的东西不是你想要的东西。也就是会出现这种情况,就是在语义错误这一类里面。
然后这种时候呢,你可能就调了半天,再调你的语法,再调你的比如说用法,那有可能实际上是逻辑错了,或者反过来有可能逻辑是没问题的,其实是你那个包的用法错了。所以导致你最后这个方法执行出来的结果不对。这时候就需要去看,去,也是一样抽离出来,是不是那个用法用错了,那个语法用错了。
- 除了语法错与逻辑错,还有“用法错”:API/库使用方式不符合预期。
- 用法错在初期表现常与逻辑错高度相似,易混淆。
- 推荐策略:将用法测试从业务逻辑中独立出来进行验证。
当然这个东西去排除的一个好方法,还是回到刚才说的,先去做简化。特别是用到这些不熟的语法,不熟的库,不熟的API,这些东西的时候,可能比起一口气试试胖子,先把用法和逻辑一口气一股脑全部实现出来,比起这么搞,更好的办法就是说,把它也拆分开来,小步快跑。先把那些用法,把这些库第三方的库,第三方的API,用最简单的方法先调一调看看,调出来的结果是不是你想要的那个结果。然后再回来套用到实际的业务逻辑上面,把这两部分给它拆分开来,防止它搅在一块,同时出问题的时候,就分不出来到底是哪出了问题。
- “小步快跑”适用于语法验证、API调用测试等低耦合操作。
- 推荐流程:
- 用最小代码测试语法/API调用是否成功
- 单独验证业务逻辑条件、数据结构
- 最后合并两者构建完整功能
表格:错误类型与排查方式对照表
| 错误类型 | 特征表现 | 常见场景 | 推荐排查方式 |
|---|---|---|---|
| 语法错误 | 编译或解释器报错 | 拼写错误、缺括号、等号混用 | 静态检查工具、IDE、debugger |
| 逻辑错误 | 程序运行但结果不符预期 | 条件判断、循环边界、顺序错误 | 单元测试、断点调试、控制台输出 |
| 用法错误 | 语法正确但结果怪异 | 第三方库、API、文档误读 | 官方文档、简化原型测试、StackOverflow |
OK,下面一个就是,开发过程当中去善用这个debugger,或者说这个叫什么,中文应该叫调试器。我不知道大家有没有用调试器的习惯。我知道有蛮多半路出家的程序员,都不习惯用调试器。对,就是习惯了console.log,或者说PHP里面习惯用那个print,就直接把那个变量打出来。当然也不是说这个绝对不好,百分之可能八九十的情况下,这么搞就足够了。但是,也是一样遇到要调试那种复杂逻辑的情况下,很多时候单纯的用那个把这个变量打出来就不太够了。
- 初学者常用
console.log/print调试,但在复杂逻辑下信息不完整或难以定位。 - 调试器(debugger)提供断点控制、变量监视、调用栈查看等更系统化的工具。
- 适用于:变量状态追踪、流程控制验证、跨函数跳转等场景。
这个东西,我很难一下子去举例讲出来,但是,反正有机会的时候,特别是不习惯用debugger,不习惯用调试器的,其实尝试去用一用调试器。然后,对,不管是那个IDE里面,看每个人习惯不一样,有的人习惯用IDE里面的调试器也可以。然后有的人习惯用那个命令行里面的调试器,是可以的。因为调试器的话,它会把所有当前的状态全部给你打出来,把内存堆栈里面所有的东西都给打出来。一下子很清晰地看到,就是你的全局变量当前是什么状态,打了断点之后停在断点的时候,很清晰地看到全局变量是什么情况,局部变量是什么情况。然后它的那个调用栈,或者叫什么方法调用栈,那个可以很清晰地一下子看到。然后也可以用那个step,可以那个分布去调试。
- 调试器核心功能包括:
- 断点暂停执行(breakpoint)
- 单步执行(step into / step over)
- 局部与全局变量实时查看
- 调用栈追踪
- 图形化 IDE 与命令行调试器各有优势,可视化调试更适合初学者。
表格:调试工具与常见功能对照表
| 工具/命令 | 功能说明 | 常见用途 |
|---|---|---|
breakpoint |
设置暂停点 | 程序运行到该行自动停止执行 |
step over |
单步执行当前行(跳过函数) | 查看函数调用前后变量变化 |
step into |
进入函数内部执行 | 调试函数内部逻辑 |
watch / locals |
查看当前作用域变量状态 | 追踪变量值变化 |
call stack |
显示函数调用顺序 | 理解程序执行路径、错误定位 |
其实debugger这背后的逻辑,就是说方便你去验证你所理解的那个业务逻辑,或者你所理解的代码逻辑,和实际跑起来的那个代码逻辑是不是一样。这个又涉及到刚才说的,就是walkthrough,这个具体的例子。实际开发的过程当中,首先其实是脑海里面有一个对于这个算法逻辑,对于这个业务逻辑的理解。然后你会知道说,如果我代入这几个输入值,大概在这段代码里面会经历什么过程,然后它会怎么去换算,最后会输出什么结果。
当然,脑海里面有这个具体的例子,能够去跟着这个逻辑一步一步往下走的时候,再去用debug的时候,其实就是相当于去验证你的那个假设,你对这个业务逻辑的假设。你就会看到说,这一个个变量一个输入的值,然后它当中是怎么经历怎么变化一步一步,最后怎么变成输出值的。然后当中每一步你就可以看到,是这一步出了问题,和你理解的那个业务逻辑出了偏差。
- Debugger 的核心用途是:验证“思维模型”与“程序实际运行”是否一致。
- 如果程序行为偏离预期,调试器能清晰显示哪一步出现差异。
当然有的时候其实验证这个很简单,就是用那个console.log,或者用print给它打出来,就有的时候足够验证了。但是有的时候它就不足以验证,有可能你觉得有问题的那个变量,其实不是有问题的变量,它可能是别的变量有了问题。特别是最容易出现状况的,就是那个全局变量。但是这是另外其他地方可能会提一句,就是能尽量少用全局变量的,少用全局变量。全局变量是最容易出问题,就是可能跟你毫不相干的代码,或者你的代码出问题。
这里用debug的时候,你会看到,你可以看到一步一步它是怎么跑下来的。然后,你就可以教它去验证,然后说你所理解的那个代码逻辑,和实际跑出来的那个代码逻辑,哪里出了差错。然后用这个,可以比较有效地去调试出来那个问题。
console.log在简单情况下有效,但在复杂流程或变量交叉情况下效果有限。- 调试器能发现隐藏更深的状态错误,如作用域混淆、全局变量污染等问题。
- 建议将调试器视为“程序内窥镜”,而非仅是“放大镜”。
接下来一个这个“fake it before make it”,这其实不只是技术层面是这样,很多产品经理喜欢用这个,或者说现在很多搞AI的也喜欢搞这个。就是还没把那个机器人做出来的时候,先用人工把它伪装成机器人。当然实际来说在开发的过程当中,比较常见的用法就是,比如说你开发一个东西,它要拆分成几个模块,或者拆分成几个方法,或者拆分成几个class。这也是一样,当你这一段代码需要拆分的时候,尽量怎么避免一口气吃成胖子,那就是先把一部分的方法、类或者什么之类的先给它“fake it”,先给它做一个简单的版本,或者做一个假的版本上去,让它勉强勉强的去交互。
- “Fake it before make it” 是一种有效的分阶段开发策略,强调先搭骨架、后填内容。
- 常用于需要模块化拆分或任务分工的场景中,便于先推进整体流程。
- 广泛应用于软件开发、产品设计、甚至 AI 原型构建中。
然后集中去先把那块大头的逻辑,打满给它解决掉,具体细节拆分出来的那些小的模块先不管,先让它只要能响应,然后按照一些特例能够响应就行。当然这是微观层面具体写代码的层面。但是宏观一点的话,那个开发的工作当中也有,就是比如说我们会说要做mockup,做那个wireframe,把那个线框图做出来等等之类的。这个也是这种,也是一样的,一脉相承的思路。就是在我们实际把那个问题完美的解决出来之前,先能够看到效果,就是假装那个效果做出来的是什么效果。
- 微观层面:“函数/模块假实现”(stub/mocked function)帮助先跑通主流程。
- 宏观层面:Mockup、Wireframe 可让项目相关者更早看到“结果轮廓”。
表格:Fake-it 方法与典型应用层级
| 应用层级 | Fake-it 方法 | 实际用途举例 |
|---|---|---|
| 微观(代码) | Stub/Mock 函数 | 返回固定值,让主流程可先测试运行 |
| 宏观(产品) | Wireframe / 原型图 / Mock UI | 向团队或客户展示未来产品的界面形态 |
| 交互(AI等) | 人工操作模拟系统响应 | 让用户误以为已自动化,提高感知体验 |
在这个项目的层面来说,可以让那个项目相关方看到那个做出来的效果是怎么样。有时候比那个直接去解决问题,这个优先级更高一些,也是有可能是更多的提前需要做的步骤。然后反过来,在微观的写代码的层面也是一样。有时候你知道说你要拆分出来一个方法,然后你要调用那个方法,但是如果把所有的方法全部写全,然后才能去开发具体的代码逻辑的时候,有的时候那个节奏会太慢,然后会太拖沓,然后会把问题搞复杂。
- 项目管理中,“优先展现效果”有助于争取共识、评估方向。
- 微观开发中,过早陷入细节实现容易导致开发节奏拖延或调试混乱。
- 建议在不确定期使用“假实现”过渡,让主流程先完整落地。
因为也是一样,刚才说的,这样有可能多个地方会报错,有可能你那个主方法里面会报错,有可能你拆分出来的那个子方法里面也会报错,或者子的模块里面会报错。就是说报错了之后你怎么调,其实会把问题搞得比较不好调。所以当中的一个思路就是先“fake it”,先给他做一个假的那个方法上去,或者是做一个假的那个类上去,或者做一个假的模块上去,只要它能在那个特例当中能够返回出来对的结果,其实就给它放着就好。这时候你就可以进行下一步,不会去阻碍到你那个主方法的实现。
- “假实现”是一种对复杂性分隔的实用技巧:主逻辑与子模块的错误不会交叉干扰。
- 能有效控制调试焦点、缩短路径依赖带来的错误链。
[Example] Stub/Mock. So that you can move on to next part first.
这里也是我也是没足够时间去整一个例子出来,但是在,比如说在算法课里面常常会遇到这种类似的情况。我不知道你们有没有上过数据结构算法课的时候,有没有实际遇到过类似的情况。就比如说我需要实现一个算法,然后这个算法要拆分成几个步骤或者说几个模块。然后,一个办法就是我可以先把某个模块,如果有现成的那种,当然算法课的前提上算法课,情况下我需要自己去写实现那个代码。但有的时候有些有部分算法,可能是有开源的解决方案,你可以直接拿过来用。
- Stub 和 Mock 是“fake it”在代码中的常见技术手段:
- Stub:返回预设值的空壳函数
- Mock:记录调用行为的伪装实现
- 在算法分步实现、多人协作开发中尤其有用。
那可以其实先把那个现成的方案拿过来用一下,然后先替代一下,然后你把整个那个逻辑开发完了,再回去去覆盖掉原来那个暂时用开源方案替换掉的东西。在这边这些例子的话,也是之后有机会的话,我们再补充一些具体例子。
- 使用已有开源实现作为过渡替代,是一种高效的“先通流程,后优化性能”的策略。
- 最终可以再进行“回填”式开发,用定制实现替换临时代码。
示意图:开发流程图
在这个文本里面,下一个,这个是特别在排查问题的时候,或者在修bug的时候,最常见会遇到的一个情况。我也是常常会被问到这种问题,就是说改bug的时候,改着改着就不知道改哪去了。然后或者说,改着改着不知道哪里哪里错了。所以这时候,最理想的情况就是说先从那个没bug的状态开始。因为你基本上不可能一个东西它一开始就是有bug的。那个bug肯定是因为加入了什么功能的过程当中引入了bug,或者说在改什么东西的过程当中引入了bug。
- 排查 bug 的首要策略:回到已知可运行的“正常状态”作为起点。
- Bug 通常来自新功能或改动,非初始状态,因此逆向回溯有助定位问题。
- 避免在“混乱状态”中大海捞针式地调试。
所以在这个调试过程当中,一开始从那个有问题的状态开始,从你那个危机点去调的话,不太好笑。还有一般来说,因为是有问题的,所以你你不知道那个就像我们讲道里面是讲有那个生理学有那个病理学。我说医生看病的时候,如果一上来就不教你生理学直接教病理学,上来就跟你说这个人病了具体哪病了,然后具体他不病的状态下面是正常人体应该是怎么运作的。如果不先学那个的话,就反正跟你说这个东西有问题,你自己开始办了,这种这种手机是不好修的,不太好修的。
同样的,如果说比如说假设修车修车,如果说你不知道那个车应该是怎么运作的,然后一上去都给你说,反正这个车开不动,不知道为什么你自己查吧。其实我说去查排查问题是不太好排查。
- 类比说明:医生需先懂正常生理状态,修车师傅也需知道引擎正常如何运作。
- 同理,调试程序也应先理解它“应当如何正常工作”。
[Example] start with example code / start with good commit
所以一样的,其实一套代码里面,如果开始状态就是它是有问题的,那要去排查这个东西,一般来说不太好排查。所以尽量去找到那个有问题之前的那个节点,因为我们有给他们,所以当然比较痛苦的一些项目就是没有给他的项目,那种那种项目是最痛苦的。有给他的项目的话,至少可以去往回去回溯,然后可以回溯到一个知道它正常的状态。
但一般来说,如果说你自己亲手做这个项目一段时间的话,一般就你大概清楚知道前因后果的话就不太需要走这个步骤。但是如果说是接受别人的项目或者说,是接着别人的开发工作在做的时候,这有时候是需要有一定比较去回溯到一个那个正常的状态,去知道它正常的状态,这套代码运行的情况是怎么样的。然后以这个为基点再去调那个错,一般来说,这样会比较好调。
- 推荐策略:使用版本控制(如 Git)回溯至“最后已知良好”的提交记录。
- 接手他人项目时,务必先找到一个可成功运行的“参考状态”。
图表:调试路径选择流程图
[代码当前无法运行]
↓
是否有示例代码?—— 是 → 从示例开始验证 → 修改最小化差异
↓ 否
是否有 Git 版本历史?—— 是 → 回退至已知良好 commit → 逐步 diff
↓ 否
尝试复原最近的操作记录 → 手动排查最近改动处
同样的思路,其实刚才也是有类似可以类比的,就是说,如果说一样去调一个不太熟悉的库的用法的话,从它的示例代码开始。因为我们假设是它示例代码是对的。所以说如果一口气也是一口气吃成胖子,然后不管那个示例代码,直接就开始套用在我们自己的代码里面,有的时候你可能不太能够确定说是不是它那个示例代码本身用法就是对的。
所以把它拆分出来,先保证说这一部分它是work的,它是对的,跟着至少跟着它那个示例代码走,这个是能正常运行。然后再去区分看说,假设它这个用法确实是没问题,那我在我这个场景里面去用是不是出了问题。
- 示例代码 = “运行正常”的已知输入 → 有利于隔离环境与参数差异带来的 bug。
- 推荐优先运行第三方库的官方示例 → 确保库自身无问题。
然后这个也是就刚才提的去回溯到之前的commit,那我这个commit这个时间节点的这个代码是有问题的,那我可不可以回溯到一个没有问题的那个代码的节点上面。然后先能够看到它正常运作的状态。然后,那一旦我们知道正常运作的状态,我们就可以做那个diff嘛。一般的git的工具都自带diff这个东西。
或者说,那其实手动去diff一下去对比一下那个正常运行的代码和现在有bug的代码之间那个区别在哪里。一般来说,这个区别比如说一百行的代码里面,可能它这个改动之后的区别可能也就只有十行左右。那也其实重点去调的那就是那改动的十行。但反过来如果不做这一步骤的话,你只知道那一百行里面是有错的。那一百行去在一百行里面去找那个错和从十行里面去找那个错,这个难度就是几何级数的区别。
- 利用
git diff聚焦于变动区域,是快速定位 bug 的利器。 - 核心思路:将“调试范围”从整段代码压缩为“具体改动行”。
所以特别是调错误的时候,这样一个思路,从这个能够运行的状态开始去调,而不是要一上来就说这个东西有问题,反正大海捞针里面去调的,这种难度是很大的。这个也是和之前那个例子有关系,刚才我提到的说之前,这边那个CP Ad Manager有分配一个程序员,做这种卡了很久。
当中一部分原因就是因为其实他不熟悉那个测试代码的框架,就不熟悉那个Rspec不熟悉Cucumber这个东西。所以然后直接就拿过来去套用在那个测试的那个环境里面去尝试开发那个测试代码了。如果套用这个情况其实最后看的话就是说他其实还没搞明白那个测试框架,那个该怎么用,或者说他其实甚至都没有把那个测试框架给的那个本身的那些例子全部走完。
- 案例说明:不了解框架时贸然使用,会导致错乱来源难以定位。
- 先让测试框架本身“跑通”,再写测试场景代码,是降低混淆的好方法。
然后直接就尝试去拿那个框架去套,去套那个测试代码去写那个新的测试代码。所以就卡在那边,他不知道是他写的测试代码有问题,还是那个测试框架有问题,还是他的对那个测试框架的用法有问题,还是还是那个test case本身有问题,还是说被测试的那个代码有问题。一旦涉及到这个多个地方都可能有问题的时候,然后就特别容易出现卡点。
但是把这些东西拆分开来,然后一部分一部分一部分,然后保证这个东西是work的状态,保证他我至少对这个怎么去用这个测试框架是有一定程度的理解,我把那个实例的代码给他跑过了。至少我知道这个状态是work的状态。然后再一步一步就调了给他加一行代码,再加一行代码,给他调成那个实际的一种场景面该有的样子。
- 拆分原则:先验证框架本身能跑 → 再添加逻辑验证 → 最后结合真实业务数据。
- 每次只改动一个变量或一段逻辑,逐步构建稳定路径。
"All happy families are alike; each unhappy family is unhappy in its own way."
所以这边这边就让我想到另外一个那个常常用到的说法,就是那个幸福的家庭都差不多,然后不幸的家庭各有各的不幸。我觉得套到代码里面也是一样,work的代码都是差不多的,跑不出来的代码各有各的问题。
- 金句提示:所有跑得通的代码结构类似,跑不通的则千差万别。
- 这句话强调了:先构建可运行基础,再追求个性化逻辑实现。
所以怎么能够解决这个问题呢,因为确实代码里面可能会有的问题,真的是多种各种各样的问题都会有。在刚才再重复一遍,就比如说,可能有语法问题,可能有语义问题,可能有库用法不对,可能那个库本身有问题,可能有业务逻辑不对,可能报了个测试,有可能是代码测试代码本身的问题,有可能是被测试代码的问题,有可能是测试的框架用法的问题,等等等等之类的,全部混在一起的时候,就是一锅粥,很难很难查出来什么问题。
但是一旦它代码也是一样,它一旦跑通了,你就知道它是跑通的。它一旦跑不通,你就你只能知道说,它某个部分没跑通。但具体哪个部分没跑通,这种是需要去查的。然后怎么去简化它,去把它剥离开来一点点去查,这个是一般最有开发过程中,难度最大的一个部分。
- 排查重点:将混合问题“切成薄片”,分别调试。
- 推荐手段包括:
- 控制变量法
- Stub / Mock 替代法
- 版本比对法(如 git bisect)
示意图: 问题定位策略图
OK,接下来再提一些其他的细节。这部分讨论的是如何借用代码的问题。我个人非常鼓励借用代码。虽然有的程序员可能更喜欢亲手编写所有代码,但过度依赖自己编写代码也会导致效率低下。关键在于如何在二者之间找到平衡,下面分享一下我的经验。
- 借用代码 ≠ 偷懒,而是一种提升开发效率、避免重复造轮子的实践策略。
- 核心在于:懂得判断哪些代码可以借用,何时该理解其原理,何时只需知道其用途。
首先,借用代码最常见的情况是我自己会使用Google搜索相关资料。现在ChatGPT也出现了,前阵子试用了一下,特别是在前端代码方面,效果还不错;虽然对于后端逻辑,它的梳理可能不够清晰,但前端修改方面表现很好。就代码生成而言,我个人更推荐使用Claude和Perplexity这两个工具。它们各有局限,所以还是需要根据实际情况自行判断。我目前的使用体验是,在某些场景下,Claude和Perplexity比ChatGPT表现更好,尽管它们也有使用限制,因此不能完全依赖。
对于这两个工具的使用,我一般的做法是:如果需要调用第三方API或依赖第三方库时,我倾向于使用Perplexity,因为它具备网络搜索功能,可以读取相关文档链接,再基于文档开发代码;而如果不需要依赖文档,我则更习惯使用Claude。由于我每天的开发时间有限,每周写代码的时间也不多,所以即便它们有每日使用限制,也不会对我造成太大影响,我只需等待一段时间即可继续使用。
总的来说,这些工具并没有严格的标准,只是我个人的使用倾向和喜好,而且未来可能会有新的模型出现。最近刚有了DeepSeek,我也有机会试用过,可能效果会更好一些。但目前它经常出现崩溃的情况,据我了解,DeepSeek对硬件要求较低,尽管如此,目前仍不太适合日常使用。此外,DeepSeek还支持在本地环境部署,可供尝试。
- 工具推荐简表:
| 场景 | 推荐工具 | 理由 |
|---|---|---|
| 第三方 API 查找与用法 | Perplexity | 具备网络检索功能,能引用官方文档 |
| 前端代码生成/样式调整 | ChatGPT、Claude | 响应快、格式整洁,适用于 HTML/CSS 层面 |
| 非联网、模型行为控制 | Claude | 模型思路清晰,回答稳定 |
| 探索新模型 | DeepSeek(待成熟) | 本地部署可能性,但当前稳定性差 |
在实际开发中,该用就用,无论是Google、ChatGPT还是其他工具;对于第三方包,我个人倾向于直接使用现成的解决方案,也就是常说的“不要重新发明轮子”。程序员普遍认同:既然别人已经发明了轮子,就没有必要再花十个小时重新造一遍。因此,能借用的代码就借用,没有必要重新编写。
- 原则总结:
- ✅ 借用已有包/库 → 提高效率
- ❌ 不必事事从零开始
- 🎯 目标是“功能达成”,不是“代码原创”
我曾遇到一个有趣的情况:在检查他人代码时,发现一个包每月下载量达到千万级别,很多人都在使用。当我和J Leheng一起开发时也使用了这个包。出于好奇,我点进去看了一下,发现里面实际上只有几行代码,仅包含一个正则表达式。实际上,我直接复制这个正则表达式后,就能正常使用。这说明,在具体场景中,有时可以直接拷贝代码,有时则需要对其进行封装,具体情况具体分析。
- 下载量高 ≠ 必然复杂,有时核心逻辑只是一个表达式。
- 借用代码的粒度需视情况灵活调整:整包 / 函数 / 一行逻辑。
通过Google搜索,你通常能在Stack Overflow上找到相关代码,直接拿来使用也是完全OK的。但需要注意的是,ChatGPT或其他AI生成的代码往往存在错误。虽然代码看起来格式正确,但仔细检查后可能发现无法正常运行。
- AI生成代码使用建议:
- ✅ 结构清晰时可借用
⚠️ 运行前务必测试- ❌ 不盲信自动生成结果,尤其涉及异步、权限、状态变更逻辑
整体来说,借用代码主要分为两种情况。第一种是使用现成的代码库或包:当你遇到问题,并且大概率已经有人开发了解决方案时,尽量使用现成的包;如果包的内容较简单,也可以直接复制其代码。
- 借用方式分类:
| 类型 | 特征描述 | 推荐做法 |
|---|---|---|
| 第三方库 | 维护活跃、有文档、多人使用 | 可直接安装并阅读文档 |
| 轻量函数段 | 小于 30 行、无依赖、逻辑清晰 | 复制后根据实际需求调整 |
在使用包时,通常无需了解其具体实现。开发过程中,只需掌握如何使用即可。当然,如果有兴趣,可以利用业余时间深入研究包的底层逻辑。但在实际应用中,只要知道如何调用并对其基本功能有信心就足够了。比如,你可以通过查看包在GitHub上的star数、关注量和每月下载量来判断其可靠性。
- 使用包时:
- 实际开发 → 知其然(怎么用)
- 技术提升 → 知其所以然(怎么实现)
长时间只编写业务代码可能会导致编程水平停滞不前。因此,我建议大家利用业余时间阅读一些高质量包的源码,尝试理解其业务逻辑、编码风格以及设计模式。虽然这并不是解决当前问题的直接方法,但对提升个人编码能力是一种长期投资。
- 推荐阅读对象:
- Web 框架(如 Flask、Express)
- 工具库(如 Lodash、Requests)
- 流行 SDK 或数据库驱动(如 Prisma、SQLAlchemy)
使用第三方包意味着在不完全了解其内部实现的情况下借用代码。只要你能通过GitHub的星标、关注和下载数据等指标确认其可靠性,就可以放心地使用。然而,前段时间曾有两个知名包被注入木马,供应链攻击中也出现过贡献者提交恶意代码的情况,这都提醒我们,借用代码必然存在风险。归根结底,这是一种投入产出比的权衡:自己编写代码耗费精力较多,而借用他人代码虽然提高效率,但可能带来安全隐患或bug,因此必须谨慎选择。
- 安全建议:
- 检查维护活跃度(最后一次提交时间)
- 关注 GitHub issue、CVE 风险通报
- 企业项目避免使用“0 star / 0维护者”的包
另一种情况是使用小段代码。例如,通过Google搜索到的代码通常只有十几行或二十几行,不适合大段复制粘贴,通常需要根据实际情况进行调整;仅在部分情况下,代码可以直接使用。
- ✅ 可以借用的小段代码:
- 数组去重
- 文件路径拼接
- 时间格式转换等小型通用逻辑
对于小段代码的借用,我建议大家尽量理解其逻辑,哪怕不必完全透彻,但至少要知道代码的作用,因为大部分情况下你都需要对其进行调整。这样可以避免仅仅依赖复制粘贴。现在有些入门程序员可能倾向于直接复制粘贴代码,但长期来看,如果不理解代码的运作,就有可能被AI取代。因此,至少要对代码的基本思路有清晰认识。
- 理解代码 ≠ 背下所有实现细节
- 最起码要能回答:“这段代码做了什么?改一个参数后会发生什么?”
对于AI生成的代码,我建议大家根据具体情况进行权衡。尤其是初次使用时,应尽量弄清楚AI修改了哪些部分。举例来说,前段时间我让AI修改一个圣经问答游戏的前端代码,我大致了解它的改动,因为前端主要涉及HTML语法,所以不必逐行审核。但当遇到不熟悉的代码逻辑时,最好花些时间仔细审查,避免盲目复制粘贴,否则长期可能会落后于AI的步伐。
- 建议:“能被 AI 替代的代码,也能被人类低门槛替代。”
- 利用 AI 提高效率 → ✅
- 盲信 AI,丧失判断力 →
⚠️
关于环境变量和敏感数据,将这些信息暴露给AI存在一定风险。因此,在处理敏感项目时,应避免使用免费AI工具,而选择具有商业安全保障和保密协议的产品;对于不太敏感的项目,风险则相对较低。
- 处理敏感数据时的建议:
- 本地部署模型优先(如 Ollama / DeepSeek)
- 避免将
.env、API Key、数据库密码直接粘贴进 AI 窗口 - 商用项目优先使用付费带协议的企业产品
示意图:借用代码的判断流程图
接下来讲的,其实是“小步快跑”的另一面。我好像少写了一段内容,就在这里补上,是关于“小步快跑”或者“一口气吃成胖子”这种做法的对比。我偶尔会看到有些程序员,特别是刚接触编程的新手,很容易一下子写一大段代码,然后再跑一遍测试,比如写了十几二十行代码后再来验证一下效果。这种做法其实挺危险的,或者说效率挺低的。虽然表面上看起来效率高,觉得“一口气写一大段,跑通了就可以继续往下写”,但现实中更多时候是跑不通的。就像之前讲过的,大部分情况下肯定会出问题。
- 开发中的“破坏”不可避免,关键在于:是否能快速发现问题并及时修复。
- 写完十几行代码再测试,往往导致错误定位难度上升。
- 相比“积累错误后统一调试”,边写边验证更安全、更高效。
所以如果你是十行十行地写代码,那你就得十行十行地去调试。如果是一行一行写,一行一行调试,那调试效率会高很多。就像前面提到的,如果你在一百行代码中找一处错误,那就是在大海捞针。同样的道理,在十行代码中调试一个问题也可能很困难。比如你写了十行代码,觉得问题可能出在前五行,但实际上问题却出在第六到第十行。你可能白白花了半天时间在错误的地方排查。
所以要遵循“test often and break fast”的原则,这是程序员圈子里常提的一句话。也就是说,要频繁地测试、快速地让问题暴露出来。不要等写到第十行才回头去调第一行的问题。尽量写一行就跑一行,及时发现问题。为了能做到这一点,就需要有一个方便调试的开发环境。
-
🌟 原则:Test Often, Break Fast
- 越早触发错误,越快定位问题
- 错误堆积越久,越难还原上下文
-
✅ 推荐实践:
- 写 2~3 行逻辑 → 马上运行验证
- 错误一出现 → 优先定位修复后再继续写
我自己也遇到过这种情况:有时候我不愿意写一行代码就去调试,原因就是——调试过程太麻烦。如果每次测试都要花十分钟,或者为了测试某个功能得点开浏览器,点击几个页面花上一分钟才能到目标页面,那我就会觉得烦,就会倾向于多写点代码,一次性调试,避免频繁测试。但这样做的风险就是,一旦出问题,调试起来会非常麻烦。就像前面说的,在十行代码中找一行问题,和在两行代码中找一行问题,难度完全不一样。
所以,如果条件允许,我建议大家尽量把开发环境搭建得足够顺手。因为人是有惰性的,一旦调试麻烦,就会下意识地避免去调。如果开发环境不舒服,我自己也会不想调试,就会倾向于先写一大段再一起调,这样出了问题就更难排查。所以,建议在有条件的情况下,把开发环境优化得尽量舒适。比如能做到五秒钟就能跑一次代码,改完一行就能立刻验证,这种节奏其实是最理想的。
- 开发节奏不顺的最大障碍:调试成本太高。
- ⚙️ 推荐工具/方式:
- 自动保存 & 编译(Auto-reload, Hot Reload)
- 一键运行测试脚本或页面
- 快捷输入测试数据(mock data, 工具栏注入)
有些框架本身就不支持热更新或Live Reload。当然具体怎么搭建舒服的开发环境,这因人而异,每个人喜好不同。但原则上就是:把环境配置得足够顺手,让自己愿意写一行代码就去测试一下效果,这种状态是最理想的。虽然不是所有项目都能做到这一点。
像有些PHP项目,特别是我们现在还在维护的一些老项目(虽然大部分已经完成更新换代),那种老项目就啥都没有:没有开发环境、没有文档、没有测试数据,什么都缺。你想调试某个功能,只能直接上服务器去调PHP代码。像这种极端情况确实存在。这并不是说完全不可能搭建出开发环境,而是说原先项目就没配好,要重新搭建就需要投入很多精力,要花时间、调配置、搞清楚原理等等。
所以也不是说写一行代码就必须马上测试一遍是“硬性标准”。而是在条件允许的情况下,尤其是已有基本开发环境,或者至少有思路知道该怎么搭建开发环境时,建议把环境弄得尽量舒服。这样更利于小步快跑,快速调试,是一种理想的最佳实践方式。
- 开发环境舒适度,直接影响“测试频率” → 间接影响“调试效率”。
- 即使在不理想的老项目中,也应思考是否有机会先搭建最小化测试入口。
流程图示(可生成流程图):
[开始开发]
↓
是否已配置快速测试环境?
↓是 ↓否
[每2-3行测试一次] [优先考虑搭建简易调试入口]
↓ ↓
[快速发现问题] [减少未来调试时间成本]
↓
[更小粒度调试 → 更少错误 → 更高开发效率]
最后一点,是关于临时解决方案和最佳实践之间的取舍。这两者在实际开发中经常发生冲突。这里引用一句话,“Perfection is not attainable, but if we chase perfection, we can catch excellence.” 中文大概可以翻成:“完美是永远无法企及的,但在我们追求完美的过程中,我们可以接近卓越。” 实际上在开发工作中,很多代码总是存在优化空间——不管是代码风格、逻辑结构、设计模式,还是复用性、抽象能力,甚至是整个架构层面,总会有一个更理想的解法,也就是所谓的最佳实践。
- 🎯 最佳实践(Best Practice)是“长期的方向感”,而非“短期的强制标准”。
- ✅ 允许先写出功能 → 但要保留“可以优化”的意识与清单。
- 🛠️ 软件开发从来不是“写完就完”,而是不断逼近理想的过程。
但很多时候,我们在实际工作中并没有条件去实现那个最理想的解法。比如说赶工期、时间紧迫,或者我们已经感到很吃力,特别是在刚接触一个新项目、新框架或新语言时,光是把功能实现出来可能就已经耗尽了大量精力。此时就很难再花心力去打磨代码、寻找最优解。无论是出于精力有限,还是项目进度受限,我们往往只能退而求其次,采用临时解决方案或非最优方案。
- 现实中常见的限制因素:
- ⏱️ 紧张的项目周期 / 快速上线需求
- 🧠 新手阶段 / 技术栈尚不熟练
- 🤝 团队协作复杂度 / 代码风格不统一
- 临时解决 ≠ 错误,而是在约束下的务实选择。
但我想说的是,尽管现实条件常常限制我们无法做出最优解,但这并不妨碍我们保持追求最优解的意识。换句话说,不要“摆烂”。我见过有些程序员属于彻底摆烂型的——对代码质量毫无要求,只要功能能跑通就算完成,拍拍手就转头做下一个任务。虽然这在短期内任务是完成了,但如果你对自己有一点追求,还是不应该满足于仅仅完成任务。即使时间不允许你完全写出理想中的优雅代码,但至少要有那个方向感。
- 不要放弃追求,只是先放一放。
- ✨ 拥有“改善意识”比代码本身更重要。
- 方向比当下状态更决定成长速度。
换个说法,用“一堂”的“刻意练习”理念来解释,就是要建立起代码的审美标准。知道什么是好的代码、什么是清晰的结构。即使你当前写出来的代码还达不到那个“漂亮”的标准,至少你要知道理想状态是什么样的,并在有余力时往那个方向靠拢。这其实本身就是一种训练。当你越来越习惯去思考“怎样写出更好的代码”,你的代码质量自然也会随之提升。开始时你可能觉得写高质量代码是一种额外的付出,但当你养成习惯之后,这种思考就会变成一种下意识的行为,不需要特意花时间去“优化”,你写出来的代码本身就已经是质量更高的了。
- 💡 建立“代码美感”的方式:
- 阅读高质量开源项目(如 Rails / Vue / Next.js)
- 自己写完代码后回头重构一次
- 留意他人代码中的“命名 / 分层 / 注释习惯”
[Quote] "Perfection is not attainable, but if we chase perfection, we can catch excellence." - Vince Lombardi
就像这句话所说的,虽然我们可能永远无法达到完美,但追求完美的过程中,我们会不断接近卓越。即使在某些项目中无法做到尽善尽美,我们也可以通过不断尝试、不断思考,让自己的代码越来越接近高质量标准。
- ✨ “追求完美”从来不是为了实现完美,而是为了拉高下限,拔高上限。
- 写不出极致,也别放弃“写得更好”。
但同样也要避免另一个极端:过度追求完美。在很多现实情况下,项目进度、团队协作、资源限制等等都不允许你一个人花大量时间去抠每一个细节,把代码打磨得完美无缺。所以这中间要找到一个平衡点。一方面接受现实的不完美,另一方面也不能因为不完美就彻底放弃追求。也就是说,不要因为没时间、没精力,就随便写一写,不管代码质量。这种“躺平式编程”也是不可取的。
- 极端一:❌“完美主义强迫症” → 拖延上线、浪费时间
- 极端二:❌“能跑就行摆烂型” → 技术成长停滞、质量难维护
- ✅ 推荐状态:“写得成 + 留得住 + 改得动” = 稳定 + 可维护 + 有提升空间
图表:理想与现实的代码平衡图
| 思维状态 | 典型行为 | 风险 | 调整方向 |
|---|---|---|---|
| 极端完美主义 | 拒绝上线,过度重构 | 拖延 / 交付失败 | 明确迭代节奏,先交付后优化 |
| 功能导向 / 无质量标准 | 能跑就行,从不回头看 | 技术债堆积 / 后续维护困难 | 建立审美意识,阶段性重构 |
| 平衡思维 | 有目标感,阶段容忍瑕疵 | 有意识妥协,但追求长期优化路径 | 留 TODO / code review 后处理积压 |
有没有什么问题?对,说到底,其实就是个“平衡”的问题,是的。有些人可能说,要像给自己孩子一样对待代码,呵呵。对,不能总是抱着“凑合能跑就行”的心态,也不能一年就写几行代码。没错。我觉得做人也是一样,很多事其实都是在找那个平衡点。我们可能都希望把事情做到最好,但现实不总是允许我们那样做。能意识到自己的理想和现实之间的差距,并持续朝着理想靠近,其实就是成长。