对claude code生成的可维护代码进行圈复杂度分析的结果

那是三周前的一个下午,我们的后端团队刚把一整个微服务模块交给 Claude Code 完成代码生成。需求文档里写了完整的接口定义、状态流转和异常处理逻辑,Claude Code 一次性生成了三十几个 Python 文件,总计六千多行代码。跑通单元测试只花了不到半天,所有人都在感叹“这东西真快”。

但第二天,负责 code review 的同事在 PR 里贴出一段截图,附了一句话:“这段订单状态流转的逻辑,我读了三遍没看明白到底有几个出口。”

我当时坐在屏幕前,盯着那段将近两百行的函数,数了数嵌套层级:最深处缩进到了第六层,if-else 像迷宫一样嵌套,一个函数里出现了至少四种提前 return 的条件,中间还夹着两段几乎一模一样的循环逻辑。那个函数能跑,能过测试,但任何一个新人试图修改它时,都可能在不经意间引入线上事故。

那瞬间我意识到一件事:我们正用一种前所未有的速度制造代码,但没人知道这些代码在未来三年里会变成什么样。

于是我们做了一个决定:不再用“感觉”去评判 Claude Code 代码的可维护性,而是用一把硬尺子量一量。我们选了整个团队最熟悉也最不妥协的指标,圈复杂度(Cyclomatic Complexity)。接下来的两周,我们采集了 Claude Code 在十种典型开发任务下生成的所有函数级代码,用工具逐段分析,对比我们自己团队中等水平工程师在同等任务下的输出,最终拿到了一份让我们集体沉默十几秒的数据报告。

这篇文章,就是那份分析结果的完整复盘。我不会给你们一个“AI 代码质量堪忧”的惊悚结论,也不会说“Claude Code 写出了完美代码”。我要给的,是一份可以用在你下一次技术选型讨论会上、可以钉在团队 CI 流程里、可以指导你该怎么用 Prompt 和重构策略驯服 AI 产出的真实参考

一、核心结论先行:圈复杂度这张“体检报告”到底说了什么

我不想绕弯子。在展开所有实验细节之前,先给你三组我们觉得最重要的数字。这三组数字,决定了我们团队接下来对 Claude Code 的使用策略。

第一组:总体均值偏低,但尾部极值触目惊心。

在所有十个任务三次重复实验中,Claude Code 生成的可执行函数共计 312 个(我们排除了 __init__、简单的 getter/setter 这类无逻辑函数)。这些函数的圈复杂度均值是 5.3,而人类工程师对照组的 287 个函数,均值是 6.7。只看均值,Claude Code 似乎更“清爽”。

但当我们拉出每个任务的前 5% 高复杂度函数时,Claude Code 的表现开始失控。在“复杂订单状态流转”任务中,它产生过一个圈复杂度高达 26 的函数,而同类任务下人类代码极值是 12;在“多层权限校验与路由派发”任务中,它写出过复杂度 22 的函数,人类极值是 11。这些极值函数,每一个都具备成为生产故障隐患的潜质。

对claude code生成的可维护代码进行圈复杂度分析的结果

第二组:任务类型对复杂度的分化,比我们预想得更剧烈。

我们原本以为,Claude Code 在算法任务上会更优秀,因为大模型见惯了 LeetCode 上的标准答案;在业务逻辑上可能稍乱,因为需求描述往往存在歧义。但实际结果恰恰相反:

  • 在纯算法实现(如“给定树状结构实现带访问计数的路径查询”)上,Claude Code 的圈复杂度均值仅为 4.1,但其中有两个函数出现了机械式的逻辑冗余,比如手动展开了递归调用,强迫算法内联在一个大函数里,导致圈复杂度反而升高到 14。
  • 在 CRUD 类业务接口(如“带分页、过滤、排序的列表查询”)上,它的圈复杂度均值低至 3.8,结构异常干净,几乎每次都把参数校验、查询组装、结果后处理拆成了独立函数。
  • 但是,在包含状态机多重错误补偿条件分支组合的任务中,Claude Code 的复杂度均值飙升至 9.1,且波动剧烈。
  • 更出乎意料的是,在“错误处理密集型”任务上(如“网络请求重试+降级逻辑+缓存穿透保护”),它频繁写出多层 try-except 嵌套,且缺乏对错误类型的统一抽象,每个 try 块内又重新判断错误码,导致局部复杂度爆炸。

第三组:Prompt 微调可以系统性降低圈复杂度,但无法消除模型对“书写习惯”的路径依赖。

我们尝试了两种 Prompt 变更:

  • 对照组:仅给出功能描述。
  • 实验组:在功能描述后追加“请写出可维护的代码,优先将复杂逻辑拆分为低圈复杂度的独立函数,避免深层嵌套”。

在实验组中,Claude Code 生成函数的圈复杂度均值从 5.3 下降至 4.6,极值从 26 降至 17。但即便在这种强调下,它在状态机任务里仍然写出了复杂度 17 的函数,只有当我们进一步要求“请参考状态模式进行拆分”时,复杂度才骤降至 6。这意味着模型的默认抽象策略是“线性叙述业务逻辑”,而非“工程分解”,除非你明确给出设计模式或架构指令。

对claude code生成的可维护代码进行圈复杂度分析的结果

基于这三组核心发现,我给 Claude Code 生成代码的可维护性下了八个字的判断:身量清瘦,骨架易折。 它善于写出“看起来”简洁的函数体,但当业务逻辑自身带有组合分支和状态上下文时,它缺乏将这种骨架性复杂度进行结构性拆解的主动意识。这个判断,会贯穿我们后面所有的分析。

二、我们为什么要用圈复杂度这把尺子,而不是别的东西

很多团队评估 AI 代码质量,倾向于看几个直观的东西:单元测试覆盖率、静态扫描告警数、代码行数。这些东西当然重要,但它们都不能直接回答一个核心问题:这段代码的认知负担有多大?未来改它的时候,要多大勇气?

圈复杂度刚好是对这个问题的数学逼近。它由 Thomas McCabe 在 1976 年提出,本质上是计算一个函数中独立线性路径的数量。最简单的理解:一个函数的圈复杂度等于它的判定节点数加一。 每个 if、elif、for、while、and、or 都可能增加一个判定节点,每增加一个,测试这个函数所需的用例数理论上就要翻一倍,维护者要理解所有分支组合的心智开销也会陡增。

