这次我们不看模型也不看框架而是一个 LLM 辅助开发里很少被认真讨论的问题认知债务。简单说当你把 LLM 生成的代码直接复制粘贴进项目它虽然能跑但你的脑子里并没有真正拥有这一段代码。等到要改功能、调 bug、或者三天后重新审阅时你发现自己只能对着代码发愣然后再次打开对话框问 AI“帮我改一下”。这个循环维持得越久项目积累的理解缺口就越多最终变成比技术债更隐蔽的债务认知债务。这篇文章要展开的是一个听起来很“笨”的应对思路手动重新输入 LLM 生成的代码。不是逐字抄写而是先理解原始代码的结构和意图关掉参考窗口靠自己的记忆和推理把代码再实现一遍。这个过程会强制你逐行、逐个分支、逐个边界条件地把代码过一遍把“它为什么这么写”变成“我知道我写了什么”。这是一套可以直接放进日常开发流程的实践方法不依赖额外硬件也不要求专门工具只需要编辑器、终端和一点耐心。全文会按照“是什么 - 为什么 - 怎么做 - 怎么验证 - 怎么排查”的顺序展开。你可以把它当作一份操作手册挑一个小函数先试一次再逐步扩大到模块级代码。适合使用 ChatGPT、Claude Code、Copilot 等辅助编程工具生成代码并且关注项目长期维护成本的开发者阅读。1. 核心观点速览能力项说明方法属性一种代码实践与学习策略不是新编程语言或工具核心目标减少 LLM 生成代码带来的认知债务提升对代码的真实理解适用对象使用 LLM 辅助编程的开发者、技术团队、自学编程者硬件要求无特殊要求普通开发机即可依赖环境文本编辑器/IDE、Git、测试框架、LLM 代码生成工具可选启动成本低不需要安装额外服务是否支持 API不依赖 LLM API但 API 生成代码场景同样适用是否支持批量任务可以通过“先重写样板后批量微调”的方式落地核心收益代码可维护性提升、代码审查更有实质、减少对 LLM 的重复提问潜在成本初期耗时增加不适合所有一次性脚本场景这张表的核心是本方法不解决“代码能不能跑”而解决“你知不知道代码为什么这么跑”。如果你的项目需要长期维护并且代码质量高度依赖团队共同理解那它比单纯加速生成代码重要得多。2. 什么是认知债务为什么 LLM 代码特别容易制造认知债务认知债务不是指代码里的逻辑错误也不是指架构设计缺陷。它指的是开发者对代码运行机制的“理解缺口”在时间上的累计。一段代码如果从来没有人真正读懂过那么围绕它的所有后续修改、评审、排查都会变成低成本猜测。LLM 生成代码天然容易造成认知债务原因有五点。第一生成结果是黑盒。你只看到了最终代码没有看到模型在“思考”过程中选择变量名、分支结构、错误处理方式时的权衡。很多代码看似简单实际上隐含着对输入格式、异常场景和性能取舍的判断。你复制过来但判断过程没有复制过来。第二代码能运行会掩盖理解缺失。LLM 生成的代码通常经过了训练数据中的常见模式校准所以不是明显报错的情况下它往往能通过测试。这种“能跑”的状态会给你一种虚假的安全感让你忽略对内部实现的理解。第三现代开发流程里代码评审经常变成“看个大概”。人类评审者面对一段快速生成的代码下意识会倾向于接受它可以运行而不是追问每一个边界条件。当代码本身由 LLM 生成评审者更难提出深刻问题因为双方都没有完整的“代码为什么长这样”的上下文。第四短期效率优先。在迭代压力下开发者的目标从“理解这段代码”变成了“让任务完成”。这个目标切换是认知债务产生的最直接原因。第五LLM 生成代码的模式高度重复反而容易让开发者失去练习机会。比如以前你每天会手写几十行数组处理逻辑练出了对边界条件的敏感度现在你会直接让模型生成长此以往你对这些模式的敏感度会下降理解能力变得依赖模型。所以认知债务的本质是项目负债表上多出了很多“看似存在资产、实际没有所有权”的代码块。手动重新输入代码是抵消这种负债的最直接动作。3. 适用场景与使用边界3.1 适合的场景手动重新输入 LLM 生成代码不是所有代码都值得这么做。我的建议是优先处理以下四类。核心业务逻辑订单状态机、支付金额计算、权限校验、数据转换规则。这类代码一旦出错损失远大于重打时间。需要长期演进的功能你预计未来三个月内会多次修改这部分逻辑那么首轮花时间理解比每次改代码时重新读一遍更划算。多团队共享的基础模块工具函数、公共类库、API 封装。这些代码会被人多次调用别人也可能根据你的实现做扩展理解度直接影响协作质量。你自己不熟悉的领域比如你本来不熟悉并发库LLM 生成了asyncio.gather相关代码手动重输入可以帮助你在写的过程中理解协程生命周期。3.2 不适合的场景一次性部署脚本例如临时迁移数据、清洗一次日志。跑完就弃用不值得投入。脚手架代码项目初始化生成的目录结构、配置文件重写它们没有知识增量。你完全熟悉且结果确定的样板代码比如每个项目里都长一样的 HTTP 客户端配置直接复用更高效。代码量极大但逻辑重复的部分几千行的 CRUD 接口重点应该是理解数据模型和业务规则而不是逐行重打。3.3 合规与安全边界LLM 生成代码可能存在许可证、版权和被训练数据污染的问题。手动重输入时要记住三点。不要因为“重新输入了一遍”就认为代码原创。如果代码的结构、核心算法和逻辑顺序与 LLM 输出相同它可能仍属于派生作品需要遵守原始训练数据或工具服务条款。不要把敏感的、未公开的代码粘到第三方 LLM 对话中。部分工具会用输入内容改进服务这存在信息泄露风险。涉及人脸、声音、版权素材等内容的代码务必确认授权。例如生成图片识别人脸、语音克隆相关代码部署前要重新验证合规边界。4. 前置准备开发环境与工作流手动重输入不是靠记忆硬写而是需要一套支撑环境让你能安全地反复尝试。4.1 基础环境文本编辑器或 IDE推荐使用 VSCode、IntelliJ IDEA 或 Vim只要能清晰展示 diff 即可。Git 或类似版本控制工具这是必须项。你需要在重写前创建独立分支方便随时对比和回滚。测试框架根据语言选择 pytest、JUnit、jest 等。至少准备一个简单的断言脚本用来验证重写前后行为一致。LLM 生成代码的记录把你让模型生成代码的完整 prompt、模型返回结果保存到本地文件比如llm_output_dedup.py。这个文件只是参考不能直接作为最终代码。4.2 工作流设计建议采用“先复制到独立目录再重写再 diff”的流程。# 新建分支避免污染主分支 git checkout -b refactor/llm-code-understanding # 把 LLM 生成的代码放入参考目录 mkdir -p refs cp llm_output.py refs/llm_output.py你可以在refs/目录保留所有模型输出在工作目录中只放入你手动重写后的实现。这样既能随时对比也能避免误把模型输出提交到生产代码。5. 手动重输入的完整操作步骤以下步骤适用于一个函数、一个类或一个模块。第一次练习时建议只选一个 30 到 100 行的函数。5.1 第一步确认需求与输入输出先不要看代码。根据你给 LLM 的 prompt写出功能需求、输入、输出、边界条件。例如你的 prompt 是“写一个函数输入一个列表返回去重后的列表保持原顺序”。那么你应写下功能去重 输入list元素类型未知但应可哈希 输出list保持原顺序重复元素只保留第一个 边界空列表、全重复列表、元素包含 None 和 0、元素不可哈希这一步的目的是建立“自己理解的需求”而不是“LLM 答复的需求”。5.2 第二步通读 LLM 生成代码认真读一遍模型输出画出它的结构。不需要背代码只需确认它是用什么思路解决需求。# refs/llm_output.py def deduplicate(items): seen set() result [] for item in items: if item not in seen: seen.add(item) result.append(item) return result在这个例子里模型用set记录已见元素用result列表保存顺序。思路清晰但要注意如果items含不可哈希元素set()会报错。5.3 第三步关掉参考手动重新输入把refs/llm_output.py最小化或关闭新建一个dedup_impl.py基于自己写的需求实现。# working_dir/dedup_impl.py def deduplicate(items): seen set() result [] for item in items: if item in seen: continue seen.add(item) result.append(item) return result你会发现我写出来的版本和 LLM 版本并不完全一样用了continue而不是if not in。这种差异没关系重点是我写的时候已经想清楚了每个分支的逻辑知道自己为什么用seen也清楚continue可以少一层缩进。5.4 第四步对比差异并思考用 diff 对比手动重写版和模型版本。diff refs/llm_output.py working_dir/dedup_impl.py出现差异时逐个问自己差异是否改变行为如果行为不同哪个版本更正确如果行为相同为什么我选择了不同写法是习惯还是对性能有不同理解有些差异说明你还没理解模型的设计有些差异则说明你发现了更符合自己项目风格的写法。这个对比过程才是真正的“认知修正”。5.5 第五步补充边界条件处理手动重输入后可以继续改进。比如上面例子里items可能含不可哈希元素那么可以补充一个类型判断分支def deduplicate(items): seen set() result [] for item in items: try: if item in seen: continue seen.add(item) except TypeError: # 对不可哈希元素按 id 去重 if not any(item is existing for existing in result): result.append(item) else: result.append(item) return result补充边界条件不是必须步骤但在练习阶段非常有效。它能让你从“重打”走向“改进”这才是理解代码的高级形态。6. 功能测试与效果验证重写完之后只凭“感觉懂了”不够。要用测试来验证行为和原代码一致用解释来验证自己的理解深度。6.1 测试用例设计为被重写的函数设计覆盖正常、边界和异常场景的测试。以下是用 pytest 为去重函数写的示例。# test_dedup.py import pytest from dedup_impl import deduplicate def test_normal_order(): assert deduplicate([1, 2, 2, 3, 1]) [1, 2, 3] def test_empty_list(): assert deduplicate([]) [] def test_all_same(): assert deduplicate([7, 7, 7]) [7] def test_mixed_types(): assert deduplicate([1, 1, 1.0]) [1, 1] def test_boolean_and_int(): result deduplicate([True, 1, False, 0]) assert result [True, False] # 因为 True 1注意test_mixed_types里1和1.0在 set 中相等所以去重会保留第一个元素这是 Python 本身行为test_boolean_and_int中 True 与 1 相等也是同样的原因。这类测试会触发你思考类型系统而非只满足于代码能运行。6.2 运行测试并对比把测试同时指向 LLM 版本和你的重写版本确保两个版本行为一致。pytest test_dedup.py -v如果测试失败先不要急着改代码先确认是自己的实现有误还是测试条件假设不对。这个环节最能暴露“我以为我理解了其实没理解”的问题。6.3 自我解释测试跑完测试后不要停在这里。拿出一张纸或注释区用一句话解释每个关键逻辑。# 为什么用 seen: 用 set 做 O(1) 的已见判断 # 为什么 result 是 list: 需要保持原始顺序 # 为什么要先判断 item in seen: 为了过滤重复元素 # 为什么不直接 return list(set(items)): set 会改变顺序如果发现自己对某一行解释不出来说明这个位置还存在认知缺口。回到原始模型输出重新理解再重写一次。6.4 判断标准一次有效的重输入至少应满足测试用例全部通过你能向同事清楚解释每个分支的触发条件你能在不看参考代码的前提下提出至少一个改进点如果需求变化你能直接修改重写后的代码而不是重新问 LLM。7. 批量任务与接口调用场景的处理方式开发中经常遇到批量脚本、定时任务、API 调用等代码。这些代码格式高度重复但涉及到授权、限流、错误重试等细节同样需要理解。7.1 批量任务中的重输入策略对于批量任务不建议逐行重打每一个脚本。更好的策略是第一次编写批量任务框架时完整重写一次核心循环后续为不同任务写类似脚本时只重写差异部分即数据处理函数和参数配置把公共逻辑沉淀为模块避免反复生成同一段逻辑。例如一个批量处理文件的任务框架import os from pathlib import Path def process_files(input_dir: str, output_dir: str, handler): input_path Path(input_dir) output_path Path(output_dir) output_path.mkdir(parentsTrue, exist_okTrue) for file_path in input_path.iterdir(): if not file_path.is_file(): continue with open(file_path, r, encodingutf-8) as f: data f.read() result handler(data) out_file output_path / (file_path.stem .out) with open(out_file, w, encodingutf-8) as f: f.write(result)如果你能完全理解这个框架那么之后每次生成类似的批量处理代码你只需要从框架复制修改而不是把整段没读懂的逻辑塞进项目。7.2 API 调用代码的通用重写示例LLM 经常生成调用 OpenAI、Spark、通义等 API 的代码。这类代码核心是理解请求参数和返回结构。下面是一个通用示例你也可以用同样的结构替换到自己的 API 场景。import requests def call_llm_api(prompt: str, api_key: str, endpoint: str) - str: headers { Authorization: fBearer {api_key}, Content-Type: application/json, } payload { model: your-model-name, messages: [{role: user, content: prompt}], temperature: 0.7, } resp requests.post(endpoint, headersheaders, jsonpayload, timeout60) resp.raise_for_status() result resp.json() return result[choices][0][message][content]手动重输入这段代码时要关注的点包括为什么用timeout60防止请求长时间阻塞。为什么要resp.raise_for_status()非 2xx 状态码应该报错而不是继续解析。返回体结构result[choices][0][message][content]是否有兜底如果模型返回异常结构这里会抛 KeyError更稳妥的做法是用get()加默认值。只复制粘贴而不理解这些细节一旦接口限流、返回结构升级你会完全失去定位问题的能力。7.3 接口密钥与隐私如果 LLM 生成的代码包含 API key不要硬编码到项目里。统一使用环境变量export LLM_API_KEYsk-xxxximport os api_key os.getenv(LLM_API_KEY)这不仅是安全要求也是减少认知债务的一部分。硬编码的密钥会让代码环境不可复现每次运行都要猜变量来源。8. 常见问题与排查方法手动重输入 LLM 生成代码并不是一种完全顺滑的体验实践中会遇到不少障碍。下面是常见问题与解决思路。问题现象可能原因排查方式解决方案重写代码后测试不过手动重写时遗漏分支或变量名错误对比 diff检查条件分支和返回值对照模型输出逐行检查修复遗漏逻辑不知道从哪里开始重写需求理解不够先写自己的需求描述和输入输出示例回到 prompt 和模型的说明文本弄清功能意图重写太耗时代码量超出新手阶段承受范围拆分为更小的函数一次只重写一个函数不要整文件重写重写后代码风格和项目不一致可能习惯了模型风格忽视项目规范运行项目的 lint 和格式化重写后执行black、eslint --fix等工具统一风格重写丢失了模型代码里的边界处理阅读时没有标出边界条件阅读代码时边写注释边列出异常输入把边界条件测试加入测试用例用测试驱动重写测试用例覆盖不足重写后隐藏 bug只写了 happy path设计边界测试与异常测试使用输入作为函数参数补齐空、重复、特殊类型用例团队里其他人不理解为什么要手打一遍缺乏对认知债务的共识分享案例分析展示“复制粘贴后无法改代码”的场景先试点小范围重构验证后再推广出于版权或许可原因不能直接复制代码来自商业模型或闭源工具阅读服务条款与许可证手动重输入只能作为理解手段不视为原创来源必要时重写算法重新实现针对“重写太耗时”的问题我的建议是第一次练习选择 20 行左右的纯函数多练习几次后再挑战 100 行以上带状态管理的类。时间成本会随着熟练度显著下降。针对“重写后代码风格不一致”的问题务必在重写后就执行格式化。先统一风格再继续后续功能开发否则代码 review 时会因为风格问题掩盖更有价值的内容讨论。9. 最佳实践与使用建议经过多次实践我建议把这条方法论固化成一套可复制的工程流程。9.1 先从最高价值代码开始不要试图对你用 LLM 生成的所有代码都手动重输入。挑出以下三类代码优先处理会被多处调用的公共函数包含复杂状态变化的类你自己之前不熟悉但后续会频繁接触的实现。一个可执行的做法是每次 LLM 生成代码后先问自己“假如三个月后这段代码崩溃我能不依赖模型解决吗”如果答案是否那就值得重输入。9.2 在每一次重写中记录设计决策重写代码的过程最好同时写一个简短设计记录说明几个关键决策。比如在函数上方加上注释或在项目的docs/decisions/目录里记一段。- 选择 set 去重而不是 list.index: 需要 O(1) 判断 - 保持原顺序: 业务要求 - 处理不可哈希元素: 使用列表逐项比较空间换时间这些记录是“认知债务”的资产侧。代码本身不停变化但设计理由可以长期保留。9.3 结合测试驱动开发重写 LLM 生成代码时先写测试再写实现比先写实现再补测试更有效。因为你已经知道功能需求把测试作为“行为的契约”。重写过程中一旦测试偏离要么你在重新设计要么原模型输出有问题。两种情况都值得停下来思考。9.4 与代码审查结合手动重输入之后你的代码会带有你自己的思考痕迹。此时再交给同事 review讨论会更有实质。你可以说“这里我改成try/except是为了处理不可哈希元素原模型代码没有处理”而不是“这段代码是 AI 生成的我看了一下感觉没问题”。9.5 合规与使用边界再次强调几个原则不把敏感代码直接发送给外部 LLM 服务尤其是包含客户信息、内部架构、加密密钥的代码不把“手动重输入”当作绕过版权的借口原创性要看最终代码的实质而不是输入过程如果公司有明确 AI 辅助编程规范遵循公司要求在代码注释或 PR 描述中可以说明哪些代码来自 LLM 辅助、哪些经过了人工重写增加团队上下文透明度。10. 总结与下一步手动重新输入 LLM 生成的代码本质上是一种刻意练习。它不能帮你写得更快但能帮你隔断“不会看代码就继续问 AI”的负循环。尤其当你维护的是一个两三个季度前写的功能而当时那个功能完全由 LLM 生成时你一定会庆幸曾花时间重写过它。最值得先尝试的是拿一个最近的 LLM 生成函数按本文步骤走一遍写需求、读代码、关窗口重写、跑测试、写解释。你会发现原来很多自以为清楚的部分其实只停留在“知道它能跑”的程度。最容易踩的坑是闭卷重写时把边界条件写丢。解决方式也最简单先用测试固守边界再动手重写。后面可以继续扩展的方向包括把这套方法变成团队代码评审的固定环节、为常用 AI 生成代码维护一套“理解清单”、或者结合 git history 观察重写后的代码在后续需求变更中的维护成本。这些都能让 LLM 辅助编程从一个“快”的手段变得“又快又稳”。建议先收藏下次 LLM 给你生成代码时挑一小段试一下。