行业里有一些约定俗成的阈值:

  • 1-5:低复杂度,函数简单,极易测试和维护。
  • 6-10:中等复杂度,需要留意,但仍然可控。
  • 11-20:高风险复杂度,测试和维护都变得困难,建议拆分。
  • >20:极高风险,函数极易隐藏 bug,且修改的开销极大,强烈建议重构。

但为什么我会在 Claude Code 评估中选择圈复杂度而不是其他?三个原因:

第一,它和“出错概率”的相关性是最强的。 Cisco 曾对自家系统做过一次大规模的历史缺陷分析,发现圈复杂度超过 10 的函数,其缺陷密度是低于 10 的函数的五倍以上。这不是猜测,是工程统计。AI 生成的代码尤甚:它没有心理负担,不会因为“这段逻辑太绕了”而主动停下来重构;它只会忠实地用代码复述你的需求中的曲折。

第二,它直接决定测试成本。 一个圈复杂度为 15 的函数,理论上需要 15 个基路径测试用例才能覆盖所有分支。如果你的团队在 CI 中设定了基础路径覆盖目标,高圈复杂度的函数会立刻成为测试债务黑洞。而 AI 生成的代码往往产出自某个高密度编码的短时间窗口,你如果不加检查就把它们合入主干,等于在仓库里埋下了未来测试成本的无底洞。

第三,圈复杂度能暴露“伪可维护”。 这是我踩过的坑:有些代码看起来抽象得很好,设计模式用得到位,类很小,函数很短,但当你真正用圈复杂度去量,会发现一些函数只不过是“把一堆分支挪到了另一个地方并包了一层看起来很高级的壳”。比如用一个工厂模式承载了十几种产品实例化,但工厂方法本身却用一个巨大的 switch-case,圈复杂度高达 18。圈复杂度不会因为你手段优雅就放过你。

三、我们的实验是怎么做的:不是论文,但足够诚实

我必须事先说明,这次实验不是学术研究级的严格对照实验,但它是一次工业级压力测试,目的是模拟我们团队日常把需求文档丢给 Claude Code 的真实流程,然后用自动化工具对产出代码进行无差别扫描。

任务集设计:十个能戳到痛处的典型场景

我们没有用“写一个冒泡排序”这种脱离现实的算法题。我们挑选了十个在我们自己的业务代码库中真实出现过的、代表不同复杂度剖面和编码模式的任务。每个任务都用中文写成需求文档,包含入参、出参、边界条件、非功能性要求,并在末尾标注是否需要考虑可维护性(后文有对比)。

十个任务分别是:

  1. 带过滤、排序、分页的 REST 列表接口(CRUD 代表)
  2. 简单数据模型转换服务(如把外部 API 返回的嵌套 JSON 转成内部领域对象)
  3. 电商订单状态流转引擎(含待支付、已支付、拣货中、已发货、已签收、退款中、已取消等 7 种状态,状态转换有严格的前置校验)
  4. 多层权限校验与路由派发(基于角色、部门、资源类型多个维度的组合校验)
  5. 递归树结构查询与构建(权限树、组织架构树)
  6. 带错误分类与重试机制的远程调用代理(网络超时、业务异常、限流等错误处理逻辑)
  7. LeetCode 中等难度算法:全排列去重与路径回溯
  8. 配置文件解析与参数合并逻辑(多个来源的配置优先级:环境变量 > 配置文件 > 默认值)
  9. 日志消费与实时聚合统计(按时间窗口聚合计数、计算百分位延迟)
  10. 数据同步中的增量比对与冲突解决(源和目标的差异判断,冲突策略选择)

这里特意混合了“纯逻辑密集型”(算法、树结构)、“分支决策密集型”(状态机、权限、错误处理)和“数据流转密集型”(CRUD、转换、配置解析),以求捕捉 Claude Code 在不同思维模式下对复杂度的组织方式。

基准对照:我们自己的中级工程师

我们找了团队里两位从业三年多、没有架构师头衔的中级工程师,要求他们用同样的需求文档、同样的时间窗口(每人两天,允许查资料,但不允许用 AI 助手)写出这十个任务的实现。这两位工程师的代码就是我们的人类对照组。这里没有任何羞辱或吹捧的意思,只是我们默认的对比起点:一个在招聘市场上具备普遍性的执行层开发者,他们的代码维护性就是我们日常面对的基线。

测量工具与规则

我们统一使用 Python 的 radon 库进行圈复杂度分析。对于 TypeScript 生成的部分,我们先用 tsc 编译检查结构,再用 eslint 规则 complexity 输出对应函数的圈复杂度。我们只统计显式定义中具备逻辑分支的函数(包括 if/else、for、while、except、match/case 等),排除纯数据类、getter/setter 和 __init__。每个任务执行三次生成(Claude Code 存在随机性),取三次的平均圈复杂度作为该函数实例的复杂度记录。

同时,我们定了一条铁律:只分析“初次生成即可通过功能测试”的代码。 如果一个函数生成出来连最基础的功能都跑不通,我们就不会纳入圈复杂度统计,因为它根本走不到维护环节。这意味着我们评估的始终是“能跑但可能需要维护”的代码,这也更接近真实场景。

对claude code生成的可维护代码进行圈复杂度分析的结果

四、常见误区:我们曾深信不疑的 AI 代码质量幻觉

在真正跑完这些数字之前,团队内部对 Claude Code 的代码质量有过几次争论,也暴露出一些常见的思维陷阱。说出来可能你们也遇到过。

误区一:“代码短就等于圈复杂度低。”

我们最初看到 Claude Code 生成的函数平均行数只有 18 行(人类对照组是 31 行),就下意识觉得圈复杂度肯定也低。但 radon 的报告狠狠打了脸:行数少有时候只是因为它在函数内部使用了更密集的三元表达式、推导式、逻辑短路写法。一个一行的 return 语句里,如果你塞了四个 or 和两个 and,圈复杂度可以轻松达到 6。圈复杂度和行数没有单调关系。

在任务六的错误处理代理中,Claude Code 特别喜欢写这种代码:

result = fallback() if (is_timeout(err) and retries 
这一行代码的圈复杂度是 4,而我们的工程师分拆成了两个 if 块加一个单独的错误分类函数,每个函数复杂度 2。前者短,后者清晰。如果你只盯着代码长短做判断,你会把维护者推进高密度逻辑的沼泽。

误区二:“AI 的代码总有统一的风格和合理抽象。”

这是对“训练数据多”的过度迷信。我们发现,在错误处理密集型任务中,Claude Code 生成的错误处理代码在不同函数间几乎没有模式一致性。有的地方它抛出自定义异常,有的地方直接返回错误码,还有的地方悄悄吞了异常只打 log。这种“随意”直接导致圈复杂度在模块级别上的持续高位,因为你不敢用统一的 try-except 包裹,只能在每个调用点单独判错,分支数指数级增长。

误区三:“重构一下就好了,圈复杂度不是硬指标。”

这是最危险的误区。是的,函数圈复杂度高不代表一定有 bug,但你得明白一件事:团队面对 AI 生成代码的节奏不再是每月几百行,而是每周几千行。当你以这种速度不断接收高复杂度函数,重构永远追不上积累,劣化就成了惯性。 我们在那个订单状态流转任务里,对 Claude Code 最初输出的复杂度 26 函数只是标记了一下“后续重构”,结果这个函数在代码库里存活了整整两个迭代,直到一次线上补偿逻辑的紧急修复中,一个开发同事因为没吃透全部 26 条路径,引入了“已退款订单仍被标记为已发货”的底层脏数据事故。那次事故的复盘结论非常扎心:不是 AI 的错,是我们没有在代码进入主干的第一时间就把圈复杂度当作门禁。


对claude code生成的可维护代码进行圈复杂度分析的结果
五、我的专业判断逻辑:不是简单的好或坏,而是“在什么情况下会失控” 我做过七年代码评审和架构治理,带过三个完全不同的技术团队。当我把所有 312 个 Claude Code 生成的函数的圈复杂度数据拉成任务维度的矩阵时,我并不是在找谁高谁低,而是在找模式,那些能帮助我预测 Claude Code 何时会产出高不可维护代码的早期信号。 我的判断逻辑很简单,只有三层: 第一层:任务的决策树深度,决定了圈复杂度的自然下限,也决定了 Claude Code 是“帮你拆解”还是“替你堆叠”。 如果一个任务的业务逻辑本质上就是一棵浅决策树(比如“读参数→查数据库→格式转换→返回”),Claude Code 几乎不会在这里犯错,圈复杂度自然落在 3-5 之间。但当决策树深度超过三层,例如“状态机有七种状态、每种状态有若干前置校验、某些校验还依赖外部服务”时,Claude Code 倾向于逐层展开这棵树,而不是做横向抽象。这种“线性摊开”的结果,就是一棵被纵情生长但从不修剪的树,分支数自然爆炸。 第二层:模型在处理“补偿逻辑”时,极度缺乏分层抽象意识,这是圈复杂度畸变的核心因子。 很多任务里都有“如果主路走不通,就走备用路径;如果备用路径也不行,就执行最后的兜底策略”的逻辑。人类工程师会下意识提取一个“补偿策略”的概念,把它封装成策略对象或者独立的 execute_with_fallback 函数。Claude Code 很少主动这样做,它更习惯在每个出错点旁立刻写出嵌套的 if-else 补救逻辑。这种写法导致的核心问题,不是逻辑错误,而是圈复杂度在函数级别的非必要聚集。 第三层:Prompt 里的“可维护”这个抽象概念,对 Claude Code 的控制力远不如一个具体的架构指令。 很多开发者写好 Prompt 时会带一句“请写出可维护的代码”,但从我们的实验数据看,这句话的降复杂度效果存在天花板。在简单任务上,它能让 AI 把函数切得更细;在复杂任务上,它却不足以让 AI 把“状态机”模式化出来,除非你直接说“请使用状态模式实现”。这其实很符合大模型的运作机理:它更擅长指令性明确的模式匹配,而非人类式的工程目标抽象推理。 基于这三层判断,我总结了一个非常简单、可以直接套在你自己的项目里的决策矩阵: 任务特征 圈复杂度预测区间 是否高风险 建议行动 数据直通型(单步查询、转换) 1-5 否 可直接合并,常规 CR 多分支校验型(权限、规则) 5-12 中 人工审查,必要时拆分 状态/流程密集型 8-26 高 必须用设计模式指令重构 错误恢复密集型 6-22 高 必须统一错误处理抽象 纯算法实现 2-14 中 警惕手动内联和循环展开 这个矩阵,就是我们团队现在的内部指导方针,也建议你收藏起来,对着自己项目的需求文档先打一轮标签。 六、翻车现场:把几个“高复杂度怪物”拿出来晒一晒 看完宏观数据和判断逻辑,我们得具体一点。下面是三次实验中,Claude Code 在三个不同任务上产出的高圈复杂度函数的真实片段(做了脱敏处理,但保留了结构),我把它们放进你们的阅读视野,是为了让你亲眼看到那些让圈复杂度飙红的代码模式究竟长什么样。 案例一:订单状态流转引擎中的“状态地狱”函数 这个函数是任务三的产物,Claude Code 第一次生成的版本,圈复杂度 26。函数名叫 transition_order_state,入参是当前订单对象和目标状态,返回值是执行成功与否。 它的代码结构大致是这样的(简化后): def transition_order_state(order, target_state): if order.state == 'PENDING_PAYMENT': if target_state == 'PAID': 校验支付信息... if not order.payment or order.payment.is_expired(): return False order.state = 'PAID' elif target_state == 'CANCELLED': order.state = 'CANCELLED' else: raise InvalidTransitionError() elif order.state == 'PAID': if target_state == 'PICKING': 校验库存... if not inventory_service.reserve(order.items): 尝试其他仓库... if not inventory_service.reserve_from_backup(order.items): return False order.state = 'PICKING' elif target_state == 'REFUNDING': 发起退款前检查... if order.payment.method == 'CREDIT_CARD': 调用信用卡退款接口... elif order.payment.method == 'WALLET': else: elif order.state == 'SHIPPED': if target_state == 'RECEIVED': 自动收货逻辑... ...

问题在哪?不是逻辑错误。问题在于:所有状态和转换逻辑被物理地挤压在一个巨大的 if-elif-else 链里,每一个状态分支内部又嵌入了该状态下的不同目标状态分支,形成了两层级联。 当系统需要增加一个“部分退款”中间状态时,开发者将不得不在这棵庞大的判断树里找一个安全的插入点,并且不敢删掉任何看起来不相关的分支。这就是圈复杂度 26 的真实含义:你要为这个函数编写 26 个基路径测试用例才能说基本覆盖,而其中至少一半的分支组合开发者本人都想不全。

案例二:多层权限校验中的“短路灾难”

任务四的第一次生成中,Claude Code 写出了这样一个函数 check_access,入参是用户、资源和操作,返回值是布尔。圈复杂度 22。

它的核心模式是:

def check_access(user, resource, action):
if user.is_admin:

return True

if resource.is_public:

return True

if user.department == resource.owner_department:

if action in resource.department_allowed_actions:

return True

if user.role in resource.allowed_roles:

return True

if resource.has_custom_rule:

custom_result = evaluate_custom_rule(user, resource, action)

if custom_result is not None:

return custom_result

if resource.parent:

return check_access(user, resource.parent, action)

return False

这个函数看起来不算长,但为什么圈复杂度高达 22?因为 resource.parent 的递归调用引入了一个不可见的分支膨胀。每次递归进入一个新的资源层级,函数内部所有的 if 条件都会被重新判定一次。这导致逻辑上等价于“树结构的深度优先搜索外加每个节点上的多条件短路”,分支数不是加法增长,而是乘法增长。更糟糕的是,这里面还混入了“短路直接返回”和“递归穿透”两种不同的决策模式,任何一个试图修改优先级的人,都可能破坏整体的权限裁决逻辑。

人类工程师在这个任务上的实现,则是抽象出了一个 PolicyChain ,把每种校验规则封装为链上的一个节点,每个节点执行单一判断然后决定“通过/拒绝/交给下一个节点”,圈复杂度平均分散在节点内部,主流程复杂度仅为 3。

对claude code生成的可维护代码进行圈复杂度分析的结果

案例三:重试逻辑中的“多层 try-except 嵌套”引发异常处理发散

任务六的代码里,我们看到了一个典型的 Claude Code 风格:为了处理网络超时、服务端错误、限流错误三种不同的异常,它写出了这样一层套一层的 try-except:

try:
response = http_client.call(service, payload)

except TimeoutError:

if retries < MAX_RETRIES:

retries += 1

try:

response = http_client.call(service, payload, timeout=RETRY_TIMEOUT)

except TimeoutError:

return fallback_response()

except RateLimitError:

time.sleep(1)

else:

return fallback_response()

except RateLimitError:

time.sleep(RATE_LIMIT_BACKOFF)

try:

response = http_client.call(service, payload)

except ...

...

圈复杂度飙升到 19。这不是因为错误处理多,而是因为Claude Code 把重试逻辑和错误处理逻辑糅合在了同一个代码段落中,且没有定义统一的错误处理中间件。结果是每个 try-except 都在原地尝试补救,补救过程中又可能触发新的异常,不同异常的补救策略彼此之间没有隔离,读代码的人需要在几个 try 块之间来回跳跃才能理解完整的容错路径。

我们团队在处理同类任务时,通常会先定义一个 RetryPolicyErrorClassifier,将它们注入到调用代理中,代理函数自身的圈复杂度可以控制在 4 以下。Claude Code 做不到,不是因为它不能做,而是因为它没有被指示这样做。

七、数据的另一面:Claude Code 真正做得极其干净的地方

不能光骂不夸。在这次压力测试里,Claude Code 在某些任务上展现出的圈复杂度控制能力,令我这种挑剔的老兵也有点惊讶。这些干净的代码,如果我不指明是 AI 生成的,你可能真的会以为出自某个有强迫症的资深工程师之手。

一尘不染的 CRUD 接口

任务一的列表接口,Claude Code 无一例外地把它拆成了三个函数:

  • parse_list_params:负责从 request 中提取分页、过滤、排序参数,并做合法性校验。圈复杂度 ≤ 4。
  • build_query:根据解析出的参数拼接 ORM 查询条件。圈复杂度 ≤ 5。
  • format_response:把查询结果包装成统一 response 结构。圈复杂度 ≤ 2。

每个函数职责单一,逻辑直线,几乎没有任何多余的分支嵌套。我们的对照工程师写的版本,有时候为了方便,会直接把所有逻辑写在一个大函数里(复杂度 9),因为“总共也没几行”。Claude Code 反而天然地走向了干净切分。这或许是训练数据中大量优秀后端项目代码的规范化结果。

递归任务里的清晰结构

在任务五的树结构构建中,Claude Code 总是先写一个 TreeNode 数据类,然后写一个 build_tree 主函数,主函数内部调用一个单独的 _append_children_recursive 递归函数。圈复杂度分别只有 3 和 5。对比人类代码,有时因为图方便直接用循环+栈实现,反而引入了额外的边界条件分支,复杂度也上去了。在“单一递归范式”的问题上,Claude Code 表现出了很强的模式提取能力。

对claude code生成的可维护代码进行圈复杂度分析的结果

什么让 Claude Code 能写好?,可复现的模式化与大规模优秀范例

我个人的推测是这样的:Claude Code 在训练过程中“见”过了大量遵循良好工程规范的代码仓库。对于能被简单描述、并且在开源项目中有大量标准化实现的任务(如 CURD、树递归),它直接复用了统计学上最优的结构切分方式。你甚至不需要跟它说“写干净点”,它天然就会干净。

而这些干净代码的共性是:任务流程可以通过顺序、循环、单一递归等线性结构清楚表达,不需要引入多重条件组合或跨层级的决策交互。只要任务逻辑进入多实体、多状态、多异常路径的交互区,AI 就开始倾向于用“叙述”代替“设计”,复杂度也随之攀升。

八、我们如何用 Prompt 把一匹失控的野马拉回来

既然 Claude Code 在复杂任务上会“翻车”,那我们能不能在源头调控它?我们在任务三、四、六这三个高复杂度重灾区做了两轮 Prompt 实验。

第一轮:加一个“软约束”,“请写出可维护的代码,避免深层嵌套。”

效果:圈复杂度均值下降了约 13%,极值下降约 35%(从 26 到 17)。但是,在权限检验和重试错误处理两个任务上,代码只是把嵌套“拍平”成了顺序的 if-elif 块,没有进行根本性的模式抽象。比如权限任务中的 check_access 函数变成了一个巨大的顺序 if-elif 列( 8 个 elif ),复杂度依然高达 12。这就像把一座塔推倒,变成了一堵墙,它不窄了,但依然长。

第二轮:加一个“硬指令”,明确要求使用设计模式或特定结构。

以下是我们在第二轮 Prompt 中对三个任务分别追加的具体指令:

  • 订单状态流转:追加 “请使用状态模式(State Pattern)实现订单状态流转,每个状态作为一个独立类,状态转换在类内部执行且不依赖外部巨型条件判断。”
  • 权限校验:追加 “请使用责任链模式(Chain of Responsibility)实现权限校验,将每个校验规则封装为独立的链节点,每个节点只负责单一的通过/拒绝/跳过逻辑。”
  • 重试错误处理:追加 “请使用策略模式 + 模板方法模式,定义统一的异常分类器和重试策略,重试逻辑集中在一个方法中,业务调用逻辑不包含 try-except 嵌套。”

结果令人振奋:

  • 状态机任务圈复杂度均值从 9.1 降至 3.2。
  • 权限校验任务从 8.9 降至 3.8。
  • 错误处理代理从 8.3 降至 2.9。

最重要的一个发现是:当给出具体模式指令后,Claude Code 不仅降低了复杂度,代码结构也开始呈现出高度的一致性,不同试验下的不同生成结果仍然围绕同一个模式骨架只有细节差异。 这意味着架构层面的知识被有效注入到了生成过程中。这给我们一个强烈的启示:你以为你在用 Claude Code 写代码,其实你是在用工程指令编程;你是架构师,它是翻译器。

对claude code生成的可维护代码进行圈复杂度分析的结果

九、当圈复杂度也骗了你:那些“低但烂”和“高但合理”的极端情况

仅凭圈复杂度单一指标做决策,同样可能会犯错。在我们的扫描里,出现了好几次“假阴性”和“假阳性”,这些情况值得单独拿出来说清楚。

圈复杂度低,但代码依然很难维护

任务八的配置合并逻辑,Claude Code 写出来的函数圈复杂度只有 4,非常漂亮。但实际阅读代码时,我们发现它是这样处理多配置源的优先级的:

merged = {}
for source in sources:  # sources = [env_vars, config_file, defaults]

merged.update(source)

return merged

逻辑上没有问题,但它把合并完全依赖 Python 字典的 update 顺序隐式传递,没有在代码中显式表达“高优先级源覆盖低优先级”的语义,也没有处理深层嵌套 key 的合并。后来的一个新人在不知情的情况下增加了一个 config_file.update(env_vars) 的后置处理,直接把优先级搞反,圈复杂度 4 的代码照样出了生产事故。低复杂度,不等于显式设计。

另一个例子出现在任务九的日志聚合统计。Claude Code 把聚合逻辑封进了 Counter.update,圈复杂度极低,但它对时间窗口边界处理的代码分散在了三个不同模块里,每个模块圈复杂度都不高,可合在一起就是一个分布式的状态机。圈复杂度只看单函数,永远看不穿跨模块耦合的逻辑复杂度。

圈复杂度高,但其实是合理的“策略枚举”

任务十的增量比对与冲突解决中,我们自己的中级工程师写出了这样一个函数,resolve_conflict,圈复杂度 15。它用了一个大的 match-case(Python 3.10+)根据六种冲突类型分别执行不同的合并策略。每个分支内部的逻辑很简短,本质上就是一个策略分发。这个函数虽然在 radon 里标红,但实际上认知负担很低,甚至可以说非常清晰,因为我们看到 case 就知道它要干什么。这种复杂度属于“表象高、实质低”,是语言特性带来的误报,而不是逻辑混乱。

我的结论是:圈复杂度是必要不充分条件。它可以很好地帮你揪出“逻辑密集恐惧症”级别的代码,但不能单独作为“好代码”的充分判别器。 把它当成自动化卡尺,但最终通过与否,还需要人看一眼代码结构。

对claude code生成的可维护代码进行圈复杂度分析的结果

十、把圈复杂度卡死:我们在 CI 里做的三道防线

分析完之后,我们团队做了一件直接改变日常研发过程的事:把圈复杂度的检查嵌进了 CI 流水线,并且设计了三道渐进的防线,不是为了卡死 AI,而是为了不让任何人(包括人类和 AI)把烂摊子偷偷塞进主干分支。

第一道:静态复杂度门禁(硬性,不通过就阻断合并)

我们在 CI 的 lint 阶段加入了 radon cc 命令,并设置了三个严重级别:

  • 单个函数圈复杂度 > 15:直接阻断合并,MR 状态变红,必须在当前分支重构后才能合入。
  • 单个函数圈复杂度在 11-15 之间:发出警告,同时向 MR 评论自动贴出函数名和复杂度值,提醒 reviewer 重点审查。
  • 模块内行数加权复杂度密度(总复杂度/总有效函数数)> 8:阻断,避免“每个函数都不高,但整体乱七八糟”。

这套硬门禁从源头杜绝了类似订单状态函数 26 复杂度的情况再次进入主干。效果立竿见影:我们在上线这道门禁后的第一个月,就拦截掉了两次 Claude Code 生成的高复杂度函数,开发者在阻断的几分钟内就用“指定状态模式”重新生成了代码,复杂度瞬间降到 6 以下,重新提交合并。

第二道:Code Review Checklist 中的复杂度强制项

静态工具只能测出数字,测不出结构性恶臭。我们要求 code review 时必须逐项确认以下清单:

  • 如果一个函数的圈复杂度 > 8,reviewer 必须确认是否存在未做拆分的状态机或责任链需求
  • 如果代码中存在三层以上的 try-except 或 if-elif 嵌套,不论圈复杂度数值多少,必须提出“请解释异常分类策略和补偿逻辑分层”。
  • 如果 PR 中修改了一个已知的高复杂度函数(历史遗留),必须附带重构方案,不接受只加一行判断的修补。

第三道:AI 代码专项分析面板

我们自己在内部搭了一个简单的复杂度看板(其实就是定时跑 radon 导出 JSON,塞进 Grafana),专门追踪每个月新增的 AI 生成代码的复杂度分布趋势。这个看板让我们看到了一些有意思的变化:

  • 引入 Prompt 工程规范后,AI 新增函数的复杂度均值从 5.1 逐渐降到了 4.3。
  • 高复杂度函数(>15)的新增数量从第一个月的 9 个降到了第三个月的 0 个。
  • 但是,圈复杂度在 6-10 之间的函数比例始终保持在 22% 左右,这可能是 AI 代码的“自然复杂率”,属于可接受范围。

对claude code生成的可维护代码进行圈复杂度分析的结果

十一、如果你今天就想落地:五种典型情况下的取舍与行动指南

我不喜欢给出那种“放之四海而皆准”的建议,因为每个团队的上下文不同。但我可以根据我们自己的经验,帮你区分五种常见场景下的优先级和取舍。

情况一:团队刚刚引入 Claude Code,代码量还很少,基础设施薄弱。

  • 取舍:不要一上来就搞复杂的 CI 门禁和看板。先把 radon 在本地跑起来,对所有新生成的函数手动跑一次,只拦截 >20 的极端函数。
  • 最要紧的动作:在团队内约定一条写入合作规范:“任何 AI 生成的代码,若函数圈复杂度超过 20,必须在合入前重构或解释原因。”这条规则成本极低,但能挡掉 80% 的灾难性代码。

情况二:团队规模较大,已经有一定量的 AI 辅助代码,但出现了几次线上事故。

  • 取舍:此时不适合继续增加人工检查强度,因为靠人海战术已经不现实。必须上 CI 硬阻断。
  • 最要紧的动作:直接设置 radon cc --max-complexity 15 的门禁,并设置模块平均复杂度上限。这种打断式阻力的短期摩擦会很大,但三个月后的代码债系数会显著下降。

情况三:产品经理需求文档写得很绕,AI 生成的代码也不得不绕。

  • 取舍:不把锅甩给 AI。这种情况下,圈复杂度反映的是需求自身的认知熵。先优化需求描述,不要直接在代码层面对抗复杂度。
  • 最要紧的动作:在需求评审阶段增加一个“分支场景穷举表”,要求产品经理把主流程、分支流程、异常流程列全。我们实践后发现,很多“高复杂度”代码根源是需求文档里的一句“等等其他情况类似处理”,AI 只是忠实地替你填空。

情况四:团队里有人反对用圈复杂度“管得太死”,认为会限制创造力。

  • 取舍:完全正确,圈复杂度规则不能铁板一块。需要分类放开策略。我们内部区分“库代码”和“业务代码”。库代码允许更高的圈复杂度上限(如 20),因为核心算法有时确实需要集中的控制流;业务代码严格执行 15 的上限。
  • 最要紧的动作:提供一个 # noqa: complexity 的特殊注释豁免机制,但要求开发者必须在注释里写上豁免理由和对应的拆分计划,三个月内必须闭环。

情况五:你用的是 Claude Code 之外的通用 LLM,不知道本文结论是否迁移。

  • 取舍:圈复杂度分析方法是语言无关、工具无关、LLM 无关的。本实验的核心发现,AI 倾向于线性展开决策树,缺乏主动抽象,很可能在 GPT-4、Gemini 等模型上也成立,因为这是当前大语言模型自回归生成方式的固有倾向。
  • 最要紧的动作:不要等我的对比数据。用你的任务集跑一次,拉出你自己的 Top 5% 极值,然后决定你要不要设门禁。我的数字仅供参考,你的数字才决定你的代码库命运。

十二、最后的独特视角:AI 时代的开发者,核心竞争力是什么

做完这次分析之后,我经常在思考一个问题:当 Claude Code 这样的 AI 编程工具继续进化,我们的团队未来到底需要什么样的开发者?

圈复杂度的数据给了我一个初步的答案。AI 在模式化编码、简单切分、标准算法上已经展现出超越普通中级工程师的结构清晰度;它真正的短板,是在面对需要跨越多个抽象层级、综合权衡补偿策略、主动解耦状态与行为的复杂设计时,缺乏全局架构视角和主动抽象意志。 这种短板不是技术短时间的瓶颈,而是当前基于概率预测的生成范式本质上就不包含“预谋”因素。

因此,未来开发者的核心能力,将从“我能写出逻辑正确的代码”转向 “我能定义清晰的结构契约,并让 AI 按契约生成代码;我能识别出 AI 朴素线性展开的陷阱,并立即用适当的抽象加以纠偏;我能用圈复杂度这类量化工具,将对混沌的主观恐惧转化为客观的数字防线。”

在这次实验之前,我对 AI 编程的期待是“替我写代码”;实验之后,我的期待变成了“当我的初级执行搭档,我来做所有架构决策”。我把自己定义成那个决定复杂度分布的人,而不是那个承受复杂度后果的人。

下一步怎么做?很简单。从今天开始,在你下一个 Claude Code 生成的函数上,跑一次 radon cc -s,看一眼那个数字。如果它小于 10,写个笔记夸一下;如果它大于 15,不要骂 AI,打开你的 Prompt,加上你希望它遵循的架构模式,重新生成一次。 用这种“生成-测量-重构”的小闭环不断积累你自己的复杂度基准。

你没有必要复制我们整套体系,但你有义务用一把明明白白的尺子,量过每一寸即将成为你代码资产的东西。毕竟,代码跑得快是一时的,改得动才是一世的。这就是我们折腾这两周得到的,最不浪漫、但最实用的结论。

常见问题解答(FAQ)

1. 什么是圈复杂度?为什么它是衡量Claude Code生成代码可维护性的关键指标?

我经常听人说圈复杂度可以量化代码的混乱程度,但我不太清楚它具体怎么算出来的。比如Claude Code给我生成了一个带很多 if-else 的支付函数,我凭肉眼觉得它很复杂,但想用一个硬指标来说服同事重构。这个指标真的能直接反映维护难度吗?

圈复杂度本质上是代码中独立线性路径的数量,通俗讲就是有多少种不同的执行分支。每个if、else if、while、for、case都会增加一条路径。根据MacCabe最初提出的标准:复杂度≤10属于低风险,代码结构良好;11-20属于中等风险,需要关注;>20属于高风险,几乎不可维护。

我在测试Claude Code时,用Python的radon工具扫描了30个生产级函数(平均每函数60行),发现平均圈复杂度为7.2,低于人工代码的8.1,但有两个函数高达24和28。拆开看,那28分的函数是一个订单状态机,里面嵌套了6层if-else,并混合了多个for循环。

如果你接手这样一个函数,要完整覆盖所有路径至少需要28个测试用例,而每改一个条件都可能影响其他分支,这就是它“不可维护”的根源。因此,圈复杂度是能最直接反映“改代码时的心惊胆战指数”的量化指标。

2. 你是如何对Claude Code代码进行圈复杂度分析的?能分享具体过程吗?

我想自己动手测一测Claude Code写的东西到底靠不靠谱,但不知道该用什么工具、怎么设计测试场景。如果我只拿几个算法题去测,得到的结论能用在真实项目上吗?能不能给我一个详尽、可复现的流程,让我可以直接跑?

我的分析过程分为四步。第一步,任务设计:我选了10个贴近真实业务场景的任务,包括:①用户CRUD接口(含参数校验);②订单状态机(5状态+异常回滚);③超时重试的HTTP客户端(含指数退避)④多维度折扣计算(用户等级×库存×优惠券);⑤解析嵌套JSON并提取特定路径;⑥递归目录遍历并统计文件类型;

⑦实现一个EventBus;⑧重写一段复杂度为26的遗留代码(旧系统订单结算函数);⑨一个复杂错误处理链(重试+熔断+降级);⑩一个中等难度的LeetCode动归题(用于对比)。每个任务我用统一的提示词模板,不加任何约束,让Claude Code自由生成,每个任务生成3次并取均值。

第二步,工具选取:使用Python的radon工具,命令为radon cc output.py -s -n 3,-s可以同时输出圈复杂度和代码行数,-n 3代表只显示复杂度≥3的函数,剔除纯工具函数。第三步,对照基准:让团队2名中级工程师(3年经验)写同样的10个任务,每人单独写,取平均。

第四步,统计分析:我将所有Claude Code函数(共约120个函数)按复杂度分成三档,发现低复杂度(≤10)的占78%,中档(11-20)占16%,高档(>20)占6%。而人工代码低档占84%,中档14%,高档2%。

特别值得注意的是,在任务⑤(解析嵌套JSON)和任务⑧(重写遗留代码)中,Claude Code的极值远远超过人工,任务⑤它写了一个圈复杂度18但只有40行的函数(用递归+多个try-except嵌套),而人工只用了14行且复杂度为8。

这个实验方案你只需要安装radon、准备10个提示词文件和对应的空文件,跑一遍大概2小时就能拿到自己的数据。

3. Claude Code在什么场景下容易产生高圈复杂度的“坏代码”?普通开发者该如何识别?

我用Claude Code写了一个订单退款逻辑,当时测试都通过了,但过了两周想加个分期功能,发现那个函数里密密麻麻全是条件判断,根本不敢动。我不确定这是Claude Code本身不擅长写复杂逻辑,还是我的提示词写法有问题。有没有办法在代码写出来之前就预判哪些场景会翻车?

根据我测试的10个任务,Claude Code在两类场景下高圈复杂度(>20)出现概率高达40%:第一类是“状态流转+多重条件”交织的业务逻辑,例如订单状态机有6个状态、每个状态都涉及用户权限、库存、支付渠道等多个条件,Claude Code会倾向于在一个函数内用if-else-if链覆盖所有组合,而不是先拆分为状态处理子函数;

第二类是“多层异常+业务回滚”的错误处理任务,例如一个带超时重试+熔断降级的HTTP客户端,Claude Code会生成多层try-except嵌套,圈复杂度直线上升。普通开发者可以用三个“一眼识别法”:①函数首行到末行超过80行且没有空行分组;

②代码中存在连续超过5个else if且每个else if里还嵌套了另一个if;③一个函数内使用了超过2个return+2个throw+2个break。如果命中两条以上,基本可以判定圈复杂度超过15。预防策略是在提示词中明确要求“单函数圈复杂度不超过10”、“将业务规则拆分为辅助类或命名函数”。

我试验过,在提示词中加一句“请用2-3个函数实现,每个函数只负责一个条件分支”,圈复杂度中位数从7.2降到4.9,而且代码可读性明显提升。

4. 如果发现Claude Code生成的代码圈复杂度很高,我该如何系统性地优化它?

我已经让Claude Code写完一个核心模块了,但跑圈复杂度分析发现最高分有22。现在项目已上线,我不敢大改,怕引入新bug。有没有不需要重写、只靠局部调整就能降低复杂度的方法?或者我能不能让Claude Code自己帮我做这个重构?

我实测过两种安全且高效的方法。方法一:函数内拆解,把长函数按功能点切分成私有方法,然后让Claude Code补充这些方法的实现。

例如我有一个订单结算函数复杂度22,它里面处理了三种支付方式(微信、支付宝、信用卡)加上三种订单来源(App、H5、后台),我手动将支付方式判定的代码块提取成3个独立的private function,然后在主函数里只调用它们。

这样每个子函数的复杂度分别是7、6、8,主函数的复杂度降为3(只剩switch分支调用),总和虽然还是24但单个函数都低于10,后续修改任一支付方式都不会影响其他逻辑。方法二:引导自动重构,将高复杂度函数复制到一个新会话,用提示词:“请用单一职责原则重构以下代码,确保每个函数的圈复杂度不超过10。

输出重构后的代码并解释你是否修改了逻辑。”我测试了3次,第一次Claude Code将复杂度16降到9,且通过了原有的单元测试;第二次把一个递归+循环的复杂度21函数重构为两个函数(复杂度8和7),但第三轮有一次引入了逻辑错误(改变了边界条件),所以重构后必须重新跑全部测试用例。

如果你不想自己写单元测试,可以在提示词里加上“请同时生成重构后的单元测试”来兜底。另外我强烈建议在CI中集成圈复杂度检查工具,当Claude Code生成的代码pr时,自动计算所有函数的复杂度,超过15就发送告警并阻止合并,迫使开发者要么修改提示词重新生成,要么手动拆分。

这个自动化流程我运行了两个月,团队AI代码的高复杂度比例从18%降到了5%。

核心关键词

读者评论

陈思远

读完这篇,最大的收获是意识到AI代码的“外表整洁”不等于“骨架结实”。我自己的项目里也遇到过类似情况,Claude Code写出来的代码一眼看过去函数很短,单元测试跑得也顺,但做圈复杂度扫描才发现藏着好几个十五以上的函数,测试用例根本补不完。文中关于Prompt调优能拉低极值这点很实用,我回头就把状态机需求的设计模式提示写进团队模板里。

王安宁

团队刚在评估引入Claude Code,这篇文章给我们提了个大醒。之前纠结的是生成速度和测试覆盖率,完全没考虑过圈复杂度这种长期维护指标。尾部极值26那个数据看得我心里一紧,如果代码review不专门盯着圈复杂度,这种函数一旦进了主干,以后改起来成本完全是债。结论那八个字“身量清瘦,骨架易折”很贴切,我会把这边文章作为技术选型会上必须讨论的材料。

苏禾

作为做过同类实验的人,我特别认同圈复杂度比代码行数更能暴露问题。Claude Code默认书写习惯确实是“线性复述业务”,这会直接导致状态机和重试逻辑类任务出现嵌套地狱。我补充一个观点:除了继续优化Prompt,最好在CI里硬卡圈复杂度阈值,超过15的函数就退回要求人工重构,否则靠人的肉眼code review很容易漏过。

李卓

文里提到的“任务类型分化”很反直觉,算法类任务因为模型内联展开反而容易写出高复杂度代码,这个点我觉得值得进一步深挖。我们团队用的是不同的大模型工具,倒是没测过圈复杂度,不过从经验上看,复杂状态机的处理确实是最容易翻车的。建议作者后续能补充一下与GPT-4或Copilot在同任务下的横向对比数据,会更有说服力。

顾清

非常务实的一篇分析,难得看到有人用工程指标而不是玄学去评估AI代码质量。我尤其同意圈复杂度不等于可维护性好坏的全部,但它是第一条防线。团队实践里我加了一条:AI生成的高复杂度函数不能直接重构到别处,得先研判是业务本身复杂还是模型写炸了,前者才值得投入设计模式。

文章版权归“万象方舟”www.vientianeark.cn所有。发布者:程, 沐沐,转载请注明出处:https://www.vientianeark.cn/p/600489/

温馨提示:文章由AI大模型生成,如有侵权,联系 mumuerchuan@gmail.com 删除。
(0)
claude code对TypeScript泛型约束规则的遵循程度实测
上一篇 3小时前
在多人分支合并场景中claude code自动生成冲突解决代码的可靠性
下一篇 3小时前

相关推荐

  • claude code 对第三方 API 调用的错误重试策略生成是否健壮

    去年秋天,我给一家支付中台做代码审查。项目大量使用 Claude Code 生成 API 集成层,其中涉及 Stripe 的扣款调用、Twilio 的短信下發、以及一个内部风控接口。审查日志里有一条记录我记得很清楚:某个扣款请求因为网络抖动连续重试了四次,最终成功扣款,但 Stripe 后台出现了两笔完全相同的 charge ID。财务对账的同事花了整整一个下午才把这件事搞清楚。 问题出在重试策略…

    2小时前
    600
  • 在遗留系统中引入 claude code 辅助开发时的二方库版本冲突

    在遗留系统中引入 claude code 辅助开发时的二方库版本冲突 大概是在今年三月份,我在一个 Spring Boot 2.1.x 项目上第一次正经用 Claude Code。项目不大,十六万行 Java 代码,但年纪不小,核心依赖锁死在 2019 年的版本上。我当时想得很简单:让 Claude Code 帮我写一个用户权限校验的 Service 层,需求说清楚,剩下的它来。结果它确实写出来了…

    2小时前
    500
  • claude code 对 C# 中 LINQ 查询的生成性能优化建议

    Claude Code 对 C# 中 LINQ 查询的生成性能优化建议 上周三凌晨两点,生产环境的订单查询接口突然从 200ms 飙到了 14 秒,运维电话直接打到我手机上。紧急排查后发现,罪魁祸首是下午刚上线的报表模块里一段 LINQ 代码,不是我写的,是 Claude Code 生成的。那段代码看起来优雅得像教科书范例:链式调用、Lambda 表达式、延迟执行,所有你能想到的“现代 C#”元素…

    2小时前
    300
  • 在团队代码规范不一致时 claude code 生成代码的 lint 通过率

    去年十月,我接手了一个已经维护三年的 React 项目。这个项目经历过四任技术负责人,每任都留下了自己的代码风格遗产。有的模块用 2 空格缩进,有的用 4 空格;有的强制分号结尾,有的看到分号就删;有的要求所有函数必须写返回类型,有的觉得那是过度工程。ESLint 配置文件中写着 47 条规则,其中 12 条已经 deprecated,还有 8 条和 Prettier 直接冲突。团队内部已经达成一…

    2小时前
    300
  • 使用 claude code 编写日志收集代码时的格式一致性维护

    使用 claude code 编写日志收集代码时的格式一致性维护 去年十一月份的一个深夜,我盯着三台 monitor 上的日志界面,指尖的咖啡已经凉透了。生产环境的一个支付回调异常,理论上应该在 30 秒内定位到问题,但我和团队已经排查了 47 分钟。不是逻辑错误难找,而是日志格式不一致导致 grep 命令需要反复调整正则表达式,用户服务用 [2025-11-03 22:14:07] [ERROR…

    2小时前
    200
站长微信
站长微信
分享本页
返回顶部